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

终于到了讲解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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容