源码分析三:OkHttp—ConnectInterceptor

1,931 阅读8分钟
ConnectInterceptor负责网络连接,其本质是复用连接池中socket连接。

开始撸源码:

intercept(拦截)

 @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    //从拦截器链里得到StreamAllocation对象
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    //创建连接等一系列的操作
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    //这里是获取前一步的connection.
    RealConnection connection = streamAllocation.connection();
   
    //把前面创建的连接,传递到下一个拦截器
    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }

步骤详细分析

基本步骤就上面展示了,代码看拦截这里很简单,我们理清楚一下几个类的调用关系,来分析一下连接是如何一步步建立的:

  1. StreamAllocation
  2. ConnectionPool
  3. RealConnection

StreamAllocation

首先,StreamAllocation的初始化在第一个拦截器RetryAndFollowUpInterceptor里面,

streamAllocation = new StreamAllocation(
        client.connectionPool(), createAddress(request.url()), callStackTrace);

传入了三个参数,一个连接池,一个地址类,一个调用堆栈跟踪相关的。

在StreamAllocation构造函数中,主要是把这个三个参数保存为内部变量,供后面使用,还有一个就是同时创建了一个线路选择器:

this.routeSelector = new RouteSelector(address, routeDatabase());

该构造器里面有两个重要的参数:
1.使用了Okhttp的连接池ConnectionPool
2.通过url创建了一个Address对象。

Okhttp连接池简单说明
本篇只是对连接池做最简单的说明,内部的实现原理暂时不细讲。在Okhttp内部的连接池实现类为ConnectionPool,该类持有一个ArrayDeque队列作为缓存池,该队列里的元素为RealConnection(通过这个名字应该不难猜出RealConnection是来干嘛的)。

该链接池在初始化OkhttpClient对象的时候由OkhttpClient的Builder类创建,并且ConnectionPool提供了put、get、evictAll等操作。但是Okhttp并没有直接对连接池进行获取,插入等操作;而是专门提供了一个叫Internal的抽象类来操作缓冲池:比如向缓冲池里面put一个RealConnection,从缓冲池get一个RealConnection对象,该类里面有一个public且为static的Internal类型的引用:

//抽象类
public abstract class Internal {
  public static Internal instance;
}1234

instance的初始化是在OkhttpClient的static语句块完成的:

static {
    Internal.instance = new Internal() {
       //省略部分代码
    };
  }

newStream()方法

主要做了如下工作:

1)从缓冲池ConnectionPool获取一个RealConnection对象,如果缓冲池里面没有就创建一个RealConnection对象并且放入缓冲池中,具体的说是放入ConnectionPool的ArrayDeque队列中。

2)获取RealConnection对象后并调用其connect(打开Socket链接)。

下面具体分析:

public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
    //1. 获取设置的连接超时时间,读写超时的时间,以及是否进行重连。 
    int connectTimeout = client.connectTimeoutMillis();
    int readTimeout = client.readTimeoutMillis();
    int writeTimeout = client.writeTimeoutMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
     // 2. 获取健康可用的连接
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
     
     //3. 通过resultConnection初始化,对请求以及结果 编解码的类(分http 1.1 和http 2.0)。
     // 这里主要是初始化,在后面一个拦截器才用到这相关的东西。
      HttpCodec resultCodec = resultConnection.newCodec(client, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }

获取连接

在上面的代码中最重要的,是注释 第二点,获取健康可用的连接findHealthyConnection,那我们继续深入:

findHealthyConnection()

 private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
      throws IOException {

   // 1. 加了个死循环,一直找可用的连接
    while (true) {
     
      // 2. 这里继续去挖掘,寻找连接
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          connectionRetryEnabled);

     // 3. 连接池同步获取,上面找到的连接是否是一个新的连接,如果是的话,就直接返回了,就是我们需要找
    // 的连接了
      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }

      //4.  如果不是一个新的连接,那么通过判断,是否一个可用的连接。
      // 里面是通过Socket的一些方法进行判断的,有兴趣的,可以继续研究一下
      // 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。

findConnection()

/**
   * Returns a connection to host a new stream. This prefers the existing connection if it exists,
   * then the pool, finally building a new connection.
   */
  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      boolean connectionRetryEnabled) throws IOException {

    Route selectedRoute;
   
   // 1. 同步线程池,来获取里面的连接
    synchronized (connectionPool) {
        
         // 2.  异常的处理。做些判断,是否已经释放,是否编解码类为空,是否用户已经取消         if (released) throw new IllegalStateException("released");
         if (codec != null) throw new IllegalStateException("codec != null");
         if (canceled) throw new IOException("Canceled");
         
         // 3. 尝试用一下现在的连接,判断一下,是否有可用的连接。(使用已存在的连接)
         // Attempt to use an already-allocated connection.
          RealConnection allocatedConnection = this.connection;
         if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
            return allocatedConnection;
          }
          
         // 4. 尝试在连接池中获取一个连接,get方法中会直接调用,注意最后一个参数为空
         // 里面是一个for循环,在连接池里面,寻找合格的连接
         // 而合格的连接会通过,StreamAllocation中的acquire方法,更新connection的值。(从缓存中获取)      
         // Attempt to get a connection from the pool.
         Internal.instance.get(connectionPool, address, this, null);
         if (connection != null) {
             return connection;
          }

          selectedRoute = route;
    }
    //5. 判断上面得到的线路,是否空,如果为空的,寻找一个可用的线路
    // 对于线路的选,可以深究一下这个RouteSeletor
    // 线路的选择,多ip的支持       if (selectedRoute == null) {
      selectedRoute = routeSelector.next();// 里面又个神奇的递归
    }

    RealConnection result;

    //6.  以上都不符合,创建一个连接
    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");
   
      // 7. 由于上面我们获取了一个线路,无论是新建的,或者已有的。
      // 我们通过这个线路,继续在连接池中寻找是否有可用的连接。
      // Now that we have an IP address, make another attempt at getting a connection from the pool.
      // This could match due to connection coalescing.
      Internal.instance.get(connectionPool, address, this, selectedRoute);
      if (connection != null) return connection;

      // 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;
      
      // 8. 如果前面这么寻找,都没在连接池中找打可用的连接,那么就新建一个
      result = new RealConnection(connectionPool, selectedRoute);
      acquire(result);
    }
    
    // 9. 这里就是就是连接的操作了,终于找到连接的正主了,这里会调用RealConnection的连接方法,进行连接操作。
    // 如果是普通的http请求,会使用Socket进行连接
    // 如果是https,会进行相应的握手,建立通道的操作。
    // 这里就不对里面的操作进行详细分析了,有兴趣可以在进去看看
    // Do TCP + TLS handshakes. This is a blocking operation.
    result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
    routeDatabase().connected(result.route());

    Socket socket = null;
    
   // 10. 最后就是同步加到 连接池里面了
    synchronized (connectionPool) {
      // Pool the connection.
      Internal.instance.put(connectionPool, result);

      // 最后加了一个多路复用的判断,这个是http2才有的
      // If another multiplexed connection to the same address was created concurrently, then
      // release this connection and acquire that one.
      if (result.isMultiplexed()) {
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
    closeQuietly(socket);

    return result;
  }

建立连接

connect

建立连接是比较重要的一步了。如果是Https还有证书步骤

public void connect(
    int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) {
  if (protocol != null) throw new IllegalStateException("already connected");
// 线路的选择
  RouteException routeException = null;
  List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
  ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);

  if (route.address().sslSocketFactory() == null) {
    if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
      throw new RouteException(new UnknownServiceException(
          "CLEARTEXT communication not enabled for client"));
    }
    String host = route.address().url().host();
    if (!Platform.get().isCleartextTrafficPermitted(host)) {
      throw new RouteException(new UnknownServiceException(
          "CLEARTEXT communication to " + host + " not permitted by network security policy"));
    }
  }

// 连接开始
  while (true) {
    try {
    // 如果要求通道模式,建立通道连接,通常不是这种
      if (route.requiresTunnel()) {
        connectTunnel(connectTimeout, readTimeout, writeTimeout);
      } else {
      // 一般都走这条逻辑了,实际上很简单就是socket的连接
        connectSocket(connectTimeout, readTimeout);
      }
      // https的建立
      establishProtocol(connectionSpecSelector);
      break;
    } catch (IOException e) {
      closeQuietly(socket);
      closeQuietly(rawSocket);
      socket = null;
      rawSocket = null;
      source = null;
      sink = null;
      handshake = null;
      protocol = null;
      http2Connection = null;

      if (routeException == null) {
        routeException = new RouteException(e);
      } else {
        routeException.addConnectException(e);
      }

      if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
        throw routeException;
      }
    }
  }

  if (http2Connection != null) {
    synchronized (connectionPool) {
      allocationLimit = http2Connection.maxConcurrentStreams();
    }
  }
}

我们进入connectSocket()函数看看,

connectSocket

private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
  Proxy proxy = route.proxy();
  Address address = route.address();

  // 根据代理类型处理socket,为true
  rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
      ? address.socketFactory().createSocket()
      : new Socket(proxy);

  rawSocket.setSoTimeout(readTimeout);
  try {
    // 连接socket,之所以这样写是因为支持不同的平台
    /**
    * 里面实际上是  socket.connect(address, connectTimeout);
    *
    */
    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;
  }
  // 得到输入/输出流
  source = Okio.buffer(Okio.source(rawSocket));
  sink = Okio.buffer(Okio.sink(rawSocket));
}

如果使用的是https协议,并且配置有真实将会协议升级。

Https协议的建立 connectTls

如果使用的是https协议socket连接完成后还有一步,就是Tls的处理

private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
  Address address = route.address();
  SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
  boolean success = false;
  SSLSocket sslSocket = null;
  try {
    // 在原来socket上加一层ssl
    // Create the wrapper over the connected socket.
    sslSocket = (SSLSocket) sslSocketFactory.createSocket(
        rawSocket, address.url().host(), address.url().port(), true /* autoClose */);

    // Configure the socket's ciphers, TLS versions, and extensions.
    ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
    if (connectionSpec.supportsTlsExtensions()) {
      Platform.get().configureTlsExtensions(
          sslSocket, address.url().host(), address.protocols());
    }

    // Force handshake. This can throw!
    sslSocket.startHandshake();
    Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());

    // Verify that the socket's certificates are acceptable for the target host.
    if (!address.hostnameVerifier().verify(address.url().host(), sslSocket.getSession())) {
      X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
      throw new SSLPeerUnverifiedException("Hostname " + address.url().host() + " not verified:"
          + "\n    certificate: " + CertificatePinner.pin(cert)
          + "\n    DN: " + cert.getSubjectDN().getName()
          + "\n    subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
    }

    // Check that the certificate pinner is satisfied by the certificates presented.
    address.certificatePinner().check(address.url().host(),
        unverifiedHandshake.peerCertificates());

    // Success! Save the handshake and the ALPN protocol.
    String maybeProtocol = connectionSpec.supportsTlsExtensions()
        ? Platform.get().getSelectedProtocol(sslSocket)
        : null;
    socket = sslSocket;
    source = Okio.buffer(Okio.source(socket));
    sink = Okio.buffer(Okio.sink(socket));
    handshake = unverifiedHandshake;
    protocol = maybeProtocol != null
        ? Protocol.get(maybeProtocol)
        : Protocol.HTTP_1_1;
    success = true;
  } catch (AssertionError e) {
    if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
    throw e;
  } finally {
    if (sslSocket != null) {
      Platform.get().afterHandshake(sslSocket);
    }
    if (!success) {
      closeQuietly(sslSocket);
    }
  }
}

如果对https熟悉的话,应该知道,https就是在http的基础上加了一层。

到此链接完成RealConnection实例化完成。