系列索引
本系列文章基于 OkHttp3.14
OkHttp 源码剖析系列(一)——请求的发起及拦截器机制概述
OkHttp 源码剖析系列(二)——拦截器大体流程分析
OkHttp 源码剖析系列(三)——缓存机制分析
OkHttp 源码剖析系列(四)——连接的建立概述
OkHttp 源码剖析系列(五)——路由选择机制
OkHttp 源码剖析系列(六)——连接复用机制及连接的建立
OkHttp 源码剖析系列(七)——请求的发起及响应的读取
前言
当 findConnection
的过程中无法从 transmitter
中取得 Connection
时,会调用 connectionPool.transmitterAcquirePooledConnection
方法来尝试从连接池中获取连接,让我们从这篇文章开始研究一下 OkHttp 中连接池的实现。
HTTP 中的复用机制
HTTP/1.0
在 HTTP/1.0 中,由于 HTTP 协议是一种无连接的网络协议,进行一次 HTTP 请求是这样的一条流程:
这样设计可以保证每条 HTTP 请求都是独立的,互不干扰。但这样的设计有一个致命的缺点——如果我们向同一个服务器发起数十个 HTTP 请求,则我们的每条 HTTP 请求都需要与这个服务器建立一条 TCP 连接。而我们知道,建立 TCP 连接需要经过三次握手,而关闭 TCP 连接则需要四次挥手,可想而知这样频繁地建立与关闭 TCP 连接对网络资源的消耗是十分严重的,极大地降低了网络的效率,并且提高了服务器的压力。
在 HTTP/1.0 中存在一个名为 Connection:Keep-Alive
的 Header,但没有官方的标准规定其工作机制,它默认是关闭的,可以通过在 Header 中加入从而开启。当客户端及服务端都对 Keep-Alive
机制支持时,就可以维持该 TCP 连接从而使得下一次可以进行复用。
HTTP/1.1
而在 HTTP/1.1 中,真正引入了 Keep-Alive
机制,它默认是开启的,可以通过 Connection:close
进行关闭。在 HTTP 请求结束时,若启动了 Keep-Alive
机制,则该连接并不会立即关闭,此时如果有新的请求到来,且 host 相同,则会复用这条 TCP 连接进行请求,减少了 TCP 连接的频繁建立与关闭的资源消耗。
通过这样的连接复用的做法,可以大幅地减少对资源的消耗,如下图所示:
同时,在 HTTP/1.1 中还引入了 Keep-Alive
请求头,在其中可以设定两个值:timeout
与 max
,从而设定这个连接何时被关闭。
-
timeout
:指定了一个空闲连接需要保持打开状态的最小时长(以秒为单位) -
max
:在连接关闭之前,在此连接可以发送的请求的最大值
但这样就存在了一个问题,在原来不采用 Keep-Alive
的时候,客户端可以通过 TCP 连接是否关闭来判断数据是否接收完成,但在采用了 Keep-Alive
的情况下,客户端如何才能得知自己需要的数据已经接收完毕了呢?
Content-Length
看过我之前的多线程下载的实现博文的读者,应该知道在服务端的 Response
的 Header
中,会包含 Content-Length
这一字段,它表示了实体内容的长度(比如文件 / 图片的大小),通过该字段客户端就可以确定自己需要接受的字节数。从而确认数据已接收完成。
Transfer-Encoding:chunked
前面的 Content-Length
看上去完美解决了无法判断数据接收完毕的问题。但对于一些动态的场景,比如一些动态页面,服务端是无法预先知道该页面的大小的,在该页面创建完成前,其长度是不可知的,服务端也就无法返回一个确切的 Content-Length
字段给客户端了,只能开启一个足够大的 buffer。
此时,就可以采用 Transfer-Encoding:chunked
来实现,它表示一种分块编码的意思,它只在 HTTP/1.1 中提供,允许服务端将发送给客户端的数据分成多个部分。
如果使用了分块编码,则请求及响应有以下的特点:
- 在 Header 中加入
Transfer-Encoding:chunked
,表示使用分块编码 - 每一个分块有两行,每一行都以
\r\n
结尾,第一行表示这个分块的数据长度,是一个十六进制的数(不包括数据结尾的\r\n
),第二行则是这个分块的具体数据。 - 最后一个分块长度为0,且数据没有内容,表示整个实体的结束。
HTTP/2
在前面的 HTTP/1.1 中,虽然实现了 TCP 连接的复用,但仍有如下几个缺陷:
- 如果客户端想要发起并行的请求,则必须建立多个 TCP 连接,这对网络资源的消耗也是十分严重的。
- 不会读对请求及响应的 Header 进行压缩,造成了网络流量的浪费。
- 不支持资源优先级导致 TCP 连接利用率低下。
多路复用
为了解决上面几个问题,HTTP/2 引入了多路复用机制,同时引入了几个新的概念:
- 数据流:基于 TCP 连接上的一个双向的字节流,每发起一个请求,就会建立一个数据流,后续的请求过程的数据传递都通过该流进行
- 数据帧:HTTP/2 中的数据最小切片单位,其中又分为了
Header Frame
、Data Frame
等等。 - 消息:一个请求或响应对应的一系列数据帧。
引入了这些概念之后,在 HTTP 请求的过程中,服务端/客户端首先会将我们的请求/响应切分为不同的数据帧,当另一方接收到后再将其组装从而形成完整的请求/响应,如下所示
这样,就实现了对 TCP 连接的多路复用,将一个请求或响应分为了一个个的数据帧,使得多个请求可以并行地进行。
多路复用与 Keep-Alive 的区别
- Keep-Alive 机制虽然解决了复用 TCP 连接问题,但没有解决请求阻塞的问题,需要等到上一个请求结束后,才能复用该 TCP 连接进行下一个请求。
- HTTP/1.x 对数据的传递仍然是以一个整体进行传递,而在 HTTP/2 中引入了数据帧的概念,使得多个请求可以同时在流中进行传递。
- HTTP/2 采用了 HPACK 压缩算法对 Header 进行压缩,降低了请求的流量消耗。
OkHttp 中的复用机制
前面提了 HTTP 中的复用机制,通过对 TCP 连接的复用,大幅提高了网络请求的效率。无论是 HTTP/1.1 中的 Keep-Alive
还是 HTTP/2 中的多路复用,都需要连接池来维护 TCP 连接,让我们看看 OkHttp 中连接池的实现。
我们知道,在 findConnection
过程中,若无法从 transimitter
中获取到连接,则会尝试从连接池中获取连接。
我们可以看到 RealConnectionPool.connections
,它是一个 Deque
,保存了所有的连接:
private final Deque connections = new ArrayDeque<>();
连接清理机制
同时会发现,在这个类中还存在着一个 executor
,它的设置与 OkHttp 用于异步请求的线程池的设置几乎一样,它是用来做什么的呢?
/**
* 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));
通过上面的注释可以看出,它是用来执行清理过期连接的任务的,并且最多每个连接池只会有一个线程在执行清理任务。这个清理的任务就是下面的 cleanupRunnable
:
private final Runnable cleanupRunnable = () -> {
while (true) {
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (RealConnectionPool.this) {
try {
RealConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
};
可以看到它是采用一个循环的方式调用 cleanup
方法进行清理,并从返回值中获取了需要 wait 的秒数,调用 wait
方法进入阻塞,也就是说每次清理的间隔由 cleanup
的返回值进行决定。
我们看到 cleanup
方法:
/**
* Performs maintenance on this pool, evicting the connection that has been idle the longest if
* either it has exceeded the keep alive limit or the idle connections limit.
*
* Returns the duration in nanos to sleep until the next scheduled call to this method. Returns
* -1 if no further cleanups are required.
*/
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();
// 统计连接被引用的transimitter的个数,若大于0则说明是正在使用的连接
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
// 否则是空闲连接
idleConnectionCount++;
// 找出空闲连接中空闲时间最长的连接
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// 如果发现空闲时间最久的连接所空闲时间超过了Keep-Alive设定的时间,或者是空闲连接数超过了最大空闲连接数
// 将前面的其从队列中删除,并且在之后对其socket进行关闭
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// 返回离达到keep-alive设定的时间的距离,将在达到时执行进行清理
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// 如果当前连接都是正在使用的,返回keep-alive所设定的时间
return keepAliveDurationNs;
} else {
// 没有连接了,停止运行cleanup
cleanupRunning = false;
return -1;
}
}
// 关闭空闲最久的连接,继续尝试清理
closeQuietly(longestIdleConnection.socket());
return 0;
}
可以看到,主要是下面几步:
- 调用
pruneAndGetAllocationCount
方法统计连接被引用的数量,大于 0 说明连接正在被使用 - 通过上面的方法统计空闲连接数及正在使用的连接数,并从中找出空闲最久的连接
- 若空闲最久的连接空闲的时间超过了所设定的
keepAliveDurationNs
(这里不是指的Keep-Alive
所设定时间),或者空闲连接数超过了所设定的maxIdleConnections
,清理该连接(移除并关闭socket),并返回 0 表示立即继续清理。 - 若还未超过,则返回下一次超过外部设定的
keepAliveDurationNs
,表示等到下次超时的时候再进行清理 - 若当前连接都正处于使用中,返回所设定的
keepAliveDurationNs
- 若当前没有连接,则将
cleanupRunning
置为 false 停止清理
在 OkHttp 中,将空闲连接的最长存活时间设定为了 5 分钟,并且将最大空闲连接数设置为了 5
我们看看 pruneAndGetAllocationCount
是如何对连接被引用的数量进行统计的:
/**
* Prunes any leaked transmitters and then returns the number of remaining live transmitters on
* {@code connection}. Transmitters are leaked if the connection is tracking them but the
* application code has abandoned them. Leak detection is imprecise and relies on garbage
* collection.
*/
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
List> references = connection.transmitters;
for (int i = 0; i < references.size(); ) {
Reference reference = references.get(i);
if (reference.get() != null) {
i++;
continue;
}
// We've discovered a leaked transmitter. This is an application bug.
TransmitterReference transmitterRef = (TransmitterReference) reference;
String message = "A connection to " + connection.route().address().url()
+ " was leaked. Did you forget to close a response body?";
Platform.get().logCloseableLeak(message, transmitterRef.callStackTrace);
references.remove(i);
connection.noNewExchanges = true;
// If this was the last allocation, the connection is eligible for immediate eviction.
if (references.isEmpty()) {
connection.idleAtNanos = now - keepAliveDurationNs;
return 0;
}
}
return references.size();
}
可以看到,connection
中是有维护一个引用它的 Transmitter
的 Reference
队列的,通过遍历并判断该 Transimitter
是否为 null 即可进行统计。这里的 Reference
所存的实际是一个继承自 WeakReference
的 TransimitterReference
类:
static final class TransmitterReference extends WeakReference {
// ...
}
可以发现,这种设计有点像 JVM 中的引用计数法 + 标记清除,实际上就是 OkHttp 仿照 JVM 的垃圾回收设计了这样一种类似引用计数法的方式来统计一个连接是否是空闲连接,同时采用标记清除法对空闲且不满足设定的规则的连接进行清除。
获取连接
我们看到 connectionPool.transmitterAcquirePooledConnection
方法,了解一下连接池获取连接的过程:
/**
* Attempts to acquire a recycled connection to {@code address} for {@code transmitter}. Returns
* true if a connection was acquired.
*
* If {@code routes} is non-null these are the resolved routes (ie. IP addresses) for the
* connection. This is used to coalesce related domains to the same HTTP/2 connection, such as
* {@code square.com} and {@code square.ca}.
*/
boolean transmitterAcquirePooledConnection(Address address, Transmitter transmitter,
@Nullable List routes, boolean requireMultiplexed) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (requireMultiplexed && !connection.isMultiplexed()) continue;
if (!connection.isEligible(address, routes)) continue;
transmitter.acquireConnectionNoEvents(connection);
return true;
}
return false;
}
可以看到,首先注释中对我们传入不同的 routes
参数进行了解释,若 routes
不为 null 说明这是已解析过的路由,可以将其合并到同一个 HTTP/2 连接。
而在 connection.isMultiplexed
的注释中说到,若该连接为 HTTP/2 连接,则会返回 true。
在 connection.isEligible
注释中则说到,若该连接可以给对应的 address
分配 stream,则返回 true。
在代码中,对 connections
进行了遍历:
当需要进行多路费用且当前的连接不是 HTTP/2 连接时,则放弃当前连接
当当前连接不能用于为
address
分配 stream,则放弃当前连接。前两者都不满足,则获取该连接,并设置到
transimitter
中。
三次获取连接的区别
我们回顾一下 findConnection
中三次尝试从连接池获取连接的过程:
第一次尝试:
connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)
第二次尝试(需要在进行了路由选择的情况下):
connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)
第三次尝试:
connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)
可以发现,其传入的参数是不同的。第一次由于是尝试从已经解析过的路由的连接池中获取连接,因此 route
设置为 null。
第二次由于是在无法找到对应的连接,在进行了路由选择的条件下进行的,因此将 route
设置为了 null。
而最后一次尝试从连接池获取连接之所以需要将 requireMultiplexed
设置为 true,因为这次只有可能是在多个请求并行进行的情况下才有可能发生,这种情况只有 HTTP/2 的连接才有可能发生。
加入连接
通过 RealConnectionPool.put
方法可以向连接池中加入连接:
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
由于之前判断了如果连接池中没有连接,就会暂停连接清理线程,所以这里如果放入了新的连接,就会判断连接清理线程是否正在执行,若已停止执行则将其继续执行。之后将该连接放入了 Deque
中。
通知连接空闲
每当外部调用了 Transimitter.releaseConnectionNoEvents
方法时,最后都会调用到 RealConnection.connectionBecameIdle
方法来通知连接池连接进入了空闲状态:
/**
* Notify this pool that {@code connection} has become idle. Returns true if the connection has
* been removed from the pool and should be closed.
*/
boolean connectionBecameIdle(RealConnection connection) {
assert (Thread.holdsLock(this));
if (connection.noNewExchanges || maxIdleConnections == 0) {
connections.remove(connection);
return true;
} else {
notifyAll(); // Awake the cleanup thread: we may have exceeded the idle connection limit.
return false;
}
}
此时如果该连接不支持用于创建新 Exchange
,或不允许有空闲连接,则会直接将该连接移除,否则会通过 notifyAll
方法唤醒阻塞的清理线程,尝试对空闲连接进行清理,这样能保证每当有空闲连接时最及时地对连接池进行清理。
连接的建立
我们知道,在寻找连接的过程中,若从 Transimitter
及连接池中都无法获取到连接时,就会创建一个新的连接,让我们看看这个创建连接的过程是怎样的:
在寻找连接的代码中,创建连接的核心代码如下:
// ...
result = new RealConnection(connectionPool, selectedRoute);
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
我们先看到 RealConnection
的构造函数:
public RealConnection(RealConnectionPool connectionPool, Route route) {
this.connectionPool = connectionPool;
this.route = route;
}
只是进行了简单的赋值,我们接着看到 RealConnection.connect
方法:
public void connect(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
EventListener eventListener) {
if (protocol != null) throw new IllegalStateException("already connected");
RouteException routeException = null;
List connectionSpecs = route.address().connectionSpecs();
ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
// ...一些错误处理
while (true) {
try {
if (route.requiresTunnel()) {
// 如果使用了隧道技术,调用connectTunnel方法
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
if (rawSocket == null) {
// We were unable to connect the tunnel but properly closed down our resources.
break;
}
} else {
// 未使用隧道技术,调用connectSocket方法
connectSocket(connectTimeout, readTimeout, call, eventListener);
}
// 建立协议
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
break;
} catch (IOException e) {
//... 异常下的资源释放
}
}
// ... 一些错误处理
}
可以看到,这里是一个循环,不断尝试建立连接,其中核心步骤如下:
- 若使用了隧道技术,调用
connectTunnel
方法 - 若未使用隧道技术,调用
connectSocket
方法 - 调用
establishProtocol
方法建立协议
让我们看看三个方法分别是如何实现的。
直接连接
我们先看看直接连接是如何实现的,我们看到 connectSocket
方法:
/**
* Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket.
*/
private void connectSocket(int connectTimeout, int readTimeout, Call call,
EventListener eventListener) throws IOException {
Proxy proxy = route.proxy();
Address address = route.address();
// 初始化rawSocket,其中对SOCKS代理采用了SOCKS代理服务器
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy);
eventListener.connectStart(call, route.socketAddress(), proxy);
rawSocket.setSoTimeout(readTimeout);
try {
// 调用connectSocket方法对Socket进行连接,这里预置了不同平台的实现
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
} catch (ConnectException e) {
ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
ce.initCause(e);
throw ce;
}
// 获取source及sink,用于读取及写入
try {
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
} catch (NullPointerException npe) {
if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
throw new IOException(npe);
}
}
}
可以看到,这里主要是进行 Socket
的连接,首先根据代理类型创建了 Socket
,之后调用了 connectSocket
方法进行连接(里面调用的其实仍然是 socket.connect
方法)。最后调用 Okio 的方法获取 source
及 sink
。
这个过程还是比较简单的,和正常使用 Socket 的流程大致相同:创建Socket=>连接=>获取 stream,其中在 connectSocket
时根据不同平台做了不同的处理。
通过隧道连接
首先我们要理解一下什么是隧道。这个其实是计网中的知识,之前在 《计算机网络——自顶向下方法》中看到过,不过书中没有详细介绍,这里刚好学习一下。
隧道技术的出现主要是为了适配 IPv4 到 IPv6 的转变。通过这种隧道技术,可以通过一种网络协议来传输另外一种网络协议的数据,比如 A 主机与 B 主机都是采用 IPv6,而连接 A 与 B 的是 IPv4 的网络,为了实现 A 与 B 的通信,可以使用隧道技术,数据包经过 IPv4 的多协议路由时将 IPv6 的数据包放入 IPv4 的数据包中,传递给 B。当到达 B 的路由器时,数据又被剥离之后传递给 B。这样在 A 与 B 看来,它们使用的都是 IPv6 与对方通信。如下图所示:
那么怎么打开隧道呢?
HTTP 提供了一个特殊的 method
—— CONNECT,它是 HTTP/1.1 协议中预留的方法,可以通过它将连接改为隧道的代理服务器。客户端发送一个 CONNECT 请求给隧道网关请求打开一条 TCP 连接,当隧道打通之后,客户端通过 HTTP 隧道发送的所有数据会转发给 TCP 连接,服务器响应的所有数据会通过隧道发给客户端。
而在 OkHttp 中,对隧道的支持主要是为了支持 SSL 隧道——SSL 隧道的初衷是为了通过防火墙来传输加密的 SSL 数据,此时隧道的作用就是将非 HTTP 的流量(SSL流量)传过防火墙到达指定的服务器(比如 HTTPS)。
接着我们看到 connectTunnel
方法的实现:
/**
* Does all the work to build an HTTPS connection over a proxy tunnel. The catch here is that a
* proxy server can issue an auth challenge and then close the connection.
*/
private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout, Call call,
EventListener eventListener) throws IOException {
// 创建隧道Request
Request tunnelRequest = createTunnelRequest();
HttpUrl url = tunnelRequest.url();
for (int i = 0; i < MAX_TUNNEL_ATTEMPTS; i++) {
// 通过connectSocket建立Socket
connectSocket(connectTimeout, readTimeout, call, eventListener);
// 创建隧道
tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);
// 当创建的隧道为null时,说明隧道成功建立,break
if (tunnelRequest == null) break;
// 回收资源
closeQuietly(rawSocket);
rawSocket = null;
sink = null;
source = null;
eventListener.connectEnd(call, route.socketAddress(), route.proxy(), null);
}
}
这里首先构建了一个隧道的 tunnelRequest
。之后进行了循环,不断尝试建立隧道,不过 OkHttp 限制了其最大尝试次数为 21 次。
建立隧道的过程首先通过 connectSocket
方法建立了 Socket 连接,然后通过 createTunnel
方法建立隧道。
我们看看 createTunnelRequest
方法做了什么:
private Request createTunnelRequest() throws IOException {
Request proxyConnectRequest = new Request.Builder()
.url(route.address().url())
.method("CONNECT", null)
.header("Host", Util.hostHeader(route.address().url(), true))
.header("Proxy-Connection", "Keep-Alive") // For HTTP/1.0 proxies like Squid.
.header("User-Agent", Version.userAgent())
.build();
Response fakeAuthChallengeResponse = new Response.Builder()
.request(proxyConnectRequest)
.protocol(Protocol.HTTP_1_1)
.code(HttpURLConnection.HTTP_PROXY_AUTH)
.message("Preemptive Authenticate")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(-1L)
.header("Proxy-Authenticate", "OkHttp-Preemptive")
.build();
Request authenticatedRequest = route.address().proxyAuthenticator()
.authenticate(route, fakeAuthChallengeResponse);
return authenticatedRequest != null
? authenticatedRequest
: proxyConnectRequest;
}
可以看到,这里构建了一个 method 为 CONENCT 的请求。
我们接着看看 createTunnel
方法又做了什么事情:
/**
* To make an HTTPS connection over an HTTP proxy, send an unencrypted CONNECT request to create
* the proxy connection. This may need to be retried if the proxy requires authorization.
*/
private Request createTunnel(int readTimeout, int writeTimeout, Request tunnelRequest,
HttpUrl url) throws IOException {
// 构造HTTP/1.1请求
String requestLine = "CONNECT " + Util.hostHeader(url, true) + " HTTP/1.1";
while (true) {
Http1ExchangeCodec tunnelCodec = new Http1ExchangeCodec(null, null, source, sink);
source.timeout().timeout(readTimeout, MILLISECONDS);
sink.timeout().timeout(writeTimeout, MILLISECONDS);
tunnelCodec.writeRequest(tunnelRequest.headers(), requestLine);
tunnelCodec.finishRequest();
// 发出隧道请求
Response response = tunnelCodec.readResponseHeaders(false)
.request(tunnelRequest)
.build();
tunnelCodec.skipConnectBody(response);
switch (response.code()) {
case HTTP_OK:
// 返回200说明成功建立隧道,返回null
if (!source.getBuffer().exhausted() || !sink.buffer().exhausted()) {
throw new IOException("TLS tunnel buffered too many bytes!");
}
return null;
case HTTP_PROXY_AUTH: // 表示服务端要进行代理认证
// 进行代理认证
tunnelRequest = route.address().proxyAuthenticator().authenticate(route, response);
// 代理认证不通过
if (tunnelRequest == null) throw new IOException("Failed to authenticate with proxy");
// 代理认证通过,但需要关闭TCP连接
if ("close".equalsIgnoreCase(response.header("Connection"))) {
return tunnelRequest;
}
break;
default:
throw new IOException(
"Unexpected response code for CONNECT: " + response.code());
}
}
}
可以看到,这里主要进行如下的工作:
- 拼接 HTTP/1.1 请求
- 发出隧道请求,读取响应
- 若隧道请求返回 200,说明隧道建立成功,返回 null
- 若隧道返回 407,说明服务器需要进行代理认证,调用对应方法进行代理认证
隧道打通之后,就可以通过隧道进行网络请求了。
发布协议
经过前面的步骤,我们建立了一条与服务端的 Socket 通道,我们接着看到 establishProtocol
方法:
private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
// 如果不是https地址
if (route.address().sslSocketFactory() == null) {
// 如果协议中包含了 http2 with prior knowledge
if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
socket = rawSocket;
protocol = Protocol.H2_PRIOR_KNOWLEDGE;
startHttp2(pingIntervalMillis);
return;
}
// 协议为 HTTP/1.1
socket = rawSocket;
protocol = Protocol.HTTP_1_1;
return;
}
eventListener.secureConnectStart(call);
// TLS握手
connectTls(connectionSpecSelector);
eventListener.secureConnectEnd(call, handshake);
if (protocol == Protocol.HTTP_2) {
// 如果是HTTP2协议,调用 startHttp2 方法
startHttp2(pingIntervalMillis);
}
}
可以看到,这个方法主要是在建立了 Socket 连接的基础上,对各个协议进行支持。
首先判断了当前地址是否是 HTTPS 地址。
不是 HTTPS 的情况下,若协议中包含了 H2_PRIOR_KNOWLEDGE
则采用 HTTP/2 进行请求,调用 startHttp2
方法,否则采用 HTTP/1.1。
是 HTTPS 的情况下,首先调用了 connectTls
方法进行 TLS 握手,之后若是 HTTP/2 协议,则调用 startHttp2
方法。
启动 HTTP/2 连接
让我们先看看 startHttp2
方法究竟是做了什么:
private void startHttp2(int pingIntervalMillis) throws IOException {
socket.setSoTimeout(0); // HTTP/2 connection timeouts are set per-stream.
http2Connection = new Http2Connection.Builder(true)
.socket(socket, route.address().url().host(), source, sink)
.listener(this)
.pingIntervalMillis(pingIntervalMillis)
.build();
http2Connection.start();
}
这里主要是构建了一个 HTTP/2 的 Http2Connection
,并且将 listener
设置为了该 RealConnection
,之后通过 http2Connection.start
方法启动了 HTTP/2 连接。
/**
* @param sendConnectionPreface true to send connection preface frames. This should always be true
* except for in tests that don't check for a connection preface.
*/
void start(boolean sendConnectionPreface) throws
IOException {
if (sendConnectionPreface) {
writer.connectionPreface();
writer.settings(okHttpSettings);
int windowSize = okHttpSettings.getInitialWindowSize();
if (windowSize != Settings.DEFAULT_INITIAL_WINDOW_SIZE) {
writer.windowUpdate(0, windowSize - Settings.DEFAULT_INITIAL_WINDOW_SIZE);
}
}
new Thread(readerRunnable).start(); // Not a daemon thread.
}
这里 sendConnectionPreface
默认为 true,它首先调用了 writer.connectionPreface
方法,之后调用了 writer.settings
方法。最后,启用了一个 readerRunnable
的读取线程。
在 HTTP/2 中,每个终端都需要发送一个连接 preface 作为在使用的协议的一个最终的确认,并为 HTTP/2 连接建立初始的设定。客户端和服务器相互发送一个不同的连接 preface。
连接 preface 以字符串 PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
开始,这个序列后面必须跟着一个 SETTINGS
帧。因此,在之后又调用了 writer.settings
方法,写入 SETTINGS
帧。
我们先看到 connectionPreface
方法:
public synchronized void connectionPreface() throws IOException {
if (closed) throw new IOException("closed");
if (!client) return; // Nothing to write; servers don't send connection headers!
if (logger.isLoggable(FINE)) {
logger.fine(format(">> CONNECTION %s", CONNECTION_PREFACE.hex()));
}
sink.write(CONNECTION_PREFACE.toByteArray());
sink.flush();
}
这里实际上是向 HTTP/2 连接的 Socket 中写入了 PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
这一字符串。之后我们看到 writer.settings
方法:
/**
* Write okhttp's settings to the peer.
*/
public synchronized void settings(Settings settings) throws IOException {
if (closed) throw new IOException("closed");
int length = settings.size() * 6;
byte type = TYPE_SETTINGS;
byte flags = FLAG_NONE;
int streamId = 0;
frameHeader(streamId, length, type, flags);
for (int i = 0; i < Settings.COUNT; i++) {
if (!settings.isSet(i)) continue;
int id = i;
if (id == 4) {
id = 3; // SETTINGS_MAX_CONCURRENT_STREAMS renumbered.
} else if (id == 7) {
id = 4; // SETTINGS_INITIAL_WINDOW_SIZE renumbered.
}
sink.writeShort(id);
sink.writeInt(settings.get(i));
}
sink.flush();
}
这里主要是写入了一些配置的数据,其中调用了 frameHeader
写入了帧头。
最后我们看到 readerRunnable.execute
:
@Override
protected void execute() {
ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR;
ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR;
IOException errorException = null;
try {
reader.readConnectionPreface(this);
while (reader.nextFrame(false, this)) {
}
connectionErrorCode = ErrorCode.NO_ERROR;
streamErrorCode = ErrorCode.CANCEL;
} catch (IOException e) {
errorException = e;
connectionErrorCode = ErrorCode.PROTOCOL_ERROR;
streamErrorCode = ErrorCode.PROTOCOL_ERROR;
} finally {
close(connectionErrorCode, streamErrorCode, errorException);
Util.closeQuietly(reader);
}
}
可以看到,这里主要是调用了 reader.readConnectionPreface
方法读取服务端发送来的 preface,并判断是否为对应字符串,从而完成 HTTP/2 连接的启动。
TLS 握手
接着我们看到 TLS 握手的过程,让我们看看 connectTls
方法:
private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
Address address = route.address();
SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
boolean success = false;
SSLSocket sslSocket = null;
try {
// 基于之前建立的Socket建立一个包装对象SSLSocket
sslSocket = (SSLSocket) sslSocketFactory.createSocket(
rawSocket, address.url().host(), address.url().port(), true /* autoClose */);
// 对TLS相关信息进行配置
ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
if (connectionSpec.supportsTlsExtensions()) {
Platform.get().configureTlsExtensions(
sslSocket, address.url().host(), address.protocols());
}
// 进行握手
sslSocket.startHandshake();
// 获取SSLSession
SSLSession sslSocketSession = sslSocket.getSession();
Handshake unverifiedHandshake = Handshake.get(sslSocketSession);
// 验证证书对该主机是否有效
if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) {
List peerCertificates = unverifiedHandshake.peerCertificates();
if (!peerCertificates.isEmpty()) {
X509Certificate cert = (X509Certificate) peerCertificates.get(0);
throw new SSLPeerUnverifiedException(
"Hostname " + address.url().host() + " not verified:"
+ "\n certificate: " + CertificatePinner.pin(cert)
+ "\n DN: " + cert.getSubjectDN().getName()
+ "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
} else {
throw new SSLPeerUnverifiedException(
"Hostname " + address.url().host() + " not verified (no certificates)");
}
}
address.certificatePinner().check(address.url().host(),
unverifiedHandshake.peerCertificates());
String maybeProtocol = connectionSpec.supportsTlsExtensions()
? Platform.get().getSelectedProtocol(sslSocket)
: null;
socket = sslSocket;
// 获取source及sink
source = Okio.buffer(Okio.source(socket));
sink = Okio.buffer(Okio.sink(socket));
handshake = unverifiedHandshake;
protocol = maybeProtocol != null
? Protocol.get(maybeProtocol)
: Protocol.HTTP_1_1;
success = true;
} catch (AssertionError e) {
if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
} finally {
if (sslSocket != null) {
Platform.get().afterHandshake(sslSocket);
}
if (!success) {
closeQuietly(sslSocket);
}
}
}
可以看到,这里的步骤主要是下列步骤:
- 基于之前建立的
Socket
建立包装类SSLSocket
- 对 TLS 相关信息进行配置
- 通过
SSLSocket
进行握手 - 验证一些证书相关信息
- 获取
source
及sink
总结
OkHttp 中采用了连接池机制实现了连接的复用,避免了每次都创建新的连接从而导致资源的浪费。获取连接的过程主要如下:
- 尝试在
transimitter
中寻找已经分配的连接 -
transimitter
中获取不到,尝试从连接池中获取连接 - 连接池中仍然获取不到,尝试进行一次路由选择,再次从连接池中获取连接
- 连接池中仍然找不到需要的连接,则创建一个新的连接
- 由于 HTTP/2 下采用了连接的多路复用机制,所以连接可以并行进行,因此再次尝试从连接池中获取连接,获取到则丢弃创建的连接
- 若连接池中仍获取不到连接,则将刚刚创建的连接放入连接池
其中,在连接池中采用了一个清理线程对超过了设定参数的空闲连接进行清理,每次清理后会计算下一次需要清理的时间并进入阻塞,每当有新连接进入或连接进入空闲时会重新唤醒该清理线程。
对于每个连接,都采用了一种类似 GC 中的引用计数法的形式,每个 RealConnection
都持有了使用它的 Transimitter
的弱引用,通过判断持有的弱引用个数从而判断该连接是否空闲。
OkHttp 默认将最大存活空闲连接个数设置为了 5,且每个连接空闲时间不能超过 5 分钟,否则将被清理线程所回收。
而在连接建立过程中,首先会判断该连接是否需要 SSL 隧道,若不需要则直接建立了 Socket 并获取了其 source
及 sink
,若需要则会先尝试建立 SSL 隧道,最后再进行 Socket
连接。
Socket
连接建立成功后,会通过 establishProtocol
方法对每个协议进行不同的处理,从而对各个协议进行支持(如对 HTTPS 的支持)
参考资料
Keep-Alive
【HTTP】keep-alive
HTTP Keep-Alive模式
okhttp连接池复用机制
Okhttp对http2的支持简单分析