okhttp之旅(十)--连接池

系统学习详见OKhttp源码解析详解系列

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

1 连接池构造方法

  • 构造方法中 设置了每个地址的最大空闲连接数maxIdleConnections以及默认每个连接的存活时间keepAliveDurationNs
public final class ConnectionPool {
    //最大的空闲连接数--每个地址的最大空闲连接数
    private final int maxIdleConnections;
    //连接持续时间
    private final long keepAliveDurationNs;

    //默认每个地址的最大连接数是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);
        }
    }

}

设置连接池

OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
        //创建具有自定义设置的共享实例
public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
    static final List DEFAULT_PROTOCOLS = Util.immutableList(
   public Builder() {
   ...
        //连接池 管理HTTP和HTTP / 2连接的重用以减少网络延迟。
        //默认每个地址的最大连接数是5个
        //默认每个连接的存活时间为5分钟
        connectionPool = new ConnectionPool();
   ...
   }
}

2 连接池--连接的存储与删除

2.1 双端队列

连接池中维护了一个双端队列Deque来存储连接

private final Deque connections = new ArrayDeque<>();

2.2 连接的存储

将连接加入到双端队列

public final class ConnectionPool {
    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-->创建新链接后调用

2.3 连接的清理

2.3.1 连接池中维护类一个线程池,这个线程池中只开启了一个线程用来清理链接。

  • 整个方法的流程如下所示:
  • 1.查询此连接内部的StreanAllocation的引用数量。
  • 2.标记空闲连接。
  • 3.如果空闲连接超过5个或者keepalive时间大于5分钟,则将该连接清理掉。
  • 4.返回此连接的到期时间,供下次进行清理。
  • 5.全部都是活跃连接,5分钟时候再进行清理。
  • 6.没有任何连接,跳出循环。
  • 7.关闭连接,返回时间0,立即再次进行清理。
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(), Util.threadFactory("OkHttp ConnectionPool", true));
    //corePoolSize=0 maximumPoolSize=Integer.MAX_VALUE 使用SynchronousQueue直接提交队列,
    // 在执行execute会立马交由复用的线程或新创建线程执行任务
    //清理连接,在线程池executor里调用。
    private final Runnable cleanupRunnable = new Runnable() {
        @Override
        public void run() {
            while (true) {
                //执行清理,并返回下次需要清理的时间。
                // 没有任何连接时,cleanupRunning = false;

                long waitNanos = cleanup(System.nanoTime());
                if (waitNanos == -1) return;
                if (waitNanos > 0) {
                    long waitMillis = waitNanos / 1000000L;
                    waitNanos -= (waitMillis * 1000000L);
                    synchronized (ConnectionPool.this) {
                        //在timeout时间内释放锁
                        try {
                            ConnectionPool.this.wait(waitMillis, (int) waitNanos);
                        } catch (InterruptedException ignored) {
                        }
                    }
                }
            }
        }
    };

2.3.2 cleanup(long now)

在符合条件下将连接清理出双端队列。

    //对该池执行维护,如果它超出保持活动限制或空闲连接限制,则驱逐空闲时间最长的连接。
    //返回纳秒级的持续时间,直到下次预定调用此方法为止。 如果不需要进一步清理,则返回-1。
    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 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;
    }

2.3.3查询此连接内部的StreanAllocation的引用数量(连接是否可用)

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

public final class RealConnection extends Http2Connection.Listener implements Connection {
    /**
     * Current streams carried by this connection.
     * 由此连接携带的当前流。
     */
    public final List> allocations = new ArrayList<>();
}

pruneAndGetAllocationCount

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.
     * *修剪任何泄漏的分配,然后返回{@code connection}上剩余的实时分配数量。
     * 如果连接正在跟踪它们,但是应用程序代码已经放弃它们,则分配会泄漏。
     * 泄漏检测不准确,依靠垃圾收集。
     */


    private int pruneAndGetAllocationCount(RealConnection connection, long now) {
        //虚引用列表
        List> references = connection.allocations;
        //遍历虚引用列表
        for (int i = 0; i < references.size(); ) {
            Reference 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();
    }
}

参考

https://juejin.im/post/5a704ed05188255a8817f4c9#heading-12

你可能感兴趣的:(okhttp之旅(十)--连接池)