OKHttp拦截器之ConnectInterceptor连接拦截

2019-12-12  本文已影响0人  24K纯帅豆

连接拦截器,它的作用主要是和服务器建立一个连接,只有建立连接了客户端才能与服务端交换数据,算是比较重要的一环了,我们来看一下这个拦截器的一些实现:

public final class ConnectInterceptor implements Interceptor {
  public final OkHttpClient 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");
    // 有两个实现类,分别是Http1Codec和Http2Codec,主要是用来进行Http请求和响应的编码/解码操作
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    //交给下一个拦截器执行真正的网络请求
    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }
}

看到这里,可能有人就会说了,逗我呢,这么重要的拦截器,才这么几行代码,没错,本身这个拦截器没啥东西,但是有一个很重要的类 StreamAllocation 负责管理连接、流和请求这三者;不知道还有没有印象,在之前的重试拦截器中我们创建了一个 StreamAllocation 对象,然后传到这个连接拦截器中,然后通过 StreamAllocation 来生成一个 HttpCodec,这个主要是用来进行Http请求和响应的编码/解码,看看这个方法:

public HttpCodec newStream(OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    try {
      // 获取可用的连接
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      // 构造一个HttpCodec,后面一个拦截器会用到
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
}

这个方法主要就是寻找一个可用的连接,然后通过找到的连接来生成一个HttpCodec,那是怎么样去找这个可用的连接的呢?

private RealConnection findHealthyConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks) throws IOException {
    // 这里会一直去找一个可用的连接,直到找到为止
    while (true) {
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          pingIntervalMillis, connectionRetryEnabled);
      // If this is a brand new connection, we can skip the extensive health checks.
      // 同步连接池,判断是否是新的连接,如果是就直接返回
      synchronized (connectionPool) {
        // 如果是新连接的话successCount一定为0
        if (candidate.successCount == 0) {
          return candidate;
        }
      }
      // 否则的话会判断是否是可用的连接
      // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
      // isn't, take it out of the pool and start again.
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        // 禁止新的流被创建
        noNewStreams();
        continue;
      }
      return candidate;
    }
}      

可以看到,这里开了一个死循环会通过 findConnection 方法一直找有没有连接,找到之后会判断是否是可用的连接,如果可用就直接返回,否则会继续寻找,那么问题来了,何为可用的连接呢?怎么判断?

public boolean isHealthy(boolean doExtensiveChecks) {
    // 检查socket的状态
    if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
      return false;
    }
    // 检查http2Connection是否关闭
    if (http2Connection != null) {
      return !http2Connection.isShutdown();
    }
    if (doExtensiveChecks) {
      // 非GET请求会判断Socket的inputStream相关的read操作阻塞的等待时间
      try {
        int readTimeout = socket.getSoTimeout();
        try {
          socket.setSoTimeout(1);
          // 流是否用完
          if (source.exhausted()) {
            return false; // Stream is exhausted; socket is closed.
          }
          return true;
        } finally {
          socket.setSoTimeout(readTimeout);
        }
      } catch (SocketTimeoutException ignored) {
        // Read timed out; socket is good.
      } catch (IOException e) {
        return false; // Couldn't read; socket is closed.
      }
    }
    return true;
}

首先会检查socket的状态,以及socket的input和output是否关闭了;然后看有没有使用http2,会判断http2连接是否关闭;最后如果是非GET请求的话会判断Socket的inputStream相关的read操作阻塞的等待时间;通过上述操作来判断一个连接是否可用。再回到前面,看看findConnection 的内部是怎么找连接的:

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    
    ...
    // 判断当前的连接是否为空,不为空则复用当前的
    if (this.connection != null) {
      // We had an already-allocated connection and it's good.
      result = this.connection;
      releasedConnection = null;
    }
    
    if (result == null) {
      // Attempt to get a connection from the pool.
      // 尝试从连接池中获取一个连接,get方法是从连接池中的队列中获取
      Internal.instance.get(connectionPool, address, this, null);
      if (connection != null) {
        foundPooledConnection = true;
        result = connection;
      } else {
        selectedRoute = route;
      }
    }
    ...
    // 否则尝试切换路由
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
      newRouteSelection = true;
      routeSelection = routeSelector.next();
    }
    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");
      if (newRouteSelection) {
        // Now that we have a set of IP addresses, make another attempt at getting a connection from
        // the pool. This could match due to connection coalescing.
        List<Route> routes = routeSelection.getAll();
        for (int i = 0, size = routes.size(); i < size; i++) {
          Route route = routes.get(i);
          // 每切换一次路由都尝试从连接池中寻找一个连接,有的话就返回,没有就继续切换路由
          Internal.instance.get(connectionPool, address, this, route);
          if (connection != null) {
            foundPooledConnection = true;
            result = connection;
            this.route = route;
            break;
          }
        }
      }
      // 最后还没找到的话,就会构造一个新的,
      if (!foundPooledConnection) {
        if (selectedRoute == null) {
          selectedRoute = routeSelection.next();
        }
        // Create a connection and assign it to this allocation immediately. This makes it possible
        // for an asynchronous cancel() to interrupt the handshake we're about to do.
        route = selectedRoute;
        refusedStreamCount = 0;
        result = new RealConnection(connectionPool, selectedRoute);
        // 引用计数
        acquire(result, false);
      }
    }
    // Do TCP + TLS handshakes. This is a blocking operation.
    // 创建的新连接需要进行connect操作,也就是TCP三次握手,阻塞操作,会判断是否超时
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    routeDatabase().connected(result.route());
    Socket socket = null;
    synchronized (connectionPool) {
      reportedAcquired = true;
      // Pool the connection.
      // 连接之后同步添加到连接池,复用
      Internal.instance.put(connectionPool, result);
      // If another multiplexed connection to the same address was created concurrently, then
      // release this connection and acquire that one.
      // Http2的多路复用判断
      if (result.isMultiplexed()) {
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
}

上述代码比较长,我们分成几个部分来看:

这里有两个比较重要的逻辑,第一就是路由的切换,简单说一下,相信大家都知道一个域名是对应多个IP地址的,而我们发起请求目标服务器的IP是唯一一个,所以需要找到我们实际请求的目标服务器IP地址,而路由选择器的作用就是帮我们找到匹配的目标服务器IP,这个过程中DNS会帮我们解析域名服务器的IP地址信息,然后存到路由选择器里,每次切换路由就会挨个取出来,然后从连接池中取出连接将当前的地址信息和路由中的进行比对,如果匹配的上就说明该连接是可以拿出来复用的,就不用重新构造新的连接;第二就是新创建的连接需要进行 connect 操作,我们来看一下是干嘛的:

// TCP TLS,区分Http1/Http2,Http2需要进行TLS数据加密传输,以及握手,证书认证等一系列操作
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");
    if (route.address().sslSocketFactory() == null) {
      // Http1明文判断
      if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication not enabled for client"));
      }
      String host = route.address().url().host();
      // 是否允许明文传输,在Android 9.0以上不允许明文传输,于是乎就有了网上的解决方案
      if (!Platform.get().isCleartextTrafficPermitted(host)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication to " + host + " not permitted by network security policy"));
      }
    }
    while (true) {
      // 判断是使用Socket连接还是隧道连接(需要三次握手等操作)
      try {
        // 如果是Https请求并且使用了Http代理,就是用隧道连接的方式
        if (route.requiresTunnel()) {
          // 隧道连接
          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);
        }
        // 建立协议
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
        eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
        break;
      } catch (IOException e) {
        closeQuietly(socket);
        closeQuietly(rawSocket);
        socket = null;
        rawSocket = null;
        source = null;
        sink = null;
        handshake = null;
        protocol = null;
        http2Connection = null;
        eventListener.connectFailed(call, route.socketAddress(), route.proxy(), null, e);
        if (routeException == null) {
          routeException = new RouteException(e);
        } else {
          routeException.addConnectException(e);
        }
        if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
          throw routeException;
        }
      }
    }
}

首先还是一些前置的判断,判断当前协议协议是否存在,如果存在的话那么说明已经连接过了,这时候会抛出异常;然后会进行Http的明文判断,是否允许明文;然后会根据路由来判断是使用Socket连接还是使用隧道连接,建立连接之后还会建立连接的协议,这个我们后面来看,先来看一下Socket连接(我们一般的请求都不会用到代理),因为隧道连接也是需要进行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);
    eventListener.connectStart(call, route.socketAddress(), proxy);
    // 读取数据时阻塞链路的超时时间
    rawSocket.setSoTimeout(readTimeout);
    try {
      // 打开Socket连接
      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 {
      // 使用Okio来进行数据的读写(数据交换)操作
      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);
      }
    }
}

首先会拿到代理和路由地址的信息,因为需要根据是否有代理来创建不同的Socket,然后设置一下超时时间,最后通过 connectSocket 方法(会调用Socket的connect方法)打开一个Socket连接,连接完成之后最重要的就是数据的交换了,这里都交给Okio的Source和Sink来完成。好,现在再回过头来看看建立连接之后是怎么建立协议的:

private void establishProtocol(ConnectionSpecSelector connectionSpecSelector, int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
    // Http1
    if (route.address().sslSocketFactory() == null) {
      protocol = Protocol.HTTP_1_1;
      socket = rawSocket;
      return;
    }
    eventListener.secureConnectStart(call);
    // 连接TLS
    connectTls(connectionSpecSelector);
    eventListener.secureConnectEnd(call, handshake);
    // Http2
    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)
          .pingIntervalMillis(pingIntervalMillis)
          .build();
      http2Connection.start();
    }
}

因为我们Http1和Http2的请求不太一样,所以建立的协议也不太一样,总的来说Http2请求会复杂一点,Http2请求会建立TLS协议,也就是我们通常说的加密传输,这个阶段会进行TLS握手以及证书的验证等等。

OKHttp其他拦截器详细的说明,可以看我Github上的项目

上一篇下一篇

猜你喜欢

热点阅读