連接攔截器,它的作用主要是和服務(wù)器建立一個連接,只有建立連接了客戶端才能與服務(wù)端交換數(shù)據(jù),算是比較重要的一環(huán)了,我們來看一下這個攔截器的一些實現(xiàn):
public final class ConnectInterceptor implements Interceptor {
public final OkHttpClient client;
@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");
// 有兩個實現(xiàn)類,分別是Http1Codec和Http2Codec,主要是用來進行Http請求和響應(yīng)的編碼/解碼操作
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
//交給下一個攔截器執(zhí)行真正的網(wǎng)絡(luò)請求
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}
看到這里,可能有人就會說了,逗我呢,這么重要的攔截器,才這么幾行代碼,沒錯,本身這個攔截器沒啥東西,但是有一個很重要的類 StreamAllocation 負責管理連接、流和請求這三者;不知道還有沒有印象,在之前的重試攔截器中我們創(chuàng)建了一個 StreamAllocation 對象,然后傳到這個連接攔截器中,然后通過 StreamAllocation 來生成一個 HttpCodec,這個主要是用來進行Http請求和響應(yīng)的編碼/解碼,看看這個方法:
public HttpCodec newStream(OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
try {
// 獲取可用的連接
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
// 構(gòu)造一個HttpCodec,后面一個攔截器會用到
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
這個方法主要就是尋找一個可用的連接,然后通過找到的連接來生成一個HttpCodec,那是怎么樣去找這個可用的連接的呢?
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks) throws IOException {
// 這里會一直去找一個可用的連接,直到找到為止
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) {
// 如果是新連接的話successCount一定為0
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)) {
// 禁止新的流被創(chuàng)建
noNewStreams();
continue;
}
return candidate;
}
}
可以看到,這里開了一個死循環(huán)會通過 findConnection 方法一直找有沒有連接,找到之后會判斷是否是可用的連接,如果可用就直接返回,否則會繼續(xù)尋找,那么問題來了,何為可用的連接呢?怎么判斷?
public boolean isHealthy(boolean doExtensiveChecks) {
// 檢查socket的狀態(tài)
if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
return false;
}
// 檢查http2Connection是否關(guān)閉
if (http2Connection != null) {
return !http2Connection.isShutdown();
}
if (doExtensiveChecks) {
// 非GET請求會判斷Socket的inputStream相關(guān)的read操作阻塞的等待時間
try {
int readTimeout = socket.getSoTimeout();
try {
socket.setSoTimeout(1);
// 流是否用完
if (source.exhausted()) {
return false; // Stream is exhausted; socket is closed.
}
return true;
} finally {
socket.setSoTimeout(readTimeout);
}
} catch (SocketTimeoutException ignored) {
// Read timed out; socket is good.
} catch (IOException e) {
return false; // Couldn't read; socket is closed.
}
}
return true;
}
首先會檢查socket的狀態(tài),以及socket的input和output是否關(guān)閉了;然后看有沒有使用http2,會判斷http2連接是否關(guān)閉;最后如果是非GET請求的話會判斷Socket的inputStream相關(guān)的read操作阻塞的等待時間;通過上述操作來判斷一個連接是否可用。再回到前面,看看findConnection 的內(nèi)部是怎么找連接的:
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
...
// 判斷當前的連接是否為空,不為空則復用當前的
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.
// 嘗試從連接池中獲取一個連接,get方法是從連接池中的隊列中獲取
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
...
// 否則嘗試切換路由
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);
// 每切換一次路由都嘗試從連接池中尋找一個連接,有的話就返回,沒有就繼續(xù)切換路由
Internal.instance.get(connectionPool, address, this, route);
if (connection != null) {
foundPooledConnection = true;
result = connection;
this.route = route;
break;
}
}
}
// 最后還沒找到的話,就會構(gòu)造一個新的,
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);
// 引用計數(shù)
acquire(result, false);
}
}
// Do TCP + TLS handshakes. This is a blocking operation.
// 創(chuàng)建的新連接需要進行connect操作,也就是TCP三次握手,阻塞操作,會判斷是否超時
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());
Socket socket = null;
synchronized (connectionPool) {
reportedAcquired = true;
// 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.
// Http2的多路復用判斷
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(connectionPool, address, this);
result = connection;
}
}
}
上述代碼比較長,我們分成幾個部分來看:
- 1、首先有幾個前置的判斷,判讀當前連接是否釋放了,是否編碼了,是否被用戶取消了
- 2、然后會嘗試用當前連接(不為空)作為返回值返回
- 3、否則的話會嘗試從連接池中獲取
- 4、如果還沒獲取到就會嘗試切換路由,再重復從連接池中獲取
- 5、最后如果還沒獲取到的話就會創(chuàng)建一個新的,然后進行連接操作,再將該連接放入連接池等待下一次被復用
這里有兩個比較重要的邏輯,第一就是路由的切換,簡單說一下,相信大家都知道一個域名是對應(yīng)多個IP地址的,而我們發(fā)起請求目標服務(wù)器的IP是唯一一個,所以需要找到我們實際請求的目標服務(wù)器IP地址,而路由選擇器的作用就是幫我們找到匹配的目標服務(wù)器IP,這個過程中DNS會幫我們解析域名服務(wù)器的IP地址信息,然后存到路由選擇器里,每次切換路由就會挨個取出來,然后從連接池中取出連接將當前的地址信息和路由中的進行比對,如果匹配的上就說明該連接是可以拿出來復用的,就不用重新構(gòu)造新的連接;第二就是新創(chuàng)建的連接需要進行 connect 操作,我們來看一下是干嘛的:
// TCP TLS,區(qū)分Http1/Http2,Http2需要進行TLS數(shù)據(jù)加密傳輸,以及握手,證書認證等一系列操作
public void connect(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, Call call, EventListener eventListener) {
// 協(xié)議已經(jīng)存在,說明已經(jīng)連接了,拋出異常
if (protocol != null) throw new IllegalStateException("already connected");
if (route.address().sslSocketFactory() == null) {
// Http1明文判斷
if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication not enabled for client"));
}
String host = route.address().url().host();
// 是否允許明文傳輸,在Android 9.0以上不允許明文傳輸,于是乎就有了網(wǎng)上的解決方案
if (!Platform.get().isCleartextTrafficPermitted(host)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication to " + host + " not permitted by network security policy"));
}
}
while (true) {
// 判斷是使用Socket連接還是隧道連接(需要三次握手等操作)
try {
// 如果是Https請求并且使用了Http代理,就是用隧道連接的方式
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 {
// socket連接
connectSocket(connectTimeout, readTimeout, call, eventListener);
}
// 建立協(xié)議
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
break;
} catch (IOException e) {
closeQuietly(socket);
closeQuietly(rawSocket);
socket = null;
rawSocket = null;
source = null;
sink = null;
handshake = null;
protocol = null;
http2Connection = null;
eventListener.connectFailed(call, route.socketAddress(), route.proxy(), null, e);
if (routeException == null) {
routeException = new RouteException(e);
} else {
routeException.addConnectException(e);
}
if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
throw routeException;
}
}
}
}
首先還是一些前置的判斷,判斷當前協(xié)議協(xié)議是否存在,如果存在的話那么說明已經(jīng)連接過了,這時候會拋出異常;然后會進行Http的明文判斷,是否允許明文;然后會根據(jù)路由來判斷是使用Socket連接還是使用隧道連接,建立連接之后還會建立連接的協(xié)議,這個我們后面來看,先來看一下Socket連接(我們一般的請求都不會用到代理),因為隧道連接也是需要進行Socket連接的,只不過隧道連接多了一個創(chuàng)建隧道請求的操作:
private void connectSocket(int connectTimeout, int readTimeout, Call call, EventListener eventListener) throws IOException {
// 拿到代理和路由地址
Proxy proxy = route.proxy();
Address address = route.address();
// 初始化socket連接,根據(jù)代理的類型來判斷是直接連還是使用代理連
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy);
eventListener.connectStart(call, route.socketAddress(), proxy);
// 讀取數(shù)據(jù)時阻塞鏈路的超時時間
rawSocket.setSoTimeout(readTimeout);
try {
// 打開Socket連接
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
} catch (ConnectException e) {
ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
ce.initCause(e);
throw ce;
}
try {
// 使用Okio來進行數(shù)據(jù)的讀寫(數(shù)據(jù)交換)操作
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);
}
}
}
首先會拿到代理和路由地址的信息,因為需要根據(jù)是否有代理來創(chuàng)建不同的Socket,然后設(shè)置一下超時時間,最后通過 connectSocket 方法(會調(diào)用Socket的connect方法)打開一個Socket連接,連接完成之后最重要的就是數(shù)據(jù)的交換了,這里都交給Okio的Source和Sink來完成。好,現(xiàn)在再回過頭來看看建立連接之后是怎么建立協(xié)議的:
private void establishProtocol(ConnectionSpecSelector connectionSpecSelector, int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
// Http1
if (route.address().sslSocketFactory() == null) {
protocol = Protocol.HTTP_1_1;
socket = rawSocket;
return;
}
eventListener.secureConnectStart(call);
// 連接TLS
connectTls(connectionSpecSelector);
eventListener.secureConnectEnd(call, handshake);
// Http2
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)
.pingIntervalMillis(pingIntervalMillis)
.build();
http2Connection.start();
}
}
因為我們Http1和Http2的請求不太一樣,所以建立的協(xié)議也不太一樣,總的來說Http2請求會復雜一點,Http2請求會建立TLS協(xié)議,也就是我們通常說的加密傳輸,這個階段會進行TLS握手以及證書的驗證等等。
OKHttp其他攔截器詳細的說明,可以看我Github上的項目