我们在进行http请求的时候,会有大致如下几个流程: DNS -> 建立Socket连接 -> 应用层进行 http 请求 (图片来源网络)
那么 OKHttp 是怎么进行每一步的处理呢,今天我们就来一探究竟。
在 ConnectInterceptor
中,我们可以看到如下几行代码
StreamAllocation streamAllocation = realChain.streamAllocation(); HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks); RealConnection connection = streamAllocation.connection(); 复制代码
可以看到这里初始化了一个 StreamAllocation
,开启了一次新的 newStream
,最终返回了一个 RealConnection
来表示连接的对象。
我们一步一步具体分析
newStream
中,会调用 findHealthyConnection
:
while (true) { RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled); // If this is a brand new connection, we can skip the extensive health checks. synchronized (connectionPool) { if (candidate.successCount == 0) { return candidate; } } // Do a (potentially slow) check to confirm that the pooled connection is still good. If it // isn't, take it out of the pool and start again. if (!candidate.isHealthy(doExtensiveHealthChecks)) { noNewStreams(); continue; } return candidate; } 复制代码
这里,会有一个循环,一直在寻找一个 "healthy" 的连接,如果不是全新的连接,则会释放掉,继续去建立连接。
查看 findConnection
,我留下了部分关键代码进行分析:
if (this.connection != null) { // We had an already-allocated connection and it's good. result = this.connection; releasedConnection = null; } 复制代码
通过注释我们了解到,我们已经有了一个可用的连接,直接复用。
if (result == null) { // Attempt to get a connection from the pool. Internal.instance.get(connectionPool, address, this, null); if (connection != null) { foundPooledConnection = true; result = connection; } else { selectedRoute = route; } } 复制代码
如果不存在连接,去一个叫 connectionPool
的对象中尝试去取。
if (result != null) { // If we found an already-allocated or pooled connection, we're done. return result; } 复制代码
如果这里已经找到了连接,就会直接返回。
我们继续看下面的代码,当需要我们自己创建一个连接的时候,OKHttp 是怎么处理的:
boolean newRouteSelection = false; if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) { newRouteSelection = true; routeSelection = routeSelector.next(); } 复制代码
如果这时候没有 selectedRoute
, 我们就从 routeSelector.next()
中选出一个 "路由选择"。其中包含了一套路由,每个路由有自己的地址和代理。
在拥有这组 ip 地址后,会再次尝试从 Pool
中获取连接对象。如果仍然获取不到,就自己创建一个。并调用一下 acquire(RealConnection connection, boolean reportedAcquired)
方法。
这时候如果使用的是全新的 Connect, 那么,我们就要调用 connect
方法:
// Do TCP + TLS handshakes. This is a blocking operation. result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled, call, eventListener); routeDatabase().connected(result.route()); 复制代码
并且,会把这个连接也 put 到 pool 里面:
// Pool the connection. Internal.instance.put(connectionPool, result); 复制代码
从上面的代码中,我们可以一直看到 ConnectionPool
这个对象。这个对象代表的是一个 TCP 连接池。Http 协议需要先建立每个 TCP 连接。如果 TCP 连接在满足条件的时候进行复用,无疑会节省很多系统资源。并且加快 Http 的整个过程,也可以理解成,缩短了 Http 请求回来的时间。
ConnectionPool
内部维护了:
cleanuoRunnable
RealConnection
的队列 RouteDatabase
我们关注一下连接池的存取:
@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; } 复制代码
这里会在满足条件的时候,返回已经存在队列里面的 Connection 对象。 那么什么时候是满足条件的呢?我们直接看 isEligible
方法里面的注释:
我们还可以发现:每次我们使用连接的时候,都会调用 StreamAllocation
的 acquire
方法。我们瞥一眼这个方法:
connection.allocations.add(new StreamAllocationReference(this, callStackTrace)); 复制代码
原来在每个 Connection
中,维护了一个 StreamAllocation
的弱引用的数组,来表示这个连接被谁引用。这个是一个很典型的引用计数方式。如果连接没有被引用,则可以认为这个连接是可以被清理的。
取出连接看完了,我们再看看连接建立的时候,是怎么扔到连接池的:
void put(RealConnection connection) { assert (Thread.holdsLock(this)); if (!cleanupRunning) { cleanupRunning = true; executor.execute(cleanupRunnable); } connections.add(connection); } 复制代码
这里可以看到,每次连接放进连接池的时候,会触发一次清理操作:
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
会返回纳秒为单位的下次清理时间的间隔。在时间到之前就阻塞进入冻结的状态。等待下一次清理。 cleanup
的具体逻辑不赘述。当连接的空闲时间比较长的时候,就会被清理释放。
在获取连接的过程中,我们会调用 routeSelector
的 next
方法,来获取我们的路由。那么这个路由选择内部做了什么事情呢?
public Selection next() { List<Route> routes = new ArrayList<>(); while (hasNextProxy()) { Proxy proxy = nextProxy(); for (int i = 0, size = inetSocketAddresses.size(); i < size; i++) { Route route = new Route(address, proxy, inetSocketAddresses.get(i)); if (routeDatabase.shouldPostpone(route)) { postponedRoutes.add(route); } else { routes.add(route); } } if (!routes.isEmpty()) { break; } } return new Selection(routes); } 复制代码
这里也有一个循环,会不断的获取 Proxy
,然后根据每一个 InetSocketAddress
创建 Route
对象。如果路由是通的,那么就直接返回。如果这些地址的路由在之前都存在 routeDatabase
中,说明都不是可用的,则继续下一个 Proxy
。
再看下 StreamAllocation
初始化 RouteSelector
的逻辑,会调用 resetNextProxy
方法:
List<Proxy> proxiesOrNull = address.proxySelector().select(url.uri()); proxies = proxiesOrNull != null && !proxiesOrNull.isEmpty() ? Util.immutableList(proxiesOrNull) : Util.immutableList(Proxy.NO_PROXY); 复制代码
address
的 ProxySelector
, 则是在构造 OKHttpClient
的时候创建的:
proxySelector = ProxySelector.getDefault(); 复制代码
它的实现类会去读取系统的代理。当然,我们也可以自己提供自定义的 Proxy 策略。绕过系统的代理。 这就是为什么有些时候我们给手机设置了 proxy,但是有些 APP 仍然不会走代理。
现在我们来看看,获取 Proxy
的时候,OKHttp 究竟做了哪些事情:
Proxy result = proxies.get(nextProxyIndex++); resetNextInetSocketAddress(result); return result; 复制代码
private void resetNextInetSocketAddress(Proxy proxy) { String socketHost; int socketPort; if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) { socketHost = address.url().host(); socketPort = address.url().port(); } else { SocketAddress proxyAddress = proxy.address(); InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress; socketHost = getHostString(proxySocketAddress); socketPort = proxySocketAddress.getPort(); } if (proxy.type() == Proxy.Type.SOCKS) { inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort)); } else { List<InetAddress> addresses = address.dns().lookup(socketHost); for (int i = 0, size = addresses.size(); i < size; i++) { InetAddress inetAddress = addresses.get(i); inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort)); } } } 复制代码
在代码中,Proxy 有三种模式:
当直接连接或者是 socks 代理的时候,socket 的host 和 port 从 address
中获取, 当是http代理的时候,则从 proxy 的代理中获取 host 和 port。 如果是http代理,后续会继续走 DNS 去解析代理服务器的host。最终,这些host和port都会封装成 InetSocketAddress
对象放到 ip 列表中。
介绍完连接池、路由和代理,我们来看发起 connect 这个操作的地方,即 RealConnection
的 connect
方法: (这里我删除了不关键的错误处理代码)
public void connect(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, Call call, EventListener eventListener) { while (true) { if (route.requiresTunnel()) { //1. 隧道连接 connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener); if (rawSocket == null) { break; } } else { // 2. 直接socket连接 connectSocket(connectTimeout, readTimeout, call, eventListener); } // 3. 建立连接协议 establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener); } } 复制代码
我们先来看socket连接:
private void connectSocket(int connectTimeout, int readTimeout, Call call, EventListener eventListener) throws IOException { Proxy proxy = route.proxy(); Address address = route.address(); rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP ? address.socketFactory().createSocket() : new Socket(proxy); rawSocket.setSoTimeout(readTimeout); Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout); source = Okio.buffer(Okio.source(rawSocket)); sink = Okio.buffer(Okio.sink(rawSocket)); } 复制代码
具体连接操作在不同的平台上不一样,在 Android
中是在 AndroidPlatform
的 connectSocket
中进行的:
socket.connect(address, connectTimeout); 复制代码
这时候, RealConnection
中的 source
和 sink
就分别代表了 socket 网络流的读入和写入。
隧道连接的逻辑在 connectTunnel
中:
private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout, Call call, EventListener eventListener) throws IOException { Request tunnelRequest = createTunnelRequest(); HttpUrl url = tunnelRequest.url(); for (int i = 0; i < MAX_TUNNEL_ATTEMPTS; i++) { connectSocket(connectTimeout, readTimeout, call, eventListener); tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url); if (tunnelRequest == null) break; // Tunnel successfully created. } } 复制代码
这里我们可以看到,隧道连接会先进行socket连接,然后创建隧道。如果创建不成功,会连续尝试 21 次。
private Request createTunnel(int readTimeout, int writeTimeout, Request tunnelRequest, HttpUrl url) throws IOException { String requestLine = "CONNECT " + Util.hostHeader(url, true) + " HTTP/1.1"; while (true) { Http1Codec tunnelConnection = new Http1Codec(null, null, source, sink); tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine); tunnelConnection.finishRequest(); Response response = tunnelConnection.readResponseHeaders(false) .request(tunnelRequest) .build(); Source body = tunnelConnection.newFixedLengthSource(contentLength); Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS); body.close(); switch (response.code()) { case HTTP_OK: if (!source.buffer().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"); if ("close".equalsIgnoreCase(response.header("Connection"))) { return tunnelRequest; } break; default: throw new IOException("Unexpected response code for CONNECT: " + response.code()); } } } 复制代码
在隧道或者socket连接建立完成后,会进行应用层的协议选择。查看 establishProtocol
:
if (route.address().sslSocketFactory() == null) { // 不是 ssl 连接,确认为 http 1.1 protocol = Protocol.HTTP_1_1; return; } // ssl 连接 connectTls(connectionSpecSelector); // http 2 if (protocol == Protocol.HTTP_2) { http2Connection = new Http2Connection.Builder(true) .socket(socket, route.address().url().host(), source, sink) .listener(this) .pingIntervalMillis(pingIntervalMillis) .build(); http2Connection.start(); } 复制代码
这里可以看到,如果 http 连接不支持 ssl 的话,就认为他是 http 1.1, 虽然理论上 http2 也可以是非 ssl 的,但是一般在使用中,http2 是必须支持 https 的。
如果设置了 SSLSocketFactory
, 那么先进行 SSL 的连接。
查看 connectTls
:
Address address = route.address(); SSLSocketFactory sslSocketFactory = address.sslSocketFactory(); SSLSocket sslSocket = null; // ssl socket sslSocket = (SSLSocket) sslSocketFactory.createSocket(rawSocket, address.url().host(), address.url().port(), true /* autoClose */); // configure the socket's clphers, TLS versions, adn extensions ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket); if (connectionSpec.supportsTlsExtensions()) { // 配置 TLS 扩展 Platform.get().configureTlsExtensions(sslSocket, address.url().host(), address.protocols()); } // ssl 握手 sslSocket.startHandshake(); // 校验证书 Handshake unverifiedHandshake = Handshake.get(sslSocketSession); if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) { X509Certificate cert = (X509Certificate) unverifiedHandshake.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)); } address.certificatePinner().check(address.url().host(),unverifiedHandshake.peerCertificates()); // 校验成功,判断具体的协议 String maybeProtocol = connectionSpec.supportsTlsExtensions() ? Platform.get().getSelectedProtocol(sslSocket) : null; protocol = maybeProtocol != null ? Protocol.get(maybeProtocol) : Protocol.HTTP_1_1; success = true; 复制代码
查看 Platform.get().getSelectedProtocol(sslSocket)
byte[] alpnResult = (byte[]) getAlpnSelectedProtocol.invokeWithoutCheckedException(socket); return alpnResult != null ? new String(alpnResult, Util.UTF_8) : null; 复制代码
这里会通过反射调用 OpenSSLSocketImpl
的 getAlpnSelectedProtocol
方法,最终通过 jni 层调用 NativeCrypto.cpp
去获取确定的应用层协议。可能获取到的值目前有
如果这时候支持的是 HTTP2 协议,那么我们关注点就要放到 Http2Connection
这个类上来。查看它的 start
方法:
void start(boolean sendConnectionPreface) throws IOException { if (sendConnectionPreface) { // 连接引导 writer.connectionPreface(); // 写 settings 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
中,客户端会发送 "PRI * HTTP/2.0/r/n/r/nSM/r/n/r/n"
到服务端。发送完 Connection Preface 之后,会继续发送一个 setting 帧。
Http2Connection`` 中通过 readerRunnable 来执行网络流的读取,参考
ReaderRunnable 的
execute` 方法:
reader.readConnectionPreface(this); while (reader.nextFrame(false, this)) {} 复制代码
首先,会读取 connection preface 的内容,即服务端返回的 settings 帧。如果顺利,后面会在循环中不断的读取下一帧,查看 nextFrame
:
这里对 HTTP2 不同类型的帧进行了处理。我们挑一个 data 帧查看,会继续走到 data
方法:
// 去掉了不关键代码 Http2Stream dataStream = getStream(streamId); // 获取抽象的流对象 dataStream.receiveData(source, length); // 把 datastream 读取到 source if (inFinished) { dataStream.receiveFin(); // 读取结束 } 复制代码
继续查看 receiveData
:
void receiveData(BufferedSource in, int length) { this.source.receive(in, length); } 复制代码
这里调用的是一个类型为 FramingSource
的 Source 对象。最终会调用 long read = in.read(receiveBuffer, byteCount);
方法。会把网络的 source 内容写到 receiveBuffer
中。然后把 receiveBuffer
的内容写到 readBuffer
中。这里的读写全部都是使用的 OKIO
框架。
那么 FramingSource
里面的的 readBuffer
在什么时候用到呢?在 OKHttp
的 CallServerInteceptor
里构造 ResonseBody
的时候,如果是 HTTP2 的请求,会从这个 buffer 里面读取数据。
从这里对 HTTP2 的帧处理,我们可以看到 HTTP2 的特性和 HTTP1.1 有很大的不一样,HTTP2 把数据分割成了很多的二进制帧。配合多路复用的特性,每个连接可以发送很多这样的内容较小的帧,整体上提升了 HTTP 的传输性能。每个 frame 的格式如下:
具体 HTTP2 二进制分帧的原理,我们以后再做单独探究。
现在回头看看连接池内对 HTTP2 的连接复用:
if (route == null) return false; if (route.proxy().type() != Proxy.Type.DIRECT) return false; if (this.route.proxy().type() != Proxy.Type.DIRECT) return false; if (!this.route.socketAddress().equals(route.socketAddress())) return false; if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false; if (!supportsUrl(address.url())) return false; address.certificatePinner().check(address.url().host(), handshake().peerCertificates()); 复制代码
可以看到 HTTP2 需要满足这些条件可以进行连接复用:
pinning
通过源码分析,我们也可以得到如下结论:
OKHttpClient ProxySelector DNS
现在,我们了解了 OKHTTP 对 HTTP 请求进行的连接, UML 图可以清晰的展示每个类的关系:
我们也可以对 隧道代理,SSL,HTTP2具体的帧格式等特性,进行进一步的网络知识的深入学习和分析。来寻找一些网络优化的突破点和思路。
请关注我的微信公众号 【半行代码】