一、引言
在现代网络应用开发中,HTTP 请求是极其常见的操作。然而,频繁地建立和关闭 HTTP 连接会带来显著的性能开销,因为每次建立连接都需要进行 TCP 握手、SSL 协商等操作,这些操作不仅消耗时间,还会占用大量系统资源。为了解决这个问题,OkHttp 引入了连接池模块,通过复用已经建立的连接,减少连接建立和关闭的次数,从而提高网络请求的效率,降低资源消耗。本文将从源码级别深入分析 OkHttp 连接池模块的实现原理。
二、连接池模块的基本概念与作用
2.1 连接复用的重要性
在传统的网络请求模式中,每个请求都会独立地建立和关闭一个连接。以一个简单的电商应用为例,用户在浏览商品列表时,可能会同时发起多个请求来获取商品图片、详情信息等。如果每次请求都重新建立连接,那么大量的时间和资源都会浪费在连接的建立和关闭上,导致应用响应缓慢,用户体验变差。而连接复用则允许在多个请求之间共享同一个连接,避免了重复的连接建立和关闭过程,从而显著提高了网络请求的效率。
2.2 OkHttp 连接池的功能概述
OkHttp 的连接池模块负责管理 HTTP 连接的复用。它会维护一个连接池,将空闲的连接存储在池中。当有新的请求需要建立连接时,连接池会首先检查池中是否有可用的空闲连接。如果有,则直接复用该连接;如果没有,则创建一个新的连接。同时,连接池还会定期清理过期的空闲连接,以释放系统资源。此外,连接池支持对连接的最大空闲时间和最大连接数进行配置,以满足不同应用场景的需求。
三、连接池模块的核心类与数据结构
3.1 ConnectionPool 类
3.1.1 类的作用与功能
ConnectionPool
类是 OkHttp 连接池模块的核心,它负责管理连接池的生命周期,包括连接的添加、获取、清理等操作。通过 ConnectionPool
类,开发者可以配置连接池的参数,如最大空闲连接数、连接的最大空闲时间等。
3.1.2 源码分析
java
import java.util.Deque;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit;// ConnectionPool 类定义
public final class ConnectionPool {// 用于存储连接的双端队列,使用 ConcurrentLinkedDeque 保证线程安全private final Deque<RealConnection> connections = new ConcurrentLinkedDeque<>();// 最大空闲连接数private final int maxIdleConnections;// 连接的最大空闲时间,单位为纳秒private final long keepAliveDurationNs;// 用于清理过期连接的线程任务private final Runnable cleanupRunnable = new Runnable() {@Overridepublic 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 {// 等待指定的时间后再次清理ConnectionPool.this.wait(waitMillis, (int) waitNanos);} catch (InterruptedException ignored) {}}}}}};// 清理线程private final Thread cleanupThread;// 构造函数,使用默认参数public ConnectionPool() {this(5, 5, TimeUnit.MINUTES);}// 构造函数,允许自定义参数public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {this.maxIdleConnections = maxIdleConnections;this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);// 创建清理线程并启动this.cleanupThread = new Thread(cleanupRunnable, "OkHttp ConnectionPool");this.cleanupThread.start();}// 获取连接池中的连接数量public synchronized int connectionCount() {return connections.size();}// 获取连接池中的空闲连接数量public synchronized int idleConnectionCount() {int total = 0;for (RealConnection connection : connections) {if (connection.idleAtNanos != -1) total++;}return total;}// 将连接添加到连接池中void put(RealConnection connection) {assert (Thread.holdsLock(this));if (!cleanupThread.isAlive()) {// 如果清理线程未启动,则启动它cleanupThread.start();}connections.add(connection);}// 从连接池中获取可用的连接RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {assert (Thread.holdsLock(this));for (RealConnection connection : connections) {if (connection.isEligible(address, route)) {// 如果连接符合条件,则将其分配给当前流streamAllocation.acquire(connection, true);return connection;}}return null;}// 从连接池中移除指定的连接boolean recycle(RealConnection connection) {assert (Thread.holdsLock(this));if (connection.noNewStreams || connection.idleAtNanos == -1) {return false;}connections.remove(connection);return true;}// 清理连接池中的过期连接long cleanup(long now) {int inUseConnectionCount = 0;int idleConnectionCount = 0;RealConnection longestIdleConnection = null;long longestIdleDurationNs = Long.MIN_VALUE;// 遍历连接池中的所有连接synchronized (this) {for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {RealConnection connection = i.next();// 判断连接是否正在使用,并更新分配数量if (pruneAndGetAllocationCount(connection, now) > 0) {inUseConnectionCount++;continue;}idleConnectionCount++;// 计算连接的空闲时间long idleDurationNs = now - connection.idleAtNanos;if (idleDurationNs > longestIdleDurationNs) {longestIdleDurationNs = idleDurationNs;longestIdleConnection = connection;}}// 如果空闲连接数超过最大空闲连接数,或者空闲时间超过最大空闲时间,则移除最长空闲的连接if (longestIdleDurationNs >= keepAliveDurationNs|| idleConnectionCount > maxIdleConnections) {connections.remove(longestIdleConnection);} else if (idleConnectionCount > 0) {// 返回下一次需要清理的时间return keepAliveDurationNs - longestIdleDurationNs;} else if (inUseConnectionCount > 0) {// 所有连接都在使用中,返回最大空闲时间return keepAliveDurationNs;} else {// 连接池为空,停止清理线程return -1;}}// 关闭移除的连接closeQuietly(longestIdleConnection.socket());return 0;}// 修剪连接并获取分配数量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);if (reference.get() == null) {// 移除无效的引用references.remove(i);continue;}i++;}// 更新连接的空闲时间if (connection.allocations.isEmpty()) {connection.idleAtNanos = now;}return connection.allocations.size();}// 安静地关闭连接private void closeQuietly(Closeable closeable) {if (closeable != null) {try {closeable.close();} catch (RuntimeException rethrown) {throw rethrown;} catch (Exception ignored) {}}}
}
从源码可以看出,ConnectionPool
类使用 ConcurrentLinkedDeque
来存储连接,确保在多线程环境下的线程安全。cleanupRunnable
是一个循环任务,会不断调用 cleanup
方法来清理过期的连接。put
方法用于将新的连接添加到连接池中,get
方法用于从连接池中获取可用的连接,recycle
方法用于移除不再需要的连接。cleanup
方法是核心的清理逻辑,它会遍历连接池中的所有连接,计算连接的空闲时间,并根据配置的参数决定是否移除最长空闲的连接。
3.2 RealConnection 类
3.2.1 类的作用与功能
RealConnection
类表示一个实际的 HTTP 连接,它包含了连接的各种信息,如套接字、协议、分配的流等。该类还提供了判断连接是否可用、获取连接的空闲时间等方法,用于连接池对连接进行管理。
3.2.2 源码分析
java
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;// RealConnection 类定义
final class RealConnection implements Connection {// 连接的套接字Socket socket;// 连接的协议Protocol protocol;// 分配给该连接的流列表final List<Reference<StreamAllocation>> allocations = new ArrayList<>();// 连接的空闲时间戳,-1 表示连接正在使用long idleAtNanos = -1;// 标记连接是否不再接受新的流boolean noNewStreams;// 判断连接是否符合指定的地址和路由boolean isEligible(Address address, Route route) {// 判断协议是否兼容if (protocol == Protocol.H2_PRIOR_KNOWLEDGE) {return address.url().scheme().equals("https");}// 判断地址是否匹配if (!Internal.instance.equalsNonHost(this.route().address(), address)) {return false;}// 判断路由是否匹配if (route != null &&!route.equals(this.route())) {return false;}// 判断连接是否还有可用的流if (allocations.size() >= this.route().address().maxRequestsPerConnection()) {return false;}return true;}// 获取连接的分配数量int allocationCount() {return allocations.size();}// 关闭连接void closeIfOwnedBy(StreamAllocation streamAllocation) throws IOException {if (allocations.isEmpty()) {return;}// 移除指定的流分配boolean removed = false;for (Iterator<Reference<StreamAllocation>> i = allocations.iterator(); i.hasNext(); ) {Reference<StreamAllocation> reference = i.next();if (reference.get() == streamAllocation) {i.remove();removed = true;break;}}if (!removed) {return;}// 如果没有分配的流,则关闭连接if (allocations.isEmpty()) {socket.close();}}
}
RealConnection
类中的 isEligible
方法是判断连接是否可以复用的关键。它会检查协议、地址、路由以及连接的可用流数量等条件,只有当所有条件都满足时,才会认为该连接是可用的。closeIfOwnedBy
方法用于关闭连接,当连接上的所有流都被释放后,会关闭套接字。
3.3 StreamAllocation 类
3.3.1 类的作用与功能
StreamAllocation
类用于管理连接的流分配。在 HTTP 连接中,一个连接可以同时处理多个流(如 HTTP/2 协议支持多路复用),StreamAllocation
类负责跟踪和管理这些流的分配情况。它会记录每个连接上分配的流数量,并在流关闭时更新连接的状态。
3.3.2 源码分析
java
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;// StreamAllocation 类定义
final class StreamAllocation {// 连接池final ConnectionPool connectionPool;// 请求的地址final Address address;// 路由选择器final RouteSelector routeSelector;// 标记流是否已经释放private final AtomicBoolean released = new AtomicBoolean();// 标记流是否已经取消private final AtomicBoolean canceled = new AtomicBoolean();// 分配的连接RealConnection connection;// 分配的流数量int allocationCount;// 构造函数StreamAllocation(ConnectionPool connectionPool, Address address) {this.connectionPool = connectionPool;this.address = address;this.routeSelector = new RouteSelector(address, routeDatabase());}// 获取路由数据库private RouteDatabase routeDatabase() {return Internal.instance.routeDatabase(connectionPool);}// 分配一个连接RealConnection allocateConnection() throws IOException {assert (Thread.holdsLock(connectionPool));if (released.get()) throw new IllegalStateException("released");if (canceled.get()) throw new IOException("Canceled");// 从连接池中获取可用的连接RealConnection result = connectionPool.get(address, this, null);if (result != null) {this.connection = result;allocationCount++;return result;}// 如果没有可用的连接,则创建一个新的连接result = new RealConnection(connectionPool, routeSelector.next());connectionPool.put(result);this.connection = result;allocationCount++;return result;}// 释放一个连接void release() {assert (Thread.holdsLock(connectionPool));if (released.getAndSet(true)) return;if (connection != null) {connection.closeIfOwnedBy(this);connection = null;allocationCount = 0;}}// 取消流分配void cancel() {if (canceled.getAndSet(true)) return;if (connection != null) {connection.cancel();}}
}
StreamAllocation
类的 allocateConnection
方法是分配连接的核心逻辑。它首先尝试从连接池中获取可用的连接,如果没有则创建一个新的连接并添加到连接池中。release
方法用于释放连接,当流不再使用时,会调用连接的 closeIfOwnedBy
方法来关闭连接。cancel
方法用于取消流分配,会调用连接的 cancel
方法来取消连接。
四、连接池模块的工作流程
4.1 连接的添加与复用
4.1.1 连接的添加
当一个新的请求需要建立连接时,会调用 StreamAllocation
类的 allocateConnection
方法。以下是该方法的详细调用流程:
java
// StreamAllocation 类的 allocateConnection 方法
RealConnection allocateConnection() throws IOException {assert (Thread.holdsLock(connectionPool));if (released.get()) throw new IllegalStateException("released");if (canceled.get()) throw new IOException("Canceled");// 从连接池中获取可用的连接RealConnection result = connectionPool.get(address, this, null);if (result != null) {this.connection = result;allocationCount++;return result;}// 如果没有可用的连接,则创建一个新的连接result = new RealConnection(connectionPool, routeSelector.next());connectionPool.put(result);this.connection = result;allocationCount++;return result;
}
在这个方法中,首先会检查当前流是否已经释放或取消。然后尝试从连接池中获取可用的连接,如果获取到则将该连接分配给当前流,并增加分配数量。如果没有可用的连接,则通过 RouteSelector
选择一个新的路由,创建一个新的 RealConnection
对象,并将其添加到连接池中。
4.1.2 连接的复用
当调用 ConnectionPool
类的 get
方法时,会遍历连接池中的所有连接,调用 RealConnection
类的 isEligible
方法判断连接是否符合指定的地址和路由。以下是 get
方法和 isEligible
方法的详细代码:
java
// ConnectionPool 类的 get 方法
RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {assert (Thread.holdsLock(this));for (RealConnection connection : connections) {if (connection.isEligible(address, route)) {streamAllocation.acquire(connection, true);return connection;}}return null;
}// RealConnection 类的 isEligible 方法
boolean isEligible(Address address, Route route) {// 判断协议是否兼容if (protocol == Protocol.H2_PRIOR_KNOWLEDGE) {return address.url().scheme().equals("https");}// 判断地址是否匹配if (!Internal.instance.equalsNonHost(this.route().address(), address)) {return false;}// 判断路由是否匹配if (route != null &&!route.equals(this.route())) {return false;}// 判断连接是否还有可用的流if (allocations.size() >= this.route().address().maxRequestsPerConnection()) {return false;}return true;
}
在 get
方法中,会遍历连接池中的所有连接,调用 isEligible
方法判断连接是否符合条件。isEligible
方法会检查协议、地址、路由以及连接的可用流数量等条件,只有当所有条件都满足时,才会认为该连接是可用的。如果找到可用的连接,则将其分配给当前流,并返回该连接。
4.2 连接的清理机制
4.2.1 清理线程的启动
在 ConnectionPool
类的构造函数中,会创建一个清理线程 cleanupThread
,并启动该线程。清理线程会不断调用 cleanup
方法来清理连接池中的过期连接。以下是相关的源码:
java
// ConnectionPool 类的构造函数
public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {this.maxIdleConnections = maxIdleConnections;this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);// 创建清理线程this.cleanupThread = new Thread(cleanupRunnable, "OkHttp ConnectionPool");this.cleanupThread.start();
}// 清理线程的任务
private final Runnable cleanupRunnable = new Runnable() {@Overridepublic 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 {// 等待指定的时间后再次清理ConnectionPool.this.wait(waitMillis, (int) waitNanos);} catch (InterruptedException ignored) {}}}}}
};
在构造函数中,会根据传入的参数设置最大空闲连接数和连接的最大空闲时间。然后创建一个新的线程,并将 cleanupRunnable
作为线程的任务。cleanupRunnable
是一个无限循环,会不断调用 cleanup
方法来清理连接池。如果 cleanup
方法返回的等待时间大于 0,则线程会进入等待状态,等待指定的时间后再次进行清理。
4.2.2 清理逻辑的实现
cleanup
方法是连接池清理的核心方法,它会遍历连接池中的所有连接,计算连接的空闲时间,并根据配置的参数决定是否移除最长空闲的连接。以下是 cleanup
方法的详细实现:
java
// ConnectionPool 类的 cleanup 方法
long cleanup(long now) {int inUseConnectionCount = 0;int idleConnectionCount = 0;RealConnection longestIdleConnection = null;long longestIdleDurationNs = Long.MIN_VALUE;// 遍历连接池中的所有连接synchronized (this) {for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {RealConnection connection = i.next();// 判断连接是否正在使用if (pruneAndGetAllocationCount(connection, now) > 0) {inUseConnectionCount++;continue;}idleConnectionCount++;// 计算连接的空闲时间long idleDurationNs = now - connection.idleAtNanos;if (idleDurationNs > longestIdleDurationNs) {longestIdleDurationNs = idleDurationNs;longestIdleConnection = connection;}}// 如果空闲连接数超过最大空闲连接数,或者空闲时间超过最大空闲时间,则移除最长空闲的连接if (longestIdleDurationNs >= keepAliveDurationNs|| idleConnectionCount > maxIdleConnections) {connections.remove(longestIdleConnection);} else if (idleConnectionCount > 0) {// 返回下一次需要清理的时间return keepAliveDurationNs - longestIdleDurationNs;} else if (inUseConnectionCount > 0) {// 所有连接都在使用中,返回最大空闲时间return keepAliveDurationNs;} else {// 连接池为空,停止清理线程return -1;}}// 关闭移除的连接closeQuietly(longestIdleConnection.socket());return 0;
}
在 cleanup
方法中,首先会初始化一些变量,用于记录正在使用的连接数、空闲连接数、最长空闲的连接以及其空闲时间。然后遍历连接池中的所有连接,调用 pruneAndGetAllocationCount
方法判断连接是否正在使用。如果连接正在使用,则增加正在使用的连接数;如果连接空闲,则计算其空闲时间,并更新最长空闲的连接和其空闲时间。
根据计算结果,如果空闲连接数超过最大空闲连接数,或者最长空闲连接的空闲时间超过最大空闲时间,则移除该连接。如果还有空闲连接,则返回下一次需要清理的时间;如果所有连接都在使用中,则返回最大空闲时间;如果连接池为空,则返回 -1 表示停止清理线程。最后,关闭移除的连接。
4.3 连接池模块的时序图
从时序图中可以清晰地看到连接池模块的工作流程。应用程序发起请求后,StreamAllocation
类会尝试从连接池中获取可用的连接,如果没有可用的连接,则创建一个新的连接并添加到连接池中。请求完成后,会释放连接。同时,清理线程会定期清理连接池中的过期连接。
五、连接池模块的配置与优化
5.1 连接池参数的配置
5.1.1 最大空闲连接数
maxIdleConnections
参数用于设置连接池中的最大空闲连接数。当空闲连接数超过该值时,连接池会清理最长空闲的连接。可以通过 ConnectionPool
类的构造函数来配置该参数,例如:
java
OkHttpClient client = new OkHttpClient.Builder().connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES)).build();
在这个例子中,将最大空闲连接数设置为 10。
5.1.2 连接的最大空闲时间
keepAliveDuration
参数用于设置连接的最大空闲时间。当连接的空闲时间超过该值时,连接池会清理该连接。同样可以通过 ConnectionPool
类的构造函数来配置该参数,例如:
java
OkHttpClient client = new OkHttpClient.Builder().connectionPool(new ConnectionPool(5, 10, TimeUnit.MINUTES)).build();
在这个例子中,将连接的最大空闲时间设置为 10 分钟。