OkHttp 连接池模块原理深度剖析

一、引言

在现代网络应用开发中,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 连接池模块的时序图

sequenceDiagram participant App participant StreamAllocation participant ConnectionPool participant RealConnection App->>StreamAllocation: 发起请求,调用 allocateConnection() StreamAllocation->>ConnectionPool: 调用 get() 方法获取连接 ConnectionPool->>RealConnection: 遍历连接池,调用 isEligible() 判断连接是否可用 alt 有可用连接 RealConnection-->>ConnectionPool: 返回可用连接 ConnectionPool-->>StreamAllocation: 返回可用连接 StreamAllocation->>RealConnection: 分配流,更新分配数量 else 无可用连接 ConnectionPool-->>StreamAllocation: 返回 null StreamAllocation->>RealConnection: 创建新的连接 StreamAllocation->>ConnectionPool: 调用 put() 方法添加新连接 end App->>StreamAllocation: 请求完成,调用 release() 方法释放连接 StreamAllocation->>RealConnection: 调用 closeIfOwnedBy() 方法关闭连接 loop 定期清理 ConnectionPool->>ConnectionPool: 清理线程调用 cleanup() 方法 ConnectionPool->>RealConnection: 遍历连接池,计算空闲时间 ConnectionPool->>RealConnection: 根据条件移除过期连接 end

从时序图中可以清晰地看到连接池模块的工作流程。应用程序发起请求后,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 分钟。

相关推荐
stevenzqzq30 分钟前
android paging使用教程
android
无敌发光大蟒蛇1 小时前
MySQL第一次作业
android·数据库·mysql
m0_748238922 小时前
MySQL Workbench菜单汉化为中文
android·数据库·mysql
技术蔡蔡2 小时前
Android多线程开发之线程安全
android·面试
三少爷的鞋2 小时前
深入理解 Kotlin 协程的挂起与恢复机制
android
pengyu2 小时前
系统化掌握Flutter开发之隐式动画(一):筑基之旅
android·flutter·dart
居然是阿宋2 小时前
UDP学习笔记(一)为什么UDP需要先将数据转换为字节数组
android·udp·kotlin
LLLibra1465 小时前
Android不安装证书不设置代理如何抓包?
android
_一条咸鱼_5 小时前
Android Glide 请求构建与管理模块原理深入剖析
android
_一条咸鱼_5 小时前
OkHttp 之缓存模块原理剖析
android