美文网首页
OKHttp拦截器之ConnectInterceptor连接拦截

OKHttp拦截器之ConnectInterceptor连接拦截

作者: 24K纯帅豆 | 来源:发表于2019-12-12 09:42 被阅读0次

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

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

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

    • 1、首先有几个前置的判断,判读当前连接是否释放了,是否编码了,是否被用户取消了
    • 2、然后会尝试用当前连接(不为空)作为返回值返回
    • 3、否则的话会尝试从连接池中获取
    • 4、如果还没获取到就会尝试切换路由,再重复从连接池中获取
    • 5、最后如果还没获取到的话就会创建一个新的,然后进行连接操作,再将该连接放入连接池等待下一次被复用

    这里有两个比较重要的逻辑,第一就是路由的切换,简单说一下,相信大家都知道一个域名是对应多个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上的项目

    相关文章

      网友评论

          本文标题:OKHttp拦截器之ConnectInterceptor连接拦截

          本文链接:https://www.haomeiwen.com/subject/hqpzgctx.html