你好,我是 N0tExpectErr0r,一名熱愛技術的 Android 開發
我的個人部落格:blog.N0tExpectErr0r.cn
OkHttp 原始碼剖析系列文章目錄:
OkHttp 原始碼剖析系列(一)——請求的發起及攔截器機制概述
終於來到了我們 OkHttp 的最後一個部分——請求的發起。讓我們回顧一下 CallServerInterceptor
的大體流程:
- 呼叫
exchange.writeRequestHeaders
寫入請求頭 - 呼叫
exchange.createRequestBody
獲取Sink
- 呼叫
ResponseBody.writeTo
寫入請求體 - 呼叫
exchange.readResponseHeaders
讀入響應頭 - 呼叫
exchange.openResponseBody
方法讀取響應體
而我們知道,Exchange
最後實際上轉調到了 ExchangeCodec
中的對應方法,而 ExchangeCodec
有兩個實現——Http1ExchangeCodec
及 Http2ExchangeCodec
。
它們的建立過程在建立連線的過程中的 RealConnection.newCodec
方法中實現:
ExchangeCodec newCodec(OkHttpClient client, Interceptor.Chain chain) throws SocketException {
if (http2Connection != null) {
return new Http2ExchangeCodec(client, this, chain, http2Connection);
} else {
socket.setSoTimeout(chain.readTimeoutMillis());
source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
return new Http1ExchangeCodec(client, this, source, sink);
}
}
複製程式碼
實際上是根據 Http2Connection
是否為 null 進行判斷。
下面我們分別對 HTTP1 中及 HTTP2 中的處理進行分析:
HTTP/1.x
writeRequestHeaders
@Override
public void writeRequestHeaders(Request request) throws IOException {
String requestLine = RequestLine.get(
request, realConnection.route().proxy().type());
writeRequest(request.headers(), requestLine);
}
複製程式碼
這裡首先呼叫了 RequestLine.get
方法獲取到了 requestLine
這個 String,之後通過呼叫 writeRequest
方法將其寫入。
我們首先看到 RequestLine.get
方法:
/**
* Returns the request status line, like "GET / HTTP/1.1". This is exposed to the application by
* {@link HttpURLConnection#getHeaderFields}, so it needs to be set even if the transport is
* HTTP/2.
*/
public static String get(Request request, Proxy.Type proxyType) {
StringBuilder result = new StringBuilder();
result.append(request.method());
result.append(' ');
if (includeAuthorityInRequestLine(request, proxyType)) {
result.append(request.url());
} else {
result.append(requestPath(request.url()));
}
result.append(" HTTP/1.1");
return result.toString();
}
複製程式碼
這裡實際上就是在構建 HTTP 協議中的第一行,包括請求的 method、url、HTTP版本等資訊。
我們接著看到 writeRequest
方法:
/**
* Returns bytes of a request header for sending on an HTTP transport.
*/
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;
}
複製程式碼
這裡首先寫入了 statusLine
,之後將 Header 以 key:value
的形式寫入,最後寫入了一個空行,到這裡我們的請求頭就成功寫入了(具體可以看到 HTTP/1.x 的請求格式)。
createRequestBody
@Override
public Sink createRequestBody(Request request, long contentLength) throws IOException {
if (request.body() != null && request.body().isDuplex()) {
throw new ProtocolException("Duplex connections are not supported for HTTP/1");
}
if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
// Stream a request body of unknown length.
return newChunkedSink();
}
if (contentLength != -1L) {
// Stream a request body of a known length.
return newKnownLengthSink();
}
throw new IllegalStateException(
"Cannot stream a request body without chunked encoding or a known content length!");
}
複製程式碼
這裡首先對 Transfer-Encoding:chunked
的情況進行了處理,返回了 newChunkedSink
方法的結果,之若 contentLength
是確定的,則返回 newKnownLengthSink
方法的結果。
讓我們分別看到這兩個方法。
newChunkedSink
private Sink newChunkedSink() {
if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state);
state = STATE_WRITING_REQUEST_BODY;
return new ChunkedSink();
}
複製程式碼
其實這裡就是構建並返回了一個繼承於 Sink
的 ChunkedSink
物件,我們可以看看它的 write
方法:
@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");
}
複製程式碼
首先寫入了十六進位制的資料大小,之後寫入了資料。
newKnownLengthSink
private Sink newKnownLengthSink() {
if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state);
state = STATE_WRITING_REQUEST_BODY;
return new KnownLengthSink();
}
複製程式碼
這裡也是構建並返回了一個繼承於 Sink
的 KnownLengthSink
物件,我們可以看到其 write
方法:
@Override
public void write(Buffer source, long byteCount) throws IOException {
if (closed) throw new IllegalStateException("closed");
checkOffsetAndCount(source.size(), 0, byteCount);
sink.write(source, byteCount);
}
複製程式碼
可以看到,其實就是對資料進行寫入,沒有非常特別的地方。
readRequestHeaders
@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 = StatusLine.parse(readHeaderLine());
// 構建 Response(包含status資訊及Header)
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;
} else if (statusLine.code == HTTP_CONTINUE) {
state = STATE_READ_RESPONSE_HEADERS;
return responseBuilder;
}
state = STATE_OPEN_RESPONSE_BODY;
return responseBuilder;
} catch (EOFException e) {
// Provide more context if the server ends the stream before sending a response.
String address = "unknown";
if (realConnection != null) {
address = realConnection.route().address().url().redact();
}
throw new IOException("unexpected end of stream on "
+ address, e);
}
}
複製程式碼
可以看到,這裡主要是進行了兩件事:
- 呼叫
readHeaderLine
方法讀取首行並呼叫StatusLine.parse
方法構建StatusLine
物件 - 呼叫
readHeaders
方法讀取響應頭並構建Response
我們先看到 readHeaderLine
方法:
private String readHeaderLine() throws IOException {
String line = source.readUtf8LineStrict(headerLimit);
headerLimit -= line.length();
return line;
}
複製程式碼
其實就是讀取了服務端傳送來的資料的第一行。我們接著看到 SourceLine.parse
方法:
public static StatusLine parse(String statusLine) throws IOException {
// H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
// Parse protocol like "HTTP/1.1" followed by a space.
int codeStart;
Protocol protocol;
// 如果是 HTTP/1.x
if (statusLine.startsWith("HTTP/1.")) {
if (statusLine.length() < 9 || statusLine.charAt(8) != ' ') {
throw new ProtocolException("Unexpected status line: " + statusLine);
}
int httpMinorVersion = statusLine.charAt(7) - '0';
codeStart = 9;
// 根據HTTP版本號填入HTTP/1.0或HTTP/1.1
if (httpMinorVersion == 0) {
protocol = Protocol.HTTP_1_0;
} else if (httpMinorVersion == 1) {
protocol = Protocol.HTTP_1_1;
} else {
throw new ProtocolException("Unexpected status line: " + statusLine);
}
} else if (statusLine.startsWith("ICY ")) {
// 開頭為ICY則說明是HTTP/1.0
protocol = Protocol.HTTP_1_0;
codeStart = 4;
} else {
throw new ProtocolException("Unexpected status line: " + statusLine);
}
// Parse response code like "200". Always 3 digits.
if (statusLine.length() < codeStart + 3) {
throw new ProtocolException("Unexpected status line: " + statusLine);
}
int code;
try {
// 獲取響應碼
code = Integer.parseInt(statusLine.substring(codeStart, codeStart + 3));
} catch (NumberFormatException e) {
throw new ProtocolException("Unexpected status line: " + statusLine);
}
// Parse an optional response message like "OK" or "Not Modified". If it
// exists, it is separated from the response code by a space.
String message = "";
if (statusLine.length() > codeStart + 3) {
if (statusLine.charAt(codeStart + 3) != ' ') {
throw new ProtocolException("Unexpected status line: " + statusLine);
}
// 獲取Message
message = statusLine.substring(codeStart + 4);
}
return new StatusLine(protocol, code, message);
}
複製程式碼
這裡主要做了三件事:
- 根據 StatusLine 的開頭判斷並填入協議的型別(可能是 HTTP/1.0 或 HTTP/1.1)
- 填入響應碼
- 填入 Message
openResponseBodySource
@Override
public Source openResponseBodySource(Response response) {
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);
}
return newUnknownLengthSource();
}
複製程式碼
這裡根據是否知道 body
的長度,以及是否為 chunked
,分別返回了 newFixedLengthSource
及 newChunkedSource
的返回值。
newFixedLengthSource
private Source newFixedLengthSource(long length) {
if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
state = STATE_READING_RESPONSE_BODY;
return new FixedLengthSource(length);
}
複製程式碼
這裡實際上就是構建了一個繼承了 AbstractSource
的 FixedLengthSource
類,我們看到其 read
方法:
@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 = super.read(sink, Math.min(bytesRemaining, byteCount));
if (read == -1) {
realConnection.noNewExchanges(); // The server didn t supply the promised content length.
ProtocolException e = new ProtocolException("unexpected end of stream");
responseBodyComplete();
throw e;
}
bytesRemaining -= read;
if (bytesRemaining == 0) {
responseBodyComplete();
}
return read;
}
複製程式碼
這裡實際上是呼叫了父類的 read
方法,並對讀取到的 length
進行了確認。
newChunkedSource
private Source newChunkedSource(HttpUrl url) {
if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
state = STATE_READING_RESPONSE_BODY;
return new ChunkedSource(url);
}
複製程式碼
這裡實際上也是構建了一個繼承 AbstractSource
的 ChunkedSource
類,我們看到其 read
方法:
@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 (!hasMoreChunks) return -1;
if (bytesRemainingInChunk == 0 || bytesRemainingInChunk == NO_CHUNK_YET) {
readChunkSize();
if (!hasMoreChunks) return -1;
}
long read = super.read(sink, Math.min(byteCount, bytesRemainingInChunk));
if (read == -1) {
realConnection.noNewExchanges(); // The server didn t supply the promised chunk length.
ProtocolException e = new ProtocolException("unexpected end of stream");
responseBodyComplete();
throw e;
}
bytesRemainingInChunk -= read;
return read;
}
複製程式碼
這裡首先呼叫了 readChunkSize
讀取了 Chunk 的大小,之後呼叫了父類的 read
方法讀取了這個 chunk 對應的資料。
我們看到 readChunkSize
方法:
private void readChunkSize() throws IOException {
// Read the suffix of the previous chunk.
if (bytesRemainingInChunk != NO_CHUNK_YET) {
source.readUtf8LineStrict();
}
try {
bytesRemainingInChunk = source.readHexadecimalUnsignedLong();
String extensions = source.readUtf8LineStrict().trim();
if (bytesRemainingInChunk < 0 || (!extensions.isEmpty() && !extensions.startsWith(";"))) {
throw new ProtocolException("expected chunk size and optional extensions but was \""
+ bytesRemainingInChunk + extensions + "\"");
}
} catch (NumberFormatException e) {
throw new ProtocolException(e.getMessage());
}
if (bytesRemainingInChunk == 0L) {
hasMoreChunks = false;
trailers = readHeaders();
HttpHeaders.receiveHeaders(client.cookieJar(), url, trailers);
responseBodyComplete();
}
}
複製程式碼
這裡首先讀入了一個十六進位制的數,也就是 Chunk 的大小,之後若讀入的大小為 0,則說明後續沒有更多資料,直接返回了 -1(EOF)。
到這裡,HTTP/1.x 的寫入及讀取過程就分析完畢了
HTTP/2
HTTP/2 流量控制
我們先來研究一下 HTTP/2 的流量控制機制,這與我們接下來的原始碼分析有關。
HTTP/2 中存在著流量控制機制,它的目標是在不改變協議的情況下允許使用多種流量控制演算法。
它具有如下的特徵(摘自參考資料):
- 流量控制是特定於一個連線的。每種型別的流量控制都是在單獨的兩個端點之間的。
- 流量控制基於
WINDOW_UPDATE
幀。接收方公佈自己打算在每個 Stream 以及整個連線上分別接收多少位元組。 - 流量控制是有方向的,由接收者全面控制。接收方可以為每個流和整個連線設定任意的視窗大小。傳送方必須尊重接收方設定的流量控制限制。
- 無論是新流還是整個連線,流量控制視窗的初始值是65535位元組。
- 幀的型別決定了流量控制是否適用於幀。只有 DATA 幀服從流量控制,所有其它型別的幀並不消耗流量控制視窗的空間,從而保證重要的控制幀不會因流量控制阻塞。
- 流量控制不能被禁用。
- HTTP/2 只定義了
WINDOW_UPDATE
幀的格式和語義,沒有規定接收方如何決定何時傳送幀、傳送什麼樣的值,具體實現可以選擇任何滿足需求的演算法。
HTTP/2 中採用了 WINDOW_UPDATE
幀來通知對端增加視窗的大小,在它的資料中會指定增加的視窗大小,從而告訴對方自己有足夠的空間處理新的資料。
在 OkHttp 中實現了 HTTP/2 中的流量機制,它限制了同時能傳送的資料大小,其預設值為 65535,當傳送資料時,若視窗大小不足,則會進行阻塞,直到視窗有空閒大小。這樣的設計我想是因為 HTTP/2 中採用了請求的多路複用機制,多個請求可以複用同一條連線進行併發地進行。如果同時在一條連線上進行的網路請求太多會對網路造成擁塞。因此為了保證網路的暢通性,對每條連線採取了這種視窗機制,限制了每條連線最大傳送的資料量。
writeRequestHeaders
我們接著看到 Http2ExchangeCodec
的程式碼。
@Override
public void writeRequestHeaders(Request request) throws IOException {
if (stream != null) return;
boolean hasRequestBody = request.body() != null;
List<Header> requestHeaders = http2HeadersList(request);
stream = connection.newStream(requestHeaders, hasRequestBody);
// We may have been asked to cancel while creating the new stream and sending the request
// headers, but there was still no stream to close.
if (canceled) {
stream.closeLater(ErrorCode.CANCEL);
throw new IOException("Canceled");
}
stream.readTimeout().timeout(chain.readTimeoutMillis(), TimeUnit.MILLISECONDS);
stream.writeTimeout().timeout(chain.writeTimeoutMillis(), TimeUnit.MILLISECONDS);
}
複製程式碼
首先,這裡呼叫了 http2HeaderList
方法獲取了 Header 的 List,之後呼叫了 connection.newStream
方法初始化並獲取了 Http2Stream
物件。
我們先看到 http2HeaderList
做了什麼:
public static List<Header> http2HeadersList(Request request) {
Headers headers = request.headers();
List<Header> result = new ArrayList<>(headers.size() + 4);
result.add(new Header(TARGET_METHOD, request.method()));
result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.url())));
String host = request.header("Host");
if (host != null) {
result.add(new Header(TARGET_AUTHORITY, host)); // Optional.
}
result.add(new Header(TARGET_SCHEME, request.url().scheme()));
for (int i = 0, size = headers.size(); i < size; i++) {
// header names must be lowercase.
String name = headers.name(i).toLowerCase(Locale.US);
if (!HTTP_2_SKIPPED_REQUEST_HEADERS.contains(name)
|| name.equals(TE) && headers.value(i).equals("trailers")) {
result.add(new Header(name, headers.value(i)));
}
}
return result;
}
複製程式碼
這裡實際上就是在將 request.headers
轉變為一個 List<Header>
我們接著看一下 connection.newStream
方法:
private Http2Stream newStream(
int associatedStreamId, List<Header> requestHeaders, boolean out) throws IOException {
boolean outFinished = !out;
boolean inFinished = false;
boolean flushHeaders;
Http2Stream stream;
int streamId;
synchronized (writer) {
synchronized (this) {
// 計算當前Stream的id
if (nextStreamId > Integer.MAX_VALUE / 2) {
shutdown(REFUSED_STREAM);
}
if (shutdown) {
throw new ConnectionShutdownException();
}
streamId = nextStreamId;
nextStreamId += 2;
stream = new Http2Stream(streamId, this, outFinished, inFinished, null);
// ...
}
if (associatedStreamId == 0) {
writer.headers(outFinished, streamId, requestHeaders);
} else if (client) {
throw new IllegalArgumentException("client streams shouldn't have associated stream IDs");
} else { // HTTP/2 has a PUSH_PROMISE frame.
writer.pushPromise(associatedStreamId, streamId, requestHeaders);
}
}
if (flushHeaders) {
writer.flush();
}
return stream;
}
複製程式碼
可以看到,這裡首先計算了當前請求對應的 Stream 的 id,之後用其構建了一個 Http2Stream
物件。然後對於我們剛剛寫入的 header,這裡的 associatedStreamId
為 0,會呼叫到 writer.headers
寫入 Header
資訊。若 associatedStreamId
不為 0,則會呼叫 writer.pushPromise
方法,寫入 PUSH_PROMISE
幀,它可以參考這篇部落格:PUSH_PROMISE幀。
我們看到 writer.headers
方法究竟做了什麼:
public synchronized void headers(
boolean outFinished, int streamId, List<Header> headerBlock) throws IOException {
if (closed) throw new IOException("closed");
hpackWriter.writeHeaders(headerBlock);
long byteCount = hpackBuffer.size();
int length = (int) Math.min(maxFrameSize, byteCount);
byte type = TYPE_HEADERS;
byte flags = byteCount == length ? FLAG_END_HEADERS : 0;
if (outFinished) flags |= FLAG_END_STREAM;
frameHeader(streamId, length, type, flags);
sink.write(hpackBuffer, length);
if (byteCount > length) writeContinuationFrames(streamId, byteCount - length);
}
複製程式碼
這裡呼叫了 hpackWriter.writeHeaders
對 Header 進行了 HPACK 加密,然後呼叫 frameHeader
方法寫入幀頭,之後將 Header 的資料寫入 sink
中。(在 HTTP/2 中會對 Header 的資訊進行 HPACK 加密)
createRequestBody
@Override
public Sink createRequestBody(Request request, long contentLength) {
return stream.getSink();
}
複製程式碼
這裡返回了 Http2Stream
中的 sink
,它是一個繼承自 Sink
的 FramingSink
物件,我們看看其 write
方法:
@Override
public void write(Buffer source, long byteCount) throws IOException {
assert (!Thread.holdsLock(Http2Stream.this));
sendBuffer.write(source, byteCount);
while (sendBuffer.size() >= EMIT_BUFFER_SIZE) {
emitFrame(false);
}
}
複製程式碼
這裡先將資料寫入了 sendBuffer
中,之後不斷呼叫 emitFrame
將 sendBuffer
中的資料以資料幀的形式發出。讓我們看看 emitFrame
做了什麼:
/**
* Emit a single data frame to the connection. The frame's size be limited by this stream's
* write window. This method will block until the write window is nonempty.
*/
private void emitFrame(boolean outFinishedOnLastFrame) throws IOException {
long toWrite;
synchronized (Http2Stream.this) {
writeTimeout.enter();
try {
while (bytesLeftInWriteWindow <= 0 && !finished && !closed && errorCode == null) {
waitForIo(); // Wait until we receive a WINDOW_UPDATE for this stream.
}
} finally {
writeTimeout.exitAndThrowIfTimedOut();
}
checkOutNotClosed(); // Kick out if the stream was reset or closed while waiting.
toWrite = Math.min(bytesLeftInWriteWindow, sendBuffer.size());
bytesLeftInWriteWindow -= toWrite;
}
writeTimeout.enter();
try {
boolean outFinished = outFinishedOnLastFrame && toWrite == sendBuffer.size();
connection.writeData(id, outFinished, sendBuffer, toWrite);
} finally {
writeTimeout.exitAndThrowIfTimedOut();
}
}
複製程式碼
可以看到,這裡首先會阻塞直到 bytesLeftInWriteWindow
有足夠的空間,之後會呼叫 connection.writeData
方法寫入 bytesLeftInWriteWindow
剩餘大小的資料。這裡與 HTTP/2 的流量控制機制有關,我們放到後面介紹。
我們先看看 connection.writeData
做了什麼:
public void writeData(int streamId, boolean outFinished, Buffer buffer, long byteCount)
throws IOException {
if (byteCount == 0) { // Empty data frames are not flow-controlled.
writer.data(outFinished, streamId, buffer, 0);
return;
}
while (byteCount > 0) {
int toWrite;
synchronized (Http2Connection.this) {
try {
while (bytesLeftInWriteWindow <= 0) {
// Before blocking, confirm that the stream we're writing is still open. It's possible
// that the stream has since been closed (such as if this write timed out.)
if (!streams.containsKey(streamId)) {
throw new IOException("stream closed");
}
Http2Connection.this.wait(); // Wait until we receive a WINDOW_UPDATE.
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Retain interrupted status.
throw new InterruptedIOException();
}
toWrite = (int) Math.min(byteCount, bytesLeftInWriteWindow);
toWrite = Math.min(toWrite, writer.maxDataLength());
bytesLeftInWriteWindow -= toWrite;
}
byteCount -= toWrite;
writer.data(outFinished && byteCount == 0, streamId, buffer, toWrite);
}
}
複製程式碼
可以看到,這裡首先計算了能寫入的大小,不能超過剩餘視窗大小及設定的每個幀限制大小(預設為 0x4000,即 16384)
之後呼叫了 writer.data
方法進行了資料幀的寫入:
public synchronized void data(boolean outFinished, int streamId, Buffer source, int byteCount)
throws IOException {
if (closed) throw new IOException("closed");
byte flags = FLAG_NONE;
if (outFinished) flags |= FLAG_END_STREAM;
dataFrame(streamId, flags, source, byteCount);
}
複製程式碼
最後呼叫到了 dataFrame
方法寫入了一個資料幀:
void dataFrame(int streamId, byte flags, Buffer buffer, int byteCount) throws IOException {
byte type = TYPE_DATA;
frameHeader(streamId, byteCount, type, flags);
if (byteCount > 0) {
sink.write(buffer, byteCount);
}
}
複製程式碼
這裡先寫入了一個幀頭,之後寫入了對應的資料。
readResponseHeaders
@Override
public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
Headers headers = stream.takeHeaders();
Response.Builder responseBuilder = readHttp2HeadersList(headers, protocol);
if (expectContinue && Internal.instance.code(responseBuilder) == HTTP_CONTINUE) {
return null;
}
return responseBuilder;
}
複製程式碼
這裡首先呼叫了 stream.takeHeaders
獲取到了響應中的 Headers,之後呼叫了 readHttp2HeadersList
方法構建了 Response.Builder
。
我們先看到 stream.takeHeaders
方法:
/**
* Removes and returns the stream's received response headers, blocking if necessary until headers
* have been received. If the returned list contains multiple blocks of headers the blocks will be
* delimited by 'null'.
*/
public synchronized Headers takeHeaders() throws IOException {
readTimeout.enter();
try {
while (headersQueue.isEmpty() && errorCode == null) {
waitForIo();
}
} finally {
readTimeout.exitAndThrowIfTimedOut();
}
if (!headersQueue.isEmpty()) {
return headersQueue.removeFirst();
}
throw errorException != null ? errorException : new StreamResetException(errorCode);
}
複製程式碼
這裡在等待 headersQueue
中出現了新的 Header,看來真正的讀取過程不在這裡,這裡僅僅是在等待獲取 Header,而資料真正的獲取過程在其它地方,我們在後面再討論真正的資料讀取的地方在哪裡。
拿到 Header 後,呼叫了 readHttp2HeadersList
方法構建 Response.Builder
:
/**
* Returns headers for a name value block containing an HTTP/2 response.
*/
public static Response.Builder readHttp2HeadersList(Headers headerBlock,
Protocol protocol) throws IOException {
StatusLine statusLine = null;
Headers.Builder headersBuilder = new Headers.Builder();
for (int i = 0, size = headerBlock.size(); i < size; i++) {
String name = headerBlock.name(i);
String value = headerBlock.value(i);
if (name.equals(RESPONSE_STATUS_UTF8)) {
statusLine = StatusLine.parse("HTTP/1.1 " + value);
} else if (!HTTP_2_SKIPPED_RESPONSE_HEADERS.contains(name)) {
Internal.instance.addLenient(headersBuilder, name, value);
}
}
if (statusLine == null) throw new ProtocolException("Expected ':status' header not present");
return new Response.Builder()
.protocol(protocol)
.code(statusLine.code)
.message(statusLine.message)
.headers(headersBuilder.build());
}
複製程式碼
這裡實際上就是對剛剛的 Header 中比較特殊的 Header 進行了處理,之後設定進了建立的 Response.Builder
openResponseBodySource
@Override
public Source openResponseBodySource(Response response) {
return stream.getSource();
}
複製程式碼
這裡直接返回了 stream.source
,而這個 source
實際上是 FramingSource
物件,我們看看其 read
方法:
@Override
public long read(Buffer sink, long byteCount) throws IOException {
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
while (true) {
long readBytesDelivered = -1;
IOException errorExceptionToDeliver = null;
// 1. Decide what to do in a synchronized block.
synchronized (Http2Stream.this) {
readTimeout.enter();
try {
// ...
if (closed) {
throw new IOException("stream closed");
} else if (readBuffer.size() > 0) {
// 讀取資料
readBytesDelivered = readBuffer.read(sink, Math.min(byteCount, readBuffer.size()));
unacknowledgedBytesRead += readBytesDelivered;
if (errorExceptionToDeliver == null
&& unacknowledgedBytesRead
>= connection.okHttpSettings.getInitialWindowSize() / 2) {
// 通知對方增加視窗大小
connection.writeWindowUpdateLater(id, unacknowledgedBytesRead);
unacknowledgedBytesRead = 0;
}
} else if (!finished && errorExceptionToDeliver == null) {
// 等待I/O
waitForIo();
continue;
}
} finally {
readTimeout.exitAndThrowIfTimedOut();
}
}
if (readBytesDelivered != -1) {
// Update connection.unacknowledgedBytesRead outside the synchronized block.
updateConnectionFlowControl(readBytesDelivered);
return readBytesDelivered;
}
// ...
return -1; // This source is exhausted.
}
}
複製程式碼
首先這裡如果 I/O 未結束,會阻塞等待 I/O 結束。當 I/O 結束後,會呼叫 read
方法進行資料的讀取。(這也說明了真正的資料獲取不是在這裡進行,而是在其它地方)
此時若我們這邊已經接收的資料大小超過了視窗的大小,則會呼叫 connection.writeWindowUpdateLater
方法通知對方增加視窗大小,增加的大小為我們已接收的資料大小,也就說明我們這邊有多餘的能力來處理更多流量。我們可以看到 connection.writeWindowUpdateLater
方法:
void writeWindowUpdateLater(final int streamId, final long unacknowledgedBytesRead) {
try {
writerExecutor.execute(
new NamedRunnable("OkHttp Window Update %s stream %d", connectionName, streamId) {
@Override
public void execute() {
try {
writer.windowUpdate(streamId, unacknowledgedBytesRead);
} catch (IOException e) {
failConnection(e);
}
}
});
} catch (RejectedExecutionException ignored) {
// This connection has been closed.
}
}
複製程式碼
可以看出,這裡實際上是通過向對方傳送一個 WINDOW_UPDATE
幀來實現的,由於是耗時操作,因此這裡採用了非同步的方式。
資料的讀取
那麼我們的資料究竟是在哪裡進行讀取的呢?我們可以看看前面的 headerQueue
在何時會被新增新的 Header,其中在 receiveHeaders
方法中對 headerQueue
進行了新增操作。這應該就是我們要找的方法了:
/**
* Accept headers from the network and store them until the client calls {@link #takeHeaders}, or
* {@link FramingSource#read} them.
*/
void receiveHeaders(Headers headers, boolean inFinished) {
assert (!Thread.holdsLock(Http2Stream.this));
boolean open;
synchronized (this) {
if (!hasResponseHeaders || !inFinished) {
hasResponseHeaders = true;
headersQueue.add(headers);
} else {
this.source.trailers = headers;
}
if (inFinished) {
this.source.finished = true;
}
open = isOpen();
notifyAll();
}
if (!open) {
connection.removeStream(id);
}
}
複製程式碼
那它又是何時被呼叫的呢?它被 ReadRunnable.headers
方法所呼叫,而此方法又被 Http2Reader.readHeaders
方法呼叫,最後我們找到了 Http2Reader.nextFrame
方法:
public boolean nextFrame(boolean requireSettings, Handler handler) throws IOException {
try {
source.require(9); // Frame header size
} catch (EOFException e) {
return false; // This might be a normal socket close.
}
int length = readMedium(source);
if (length < 0 || length > INITIAL_MAX_FRAME_SIZE) {
throw ioException("FRAME_SIZE_ERROR: %s", length);
}
byte type = (byte) (source.readByte() & 0xff);
if (requireSettings && type != TYPE_SETTINGS) {
throw ioException("Expected a SETTINGS frame but was %s", type);
}
byte flags = (byte) (source.readByte() & 0xff);
int streamId = (source.readInt() & 0x7fffffff); // Ignore reserved bit.
if (logger.isLoggable(FINE)) logger.fine(frameLog(true, streamId, length, type, flags));
switch (type) {
case TYPE_DATA:
readData(handler, length, flags, streamId);
break;
case TYPE_HEADERS:
readHeaders(handler, length, flags, streamId);
break;
case TYPE_PRIORITY:
readPriority(handler, length, flags, streamId);
break;
case TYPE_RST_STREAM:
readRstStream(handler, length, flags, streamId);
break;
case TYPE_SETTINGS:
readSettings(handler, length, flags, streamId);
break;
case TYPE_PUSH_PROMISE:
readPushPromise(handler, length, flags, streamId);
break;
case TYPE_PING:
readPing(handler, length, flags, streamId);
break;
case TYPE_GOAWAY:
readGoAway(handler, length, flags, streamId);
break;
case TYPE_WINDOW_UPDATE:
readWindowUpdate(handler, length, flags, streamId);
break;
default:
// Implementations MUST discard frames that have unknown or unsupported types.
source.skip(length);
}
return true;
}
複製程式碼
可以看到,這個方法是對資料幀的資料進行讀取,其中對不同的資料幀的型別進行了判斷,並呼叫了不同的方法讀取不同型別的資料。我們看看 nextFrame
又是在哪裡呼叫的。
最後我們找到了 ReaderRunnable.execute
方法:
@Override
protected void execute() {
ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR;
ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR;
IOException errorException = null;
try {
reader.readConnectionPreface(this);
while (reader.nextFrame(false, this)) {
}
connectionErrorCode = ErrorCode.NO_ERROR;
streamErrorCode = ErrorCode.CANCEL;
} catch (IOException e) {
errorException = e;
connectionErrorCode = ErrorCode.PROTOCOL_ERROR;
streamErrorCode = ErrorCode.PROTOCOL_ERROR;
} finally {
close(connectionErrorCode, streamErrorCode, errorException);
Util.closeQuietly(reader);
}
}
複製程式碼
裡面在不斷地呼叫 reader.nextFrame
讀取下一幀的資料,看來有一個地方開闢了一個執行緒來不斷地對對方的資料進行讀取。其啟動的時機實際上是 Http2Connection.start
方法:
/**
* @param sendConnectionPreface true to send connection preface frames. This should always be true
* except for in tests that don t check for a connection preface.
*/
void start(boolean sendConnectionPreface) throws
IOException {
if (sendConnectionPreface) {
writer.connectionPreface();
writer.settings(okHttpSettings);
int windowSize = okHttpSettings.getInitialWindowSize();
if (windowSize != Settings.DEFAULT_INITIAL_WINDOW_SIZE) {
writer.windowUpdate(0, windowSize - Settings.DEFAULT_INITIAL_WINDOW_SIZE);
}
}
new Thread(readerRunnable).start(); // Not a daemon thread.
}
複製程式碼
看來在 Http2 連線啟動時,就會建立一個新的執行緒不斷地對資料進行讀取,之後再將其分發到不同的 Stream 中,交給對應的請求的響應。
這樣看來,如果我們收到了 WINDOW_UPDATE
幀,就會通知我們的 HttpConnection
從而增加我們的視窗大小。因此 HTTP/2 的設計更像是一種流的設計,兩端不斷地從這個流中取出自己需要的資料。
總結
到這裡對 OkHttp 的整個流程就分析完成了,對於 HTTP/1.x 的請求,主要是對普通請求、響應及 chunked
特性下的請求、響應進行了不同的處理。
而對 HTTP/2 請求,OkHttp 很好地對 HTTP/2 的流量控制機制進行了支援,通過了視窗大小對寫入的資料大小進行了限制,通過阻塞喚醒機制實現了 I/O 任務與資料處理之間的先後排程。在 HTTP/2 連線開啟時,會啟動一個讀取執行緒不斷地從 TCP 連線中讀取資料幀,並將其分發到各個 Stream 中。從這些程式碼裡慢慢體會到了 HTTP/2 與 HTTP/1.x 的顯著區別,雖然 HTTP 協議都是面向無連線的協議,但 HTTP/2 通過這種多路複用機制實現了一個更復雜但更加有效的應用層協議。
讀到這裡不禁感嘆 OkHttp 的設計真的是十分精妙,就是通過這些細小的細節設計,才造就了這樣一個龐大但又易於擴充的網路請求框架,在這個請求的過程中幾乎每個細小的點都會將決定權交給使用者,極大提高了其擴充套件性。同時這種攔截器機制的設計也十分出色,使用者可以分別在發起請求前後及真正執行 I/O 前後對整個 HTTP 請求過程通過攔截器進行一些處理,但又不影響其他攔截器的正常執行。
雖說看上去整個 OkHttp 的實現原理我們成功進行了剖析,但還有一些小細節等待我們去進行發掘,同時我們還有一個 OkHttp 中所用到的核心庫沒有進行解析——Okio,如果有興趣的讀者可以期待後續的博文。