我们都知道HTTP协议采用请求-应答模式,为了解决TCP握手和挥手效率的问题,HTTP有一个keepalive模式。
当使用普通模式(非KeppAlive模式)时,每个请求-应答都要新建一个连接,完成后立即断开连接(HTTP是无连接的协议)
当使用KeepAlive模式(又称持久连接、连接重用)时,KeepAlive功能使客户端到服务器连接持续有效,避免了频繁地重新建立连接。
http 1.0中默认是关闭的,需要在http头加入”Connection: Keep-Alive”,才能启用Keep-Alive;http 1.1中默认启用Keep-Alive,如果加入”Connection: close “,才关闭。目前大部分浏览器都是用http1.1协议,也就是说默认都会发起Keep-Alive的连接请求。
HTTP Keep-Alive模式
谈HTTP的KeepAlive
OKHTTP默认支持5个Socket,默认KeepAlive的时间为5分钟,对于连接池的管理通过ConnectionPool来实现。
public final class ConnectionPool {
//线程池
private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
new SynchronousQueue(), Util.threadFactory("OkHttp ConnectionPool", true));
//空闲的socket最大连接数 (默认构造方法会赋值为5)
private final int maxIdleConnections;
//socket的keepAlive时间 (默认构造方法会赋值为5)
private final long keepAliveDurationNs;
//连接队列
private final Deque connections = new ArrayDeque<>();
//记录连接失败的线路名单
final RouteDatabase routeDatabase = new RouteDatabase();
boolean cleanupRunning;
//...
}
其中,通过put和get方法来放入连接和获取连接。
先来看put方法,可以看到,在加入到connections之前会先调用cleanupRunnable这个runnable清理空闲的线程。
void put(RealConnection connection) {
assert (Thread.holdsLock(this)); //检测线程是否拥有锁
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
再来看get方法,这里会遍历整个connections集合,当次数小于限制的大小,并且request的地址的缓存列表中此连接的地址完全匹配时,则直接复用缓存列表中的connection作为request的连接。
RealConnection get(Address address, StreamAllocation streamAllocation) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.allocations.size() < connection.allocationLimit
&& address.equals(connection.route().address)
&& !connection.noNewStreams) {
streamAllocation.acquire(connection);
return connection;
}
}
return null;
}
public void acquire(RealConnection connection) {
connection.allocations.add(new WeakReference<>(this));
}
之前提到在put方法的时候,会先调用cleanupRunnable来清理空闲的线程。
而OKHTTP是根据StreamAllocation引用计数是否为0来实现自动回收连接的。引用计数通过StreamAllocation的acquire和release方法来完成,实际上是在改动RealConnection的allocations列表的大小。
public void acquire(RealConnection connection) {
connection.allocations.add(new WeakReference<>(this));
}
private void release(RealConnection connection) {
for (int i = 0, size = connection.allocations.size(); i < size; i++) {
Reference reference = connection.allocations.get(i);
if (reference.get() == this) {
connection.allocations.remove(i);
return;
}
}
throw new IllegalStateException();
}
再来看cleanupRunnable
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方法来进行清理,并返回下次需要清理的间隔时间,然后调用wait方法进行等待一释放锁和时间片。当等待时间到了后,再次进行清理,并返回下次要清理的时间间隔,如此循环下去。
再来看cleanup方法,首先要明确的longestIdleDurationNs是空闲连接的keepAlive存活时间,idleConnectionCount是空闲连接数,inUseConnectionCount时活跃连接数。
long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
synchronized (this) {
for (Iterator i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
//如果查询后,活跃数>0,则inUseConnectionCount+1
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
idleConnectionCount++;
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
//如果longestIdleDurationNs(空闲连接的keepAlive)超过5分钟或者
//idleConnectionCount(空闲连接数)超过5个,则从Deque中移除此连接
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
//返回此连接即将到期的时间
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
//如果活跃连接数>0,则返回默认的keepAlive时间5分钟
return keepAliveDurationNs;
} else {
//没有任何连接,跳出循环并返回-1
cleanupRunning = false;
return -1;
}
}
closeQuietly(longestIdleConnection.socket());
// Cleanup again immediately.
return 0;
}
cleanup方法主要就是根据连接(connection)中的引用计数来计算空闲连接数和活跃连接数,然后标记出空闲的连接。
再来看一下pruneAndGetAllocationCount,这个方法是用于获取当前连接引用的计数。
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
List> references = connection.allocations;
for (int i = 0; i < references.size(); ) { //遍历
Reference reference = references.get(i);
if (reference.get() != null) { //如果被使用中,则遍历下一个
i++;
continue;
}
// 如果未被使用,则从列表中移除
Internal.logger.warning("A connection to " + connection.route().address().url()
+ " was leaked. Did you forget to close a response body?");
references.remove(i);
connection.noNewStreams = true;
// 如果列表为空,则说明连接没有引用了,这时返回0,表示此连接是空闲连接。
if (references.isEmpty()) {
connection.idleAtNanos = now - keepAliveDurationNs;
return 0;
}
}
//返回列表的size,说明还有这几个地方引用着这个连接,表示此连接是活跃连接
return references.size();
}
OKHTTP根据HTTP的KeepAlive头域,来判断缓存。
通过一个专门管理连接池的ConnectionPool类,来保存和获取连接,并会每隔一段时间清理过时的连接。