okhttp实现连接池原理
为啥需要
对于tcp/ip的网络请求,是短连接请求,每次理论上是需要三次握手和四次挥手的。频繁的进行建立Sokcet连接和断开Socket是非常消耗网络资源和浪费时间的。
然后HTTP的keep-alive也是用来优化的连接的。
- 普通的HTTP请求是客户端连接到服务端了,请求结束后关闭连接。流程是反复的。
- 带keep-alive头HTTP请求,服务端接受到这个字段,在一定的时间内,会维持这次连接,这期间的请求不会再次连接,而是直接复用了。
由此可见网络请求的优化是至关重要的。而okhttp则是采用连接池进行复用,减少连接的创建和关闭,增加系统负载能力。
如何实现
先说结论:OkHttp里面使用ConnectionPool
实现连接池,而且默认支持5个并发KeepAlive,默认链路生命为5分钟。
先看初始化位置,okhttp采用的构建者模式创建的,其中build方法有如下:
public Builder() {
dispatcher = new Dispatcher();
protocols = DEFAULT_PROTOCOLS;
connectionSpecs = DEFAULT_CONNECTION_SPECS;
eventListenerFactory = EventListener.factory(EventListener.NONE);
proxySelector = ProxySelector.getDefault();
if (proxySelector == null) {
proxySelector = new NullProxySelector();
}
cookieJar = CookieJar.NO_COOKIES;
socketFactory = SocketFactory.getDefault();
hostnameVerifier = OkHostnameVerifier.INSTANCE;
certificatePinner = CertificatePinner.DEFAULT;
proxyAuthenticator = Authenticator.NONE;
authenticator = Authenticator.NONE;
//创建默认连接池,也可以指定其他参数
connectionPool = new ConnectionPool();
dns = Dns.SYSTEM;
followSslRedirects = true;
followRedirects = true;
retryOnConnectionFailure = true;
callTimeout = 0;
connectTimeout = 10_000;
readTimeout = 10_000;
writeTimeout = 10_000;
pingInterval = 0;
}
跟进看ConnectionPool
构造方法:
/**
* 使用适合于单用户应用程序的调整参数创建一个新的连接池。 *此池中的调整参数可能会在将来的OkHttp版本中更改。当前*该池最多可容纳5个空闲连接,这些空闲连接在闲置5分钟后将被驱逐。
**/
public ConnectionPool() {
this(5, 5, TimeUnit.MINUTES);
}
public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
this.maxIdleConnections = maxIdleConnections;
this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);
// Put a floor on the keep alive duration, otherwise cleanup will spin loop.
if (keepAliveDuration <= 0) {
throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
}
}
构造中默认设置了并发数量为5个,默认存活时间为5分钟。
再看下类中静态的变量,随着类创建一起创建的,如下:
/**
* 后台线程用于清除过期的连接。每个连接池最多只能运行一个*线程。线程池执行程序允许对池本身进行*垃圾收集。
*/
private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));
这里就是创建了一个线程池,只会执行一种线程,是用来执行连接池清理任务的。任务执行内容等会再看。
类中还有个关键的全局变量,双端队列Deque,双端都能进出,用来存储连接的:
private final Deque<RealConnection> connections = new ArrayDeque<>();
再来说下是如何实现的:
put
函数:
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
//没有连接的时候调用
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
RealConnection
是双端队列connections存放的连接,先判断是否在锁状态后,继续判断是否在清理状态,cleanupRunning
首先初始化是false值,也是无连接时候。执行cleanupRunnable
只会在无连接、空闲或者使用时才会把cleanupRunning
置成false,因为放入的连接后,连接池可能超出最大规定容量或者存在存活时间超时情况,所以要执行cleanupRunnable
。最后会将这次连接放入双端队列中。
get
函数
@Nullable 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;
}
遍历队列集合,获取匹配且可用的连接并返回,没有则返回null。
cleanup
函数
前面提到的线程池来执行的任务,就是不停的调用cleanupRunnable
来清楚线程池。
先看Runnable
执行代码块:
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) {
}
}
}
}
}
};
cleanup
方法会返回休眠时间,让出cpu节省资源。
看cleanup
方法:
/**
* 在此池上执行维护,如果超过了“保持活动”限制或“空闲连接”限制,则驱逐最长的空闲连接。 <p>以nanos为单位返回睡眠时间,直到下一次计划调用此方法为止。如果不需要进一步清理,则返回-1
* -1 不需要进一步处理
*/
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();
// 如果连接在使用,则跳过继续寻找,inUseConnectionCount++
if (pruneAndGetAllocationCount(connection, now) > 0) {
//使用计数
inUseConnectionCount++;
continue;
}
//空闲计数
idleConnectionCount++;
// 如果准备好退出连接,那么我们就完成了。
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
//上面是找到了最长空闲时间连接
//keepAliveDurationNs 是构造定义的最大维持存活时间
//maxIdleConnections 是构造定义的最大存活数量
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// 符合条件,就从队列移除
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// 如果还有连接未清除,那么现在的线程的等待休眠时间
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// 所有连接都在使用中。直到我们再次运行,这至少是保持生命的持续时间。
return keepAliveDurationNs;
} else {
// 没有连接在使用
cleanupRunning = false;
return -1;
}
}
//真正的关闭socket连接
closeQuietly(longestIdleConnection.socket());
// Cleanup again immediately.
return 0;
}
那如何判断是闲置连接了?
再来看下get
函数中的一句代码:streamAllocation.acquire(connection, true);
//使用此分配来保存connection。每个对此的调用必须与同一连接上对{@link #release}的调用配对。
public void acquire(RealConnection connection, boolean reportedAcquired) {
assert (Thread.holdsLock(connectionPool));
if (this.connection != null) throw new IllegalStateException();
this.connection = connection;
this.reportedAcquired = reportedAcquired;
connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}
acquire
函数,做的就是add操作。
对比看下release
函数,其实是allocations
移除StreamAllocationReference
操作
/** Remove this allocation from the connection's list of allocations. */
private void release(RealConnection connection) {
for (int i = 0, size = connection.allocations.size(); i < size; i++) {
Reference<StreamAllocation> reference = connection.allocations.get(i);
if (reference.get() == this) {
connection.allocations.remove(i);
return;
}
}
throw new IllegalStateException();
}
其中StreamAllocationReference
是弱引用,RealConnection
中,有一个StreamAllocation引用列表allocations
。每创建一个连接,就会把连接对应的StreamAllocationReference添加进该列表中,如果连接关闭以后就将该对象移除。这样可以根据弱引用的堆栈回收信息来计数。
再看cleanup
函数中,确定计数的函数pruneAndGetAllocationCount
/**
*整理所有泄漏的分配,然后返回* {@code connection}上剩余的实时分配数。如果连接正在跟踪分配,但是应用程序代码已放弃分配,则分配将泄漏。泄漏检测不精确,并且依赖于垃圾收集。
*/
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
//拿到了上文提到的引用列表 allocations
List<Reference<StreamAllocation>> references = connection.allocations;
for (int i = 0; i < references.size(); ) {
Reference<StreamAllocation> reference = references.get(i);
if (reference.get() != null) {
i++;
continue;
}
// We've discovered a leaked allocation. This is an application bug.
StreamAllocation.StreamAllocationReference streamAllocRef =
(StreamAllocation.StreamAllocationReference) reference;
String message = "A connection to " + connection.route().address().url()
+ " was leaked. Did you forget to close a response body?";
Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);
//被回收了或者一些其他情况,导致StreamAllocation为null,然后从集合移除此引用
references.remove(i);
connection.noNewStreams = true;
// 如果这是最后一次分配,则该连接可以立即驱逐。
if (references.isEmpty()) {
connection.idleAtNanos = now - keepAliveDurationNs;
return 0;
}
}
//返回计数的size
return references.size();
}
这里配合前面的弱引用,完成计数。
其实可以这样理解,在上层反复调用acquire和release函数,来增加或减少connection.allocations所维持的集合的大小,到最后如果size大于0,则代表RealConnection还在使用连接,如果size等于0,那就说明已经处于空闲状态了。
怎么获取?
get
函数怎么调用的,其实很简单,往上找代码就行了,简单说说。