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,还能在遇到连接问题(如连接泄漏、超时)时快速定位原因。

相关推荐
Lstone736426 分钟前
Bitmap深入分析(一)
android
一起搞IT吧1 小时前
Android功耗系列专题理论之十四:Sensor功耗问题分析方法
android·c++·智能手机·性能优化
ByNotD0g2 小时前
Doris 学习笔记
android·笔记·学习
修炼者2 小时前
【Android进阶】 RenderEffect的底层实现
android
bropro2 小时前
MySQL不使用子查询的原因
android·数据库·mysql
执笔论英雄3 小时前
【cuda】 pinpaged
android·java·数据库
新青年.3 小时前
Android(Compose)使用 LibVLC 播放 RTSP 视频流
android
一见4 小时前
WorkBuddy安装Skill的方法
android·java·javascript
毛骗导演4 小时前
万字解析 OpenClaw 源码架构-跨平台应用之Android 应用
android·前端·架构