ConnectInterceptor負責網路連線,其本質是複用連線池中socket連線。
開始擼原始碼:
intercept(攔截)
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
//從攔截器鏈裡得到StreamAllocation物件
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);
//這裡是獲取前一步的connection.
RealConnection connection = streamAllocation.connection();
//把前面建立的連線,傳遞到下一個攔截器
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
複製程式碼
步驟詳細分析
基本步驟就上面展示了,程式碼看攔截這裡很簡單,我們理清楚一下幾個類的呼叫關係,來分析一下連線是如何一步步建立的:
- StreamAllocation
- ConnectionPool
- RealConnection
StreamAllocation
首先,StreamAllocation的初始化在第一個攔截器RetryAndFollowUpInterceptor裡面,
streamAllocation = new StreamAllocation(
client.connectionPool(), createAddress(request.url()), callStackTrace);
複製程式碼
傳入了三個引數,一個連線池,一個地址類,一個呼叫堆疊跟蹤相關的。
在StreamAllocation建構函式中,主要是把這個三個引數儲存為內部變數,供後面使用,還有一個就是同時建立了一個線路選擇器:
this.routeSelector = new RouteSelector(address, routeDatabase());
複製程式碼
該構造器裡面有兩個重要的引數:
1.使用了Okhttp的連線池ConnectionPool
2.通過url建立了一個Address物件。
Okhttp連線池簡單說明:
本篇只是對連線池做最簡單的說明,內部的實現原理暫時不細講。在Okhttp內部的連線池實現類為ConnectionPool,該類持有一個ArrayDeque佇列作為快取池,該佇列裡的元素為RealConnection(通過這個名字應該不難猜出RealConnection是來幹嘛的)。該連結池在初始化OkhttpClient物件的時候由OkhttpClient的Builder類建立,並且ConnectionPool提供了put、get、evictAll等操作。但是Okhttp並沒有直接對連線池進行獲取,插入等操作;而是專門提供了一個叫Internal的抽象類來操作緩衝池:比如向緩衝池裡面put一個RealConnection,從緩衝池get一個RealConnection物件,該類裡面有一個public且為static的Internal型別的引用:
//抽象類 public abstract class Internal { public static Internal instance; }1234複製程式碼
instance的初始化是在OkhttpClient的static語句塊完成的:
static { Internal.instance = new Internal() { //省略部分程式碼 }; }複製程式碼
newStream()方法
主要做了如下工作:
1)從緩衝池ConnectionPool獲取一個RealConnection物件,如果緩衝池裡面沒有就建立一個RealConnection物件並且放入緩衝池中,具體的說是放入ConnectionPool的ArrayDeque佇列中。
2)獲取RealConnection物件後並呼叫其connect(開啟Socket連結)。
下面具體分析:
public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
//1. 獲取設定的連線超時時間,讀寫超時的時間,以及是否進行重連。
int connectTimeout = client.connectTimeoutMillis();
int readTimeout = client.readTimeoutMillis();
int writeTimeout = client.writeTimeoutMillis();
boolean connectionRetryEnabled = client.retryOnConnectionFailure();
try {
// 2. 獲取健康可用的連線
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
//3. 通過resultConnection初始化,對請求以及結果 編解碼的類(分http 1.1 和http 2.0)。
// 這裡主要是初始化,在後面一個攔截器才用到這相關的東西。
HttpCodec resultCodec = resultConnection.newCodec(client, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
複製程式碼
獲取連線
在上面的程式碼中最重要的,是註釋 第二點,獲取健康可用的連線findHealthyConnection,那我們繼續深入:
findHealthyConnection()
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
throws IOException {
// 1. 加了個死迴圈,一直找可用的連線
while (true) {
// 2. 這裡繼續去挖掘,尋找連線
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
connectionRetryEnabled);
// 3. 連線池同步獲取,上面找到的連線是否是一個新的連線,如果是的話,就直接返回了,就是我們需要找
// 的連線了
// If this is a brand new connection, we can skip the extensive health checks.
synchronized (connectionPool) {
if (candidate.successCount == 0) {
return candidate;
}
}
//4. 如果不是一個新的連線,那麼通過判斷,是否一個可用的連線。
// 裡面是通過Socket的一些方法進行判斷的,有興趣的,可以繼續研究一下
// 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;
}
}
複製程式碼
上面的程式碼,重要的也是註釋的第二點:尋找連線。繼續看findConnection。
findConnection()
/**
* Returns a connection to host a new stream. This prefers the existing connection if it exists,
* then the pool, finally building a new connection.
*/
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
boolean connectionRetryEnabled) throws IOException {
Route selectedRoute;
// 1. 同步執行緒池,來獲取裡面的連線
synchronized (connectionPool) {
// 2. 異常的處理。做些判斷,是否已經釋放,是否編解碼類為空,是否使用者已經取消 if (released) throw new IllegalStateException("released");
if (codec != null) throw new IllegalStateException("codec != null");
if (canceled) throw new IOException("Canceled");
// 3. 嘗試用一下現在的連線,判斷一下,是否有可用的連線。(使用已存在的連線)
// Attempt to use an already-allocated connection.
RealConnection allocatedConnection = this.connection;
if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
return allocatedConnection;
}
// 4. 嘗試在連線池中獲取一個連線,get方法中會直接呼叫,注意最後一個引數為空
// 裡面是一個for迴圈,在連線池裡面,尋找合格的連線
// 而合格的連線會通過,StreamAllocation中的acquire方法,更新connection的值。(從快取中獲取)
// Attempt to get a connection from the pool.
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
return connection;
}
selectedRoute = route;
}
//5. 判斷上面得到的線路,是否空,如果為空的,尋找一個可用的線路
// 對於線路的選,可以深究一下這個RouteSeletor
// 線路的選擇,多ip的支援 if (selectedRoute == null) {
selectedRoute = routeSelector.next();// 裡面又個神奇的遞迴
}
RealConnection result;
//6. 以上都不符合,建立一個連線
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
// 7. 由於上面我們獲取了一個線路,無論是新建的,或者已有的。
// 我們通過這個線路,繼續在連線池中尋找是否有可用的連線。
// Now that we have an IP address, make another attempt at getting a connection from the pool.
// This could match due to connection coalescing.
Internal.instance.get(connectionPool, address, this, selectedRoute);
if (connection != null) return connection;
// 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;
// 8. 如果前面這麼尋找,都沒在連線池中找打可用的連線,那麼就新建一個
result = new RealConnection(connectionPool, selectedRoute);
acquire(result);
}
// 9. 這裡就是就是連線的操作了,終於找到連線的正主了,這裡會呼叫RealConnection的連線方法,進行連線操作。
// 如果是普通的http請求,會使用Socket進行連線
// 如果是https,會進行相應的握手,建立通道的操作。
// 這裡就不對裡面的操作進行詳細分析了,有興趣可以在進去看看
// Do TCP + TLS handshakes. This is a blocking operation.
result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
routeDatabase().connected(result.route());
Socket socket = null;
// 10. 最後就是同步加到 連線池裡面了
synchronized (connectionPool) {
// Pool the connection.
Internal.instance.put(connectionPool, result);
// 最後加了一個多路複用的判斷,這個是http2才有的
// 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;
}
複製程式碼
建立連線
connect
建立連線是比較重要的一步了。如果是Https還有證照步驟
public void connect(
int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) {
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);
} else {
// 一般都走這條邏輯了,實際上很簡單就是socket的連線
connectSocket(connectTimeout, readTimeout);
}
// https的建立
establishProtocol(connectionSpecSelector);
break;
} catch (IOException e) {
closeQuietly(socket);
closeQuietly(rawSocket);
socket = null;
rawSocket = null;
source = null;
sink = null;
handshake = null;
protocol = null;
http2Connection = null;
if (routeException == null) {
routeException = new RouteException(e);
} else {
routeException.addConnectException(e);
}
if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
throw routeException;
}
}
}
if (http2Connection != null) {
synchronized (connectionPool) {
allocationLimit = http2Connection.maxConcurrentStreams();
}
}
}複製程式碼
我們進入connectSocket()函式看看,
connectSocket
private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
Proxy proxy = route.proxy();
Address address = route.address();
// 根據代理型別處理socket,為true
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy);
rawSocket.setSoTimeout(readTimeout);
try {
// 連線socket,之所以這樣寫是因為支援不同的平臺
/**
* 裡面實際上是 socket.connect(address, connectTimeout);
*
*/
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;
}
// 得到輸入/輸出流
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
}複製程式碼
如果使用的是https協議,並且配置有真實將會協議升級。
Https協議的建立 connectTls
如果使用的是https協議socket連線完成後還有一步,就是Tls的處理
private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
Address address = route.address();
SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
boolean success = false;
SSLSocket sslSocket = null;
try {
// 在原來socket上加一層ssl
// 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) {
if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
} finally {
if (sslSocket != null) {
Platform.get().afterHandshake(sslSocket);
}
if (!success) {
closeQuietly(sslSocket);
}
}
}複製程式碼
如果對https熟悉的話,應該知道,https就是在http的基礎上加了一層。
到此連結完成RealConnection例項化完成。