OKHTTP系列(七)--拦截器之ConnectInterceptor连接拦截器

一、连接机制

连接的创建是在StreamAllocation对象统筹下完成的,我们前面面也说过它早在RetryAndFollowUpInterceptor就被创建了,StreamAllocation对象,主要用来管理两个关键角色:

  • RealConnection:真正建立连接的对象,利用Socket建立连接。
  • ConnectionPool:连接池,用来管理和复用连接。

在这里初始化了一个StreamAllocation对象,在这个StreamAllocation对象里初始化了一个Socket对象用来做连接,但是并没有真正连接。

二、ConnectInterceptor类:

/** 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();
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
//我们需要网络来满足这个request。可能用于验证条件GET。
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
//创建输出流
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
//建立连接
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }
}

ConnectInterceptorRequest阶段建立连接,处理方式也很简单,创建了两个对象:

  • HttpCodec:用来编码HTTP requests和解码HTTP responses
  • RealConnection:连接对象,负责发起与服务器的连接。

这里事实上包含了连接、连接池等一整套的Okhttp的连接机制,我们放在下面单独讲,先来继续看最后一个Interceptor:CallServerInterceptor。

三、创建连接

在上面的ConnectInterceptor类中我们可以知道,ConnectInterceptor用来完成连接,而真正的连接是在RealConnection中实现的,连接由连接池ConnectionPool管理,连接池最多保存5个地址连接的keep-alive,每个keep-alive时长为5分钟,并有异步线程清理无效的连接。

主要由两个方法完成,如下所示:

//创建输出流

  • HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);

//建立连接

  • RealConnection connection = streamAllocation.connection();

我们具体来看一看

streamAllocation.newStream(client, chain, doExtensiveHealthChecks)方法最终调用findConnection(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled)方法来建立连接。

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,
    int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
  boolean foundPooledConnection = false;
  RealConnection result = null;
  Route selectedRoute = null;
  Connection releasedConnection;
  Socket toClose;
  synchronized (connectionPool) {
    if (released) throw new IllegalStateException("released");
    if (codec != null) throw new IllegalStateException("codec != null");
    if (canceled) throw new IOException("Canceled");

    // Attempt to use an already-allocated connection. We need to be careful here because our
    // already-allocated connection may have been restricted from creating new streams.
//1、查看是否有完好的连接
    releasedConnection = this.connection;
    toClose = releaseIfNoNewStreams();
    if (this.connection != null) {
      // We had an already-allocated connection and it's good.
      result = this.connection;
      releasedConnection = null;
    }
    if (!reportedAcquired) {
      // If the connection was never reported acquired, don't report it as released!
      releasedConnection = null;
    }

    if (result == null) {
      // Attempt to get a connection from the pool.尝试从池中获取连接
//2、连接池中是否用可用的连接,有则使用
      Internal.instance.get(connectionPool, address, this, null);
      if (connection != null) {
        foundPooledConnection = true;
        result = connection;
      } else {
        selectedRoute = route;
      }
    }
  }
  closeQuietly(toClose);

  if (releasedConnection != null) {
    eventListener.connectionReleased(call, releasedConnection);
  }
  if (foundPooledConnection) {
    eventListener.connectionAcquired(call, result);
  }
  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;
//线程的选择,多IP操作
  if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
    newRouteSelection = true;
    routeSelection = routeSelector.next();
  }
//3、如果没有可用连接,则自己创建一个
  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);
    }
  }

  // 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.进行TCP + TLS握手。这是一个阻塞操作。
//4、开始TCP以及TLS握手操作
  result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
      connectionRetryEnabled, call, eventListener);
  routeDatabase().connected(result.route());
//5、将新创建的连接,放在连接池中
  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.
    if (result.isMultiplexed()) {
      socket = Internal.instance.deduplicate(connectionPool, address, this);
      result = connection;
    }
  }
  closeQuietly(socket);

  eventListener.connectionAcquired(call, result);
  return result;
}

整个流程如下:

1、查找是否有完整的连接可用:

  • Socket没有关闭
  • 输入流没有关闭
  • 输出流没有关闭
  • Http2连接没有关闭

2、连接池中是否有可用的连接,如果有则可用。

3、如果没有可用连接,则自己创建一个。

4、开始TCP连接以及TLS握手操作。

5、将新创建的连接加入连接池。

上述方法完成后会创建一个RealConnection对象,然后调用该对象的connect()方法建立连接,我们再来看看RealConnection.connect()方法的实现。

 

public final class RealConnection extends Http2Connection.Listener implements Connection {
    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);

        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"));
            }
        } else {
            if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
                throw new RouteException(new UnknownServiceException(
                        "H2_PRIOR_KNOWLEDGE cannot be used with HTTPS"));
            }
        }
//开始连接
        while (true) {
            try {
//如果是通道模式,则建立通道连接
                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);
                }
//建立https连接
                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;
                }
            }
        }

        if (route.requiresTunnel() && rawSocket == null) {
            ProtocolException exception = new ProtocolException("Too many tunnel connections attempted: "
                    + MAX_TUNNEL_ATTEMPTS);
            throw new RouteException(exception);
        }

        if (http2Connection != null) {
            synchronized (connectionPool) {
                allocationLimit = http2Connection.maxConcurrentStreams();
            }
        }
    }
/** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
//在原始套接字上构建完整的HTTP或HTTPS连接所需的所有工作都完成了吗
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;
  }

  // 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 new IOException(npe);
    }
  }
}
}

最终调用Java里的套接字Socket里的connect()方法。

Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);

public void connectSocket(Socket socket, InetSocketAddress address, int connectTimeout)
    throws IOException {
  socket.connect(address, connectTimeout);
}

四、连接池

  • 我们知道在负责的网络环境下,频繁的进行建立Sokcet连接(TCP三次握手)和断开Socket(TCP四次分手)是非常消耗网络资源和浪费时间的,HTTP中的keepalive连接对于降低延迟和提升速度有非常重要的作用。
  • 复用连接就需要对连接进行管理,这里就引入了连接池的概念。
  • Okhttp支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间),连接池有ConectionPool实现,对连接进行回收和管理。

4.1、ConectionPool在内部维护了一个线程池,来清理连接

public final class ConnectionPool {
    /**
     * Background threads are used to cleanup expired connections. There will be at most a single
     * thread running per connection pool. The thread pool executor permits the pool itself to be
     * garbage collected.
     */
    private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
            Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));

    /**
     * The maximum number of idle connections for each address.
     */
    private final int maxIdleConnections;
    private final long keepAliveDurationNs;
//清理连接,在线程池executor里调用。
    private final Runnable cleanupRunnable = new Runnable() {
        @Override
        public void run() {
            while (true) {
//执行清理,并返回下次需要清理的时间。
                long waitNanos = cleanup(System.nanoTime());
                if (waitNanos == -1) return;
                if (waitNanos > 0) {
                    long waitMillis = waitNanos / 1000000L;
                    waitNanos -= (waitMillis * 1000000L);
                    synchronized (ConnectionPool.this) {
                        try {
//在timeout时间内释放锁
                            ConnectionPool.this.wait(waitMillis, (int) waitNanos);
                        } catch (InterruptedException ignored) {
                        }
                    }
                }
            }
        }
    };
}

ConectionPool在内部维护了一个线程池,来清理连,清理任务由cleanup()方法完成,它是一个阻塞操作,首先执行清理,并返回下次需要清理的间隔时间,调用调用wait() 方法释放锁。等时间到了以后,再次进行清理,并返回下一次需要清理的时间,循环往复。接下来我们来看下cleanup()方法的具体实现。

4.2、cleanup()方法

public final class ConnectionPool {
    /**
     * Performs maintenance on this pool, evicting the connection that has been idle the longest if
     * either it has exceeded the keep alive limit or the idle connections limit.
     *
     * <p>Returns the duration in nanos to sleep until the next scheduled call to this method. Returns
     * -1 if no further cleanups are required.
     */
    long cleanup(long now) {
        int inUseConnectionCount = 0;
        int idleConnectionCount = 0;
        RealConnection longestIdleConnection = null;
        long longestIdleDurationNs = Long.MIN_VALUE;

        // Find either a connection to evict, or the time that the next eviction is due.
        synchronized (this) {
//遍历所有的连接,标记处不活跃的连接。
            for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
                RealConnection connection = i.next();

                // If the connection is in use, keep searching.如果正在使用连接,请继续搜索。
//1、查询此连接内部的StreanAllocation的引用数量。
                if (pruneAndGetAllocationCount(connection, now) > 0) {
                    inUseConnectionCount++;
                    continue;
                }

                idleConnectionCount++;

                // If the connection is ready to be evicted, we're done.如果连接准备好被清除,我们就完成了
//2、标记空闲连接。
                long idleDurationNs = now - connection.idleAtNanos;
                if (idleDurationNs > longestIdleDurationNs) {
                    longestIdleDurationNs = idleDurationNs;
                    longestIdleConnection = connection;
                }
            }

            if (longestIdleDurationNs >= this.keepAliveDurationNs
                    || idleConnectionCount > this.maxIdleConnections) {
                // We've found a connection to evict. Remove it from the list, then close it below (outside
                // of the synchronized block).
//我们找到了与驱逐的联系。 将其从列表中删除,然后将其关闭(在同步块之外)。
//3、如果空闲连接超过5个或者keepalive时间大于5分钟,则将该连接清理掉。
                connections.remove(longestIdleConnection);
            } else if (idleConnectionCount > 0) {
                // A connection will be ready to evict soon.一个连接将随时准备退出。
//4、返回此连接的到期时间,供下次进行清理。
                return keepAliveDurationNs - longestIdleDurationNs;
            } else if (inUseConnectionCount > 0) {
                // All connections are in use. It'll be at least the keep alive duration 'til we run again.
//所有连接都在使用中。至少在我们再次运行之前,它会一直存在。
//5、全部都是活跃连接,5分钟时候再进行清理。
                return keepAliveDurationNs;
            } else {
                // No connections, idle or in use.没有连接,空闲或正在使用。
//6、没有任何连接,跳出循环。
                cleanupRunning = false;
                return -1;
            }
        }
//7、关闭连接,返回时间0,立即再次进行清理。
        closeQuietly(longestIdleConnection.socket());

        // Cleanup again immediately.
        return 0;
    }
}

整个方法的流程如下所示:

  • 查询此连接内部的StreanAllocation的引用数量。
  • 标记空闲连接。
  • 如果空闲连接超过5个或者keepalive时间大于5分钟,则将该连接清理掉。
  • 返回此连接的到期时间,供下次进行清理。
  • 全部都是活跃连接,5分钟时候再进行清理。
  • 没有任何连接,跳出循环。
  • 关闭连接,返回时间0,立即再次进行清理。

RealConnection里有个StreamAllocation虚引用列表,每创建一个StreamAllocation,就会把它添加进该列表中,如果留关闭以后就将StreamAllocation 对象从该列表中移除,正是利用利用这种引用计数的方式判定一个连接是否为空闲连接,

/** Current streams carried by this connection. */
public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();

查找引用计数由pruneAndGetAllocationCount()方法实现,具体实现如下所示:

4.3、pruneAndGetAllocationCount(RealConnection connection, long now)方法

public final class ConnectionPool {
    /**
     * Prunes any leaked allocations and then returns the number of remaining live allocations on
     * {@code connection}. Allocations are leaked if the connection is tracking them but the
     * application code has abandoned them. Leak detection is imprecise and relies on garbage
     * collection.
     */
    private int pruneAndGetAllocationCount(RealConnection connection, long now) {
//虚引用列表
        List<Reference<StreamAllocation>> references = connection.allocations;
//遍历虚引用列表
        for (int i = 0; i < references.size(); ) {
            Reference<StreamAllocation> reference = references.get(i);
//如果虚引用StreamAllocation正在被使用,则跳过进行下一次循环,
            if (reference.get() != null) {
//引用计数
                i++;
                continue;
            }

            // We've discovered a leaked allocation. This is an application bug.
            StreamAllocation.StreamAllocationReference streamAllocRef =
                    (StreamAllocation.StreamAllocationReference) reference;
            String message = "A connection to " + connection.route().address().url()
                    + " was leaked. Did you forget to close a response body?";
            Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);
//否则移除该StreamAllocation引用
            references.remove(i);
            connection.noNewStreams = true;

            // If this was the last allocation, the connection is eligible for immediate eviction.
// 如果所有的StreamAllocation引用都没有了,返回引用计数0
            if (references.isEmpty()) {
                connection.idleAtNanos = now - keepAliveDurationNs;
                return 0;
            }
        }
//返回引用列表的大小,作为引用计数
        return references.size();
    }
}

4.4、连接池的构造方法

  • 构造方法中设置了每个地址的最大空闲连接数maxIdleConnections以及默认每个连接的存活时间keepAliveDurationNs

 

public final class ConnectionPool {
//最大的空闲连接数--每个地址的最大空闲连接数
private final int maxIdleConnections;
//连接持续时间
private final long keepAliveDurationNs;
    /**
     * Create a new connection pool with tuning parameters appropriate for a single-user application.
     * The tuning parameters in this pool are subject to change in future OkHttp releases. Currently
     * this pool holds up to 5 idle connections which will be evicted after 5 minutes of inactivity.
     */
//使用适合单用户应用程序的调整参数创建新的连接池。此池中的调整参数在将来的OkHttp版本中可能会发生变化。
// 目前,该池最多可容纳个空闲连接,这些连接将在5分钟不活动后被驱逐。

 //默认每个地址的最大连接数是5个    
 //默认每个连接的存活时间为5分钟
    public ConnectionPool() {
        this(5, 5, TimeUnit.MINUTES);
    }

    public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
        this.maxIdleConnections = maxIdleConnections;
        this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);

        // Put a floor on the keep alive duration, otherwise cleanup will spin loop.
        if (keepAliveDuration <= 0) {
            throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
        }
    }
}

4.5、设置连接池

public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
    static final List<Protocol> DEFAULT_PROTOCOLS = Util.immutableList(
            Protocol.HTTP_2, Protocol.HTTP_1_1);
    public Builder() {
       ...
        //连接池 管理HTTP和HTTP / 2连接的重用以减少网络延迟。
        //默认每个地址的最大连接数是5个
        //默认每个连接的存活时间为5分钟
        connectionPool = new ConnectionPool();
    }
}

4.6、双端队列

public final class ConnectionPool {
    //连接池中维护了一个双端队列Deque来存储连接
    private final Deque<RealConnection> connections = new ArrayDeque<>();
    //将连接加入到双端队列
    void put(RealConnection connection) {
        assert (Thread.holdsLock(this));
        //没有任何连接时,cleanupRunning = false;
        // 即没有任何链接时才会去执行executor.execute(cleanupRunnable);
        // 从而保证每个连接池最多只能运行一个线程。
        if (!cleanupRunning) {
            cleanupRunning = true;
            executor.execute(cleanupRunnable);
        }
        connections.add(connection);
    }
}
public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
    static {
        Internal.instance = new Internal() {
            ...
            @Override public void put(ConnectionPool pool, RealConnection connection) {
                pool.put(connection);
            }
            ...
        };
    }
}
  • put方法在ConnectInterceptor----》intercept----》streamAllocation.newStream----》findHealthyConnection----》创建新链接后调用。

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值