一、引言
在现代网络应用开发中,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
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() {
@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 {
// 等待指定的时间后再次清理
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
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
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
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
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
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() {
@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 {
// 等待指定的时间后再次清理
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
在构造函数中,会根据传入的参数设置最大空闲连接数和连接的最大空闲时间。然后创建一个新的线程,并将 cleanupRunnable
作为线程的任务。cleanupRunnable
是一个无限循环,会不断调用 cleanup
方法来清理连接池。如果 cleanup
方法返回的等待时间大于 0,则线程会进入等待状态,等待指定的时间后再次进行清理。
4.2.2 清理逻辑的实现
cleanup
方法是连接池清理的核心方法,它会遍历连接池中的所有连接,计算连接的空闲时间,并根据配置的参数决定是否移除最长空闲的连接。以下是 cleanup
方法的详细实现:
java
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
java
OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES))
.build();
在这个例子中,将最大空闲连接数设置为 10。
5.1.2 连接的最大空闲时间
keepAliveDuration
参数用于设置连接的最大空闲时间。当连接的空闲时间超过该值时,连接池会清理该连接。同样可以通过 ConnectionPool
类的构造函数来配置该参数,例如:
java
java
OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(5, 10, TimeUnit.MINUTES))
.build();
在这个例子中,将连接的最大空闲时间设置为 10 分钟。