刨解OkHttp之訪問連接

因為OkHttp能講的東西太多了依啰,上一篇文章只是講到了他的設(shè)計架構(gòu)即責(zé)任鏈模式和異步多線程網(wǎng)絡(luò)訪問油狂,這對于OkHttp只是冰山一角站超,對于一個網(wǎng)絡(luò)請求框架斤程,最重要的就是網(wǎng)絡(luò)訪問了角寸,為此我們來說一下Okttp網(wǎng)絡(luò)訪問的一些細節(jié)。

這個訪問分為兩個部分忿墅,一個部分是與服務(wù)器形成連接扁藕,另一個部分是與服務(wù)器進行交互。與服務(wù)器連接的是ConnectInterceptor攔截器疚脐,而與服務(wù)器交互的是CallServerInterceptor攔截器亿柑。我們就來講一下這兩個攔截器吧。

ConnectInterceptor

先看源碼:

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();

    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

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

這里看起來很簡單棍弄,就是給首先StreamAllocation賦值望薄,然后調(diào)用newStream()方法,那streamAllocation是什么東西呢呼畸?它是整個連接的中心,協(xié)調(diào)著幾個重要的類痕支。后面都會說。
我們看一下newStream()方法:

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

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

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

通過看代碼我們有追溯findHealthyConnection()方法,源碼如下:

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 {
    Route selectedRoute;
    synchronized (connectionPool) {
      if (released) throw new IllegalStateException("released");
      if (codec != null) throw new IllegalStateException("codec != null");
      if (canceled) throw new IOException("Canceled");

      RealConnection allocatedConnection = this.connection;
      if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
        return allocatedConnection;
      }

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

      selectedRoute = route;
    }

    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) {
        route = selectedRoute;
        return connection;
      }

      route = selectedRoute;
      refusedStreamCount = 0;
      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;
}
  

在findHealthyConnection()里通過while(true)不斷調(diào)用findConnection()去獲取健康可用的RealConnection蛮原。RealConnection是與服務(wù)器連接的一個socket的連接卧须,有和這個就可以進行三次握手的tcp連接,所以上面的result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);這一行很關(guān)鍵儒陨,就是socket連接服務(wù)器的關(guān)鍵代碼花嘶,具體里面的代碼如下:

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 connectTunnel(int connectTimeout, int readTimeout, int writeTimeout)
      throws IOException {
    Request tunnelRequest = createTunnelRequest();
    HttpUrl url = tunnelRequest.url();
    int attemptedConnections = 0;
    int maxAttempts = 21;
    while (true) {
      if (++attemptedConnections > maxAttempts) {
        throw new ProtocolException("Too many tunnel connections attempted: " + maxAttempts);
      }

      connectSocket(connectTimeout, readTimeout);
      tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);

      if (tunnelRequest == null) break; 

      closeQuietly(rawSocket);
      rawSocket = null;
      sink = null;
      source = null;
    }
  }

private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();

    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);

    rawSocket.setSoTimeout(readTimeout);
    try {
      Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    } catch (ConnectException e) {
      ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
      ce.initCause(e);
      throw ce;
    }

    try {
      source = Okio.buffer(Okio.source(rawSocket));
      sink = Okio.buffer(Okio.sink(rawSocket));
    } catch (NullPointerException npe) {
      if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
        throw new IOException(npe);
      }
    }
}    

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();
    }
}

不管是直接調(diào)用connectSocket()還是connectTunnel(),最終都會調(diào)connectSocket()方法然后通過 Platform.get().connectSocket()(rawSocket, route.socketAddress(), connectTimeout);進行Socket連接。Platform.get().connectSocket()對應(yīng)代碼如下:

 public void connectSocket(Socket socket, InetSocketAddress address,
      int connectTimeout) throws IOException {
    socket.connect(address, connectTimeout);
  }

然后有調(diào)用了establishProtocol(),起始最主要的就是初始化Http2Connection對象.接著返回StreamAllocation蹦漠,下一步就是HttpCodec resultCodec = resultConnection.newCodec(client, this);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);
    }
}

其實沒什么椭员,也就是初始化Http1Codec或Http2Codec對象,這兩個類都集成接口類HttpCodec津辩,典型的面向接口編程拆撼,我們看一下接口類是什么:

public interface HttpCodec {
  int DISCARD_STREAM_TIMEOUT_MILLIS = 100;
  //寫入請求體
  Sink createRequestBody(Request request, long contentLength);
  //寫入請求頭    
  void writeRequestHeaders(Request request) throws IOException;
  //相當(dāng)于flush,把請求刷入底層socket
  void flushRequest() throws IOException;
  //相當(dāng)于flush容劳,把請求輸入底層socket并不在發(fā)出請求
  void finishRequest() throws IOException;
  //讀取響應(yīng)頭
  Response.Builder readResponseHeaders(boolean expectContinue) throws IOException;
  //讀取響應(yīng)體
  ResponseBody openResponseBody(Response response) throws IOException;
  //取消請求
  void cancel();
}

有方法知道HttpCodec是網(wǎng)絡(luò)讀寫的管理類喘沿,而Http1Codec和Http2Codec分別對應(yīng)Http1和Http2,在后面的CallServerInterceptor就主要用這個類進行操作。最后ConnectInterceptor的RealConnection connection = streamAllocation.connection();只是獲取了前面生成的RealConnection竭贩,然后通過前一篇介紹的責(zé)任鏈模式傳給CallServerInterceptor蚜印。

CallServerInterceptor

前面的ConnectInterceptor只是socket連接了服務(wù)器,而連接后怎么操作就是CallServerInterceptor了,接著我們看一下其實現(xiàn)方法:

@Override 
public Response intercept(Chain chain) throws IOException {
    //省略代碼。留量。蓬戚。
    
    //寫入請求頭
    httpCodec.writeRequestHeaders(request);

    Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        responseBuilder = httpCodec.readResponseHeaders(true);
      }
      //寫入請求體     
      if (responseBuilder == null) {
        Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
      } else if (!connection.isMultiplexed()) {
        streamAllocation.noNewStreams();
      }
    }

    httpCodec.finishRequest();
    //讀取響應(yīng)頭
    if (responseBuilder == null) {
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();
    //讀取響應(yīng)體
    int code = response.code();
    if (forWebSocket && code == 101) {
      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;
}

這里有一個東西需要講,就是Socket連接了服務(wù)器之后伺帘,是通過Okio向服務(wù)器發(fā)送請求的与帆。再次列取writeRequestHeaders()方法,源碼如下:

@Override
public void writeRequestHeaders(Request request) throws IOException {
    String requestLine = RequestLine.get(
        request, streamAllocation.connection().route().proxy().type());
    writeRequest(request.headers(), requestLine);
}

public void writeRequest(Headers headers, String requestLine) throws IOException {
    if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
    sink.writeUtf8(requestLine).writeUtf8("\r\n");
    for (int i = 0, size = headers.size(); i < size; i++) {
      sink.writeUtf8(headers.name(i))
          .writeUtf8(": ")
          .writeUtf8(headers.value(i))
          .writeUtf8("\r\n");
    }
    sink.writeUtf8("\r\n");
    state = STATE_OPEN_REQUEST_BODY;
}

而sink就是在ConnectInterceptor已經(jīng)初始化完成了仇奶,就在上面的connectSocket()連接服務(wù)器的方法里

  source = Okio.buffer(Okio.source(rawSocket));
  sink = Okio.buffer(Okio.sink(rawSocket));

其中sink是向服務(wù)器寫數(shù)據(jù),而source是獲取服務(wù)器數(shù)據(jù),rawSocket就是我們與服務(wù)器保持連接socker,okio我只會點到為止翰灾,不然又要開新的一篇講解了。createRequestBody()的原理和writeRequestHeaders()是一樣的稚茅。
接著就是接收數(shù)據(jù)了纸淮,讀取響應(yīng)頭readResponseHeaders()和上面原理一樣的,值得講的是讀取響應(yīng)體
httpCodec.openResponseBody(response)亚享,里面的源碼如下:

@Override 
public ResponseBody openResponseBody(Response response) throws IOException {
    Source source = getTransferStream(response);
    return new RealResponseBody(response.headers(), Okio.buffer(source));
}

private Source getTransferStream(Response response) throws IOException {
    if (!HttpHeaders.hasBody(response)) {
      return newFixedLengthSource(0);
    }

    if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
      return newChunkedSource(response.request().url());
    }

    long contentLength = HttpHeaders.contentLength(response);
    if (contentLength != -1) {
      return newFixedLengthSource(contentLength);
    }

    return newUnknownLengthSource();
}

其實沒有做任何的讀取操作咽块,只是ResponseBody封裝了headers()獲取響應(yīng)頭和Source對象,而Source就可以獲取響應(yīng)體欺税,只是沒有馬上獲取而是封裝好傳遞給上一個攔截器侈沪。最后在哪里獲取響應(yīng)體呢,
回到上一篇剛開始最簡單的訪問網(wǎng)絡(luò)demo

 Request request = new Request.Builder().url(url).build();
 Response response = client.newCall(request).execute();
 return response.body().string();

我們看一下body().toString()這個方法

public @Nullable ResponseBody body() {
    return body;
}

public final String string() throws IOException {
    BufferedSource source = source();
    try {
      Charset charset = Util.bomAwareCharset(source, charset());
      return source.readString(charset);
    } finally {
      Util.closeQuietly(source);
    }
}   

source就是Okio的讀對象對應(yīng)上面的source = Okio.buffer(Okio.source(rawSocket));,bomAwareCharset是獲取字符類型晚凿,默認utf-8,調(diào)用source.readString(charset);就可以獲取他的請求體了峭竣,也就是請求內(nèi)容字符串。closeQuietly()最終這個讀對象晃虫,整個訪問流程也基本結(jié)束了皆撩。

內(nèi)容有點多,自身感覺講解的也僅講了最主要的部分哲银,很多東西還可以擴展卻因為篇幅沒說扛吞,請見諒。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末荆责,一起剝皮案震驚了整個濱河市滥比,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌做院,老刑警劉巖盲泛,帶你破解...
    沈念sama閱讀 212,029評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異键耕,居然都是意外死亡寺滚,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,395評論 3 385
  • 文/潘曉璐 我一進店門屈雄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來村视,“玉大人,你說我怎么就攤上這事酒奶∫峡祝” “怎么了奶赔?”我有些...
    開封第一講書人閱讀 157,570評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長杠氢。 經(jīng)常有香客問我站刑,道長,這世上最難降的妖魔是什么鼻百? 我笑而不...
    開封第一講書人閱讀 56,535評論 1 284
  • 正文 為了忘掉前任笛钝,我火速辦了婚禮,結(jié)果婚禮上愕宋,老公的妹妹穿的比我還像新娘玻靡。我一直安慰自己,他們只是感情好中贝,可當(dāng)我...
    茶點故事閱讀 65,650評論 6 386
  • 文/花漫 我一把揭開白布囤捻。 她就那樣靜靜地躺著,像睡著了一般邻寿。 火紅的嫁衣襯著肌膚如雪蝎土。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,850評論 1 290
  • 那天绣否,我揣著相機與錄音誊涯,去河邊找鬼。 笑死蒜撮,一個胖子當(dāng)著我的面吹牛暴构,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播段磨,決...
    沈念sama閱讀 39,006評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼取逾,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了苹支?” 一聲冷哼從身側(cè)響起砾隅,我...
    開封第一講書人閱讀 37,747評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎债蜜,沒想到半個月后晴埂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,207評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡寻定,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,536評論 2 327
  • 正文 我和宋清朗相戀三年儒洛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片特姐。...
    茶點故事閱讀 38,683評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡晶丘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出唐含,到底是詐尸還是另有隱情浅浮,我是刑警寧澤,帶...
    沈念sama閱讀 34,342評論 4 330
  • 正文 年R本政府宣布捷枯,位于F島的核電站滚秩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏淮捆。R本人自食惡果不足惜郁油,卻給世界環(huán)境...
    茶點故事閱讀 39,964評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望攀痊。 院中可真熱鬧桐腌,春花似錦、人聲如沸苟径。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,772評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽棘街。三九已至蟆盐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間遭殉,已是汗流浹背石挂。 一陣腳步聲響...
    開封第一講書人閱讀 32,004評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留险污,地道東北人痹愚。 一個月前我還...
    沈念sama閱讀 46,401評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像蛔糯,于是被迫代替她去往敵國和親里伯。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,566評論 2 349