OKHTTP--ConnectInterceptor拦截器

544 阅读8分钟
Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    // 自定义普通拦截器
    interceptors.addAll(client.interceptors());
    // 1.重试机制
    interceptors.add(retryAndFollowUpInterceptor);
    // 2.封装请求首部
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    // 3.处理缓存相关
    interceptors.add(new CacheInterceptor(client.internalCache()));
    // 4.TCP连接拦截器
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
    	// 5.自定义网络拦截器
        interceptors.addAll(client.networkInterceptors());
    }
    // 6.连接建立, 开始请求
    interceptors.add(new CallServerInterceptor(forWebSocket));
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());
    return chain.proceed(originalRequest);
}

最开始分析ConnectInterceptor时非常的吃力, 因为TCP/IP协议相关的知识掌握的太少了, 基本上每一步都需要查阅不少文章学习涉及到的网络协议知识.

这个拦截器涉及到的网络协议知识包括以下几个方面:

1、HTTP1.0、HTTP1.X、HTTP2.0之间的区别
2、DNS域名解析、负载均衡
3、代理服务器: DIRECT(直连)、HTTP、SOCKS

整个代码链路也是非常的长, 看完下来感觉挺懵逼了, 边看边画流程图, 整理总结每块代码做的事情, 防止以后再回顾时又要重头开始捋代码, 关于OKHTTP中很多都是有迹可循的, 基本都是按照书本上的知识再加上一些设计模式进行的编写.

大致是这么个流程:

(1) ROUTE流程图

Route为用于连接到服务器的具体路由, 由于存在代理或者DNS可能返回多个IP地址的情况, 所以同一个接口地址可能会对应多个ROUTE.

(2) 更细节的流程图

(3) ConnectionPool、RealConnection、StreamAllocation关系

(4) 该拦截器中所有对象描述

Route :

route用于连接到服务器的具体路由, 由于存在代理或者DNS可能返回多个IP地址的情况, 所以同一个接口地址可能会对应多个route.

RouteSelector :

Route选择器, 其中存储了到某一个代理服务器上所有可用的Route

RealConnection :

使用Socket建立HTTP/HTTPS连接, 对应一个TCP连接, 同一个RealConnection上可能装载多个HTTP请求.

ConnectionPool :

连接池, 缓存RealConnection, 与OkHttpClient是1:1关系, 一般情况全局只有一个OkHttpClient, 因此也只有一个全局的ConnectionPool, ConnectionPool具有缓存的能力, 因此肯定也提供了缓存清理的功能, 定期清理超时的RealConnection或者数量超过阈值的RealConnection

StreamAllocation :

结合源码可以看出, StreamAllocation贯穿了整个请求, 一个StreamAllocation对应一个Request, 同个StreamAllocation将ConnectionPool、RealConnection、Route、Request、RealCall、Interceptors进行关联. 可以理解为StreamAllocation就是OKHTTP网络请求层的桥梁, 将所有对象进行关联, 然后完成一次请求

1. RealInterceptorChain.streamAllocation

OKHTTP第一个拦截器中对StreamAllocation进行了初始化, 然后传入到RealInterceptorChain

RetryAndFollowUpInterceptor:public Response intercept(Chain chain) {
    ...
    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);						
}

这里需要关注两个对象: ConnectionPool、createAddress(request.url()).

结合代码可以了解到一个OkHttpClient对应一个ConnectionPool, 通常情况下或者是结合我们自己的项目, 全局实际上只有一个OkHttpClient实例. 所以也可以理解成全局只有一个ConnectionPool.

1.1 ConnectionPool
public class OkHttpClient.Builder {
    public Builder() {
        ...
        connectionPool = new ConnectionPool();
    }
}

public class ConnectionPool {
    public ConnectionPool() {
        this(5, 5, TimeUnit.MINUTES);
    }

    public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
        this.maxIdleConnections = maxIdleConnections;
        this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);
    }
}

截止到目前结合代码只知道: ConnectionPool里面记录了两个值: maxIdleConnections(最大空闲连接数)、keepAliveDurationNs(存活时间)

这里就涉及到HTTP1.0、HTTP1.X、HTTP2.0的区别:

1.2 HTTP1.0、HTTP1.X、HTTP2.0关于keep-alive

HTTP1.0 : 需要主动设置connection:keep-alive的连接方式, 才能开启长连接. Client每次请求都需要与服务器建立一个TCP连接, Server处理完成以后立即断开TCP连接, Server不跟踪每个客户端也不记录过去的请求(无状态)

HTTP1.1 : 默认支持keep-alive, 避免了连接建立和释放的开销, 但服务器必须按照客户端请求的先后顺序依次回送相应的结果, 以保证客户端能够区分出每次请求的响应内容. 通过Content-Length字段来判断当前请求的数据是否已经全部接收. 不允许同时存在两个并行的响应

HTTP2.0 : HTTP2.0实现了真正的并行传输, 它能够在一个TCP上进行任意数量的HTTP请求, 而这个强大的功能则是基于 **二进制分帧**的特性

同时还需要注意HTTP的keep-alive与TCP的KeepAlive的关系与区别

KeepAlive详解 讲述了HTTP的keep-alive与TCP的KeepAlive的区别

HTTP1.0、HTTP1.1、HTTP2.0区别 讲述了HTTP不同版本的区别, 尤其需要注意HTTP1.0与HTTP2.0连接复用的区别, 这个在接下来分析链路复用时奠定了基础

2.建立连接

StreamAllocation以及Address初始化完成之后, 开始进行连接操作

2.1 ConnectInterceptor.intercept
@Override 
public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();

    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    // 找到一个可复用的连接, 或者新建一个连接
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();
    return realChain.proceed(request, streamAllocation, httpCodec, connection);
}

streamAllocation.newStream查找可用连接或者新建连接, 调用链有点长, 流程图如下

2.1.1 ConnectionPool、RealConnection、StreamAllocation关系

一个连接池中最多持有5个空闲的连接, 每个连接上可以挂载多个Request请求

流程实在是太长了, 耐心挨个分析吧

2.2 StreamAllocation.findHealthyConnection

包括两个流程:

  • 1、获取可复用的连接、或者新建一个连接
  • 2、连接可用性校验
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
      boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
    	// 1.查找可用连接
        RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
            pingIntervalMillis, connectionRetryEnabled);
        synchronized (connectionPool) {
            if (candidate.successCount == 0) {
                return candidate;
            }
        }
        // 2.连接可用性校验
        if (!candidate.isHealthy(doExtensiveHealthChecks)) {
            noNewStreams();
            continue;
        }
        return candidate;
    }
}
2.3 StreamAllocation.findConnection

如果再次阅读这块代码, 切记直接就被带到代码细节里面无法自拔了. 一定要先看大体流程, 带着流程带着疑问再看这一大段代码

找到可复用的连接的流程比较复杂, 代码比较长, 但是分析完成之后发现也有迹可循, 主要涉及到网络协议的以下几方面的知识:

总结这一块的流程:

  • 1、
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    boolean foundPooledConnection = false;
    RealConnection result = null;
    Route selectedRoute = null;
    Connection releasedConnection;
    Socket toClose;
    synchronized (connectionPool) {
        if (released) throw new IllegalStateException("released");
        if (codec != null) throw new IllegalStateException("codec != null");
        if (canceled) throw new IOException("Canceled");

        // Attempt to use an already-allocated connection. We need to be careful here because our
        // already-allocated connection may have been restricted from creating new streams.
        releasedConnection = this.connection;
        toClose = releaseIfNoNewStreams();
        // 1.每一次Request都对应一个RealCall, 构造RealCall时会构造一个与该Request对应的StreamAllocation,
        //   每一个StreamAllocation都会尝试从一个全局的ConnectionPool中取RealConnection, 如果取到了就与自己绑定,
        //   如果没有取到, 则自己构建一个RealConnection, 然后丢到ConnectionPool中进行缓存, 以便其他Request可以复用
        if (this.connection != null) {
            // We had an already-allocated connection and it's good.
            // 如果有则直接使用
            result = this.connection;
            releasedConnection = null;
        }
        if (!reportedAcquired) {
            // If the connection was never reported acquired, don't report it as released!
            releasedConnection = null;
        }
        if (result == null) {
            // 2.连接池中找可复用的连接
            Internal.instance.get(connectionPool, address, this, null);
            if (connection != null) {
                foundPooledConnection = true;
                result = connection;
            } else {
                selectedRoute = route;
            }
        }
    }
    closeQuietly(toClose);
    // 3.这里是一个设计模式, 将关键节点通过Listener的方式对外暴露接口, 使用者可以通过hook这些关键节点获取
    //   自己需要的数据
    if (releasedConnection != null) {
        eventListener.connectionReleased(call, releasedConnection);
    }
    if (foundPooledConnection) {
        eventListener.connectionAcquired(call, result);
    }
    if (result != null) {
        // If we found an already-allocated or pooled connection, we're done.
        // 如果找到了可复用的RealConnection, 则直接返回
        return result;
    }
    boolean newRouteSelection = false;
    // 4.执行到这里说明未获取到可复用的连接. 关于Route相关放在<模块三>中进行分析
    // 这里的设计模型也是有迹可循的, 因为存在多个代理服务器或者负载均衡一个host可能对于多个IP, 所以Client
    // 到每一个Proxy之间都可能存在多个route, 每一个Proxy上的多个Route挂载到Selection上
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
        newRouteSelection = true;
        // 5.查找Client到下一个代理服务器上面的所有路由
        routeSelection = routeSelector.next();
    }
    synchronized (connectionPool) {
        if (canceled) throw new IOException("Canceled");
        if (newRouteSelection) {
            List<Route> routes = routeSelection.getAll();
            for (int i = 0, size = routes.size(); i < size; i++) {
                Route route = routes.get(i);
                // 6.遍历查找合适的RealConnection
                Internal.instance.get(connectionPool, address, this, route);
                if (connection != null) {
                    // 找到了标识位置为true
                    foundPooledConnection = true;
                    result = connection;
                    this.route = route;
                    // 如果找到了退出该循环, 未找到退出
                    break;
                }
            }
        }

        if (!foundPooledConnection) {
            // 说明未找到
            if (selectedRoute == null) {
                selectedRoute = routeSelection.next();
            }
            route = selectedRoute;
            refusedStreamCount = 0;
            // 7.新建连接, 到这里思考一个问题: 创建的这个连接在何时被添加到ConnectionPool中, 
            //   ConnectionPool中设置的最大RealConnection最大数为5, 如果超过了怎么处理新建的
            //   RealConnection
            result = new RealConnection(connectionPool, selectedRoute);
            acquire(result, false);
        }
    }
    // If we found a pooled connection on the 2nd time around, we're done.
    if (foundPooledConnection) {
        eventListener.connectionAcquired(call, result);
        return result;
    }
    // Do TCP + TLS handshakes. This is a blocking operation.
    // 8.建立连接, 连接建立流程对应<模块四>
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    routeDatabase().connected(result.route());
    Socket socket = null;
    synchronized (connectionPool) {
        reportedAcquired = true;
        // RealConnection添加到ConnectionPool中
        Internal.instance.put(connectionPool, result);
        // If another multiplexed connection to the same address was created concurrently, then
        // release this connection and acquire that one.
        if (result.isMultiplexed()) {
            socket = Internal.instance.deduplicate(connectionPool, address, this);
            result = connection;
        }
    }
    closeQuietly(socket);
    eventListener.connectionAcquired(call, result);
    return result;
}
2.4 ConnectionPool.get

从连接池中查找可复用的连接, 这里主要涉及到三个知识点:

  • 1、代理服务器
  • 2、DNS域名解析
  • 3、HTTP1.0、HTTP1.X、HTTP2.0之间keep-alive的区别
@Nullable 
RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
    	// 1.判断连接是否可复用
        if (connection.isEligible(address, route)) {
            // 2.找到可复用的连接, 使用它, 与StreamAllocation进行绑定
            streamAllocation.acquire(connection, true);
            return connection;
        }
    }
    return null;
}

如果不清楚代理服务器、DNS域名解析这些知识, 阅读这块的代码会非常吃力

2.5 找可用连接RealConnection.isEligible

Protocol枚举解析:

变量/方法解释
Protocol(String protocol)构造一个协议
Protocol get(String potocol)获取协议
HTTP_1_0("http/1.0")HTTP1.0协议
HTTP_1_1("http/1.1")HTTP1.1协议
HTTP_2("h2")HTTP2.0协议
public boolean isEligible(Address address, @Nullable Route route) {
    // 1.如果是HTTP1.0、HTTP1.1不支持, 如果是HTTP2.0支持连接复用:
    //   allocationLimit默认为1, 如果建立TCP连接时根据protocol获取当前为HTTP2.0, 
    //   则修改allocationLimit的值, 如果当前协议是HTTP1.0或者HTTP1.1, allocationLimit = 1,
    //   既不支持RealConnection复用.
    if (allocations.size() >= allocationLimit || noNewStreams) return false;
    // 2.如果Address匹配不成功, 该连接不可复用
    if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
    // 3.如果主机名相同, 则认为肯定可以复用该连接
    if (address.url().host().equals(this.route().address().url().host())) {
       return true; // This connection is a perfect match.
    }
    // 4.http2Connection表示当前协议版本类型, 如果是非HTTP2.0协议, 则认为不可复用
    if (http2Connection == null) return false;
    // 5.执行到这里, 说明address对应的host与当前RealConnection的host不相同, 但是host不相同并不代表连接不可复用,
    //   因为DNS解析, 请求域名最终会被解析为IP地址.
    if (route == null) return false;
    if (route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (!this.route.socketAddress().equals(route.socketAddress())) return false;
    if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
    if (!supportsUrl(address.url())) return false;
    try {
        address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
    } catch (SSLPeerUnverifiedException e) {
        return false;
    }
    return true; // The caller's address can be carried by this connection.
}
2.6 连接绑定StreamAllocation.acquire
public void acquire(RealConnection connection, boolean reportedAcquired) {
    assert (Thread.holdsLock(connectionPool));
    if (this.connection != null) throw new IllegalStateException();\
    // 1.连接绑定
    this.connection = connection;
    // 2.标识位置为true
    this.reportedAcquired = reportedAcquired;
    // 3.将StreamAllocation挂载到RealConnection上面
    connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}

三、路由Route

  • 1、Route为用于连接到服务器的具体路由, 由于存在代理或者DNS可能返回多个IP地址的情况, 所以同一个接口地址可能会对应多个ROUTE.
  • 2、RouteSelector: Route选择器, 其中存储了所有可用的route, 在准备连接时通过 RouteSelector.next 方法获取下一个route
3.1 RouteSelector

结合源码RouteSelector在StreamAllocation中进行初始化

public StreamAllocation(ConnectionPool connectionPool, Address address, Call call,
      EventListener eventListener, Object callStackTrace) {
    this.routeSelector = new RouteSelector(address, routeDatabase(), call, eventListener);
}

public RouteSelector(Address address, RouteDatabase routeDatabase, Call call,
      EventListener eventListener) {
    // 初始化代理服务器, 如果没有, 使用Proxy.NO_PROXY
    resetNextProxy(address.url(), address.proxy());
}