本篇文章為okhttp源碼學(xué)習(xí)筆記系列的第二篇文章,本篇文章的主要內(nèi)容為okhttp中的連接與連接的管理,因此需要重點(diǎn)介紹連接的概念。客戶端通過HTTP協(xié)議與服務(wù)器進(jìn)行通信,首先需要建立連接,okhttp并沒有使用URLConnection, 而是對socket直接進(jìn)行封裝,在socket之上建立了connection的概念,代表這物理連接。同時,一對請求與響應(yīng)對應(yīng)著輸出和輸入流, okhttp中同樣使用流的邏輯概念,建立在connection的物理連接之上進(jìn)行數(shù)據(jù)通信,(只不過不知道為什么okhttp中流的類名為HttpCodec)。在HTTP1.1以及之前的協(xié)議中,一個連接只能同時支持單個流,而SPDY和HTTP2.0協(xié)議則可以同時支持多個流,本文先考慮HTTP1.1協(xié)議的情況。connection負(fù)責(zé)與遠(yuǎn)程服務(wù)器建立連接, HttpCodec負(fù)責(zé)請求的寫入與響應(yīng)的讀出,但是除此之外還存在在一個連接上建立新的流,取消一個請求對應(yīng)的流,釋放完成任務(wù)的流等操作,為了使HttpCodec不至于過于復(fù)雜,okhttp中引入了StreamAllocation負(fù)責(zé)管理一個連接上的流,同時在connection中也通過一個StreamAllocation的引用的列表來管理一個連接的流,從而使得連接與流之間解耦。另外,熟悉HTTP協(xié)議的同學(xué)肯定知道建立連接是昂貴的,因此在請求任務(wù)完成以后(即流結(jié)束以后)不會立即關(guān)閉連接,使得連接可以復(fù)用,okhttp中通過ConnecitonPool來完成連接的管理和復(fù)用。
因此本文會首先從兩個攔截器開始引入StreamAllocation的概念,其次會依次介紹Connection和的一個實(shí)現(xiàn)類Connection,最后再通過分析ConnectionPool來分析連接的管理機(jī)制,期間還有涉及Rout, RoutSelector等一些為建立連接或建立流有關(guān)的輔助類。
0. 從兩個攔截器開始
在上一篇文章中我們了解到okhttp中的網(wǎng)絡(luò)請求過程其實(shí)就是一系列攔截器的處理過程,那么我們就可以以這些攔截器為主線看一下okhttp的實(shí)現(xiàn)中都做了哪些事情,首先我們再次貼出RealCall#getResponseWithInterceptor方法:
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, 0, originalRequest);
return chain.proceed(originalRequest);
}
從方法中可以看出okhttp內(nèi)部定義了五個攔截器,分別負(fù)責(zé)重試(包括失敗重連,添加認(rèn)證頭部信息,重定向等),request和response的轉(zhuǎn)換(主要是將Request和Response對象轉(zhuǎn)換成Http協(xié)議定義的報(bào)文形式), 緩存以及連接處理,最后一個與其他稍有不同,它負(fù)責(zé)流的處理, 將請求數(shù)據(jù)寫入到socket的輸出流,并從輸入流中獲取響應(yīng)數(shù)據(jù),這個在第三篇文章中做分析。
本篇文章的重點(diǎn)是連接以及連接管理,可以看出與連接有關(guān)的時RetryAndFollowUpInterceptor和ConnectionInterceptor,那么我們就從這兩個攔截器開始分析。
在前面一篇文章中我們了解到攔截器會對外提供一個方法,即intercept(chain)方法,在該方法中處理請求,并調(diào)用chain.proceed()方法獲取響應(yīng),處理后返回響應(yīng)結(jié)果,那么我們首先來看RetryAndFollowUpInterceptor#intercept()方法:
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
streamAllocation = new StreamAllocation(
client.connectionPool(), createAddress(request.url()), callStackTrace);
int followUpCount = 0;
Response priorResponse = null;
while (true) {
...
Response response = null;
boolean releaseConnection = true;
try {
response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (... e) {
...
} finally {
// We're throwing an unchecked exception. Release any resources.
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
...
這里的代碼只是前一半,并且做了部分簡化,由于本篇文章分析的是連接與連接管理,因此這里我們只關(guān)注與連接相關(guān)的部分。這里我們看到在該方法中,首先創(chuàng)建了StreamAllocation對象,該對象是連接與流的橋梁,okhttp處理一個請求時,它負(fù)責(zé)為該請求找到一個合適的連接(找到是指從連接池中復(fù)用連接或者創(chuàng)建新連接), 并在該連接上創(chuàng)建流對象,通過該流對象完成數(shù)據(jù)通信。這里的流對象負(fù)責(zé)數(shù)據(jù)通信,即向socket的輸出流中寫請求數(shù)據(jù),并從socket的輸出流中讀取響應(yīng)數(shù)據(jù),而StreamAllocation則負(fù)責(zé)管理流,包括為請求查找連接,并在該連接上建立流對象,將連接封裝的socket對象中的輸入輸出流傳遞到okhttp流對象中,使他去完成數(shù)據(jù)通信任務(wù),這是一種功能或責(zé)任的分離,這一點(diǎn)有點(diǎn)像之前分析的Request對象和Call對象的關(guān)系,另外一個連接也是通過持有一個關(guān)于StreamAllocation的集合來管理一個連接上的多個流(只有在SPDY和HTTP2上存在一個連接上多個流,HTTP1.1及1.0則是在一個連接上同時只有一個流對象)。
從代碼中我們看到,創(chuàng)建的streamAllocation對象傳遞到proceed()方法中,之前對于攔截器鏈的分析中我們知道,在proceed()方法中遞歸地創(chuàng)建新的chain對象,并添加proceed()方法中傳遞進(jìn)來的對象,提供給后面的攔截器使用,這包括StreamAllocation, Connection, HttpCodec三個主要的類的對象,其中最后一個就是所謂的流對象(不太清楚okhttp為何如此命名)。
RetryAndFollowUpInterceptor#intercept()方法中其他的邏輯我們在之后的文章中再做分析,這里我們主要是需要了解StreamAllocation對象,以及繼續(xù)熟悉okhttp中攔截器鏈的執(zhí)行機(jī)制,下面我們再來分析Connection#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);
}
這里沒有對代碼做簡化,可以看出ConnectionInterceptor的代碼很簡單,我們暫且忽略doExtensiveHealthChecks,這個跟具體的HTTP協(xié)議的規(guī)則有關(guān), 這里最關(guān)鍵的是streamAllocation的方法,即newStream()方法,該方法負(fù)責(zé)尋找連接,并在該連接上建立新的流對象,并返回,而connection()方法只不過是分會找到的連接而已,這里看到三個對象在這時都已經(jīng)分別創(chuàng)建出來,此時傳遞到InterceptorChain上,CallServerInterceptor就可以使用這些對象完成數(shù)據(jù)通信了,這一點(diǎn)在下一篇文章中分析流對象時再做分析,這里我們只是關(guān)注連接,那么我們就要StreamAllocation開始。
1. 連接與流的橋梁
首先我們明白HTTP通信執(zhí)行網(wǎng)絡(luò)請求需要在連接之上建立一個新的流執(zhí)行該任務(wù), 我們這里將StreamAllocation稱之為連接與流的橋梁, 它負(fù)責(zé)為一次請求尋找連接并建立流,從而完成遠(yuǎn)程通信,所以StreamAllocation與請求,連接,流都相關(guān),因此我們首先熟悉一下這三個概念,對于它們?nèi)齻€, StreamAllocation類之前的注釋已經(jī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>
...
**/
注釋說明的很清楚,Connection時建立在Socket之上的物理通信信道,而Stream則是代表邏輯的HTTP請求/響應(yīng)對, 至于Call,是對一次請求任務(wù)或是說請求過程的封裝,在第一篇文章中我們已經(jīng)做出介紹,而一個Call可能會涉及多個流(如請求重定向,auth認(rèn)證等情況), 而Okhttp使用同一個連接完成這一系列的流上的請求任務(wù),這一點(diǎn)的實(shí)現(xiàn)我們將在介紹RetryInterceptor的部分中說明。
下面我們來思考StreamAllocation所要解決的問題,簡單來講就是在合適的連接之上建立一個新的流,這個問題劃分為兩步就是尋找連接和新建流。那么,StreamAllocation的數(shù)據(jù)結(jié)構(gòu)中應(yīng)該包含Stream(okhttp中將接口的名字定義為HttpCodec), Connction, 其次為了尋找合適的連接,應(yīng)該包含一個URL地址, 連接池ConnectionPool, 而方法則應(yīng)該包括我們前面提到的newStream()方法, 而findConnection則是為之服務(wù)的方法,其次在完成請求任務(wù)之后應(yīng)該有finish方法用來關(guān)閉流對象,還有終止和取消等方法, 以及釋放資源的方法。下面我們從StreamAllocation中尋找對應(yīng)的屬性和方法, 首先來看它的屬性域:
public final class StreamAllocation {
public final Address address;
private Route route;
private final ConnectionPool connectionPool;
private final Object callStackTrace;
// State guarded by connectionPool.
private final RouteSelector routeSelector;
private int refusedStreamCount;
private RealConnection connection;
private boolean released;
private boolean canceled;
private HttpCodec codec;
public StreamAllocation(ConnectionPool connectionPool, Address address, Object callStackTrace) {
this.connectionPool = connectionPool;
this.address = address;
this.routeSelector = new RouteSelector(address, routeDatabase());
this.callStackTrace = callStackTrace;
}
...
這里我們看并沒有使用URL代表一個地址,而是使用Address對象, 如果查看其代碼可以看到它封裝了一個URL, 其中還包括dns, 代理等信息,可以更好地滿足HTTP協(xié)議中規(guī)定的細(xì)節(jié)。使用Address對象也可以直接ConnectionPool中查找對應(yīng)滿足條件的連接,同時Address對象可以用于在RoutSelector查詢一個合適的路徑Rout對象,該Route對象可以用于建立連接, 然后我們忽略標(biāo)志位和統(tǒng)計(jì)信息,剩下的則是Connection和HttpCodec對象,這里我們先忽略調(diào)用棧callStackTrace, 不考慮。以上就是它所有的屬性域,那么下面我們再來看它最重要的方法,即新建流的方法:
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;
if (resultConnection.http2Connection != null) {
resultCodec = new Http2Codec(client, this, resultConnection.http2Connection);
} else {
resultConnection.socket().setSoTimeout(readTimeout);
resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS);
resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS);
resultCodec = new Http1Codec(
client, this, resultConnection.source, resultConnection.sink);
}
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
其流程很明確,我們先不考慮HTTP2.0的情況, 流程就是找到合適的良好連接,然后實(shí)例化HttpCodec對象,并設(shè)置對應(yīng)的屬性,返回該流對象即可,該流對象依賴連接的輸入流和輸出流,從而可以在流之上進(jìn)行請求的寫入和響應(yīng)的讀出。下面來開findConnection的代碼:
/**
* Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
* until a healthy connection is found.
*/
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記錄該連接上執(zhí)行流任務(wù)的次數(shù),為零說明是新建立的連接
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(); //在該方法中會設(shè)置該Allocation對應(yīng)的Connection對象的noNewStream標(biāo)志位,標(biāo)識這在該連接不再使用,在回收的線程中會將其回收
continue;
}
return candidate;
}
}
/**
* 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;
synchronized (connectionPool) {
//一系列條件判斷
...
RealConnection allocatedConnection = this.connection;
if (allocatedConnection != null && !allocatedConnection.noNewStreams) {//noNewStream是一個標(biāo)識為,標(biāo)識該連接不可用
return allocatedConnection;
}
// Attempt to get a connection from the pool.
//可以在OkhttpClient中查看到該方法, 其實(shí)就是調(diào)用connnctionPool.get(address, streamAllocation);
RealConnection pooledConnection = Internal.instance.get(connectionPool, address, this);
if (pooledConnection != null) {
this.connection = pooledConnection;
return pooledConnection;
}
selectedRoute = route;
}
if (selectedRoute == null) {
selectedRoute = routeSelector.next(); //選擇下一個路線Rout
synchronized (connectionPool) {
route = selectedRoute;
refusedStreamCount = 0;
}
}
RealConnection newConnection = new RealConnection(selectedRoute);
synchronized (connectionPool) {
acquire(newConnection); //1. 將該StreamAllocation對象,即this 添加到Connection對象的StreamAllocation引用列表中,標(biāo)識在建立新的流使用到了該連接
Internal.instance.put(connectionPool, newConnection); //2. 將新建的連接加入到連接池, 與get方法類型,也是在OkHttpClient調(diào)用的pool.put()方法
this.connection = newConnection;
if (canceled) throw new IOException("Canceled");
}
newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.connectionSpecs(),
connectionRetryEnabled); //3. 連接方法,將在介紹Connection的部分介紹
routeDatabase().connected(newConnection.route()); //4.新建的連接一定可用,所以將該連接移除黑名單
return newConnection;
}
...
/**
* Use this allocation to hold {@code connection}. Each call to this must be paired with a call to
* {@link #release} on the same connection.
*/
public void acquire(RealConnection connection) {
assert (Thread.holdsLock(connectionPool));
connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}
這兩個方法完成了尋找合適的連接的功能,這里我們可以將其分成兩種類型:
- 從連接池中復(fù)用連接,這一點(diǎn)我們在連接管理部分中會有介紹,其實(shí)就是從維護(hù)的隊(duì)列中找到合適的連接并返回,查找的依據(jù)就是Address對象;
- 新建連接,新建連接需要首先查找一個合適的路徑,然后在該路徑上實(shí)例化一個新的connection對象,在建立一個新的連接以后需要執(zhí)行一系列的步驟,在代碼中已經(jīng)用注釋的方式分四步標(biāo)出。
在或許到連接以后還需要執(zhí)行檢查過程,通常來說以上兩種類型選擇的連接都需要執(zhí)行檢查,只是這里新的連接一定可用,所以會跳過檢查,其實(shí)檢查的過程就是查看該連接的socket是否被關(guān)閉以及是否可以正常使用,有興趣的可以自行查看代碼。
在建立新的連接以后需要處理一些事情,代碼中的中文注釋分了四步將其標(biāo)出,其中需要說明第一步,acquire方法,connection中維護(hù)這一張?jiān)谝粋€連接上的流的鏈表,該鏈表保存的是StreamAllocation的引用 Connction的該鏈表為空時說明該連接已經(jīng)可以回收了,這部分在連接管理部分會有說明。其實(shí)在調(diào)用ConnectionPool.get()方法時,傳入StreamAllocation對象也是為了調(diào)用StreamAllocation#acquire方法,將該StreamAllocation對象的引用添加到連接對應(yīng)的鏈表中,用于管理一個連接上的流。
此外還需要說明的兩點(diǎn),一是關(guān)于Internal, 這是一個抽象類,該類只有一個實(shí)現(xiàn)類,時HttpClient的一個匿名內(nèi)部類,該類的一系列方法都是一個功能,就是將okhttp3中一些包訪問權(quán)限的方法對外提供一個public的訪問方法,至于為什么這么實(shí)現(xiàn),目前還不太清楚,估計(jì)是在Okhttp的框架中方便使用包權(quán)限的方法, 如ConnectionPool的put和get方法,在Okhttp的框架代碼中通過Internal代理訪問, 而在外部使用時無法訪問到這些方法,至于還有沒有其他考慮有清楚的同學(xué)歡迎賜教。
需要說明的第二點(diǎn)是RouteDatabase是一個黑名單,記錄著不可用的路線,避免在同一個坑里栽倒兩次,connect()方法就是將該路線移除黑名單,這里為了不影響StreamAllocation分析的連貫性,本文將RoutSelector和RouteDatabase等與路線選擇相關(guān)的代碼放到附錄部分,這里我們暫且先了解它的功能而不關(guān)注它的實(shí)現(xiàn)。
至此就分析完了建立新流的過程,那么剩下的就是釋放資源的邏輯,由于這一部分很多地方都與連接的復(fù)用管理,流的操作以及RetryAndFollowUpInterceptor中的邏輯有關(guān),所以在這里暫且略過,后續(xù)部分再做分析,我們此處重點(diǎn)是了解如何查詢連接,如何新建連接以及如何新建流對象。
2. 連接
下面開始分析連接對象,在okhttp中定義了Connection的接口,而該接口在okhttp只有一個實(shí)現(xiàn)類,即RealConnetion,下面我們重點(diǎn)分析該類。該類的主要功能就是封裝Socket并對外提供輸入輸出流,那么它的內(nèi)部結(jié)構(gòu)也很容易聯(lián)想到,它應(yīng)該持有一個Socket, 并提供一個輸入流一個輸入流,在功能方法中對外提供connect()方法建立連接。這里由于okhttp是支持Https和HTTP2.0,如果不考慮這兩種情況,RealConnection的代碼將會比較簡單,下面首先來看它的屬性域:
public final class RealConnection extends Http2Connection.Listener implements Connection {
private final Route route;
/** 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.
*/
public Socket socket;
...
private Protocol protocol;
...
public int successCount;
public BufferedSource source;
public BufferedSink sink;
public int allocationLimit;
public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();
public boolean noNewStreams;
public long idleAtNanos = Long.MAX_VALUE;
public RealConnection(Route route) {
this.route = route;
}
...
RealConnection通過一個Route路線(或者說路由或路徑)來建立連接,它封裝一個Socket, 由于考慮到Https的情況,socket有可能時SSLSocket或者RawSocket, 所以這里有兩個socket域, 一個在底層,一個在上層,由于我們不考慮Https的情況,那么兩個就是等價的,我們只需要明白內(nèi)部建立rawSocket, 對外提供socket就可以了(注釋中也已經(jīng)明白解釋)。另外除了輸入流和輸出流之外還有連接所使用到的協(xié)議,在連接方法中會用到,最后剩下的就是跟連接管理部分相關(guān)的統(tǒng)計(jì)信息,allocationLimit是分配流的數(shù)量上限,對應(yīng)HTTP1.1來說它就是1, allocations在StreamAllocation部分我們已經(jīng)熟悉,它是用來統(tǒng)計(jì)在一個連接上建立了哪些流,通過StreamAllocation的acquire方法和release方法可以將一個allocation對象添加到鏈表或者移除鏈表,(不太清楚這兩個方法放在connection中是不是更合理一些), noNewStream之前說過可以簡單理解為它標(biāo)識該連接已經(jīng)不可用,idleAtNanos記錄該連接處于空閑狀態(tài)的時間,這些將會在第三部分連接管理中做介紹,這里暫且略過不考慮。
下面就開始看它的連接方法, connect()方法
public void connect(int connectTimeout, int readTimeout, int writeTimeout,
List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) {
if (protocol != null) throw new IllegalStateException("already connected");
...
while (protocol == null) {
try {
buildConnection(connectTimeout, readTimeout, writeTimeout, connectionSpecSelector);
} catch (IOException e) {
...
}
}
這里對代碼做了最大的簡化,主要去掉了異常處理的部分以及Https需要考慮的部分,從代碼中可以看出,建立連接是通過判斷protocol是否為空來確定是否已經(jīng)建立 連接的, 下面就繼續(xù)看buildeConnection()方法:
private void buildConnection(int connectTimeout, int readTimeout, int writeTimeout,
ConnectionSpecSelector connectionSpecSelector) throws IOException {
connectSocket(connectTimeout, readTimeout);
establishProtocol(readTimeout, writeTimeout, connectionSpecSelector);
}
private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
Proxy proxy = route.proxy();
Address address = route.address();
//根據(jù)是否需要代理建立不同的Socket
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy);
rawSocket.setSoTimeout(readTimeout);
try {
//內(nèi)部就是調(diào)用socket.connect(InetAddress, timeout)方法建立連接
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
} catch (ConnectException e) {
...
}
//獲取輸入輸出流
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
}
private void establishProtocol(int readTimeout, int writeTimeout,
ConnectionSpecSelector connectionSpecSelector) throws IOException {
if (route.address().sslSocketFactory() != null) {
...
} else {
protocol = Protocol.HTTP_1_1;
socket = rawSocket;
}
if (protocol == Protocol.HTTP_2) {
...
} else {
//HTTP1.1及以下版本中, 每個連接只允許有一個流
this.allocationLimit = 1;
}
}
這里同樣對代碼做了簡化,并在重要的地方做了注釋, 流程也很清楚,就是建立socket連接,并獲取輸入輸出流,然后設(shè)置正確的協(xié)議,此時connect()方法就可以跳出while循環(huán),完成連接。
至此,就完成了連接過程,所以如果除去HTTPs和HTTP2的部分,RealConnection的代碼很簡單,不過對于HTTPS和HTTP2的處理還是挺多,后續(xù)還會繼續(xù)學(xué)習(xí)。下面再看另外一個概念,流。
3. 連接管理
對于連接的管理主要是分析ConnectionPool,以連接池的形式管理連接的復(fù)用, okhttp中盡可能對于相同地址的遠(yuǎn)程通信復(fù)用同一個連接,這樣就節(jié)省了連接的代價。那么我們現(xiàn)在明白了ConnectionPool所要解決的問題就可以去思考它應(yīng)當(dāng)如何實(shí)現(xiàn),它應(yīng)當(dāng)具備的功能可以簡單分為三個方法,get, put, cleanup,即獲取添加和清理的方法。其中最重要也是最復(fù)雜的是清理,即在連接池放滿的情況下,如何選擇需要淘汰的連接。這里需要考慮的指標(biāo)包括連接的數(shù)量,連接的空閑時長等,那么下面我們來看ConnectionPool的代碼:
/**
* Manages reuse of HTTP and HTTP/2 connections for reduced network latency. HTTP requests that
* share the same {@link Address} may share a {@link Connection}. This class implements the policy
* of which connections to keep open for future use.
*/
public final class ConnectionPool {
/**
* Background threads are used to cleanup expired connections. There will be at most a single
* thread running per connection pool. The thread pool executor permits the pool itself to be
* garbage collected.
*/
private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));
boolean cleanupRunning;
private final Runnable cleanupRunnable = new Runnable() {
...
};
/** The maximum number of idle connections for each address. */
private final int maxIdleConnections;
private final long keepAliveDurationNs;
private final Deque<RealConnection> connections = new ArrayDeque<>();
final RouteDatabase routeDatabase = new RouteDatabase();
/**
* Create a new connection pool with tuning parameters appropriate for a single-user application.
* The tuning parameters in this pool are subject to change in future OkHttp releases. Currently
* this pool holds up to 5 idle connections which will be evicted after 5 minutes of inactivity.
*/
public ConnectionPool() {
this(5, 5, TimeUnit.MINUTES);
}
public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
this.maxIdleConnections = maxIdleConnections;
this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);
// Put a floor on the keep alive duration, otherwise cleanup will spin loop.
if (keepAliveDuration <= 0) {
throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
}
}
首先來看它的屬性域,最主要的就是connections,可見在ConnectionPool內(nèi)部以隊(duì)列的方式存儲連接,而routeDatabase是一個黑名單,用來記錄不可用的route,但是看代碼目測ConnectionPool中并沒有使用它,可能后續(xù)還有其他用處,此處不做分析。剩下的就是與清理相關(guān)的,從名字則可以看出他們的用途,最開始的三個分別是執(zhí)行清理任務(wù)的線程池,標(biāo)志位以及清理任務(wù),而maxIdleConnections和keepAliveDurationNs則是清理中淘汰連接的指標(biāo),這里需要說明的是maxIdleConnections是值每個地址上最大的空閑連接數(shù)(如注釋所說),看來okhttp只是限制了與同一個遠(yuǎn)程服務(wù)器的空閑連接數(shù)量,對整體的空閑連接數(shù)量并沒有做限制,但是從代碼來看并不是如此,該值時標(biāo)識該連接池中整體的空閑連接的最大數(shù)量,當(dāng)一個連接數(shù)量超過這個值時則會觸發(fā)清理,這里留有疑問,不知注釋是何意, 另外還有一個疑問是okhttp并沒有限制一個連接池中連接的最大數(shù)量,而只是限制了連接池中的最大空閑連接數(shù)量,由于不懂HTTP協(xié)議,水平也有限,不太清楚這里需不需要限制,有了解的歡迎在評論中告知,不勝感激。
最后我們可以從默認(rèn)的構(gòu)造器中看出okhttp允許每個地址同時可以有五個連接,每個連接空閑時間最多為五分鐘。
下面我們首先來看較為簡單一些的get和put方法
/** Returns a recycled connection to {@code address}, or null if no such connection exists. */
RealConnection get(Address address, StreamAllocation streamAllocation) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.allocations.size() < connection.allocationLimit
&& address.equals(connection.route().address)
&& !connection.noNewStreams) {
streamAllocation.acquire(connection);
return connection;
}
}
return null;
}
獲取一個connection時按照Address來匹配,而建立連接也是通過Address查詢一個合適的Route來建立的,當(dāng)匹配到連接時,會將新建立的流對應(yīng)的StreamAllocation添加到connection.allocations中, 如果這里調(diào)用connection.allocations.add(StreamAllocation)或者Connection自定義的add方法會不會更清晰一些,不太明白為什么將該任務(wù)放在了StreamAllocation的acquire方法中,此處的streamAllocation的acquire方法其實(shí)也就是做了這件事情,用來管理一個連接上的流。
put方法
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
put方法更為簡單,就是異步觸發(fā)清理任務(wù),然后將連接添加到隊(duì)列中。那么下面開始重點(diǎn)分析它的清理過程,首先來看清理任務(wù)的定義:
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
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) {
}
}
}
}
}
};
邏輯也很簡單,就是調(diào)用cleanup方法執(zhí)行清理,并等待一段時間,持續(xù)清理,而等待的時間長度時有cleanup函數(shù)返回值指定的,那么我們繼續(xù)來看cleanup函數(shù)
/**
* Performs maintenance on this pool, evicting the connection that has been idle the longest if
* either it has exceeded the keep alive limit or the idle connections limit.
*
* <p>Returns the duration in nanos to sleep until the next scheduled call to this method. Returns
* -1 if no further cleanups are required.
*/
long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
// Find either a connection to evict, or the time that the next eviction is due.
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
// If the connection is in use, keep searching.
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
//統(tǒng)計(jì)空閑連接的數(shù)量
idleConnectionCount++;
// If the connection is ready to be evicted, we're done.
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {//找出空閑時間最長的連接以及對應(yīng)的空閑時間
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// We've found a connection to evict. Remove it from the list, then close it below (outside
// of the synchronized block).
connections.remove(longestIdleConnection); //在符合清理?xiàng)l件下,清理空閑時間最長的連接
} else if (idleConnectionCount > 0) {
// A connection will be ready to evict soon.
return keepAliveDurationNs - longestIdleDurationNs; //不符合清理?xiàng)l件,則返回下次需要執(zhí)行清理的等待時間
} else if (inUseConnectionCount > 0) {
// All connections are in use. It'll be at least the keep alive duration 'til we run again.
return keepAliveDurationNs; //沒有空閑的連接,則隔keepAliveDuration之后再次執(zhí)行
} else {
// No connections, idle or in use.
cleanupRunning = false; //清理結(jié)束
return -1;
}
}
closeQuietly(longestIdleConnection.socket()); //關(guān)閉socket資源
// Cleanup again immediately.
return 0; //這里是在清理一個空閑時間最長的連接以后會執(zhí)行到這里,需要立即再次執(zhí)行清理
}
這里的首先統(tǒng)計(jì)空閑連接數(shù)量,然后查找最長空閑時間的連接以及對應(yīng)空閑時長,然后判斷是否超出最大空閑連接數(shù)量或者超過最大空閑時長,滿足其一則執(zhí)行清理最長空閑時長的連接,然后立即再次執(zhí)行清理,否則會返回對應(yīng)的等待時間,代碼中中文注釋已做說明。方法中用到了一個方法來查看一個連接上分配流的數(shù)量,這里不再貼出,有興趣的可以自行查看。
接下來我們梳理一下清理的任務(wù),清理任務(wù)是異步執(zhí)行的,遵循兩個指標(biāo),最大空閑連接數(shù)量和最大空閑時長,滿足其一則清理空閑時長最大的那個連接,然后循環(huán)執(zhí)行,要么等待一段時間,要么繼續(xù)清理下一個連接,直到清理所有連接,清理任務(wù)才可以結(jié)束,下一次put方法調(diào)用時,如果已經(jīng)停止的清理任務(wù)則會被再次觸發(fā)開始。
ConnectionPool的主要方法就是這三個,其余的則是工具私有方法或者getter, setter方法,另外還有一個需要介紹一下我們在StreamAllocation中遇到的方法 connectionBecameIdle標(biāo)識一個連接處于了空閑狀態(tài),即沒有流任務(wù),那么就需要調(diào)用該方法,有ConnectionPool來決定是否需要清理該連接:
/**
* Notify this pool that {@code connection} has become idle. Returns true if the connection has
* been removed from the pool and should be closed.
*/
boolean connectionBecameIdle(RealConnection connection) {
assert (Thread.holdsLock(this));
if (connection.noNewStreams || maxIdleConnections == 0) {
connections.remove(connection);
return true;
} else {
notifyAll(); // Awake the cleanup thread: we may have exceeded the idle connection limit.
return false;
}
}
這里noNewStream標(biāo)志位之前說過,它可以理解為該連接已經(jīng)不可用,所以可以直接清理,而maxIdleConnections==0則標(biāo)識不允許有空閑連接,也是可以直接清理的,否則喚醒清理任務(wù)的線程,執(zhí)行清理方法。
至此則分析完了連接管理的邏輯,其實(shí)就是連接復(fù)用,主要包括get, put , cleanup三個方法,重點(diǎn)時清理任務(wù)的執(zhí)行,我們可以在OkHttpClient中配置
后記
關(guān)于okhttp的連接和連接管理,邏輯還是比較容易理解,但是StreamAllocation的概念在剛接觸時還是比較令人費(fèi)解,但是為了邏輯順序本篇文章還是從StreamAllocation開始分析,進(jìn)而引入了連接和連接復(fù)用管理部分,讀者可在理解了連接和連接復(fù)用管理部分的代碼以后在回頭再去讀StreamAllocation的代碼或許更容易理解一些。在最初的計(jì)劃中是將連接與流一起分析,正是因?yàn)镾treamAlloc
附錄
在前面的分析中,我們了解到Connection需要一個Route路徑對象來建立連接,在這一部分我們主要分析Route和RouteSelector,來學(xué)習(xí)okhttp中是如何通過Address對象選擇合理的路徑對象的,本部分可能比較雞肋,對整個網(wǎng)絡(luò)請求的流程并沒有太大影響,因此有興趣的同學(xué)可以繼續(xù)閱讀,沒有興趣可以略過,并不影響其他部分的分析。
首先我們先來看一下Address類,這個類用來表示需要連接遠(yuǎn)程主機(jī)的地址,它內(nèi)部包含url, proxy, dns等信息,還有一些關(guān)于Https會用到的驗(yàn)證信息,這里我們不再分析其源碼, 代碼很簡單,只是封裝了一些屬性域而已。但是它的注釋還是可以了解一下,可以更好地把握這個類的作用:
/**
* A specification for a connection to an origin server. For simple connections, this is the
* server's hostname and port. If an explicit proxy is requested (or {@linkplain Proxy#NO_PROXY no
* proxy} is explicitly requested), this also includes that proxy information. For secure
* connections the address also includes the SSL socket factory, hostname verifier, and certificate
* pinner.
*
* <p>HTTP requests that share the same {@code Address} may also share the same {@link Connection}.
*/
通過閱讀類的注釋,我們就可以發(fā)現(xiàn),其實(shí)它就是對于連接對應(yīng)的遠(yuǎn)程連接的地址說明,一般情況下會是主機(jī)名和端口號,也就是url, 在有代理的情況下還會包含代理信息,而對于Https會包含一些其他的驗(yàn)證信息等。
下面我們再來看Route對象,我們還是來看它的注釋:
/**
* The concrete route used by a connection to reach an abstract origin server. When creating a
* connection the client has many options:
*
* <ul>
* <li><strong>HTTP proxy:</strong> a proxy server may be explicitly configured for the client.
* Otherwise the {@linkplain java.net.ProxySelector proxy selector} is used. It may return
* multiple proxies to attempt.
* <li><strong>IP address:</strong> whether connecting directly to an origin server or a proxy,
* opening a socket requires an IP address. The DNS server may return multiple IP addresses
* to attempt.
* </ul>
*
* <p>Each route is a specific selection of these options.
*/
從注釋中我們可以看出,首先Route對象可以用于建立連接,而選擇一個合適的Route,有很多種選項(xiàng),第一個就是Proxy代理,代理可以明確指定,也可以由ProxySelector中返回多個可用的代理,第二個選項(xiàng)就是IP地址,在Java中表示就是InetAddress對象,dns會返回一個服務(wù)器對應(yīng)的多個IP地址,這兩個選項(xiàng)在RouteSelector中馬上就會看到,下面我們來分析RouteSelector是如何選擇Route對象的。
首先來看它的屬性域:
/**
* Selects routes to connect to an origin server. Each connection requires a choice of proxy server,
* IP address, and TLS mode. Connections may also be recycled.
*/
public final class RouteSelector {
private final Address address;
private final RouteDatabase routeDatabase;
/* The most recently attempted route. */
private Proxy lastProxy;
private InetSocketAddress lastInetSocketAddress;
/* State for negotiating the next proxy to use. */
private List<Proxy> proxies = Collections.emptyList();
private int nextProxyIndex;
/* State for negotiating the next socket address to use. */
private List<InetSocketAddress> inetSocketAddresses = Collections.emptyList();
private int nextInetSocketAddressIndex;
/* State for negotiating failed routes */
private final List<Route> postponedRoutes = new ArrayList<>();
...
}
注釋說的也很清楚,連接遠(yuǎn)程服務(wù)器,每一個連接都需要選擇一個代理和IP地址,這里我們忽略與HTTPs有關(guān)的東西,下面的屬性域也就很容易理解,address代表地址, routeDatabase是一個黑名單,存儲那些不可用的Route對象,接下來的六個屬性則是與Proxy和InetSocketAddress相關(guān)了,包括最近使用的,可以選擇的,以及下一個可選擇的索引值,最后postponedRoutes表示那些加入到黑名單中的且符合地址條件(即滿足條件但是之前有過不可用記錄的Route對象)所有Route對象集合,當(dāng)沒有可選擇余地時會選擇使用它們,總比沒有要好。
首先我們從構(gòu)造器開始:
public RouteSelector(Address address, RouteDatabase routeDatabase) {
this.address = address;
this.routeDatabase = routeDatabase;
resetNextProxy(address.url(), address.proxy());
}
這里我們看到調(diào)用了resetNextProxy()方法,其實(shí)可以將其理解為一個初始化或者重置Proxy方法,我們來看起代碼:
/** Prepares the proxy servers to try. */
private void resetNextProxy(HttpUrl url, Proxy proxy) {
if (proxy != null) {
// If the user specifies a proxy, try that and only that.
proxies = Collections.singletonList(proxy);
} else {
// Try each of the ProxySelector choices until one connection succeeds.
List<Proxy> proxiesOrNull = address.proxySelector().select(url.uri());
proxies = proxiesOrNull != null && !proxiesOrNull.isEmpty()
? Util.immutableList(proxiesOrNull)
: Util.immutableList(Proxy.NO_PROXY);
}
nextProxyIndex = 0;
}
邏輯流程也容易理解,優(yōu)先使用在address對象中的指定的proxy對象,該方法由構(gòu)造器調(diào)用,傳遞進(jìn)來的,如果該P(yáng)roxy對象為空,則從address.proxySelector中選擇一個Proxy, 如果還沒有則使用Proxy.NO_PROXY,并將索引初始化為0,接下來我們一并看一下resetInetSocketAddress()方法:
/** 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<>();
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 {
// Try each address for best behavior in mixed IPv4/IPv6 environments.
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));
}
}
nextInetSocketAddressIndex = 0;
}
這里省略了部分條件判斷,邏輯流程同意很清楚,首先是獲取到主機(jī)地址和端口號,然后創(chuàng)建InetSocketAddress或者通過address中的dns查詢所有有可能的InetSocketAddress地址。在有了以上的了解以后我們就可以看RouteSelector中最重要的方法,即選擇一個Route對象,next()方法:
public Route next() throws IOException {
// Compute the next route to attempt.
if (!hasNextInetSocketAddress()) {
if (!hasNextProxy()) {
if (!hasNextPostponed()) {
throw new NoSuchElementException();
}
return nextPostponed();
}
lastProxy = nextProxy();
}
lastInetSocketAddress = nextInetSocketAddress();
Route route = new Route(address, lastProxy, lastInetSocketAddress);
if (routeDatabase.shouldPostpone(route)) {
postponedRoutes.add(route);
// We will only recurse in order to skip previously failed routes. They will be tried last.
return next();
}
return route;
}
這段代碼第一次看可能有些迷惑,不過仔細(xì)去分析其實(shí)邏輯很清晰,首先我們假定還有可用的InetSocketAddress,那么通過nextInetSocketAddress()直接獲取并創(chuàng)建Route對象,然后檢查是否在黑名單中,在黑名單則加入候選隊(duì)列,其實(shí)就是最后迫于無奈才會使用。如果沒有了InetSocketAddress,則選擇下一個可用的代理Proxy,在nextProxy中會調(diào)用resetNextInetSocketAddress()方法重置InetSocketAddress的隊(duì)列,繼續(xù)查詢下個可用的InetSocketAddress對象,如果沒有可用的Proxy,則從候選隊(duì)列中postpone中選擇,即迫于無奈的選擇,如果候選隊(duì)列也為空那就無能為力了,只能拋異常。這樣就分析完了整個選擇路徑的過程,過程的邏輯很清晰也很容易理解,另外該類中還有一些其他的方法,如用于更新黑名單等,有興趣的可以自行查閱。
分析到這里,整個連接的過程就分析結(jié)束了,可以總結(jié)為在網(wǎng)絡(luò)請求過程中,首先創(chuàng)建一個StreamAllocation的對象,然后調(diào)用其newStream()方法,查找一個可用連接,要么復(fù)用連接,要么新建連接,復(fù)用連接則根據(jù)address從連接池中查找,新建連接則是根據(jù)address查找一個Route對象建立連接,建立連接以后會將該連接添加到連接池中,同時連接池的清理任務(wù)停止的情況下,添加新的連接進(jìn)去會觸發(fā)開啟清理任務(wù)。這是建立連接和管理連接的整個過程,當(dāng)擁有連接以后,StreamAllocation就會在連接上建立一個流對象,該流持有connection的輸入輸出流,也就是socket的輸入輸出流,通過它們最終完成數(shù)據(jù)通信的過程,所以下一節(jié)中將會重點(diǎn)分析流對象Http1Codec,以及數(shù)據(jù)通信的過程。