OKHttp源码解析之网络请求

OKHttp是square公司的开源项目,当前android开发中最经常使用的轻量级框架。本文中主要是解析OKHttp是如何创建网络链接,即HttpEngine,Connection中的部分代码。(注:解析的版本是2.5.0版本)java

在开始前咱们先要肯定如下几个问题,这将助于对源码的理解(若是已经清楚的大神能够跳过),问题以下:android

1.http同tcp有什么关系?git

http是应用层协议,依赖于传输层的tcp协议。通俗的讲http就是一个tcp链接,只不过它是以一种“短链接”的形式存在。github

 

2.https 的具体过程是怎么样的?web

https是由两部分组成: http + ssl/tls。 即在http的基础上一层处理加密信息的模块,使客户端与服务端的通信内容进行加密。如下是https身份认证的过程。缓存

 

3.HTTP 1.1,SPDY,HTTP 2.0有什么区别服务器

HTTP 1.1 是互联网中主要的协议,但随着科技的发展,原有的HTTP 1.1已不能知足要求。在2012年Google 推出协议 SPDY,解决 HTTP 1.1 中广为人知的性能问题。再到2015年,基于SPDY的HTTP 2.0正式发布.websocket

相对 HTTP 1.1,HTTP 2.0 主要有如下主要变化:网络

  1. 二进制分帧:请求和响应等,消息由一个或多个帧组成,并采用二进制格式传输数据,而非 HTTP 1.x 的文本格式,二进制协议解析起来更高效
  2. 多路复用:HTTP 1.1 中,若是想并发多个请求,必须使用多个 TCP 连接,但在 HTTP 2.0 中一个tcp链接能够被多个请求复用
  3. 头部压缩:将http请求中的header进行压缩传输,可以节省消息头占用的网络的流量

而HTTP 1.1相对SPDY,在总体上与HTTP 2.0上没有太大区别,但优点更加明显:并发

  • HTTP/2采用二进制格式传输数据,其在协议的解析和优化扩展上带来更多的优点和可能
  • HTTP/2对消息头采用HPACK进行压缩传输,可以节省消息头占用的网络的流量
  • Server Push的服务端可以更快地把资源推送给客户端。

 

4.一次完整的http请求须要通过哪些步骤?

  1. DNS 解析
  2. 与服务端创建tcp链接
  3. https会有ssl/tls认证
  4. 发送请求内容
  5. 等待服务器响应
  6. 接收服务器响应内容
  7. 关闭tcp链接

 

 

说了那么多废话,让咱们回到源码解析的正题上,在此先上一发图

 

该图是OKHttp如何执行一个网络请求的代码调用过程,注意红色下划线的部分,它的代码执行顺序是与前面提到网络请求须要通过的步骤里面说的是一致的,只要理解这个过程,OKHttp无论内部代码若是变化,它的调用顺序依然是要围绕这个变化。图没看懂不要紧,让咱们再结合源码进行分析。

 

首先看一下开始部分

/**
   * Figures out what the response source will be, and opens a socket to that
   * source if necessary. Prepares the request headers and gets ready to start
   * writing the request body if it exists.
   *
   * @throws RequestException if there was a problem with request setup. Unrecoverable.
   * @throws RouteException if the was a problem during connection via a specific route. Sometimes
   *     recoverable. See {@link #recover(RouteException)}.
   * @throws IOException if there was a problem while making a request. Sometimes recoverable. See
   *     {@link #recover(IOException)}.
   *
   */
  public void sendRequest() throws RequestException, RouteException, IOException {
    //...省略部分代码

    //初始化request
    Request request = networkRequest(userRequest);
InternalCache responseCache = Internal.instance.internalCache(client);
    Response cacheCandidate = responseCache != null
        ? responseCache.get(request)
        : null;

    //查询缓存记录
    long now = System.currentTimeMillis();
    cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
    networkRequest = cacheStrategy.networkRequest;
    cacheResponse = cacheStrategy.cacheResponse;

    if (responseCache != null) {
      responseCache.trackResponse(cacheStrategy);
    }

    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    // 判断是否有缓存
    if (networkRequest != null) {
      // Open a connection unless we inherited one from a redirect.
      if (connection == null) {
     //此处开始进行干正事 connect(); } transport
= Internal.instance.newTransport(connection, this); // //因为看源码中 callerWritesRequestBody 老是为false,因此如下if代码块不会执行 if (callerWritesRequestBody && permitsRequestBody() && requestBodyOut == null) { long contentLength = OkHeaders.contentLength(request); if (bufferRequestBody) { if (contentLength > Integer.MAX_VALUE) { throw new IllegalStateException("Use setFixedLengthStreamingMode() or " + "setChunkedStreamingMode() for requests larger than 2 GiB."); } if (contentLength != -1) { // Buffer a request body of a known length. transport.writeRequestHeaders(networkRequest); requestBodyOut = new RetryableSink((int) contentLength); } else { // Buffer a request body of an unknown length. Don't write request // headers until the entire body is ready; otherwise we can't set the // Content-Length header correctly. requestBodyOut = new RetryableSink(); } } else { transport.writeRequestHeaders(networkRequest); requestBodyOut = transport.createRequestBody(networkRequest, contentLength); } } } else { //...省略对http缓存的加载 } }

注:Okhttp 2.5的源码中,建立HttpEngine时老是对 callerWritesRequestBody变量置为false,而为true的状况同false在总体上的区别不大,这里就不分析另外部分代码

以上的代码是HttpEngine的sendRequest方法,主要对request的初始化,同时若是以前有缓存内容,则会优先加载缓存内容。因为本文的主要内容请求的执行部分,Cache等其它部分就忽略过了,再看看如下connect里面的代码

 

 /** Connect to the origin server either directly or via a proxy. */
  private void connect() throws RequestException, RouteException {
    if (connection != null) throw new IllegalStateException();
    
    if (routeSelector == null) {
      //该部分只是建立address对象,并经过routeSelector对象建立RouteSelector对象
      address = createAddress(client, networkRequest);
      try {
        routeSelector = RouteSelector.get(address, networkRequest, client);
      } catch (IOException e) {
        throw new RequestException(e);
      }
    }

    connection = createNextConnection();
    //获取到了Connection对象,就即将调用Connection.connectAndSetOwner里创建链接
    Internal.instance.connectAndSetOwner(client, connection, this, networkRequest);
    route = connection.getRoute();
  }


  private Connection createNextConnection() throws RouteException {
    ConnectionPool pool = client.getConnectionPool();

     
    // Always prefer pooled connections over new connections.
    for (Connection pooled; (pooled = pool.get(address)) != null; ) {
      if (networkRequest.method().equals("GET") || Internal.instance.isReadable(pooled)) {
       //针对address一致,Connection还alive的状况,则可复用Connection
        return pooled;
      }
      closeQuietly(pooled.getSocket());
    }

    try {
      //RouteSelector.next是负责dns解析,而且选取合适的proxy服务(此proxy服务也多是直接链接服务端的配置)
      Route route = routeSelector.next();
      return new Connection(pool, route);
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }    

  private static Address createAddress(OkHttpClient client, Request request) {
    SSLSocketFactory sslSocketFactory = null;
    HostnameVerifier hostnameVerifier = null;
    CertificatePinner certificatePinner = null;
    if (request.isHttps()) {
      sslSocketFactory = client.getSslSocketFactory();
      hostnameVerifier = client.getHostnameVerifier();
      certificatePinner = client.getCertificatePinner();
    }

    return new Address(request.httpUrl().host(), request.httpUrl().port(),
        client.getSocketFactory(), sslSocketFactory, hostnameVerifier, certificatePinner,
        client.getAuthenticator(), client.getProxy(), client.getProtocols(),
        client.getConnectionSpecs(), client.getProxySelector());
  }

          

 该段最主要的代码是 connect方法中的createNextConnection,前面的routeSelector和address建立只是在这里为它作铺垫。且createNextConnection的调用主要是获取到Connection对象,该对象能够理接为就是用于传递请求和接收内容的链路。Internal.instance.connectAndSetOwner()的解发实际就是调用Connection.connectAndSetOwner()

  /**
   * Connects this connection if it isn't already. This creates tunnels, shares
   * the connection with the connection pool, and configures timeouts.
   */
  void connectAndSetOwner(OkHttpClient client, Object owner, Request request)
      throws RouteException {
    //标记当前Connection对象是由谁建立
    setOwner(owner);
    
    //如果没有链接,则须要先进行链接
    if (!isConnected()) {
      List<ConnectionSpec> connectionSpecs = route.address.getConnectionSpecs();
     //connect 的内容请看下面方法
      connect(client.getConnectTimeout(), client.getReadTimeout(), client.getWriteTimeout(),
          request, connectionSpecs, client.getRetryOnConnectionFailure());
      if (isFramed()) {
       //若是是SPDY或者HTTP2.0,则分享此链接
        client.getConnectionPool().share(this);
      }
      client.routeDatabase().connected(getRoute());
    }

    setTimeouts(client.getReadTimeout(), client.getWriteTimeout());
  }


  
  void connect(int connectTimeout, int readTimeout, int writeTimeout, Request request,
      List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) throws RouteException {
    if (connected) throw new IllegalStateException("already connected");
    
   //创建链接须要用的配置整整齐齐的召唤出来而已
    RouteException routeException = null;
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
    Proxy proxy = route.getProxy();
    Address address = route.getAddress();

    //如果为https的状况则另外须要知足它的配置要求
    if (route.address.getSslSocketFactory() == null
        && !connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
      throw new RouteException(new UnknownServiceException(
          "CLEARTEXT communication not supported: " + connectionSpecs));
    }

    while (!connected) {
      try {
        //前面说过http 本质上就是一个tcp,那么tcp链接确定须要创建一个socket对象,如今终于有socket对象的建立,就说明要开始链接的操做不远了。
        socket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
            ? address.getSocketFactory().createSocket()
            : new Socket(proxy);
        //请看下面方法
        connectSocket(connectTimeout, readTimeout, writeTimeout, request,
            connectionSpecSelector);
        connected = true; // Success!
      } catch (IOException e) {
        //...忽略部分代码
      }
    }
  }    


/** 
这里就是真正发起tcp链接的地方!!!!!!
Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
  private void connectSocket(int connectTimeout, int readTimeout, int writeTimeout,
      Request request, ConnectionSpecSelector connectionSpecSelector) throws IOException {
    socket.setSoTimeout(readTimeout);
    //这里就是socket.connect调用的地方!!!若是address没问题,那么是可以链接上的
    Platform.get().connectSocket(socket, route.getSocketAddress(), connectTimeout);
     
    if (route.address.getSslSocketFactory() != null) {
     //connectTls是对https作证书认证,这里不作分析,有兴趣的同窗能够了解SSLSocket的使用
      connectTls(readTimeout, writeTimeout, request, connectionSpecSelector);
    }

    /***到这里来讲明已经和服务器链接上了,而这里会根据http协议的状况进行又建立不一样的Connection对象,
前面提到,spdy,http2.0在协议上与http1.1有所不一样,如头部压缩的特性,因此该Connection是对通信协议的封装**
*/ if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) { socket.setSoTimeout(0); // Framed connection timeouts are set per-stream. framedConnection = new FramedConnection.Builder(route.address.uriHost, true, socket) .protocol(protocol).build(); framedConnection.sendConnectionPreface(); } else { httpConnection = new HttpConnection(pool, this, socket); } }

 以上代码Connection对象里链接所执行的操做,这里主要强调就是connectSocket()方法中执行的几步重要的步骤:

Platform.get().connectSocket():本质就是Socket.connect()方法的调用,与服务器创建tcp链接,打开外面世界的大门。

connectTls():进行 ssl/tls 证书认证,但有兴趣的同窗能够去了解一下SSLSocket.java这个类

HttpConnection或FramedConnection的建立:该对象内部包含了http通信协议的封装/解析,帮助后面发/收内容            



上部分都是为了与服务端创建链接而已,以后发送request及接收response的内容就从HttpEngine方法里的readResponse()开始

/**
   * Flushes the remaining request header and body, parses the HTTP response
   * headers and starts reading the HTTP response body if it exists.
   */
  public void readResponse() throws IOException {
    //...忽略部分代码

    Response networkResponse;

    if (forWebSocket) {
      //...websocket不在讨论范围内,暂时忽略

    } else if (!callerWritesRequestBody) {
      //此处callerWritesRequestBody总为false,因此老是会执行到这里来
      networkResponse = new NetworkInterceptorChain(0, networkRequest).proceed(networkRequest);

    } else {
       //...忽略这里的代码
    }
    
   //到这里来讲明已经通信完毕了,此处是对reponse作一个漂亮的封装
    userResponse = networkResponse.newBuilder()
        .request(userRequest)
        .priorResponse(stripBody(priorResponse))
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (hasBody(userResponse)) {
     //对能够须要且能够缓存的内容进行缓存
      maybeCache();
     //对通过压缩的内容进行解压
      userResponse = unzip(cacheWritingResponse(storeRequest, userResponse));
    }
  }

 readResponse的底下部分是已经获取到服务端响应内容的时候了,只是对response作点加工 ,主要的读写操做是在NetworkInterceptorChain.proceed()中,因此继续往下看

class NetworkInterceptorChain implements Interceptor.Chain {
    private final int index;
    private final Request request;
    private int calls;

   //...忽略部分代码   

    @Override public Response proceed(Request request) throws IOException {
      //...忽略部分代码

      //写入http的request的头部
      transport.writeRequestHeaders(request);

      //Update the networkRequest with the possibly updated interceptor request.
      networkRequest = request;
      
      if (permitsRequestBody() && request.body() != null) {
        //针对相似post请求,有body的部分,须要将body也写入
        Sink requestBodyOut = transport.createRequestBody(request, request.body().contentLength());
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
      }
      
      //读取服务响应内容
      Response response = readNetworkResponse();

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

      return response;
    }
  }

  private Response readNetworkResponse() throws IOException {
    //以前的header与body的写入还并无发送给服务端,当调用finishRequest才正式发送
    transport.finishRequest();

    //等待服务器响应而且解析内容
    Response networkResponse = transport.readResponseHeaders()
        .request(networkRequest)
        .handshake(connection.getHandshake())
        .header(OkHeaders.SENT_MILLIS, Long.toString(sentRequestMillis))
        .header(OkHeaders.RECEIVED_MILLIS, Long.toString(System.currentTimeMillis()))
        .build();

    if (!forWebSocket) {
       //...略过部分代码
    }

    Internal.instance.setProtocol(connection, networkResponse.protocol());
    return networkResponse;
  }

此部分中 transport 是Transport接口的实现对象,内部是对前面提到的HttpConnection和FrameConnection的封装调用。而在HttpConnection或FrameConnection中的读写实现,除了协议的封装部分,剩下就是对数据流的读写操做(代码提到的Source或Sink对象就是对Stream的输入输出流封装,见okio)。读/写操做自己只要理解了协议内容自己就不是什么难点,至于我才疏学浅就不深刻到协议自己去了。

到了这里,整个请求的过程已经完整了,剩下的是对socket的链接释放就再也不多说了。总的来讲okHttp网络请求的调用部分代码不复杂,只要理解http请求所须要的步骤,跟着顺序走下来,就能很好的理解。

本人当前也是在学习阶段,若是有没有写的不对的地方,但愿各位大牛能帮忙指出,谢谢

参考资源:

一文读懂http/2 http://support.upyun.com/hc/kb/article/1048799/

Https的通信原理:https://juejin.im/entry/5a742ff5f265da4e82631c95

HTTP与TCP的区别和联系 https://blog.csdn.net/u013485792/article/details/52100533

相关文章
相关标签/搜索