- 缓存篇
- 流程图
- `ConnectInterceptor`详解
- ExchangeFinder
- `ExchangeFinder#findConnection`源码分析
- RealConnection
- 连接池ConnectionPool
- Transmitter
- 代理和DNS
- Java定义的代理类型三种`DIRECT`、`HTTP`、`SOCKS`
- DNS解析
- https://blog.csdn.net/followYouself/article/details/121086502
- 连接管理拦截器。有一个连接池。可以建立http、https、http2三种连接类型。
- 默认情况下,连接池最多会持有5个连接,保活时间是5分钟。参考ConnectionPool#ConnectionPool(int, long, java.util.concurrent.TimeUnit)。
- 在这个拦截器中建立了socket、sslSocket连接。http、https连接,协商了http2协议。
- 参考资料: https://juejin.cn/post/6844904196219600910
- 参考资料:https://blog.csdn.net/hfy8971613/article/details/106541640
- 该类主要通过findHealthyConnection函数生成一个RealConnection,然后构造一个ExchangeCodec对象用于网络请求。
- 生成RealConnection基于如下规则:
- 如果已经有connection连接满足要求,那么直接服用当前链路。
- 如果没有满足要求的connection连接,那么在ConnectionPool连接池中寻找可用连接,进行连接服用。
- 如果连接池中也没有找到可用connection连接,那么根据路由表(可能会DNS请求),尝试建立一个可用连接。
ExchangeFinder#findConnection源码分析
- 注释1处transmitter中的connection不为空,而且是可用的直接返回当前连接。
- 注释2处说明当前transmitter中的connection为空,尝试通过transmitterAcquirePooledConnection获取ConnectionPool连接池中的可用连接。
- 注释3,nextRouteToTry是在注释13处赋值的,它并不是记录的routes列表中的下一个路由地址。它只是记录一种异常情况:在ConnectionPool获取到可用连接,但是这个连接又马上unhealthy。
- 重试当前的Route路由地址。
- 返回可用连接,有可能是transmitter本身自带的,也有可能是ConnectionPool中获取的
- 获取下一个Proxy的路由表,此处会进行DNS。返回RouteSelector.Selection对象。每个Selection包含一个路由表。
- 根据获取的路由表,再次尝试获取ConnectionPool连接池中的可用连接。
- ConnectionPool没有找到可用连接,获取一个Route路由对象,每个。
- 创建一个RealConnection对象,并且传入Route路由对象。
- 如果注释7出找到了可用连接,直接返回。
- 真正的想服务器发起socket连接。可能进行TCP + TLS handshakes + Http/2.0协商 等步骤
- 在注释11建立一条新的RealConnection后,在尝试在ConnectionPool获取是否有可用连接,如果找到了可用连接,优先复用在连接池中找到的连接。并且关闭注释11中建立的RealConnection,给nextRouteToTry赋值当前的Route路由对象(因为这个route对象是可用的)。注意传入的最后一个参数是true,表明是要求http2.0多路复用。
- 参考注释3 和 注释12
- 注释12中没有在ConnectionPool中找到可用连接,将本次新建立的连接放到连接池中。
- 给transmitter对象中的RealConnection变量赋值。
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
boolean foundPooledConnection = false;
RealConnection result = null;
Route selectedRoute = null;
RealConnection releasedConnection;
Socket toClose;
synchronized (connectionPool) {
if (transmitter.isCanceled()) throw new IOException("Canceled");
hasStreamFailure = false; // This is a fresh attempt.
// Attempt to use an already-allocated connection. We need to be careful here because our
// already-allocated connection may have been restricted from creating new exchanges.
releasedConnection = transmitter.connection;
toClose = transmitter.connection != null && transmitter.connection.nonewExchanges
? transmitter.releaseConnectionNoEvents() : null; // 注释1
if (transmitter.connection != null) { // 注释1
// We had an already-allocated connection and it's good.
result = transmitter.connection;
releasedConnection = null;
}
if (result == null) { // 注释2
// Attempt to get a connection from the pool.
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
foundPooledConnection = true;
result = transmitter.connection;
} else if (nextRouteToTry != null) { // 注释3
selectedRoute = nextRouteToTry;
nextRouteToTry = null;
} else if (retryCurrentRoute()) { // 注释4
selectedRoute = transmitter.connection.route();
}
}
}
// 省略代码...
if (result != null) {
// If we found an already-allocated or pooled connection, we're done.
return result; // 注释5
}
// If we need a route selection, make one. This is a blocking operation.
boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
newRouteSelection = true;
routeSelection = routeSelector.next(); // 注释6
}
List routes = null;
synchronized (connectionPool) {
if (transmitter.isCanceled()) throw new IOException("Canceled");
if (newRouteSelection) {
// Now that we have a set of IP addresses, make another attempt at getting a connection from
// the pool. This could match due to connection coalescing.
routes = routeSelection.getAll();
if (connectionPool.transmitterAcquirePooledConnection(
address, transmitter, routes, false)) { // 注释7
foundPooledConnection = true;
result = transmitter.connection;
}
}
if (!foundPooledConnection) {
if (selectedRoute == null) {
selectedRoute = routeSelection.next(); // 注释8
}
// Create a connection and assign it to this allocation immediately. This makes it possible
// for an asynchronous cancel() to interrupt the handshake we're about to do.
result = new RealConnection(connectionPool, selectedRoute); // 注释9
connectingConnection = result;
}
}
// If we found a pooled connection on the 2nd time around, we're done.
if (foundPooledConnection) { // 注释10
eventListener.connectionAcquired(call, result);
return result;
}
// Do TCP + TLS handshakes. This is a blocking operation.
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener); // 注释11
connectionPool.routeDatabase.connected(result.route());
Socket socket = null;
synchronized (connectionPool) {
connectingConnection = null;
// Last attempt at connection coalescing, which only occurs if we attempted multiple
// concurrent connections to the same host.
// 注释12
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
// We lost the race! Close the connection we created and return the pooled connection.
result.nonewExchanges = true;
socket = result.socket();
result = transmitter.connection;
// It's possible for us to obtain a coalesced connection that is immediately unhealthy. In
// that case we will retry the route we just successfully connected with.
nextRouteToTry = selectedRoute; // 注释13
} else {
connectionPool.put(result); // 注释14
transmitter.acquireConnectionNoEvents(result); // 注释15
}
}
closeQuietly(socket);
eventListener.connectionAcquired(call, result);
return result;
}
RealConnection
- 表示一个真实的socket连接。
- RealConnection#connect是一个重要的函数,这个函数建立了TCP socket、sslSocket连接、进行https TLS安全协商过程、HTTP 2.0协议协商。
- socket的连接动作都是在这个函数完成的。几个关键函数RealConnection#connect、connectSocket、createTunnel、establishProtocol、connectTls、startHttp2。
- createTunnel中发送的CONNECT消息,参考资料https://www.jianshu.com/p/54357cdd4736。
| http协议版本 | 连接流程 |
|---|---|
| http/1.1 | 1. DNS 2. TCP Socket(未加密) |
| https/1.1 | 1. DNS 2. TCP Socket(未加密) 3. TLS安全隧道。"CONNECT"消息、TLS协商 |
| http/2.0 | 1. DNS 2. TCP Socket(未加密) 3. TLS安全隧道。"CONNECT"消息、TLS协商 4. Http2协商。sendConnectionPreface、settings |
- transmitterAcquirePooledConnection获取缓存池中的可用连接。
- put函数,向缓存池中增加可用连接。
- 需要注意的是HTTP代理和HTTP协议是两个不同的概念。
- SOCKS代理是不需要在本地DNS解析的,可以直接将域名发送到SOCKS代理服务器上。DIRECT、HTTP代理需要进行DNS解析。参考方法RouteSelector#resetNextInetSocketAddress。
- HTTP代理分为普通代理和隧道代理。普通代理是明文传输,即http请求;隧道代理是加密传输,仅仅转发TCP包,即https和http2.0。隧道代理在建立时,会要求先发送connect请求建立隧道,进行SSL握手,因为数据是加密的,代理服务器没有办法解析数据,只能转发数据。
- 默认的ProxySelector类是sun.net.spi.DefaultProxySelector,在OkHttpClient.Builder#Builder()初始化默认值。参考方法java.net.ProxySelector#getDefault。
- 代理和路由相关的参考资料:https://www.jianshu.com/p/63ba15d8877a
- 提供一个域名,返回一个IP地址列表。
- URL通过DNS解析成为具体的服务器IP地址。java提供的DNS解析方法java.net.InetAddress#getAllByName。可以解析IPV4 和 IPV6地址。
- Dns接口定义了lookup方法寻找IP地址。
- Dns#SYSTEM变量实现了该接口。匿名内部类。
- DNS寻址动作在 RouteSelector#resetNextInetSocketAddress方法中实现
//okhttp3.internal.connection.RouteSelector#resetNextInetSocketAddress
if (proxy.type() == Proxy.Type.SOCKS) { // 如果是socks代理,是不需要DNS解析的,可以直接将域名发送到服务器进行解析。
inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
} else { // DIRECT、HTTP代理需要进行DNS解析
eventListener.dnsStart(call, socketHost);
// Try each address for best behavior in mixed IPv4/IPv6 environments.
List addresses = address.dns().lookup(socketHost); // DNS解析
if (addresses.isEmpty()) {
throw new UnknownHostException(address.dns() + " returned no addresses for " + socketHost);
}
eventListener.dnsEnd(call, socketHost, addresses);
for (int i = 0, size = addresses.size(); i < size; i++) {
InetAddress inetAddress = addresses.get(i);
inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
}
}



