一、前言
上一篇文章OkHttp 源碼深入分析(一)讓我們了解了 OKHttp 從初始化到建立連接的一系列過程龄捡,同時也對 OkHttp 的幾個關(guān)鍵類以及前幾個攔截器汁果,尤其是 ConnectInterceptor 攔截器有了更深的了解湃望,接下來我還會接著上一篇繼續(xù)分析后續(xù)的過程.
二驶赏、ConnectInterceptor 中的隱藏 socket 連接和 DNS
Socket 在哪連接的耕驰, DNS 解析好像也沒看到爷辱, ExchangeCodec實例對象是怎么創(chuàng)建的?
這是上一篇文章的最后我拋出的幾個問題朦肘,接下來我會一一解答饭弓,首先讓我們回到 findConnection 方法
final class ExchangeFinder {
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
//省略若干代碼...
// 如果上面的條件都沒有獲取到連接,那么說明我們要創(chuàng)建一個新的連接
//并通過 DNS 解析獲取要連接的 route
boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
newRouteSelection = true;
routeSelection = routeSelector.next();
}
// 做 TCP+TLS 握手媒抠,注意這是一個阻塞操作
//到這一步執(zhí)行完整個連接算是真正的建立了
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
//省略若干代碼...
return result;
}
}
2.1弟断、DNS 解析
代碼做了精簡,我們首先看看 DNS 解析的地方趴生,在分析之前先說說幾個類,我們在前面分析代碼的時候總會看見這幾個類如下
- class RouterSelector
- class Selection
- class Route
這幾個類其實做的事情并不復(fù)雜阀趴, RouterSelector 的主要作用是為了幫我門獲取 DNS 解析的 IP 地址,內(nèi)部有個 next 方法代碼如下
public Selection next() throws IOException {
//省略若干代碼...
List<Route> routes = new ArrayList<>();
while (hasNextProxy()) {//循環(huán)遍歷所有的代理方式
//DNS 解析
Proxy proxy = nextProxy();
//遍歷解析的地址苍匆,添加到 Route 集合中
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;
}
}
//將解析的地址包裝成 Selection 對象返回
return new Selection(routes);
}
Selection 對象是 RouteSelector 的一個靜態(tài)類刘急,有一個關(guān)鍵方法 next 用來獲取 route, 代碼很簡單就不貼了,繼續(xù)進入到 nextProxy 方法后發(fā)現(xiàn)最終調(diào)用的是 resetNextInetSocketAddress 方法
/** Prepares the socket addresses to attempt for the current proxy or host. */
private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
// Clear the addresses. Necessary if getAllByName() below throws!
inetSocketAddresses = new ArrayList<>();
//調(diào)用系統(tǒng) DNS 獲取所有地址
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));
}
}
}
代碼很多這里我們只看關(guān)鍵的代碼浸踩,經(jīng)過這么多層的兜兜轉(zhuǎn)轉(zhuǎn)終于是拿到了 IP 地址叔汁,這些 IP 地址最終會變成一個 Route 集合被包裝成 Selection 對象供 Socket 使用,而這個 DNS 的實例對象其實是在創(chuàng)建 OkHttpClient 的時候通過 builder 構(gòu)建出來的检碗,我當初找到這里的時候就納悶据块,這個 DNS 哪里冒出來的,沒看到在那創(chuàng)建的啊折剃,找了半天才發(fā)現(xiàn)原來隱藏在初始化的過程里另假。至于 Router 對象則是對我們請求信息的一個封裝,包含我們請求地址怕犁,代理類型浪谴,和解析后的 IP 地址的封裝類
2.2 Socket 連接和 ExchangeCodec 的創(chuàng)建
在上面代碼中可以發(fā)現(xiàn) Scoket 的連接是通過 result.connect 處理的,我們進去看看
public void connect(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, 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);
//省略大量的 address 判斷
while (true) {
try {
//判斷是否是 Http 代理的一個 Https 請求
if (route.requiresTunnel()) {
//連接 Https 隧道?
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
if (rawSocket == null) {
// We were unable to connect the tunnel but properly closed down our resources.
break;
}
} else {
//直接連接 socket
connectSocket(connectTimeout, readTimeout, call, eventListener);
}
//處理 https 的 TLS 建立
establishProtocol(connectionSpecSelector, pingIntervalMillis, call,
//省略若干代碼
break;
} catch (IOException e) {
//省略若干代碼...
}
}
上面的代碼基本都是 http/https 的具體協(xié)議處理過程因苹,這里就不過多解讀了(主要還是我本人網(wǎng)絡(luò)底子也不好就不獻丑了)苟耻,而且不影響我們理解整體流程,不過還是要說明一下扶檐, connectTunnel 最終還是會調(diào)用 connectSocket 方法凶杖, connectSocket 內(nèi)部會創(chuàng)建 Socket 的實例并創(chuàng)建 I/O 流進行關(guān)聯(lián),代碼如下
/** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
private void connectSocket(int connectTimeout, int readTimeout, Call call,
EventListener eventListener) throws IOException {
Proxy proxy = route.proxy();
Address address = route.address();
//創(chuàng)建 Socket 實例
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy);
rawSocket.setSoTimeout(readTimeout);
//這里開始根據(jù) route 提供的地址和目標服務(wù)器建立連接
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
try {
//創(chuàng)建連接所需要的 I/O 流款筑,負責(zé)寫入請求的 header智蝠、body 讀取響應(yīng)的 header腾么、 body
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
} catch (NullPointerException npe) {
if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
throw new IOException(npe);
}
}
}
接下來我們重點看看 establishProtocol 方法
private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
// 如果SSL 協(xié)議不為空且是 HTTP2 連接
if (route.address().sslSocketFactory() == null) {
if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
socket = rawSocket;
//協(xié)議標記為 http2
protocol = Protocol.H2_PRIOR_KNOWLEDGE;
//創(chuàng)建一個 Http2Connection 對象并啟動
startHttp2(pingIntervalMillis);
return;
}
socket = rawSocket;
//協(xié)議標記為 http1
protocol = Protocol.HTTP_1_1;
return;
}
eventListener.secureConnectStart(call);
//建立 TLS 鏈接
connectTls(connectionSpecSelector);
eventListener.secureConnectEnd(call, handshake);
if (protocol == Protocol.HTTP_2) {
startHttp2(pingIntervalMillis);
}
}
代碼不多,也很好理解杈湾,establishProtocol 內(nèi)部除了處理 Https 連接的問題解虱,更重要的是會判斷是否采用 Http2 連接并創(chuàng)建對應(yīng)的 Http2Connection 對象,這也為后面 創(chuàng)建 ExchangeCodec 實例對象提供了判斷依據(jù)漆撞。
當 establishProtocol 方法執(zhí)行完后所有跟 scoket 連接的大致流程也就分析完了殴泰,所以讓我們回過頭在看看獲取 ExchangeCodec 實例對象的地方
final class ExchangeFinder {
public ExchangeCodec find(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
//省略若干代碼...
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
return resultConnection.newCodec(client, chain);
//省略若干代碼
}
}
可以看到創(chuàng)建 ExChangeCodec 實例對象的方法是 RealConnection 對象的 newCodec 方法,進入此方法看看
ExchangeCodec newCodec(OkHttpClient client, Interceptor.Chain chain) throws SocketException {
if (http2Connection != null) {
return new Http2ExchangeCodec(client, this, chain, http2Connection);
} else {
socket.setSoTimeout(chain.readTimeoutMillis());
source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
return new Http1ExchangeCodec(client, this, source, sink);
}
}
經(jīng)過前面的一大串分析浮驳,終于看到了 ExchangeCodec 實例對象的創(chuàng)建邏輯悍汛,上面的代碼很簡單就是判斷 http2Connection 對象是否為 null ,以此為條件判斷創(chuàng)建對應(yīng)的 ExchangeCodec 對象
三至会、CallServerInterceptor
經(jīng)過了那么大篇幅的分析离咐,終于, 我們來到了最后一個攔截器,相比于 ConnectInterceptor 攔截器奉件,CallServerInterceptor 攔截器就容易理解多了宵蛀,因為和目標服務(wù)器的連接已經(jīng)建立了,接下來就是通過 ConnectInterceptor 傳過來的 Exchange 對象將我們的 header县貌、body 通過流寫入糖埋,再將返回的 response 讀取,代碼如下
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Exchange exchange = realChain.exchange();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
//將請求頭信息寫入 stream 中
exchange.writeRequestHeaders(request);
boolean responseHeadersStarted = false;
Response.Builder responseBuilder = null;
//非 GET 請求且 body 不為空
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
// 忽略 100-continue 頭域
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
exchange.flushRequest();//將請求頭信息流推出
responseHeadersStarted = true;
exchange.responseHeadersStart();
//讀取響應(yīng)信息
responseBuilder = exchange.readResponseHeaders(true);
}
//如果服務(wù)端沒有返回響應(yīng)則創(chuàng)建一個 requestBody 寫入
if (responseBuilder == null) {
if (request.body().isDuplex()) { // http2 協(xié)議
// Prepare a duplex body so that the application can send a request body later.
exchange.flushRequest();
BufferedSink bufferedRequestBody = Okio.buffer(
exchange.createRequestBody(request, true));
request.body().writeTo(bufferedRequestBody);
} else { //http1 協(xié)議
//如果“Expect: 100-continue”的期望滿足窃这,則寫入請求體。
BufferedSink bufferedRequestBody = Okio.buffer(
exchange.createRequestBody(request, false));
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
}
}//省略部分代碼
} else {
exchange.noRequestBody();
}
//省略部分代碼...
//讀取響應(yīng)信息
if (responseBuilder == null) {
responseBuilder = exchange.readResponseHeaders(false);
}
//構(gòu)建獲取的響應(yīng)
Response response = responseBuilder
.request(request)
.handshake(exchange.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
//省略部分代碼
return response;
}
代碼進行了精簡征候,展示了主要邏輯杭攻,可以看到 CallServerInterceptor 的內(nèi)部邏輯并不復(fù)雜,就是通過 Exchange 內(nèi)部封裝的 ExchangeCodec 對象疤坝,對 request 和 response 進行寫入兆解、讀取而已,然后將從服務(wù)端獲取的信息構(gòu)建成 response 返回跑揉。
此時再看下面的代碼是不是覺得更清晰了呢
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://raw.github.com/square/okhttp/master/README.md")
.build();
Response response = client.newCall(request).execute()
System.out.println("OKHTTP : " + response.body().string());
四锅睛、結(jié)語
經(jīng)過漫長的分析,終于將所有的攔截器走了一個遍历谍,此時回想整個過程现拒,既有抓耳撓腮的煩躁,也有恍然大悟的愉悅望侈,翻到前面又仔細看了一眼開篇的流程圖印蔬,不禁會心一笑,希望讀到這里的朋友也是如此脱衙。 完結(jié)撒花~