OKHttp源码解析之网络请求

Posted wpnine

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OKHttp源码解析之网络请求相关的知识,希望对你有一定的参考价值。

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

在开始前我们先要确定以下几个问题,这将助于对源码的理解(如果已经清楚的大神可以跳过),问题如下:

1.http同tcp有什么关系?

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

 

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

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正式发布.

相对 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

 




以上是关于OKHttp源码解析之网络请求的主要内容,如果未能解决你的问题,请参考以下文章

OkHttp完全解析之整体调用流程

OkHttp解析大总结

Android网络请求框架—OKHttp 源码解析

Android网络请求框架—OKHttp 源码解析

Android网络请求框架—OKHttp 源码解析

带你一步步剖析Retrofit 源码解析:一款基于 OkHttp 实现的网络请求框架