概述
本片文章主要講解OKHttp的連接建立過(guò)程寂祥。我們先宏觀的對(duì)OkHttp連接有個(gè)初步了解:
- OKHttp是一個(gè)高效的http庫(kù)曙搬,主要表現(xiàn)在:1 支持Http1,Http1.1,Http2協(xié)議勾给,實(shí)現(xiàn)這一點(diǎn)主要因?yàn)镠ttp連接不同于以往的網(wǎng)絡(luò)框架训柴,它是利用系統(tǒng)的Scoket實(shí)現(xiàn)的Http連接哑舒,可以滿足Http1和Http2協(xié)議,通過(guò)前面的文章我們知道Http2協(xié)議解決了Http連接的復(fù)用問(wèn)題幻馁,提高了Http的效率洗鸵;2 另外針對(duì)Http協(xié)議中連接耗時(shí)的另外一個(gè)原因是TCP三次握手導(dǎo)致的,OKHttp 利用Http協(xié)議中keepalive connections機(jī)制(可以在傳輸數(shù)據(jù)后仍然保持連接)仗嗦,當(dāng)客戶端需要再次獲取數(shù)據(jù)時(shí)膘滨,直接使用剛剛空閑下來(lái)的連接而不需要再次握手,OKhttp內(nèi)部建立了連接池,用來(lái)保存Http連接稀拐,Okhttp支持5個(gè)并發(fā)KeepAlive火邓,默認(rèn)鏈路生命為5分鐘(鏈路空閑后,保持存活的時(shí)間)德撬。
- OKHttp同時(shí)支持Http和Https協(xié)議
創(chuàng)建HTTP連接的流程
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
OKHttp的連接部分可以分為兩大類:1 從連接池中獲取一個(gè)可用的連接铲咨,并將此鏈接和本次請(qǐng)求綁定,產(chǎn)生一個(gè)StreamAllcation砰逻;2:沒(méi)有可用的連接鸣驱,建立socket鏈路,完成TCP三次握手以及TLS握手蝠咆,并與本次請(qǐng)求綁定踊东,產(chǎn)生一個(gè)StreamAllcation。這里主要涉及到StreamAllcation刚操,RealConnection闸翅,和ConnectionPool三個(gè)類。
- StreamAllcation 完成獲取一個(gè)健康的Http連接,從連接池中或者新建一個(gè)Http連接
- RealConnectin 利用Socket建立一個(gè)真正的Http連接菊霜,并完成TCP坚冀、TLS握手
- ConnectionPool 主要緩沖連接池,提高Http連接的復(fù)用率鉴逞,提高Http的效率
RealConnection 建立HTTP連接
本文先從RealConnection建立HTTP開(kāi)始分析记某。RealConnection建立HTTP的連接根據(jù)代理的不同司训,HTTP的連接分如下幾種情況:
- 無(wú)代理的HTTP請(qǐng)求,與服務(wù)器建立TCP連接液南;
- 無(wú)代理的HTTPS請(qǐng)求壳猜,與服務(wù)器建立TCP連接,并完成TLS握手滑凉;
- 無(wú)代理的HTTP2請(qǐng)求统扳,與服務(wù)器建立好TCP連接,完成TLS握手及協(xié)議協(xié)商畅姊。
- 設(shè)置了SOCKS代理的HTTP請(qǐng)求咒钟,通過(guò)代理服務(wù)器與HTTP服務(wù)器建立連接;
- 設(shè)置了SOCKS代理的HTTPS請(qǐng)求若未,通過(guò)代理服務(wù)器與HTTP服務(wù)器建立連接朱嘴,并完成TLS握手;
- 設(shè)置了SOCKS代理的HTTP/2請(qǐng)求陨瘩,通過(guò)代理服務(wù)器與HTTP服務(wù)器建立連接腕够,并完成與服務(wù)器的TLS握手及協(xié)議協(xié)商;
- 設(shè)置了HTTP代理的HTTP請(qǐng)求舌劳,與代理服務(wù)器建立TCP連接帚湘;HTTP代理服務(wù)器解析HTTP請(qǐng)求/響應(yīng)的內(nèi)容,并根據(jù)其中的信息來(lái)完成數(shù)據(jù)的轉(zhuǎn)發(fā)甚淡。HTTP服務(wù)器如何知道服務(wù)器的地址呢大诸?是通過(guò)Header中的HOST字段獲取的。
- 設(shè)置了HTTP代理的HTTPS贯卦、HTTP2請(qǐng)求资柔,與HTTP服務(wù)器建立通過(guò)HTTP代理的隧道連接,并完成TLS握手撵割;HTTP代理不再解析傳輸?shù)臄?shù)據(jù)贿堰,僅僅完成數(shù)據(jù)轉(zhuǎn)發(fā)的功能。此時(shí)HTTP代理的功能如同SOCKS代理啡彬。
public void connect(int connectTimeout, int readTimeout, int writeTimeout,
boolean connectionRetryEnabled, Call call, EventListener eventListener) {
if (protocol != null) throw new IllegalStateException("already connected");
RouteException routeException = null;
List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
if (route.address().sslSocketFactory() == null) {
if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication not enabled for client"));
}
String host = route.address().url().host();
if (!Platform.get().isCleartextTrafficPermitted(host)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication to " + host + " not permitted by network security policy"));
}
}
while (true) {
try {
if (route.requiresTunnel()) {
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(connectTimeout, readTimeout, call, eventListener);
}
establishProtocol(connectionSpecSelector, call, eventListener);
eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
break;
} catch (IOException e) {
...
}
}
if (route.requiresTunnel() && rawSocket == null) {
ProtocolException exception = new ProtocolException("Too many tunnel connections attempted: "
+ MAX_TUNNEL_ATTEMPTS);
throw new RouteException(exception);
}
if (http2Connection != null) {
synchronized (connectionPool) {
allocationLimit = http2Connection.maxConcurrentStreams();
}
} }
分析以上代碼主要流程如下:
1 通過(guò)路由獲取安全套件羹与,并驗(yàn)證安全套件是否和協(xié)議一致:對(duì)于HTTP協(xié)議的請(qǐng)求,安全套件中必須包含CLEARTEXT庶灿,CLEATTEXT代表著明文傳輸纵搁;Android平臺(tái)本身的安全策略是否允許向相應(yīng)的主機(jī)發(fā)送明文請(qǐng)求。
2 進(jìn)入循環(huán)創(chuàng)建連接直到創(chuàng)建成功往踢,跳出循環(huán)腾誉。
3 首先根據(jù)路由判斷是否需要建立隧道 ,建立隧道連接 或者建立普通的連接
4 建立協(xié)議,指的是建立TSL握手協(xié)議
5 對(duì)于HTTP2協(xié)議利职,設(shè)置連接的最大分配數(shù)趣效,指一條HTTP連接上最多同時(shí)存在的請(qǐng)求數(shù)目。
建立普通Socket連接
建立普通連接的過(guò)程非常簡(jiǎn)單猪贪,主要?jiǎng)?chuàng)建Socket和建立Socket英支。本文不做詳細(xì)的分析。
建立隧道Socket連接
是否需要建立隧道的依據(jù)如下:
- 無(wú)代理的HTTP哮伟、HTTPS、HTTP2傳輸不需要隧道妄帘。
- SOCKS代理的HTTP楞黄、HTTPs、HTTP2不需要建立隧道抡驼。
- HTTP代理的HTTP協(xié)議不需要建立隧道鬼廓。
- HTTP代理的HTTPs、HTTP2協(xié)議需要建立隧道致盟。
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.
// The proxy decided to close the connection after an auth challenge. We need to create a new
// connection, but this time with the auth credentials.
closeQuietly(rawSocket);
rawSocket = null;
sink = null;
source = null;
eventListener.connectEnd(call, route.socketAddress(), route.proxy(), null);
}}
建立隧道連接的過(guò)程如下:
- 創(chuàng)建 建立隧道連接 請(qǐng)求碎税。主要設(shè)置HEADER中的Host和Proxy-Connection屬性
- 與HTTP代理服務(wù)器建立TCP連接。
- 創(chuàng)建隧道馏锡。這主要是將 建立隧道連接 請(qǐng)求發(fā)送給HTTP代理服務(wù)器雷蹂,并處理它的響應(yīng)。
- 如果建立失敗杯道,再次嘗試建立隧道匪煌。
建立協(xié)議
private void establishProtocol(ConnectionSpecSelector connectionSpecSelector, Call call,
EventListener eventListener) throws IOException {
if (route.address().sslSocketFactory() == null) {
protocol = Protocol.HTTP_1_1;
socket = rawSocket;
return;
}
eventListener.secureConnectStart(call);
connectTls(connectionSpecSelector);
eventListener.secureConnectEnd(call, handshake);
if (protocol == Protocol.HTTP_2) {
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)
.build();
http2Connection.start();
}}
如果是HTTP協(xié)議,不需要建立協(xié)議的過(guò)程党巾,此時(shí)TCP握手已經(jīng)完成萎庭,可以在這個(gè)連接上開(kāi)始于服務(wù)器的通信;如果是HTTPS齿拂、HTTP2 協(xié)議則還需要建立協(xié)議 TLS協(xié)議驳规,完成TLS的握手,驗(yàn)證服務(wù)器證書(shū)署海,以及協(xié)商機(jī)密算法吗购、傳輸秘鑰。
建立TLS協(xié)議
private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
Address address = route.address();
SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
boolean success = false;
SSLSocket sslSocket = null;
try {
// Create the wrapper over the connected socket.
sslSocket = (SSLSocket) sslSocketFactory.createSocket(
rawSocket, address.url().host(), address.url().port(), true /* autoClose */);
// Configure the socket's ciphers, TLS versions, and extensions.
ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
if (connectionSpec.supportsTlsExtensions()) {
Platform.get().configureTlsExtensions(
sslSocket, address.url().host(), address.protocols());
}
// Force handshake. This can throw!
sslSocket.startHandshake();
Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());
// Verify that the socket's certificates are acceptable for the target host.
if (!address.hostnameVerifier().verify(address.url().host(), sslSocket.getSession())) {
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));}
// Check that the certificate pinner is satisfied by the certificates presented.
address.certificatePinner().check(address.url().host(),
unverifiedHandshake.peerCertificates());
// Success! Save the handshake and the ALPN protocol.
String maybeProtocol = connectionSpec.supportsTlsExtensions()
? Platform.get().getSelectedProtocol(sslSocket)
: null;
socket = sslSocket;
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) {
...
}}
HTTPS或者HTTP2的連接叹侄,需要在原生已經(jīng)完成TCP握手的連接基礎(chǔ)上在包裝一下巩搏,產(chǎn)生一個(gè)新的SSLSocket,并對(duì)這SSLSocket設(shè)置安全套件趾代,之后開(kāi)始進(jìn)入TLS的協(xié)商階段贯底。
建立TLS流程如下:
1 根據(jù)以上步驟中建立的socket,創(chuàng)建一個(gè)新的SSlSocket;
2 設(shè)置SSlSocket的安全套件;
3 啟動(dòng)TLS握手禽捆,完成協(xié)議版本號(hào)笙什、加密算法的協(xié)商,對(duì)服務(wù)器證書(shū)的認(rèn)證胚想,秘鑰的交換琐凭;
4 對(duì)收到的證書(shū)驗(yàn)證是否支持特定的host;
5 檢查證書(shū)pinner浊服;
6 實(shí)例化輸入輸出流等屬性统屈。
到此,一個(gè)可用的HTTP或者HTTPs牙躺,或者HTTP2的連接已經(jīng)完成了愁憔。接下來(lái)會(huì)將一個(gè)請(qǐng)求與這個(gè)連接綁定,對(duì)于HTTP1.0和HTTP1.1孽拷,一個(gè)連接只能同時(shí)綁定一個(gè)請(qǐng)求吨掌;而HTTP2連接可以同時(shí)可以綁定多個(gè)請(qǐng)求。
StreamAllocation 獲取可用的HTTP連接
OkHttp中有三個(gè)概念需要了解一下脓恕,請(qǐng)求膜宋,連接和流。我們要明白HTTP通信執(zhí)行網(wǎng)絡(luò)請(qǐng)求需要在連接上建立一個(gè)新的流炼幔。請(qǐng)求被封裝成Call對(duì)象秋茫,連接被封裝成Connection對(duì)象,流被封裝成HttpCodec乃秀。StreamAllocation是流分配的邏輯学辱,它負(fù)責(zé)為一個(gè)Call找到一個(gè)Connection,這個(gè)Connection可能是從連接池中拿到的环形,也可能是新建立的策泣。以及在請(qǐng)求完成或者取消時(shí)釋放資源。
public final Address address;//請(qǐng)求的地址
private RouteSelector.Selection routeSelection;
private Route route;//路由
private final ConnectionPool connectionPool;//連接池
public final Call call;//請(qǐng)求
public final EventListener eventListener;
private final Object callStackTrace;//日志
private final RouteSelector routeSelector;//路由選擇器
private int refusedStreamCount;//拒絕的次數(shù)
private RealConnection connection;//連接
private boolean canceled;//請(qǐng)求取消
private HttpCodec codec;//流
下面分析它分配流的過(guò)程
public HttpCodec newStream(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
int connectTimeout = chain.connectTimeoutMillis();
int readTimeout = chain.readTimeoutMillis();
int writeTimeout = chain.writeTimeoutMillis();
boolean connectionRetryEnabled = client.retryOnConnectionFailure();
try {
//獲取一個(gè)健康的連接
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
// 實(shí)例化流對(duì)象
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
這個(gè)方法完成兩件事:1 獲取一個(gè)健康的連接抬吟;2 實(shí)例化流對(duì)象
再來(lái)看獲取健康連接的過(guò)程:findHealthyConnection方法內(nèi)部通過(guò)調(diào)用findConnection獲取到一個(gè)連接萨咕,然后對(duì)這個(gè)連接判斷是否健康,如果不是健康的連接火本,再循環(huán)獲取一個(gè)連接危队。我們直接分析findConnection的邏輯。
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
boolean connectionRetryEnabled) throws IOException {
boolean foundPooledConnection = false;
RealConnection result = null;
Route selectedRoute = null;
synchronized (connectionPool) {
if (released) throw new IllegalStateException("released");
if (codec != null) throw new IllegalStateException("codec != null");
if (canceled) throw new IOException("Canceled");
// Attempt to use an already-allocated connection.
RealConnection allocatedConnection = this.connection;
if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
return allocatedConnection;
}
// 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;
}
}
// If we found a pooled connection, we're done.
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
return result;
}
// 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();
}
synchronized (connectionPool) {
if (canceled) 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.
List<Route> routes = routeSelection.getAll();
for (int i = 0, size = routes.size(); i < size; i++) {
Route route = routes.get(i);
Internal.instance.get(connectionPool, address, this, route);
if (connection != null) {
foundPooledConnection = true;
result = connection;
this.route = route;
break;
}
}
}
if (!foundPooledConnection) {
if (selectedRoute == null) {
selectedRoute = routeSelection.next();
}
// 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.
route = selectedRoute;
refusedStreamCount = 0;
result = new RealConnection(connectionPool, selectedRoute);
acquire(result);
}
}
// We have a connection. Either a connected one from the pool, or one we need to connect.
eventListener.connectionAcquired(call, result);
// If we found a pooled connection on the 2nd time around, we're done.
if (foundPooledConnection) {
return result;
}
// Do TCP + TLS handshakes. This is a blocking operation.
result.connect(
connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());
Socket socket = null;
synchronized (connectionPool) {
// Pool the connection.
Internal.instance.put(connectionPool, result);
// If another multiplexed connection to the same address was created concurrently, then
// release this connection and acquire that one.
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(connectionPool, address, this);
result = connection;
}
}
closeQuietly(socket);
return result;
}
1钙畔、先找是否有已經(jīng)存在的連接茫陆,如果有已經(jīng)存在的連接,并且可以使用則直接返回擎析。
2簿盅、根據(jù)已知的address在connectionPool里面找,如果有連接,則返回
3桨醋、更換路由棚瘟,更換線路,在connectionPool里面再次查找喜最,如果有則返回偎蘸。
4、如果以上條件都不滿足則直接new一個(gè)RealConnection出來(lái)
5瞬内、新建的RealConnection通過(guò)acquire關(guān)聯(lián)到connection.allocations上
6迷雪、做去重判斷,如果有重復(fù)的socket則關(guān)閉