前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OkHttp源码解析(十) OKHTTP中连接与请求及总结

OkHttp源码解析(十) OKHTTP中连接与请求及总结

作者头像
隔壁老李头
发布2018-08-30 11:45:14
9071
发布2018-08-30 11:45:14
举报
文章被收录于专栏:Android 研究Android 研究

终于到了讲解OkHttp中的连接与请求了,这部分内容主要是在ConnectInterceptor与CallServerInterceptor中,所以本片文章主要分2部分

  • 1、ConnectInterceptor
  • 2、CallServerInterceptor
  • 3、总结

一、ConnectInterceptor

顾名思义连接拦截器,这才是真行的开始向服务器发起器连接。 看下这个类的代码

/** Opens a connection to the target server and proceeds to the next interceptor. */
public final class ConnectInterceptor implements Interceptor {
  public final OkHttpClient client;

  public ConnectInterceptor(OkHttpClient client) {
    this.client = client;
  }

  @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, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

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

主要看下ConnectInterceptor()方法,里面代码已经很简单了,受限了通过streamAllocation的newStream方法获取一个流(HttpCodec 是个接口,根据协议的不同,由具体的子类的去实现),第二步就是获取对应的RealConnection,由于在上一篇文章已经详细解释了RealConnection和streamAllocation类了,这里就不详细说了是大概聊一下

StreamAllocation的newStream()内部其实是通过findHealthyConnection()方法获取一个RealConnection,而在findHealthyConnection()里面通过一个while(true)死循环不断去调用findConnection()方法去找RealConnection.而在findConnection()里面其实是真正的寻找RealConnection,而上面提到的findHealthyConnection()里面主要就是调用findConnection()然后去验证是否是"健康"的。在findConnection()里面主要是通过3重判断:1如果有已知连接且可用,则直接返回,2如果在连接池有对应address的连接,则返回,3切换路由再在连接池里面找下,如果有则返回,如果上述三个条件都没有满足,则直接new一个RealConnection。然后开始握手,握手结束后,把连接加入连接池,如果在连接池有重复连接,和合并连接。 至此findHealthyConnection()就分析完毕,给大家看下大缩减后的代码,如果大家想详细了解,请看上一篇文章。

  //StreamAllocation.java
  public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
    // 省略代码 
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
      HttpCodec resultCodec = resultConnection.newCodec(client, this);
    // 省略代码 
  }

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

      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        noNewStreams();
        continue;
      }
      return candidate;
    }
  }

  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      boolean connectionRetryEnabled) throws IOException {
      //省略部分代码
      //条件1如果有已知连接且可用,则直接返回
      RealConnection allocatedConnection = this.connection;
      if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
        return allocatedConnection;
      }

      //条件2 如果在连接池有对应address的连接,则返回
      Internal.instance.get(connectionPool, address, this, null);
      if (connection != null) {
        return connection;
      }

      selectedRoute = route;
    }

    // 条件3切换路由再在连接池里面找下,如果有则返回
    if (selectedRoute == null) {
      selectedRoute = routeSelector.next();
    }

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

      Internal.instance.get(connectionPool, address, this, selectedRoute);
      if (connection != null) return connection;

      
      route = selectedRoute;
      refusedStreamCount = 0;
      //以上条件都不满足则new一个
      result = new RealConnection(connectionPool, selectedRoute);
      acquire(result);
    }

    // 开始握手
    result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
    //计入数据库
    routeDatabase().connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      //加入连接池
      Internal.instance.put(connectionPool, result);

      // 如果是多路复用,则合并
      if (result.isMultiplexed()) {
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
    closeQuietly(socket);
    return result;
  }

这里再简单的说下RealConnection的connect()因为这个方法也很重要。不过大家要注意RealConnection的connect()是StreamAllocation调用的。在RealConnection的connect()的方法里面也是一个while(true)的循环,里面判断是隧道连接还是普通连接,如果是隧道连接就走connectTunnel(),如果是普通连接则走connectSocket(),最后建立协议。隧道连接这里就不介绍了,如果大家有兴趣就去上一篇文章去看。connectSocket()方噶里面就是通过okio获取source与sink。establishProtocol()方法建立连接咱们说下,里面判断是是HTTP/1.1还是HTTP/2.0。如果是HTTP/2.0则通过Builder来创建一个Http2Connection对象,并且调用Http2Connection对象的start()方法。所以判断一个RealConnection是否是HTTP/2.0其实很简单,判断RealConnection对象的http2Connection属性是否为null即可,因为只有HTTP/2的时候http2Connection才会被赋值。 代码如下:

public void connect(
      int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) {
   //省略部分代码
    while (true) {
      try {
        if (route.requiresTunnel()) {
          connectTunnel(connectTimeout, readTimeout, writeTimeout);
        } else {
          connectSocket(connectTimeout, readTimeout);
        }
        establishProtocol(connectionSpecSelector);
        break;
      } catch (IOException e) {
          //省略部分代码
      }
    }

    if (http2Connection != null) {
      synchronized (connectionPool) {
        allocationLimit = http2Connection.maxConcurrentStreams();
      }
    }
  }

  private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
    //省略部分代码    
    source = Okio.buffer(Okio.source(rawSocket));
    sink = Okio.buffer(Okio.sink(rawSocket));
  }

 private void establishProtocol(ConnectionSpecSelector connectionSpecSelector) throws IOException {
    if (route.address().sslSocketFactory() == null) {
      protocol = Protocol.HTTP_1_1;
      socket = rawSocket;
      return;
    }

    connectTls(connectionSpecSelector);

    if (protocol == Protocol.HTTP_2) {
      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)
          .build();
      http2Connection.start();
    }
  }

这时候我们在回来看下findConnection()方法里面的一行代码

acquire(result)

调用的是acquire()方法

  public void acquire(RealConnection connection) {
    assert (Thread.holdsLock(connectionPool));
    if (this.connection != null) throw new IllegalStateException();

    this.connection = connection;
    connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
  }

代码简单,这里解释一下,每一个RealConnection对象都有一个字段即allocations

public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();

connections中维护了一张在一个连接上的流的链表。该链表保存的是StreamAllocation的引用。如果connections字段为空,则说明该连接可以被回收,如果不为空,说明被引用,不能被回收。所以OkHttp使用了类似计数法与标记擦出法的混合使用。当连接空闲或者释放的时候,StreamAllcocation的数量就会渐渐变成0。从而被线程池检测并回收。

至此StreamAllocation的findHealthyConnection()就分析完毕了。那我们来看下

//StreamAllocation.java
HttpCodec resultCodec = resultConnection.newCodec(client, this);

其实是调用RealConnection的newCodec()方法

  public HttpCodec newCodec(
      OkHttpClient client, StreamAllocation streamAllocation) throws SocketException {
    if (http2Connection != null) {
      return new Http2Codec(client, streamAllocation, http2Connection);
    } else {
      socket.setSoTimeout(client.readTimeoutMillis());
      source.timeout().timeout(client.readTimeoutMillis(), MILLISECONDS);
      sink.timeout().timeout(client.writeTimeoutMillis(), MILLISECONDS);
      return new Http1Codec(client, streamAllocation, source, sink);
    }
  }

上面主要分了HTTP/2和HTTP/1.x,如果是HTTP/2(http2Connection不为null)则构建Http2Codec。如果是HTTP/1.x。则构建Http1Codec,大家注意一下在构建Http2Codec的时候并没有传入source和sink。这是为什么那?大家好好想一下,如果大家不知道为什么可以去看一下我前面的一篇介绍HTTP/2的文章,如果看了还不懂,请在下面留言,我给大家解释下。

至此关于ConnectInterceptor已经介绍完毕了。下面我们来介绍下CallServerInterceptor。最后一个Interceptor

二、CallServerInterceptor

上面我们已经成功连接到服务器了,那接下来要做什么那?相信你已经猜到了, 那就说发送数据了。

在OkHttp里面读取数据主要是通过以下四个步骤来实现的

  • 1 写入请求头
  • 2 写入请求体
  • 3 读取响应头
  • 4 读取响应体

OkHttp的流程是完全独立的。同样读写数据月是交给相关的类来处理,就是HttpCodec(解码器)来处理。

  @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();
    //写入请求头
    httpCodec.writeRequestHeaders(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();
        responseBuilder = httpCodec.readResponseHeaders(true);
      }
     //写入请求体
      if (responseBuilder == null) {
        // Write the request body if the "Expect: 100-continue" expectation was met.
        Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
      } 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) {
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();
    //读取响应体
    int code = response.code();
    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;
  }

自此整个流程已经结束了。

那我们再来看下OkHttp网络请求的整体接口图(特别声明:这个图不是我画的)

okhttp整体架构.png

关于OkHttp就的解析马上就要结束了,最后我们再来温习一下整体的流程图

流程.png

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017.06.04 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、ConnectInterceptor
  • 二、CallServerInterceptor
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档