preface

Many of you are familiar with TCP’s three-way handshake and four-way wave, and Okhttp also has connection management. This article focuses on how Okhttp manages TCP’s three-way handshake and four-way wave, or Okhttp’s connection management.

This article is based on OKHTTP 3.14.9

Github address: github.com/square/okht…

Gradle dependencies: Implementation Group: ‘com.squareup.okhttp3’, name: ‘okhttp’, version: ‘3.14.9’

Okhttp’s interceptor chain (Responsibility Chain)

Okhttp is best known for its chain of interceptors designed in the chain of responsibility mode. Okhttp has a thread pool that triggers each RealCall object to trigger a network request. If you are calling a network request asynchronously realCall.enqueue (), the following methods are called in the asynccall.execute () method. That is, interceptors assemble a collection of responsibility chain interceptors and initiate network requests. It is worth mentioning that the entire process is traversed through the chain of responsibility with chain.proceed(originalRequest). This gives birth to the main ConnectInterceptor, which is responsible for HTTP connections.

/ / RealCall. Java 210 rows
Response getResponseWithInterceptorChain(a) throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if(! forWebSocket) { interceptors.addAll(client.networkInterceptors()); } interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null.0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    boolean calledNoMoreExchanges = false;
    try {
      Response response = chain.proceed(originalRequest);
      if (transmitter.isCanceled()) {
        closeQuietly(response);
        throw new IOException("Canceled");
      }
      return response;
    } catch (IOException e) {
      calledNoMoreExchanges = true;
      throw transmitter.noMoreExchanges(e);
    } finally {
      if(! calledNoMoreExchanges) { transmitter.noMoreExchanges(null); }}}Copy the code

Connection management for Okhttp

ConnectInterceptor

// ConnectInterceptor.java
/** Opens a connection to the target server and proceeds to the next interceptor. */
public final class ConnectInterceptor implements Interceptor {
  public final OkHttpClient client;

  public ConnectInterceptor(OkHttpClient client) {
    this.client = client;
  }

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

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    booleandoExtensiveHealthChecks = ! request.method().equals("GET");
    Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);

    returnrealChain.proceed(request, transmitter, exchange); }}Copy the code

Create an Exchange object and pass it down. The connection between the client and the server has been established. Then the real data transfer will take place. So the emphasis here is on transmitter. NewExchange (Chain, doExtensiveHealthChecks).

Brief introduction of Transmitter and Exchange

  • Transmitter: BRIDGE between OkHttp application layer and network layer. It contains the OkHttpClient object, the RealConnectionPool connection pool, the Call object for the request, the EventListener object for the EventListener, and the AsyncTimeout object for managing timeouts. The Transmitter object can be understood as a management class, which is associated with the docking of the application layer and network layer of the whole network request.
  • Exchange: manages network requestsrequestandresponseCan be understood asThe network layerThe Transmitter also contains this object. In fact,It is the inside that handles the request and response of the network requestExchangeCodecobject.

As a result, an Exchange object represents a connection to the server. So with that conclusion we move on.

Transmitter.newExchange

/ / Transmitter. Java 158 rows
/** Returns a new exchange to carry a new request and response. */
Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    synchronized (connectionPool) {
      if (noMoreExchanges) {
        throw new IllegalStateException("released");
      }
      if(exchange ! =null) {
        throw new IllegalStateException("cannot make a new request because the previous response "
            + "is still open: please call response.close()");
      }
    }

    ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
    Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);

    synchronized (connectionPool) {
      this.exchange = result;
      this.exchangeRequestDone = false;
      this.exchangeResponseDone = false;
      returnresult; }}Copy the code

The core of the newExchange method is to call the ExchangeFinder.find () method to find an appropriate connection, that is, to find an appropriate Exchange Dec object and generate an Exchange object to return.

ExchangeFinder.find

  / / ExchangeFinder. Java 79 rows
  public ExchangeCodec find(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      return resultConnection.newCodec(client, chain);
    } catch (RouteException e) {
      trackFailure();
      throw e;
    } catch (IOException e) {
      trackFailure();
      throw newRouteException(e); }}Copy the code

RealConnection resultConnection = findHealthyConnection(); , which can be interpreted as obtaining available RealConnection objects through findHealthyConnection() and creating an Exchange Dec object with RealConnection for the interaction of network requests.

RealConnection is an abstraction of a connection.

Exchange or Exchange DEC is the interaction between request and Reponse that is responsible for HTTP requests.

So, to understand how Okhttp manages connections, you need to look at RealConnection lookups.

Find RealConnection

In Okhttp there is a design to reuse connections using connection pools. RealConnectionPool plays this role and maintains a double-ended queue Deque

connections to cache reusable connections. Connection release is controlled by maxIdleConnections maximum number of idle connections and keepAliveDurationNs connection duration. RealConnectionPool maintains an internal thread pool that periodically releases unwanted RealConnectionPool connections. Going back to the logic of finding available RealConnections, the findHealthyConnection method described earlier is just an encapsulation of the findConnection process, which loops in an infinite loop until an available RealConnection is retrieved.

/** * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated * until a healthy connection is found. */
  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) {
        if (candidate.successCount == 0 && !candidate.isMultiplexed()) {
          returncandidate; }}// 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)) { candidate.noNewExchanges();continue;
      }

      returncandidate; }}Copy the code

The findConnection method is now ready for the lookup phase:

  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    boolean foundPooledConnection = false;
    // Find the RealConnection result
    RealConnection result = null;
    Route selectedRoute = null;
    RealConnection releasedConnection;
    Socket toClose;
    synchronized (connectionPool) {
      if (transmitter.isCanceled()) throw new IOException("Canceled");
      hasStreamFailure = false; // This is a fresh attempt..// [1] Transmitter object itself record RealConnection reusable
      if(transmitter.connection ! =null) {
        // We had an already-allocated connection and it's good.
        result = transmitter.connection;
        releasedConnection = null;
      }
      
      if (result == null) {
        // Attempt to get a connection from the pool.
        // [2] Find a reusable RealConnection in the connectionPool with the current request address
        if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null.false)) {
          foundPooledConnection = true;
          result = transmitter.connection;
        } else if(nextRouteToTry ! =null) {
          selectedRoute = nextRouteToTry;
          nextRouteToTry = null;
        } else if(retryCurrentRoute()) { selectedRoute = transmitter.connection.route(); }}}...if(result ! =null) {
      // If we found an already-allocated or pooled connection, we're done.
      return result;
    }

    // If we need a route selection, make one. This is a blocking operation.
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null| |! routeSelection.hasNext())) { newRouteSelection =true;
      routeSelection = routeSelector.next();
    }

    List<Route> routes = null;
    synchronized (connectionPool) {
      if (transmitter.isCanceled()) 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.
        routes = routeSelection.getAll();
        // [3] Traverses the global routing paths to find reusable realConnections in the connectionPool
        if (connectionPool.transmitterAcquirePooledConnection(
            address, transmitter, routes, false)) {
          foundPooledConnection = true; result = transmitter.connection; }}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.
        // [4] If none of the previous [1][2][3] is found, a new RealConnection object will be created and a server connection will be requested.
        result = newRealConnection(connectionPool, selectedRoute); connectingConnection = result; }}// If we found a pooled connection on the 2nd time around, we're done.
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
      return result;
    }

    // Do TCP + TLS handshakes. This is a blocking operation.
    // If [4] [1][2][3] is not found, the current result is a new RealConnection object,
    // The connect method does the TCP 3-way handshake and SSL/TLS.
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    connectionPool.routeDatabase.connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      connectingConnection = null;
      // Last attempt at connection coalescing, which only occurs if we attempted multiple
      // concurrent connections to the same host.
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
        // We lost the race! Close the connection we created and return the pooled connection.
        result.noNewExchanges = true;
        socket = result.socket();
        result = transmitter.connection;

        // It's possible for us to obtain a coalesced connection that is immediately unhealthy. In
        // that case we will retry the route we just successfully connected with.
        nextRouteToTry = selectedRoute;
      } else {
        connectionPool.put(result);
        transmitter.acquireConnectionNoEvents(result);
      }
    }
    closeQuietly(socket);

    eventListener.connectionAcquired(call, result);
    return result;
  }
Copy the code

The findConnection method is longer, and you can look for comments [1], [2], [3], [4] for each case where a RealConnection object is available.

  • [1] Transmitter object itself record RealConnection reusable.
  • [2] Find reusable RealConnections in the connectionPool by the current request address.
  • [3] Traverse global routing paths and find reusable RealConnections in connectionPool. Ps: Different from [2], other request addresses will be traversed, and the judgment can be based on the same host, etc.
  • [4] If the first three steps are not found, a new connection will be created for TCP+TLS.

It is worth mentioning that every time look up the connection pool reusable connection will be called connectionPool. TransmitterAcquirePooledConnection () method (just preach the parameters of different), Inside this method is a reference to the RealConnection object found in the transmitter. And if a new connection will by transmitter. AcquireConnectionNoEvents (result) will be new RealConnection backup objects. The purpose of this is to find reusable connections more quickly when retrying, which corresponds to the lookup in [1] above.

Perform three handshakes

According to the above findConnection method. The result of the connect, will see the tracking code, eventually go to RealConnection. ConnectSocket ().

// RealConnection.java
private void connectSocket(int connectTimeout, int readTimeout, Call call,
      EventListener eventListener) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();

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

    // The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
    // More details:
    // https://github.com/square/okhttp/issues/3245
    // https://android-review.googlesource.com/#/c/271775/
    try {
      source = Okio.buffer(Okio.source(rawSocket));
      sink = Okio.buffer(Okio.sink(rawSocket));
    } catch (NullPointerException npe) {
      if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
        throw newIOException(npe); }}}Copy the code

At this point, a Socket object is created inside RealConnection for subsequent TCP connection communication.

Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout); This is where the three-way handshake is actually performed, where platform.get () is Okhttp’s adaptation logic based on the Platform (ps: probably not Android or the Android version).

// Platform.java
public void connectSocket(Socket socket, InetSocketAddress address, int connectTimeout)
      throws IOException {
    socket.connect(address, connectTimeout);
  }
Copy the code

Obviously, the TCP three-way handshake inside Okhttp relies on traditional socket connections.

Ps: The above code will create a source and a sink object using the socket object, which is Okio’s stuff and will be used if the request is Http1. Subsequent requests and Reponse use them.

Finally also need to add that after find available RealConnection object in the code above, can use it to create a new object ExchangeCodec: resultConnection. NewCodec (client, chain); . ExchangeCodec is an interface that instantiates the corresponding policy class for Http1 or Http2, depending on the protocol used.

// RealConnection.java
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); }}Copy the code

In Http1, the source and sink objects created using socket objects are referenced in Http1ExchangeCodec.

Okhttp connection release

Having said the management of the three-way handshake, let’s look at the four-way wave, or connection release. In the source you will see a number of calls to the closeQuietly method, which is used to release the join.

// Utils.java
public static void closeQuietly(Socket socket) {
    if(socket ! =null) {
      try {
        socket.close();
      } catch (AssertionError e) {
        if(! isAndroidGetsocknameError(e))throw e;
      } catch (RuntimeException rethrown) {
        throw rethrown;
      } catch (Exception ignored) {
      }
    }
  }
Copy the code

socket.close(); This is the closing (release) of the TCP connection. This is actually a real release of the connection. A common place to trigger this logic is the RealConnectionPool’s timed collection mechanism mentioned earlier.

The last

This article focuses on how Okhttp manages TCP connections, where the three-way handshake and the four-way wave occur. Finally, summarize the function of the class designed in the article!

  • ConnectInterceptor: Okhttp interceptor used to establish a connection to the server, where the connection is initiated.
  • Transmitter: Intermediate layer between Okhttp application layer and network layer. I’m going to use it here to get itExchangeObject.
  • ExchangeFinder: used to find available connections. To find the availableRealConnectionObject, instance outExchangeCodecObject, and returnExchangeThis is where all the objects are done.
  • Exchange: Event firing that encapsulates HTTP request and response, with a callback that the event listens for. Has the connection internallyExchangeCodecObject.
  • ExchangeCodec: An event that is actually used for HTTP request and response. Okio is used internally.
  • RealConnection: An abstraction of a connection, the socket object used internally for this TCP connection. Can be inTransmitterIn the,RealConnectionPoolConnection pooling or New fetch, this step takes place inExchangeFinderIn the.
  • RealConnectionPool: Used for cache reuseRealConnectionObject.