簡介
上一篇文章(OkHttp源碼解析(一):基本流程)介紹了 OkHttp 的基本流程,包括 Request 的創(chuàng)建、Dispatcher 對 Request 的調(diào)度以及 Interceptor 的使用。OkHttp 中默認會添加 RetryAndFollowUpInterceptor、BridgeInterceptor、CacheInterceptor、ConnectInterceptor 以及 CallServerInterceptor 這幾個攔截器。本文主要看一下 RetryAndFollupInterceptor 并引出建立連接相關的分析。
RetryAndFollowUpInterceptor
Interceptor 最主要的代碼都在 intercept
中,下面是 RetryAndFollowUpInterceptor#intercept
中的部分代碼:
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
streamAllocation = new StreamAllocation(
client.connectionPool(), createAddress(request.url()), callStackTrace); // 1
int followUpCount = 0;
Response priorResponse = null;
while (true) {
if (canceled) {
streamAllocation.release(); // 2
throw new IOException("Canceled");
}
...
response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null); // 3
...
}
上面注釋 1 處創(chuàng)建了一個 StreamAllocation
對象,注釋 2 處 調(diào)用了其 release
方法,注釋 3 處則把這個對象傳給了下一個 Interceptor。StreamAlloction
這個類很重要,下面就看一下它的用途。
StreamAlloction
StreamAllocation
從名字上看是流分配器,其實它是統(tǒng)籌管理了幾樣東西,注釋寫的非常清楚:
/**
* This class coordinates the relationship between three entities:
*
* <ul>
* <li><strong>Connections:</strong> physical socket connections to remote servers. These are
* potentially slow to establish so it is necessary to be able to cancel a connection
* currently being connected.
* <li><strong>Streams:</strong> logical HTTP request/response pairs that are layered on
* connections. Each connection has its own allocation limit, which defines how many
* concurrent streams that connection can carry. HTTP/1.x connections can carry 1 stream
* at a time, HTTP/2 typically carry multiple.
* <li><strong>Calls:</strong> a logical sequence of streams, typically an initial request and
* its follow up requests. We prefer to keep all streams of a single call on the same
* connection for better behavior and locality.
* </ul>
簡單來說, StreamAllocation
協(xié)調(diào)了 3 樣東西:
-
Connections
: 物理的 socket 連接 -
Streams
:邏輯上的 HTTP request/response 對。每個Connection
有個變量allocationLimit
,用于定義可以承載的并發(fā)的streams
的數(shù)量。HTTP/1.x 的Connection
一次只能有一個stream
, HTTP/2 一般可以有多個。 -
Calls
:Streams
的序列。一個初始的request
可能還會有后續(xù)的request
(如重定向)。OkHttp 傾向于讓一個call
所有的streams
運行在同一個connection
上。
StreamAllocation
提供了一些 API 來釋放以上的資源對象。 在 RetryAndFollowUpInterceptor
中創(chuàng)建的 StreamAllocation
對象下一個用到的地方是 ConnectInterceptor,其 intercept
代碼如下:
@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, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
在上面的代碼中, streamAllocation
創(chuàng)建了 httpCodec
以及 connection
對象。 httpCodec
即是上面所說的 Streams
,而 connection
則是上面的 Connection
,Connection
是一個接口,它的唯一實現(xiàn)類是 RealConnection
。
newStream
StreamAllocation
中的 newStream
方法用于尋找新的 RealConnection
以及 HttpCodec
,代碼如下:
public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
int connectTimeout = client.connectTimeoutMillis();
int readTimeout = client.readTimeoutMillis();
int writeTimeout = client.writeTimeoutMillis();
boolean connectionRetryEnabled = client.retryOnConnectionFailure();
try {
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
HttpCodec resultCodec = resultConnection.newCodec(client, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
在 newStream
中,通過 findHealthyConnection
找到可用的 Connection
,并用這個 Connection
生成一個 HttpCodec
對象。 findHealthyConnection
是找到一個健康的連接,代碼如下:
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
throws IOException {
while (true) {
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
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)) {
noNewStreams();
continue;
}
return candidate;
}
}
public boolean isHealthy(boolean doExtensiveChecks) {
if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
return false;
}
... // 省略 Http2 代碼
return true;
}
在一個無限循環(huán)中,通過 findConnection
尋找一個 connection
,并判斷是否可用,首先如果沒有使用過的肯定是健康的可直接返回,否則調(diào)用 isHealthy
,主要就是判斷 socket
是否關閉。這里的 socket
是在 findConnection
中賦值的,再看看 findConnection
的代碼:
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
boolean connectionRetryEnabled) throws IOException {
Route selectedRoute;
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.
// 1. 從 ConnectionPool 取得 connection
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
return connection;
}
selectedRoute = route;
}
// If we need a route, make one. This is a blocking operation.
if (selectedRoute == null) {
selectedRoute = routeSelector.next();
}
RealConnection result;
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
// Now that we have an IP address, make another attempt at getting a connection from the pool.
// 2. 有了 ip 地址后再從 connectionpool中取一次
// 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;
// 3. ConnectionPool 中沒有,新創(chuàng)建一個
result = new RealConnection(connectionPool, selectedRoute);
// 3. 將 StreamAllocation 加入到 `RealConnection` 中的一個隊列中
acquire(result);
}
// Do TCP + TLS handshakes. This is a blocking operation.
// 4. 建立連接,在其中創(chuàng)建 socket
result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
routeDatabase().connected(result.route());
Socket socket = null;
synchronized (connectionPool) {
// Pool the connection.
// 5. 將新創(chuàng)建的 connection 放到 ConnectionPool 中
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;
}
上面 Connection
的創(chuàng)建大體是以下幾個步驟:
- 調(diào)用
Intenal.get
方法從ConnectionPool
中獲取一個Connection
,主要根據(jù) url 的 host 判斷,相關代碼在ConnectionPool
中。 - 如果沒有并且又獲取了 IP 地址,則再獲取一次。
- 如果
ConnectionPool
中沒有, 則新創(chuàng)建一個RealConnection
,并調(diào)用acquire
將StreamAllocation
中加入RealConnection
中的一個隊列中。 - 調(diào)用
RealConnection#connect
方法建立連接,在內(nèi)部會創(chuàng)建Socket
。 - 將新創(chuàng)建的
Connection
加入到ConnectionPool
中。
獲取到了 Connection
之后,再創(chuàng)建一個 HttpCodec
對象。
public HttpCodec newCodec(
OkHttpClient client, StreamAllocation streamAllocation) throws SocketException {
if (http2Connection != null) {
return new Http2Codec(client, streamAllocation, http2Connection);
} else {
socket.setSoTimeout(client.readTimeoutMillis());
source.timeout().timeout(client.readTimeoutMillis(), MILLISECONDS);
sink.timeout().timeout(client.writeTimeoutMillis(), MILLISECONDS);
return new Http1Codec(client, streamAllocation, source, sink);
}
}
根據(jù)是 Http1 還是 Http2 創(chuàng)建對應的 HttpCodec
, 其中的 socket
是在 RealConnection
中的 connect
方法創(chuàng)建的。下面具體看看RealConnection
。
RealConnection
RealConnection
封裝的是底層的 Socket
連接,內(nèi)部必然有一個 Socket
對象,下面是 RealConnection
內(nèi)部的變量:
public final class RealConnection extends Http2Connection.Listener implements Connection {
private static final String NPE_THROW_WITH_NULL = "throw with null exception";
private final ConnectionPool connectionPool;
private final Route route;
// The fields below are initialized by connect() and never reassigned.
/** The low-level TCP socket. */
private Socket rawSocket;
/**
* The application layer socket. Either an {@link SSLSocket} layered over {@link #rawSocket}, or
* {@link #rawSocket} itself if this connection does not use SSL.
*/
private Socket socket;
private Handshake handshake;
private Protocol protocol;
private Http2Connection http2Connection;
private BufferedSource source;
private BufferedSink sink;
// The fields below track connection state and are guarded by connectionPool.
/** If true, no new streams can be created on this connection. Once true this is always true. */
public boolean noNewStreams;
public int successCount;
/**
* The maximum number of concurrent streams that can be carried by this connection. If {@code
* allocations.size() < allocationLimit} then new streams can be created on this connection.
*/
public int allocationLimit = 1;
/** Current streams carried by this connection. */
public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();
/** Nanotime timestamp when {@code allocations.size()} reached zero. */
public long idleAtNanos = Long.MAX_VALUE;
...
}
-
Route
表示的是與服務端建立的路徑,其實內(nèi)部封裝了Address
,Address
則是封裝了請求的 URL。 -
rawSocket
對象代表底層的連接,還有一個socket
是用于 Https, 對于普通的 Http 請求來說,這兩個對象是一樣的。source
和sink
則是利用 Okio 封裝socket
得到的輸入輸出流。(如果想了解 Okio 的原理,可以參考我之前的文章:Okio 源碼解析(一):數(shù)據(jù)讀取流程) -
noNewStream
對象用于標識這個Connection
不能再用于 Http 請求了,一旦設置為 true, 則不會再變。 -
allocationLimit
指的是這個Connection
最多能同時承載幾個 Http 流,對于 Http/1 來說只能是一個。 -
allocations
是一個List
對象,里面保存著正在使用這個Connection
的StreamAllocation
的弱引用,當StreamAllocation
調(diào)用acquire
時,便會將其弱引用加入這個List
,調(diào)用release
則是移除引用。allocations
為空說明此Connection
為閑置,ConnectionPool
利用這些信息來決定是否關閉這個連接。
connect
RealConnection
用于建立連接,里面有相應的 connect
方法:
public void connect(
int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) {
...
while (true) {
try {
if (route.requiresTunnel()) {
connectTunnel(connectTimeout, readTimeout, writeTimeout);
} else {
// 創(chuàng)建socket,建立連接
connectSocket(connectTimeout, readTimeout);
}
// 建立
establishProtocol(connectionSpecSelector);
break;
}
...
}
private void connectSocket(int connectTimeout, int readTimeout) 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);
try {
// 建立連接,相當于調(diào)用 socket 的 connect 方法
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 {
// 獲取輸入輸出流
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);
}
}
}
如果不是 Https, 則調(diào)用 connectSocket
,在內(nèi)部創(chuàng)建 rawSocket
對象,設置超時時間。緊接著 Platform.get().connectSocket
根據(jù)不同的平臺調(diào)用相應的 connect
方法,這樣 rawSocket
就連接到服務端了。然后是用 Okio
封裝 rawSocket
的輸入輸出流,這里的輸入輸出流最終是交給 HttpCodec
進行 Http 報文的寫入都讀取。通過以上步驟,就實現(xiàn)了 Http 請求的連接。
總結
本文從 RetryAndFollowupIntercept
中創(chuàng)建 StreamAllocation
對象,到 Connection
中創(chuàng)建 RealConnection
和 HttpCodec
,分析了 OkHttp 建立連接的基本過程。可以看出, OkHttp 中的連接由
RealConnection
封裝,Http 流的輸入輸出由 HttpCodec
操作,而 StreamAllocation
則統(tǒng)籌管理這些資源。在連接的尋找與創(chuàng)建過程,有個關鍵的東西是 ConnectionPool
, 即連接池。它負責管理所有的 Connection
,OkHttp 利用這個連接池進行 Connection
的重用以提高網(wǎng)絡請求的效率。本文并沒有詳細分析 ConnectionPool
,相關內(nèi)容可以參見下一篇: OkHttp源碼解析(三):連接池。