彻底掌握网络通信(二十)走进OkHttp3的世界(五)拦截器深入分析二

彻底掌握网络通信(一)Http协议基础知识
彻底掌握网络通信(二)Apache的HttpClient基础知识
彻底掌握网络通信(三)Android源码中HttpClient的在不同版本的使用
彻底掌握网络通信(四)Android源码中HttpClient的发送框架解析
彻底掌握网络通信(五)DefaultRequestDirector解析
彻底掌握网络通信(六)HttpRequestRetryHandler解析
彻底掌握网络通信(七)ConnectionReuseStrategy,ConnectionKeepAliveStrategy解析
彻底掌握网络通信(八)AsyncHttpClient源码解读
彻底掌握网络通信(九)AsyncHttpClient为什么无法用Fiddler来抓包
彻底掌握网络通信(十)AsyncHttpClient如何发送JSON解析JSON,以及一些其他用法
彻底掌握网络通信(十一)HttpURLConnection进行网络请求的知识准备
彻底掌握网络通信(十二)HttpURLConnection进行网络请求概览
彻底掌握网络通信(十三)HttpURLConnection进行网络请求深度分析
彻底掌握网络通信(十四)HttpURLConnection进行网络请求深度分析二:缓存
彻底掌握网络通信(十五)HttpURLConnection进行网络请求深度分析三:发送与接收详解
彻底掌握网络通信(十六)走进OkHttp3的世界(一)引言
彻底掌握网络通信(十七)走进OkHttp3的世界(二)请求/响应流程分析
彻底掌握网络通信(十八)走进OkHttp3的世界(三)详解Http请求的连接,发送和响应
彻底掌握网络通信(十九)走进OkHttp3的世界(四)拦截器深入分析

  前面我们详细的分析了RetryAndFollowUpInterceptor,BridgeInterceptor,CacheInterceptor三个拦截器,这篇开始我们将主要分析ConnectInterceptorCallServerInterceptor两个拦截器

详解ConnectInterceptor

  1. ConnectInterceptor主要作用
      建立客户端和服务端的连接,并将请求传递给CallServerInterceptor处理
  2. 核心代码
  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }

第4行,我们在RetryAndFollowUpInterceptor中,就创建了StreamAllocation实例,该实例直到ConnectInterceptor才被真正处理,这里就是获得之前创建的StreamAllocation实例;
第8行,通过newStream方法构建客户端到服务端的连接,并返回HttpCodec
第11行,将请求交由CallServerInterceptor处理

可见在ConnectInterceptor中,至关重要的元素是StreamAllocation,通过StreamAllocation我们可以建立客户端到服务端的连接


详解StreamAllocation

  1. StreamAllocation主要作用
    他主要维护客户端请求call,客户端到服务端的连接和客户端到服务端之间的流

  2. StreamAllocation主要成员
    2.1:Address address: 主要描述服务端Host和Port
    2.2:ConnectionPool connectionPool:连接池,该连接池主要维护客户端和服务端建立的连接(RealConnection)
    2.3:Route route:我理解的是地址映射,在客户端到服务端之间寻找一条合适的地址
    2.4:RealConnection connection:一个客户端到服务端的真实连接
    2.5:HttpCodec codec:主要对请求进行编码和对响应进行解码

  3. 核心代码分析

  public HttpCodec newStream(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }

第10行,通过findHealthyConnection方法返回客户端到服务端的一条真实的链接RealConnection resultConnection
第12行,通过RealConnection的newCodec方法得到HttpCodec resultCodec,这样ConnectInterceptor就可以利用此处的HttpCodec和RealConnection来和服务端进行交互了

那如何找到一条客户端到服务端的连接?,着重看下findHealthyConnection方法

  private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
      boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          pingIntervalMillis, connectionRetryEnabled);

      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }

      // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
      // isn't, take it out of the pool and start again.
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        noNewStreams();
        continue;
      }

      return candidate;
    }
  }

这个方法比较强势,开启一个循环直到找到一个健康的连接为止,那什么才是健康的连接,我们来看下代码
  第5行,通过findConnection方法返回RealConnection candidate
  第10行,如果这个连接是第一次建立,即

candidate.successCount == 0;//successCount表示这个连接被成功建立的次数

那么直接返回这个连接
  第17行,如果这个连接是不健康,则跳过本次循环,调用noNewStreams重新寻找一条连接;所以RealConnection的isHealthy方法则表示一个连接是否健康,这里就不看isHealthy方法了,直接总结下如果符合下述情况则表示为不健康的链接
  1)socket关闭
  2)socket处于半读状态,通过isInputShutdown方法获得; 客户端可以通过shutdownInput()方法关闭输入流,但程序还可以通过该Socket的输出流输出数据
  3)socket处于半写状态,通过isOutputShutdown方法获得; 客户端可以通过shutdownOuput()方法关闭输出流,程序还可以通过该Socket的输入流读取数据
  4)其他 … …

返回来看下findConnection方法

  /**
   * Returns a connection to host a new stream. This prefers the existing connection if it exists,
   * then the pool, finally building a new connection.
   */
  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();
      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) {
        // Attempt to get a connection from the pool.
        Internal.instance.get(connectionPool, address, this, null);
        if (connection != null) {
          foundPooledConnection = true;
          result = connection;
        } else {
          selectedRoute = route;
        }
      }
    }
    closeQuietly(toClose);

    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.
      return result;
    }

    // If we need a route selection, make one. This is a blocking operation.
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
      newRouteSelection = true;
      routeSelection = routeSelector.next();
    }

    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");

      if (newRouteSelection) {
        // Now that we have a set of IP addresses, make another attempt at getting a connection from
        // the pool. This could match due to connection coalescing.
        List<Route> routes = routeSelection.getAll();
        for (int i = 0, size = routes.size(); i < size; i++) {
          Route route = routes.get(i);
          Internal.instance.get(connectionPool, address, this, route);
          if (connection != null) {
            foundPooledConnection = true;
            result = connection;
            this.route = route;
            break;
          }
        }
      }

      if (!foundPooledConnection) {
        if (selectedRoute == null) {
          selectedRoute = routeSelection.next();
        }

        // Create a connection and assign it to this allocation immediately. This makes it possible
        // for an asynchronous cancel() to interrupt the handshake we're about to do.
        route = selectedRoute;
        refusedStreamCount = 0;
        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.
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    routeDatabase().connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      reportedAcquired = true;

      // Pool the connection.
      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;
  }

第19行,对releasedConnection进行赋值,因为本地有可能已经分配了一个可用的连接,那么我们就可以直接使用,而不需要经过后续处理
第50行,如果本地有可用的连接,则返回该连接
第69行~第77行,Internal.instance的内部实现在OkHttpClient中的静态成员中

static {
Internal.instance = new Internal() {}

这段代码的作用就是在现有的连接池中,根据Address,route来获得一个可以重用的RealConnection
第81行,如果连接池没有可重用的链接,则获得一个Route
第90行,新创建一个RealConnection实例
第96行,如果连接池有可重用的链接,则直接返回该连接
第101行,如果如果连接池没有可重用的链接,当创建了一个新的RealConnection实例之后,对其做tcp和tls握手。tcp握手就是我们常说的三次握手,他位于传输层;tls(安全传输层协议)也是位于传输层,用于在两个通信应用程序之间提供保密性和数据完整性
第102~104行行,通过调用conne方法完成底层socket连接
第111行,将创建的RealConnection放到连接池中

我们这里在简单分析下第102行的connect方法

  public void connect(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
      EventListener eventListener) {
    if (protocol != null) throw new IllegalStateException("already connected");

   /* 省略部分代码 */

    while (true) {
      try {
        if (route.requiresTunnel()) {
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
          if (rawSocket == null) {
            // We were unable to connect the tunnel but properly closed down our resources.
            break;
          }
        } else {
          connectSocket(connectTimeout, readTimeout, call, eventListener);
        }
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
        eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
        break;
      } 
	  
	/* 省略部分代码 */  
  }

在connect方法中,调用了一个比较重要的方法establishProtocol

 private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
      int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
    if (route.address().sslSocketFactory() == null) {
      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
        socket = rawSocket;
        protocol = Protocol.H2_PRIOR_KNOWLEDGE;
        startHttp2(pingIntervalMillis);
        return;
      }

      socket = rawSocket;
      protocol = Protocol.HTTP_1_1;
      return;
    }

    eventListener.secureConnectStart(call);
    connectTls(connectionSpecSelector);
    eventListener.secureConnectEnd(call, handshake);

    if (protocol == Protocol.HTTP_2) {
      startHttp2(pingIntervalMillis);
    }
  }

  private void startHttp2(int pingIntervalMillis) throws IOException {
    socket.setSoTimeout(0); // HTTP/2 connection timeouts are set per-stream.
    http2Connection = new Http2Connection.Builder(true)
        .socket(socket, route.address().url().host(), source, sink)
        .listener(this)
        .pingIntervalMillis(pingIntervalMillis)
        .build();
    http2Connection.start();
  }

第4行,如果Address地址里面有h2_prior_knowledge字段,则调用startHttp2方法,完成Http2Connection对象http2Connection的创建
第20行,如果协议是http2协议,则完成Http2Connection对象http2Connection的创建

由此可见在调用connect阶段,okhttp会根据协议的版本来决定是否创建Http2Connection对象http2Connection

至此,一个客户端到服务端的连接已经建立起来了;那拿到这个连接之后,还做了什么处理?之前我们提到在StreamAllocation类中有一个重要成员HttpCodec codec,其实这个对象的生成就是在拿到客户端到服务端连接之后,通过newCodec方法来获得的

  RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
      writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
  HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
  public HttpCodec newCodec(OkHttpClient client, Interceptor.Chain chain,
      StreamAllocation streamAllocation) throws SocketException {
    if (http2Connection != null) {
      return new Http2Codec(client, chain, streamAllocation, http2Connection);
    } else {
      socket.setSoTimeout(chain.readTimeoutMillis());
      source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
      sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
      return new Http1Codec(client, streamAllocation, source, sink);
    }
  }

第3行,如果http2Connection不为空,则构建Http2Codec来用于请求的编码和响应的解码
第9行,反之则构建Http1Codec来用于请求的编码和响应的解码

  Http2Codec:使用HTTP2标准请求进行编码和对响应进行解码
  Http1Codec:使用HTTP1标准请求进行编码和对响应进行解码

当完成HttpCodec 创建之后,我们就可以将这个httpCodec,RealConnection,streamAllocation一起交给下一个拦截器进行处理了。


额外补充一下Http1和Http2的一些知识点
HTTP1.1

  1. 通过Keep_Alive来保持客户端到服务端的连接,达到重用连接的目的;不至于每一次请求都经过握手的过程;http 1.0中默认是关闭的,需要在http头加入"Connection: Keep-Alive",才能启用Keep-Alive;http 1.1中默认启用Keep-Alive,如果加入"Connection: close ",才关闭
  2. 请求和响应都是顺序应答的,加入一个响应或请求阻塞了,那么后续的请求和响应将无法达到,这就是常说的对头阻塞
    HTTP2.0
  3. 多路复用技术,消除了 HTTP 1.x 中并行处理和发送请求及响应时对多个连接的依赖。可客户端和服务器可以把HTTP消息分解为互不依赖的帧,然后乱序发送,最后再在另一端把它们重新组合起来。从而避免不必要的延迟,提升效率,在请求量比较大的场景,客户端也可以轻松使用少量连接完成大量请求数据的传输
    简单描述HTTP2.0

详解CallServerInterceptor

  1. 主要功能
    将请求发送给客户端并接收服务端的响应将请求发送给客户端并接收服务端的响应

  2. 核心代码

  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    HttpCodec httpCodec = realChain.httpStream();
    StreamAllocation streamAllocation = realChain.streamAllocation();
    RealConnection connection = (RealConnection) realChain.connection();
    Request request = realChain.request();

    long sentRequestMillis = System.currentTimeMillis();

    realChain.eventListener().requestHeadersStart(realChain.call());
    httpCodec.writeRequestHeaders(request);
    realChain.eventListener().requestHeadersEnd(realChain.call(), request);

    Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
      // Continue" response before transmitting the request body. If we don't get that, return
      // what we did get (such as a 4xx response) without ever transmitting the request body.
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        realChain.eventListener().responseHeadersStart(realChain.call());
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        // Write the request body if the "Expect: 100-continue" expectation was met.
        realChain.eventListener().requestBodyStart(realChain.call());
        long contentLength = request.body().contentLength();
        CountingSink requestBodyOut =
            new CountingSink(httpCodec.createRequestBody(request, contentLength));
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);

        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
        realChain.eventListener()
            .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
      } else if (!connection.isMultiplexed()) {
        // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
        // from being reused. Otherwise we're still obligated to transmit the request body to
        // leave the connection in a consistent state.
        streamAllocation.noNewStreams();
      }
    }

    httpCodec.finishRequest();

    if (responseBuilder == null) {
      realChain.eventListener().responseHeadersStart(realChain.call());
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    int code = response.code();
    if (code == 100) {
      // server sent a 100-continue even though we did not request one.
      // try again to read the actual response
      responseBuilder = httpCodec.readResponseHeaders(false);

      response = responseBuilder
              .request(request)
              .handshake(streamAllocation.connection().handshake())
              .sentRequestAtMillis(sentRequestMillis)
              .receivedResponseAtMillis(System.currentTimeMillis())
              .build();

      code = response.code();
    }

    realChain.eventListener()
            .responseHeadersEnd(realChain.call(), response);

    if (forWebSocket && code == 101) {
      // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    }

    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      streamAllocation.noNewStreams();
    }

    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }

    return response;
  }

  第3行,拿到ConnectInterceptor.java中创建的HttpCodec httpCodec,这里我们假设请求是基于http1.1发送的,即httpCodec的实现者为Http1Codec为例
  第4行,拿到ConnectInterceptor.java传递过来的StreamAllocation streamAllocation
  第5行,拿到ConnectInterceptor.java传递过来的RealConnection connection
  第6行,拿到拦截链中的请求Request request
  第10行,回调EventListener接口,表明准备开始写入请求头信息
  第11行,调用Http1Codec的writeRequestHeaders方法将头信息写入BufferedSink中
  第12行,回调EventListener接口,表明写入头信息结束
  第15~43行,如果请求头中有Expect: 100-continue特殊字段,而进行的特殊处理;
针对post一个大数据的请求的时候,一般客户端的头信息会携带Expect:100-continue字段,这时服务器会读取请求的header并返回一个100 continue的响应,如果服务器可以提供这项服务的话(即Server愿意接受数据)。客户端再将http请求发送回去。然后服务器会读取请求的body并且在成功后返回200状态码。
  第45行,调用Http1Codec的finishRequest方法将头信息输入到流中
  第47行,一般情况下responseBuilder是为null的,因为大部分post请求携带的消息体不大
  第49行,构建Response.Builder responseBuilder(响应构建者),用于构建响应
  第52行,构建一个空的Response response,作为服务端的响应
  第60行,当服务端返回100的时候,即当客户端携带了Expect: 100-continue字段进行请求,服务端返回了100-continue
  第61行,读取服务端响应内容的头信息,返回responseBuilder
  第65行,当服务端返回100的时候,即当客户端携带了Expect: 100-continue字段进行请求时,构建响应Response response
  第75行,回调EventListener接口,表明读取响应头信息结束
  第84~87行,一般情况下,response的读取都是在这里获得的;通过调用Http1Codec的openResponseBody方法读取服务端的响应;

至此服务端的响应的内容已经完成获取

当获得响应之后,在通过上一个拦截器依次向上传递,经过

  1. CacheInterceptor拦截器的进行缓存处理
  2. BridgeInterceptor拦截器的对响应的最后查漏补缺字段
    最终交给上册完成整个流程

有的同学会问,为什么要看源码,我们会用不就好了吗

  其实不是的,会用只是你掌握了秘籍的招式,但是要想把招式打的更好,就需要多想多学,通过精读httpurlconnect,httpclient和okhttp3三个框架,不仅仅学习到了三个框架的精髓,也补充了很多网络协议方面的知识,同时也发现了当前项目中的网络框架的一些缺陷;

  框架里面还有很多方面没有涉及到,有需要的同学可以联系我,我再补充下

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值