系统学习详见OKhttp源码解析详解系列
- 频繁的进行建立Sokcet连接(TCP三次握手)和断开Socket(TCP四次分手)是非常消耗网络资源和浪费时间的,HTTP中的keepalive连接对于 降低延迟和提升速度有非常重要的作用。
- 复用连接就需要对连接进行管理,这里就引入了连接池的概念。
- Okhttp支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间),连接池有ConectionPool实现,对连接进行回收和管理。
1 连接池构造方法
- 构造方法中 设置了每个地址的最大空闲连接数maxIdleConnections以及默认每个连接的存活时间keepAliveDurationNs
public final class ConnectionPool {
//最大的空闲连接数--每个地址的最大空闲连接数
private final int maxIdleConnections;
//连接持续时间
private final long keepAliveDurationNs;
//默认每个地址的最大连接数是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);
}
}
}
设置连接池
OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
//创建具有自定义设置的共享实例
public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
static final List DEFAULT_PROTOCOLS = Util.immutableList(
public Builder() {
...
//连接池 管理HTTP和HTTP / 2连接的重用以减少网络延迟。
//默认每个地址的最大连接数是5个
//默认每个连接的存活时间为5分钟
connectionPool = new ConnectionPool();
...
}
}
2 连接池--连接的存储与删除
2.1 双端队列
连接池中维护了一个双端队列Deque来存储连接
private final Deque connections = new ArrayDeque<>();
2.2 连接的存储
将连接加入到双端队列
public final class ConnectionPool {
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
//没有任何连接时,cleanupRunning = false;
// 即没有任何链接时才会去执行executor.execute(cleanupRunnable);
// 从而保证每个连接池最多只能运行一个线程。
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
}
public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
static {
Internal.instance = new Internal() {
...
@Override
public void put(ConnectionPool pool, RealConnection connection) {
pool.put(connection);
}
...
}
}
}
- put方法在ConnectInterceptor-->intercept-->streamAllocation.newStream-->findHealthyConnection-->创建新链接后调用
2.3 连接的清理
2.3.1 连接池中维护类一个线程池,这个线程池中只开启了一个线程用来清理链接。
- 整个方法的流程如下所示:
- 1.查询此连接内部的StreanAllocation的引用数量。
- 2.标记空闲连接。
- 3.如果空闲连接超过5个或者keepalive时间大于5分钟,则将该连接清理掉。
- 4.返回此连接的到期时间,供下次进行清理。
- 5.全部都是活跃连接,5分钟时候再进行清理。
- 6.没有任何连接,跳出循环。
- 7.关闭连接,返回时间0,立即再次进行清理。
public final class ConnectionPool {
/**
* Background threads are used to cleanup expired connections. There will be at most a single
* thread running per connection pool. The thread pool executor permits the pool itself to be
* garbage collected.
* 后台线程用于清理过期的连接。 每个连接池最多只能运行一个线程。 线程池执行器允许池本身被垃圾收集。
*/
private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
new SynchronousQueue(), Util.threadFactory("OkHttp ConnectionPool", true));
//corePoolSize=0 maximumPoolSize=Integer.MAX_VALUE 使用SynchronousQueue直接提交队列,
// 在执行execute会立马交由复用的线程或新创建线程执行任务
//清理连接,在线程池executor里调用。
private final Runnable cleanupRunnable = new Runnable() {
@Override
public void run() {
while (true) {
//执行清理,并返回下次需要清理的时间。
// 没有任何连接时,cleanupRunning = false;
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
//在timeout时间内释放锁
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
2.3.2 cleanup(long now)
在符合条件下将连接清理出双端队列。
//对该池执行维护,如果它超出保持活动限制或空闲连接限制,则驱逐空闲时间最长的连接。
//返回纳秒级的持续时间,直到下次预定调用此方法为止。 如果不需要进一步清理,则返回-1。
long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
// Find either a connection to evict, or the time that the next eviction is due.
synchronized (this) {
//遍历所有的连接,标记处不活跃的连接。
for (Iterator i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
// If the connection is in use, keep searching.
//1. 查询此连接内部的StreanAllocation的引用数量。
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
idleConnectionCount++;
// If the connection is ready to be evicted, we're done.
//2. 标记空闲连接。
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// We've found a connection to evict. Remove it from the list, then close it below (outside
// of the synchronized block).
//3. 如果空闲连接超过5个或者keepalive时间大于5分钟,则将该连接清理掉。
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// A connection will be ready to evict soon.
//4. 返回此连接的到期时间,供下次进行清理。
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// All connections are in use. It'll be at least the keep alive duration 'til we run again.
//5. 全部都是活跃连接,5分钟时候再进行清理。
return keepAliveDurationNs;
} else {
// No connections, idle or in use.
//6. 没有任何连接,跳出循环。
cleanupRunning = false;
return -1;
}
}
//7. 关闭连接,返回时间0,立即再次进行清理。
closeQuietly(longestIdleConnection.socket());
// Cleanup again immediately.
return 0;
}
2.3.3查询此连接内部的StreanAllocation的引用数量(连接是否可用)
在RealConnection里有个StreamAllocation虚引用列表,每创建一个StreamAllocation,就会把它添加进该列表中,如果留关闭以后就将StreamAllocation
对象从该列表中移除,正是利用利用这种引用计数的方式判定一个连接是否为空闲连接,
public final class RealConnection extends Http2Connection.Listener implements Connection {
/**
* Current streams carried by this connection.
* 由此连接携带的当前流。
*/
public final List> allocations = new ArrayList<>();
}
pruneAndGetAllocationCount
public final class ConnectionPool {
/**
* Prunes any leaked allocations and then returns the number of remaining live allocations on
* {@code connection}. Allocations are leaked if the connection is tracking them but the
* application code has abandoned them. Leak detection is imprecise and relies on garbage
* collection.
* *修剪任何泄漏的分配,然后返回{@code connection}上剩余的实时分配数量。
* 如果连接正在跟踪它们,但是应用程序代码已经放弃它们,则分配会泄漏。
* 泄漏检测不准确,依靠垃圾收集。
*/
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
//虚引用列表
List> references = connection.allocations;
//遍历虚引用列表
for (int i = 0; i < references.size(); ) {
Reference reference = references.get(i);
//如果虚引用StreamAllocation正在被使用,则跳过进行下一次循环,
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引用
references.remove(i);
connection.noNewStreams = true;
// If this was the last allocation, the connection is eligible for immediate eviction.
// 如果所有的StreamAllocation引用都没有了,返回引用计数0
if (references.isEmpty()) {
connection.idleAtNanos = now - keepAliveDurationNs;
return 0;
}
}
//返回引用列表的大小,作为引用计数
return references.size();
}
}
参考
https://juejin.im/post/5a704ed05188255a8817f4c9#heading-12