在上一篇文章中介紹了okhttp中連接概念以及連接建立和管理,其中在攔截器鏈中的ConnectInterceptor負責建立連接,并在該連接上建立流,將其放置在攔截器鏈中,在攔截器鏈中的最后一個攔截器CallServerInterceptor,通過使用流的操作完成網絡請求的數據交換。下面從該攔截器開始學習okhttp時如果通過流的操作完成網絡通信的。
1. 最后一個攔截器CallServerInterceptor
CallServerInterceptor是okhttp中的最后一個攔截器,在攔截器鏈中,攔截器的順序是用戶定義的應用攔截器,RetryAndFollowUpInterceptor, BridgeInterceptor, CacheInterceptor, ConnectInterceptor, 網絡攔截器,以及最后的CallServerInterceptor。前面的若干攔截器的作用主要包括兩個方面,一是在遞歸調用鏈中對Request和返回的Response進行處理,這主要是針對用戶自定義的攔截器,完成我們在應用中不同的需求;二是在攔截器鏈中完成一定功能,為最終的網絡請求提供幫助,這主要是針對okhttp中定義的攔截器,比如ConnectInterceptor主要作用就是根據請求建立連接和流對象,從而幫助最終完成網絡請求。雖然作用不同,但是這兩類攔截器的代碼結構都有共同的特點,即基本包括三個步驟,從chain對象中獲取request對象以及其他對象,對請求做處理,然后調用chain.proceed()方法獲取網絡請求response, 最后在第三步中對響應做響應處理并返回處理之后的Response,當然部分攔截器可以沒有第一步和第三步,但是基本結構都是一致的。然而,最后的一個攔截器CallServerInterceptor,其結構則與上述攔截器的結構有所不同,它的主要功能就是建立好的流對象,完成數據通信。
這里首先簡單介紹一下流對象,在okhttp中,流對象對應著HttpCodec, 它有對應的兩個子類, Http1Codec和Http2Codec, 分別對應Http1.1協議以及Http2.0協議,本文主要學習前者。在Http1Codec中主要包括兩個重要的屬性,即source和sink,它們分別封裝了socket的輸入和輸出,CallServerInterceptor正是利用HttpCodec提供的I/O操作完成網絡通信。下面來看CallServerInterceptor的源碼。
對于攔截器,依然是學習它的intercept方法:
@Override
public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
HttpCodec httpCodec = realChain.httpStream();
StreamAllocation streamAllocation = realChain.streamAllocation();
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
//1. 向socket中寫入請求header信息
httpCodec.writeRequestHeaders(request);
Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
//a. 省略部分
...
if (responseBuilder == null) {
//2. 向socket中寫入請求body信息
Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
} else if (!connection.isMultiplexed()) {
//b.省略部分
}
}
//3. 完成網絡請求的寫入
httpCodec.finishRequest();
//4. 讀取網絡響應header信息
if (responseBuilder == null) {
responseBuilder = httpCodec.readResponseHeaders(false);
}
Response response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
int code = response.code();
if (forWebSocket && code == 101) {
//c. 省略部分
} else {
//5. 讀取網絡響應的body信息
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
}
// d. 省略部分
return response;
}
本文對代碼做部分的省略,突出主要的流程,省略已經做了a, b, c, b的標注,后面再分別對其學習分析,然后是最主要的流程做了注釋,主要是分成5步,在執行網絡請求之前,首先時從攔截器鏈中獲取連接,流以及它們二者的管理者streamAllocation,還有原始的網絡請求request,然后執行以下五個步驟
- 1 向socket中寫入請求header信息
- 2 向socket中寫入請求body信息
- 3 完成網絡請求的寫入
- 4 讀取網絡響應header信息
- 5 讀取網絡響應的body信息
注意在一次網絡請求中可能并不包括所有的這五個步驟,比如第二不,寫入請求體,只有請求方法中有請求體的時候才會寫入,而且有些情況只有在得到服務器允許的時候才會寫入請求體,這一點后面會有提到;另外這里所使用的寫入和讀取兩個詞并不完全準確,比如第二步只是內存與socket的輸出流建立關系,并沒有真正寫入,直到第三步刷新時才會將請求的信息寫入到socket的輸出流中,同樣地,第五步中獲取到響應的body信息,只是獲取一個流對象,只有在應用代碼中調用流對象的讀方法或者response.string()方法等,才會從socket的輸入流中讀取信息到應用的內存中使用。
這五步邏輯很清晰,也很容易理解,下面中逐個學習省略的四個部分。首先看省略部分a的代碼:
// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
// Continue" response before transmitting the request body. If we don't get that, return what
// we did get (such as a 4xx response) without ever transmitting the request body.
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
responseBuilder = httpCodec.readResponseHeaders(true);
}
這里時處理一種特殊情況,即首先發送詢問服務器是否可以發送帶有請求體的請求,在該請求中請求的頭部信息中添加Expect:100-continue字段,服務器如果可以接受請求體則可以返回一個100的響應碼,客戶端繼續發送請求,具體的可以參考相關文章對100響應碼的介紹。
這里我們看到如果請求中有該頭部信息會跳過第二步,直接執行三四步,獲取響應信息,我們繼續往下看okhttp對該特殊情況的處理邏輯。這里我們有必要提前看一下HttpCodec的具體實現,當然我們這里是分析Http1Codec的readResponseHeaders(boolean)代碼:
@Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
...
try {
StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());
Response.Builder responseBuilder = new Response.Builder()
.protocol(statusLine.protocol)
.code(statusLine.code)
.message(statusLine.message)
.headers(readHeaders());
if (expectContinue && statusLine.code == HTTP_CONTINUE) {
return null;
}
...
} catch (EOFException e) {
...
}
}
這里因為時分析特殊情況,所以這里只看參數為true的情況,從代碼中可以看到獲取響應頭部信息包括獲取響應行和響應頭兩部分,具體代碼可以自行查看,這里不再展開,當服務器同意接收請求體時回返回100的響應碼,可見此時該方法返回空,其他情況會返回非空的響應對象。下面再回到CallServerInterceptor中的代碼
當responseBuilder為空時繼續執行正常邏輯,即從第二步開始執行。當responseBuilder不為空時,就不可以寫如請求體信息,下面的else if()語句時針對Http2協議時關閉當前連接,這里我么暫時不考慮,Http1.1協議下,代碼會跳過寫請求體的步驟,繼續執行,并且因為responseBuilder不為空也會跳過讀取響應頭的步驟,因為之前讀過一次,但是響應碼不是100而已,可見當響應碼為100時會讀取兩次響應頭,當然也執行了兩次請求(注意httpCodec.finishRequest()方法的調用就是刷新輸出流,也就相當于執行了一次請求)。
c省略部分是針對websocket所做的處理,由于對H5以及websocket不了解,這里就跳過該部分,有興趣的同學可以自行查看。
d省略部分就是收尾工作,對一些特殊情況的處理,下面為代碼:
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
streamAllocation.noNewStreams();
}
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
代碼邏輯很簡單,根據響應信息在必要時關閉連接,在上一篇關于連接的文章中我們介紹了streamAllocation的作用,對應關閉和回收資源的問題沒有考慮清除,本文會在介紹完流的概念以后,再具體分析關于連接的關閉以及資源回收的問題。最后為檢查響應碼204和205兩種響應,這兩種響應沒有響應體。
至此我們分析完了CallServerInterceptor的代碼,可以看出由它實現的網絡請求,而在完成這一功能的若干步驟中都是依賴HttpCodec提供的功能來完成的,我們只考慮Http1.1協議,所以下面開始分析Http1Codec的代碼。
2. OkHttp中的流 HttpCodec
在分析以上所提到的五個步驟之前,需要說明在Http1Codec中使用了狀態模式,其實就是對象維護它所處的狀態,在不同的狀態下執行對應的邏輯,并更新狀態,在執行邏輯之前通過檢查對象的狀態避免網絡請求的若干執行步驟發生錯亂。首先來看狀態的定義:
private static final int STATE_IDLE = 0; // Idle connections are ready to write request headers.
private static final int STATE_OPEN_REQUEST_BODY = 1;
private static final int STATE_WRITING_REQUEST_BODY = 2;
private static final int STATE_READ_RESPONSE_HEADERS = 3;
private static final int STATE_OPEN_RESPONSE_BODY = 4;
private static final int STATE_READING_RESPONSE_BODY = 5;
private static final int STATE_CLOSED = 6;
下面是Http1Codec的屬性域:
/** The client that configures this stream. May be null for HTTPS proxy tunnels. */
final OkHttpClient client;
/** The stream allocation that owns this stream. May be null for HTTPS proxy tunnels. */
final StreamAllocation streamAllocation;
final BufferedSource source;
final BufferedSink sink;
int state = STATE_IDLE;
這些屬性域很容易理解,首先持有client,可以使用它所提供的功能,通常是獲取一些用戶所設置的屬性,其次是streamAllocation,它是連接與流的橋梁,所以很容易理解需要它獲取關于連接的功能。然后就是該流對象封裝的輸出流和輸入流,兩個流內部封裝的自然就是socket的了。最后就是對象所處的狀態。介紹完屬性域以后我們就可以分步驟分析Http1Codec提供的功能了,在這些步驟中,邏輯很明確,首先檢查狀態,然后執行邏輯,最后更新狀態,當然執行邏輯和更新狀態是可以交換的,不會造成影響,這步驟分析中我們不再考慮狀態的問題重點只是關系邏輯的執行。
-1. 首先第一步,寫請求頭:
@Override public void writeRequestHeaders(Request request) throws IOException {
String requestLine = RequestLine.get(
request, streamAllocation.connection().route().proxy().type());
writeRequest(request.headers(), requestLine);
}
public void writeRequest(Headers headers, String requestLine) throws IOException {
if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
sink.writeUtf8(requestLine).writeUtf8("\r\n");
for (int i = 0, size = headers.size(); i < size; i++) {
sink.writeUtf8(headers.name(i))
.writeUtf8(": ")
.writeUtf8(headers.value(i))
.writeUtf8("\r\n");
}
sink.writeUtf8("\r\n");
state = STATE_OPEN_REQUEST_BODY;
}
執行邏輯很清晰,可以分成兩部分,對應Http協議,即寫入請求行和請求頭,至于請求行的獲取有興趣的同學可以自行查看源碼。
-2. 接著,第二步,寫請求體,這一步中Http1Codec提供一個包裝了sink的輸出流,也是一個Sink, 這里我們看是如何封裝sink的:
@Override public Sink createRequestBody(Request request, long contentLength) {
if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
// Stream a request body of unknown length.
return newChunkedSink();
}
if (contentLength != -1) {
// Stream a request body of a known length.
return newFixedLengthSink(contentLength);
}
throw new IllegalStateException(
"Cannot stream a request body without chunked encoding or a known content length!");
}
屬性Http協議的同學都知道其實請求體和響應體可以分成固定長度和非固定長度兩種,其中非固定長度由頭部信息中Transfer-Encoding=chunked來標識,固定長度則有對應的頭部信息標識實體信息的對應長度。這里我們以非固定長度為例分析Http1Codec是如何封裝sink的,對于固定長度的也是類似的邏輯。
public Sink newChunkedSink() {
if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state);
state = STATE_WRITING_REQUEST_BODY;
return new ChunkedSink();
}
private final class ChunkedSink implements Sink {
...
@Override public void write(Buffer source, long byteCount) throws IOException {
if (closed) throw new IllegalStateException("closed");
if (byteCount == 0) return;
sink.writeHexadecimalUnsignedLong(byteCount);
sink.writeUtf8("\r\n");
sink.write(source, byteCount);
sink.writeUtf8("\r\n");
}
@Override public synchronized void flush() throws IOException {
if (closed) return; // Don't throw; this stream might have been closed on the caller's behalf.
sink.flush();
}
@Override public synchronized void close() throws IOException {
if (closed) return;
closed = true;
sink.writeUtf8("0\r\n\r\n");
detachTimeout(timeout);
state = STATE_READ_RESPONSE_HEADERS;
}
}
這里使用一個內部類來封裝sink, 這里我們只看其中的三個重要的方法,即write() flush() close()方法,邏輯都很清晰,非固定長度的請求體,都是在第一行寫入一段數據的長度,然后在之后寫入該段數據,從write()方法中可以看出是講buffer中的數據寫入到sink對象中,如果熟悉okio的執行邏輯,對此應該很容易理解。然后刷新和關閉邏輯則很簡單,其中關閉時注意更新狀態。
對于固定長度的請求體,其封裝sink的邏輯是類似的,其中需要傳入一個RemainingLength, 保證寫數據結束時保證數據長度是正確的即可,有興趣的可以查看代碼。
-3. 第三步是完成請求的寫入,其實這一步其實很簡單,只有一行代碼,就是執行流的刷新:
@Override public void finishRequest() throws IOException {
sink.flush();
}
注意這一步是不需要檢查狀態的,因為此時的狀態有可能是STATE_OPEN_REQUEST_BODY(沒有請求體的情況)或者STATE_READ_RESPONSE_HEADERS(已經完成請求體寫入的情況)。這一步只是刷新流,所以什么情況下都不會造成影響,所以沒有必要檢查狀態,也沒有更新狀態,保持之前的狀態即可。
-4. 第四步讀取請求頭,這一步的代碼我們在前面是見到過的,這里再次貼出,方便查閱,并且沒有省略:
@Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) {
throw new IllegalStateException("state: " + state);
}
try {
StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());
Response.Builder responseBuilder = new Response.Builder()
.protocol(statusLine.protocol)
.code(statusLine.code)
.message(statusLine.message)
.headers(readHeaders());
if (expectContinue && statusLine.code == HTTP_CONTINUE) {
return null;
}
state = STATE_OPEN_RESPONSE_BODY;
return responseBuilder;
} catch (EOFException e) {
// Provide more context if the server ends the stream before sending a response.
IOException exception = new IOException("unexpected end of stream on " + streamAllocation);
exception.initCause(e);
throw exception;
}
}
可以此時所處的狀態有可能為STATE_OPEN_REQUEST_BODY和STATE_READ_RESPONSE_HEADERS兩種,然后讀取請求行和請求頭部信息,并返回響應的Builder。
-5. 第五步為獲取響應體:
@Override public ResponseBody openResponseBody(Response response) throws IOException {
Source source = getTransferStream(response);
return new RealResponseBody(response.headers(), Okio.buffer(source));
}
在之前的介紹中,我們知道響應Response對象中是封裝一個source對象,用于讀取響應數據。所以ResponseBody的構建就是需要響應頭和響應體兩部分即可,響應頭在上一部分中已經添加到response對象中了,headers()獲取響應頭即可。下面分析,如何封裝source對象,獲取一個對應的source對象,可能有些拗口,如果你熟悉裝飾模式,以及okio的結構應該很容易明白。下面看getTransferStream的代碼:
private Source getTransferStream(Response response) throws IOException {
if (!HttpHeaders.hasBody(response)) {
return newFixedLengthSource(0);
}
if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
return newChunkedSource(response.request().url());
}
long contentLength = HttpHeaders.contentLength(response);
if (contentLength != -1) {
return newFixedLengthSource(contentLength);
}
// Wrap the input stream from the connection (rather than just returning
// "socketIn" directly here), so that we can control its use after the
// reference escapes.
return newUnknownLengthSource();
}
這里和寫入請求體的地方十分類似,響應體也是分為固定長度和非固定長度兩種,除此以外,為了代碼的健壯性okhttp還定義了UnknownLengthSource,這里我們不對該意外情況分析,下面我們以固定長度為例分析source的封裝。
private class FixedLengthSource extends AbstractSource {
private long bytesRemaining;
public FixedLengthSource(long length) throws IOException {
bytesRemaining = length;
if (bytesRemaining == 0) {
endOfInput(true);
}
}
@Override public long read(Buffer sink, long byteCount) throws IOException {
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (closed) throw new IllegalStateException("closed");
if (bytesRemaining == 0) return -1;
long read = source.read(sink, Math.min(bytesRemaining, byteCount));
if (read == -1) {
endOfInput(false); // The server didn't supply the promised content length.
throw new ProtocolException("unexpected end of stream");
}
bytesRemaining -= read;
if (bytesRemaining == 0) {
endOfInput(true);
}
return read;
}
@Override public void close() throws IOException {
if (closed) return;
if (bytesRemaining != 0 && !Util.discard(this, DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
endOfInput(false);
}
closed = true;
}
}
這里可以看到有一個成員變量bytesRemaining標識剩余的字節數,保證讀取到的字節長度與頭部信息中的長度保證一致。read()中的代碼可以看到就是將該source對象的數據讀取到封裝的source中,用于構建ResponseBody。
這里需要注意在本篇文章中,我們不斷地提到sink,source對象以及封裝的sink和source對象,這里解釋一下,前面兩個代表http1Codec對象中的sink和source對象,即封裝了socket的輸出和輸入流,而封裝的sink和source對象則是構建的固定長度和非固定長度的輸出輸入流,其實它們只是對http1Codec成員變量中的sink和source的一種封裝,其實就是裝飾模式,封裝以后的sink和source對象可以用于在外部寫請求體和構建ResponseBody。
這里代碼邏輯很清晰,不再詳細介紹,相信通過閱讀代碼都可以明白,不過這里需要提及一下endOfInput()方法:
protected final void endOfInput(boolean reuseConnection) throws IOException {
if (state == STATE_CLOSED) return;
if (state != STATE_READING_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
detachTimeout(timeout);
state = STATE_CLOSED;
if (streamAllocation != null) {
streamAllocation.streamFinished(!reuseConnection, Http1Codec.this);
}
}
這里的執行邏輯也不多,除去檢查狀態和更新狀態之外,就是接觸超時機制,最后需要注意就是調用streamAllocation的streamFinished()方法,該方法的參數包括連接是否可以繼續使用,以及流對象本身,該方法用于連接和流的清理以及資源回收工作。在上一篇介紹連接的文章中,由于對流不熟悉,所以對這一部分介紹的不清楚,下一小節中則對該部分內容整體學習一下。
至此再去看Http1Codec的代碼,基本上已經全部覆蓋,沒有分析到的其實就剩下幾個關于Sink和Source的內部類,都分為固定長度和非固定長度,有興趣的可自行查看源碼。除此以外還有一個cancel()方法,如下:
@Override public void cancel() {
RealConnection connection = streamAllocation.connection();
if (connection != null) connection.cancel();
}
這里可以看出就是調用該流對應的cancel方法,關于這一點在下一小節中統一分析講述。
3. Okhttp中連接與流的資源回收與清理
經過前面的學習,我們知道在okhttp中關于連接和流,有三個重要的類,即RealConnection, StreamAllocation和Http1Codec,這里我們只是分析Http1.1協議,所以這三個類是一一對應的,即在一個連接上分配一個流對象,通過該流對象完成最終的網絡請求。而完成一次網絡請求可能會有成功和失敗兩種可能,同時也還會有在中途中斷的可能,針對以上情況,當我們建立連接獲取到socket對象,并在該連接之上建立請求之后,無論那一種情況發生,都需要資源的回收和清理工作,而StreamAllocation作為連接和流的橋梁,自然承擔該項工作的主要責任。
我們知道一次請求包括成功,失敗和中斷等幾種情況,在StreamAllocation中對資源回收和清理時,最終都會調用deallocate()方法,這里我們首先看該方法,然后自底向上分析:
private Socket deallocate(boolean noNewStreams, boolean released, boolean streamFinished) {
assert (Thread.holdsLock(connectionPool));
//1. 修改屬性,置狀態
if (streamFinished) {
this.codec = null;
}
if (released) {
this.released = true;
}
Socket socket = null;
if (connection != null) {
if (noNewStreams) {
connection.noNewStreams = true;
}
if (this.codec == null && (this.released || connection.noNewStreams)) {
//2. 清理該StreamAllocation對象在Connection中的引用
release(connection);
//3. 執行清理邏輯
if (connection.allocations.isEmpty()) {
connection.idleAtNanos = System.nanoTime();
if (Internal.instance.connectionBecameIdle(connectionPool, connection)) {
socket = connection.socket();
}
}
connection = null;
}
}
//4. 返回需要關閉的socket對象
return socket;
}
這里首先介紹三個參數的含義,noNewStreams代表該對象對應的連接對象不再可用了,released表示可以將該streamAllocation對象置為可以釋放的狀態了,streamFinished表示該對象對應的流已經完成了任務,或成功或失敗,就是可以將codec置為空了。
下面開始分析它的邏輯,分成四步,已在注釋中說明。首先是根據三個參數分別置不同的屬性狀態,其中released是代表streamAllocation的狀態,noNewStreams代表連接對象的狀態。然后是根據不同的狀態執行清理工作,這里需要注意在判斷條件時是使用的成員變量而不是參數,也就是各對象目前所處的狀態,如果流對象被置空(說明完成網絡請求或者請求失敗),并且該對象處于可以釋放的狀態或者連接不可用了(可能是因為中斷時設置的狀態,流成功或失敗結束時也會設置狀態,具體后面分析),此時就將連接置為空閑狀態,并在必要時返回可以關閉的socket對象(這里所說的必要時,是有ConnectionPool對連接維護的邏輯決定的,可以參考上一篇對連接描述的文章)。在熟悉了這個流程以后,我們就可以分成四種情況(即成功,失敗,中斷,取消),分別分析它們對應的過程。至于第三步清理引用,即release()方法,可以自行查看代碼,較為簡單,其實就是將自己從connection中的streamAllocation列表中移除。
1. 成功的網絡請求
在上一小節的最后我們介紹了endOfInput()方法,即在讀取響應體結束以后調用,此時是完成了網絡請求,執行清理工作,此時調用了StreamAllocation的finishStreamed()方法,代碼如下:
public void streamFinished(boolean noNewStreams, HttpCodec codec) {
Socket socket;
synchronized (connectionPool) {
if (codec == null || codec != this.codec) {
throw new IllegalStateException("expected " + this.codec + " but was " + codec);
}
if (!noNewStreams) {
connection.successCount++;
}
socket = deallocate(noNewStreams, false, true);
}
closeQuietly(socket);
}
這里的noNewStreams依然標識對應的該連接是否可以繼續使用,然后調用上面的deallocate()方法,表示網絡請求過程已經完成,會置空codec對象,但是并沒有設置released狀態,如果連接依然可用,并且此時streamAllocation并不是released的狀態,此時并不會將連接置為空閑狀態,還可以在該連接上繼續分配新的流對象,完成新的網絡請求,只不過請求的地址需要是完全相同的。如果是讀取響應實體時發生的錯誤,此時endOfInput方法傳入false,也就是對應noNewStreams參數,此時就會將連接置為空閑狀態,并在必要時關閉socket(這里將請求成功,但是讀取失敗歸到了成功完成網絡請求的類別中)。除此以外,網絡請求成功時,還有可能在響應頭部信息中connection設置為close屬性,此時需要關閉連接,此時也是調用的該方法,noNewStreams參數也為true,在CallServerInterceptor分析的最后提到過該邏輯。
2. 失敗的網絡請求
在網絡請求失敗后,即在RetryAndFollowUpInterceptor中會調用StreamAllocation的streamFailed()方法,該方法的代碼如下:
public void streamFailed(IOException e) {
Socket socket;
boolean noNewStreams = false;
synchronized (connectionPool) {
if (e instanceof StreamResetException) {
...
} else if (connection != null
&& (!connection.isMultiplexed() || e instanceof ConnectionShutdownException)) {
noNewStreams = true;
// If this route hasn't completed a call, avoid it for new connections.
if (connection.successCount == 0) {
if (route != null && e != null) {
routeSelector.connectFailed(route, e);
}
route = null;
}
}
socket = deallocate(noNewStreams, false, true);
}
closeQuietly(socket);
}
在RetryAndFollowUpInterceptor中調用該方法時,參數為null, 所以這里我們先看參數為null的情況,此時連接不可用,所以noNewStreams參數為true,這里還有一個邏輯就是此時檢查一下該連接的成功次數,這個值在完成一次網絡請求時并且該鏈接不關閉時會加一,如果這個連接從來都沒有成功過,那么就要將它加入到黑名單中,這個黑名單在ConnectionPool的維護連接的邏輯中會用到,具體可以參考上一篇文章。接著調用deallocation()方法,并在必要時關閉socket。對于該方法的參數不為空時,并且異常是StreamResetException的子類時,這里暫且不分析,不明白其中的原理,也沒有找到在哪里調用的該方法,有清楚的可以在評論中告知,不勝感激。
3. 中斷的網絡請求
在RetryAndFollowUpInterceptor中,網絡請求不斷重試或重定向,如果不可重試或重定向,此時都需要將StreamAllocation置為可以釋放的狀態,此時會調用StreamAllocation的方法release()方法,該方法的代碼如下:
public void release() {
Socket socket;
synchronized (connectionPool) {
socket = deallocate(false, true, false);
}
closeQuietly(socket);
}
可以看到該方法很簡單,只是調用deallocate方法,release為true,它設置StreamAllocation為可釋放狀態,如果沒有流對象或者連接不可用時,就開始回收資源,必要時關閉socket。
4. 取消的網絡請求
我們都知道在okhttp中,對網絡請求的封裝是使用的Callu對象,該對象有cancel()方法,也就是可以在任意時候取消一次網絡請求,下面我們就從Call的cancel()方法開始學習如何取消一次網絡請求
Call.cancel():
@Override public void cancel() {
retryAndFollowUpInterceptor.cancel();
}
可見是調用了RetryAndFollowUpInterceptor的cancel()方法,該方法的代碼為:
public void cancel() {
canceled = true;
StreamAllocation streamAllocation = this.streamAllocation;
if (streamAllocation != null) streamAllocation.cancel();
}
修改成員變量,并調用StreamAllocation()的cancel()方法(關于這里的賦值,使用局部變量的意圖還不太明白),這里終于調用到了StreamAllocationd 取消方法,下面看其cancel()方法:
public void cancel() {
HttpCodec codecToCancel;
RealConnection connectionToCancel;
synchronized (connectionPool) {
canceled = true;
codecToCancel = codec;
connectionToCancel = connection;
}
if (codecToCancel != null) {
codecToCancel.cancel();
} else if (connectionToCancel != null) {
connectionToCancel.cancel();
}
}
邏輯可以總結為如果有可以取消的流則取消流,沒有則取消連接,其實我們在前面介紹Http1Codec的cancel()方法中也看到了它也是調用的對應的連接對象的cancel()方法,下面我們來看connection的cancel()方法:
public void cancel() {
// Close the raw socket so we don't end up doing synchronous I/O.
closeQuietly(rawSocket);
}
關閉原始的socket對象,簡單直接。關閉socket對象以后,所有的I/O邏輯都會拋出一場,因此取消了一次網絡請求,此時需要手動關閉一些建立的流對象。
到這里我們就分析完了StreamAllocation在回收和清理資源方面所做的工作??偨Y起來有四個可以調用的方法,其中前三個最后對會調用deallocate方法去處理連接和流對象,而cancel()方法則是直接關閉了socket對象。如果在這里還不太明白,比較難理解的是中間的兩個,其實可以結合RetryAndFollowUpIntercepter理解,中間的兩個方法中在重試攔截器中被多次調用,這與具體的網絡請求邏輯相關,關于RetryAndFollowUpInterceptor在前一篇文章中分析較少,因此下一小節會對其做一次全面的分析學習,從而也可以更好地輔助理解StreamAllocation對清理和回收所做的工作。
4. 再談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) {
//1. 判斷是否為取消狀態
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
Response response = null;
boolean releaseConnection = true;
try {
response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
//2. 出現異常狀況
// The attempt to connect via a route failed. The request will not have been sent.
if (!recover(e.getLastConnectException(), false, request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
continue;
} catch (IOException e) {
// An attempt to communicate with a server failed. The request may have been sent.
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
//3. 最后判斷是否需要執行清理
// We're throwing an unchecked exception. Release any resources.
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
...
}
}
這里由于該方法較長,我將他們分成了兩個部分,前半部分是針對重試請求的處理,后半部分是針對重定向請求的處理。我們還是首先看該方法的前半部分,這里我將前半部分劃分成了三步,首先是判斷該請求是否被取消了,如果被取消了,此時有兩種可能,一是還沒有流對象,此時調用release()方法,置為可釋放狀態,終止網絡請求,并將在條件滿足時將連接設置為空閑狀態。第二種可能是已經有流對象,此次網絡請求就沒有辦法終止,此時只是設置可釋放狀態,等網絡請求結束以后將流對象置空,此時可以立刻在條件滿足時將連接置為空閑狀態。
如果沒有取消,則開始執行網絡請求,這里也分成兩種可能,一是網絡請求出現意外狀態,即拋出異常,此時需要判斷是否可以重試,判斷條件可以自行查看,這里不詳細說明,但是在判斷方法中,調用了streamAllocation的streamFailed()方法,并傳遞了Exception。如果可以重試,此時的releaseConnection為false, finnally中不執行任何邏輯,此時會執行下一次循環,如果不可以重試,則執行第三步,及finnally中的邏輯,即調用StreamAllocation的失敗方法,此時會將流對象置空,同時調用release()方法,由于codec已經為空,release()方法調用完以后則會在條件滿足時將connection置為空閑狀態,然后由于拋出異常,此時會跳出while循環。
上面說如果沒有取消,還會有另外一種情況,即正確完成網絡請求,此時處理重定向情況,直接去執行該方法的第二部分,下面是第二部分代碼,注意這里都是在while()循環之中:
...
Request followUp = followUpRequest(response);
//沒有重定向或重試情況
if (followUp == null) {
if (!forWebSocket) {
streamAllocation.release();
}
return response;
}
//有重定向情況
closeQuietly(response.body());
//兩種不可以重定向的情況
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
if (followUp.body() instanceof UnrepeatableRequestBody) {
streamAllocation.release();
throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
}
//對于不同的連接,釋放原來的streamAllocation, 建立新的streamAllocation
if (!sameConnection(response, followUp.url())) {
streamAllocation.release();
streamAllocation = new StreamAllocation(
client.connectionPool(), createAddress(followUp.url()), callStackTrace);
} else if (streamAllocation.codec() != null) {
throw new IllegalStateException("Closing the body of " + response
+ " didn't close its backing stream. Bad interceptor?");
}
//設置屬性,開始重試,執行下一次循環
request = followUp;
priorResponse = response;
}
}
首先獲取重定向請求對象,對于不需要重定向的請求,直接返回相應對象,結束循環。如果需要重定向則需要關閉上一個響應對象的響應體,此時調用body的close()方法,最終對調用streamAllocation的streamFinished()方法,將其上的codec對象置空。然后判斷兩種不可以重定向的情況,拋出異常結束循環。如果可以執行重定向的話,對于不同連接則需要釋放原來的streamAllocation,并建立新的streamAllocation,而對于相同連接直接使用即可,即在相同連接上建立新的流,這里判斷了原來的codec對象一定置為空了。最后設置新的請求對象,再次執行循環,完成重定向的請求。
至此較為完整地分析了RetryAndFollowUpInterceptor的代碼,這里需要注意重試和重定向沒有任何關系,這里只是寫到一個攔截器中,放在一個無限循環中而已。通過這一段代碼的分析也可以更好地了解到StreamAllocation在資源管理中,回收和清理工作的幾個函數的作用。不過由于水平有限,對于recover()函數,以及拋出的各種異常類型沒有深入分析,StreamAllocation的streamFailed()方法也沒有分析透徹,有了解的歡迎一起探討。不過這里已經不影響對整體流程的分析。
5. 總結
本篇文章主要是介紹了Okhttp流的概念,以及CallServerInterceptor是如果使用HttpCodec提供的功能完成網絡請求的數據通信。在介紹完流的概念以后,結合之前的連接,重新對StreamAllocation中資源的回收和清理做了學習和分析,主要介紹了四個可以調用的方法,分別處理不同的情況,最后重新介紹了RetryAndFollowUpInterceptor的執行邏輯,以及在執行重試或重定向時是如何調用StreamAllocation的清理方法的。
通過三篇文章,我們已經大概分析了okhttp中一次網絡請求的大致過程。從Call對象對請求的封裝,到使用dispatcher對請求的分發,再到執行請求時調用getResponseWithInterceptors()方法獲取請求,最后說明這個攔截器鏈的遞歸調用結構。在這個攔截器鏈中,RetryAndFollowUpInterceptor負責重試和重定向,ConnectionInterceptor負責建立連接和流對象,CallServerInterceptor負責完成最終的網絡請求,以上就是幾乎整個的網絡請求過程。在攔截器鏈中還有兩個攔截器沒有介紹,其中較為簡單的是BridgeInterceptor,它主要負責okhttp中請求和響應對象與實際Http協議中定義的請求和響應之間的轉換,以及處理cookie相關的內容,另一個是個CacheInterceptor,顧名思義也就是用來處理緩存的攔截器,接下來將會分成兩篇文章分別介紹它們的功能。