OKhttp的底层用的是Socket连接而不是URLConnection,所以整体来说还是比较复杂的,涉及到Http协议的封装和解封装、TLS/SSL安全协议的封装、Http2的封装等等,但还是非常值得我们去学习一下的,本篇文章不会对这些底层原理有详细的解析,但读完之后至少大概知道Http请求的底层是如何实现的
一.HTTP请求的优化
1.keep-alive机制
我们知道,一个HTTP的请求需要经过三次握手和四次挥手,一般的流程为先tcp握手,然后传输数据,后面释放资源。在HTTP1.0是没有keep-alive机制的,它的流程大概如下:
这种请求方式本来没什么问题,但是在复杂的请求场景中就显示出它的缺点了。比如在一个网页中同时有十几个资源要请求,按照HTTP1.0的做法, 每一个请求都要创建一个tcp连接,这样就创建了十几个tcp连接,而每一个连接都要经过三次握手和四次挥手,这种情景下,资源消耗是很大的,极大得影响了网络请求效率。
针对这种情况,在HTTP1.x中提出了keep-alive机制,即当一个tcp连接请求完成以后不立即释放,如果下一个http请求的host跟上一次的相同,那么就复用上一次的tcp连接,这样就免去了tcp重复的创建和销毁的开销,流程如下
一般在浏览器中,都会保持6~8个keep-alive的socket连接,且保持一定的生命周期,当不需要的时候才会关闭。
2.HTTP/2
HTTP/2致力于解决HTTP1.x中存在的一些不足,HTTP1.x中客户端想发起多个并行的请求就要建立多个tcp连接,HTTP1.x不会压缩请求头和响应头,导致流量的浪费,HTTP1.x不支持资源的优先级传输,所以针对这些问题,HTTP2实现了如下功能:
① 报头压缩:HTTP/2使用HPACK压缩格式压缩请求和响应报头数据,减少了传输流量的输出
② 多路复用:在HTTP1.x中每个tcp连接只能同时处理一个请求-响应,浏览器按照FIFO原则处理请求的执行,上一个请求未完成会阻塞下一个请求的执行。在HTTP/2中,我们可以把一个HTTP请求当作一个流,每个流分解成不同的帧,帧是传输的最小单位,而且每个帧都有标记来识别它属于哪个流,不同流的帧可以在同一个连接中交错发送,然后在另一端重新组装,这样就实现了一个连接可以有多个请求-响应并行执行
③ 数据流的优先级:HTTP/2中每个帧都是可以交错发送的,这样一来发送的顺序就很关键了,我们可以设置每个数据流的权重和依赖关系,这样就可以做到权重高的流优先发送
HTTP/2功能增强的核心是新增了二进制分帧层Binary Framing,它定义了如何封装HTTP消息并在客户端和服务端之间进行传输
HTTP/2的数据传输是以帧的方式交错发送的,如下图示:
二.关键类:ConnectionPool、RealConnection、StreamAllocation
上面讲了http1.x和http2的相关知识,在OKhttp中不管是哪种连接都需要用到连接池来维护,通过连接池可以有效地提高连接的使用效率,连接池的实现类为ConnectionPool
1.ConnectionPool类详解
主要用来管理http1.x或者http/2的链接,该类实现了链接的复用和清理工作,来看看它的主要属性
//清除过期链接的线程池
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));
//最大的空闲链接数和保活时间
private final int maxIdleConnections;
private final long keepAliveDurationNs;
//双端队列,保存链接
private final Deque<RealConnection> connections = new ArrayDeque<>();
final RouteDatabase routeDatabase = new RouteDatabase();
boolean cleanupRunning;
//创建连接池,默认最大空闲连接为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);
}
}
既然是Http链接的管理,那么就会有添加连接、获取连接、删除连接等行为的实现
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;
}
put方法:添加连接
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
//如果连接清除工作已停止,则触发清除工作
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
//将连接添加到连接池
connections.add(connection);
}
每当添加一个新的连接,如果当前连接的清理工作已经停止,则会触发连接的清理工作,把符合清除条件的连接清理掉或者在指定时间删除即将过期的连接,连接清除工作需要启动一个工作线程来执行,来看看
连接的清理:
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
//一个无限循环
while (true) {
//调用leanup方法清除,返回下一次清理的等待时间
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方法
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 the connection is in use, keep searching.
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;