OKHttp源码解析-ConnectionPool对Connection重用机制&Http/Https/SPDY协议选择

因文章很快被人转载到一些其他网站,所以本人在此声明:
转载请标明转载出处:http://frodoking.github.io/2015/06/29/android-okhttp-connectionpool-http1-x-http2-x/

距离上一次的OKHttp源码解析过去快3月了。最近一直在忙工作上的事情,另外也再尝试一门新的语言Go。所以一直没花很多心思在Android这边。最近看到一些网友建议把okhttp的连接池对Connection的重用维护机制以及HTTP和SPDY协议如何得到区分这两个核心内容做深入的分析。
因此,这几天就打算好好说一说这块儿的实现方式。SPDY既是http1.x的增强版也是http2.x的过渡版本,虽然现在很多都直接切入到http2.0,不过SPDY的应用仍然值得关注。

ConnectionPool对Connection的重用机制

从上一篇文章的HttpEngine.connect()说起,在这个方法中有connection = nextConnection();这是Connection创建或者重用的起点。那我们先来看看nextConnection()方法:

HttpEngine.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public final class HttpEngine {
// ...省略.....
/**
* Returns the next connection to attempt.
*
* @throws java.util.NoSuchElementException if there are no more routes to attempt.
*/
private Connection nextConnection() throws IOException {
Connection connection = createNextConnection();
Internal.instance.connectAndSetOwner(client, connection, this, networkRequest);
return connection;
}

private Connection createNextConnection() throws IOException {
ConnectionPool pool = client.getConnectionPool();

// Always prefer pooled connections over new connections.
// 这里表示先从连接池中选拔一个已经缓存过的Connection
// 先通过连接池内部的get方法获取(下面代码再展开)
for (Connection pooled; (pooled = pool.get(address)) != null; ) {
// 匹配GET方法,判断当前命中的Connection是否是可读取的,这里SPDY类型连接默认是true,
// 而http1.x通过判断socket是否已经关闭来作为是否可读取判断依据
if (networkRequest.method().equals("GET") || Internal.instance.isReadable(pooled)) {
return pooled;
}
// 如果不满足可循环使用,当然就是关闭当前的连接
pooled.getSocket().close();
}

// 新开一个Connection
Route route = routeSelector.next();
return new Connection(pool, route);
}
// ...省略.....
}

ConnectionPool.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public final class ConnectionPool {
//executor 的内部构建方式贴出来,希望读者也能注意到可定制线程池的使用。定制化的差别还是很大的,这里主要使用了LinkedBlockingQueue。
private Executor executor = new ThreadPoolExecutor(
0 /* corePoolSize */, 1 /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));
// 内部一个Connection的缓存列表,主要用于可循环使用连接的缓存作用。
private final LinkedList<Connection> connections = new LinkedList<>();
// ...省略.....

/** Returns a recycled connection to {@code address}, or null if no such connection exists. */
返回一个循环使用的Connection
public synchronized Connection get(Address address) {
Connection foundConnection = null;
for (ListIterator<Connection> i = connections.listIterator(connections.size());
i.hasPrevious(); ) {
Connection connection = i.previous();
if (!connection.getRoute().getAddress().equals(address)
|| !connection.isAlive()
|| System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) {
continue;
}
i.remove();
// 如果不是spdy连接。
if (!connection.isSpdy()) {
try {
// 通过反射 -- 这是Android平台下的适配 主要是反射到“android.net.TrafficStats.Socket.class”,
// 在这下边会有tagSocket和untagSocket方法,如果想了解详细的情况,建议再对照Platform这个类仔细研究一下
// // Non-null on Android 4.0+.
// private final Method trafficStatsTagSocket;
// private final Method trafficStatsUntagSocket;
Platform.get().tagSocket(connection.getSocket());
} catch (SocketException e) {
Util.closeQuietly(connection.getSocket());
// When unable to tag, skip recycling and close
Platform.get().logW("Unable to tagSocket(): " + e);
continue;
}
}
// 命中可循环使用Connection
foundConnection = connection;
break;
}

// 这里针对SPDY的Connection的重用,添加到队列头部
if (foundConnection != null && foundConnection.isSpdy()) {
connections.addFirst(foundConnection); // Add it back after iteration.
}

return foundConnection;
}

void recycle(Connection connection) {
// ...省略.....
addConnection(connection);
// ...省略.....
}

void share(Connection connection) {
// ...省略.....
addConnection(connection);
// ...省略.....
}
// ...省略.....
}

在代码段中重点标注了一下get方法来命中缓存的可循环使用的Connection,这里单独说一下这些Connection是什么时候被放入到缓存池中的:
1、recycle方法
这个recycle方法主要是针对Http1.x协议的Connection
2、share方法
在HttpEngine的nextConection()方法中,当创建完成了Connection后会执行Internal.instance.connectAndSetOwner(client, connection, this, networkRequest);
而这个方法最后执行时Connection.connectAndSetOwner(xxx)方法

Connection.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ...省略.....
/**
* Connects this connection if it isn't already. This creates tunnels, shares
* the connection with the connection pool, and configures timeouts.
*/
void connectAndSetOwner(OkHttpClient client, Object owner, Request request) throws IOException {
setOwner(owner);

if (!isConnected()) {
// 对请求头部的处理
Request tunnelRequest = tunnelRequest(request);
// 发起连接请求
connect(client.getConnectTimeout(), client.getReadTimeout(),
client.getWriteTimeout(), tunnelRequest);
if (isSpdy()) {
client.getConnectionPool().share(this);
}
client.routeDatabase().connected(getRoute());
}

setTimeouts(client.getReadTimeout(), client.getWriteTimeout());
}
// ...省略.....

owner作用标示当前连接是谁持有,如果是spdy的连接、属于连接池或者被丢弃那么owner都是null的。在循环使用这里是很有用的。
另外说一点tunnelRequest(request)方法,这里主要是为request通过HTTP proxy创建一个TLS的管道,这里牵涉到加解密的一些问题。
如果对网络编程比较熟悉的同学应该一看就非常明白这个方法对请求头部的关键字处理。这里就不详细展开了。
回到上边的代码,如果是SPDY的连接,这个Connection就会被共享,那么就会被缓存下来。

再简单说一下HttpEngine的Connection真正发起重用的地方,HttpEngine.releaseConnection()。每一个HttpEngine对应一个Transport接口,而Transport接口分HttpTransport和SpdyTransport。
在释放连接的时候会通过各自的Transport执行canReuseConnection(),如果可以重用,那么将状态置为idle状态,同时将连接放入到连接池。
SPDYTransport默认是可以重用的,而HttpTransport则需要判断request和Response的状态以及连接是否关闭来决定。
ok,连接的缓存就讲到这里吧。

Connection对Http/Https/SPDY协议的选择

关于协议的选择,到底是走http1.x还是走http2.x的spdy,主要得从HttpEngine的Transport接口选择说起。
任何与网络相关的,当然第一入口就是发起请求。在HttpEngine.sendRequest()方法中可以看到Transport创建的身影Internal.instance.newTransport(connection, this);
通过跟踪,绕了大半圈回到Connection.newTransport方法:

1
2
3
4
5
6
 /** Returns the transport appropriate for this connection. */
Transport newTransport(HttpEngine httpEngine) throws IOException {
return (spdyConnection != null)
? new SpdyTransport(httpEngine, spdyConnection)
: new HttpTransport(httpEngine, httpConnection);
}

这里是通过spdyConnection是否为空来作为判断依据。那好,继续跟踪这个域到底是怎么创建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void upgradeToTls(Request tunnelRequest, int readTimeout, int writeTimeout)
throws IOException {
Platform platform = Platform.get();
// ...省略.....
try {
// Force handshake. This can throw!
sslSocket.startHandshake();

String maybeProtocol;
if (route.connectionSpec.supportsTlsExtensions()
&& (maybeProtocol = platform.getSelectedProtocol(sslSocket)) != null) {
protocol = Protocol.get(maybeProtocol); // Throws IOE on unknown.
}
} finally {
platform.afterHandshake(sslSocket);
}
// ...省略.....
if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) {
sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, socket)
.protocol(protocol).build();
spdyConnection.sendConnectionPreface();
} else {
httpConnection = new HttpConnection(pool, this, socket);
}
// ...省略.....
}

中间的各种加密以及握手操作这里都省略,因为我们最想看到具体体现根据不同协议创建不同连接的地方。
另外特意贴出了一段关于Protocol的获取方法。通过强行发起握手,感知不同平台支持的协议。有兴趣同学可以更加深入了解一下源码内部给出的Android和JdkWithJettyBootPlatform这两个类。

upgradeToTls会被Connection.connect方法调起,而connect方法被上一节说连接缓存的connectAndSetOwner方法调用。这是不是就全部串联起来了呢?

由于visio密钥过期,导致没法画时序图,就采用下边的一个执行箭头表示吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HttpEngine
--> HttpEngine.sendRequest()
|--> HttpEngine.connect()
|--> HttpEngine.nextConnection()
| --> HttpEngine.createNextConnection()
| --> ConnectionPool.get(address)
| --> Connection.connectAndSetOwner()
| -->ConnectionPool.share()
| --> Connection.connect()
| --> Connection.upgradeToTls()
|--> Connection.newTransport()

http1.x的reuse过程
--> HttpEngine.releaseConnection()
--> HttpTransport.releaseConnectionOnIdle()
--> HttpConnection.poolOnIdle()
--> ConnectionPool.recycle()

关于http1.x和spdy协议的一些对比:

SPDY(读作“SPeeDY”)是Google开发的基于TCP的应用层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验。
SPDY并不是一种用于替代HTTP的协议,而是对HTTP协议的增强。新协议的功能包括降低延迟、数据流的多路复用、请求优先级、HTTP报头压缩以及安全强制性使用 TLS。这个从上边的源码也能看到协议的选择就有判断。
详细的区别这里不做进一步讨论,后面有时间个人觉得还是有必要再多深入了解一下这方面实现。由于google的推动作用,现在http2.x的已经得到很多浏览器的支持。

在infoq上有一篇关于HTTPS、SPDY和HTTP/2的性能比较的文章。有需要的同学可以去看看吧。