OkHttp 源码深入分析(二)

一、前言

上一篇文章OkHttp 源码深入分析(一)让我们了解了 OKHttp 从初始化到建立连接的一系列过程,同时也对 OkHttp 的几个关键类以及前几个拦截器,尤其是 ConnectInterceptor 拦截器有了更深的了解,接下来我还会接着上一篇继续分析后续的过程.

二、ConnectInterceptor 中的隐藏 socket 连接和 DNS

Socket 在哪连接的, DNS 解析好像也没看到, ExchangeCodec实例对象是怎么创建的?

这是上一篇文章的最后我抛出的几个问题,接下来我会一一解答,首先让我们回到 findConnection 方法

final class ExchangeFinder {

    private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {

   //省略若干代码...

   // 如果上面的条件都没有获取到连接,那么说明我们要创建一个新的连接
    //并通过 DNS 解析获取要连接的 route
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
      newRouteSelection = true;
      routeSelection = routeSelector.next();
    }

     // 做 TCP+TLS 握手,注意这是一个阻塞操作
    //到这一步执行完整个连接算是真正的建立了
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    //省略若干代码...
    return result;
  }
}
2.1、DNS 解析

代码做了精简,我们首先看看 DNS 解析的地方,在分析之前先说说几个类,我们在前面分析代码的时候总会看见这几个类如下

  • class RouterSelector
  • class Selection
  • class Route

这几个类其实做的事情并不复杂, RouterSelector 的主要作用是为了帮我门获取 DNS 解析的 IP 地址,内部有个 next 方法代码如下

 public Selection next() throws IOException {
   //省略若干代码...
   
    List<Route> routes = new ArrayList<>();
    while (hasNextProxy()) {//循环遍历所有的代理方式
    
      //DNS 解析
      Proxy proxy = nextProxy();
      //遍历解析的地址,添加到 Route 集合中
      for (int i = 0, size = inetSocketAddresses.size(); i < size; i++) {
        Route route = new Route(address, proxy, inetSocketAddresses.get(i));
        if (routeDatabase.shouldPostpone(route)) {
          postponedRoutes.add(route);
        } else {
          routes.add(route);
        }
      }

      if (!routes.isEmpty()) {
        break;
      }
    
    }
    //将解析的地址包装成 Selection 对象返回
    return new Selection(routes);
}

Selection 对象是 RouteSelector 的一个静态类,有一个关键方法 next 用来获取 route, 代码很简单就不贴了,继续进入到 nextProxy 方法后发现最终调用的是 resetNextInetSocketAddress 方法

/** Prepares the socket addresses to attempt for the current proxy or host. */
  private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
    // Clear the addresses. Necessary if getAllByName() below throws!
    inetSocketAddresses = new ArrayList<>();
        
     //调用系统 DNS 获取所有地址
      List<InetAddress> addresses = address.dns().lookup(socketHost);
     
    
      //将解析的地址放入地址列表中
      for (int i = 0, size = addresses.size(); i < size; i++) {
        InetAddress inetAddress = addresses.get(i);
        inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
      }
    }
  }

代码很多这里我们只看关键的代码,经过这么多层的兜兜转转终于是拿到了 IP 地址,这些 IP 地址最终会变成一个 Route 集合被包装成 Selection 对象供 Socket 使用,而这个 DNS 的实例对象其实是在创建 OkHttpClient 的时候通过 builder 构建出来的,我当初找到这里的时候就纳闷,这个 DNS 哪里冒出来的,没看到在那创建的啊,找了半天才发现原来隐藏在初始化的过程里。至于 Router 对象则是对我们请求信息的一个封装,包含我们请求地址,代理类型,和解析后的 IP 地址的封装类

2.2 Socket 连接和 ExchangeCodec 的创建

在上面代码中可以发现 Scoket 的连接是通过 result.connect 处理的,我们进去看看

public void connect(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
      EventListener eventListener) {
    if (protocol != null) throw new IllegalStateException("already connected");

    RouteException routeException = null;
    List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);

    //省略大量的 address 判断

    while (true) {
      try {
        //判断是否是 Http 代理的一个 Https 请求
        if (route.requiresTunnel()) {
         //连接 Https 隧道¡
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
          if (rawSocket == null) {
            // We were unable to connect the tunnel but properly closed down our resources.
            break;
          }
        } else {
        //直接连接 socket 
          connectSocket(connectTimeout, readTimeout, call, eventListener);
        }
        //处理 https 的 TLS 建立
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, 
        //省略若干代码
        break;
      } catch (IOException e) {
        //省略若干代码...
      }
    }

上面的代码基本都是 http/https 的具体协议处理过程,这里就不过多解读了(主要还是我本人网络底子也不好就不献丑了),而且不影响我们理解整体流程,不过还是要说明一下, connectTunnel 最终还是会调用 connectSocket 方法, connectSocket 内部会创建 Socket 的实例并创建 I/O 流进行关联,代码如下

/** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
  private void connectSocket(int connectTimeout, int readTimeout, Call call,
      EventListener eventListener) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();

    //创建 Socket 实例
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);
    
    rawSocket.setSoTimeout(readTimeout);
    //这里开始根据 route 提供的地址和目标服务器建立连接
    Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
  
  
    try {
      //创建连接所需要的 I/O 流,负责写入请求的 header、body 读取响应的 header、 body
      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);
      }
    }
  }

接下来我们重点看看 establishProtocol 方法

 private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
      int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
      // 如果SSL 协议不为空且是 HTTP2 连接 
    if (route.address().sslSocketFactory() == null) {
      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
        socket = rawSocket;
        //协议标记为 http2
        protocol = Protocol.H2_PRIOR_KNOWLEDGE;
        //创建一个 Http2Connection 对象并启动
        startHttp2(pingIntervalMillis);
        return;
      }
        
      socket = rawSocket;
      //协议标记为 http1
      protocol = Protocol.HTTP_1_1;
      return;
    }

    eventListener.secureConnectStart(call);
    //建立 TLS 链接
    connectTls(connectionSpecSelector);
    eventListener.secureConnectEnd(call, handshake);

    if (protocol == Protocol.HTTP_2) {
      startHttp2(pingIntervalMillis);
    }
  }

代码不多,也很好理解,establishProtocol 内部除了处理 Https 连接的问题,更重要的是会判断是否采用 Http2 连接并创建对应的 Http2Connection 对象,这也为后面 创建 ExchangeCodec 实例对象提供了判断依据。

当 establishProtocol 方法执行完后所有跟 scoket 连接的大致流程也就分析完了,所以让我们回过头在看看获取 ExchangeCodec 实例对象的地方

final class ExchangeFinder {
    public ExchangeCodec find(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
        //省略若干代码...
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      return resultConnection.newCodec(client, chain);
      //省略若干代码
  }
}

可以看到创建 ExChangeCodec 实例对象的方法是 RealConnection 对象的 newCodec 方法,进入此方法看看

ExchangeCodec newCodec(OkHttpClient client, Interceptor.Chain chain) throws SocketException {
    if (http2Connection != null) {
      return new Http2ExchangeCodec(client, this, chain, http2Connection);
    } else {
      socket.setSoTimeout(chain.readTimeoutMillis());
      source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
      sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
      return new Http1ExchangeCodec(client, this, source, sink);
    }
  }

经过前面的一大串分析,终于看到了 ExchangeCodec 实例对象的创建逻辑,上面的代码很简单就是判断 http2Connection 对象是否为 null ,以此为条件判断创建对应的 ExchangeCodec 对象

三、CallServerInterceptor

经过了那么大篇幅的分析,终于, 我们来到了最后一个拦截器,相比于 ConnectInterceptor 拦截器,CallServerInterceptor 拦截器就容易理解多了,因为和目标服务器的连接已经建立了,接下来就是通过 ConnectInterceptor 传过来的 Exchange 对象将我们的 header、body 通过流写入,再将返回的 response 读取,代码如下

@Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Exchange exchange = realChain.exchange();
    Request request = realChain.request();

    long sentRequestMillis = System.currentTimeMillis();
    
    //将请求头信息写入 stream 中
    exchange.writeRequestHeaders(request);

    boolean responseHeadersStarted = false;
    Response.Builder responseBuilder = null;
    //非 GET 请求且 body 不为空
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      // 忽略 100-continue 头域
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        exchange.flushRequest();//将请求头信息流推出
        responseHeadersStarted = true;
        exchange.responseHeadersStart();
        //读取响应信息
        responseBuilder = exchange.readResponseHeaders(true);
      }

     //如果服务端没有返回响应则创建一个 requestBody 写入
      if (responseBuilder == null) {
        if (request.body().isDuplex()) { // http2 协议
          // Prepare a duplex body so that the application can send a request body later.
          exchange.flushRequest();
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, true));
          request.body().writeTo(bufferedRequestBody);
        } else { //http1 协议
          //如果“Expect: 100-continue”的期望满足,则写入请求体。
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, false));
          request.body().writeTo(bufferedRequestBody);
          bufferedRequestBody.close();
        }
      }//省略部分代码
    } else {
      exchange.noRequestBody();
    }

    //省略部分代码...
    
    //读取响应信息
    if (responseBuilder == null) {
      responseBuilder = exchange.readResponseHeaders(false);
    }
    
    //构建获取的响应
    Response response = responseBuilder
        .request(request)
        .handshake(exchange.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    
    //省略部分代码
    return response;
  }

代码进行了精简,展示了主要逻辑,可以看到 CallServerInterceptor 的内部逻辑并不复杂,就是通过 Exchange 内部封装的 ExchangeCodec 对象,对 request 和 response 进行写入、读取而已,然后将从服务端获取的信息构建成 response 返回。

此时再看下面的代码是不是觉得更清晰了呢

OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
                .url("https://raw.github.com/square/okhttp/master/README.md")
                .build();
                
Response response = client.newCall(request).execute()

System.out.println("OKHTTP : " + response.body().string());
四、结语

经过漫长的分析,终于将所有的拦截器走了一个遍,此时回想整个过程,既有抓耳挠腮的烦躁,也有恍然大悟的愉悦,翻到前面又仔细看了一眼开篇的流程图,不禁会心一笑,希望读到这里的朋友也是如此。 完结撒花~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值