咱們在進行http請求的時候,會有大體以下幾個流程: DNS -> 創建Socket鏈接 -> 應用層進行 http 請求 (圖片來源網絡)java
那麼 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" 的鏈接,若是不是全新的鏈接,則會釋放掉,繼續去創建鏈接。socket
查看 findConnection
,我留下了部分關鍵代碼進行分析:源碼分析
if (this.connection != null) {
// We had an already-allocated connection and it's good.
result = this.connection;
releasedConnection = null;
}
複製代碼
經過註釋咱們瞭解到,咱們已經有了一個可用的鏈接,直接複用。post
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
必須和 host 匹配經過源碼分析,咱們也能夠獲得以下結論:
OKHttpClient
,由於鏈接池不是多個 client 共享ProxySelector
來自定義咱們在代理下的行爲,例如:有代理也不走DNS
,在裏面作咱們本身的 DNS 解析邏輯如今,咱們瞭解了 OKHTTP 對 HTTP 請求進行的鏈接, UML 圖能夠清晰的展現每一個類的關係:
咱們也能夠對 隧道代理,SSL,HTTP2具體的幀格式等特性,進行進一步的網絡知識的深刻學習和分析。來尋找一些網絡優化的突破點和思路。
請關注個人微信公衆號 【半行代碼】