Android Okhttp ConnectionPool 笔记

🎯 为什么需要连接池?

在没有连接池的年代,每次 HTTP 请求都要经历:

  1. DNS 解析(域名转 IP)
  2. TCP 三次握手(建立连接)
  3. TLS 握手(如果是 HTTPS)
  4. 发送请求、接收响应
  5. 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 连接。它封装了 SocketConnectionSpecHandshake 等信息。关键字段:

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(用于编码/解码请求和响应)。StreamAllocationnewStream() 方法会调用 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,还能在遇到连接问题(如连接泄漏、超时)时快速定位原因。

相关推荐
城东米粉儿3 小时前
Android Retrofit 笔记
android
城东米粉儿3 小时前
Android Retrofit 线程切换 笔记
android
城东米粉儿5 小时前
Kotlin @JvmOverLoads 笔记
android
alexhilton5 小时前
把离线AI代理装进口袋里
android·kotlin·android jetpack
哈哈浩丶6 小时前
ATF (ARM Trusted Firmware) -2:完整启动流程(冷启动)
android·linux·arm开发·驱动开发
哈哈浩丶6 小时前
ATF (ARM Trusted Firmware) -3:完整启动流程(热启动)
android·linux·arm开发
哈哈浩丶6 小时前
OP-TEE-OS:综述
android·linux·驱动开发
恋猫de小郭16 小时前
你是不是觉得 R8 很讨厌,但 Android 为什么选择 R8 ?也许你对 R8 还不够了解
android·前端·flutter
城东米粉儿17 小时前
Android Glide 笔记
android