🎯 为什么需要连接池?
在没有连接池的年代,每次 HTTP 请求都要经历:
- DNS 解析(域名转 IP)
- TCP 三次握手(建立连接)
- TLS 握手(如果是 HTTPS)
- 发送请求、接收响应
- TCP 四次挥手(关闭连接)
这个过程非常耗时,尤其是 TCP 和 TLS 握手。而且频繁地创建和关闭 socket 连接对系统资源也是巨大的浪费。
连接池的核心思想就是:复用已建立的连接,避免重复握手开销。当一个请求完成后,不立即关闭连接,而是把它放进一个"池子"里,后续请求如果目的地相同(host、port、协议一致),就直接从池子里捞一条连接出来用。
🏗️ 连接池的核心组件
1. ConnectionPool
ConnectionPool 是连接池的门面,它负责存储和管理所有连接。它的主要成员:
java
public final class ConnectionPool {
// 存储连接的队列
private final Deque<RealConnection> connections = new ArrayDeque<>();
// 清理连接的线程(单例)
private final Runnable cleanupRunnable = () -> {
while (true) {
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
synchronized (ConnectionPool.this) {
try {
ConnectionPool.this.wait(TimeUnit.NANOSECONDS.toMillis(waitNanos));
} catch (InterruptedException ignored) {
}
}
}
}
};
// 最大空闲连接数
private final int maxIdleConnections;
// 空闲连接存活时间
private final long keepAliveDurationNs;
// 清理线程是否已启动
private boolean cleanupRunning;
}
connections 是一个 Deque(双端队列),存放所有可用的 RealConnection(实际连接对象)。最大空闲连接数和存活时间可以通过建造者配置。
2. RealConnection
RealConnection 代表一个真实的 socket 连接。它封装了 Socket、ConnectionSpec、Handshake 等信息。关键字段:
java
public final class RealConnection extends Http2Connection.Listener implements Connection {
// 连接的路由(包含地址、代理等)
private Route route;
// 原始 socket
private Socket rawSocket;
// 应用层 socket(可能是 TLS 包装后的)
private Socket socket;
// HTTP/2 连接(如果协议升级)
private Http2Connection http2Connection;
// 协议(HTTP/1.1 或 HTTP/2)
private Protocol protocol;
// 该连接承载的流(请求/响应)数量
public int allocationLimit = 1; // HTTP/1.1 最多 1 个流,HTTP/2 可以大于 1
// 该连接当前正在进行的流(Allocation)列表
public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();
// 空闲时间纳秒(用于清理)
public long idleAtNanos = Long.MAX_VALUE;
}
对于 HTTP/1.1,一个连接同时只能处理一个请求(串行),所以 allocationLimit 为 1。对于 HTTP/2,它可以处理多个并发流,allocationLimit 等于 Settings.Settings.DEFAULT_MAX_CONCURRENT_STREAMS(通常 > 100)。
3. StreamAllocation
StreamAllocation 是连接分配的协调者,它负责从连接池获取连接、释放连接、以及处理连接失败时的重试。它内部持有:
Address:请求的地址(主机、端口、协议等)ConnectionPool:引用池子RealConnection:当前分配到的连接RouteSelector:路由选择器(用于重试时切换 IP)
它的工作流程很像"连接管理员":需要连接时找池子要;用完后通知池子可以回收;如果连接出问题,负责标记并尝试获取新连接。
🔄 连接复用流程详解
我们从一次典型的 HTTP 请求(如 Call.enqueue())来看连接池是如何介入的。
第一步:寻找连接
当请求进入 ConnectInterceptor 时,它会通过 StreamAllocation 来获取一个 HttpCodec(用于编码/解码请求和响应)。StreamAllocation 的 newStream() 方法会调用 findHealthyConnection() 来找到一个可用的连接。
findConnection() 的简化逻辑如下:
java
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
// 1. 如果已经分配了连接且仍然可用,直接复用
if (this.connection != null) {
return this.connection;
}
// 2. 尝试从连接池中获取复用连接
RealConnection result = connectionPool.get(routeSelection.address(), this, null);
if (result != null) {
this.connection = result;
return result;
}
// 3. 池子里没有,创建新连接
result = new RealConnection(routeSelection, connectionPool);
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
// 4. 将新连接放入连接池
connectionPool.put(result);
this.connection = result;
return result;
}
关键在第二步:connectionPool.get(...) 会遍历池中所有连接,找到第一个符合以下条件的连接:
- 地址匹配 (
Address.equals()):host、port、代理、DNS、SSL 套接字工厂等都相同。 - 连接是空闲的 (
allocations.isEmpty()或当前流数未达到上限)。 - 连接未过期(存活时间范围内)。
第二步:连接池的查找逻辑
ConnectionPool.get() 方法内部:
java
RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
for (RealConnection connection : connections) {
if (connection.isEligible(address, route)) {
// 找到可用连接,增加引用计数
streamAllocation.acquire(connection, true);
return connection;
}
}
return null;
}
isEligible() 会检查:
address是否完全匹配。- 如果是 HTTP/2,还要检查连接是否有多余的流槽位(
connection.allocations.size() < connection.allocationLimit)。 - 如果是 HTTP/1.1,检查
allocations是否为空(即当前没有请求占用)。
如果找到可用连接,streamAllocation.acquire() 会将当前 StreamAllocation 加入连接的 allocations 列表中,这样连接就知道自己被占用了。
第三步:使用完毕,归还连接
当请求完成(无论成功或失败),StreamAllocation 会被关闭。在 close() 或 release() 方法中,它会调用 connection.release(streamAllocation),将自身从 allocations 列表中移除。
如果 allocations 变为空,说明连接进入空闲状态,会记录当前时间戳 idleAtNanos,方便清理线程扫描。
第四步:连接池清理
清理线程定期运行 cleanup() 方法,遍历所有连接:
- 对于正在使用的连接(
allocations非空),跳过。 - 对于空闲连接,检查空闲时间是否超过
keepAliveDurationNs。如果超过,将其标记为待移除。 - 如果空闲连接数超过
maxIdleConnections,也要移除多余的空闲连接(优先移除最老的)。 - 最终返回下一次需要清理的等待时间。
java
long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
synchronized (this) {
for (RealConnection connection : connections) {
// 如果连接正在被使用(allocations 非空),计数 inUse
if (connection.allocations.size() > 0) {
inUseConnectionCount++;
continue;
}
idleConnectionCount++;
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
// 检查是否超时或超过最大空闲数
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// 移除最老的空闲连接
connections.remove(longestIdleConnection);
return 0; // 立即再次清理
}
if (idleConnectionCount > 0) {
// 返回还需多久需要清理
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
return keepAliveDurationNs; // 都在用,等 keepAlive 时间后再看
} else {
return -1; // 没有连接,停止清理线程
}
}
}
清理线程通过 wait(等待时间) 来避免轮询,非常高效。
🌐 HTTP/2 的多路复用与连接池
HTTP/2 允许在同一个 TCP 连接上并发发送多个请求(流),每个流独立互不干扰。这就极大地提高了连接利用率。
在 OkHttp 中,对于 HTTP/2,一个 RealConnection 对应一个 Http2Connection 对象,内部管理多个流。连接池中同样只保存一个 RealConnection,但是 allocations 列表里可以同时有多个 StreamAllocation 指向它,每个代表一个活跃流。
当新的请求到来,如果已经有一个 HTTP/2 连接,且该连接上的当前流数未达到 allocationLimit(即还有剩余并发流位置),就可以直接复用该连接,无需创建新连接。
这带来了巨大的性能提升:对于同一个域名,可能只需要一条 TCP 连接就能处理所有并发请求,大大减少了握手开销。
⚙️ 配置与调优
我们可以通过 OkHttpClient.Builder 调整连接池参数:
java
OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(
5, // 最大空闲连接数(默认 5)
30, TimeUnit.SECONDS // 空闲连接存活时间(默认 5 分钟)
))
.build();
- 最大空闲连接数:针对 HTTP/1.1,避免同时保持太多空闲连接浪费资源。对于 HTTP/2,通常不需要很多空闲连接,因为一条连接可以复用。
- 存活时间:连接空闲多久后会被关闭。如果应用有频繁的脉冲式请求,可以适当调高,避免连接被过早关闭。
另外,可以通过 addNetworkInterceptor 来监控连接复用情况,或者开启 OkHttp 的日志(HttpLoggingInterceptor)观察连接行为。
📝 总结
OkHttp 的连接池是一个设计精巧的组件:
- 复用连接,避免重复握手,显著提升性能。
- 管理空闲连接,通过清理线程自动回收不活跃的连接,防止资源泄漏。
- 同时支持 HTTP/1.1 和 HTTP/2,对上层透明,自动优化。
- 与
StreamAllocation配合,精确追踪每个连接的引用计数,确保安全释放。
理解连接池的原理,不仅能帮助我们更好地配置 OkHttp,还能在遇到连接问题(如连接泄漏、超时)时快速定位原因。