OkHttp 原始碼剖析系列(七)——請求的發起及響應的讀取

N0tExpectErr0r發表於2020-01-03

你好,我是 N0tExpectErr0r,一名熱愛技術的 Android 開發

我的個人部落格:blog.N0tExpectErr0r.cn

OkHttp 原始碼剖析系列文章目錄:

OkHttp 原始碼剖析系列(一)——請求的發起及攔截器機制概述

OkHttp 原始碼剖析系列(二)——攔截器整體流程分析

OkHttp 原始碼剖析系列(三)——快取機制

OkHttp 原始碼剖析系列(四)——連線建立概述

OkHttp 原始碼剖析系列(五)——代理路由選擇

OkHttp 原始碼剖析系列(六)——連線複用機制及連線的建立

OkHttp 原始碼剖析系列(七)——請求的發起及響應的讀取

終於來到了我們 OkHttp 的最後一個部分——請求的發起。讓我們回顧一下 CallServerInterceptor 的大體流程:

  1. 呼叫 exchange.writeRequestHeaders 寫入請求頭
  2. 呼叫 exchange.createRequestBody 獲取 Sink
  3. 呼叫 ResponseBody.writeTo 寫入請求體
  4. 呼叫 exchange.readResponseHeaders 讀入響應頭
  5. 呼叫 exchange.openResponseBody 方法讀取響應體

而我們知道,Exchange 最後實際上轉調到了 ExchangeCodec 中的對應方法,而 ExchangeCodec 有兩個實現——Http1ExchangeCodecHttp2ExchangeCodec

它們的建立過程在建立連線的過程中的 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();
}
複製程式碼

其實這裡就是構建並返回了一個繼承於 SinkChunkedSink 物件,我們可以看看它的 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();
}
複製程式碼

這裡也是構建並返回了一個繼承於 SinkKnownLengthSink 物件,我們可以看到其 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);
    }
}
複製程式碼

可以看到,這裡主要是進行了兩件事:

  1. 呼叫 readHeaderLine 方法讀取首行並呼叫 StatusLine.parse 方法構建 StatusLine 物件
  2. 呼叫 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);
}
複製程式碼

這裡主要做了三件事:

  1. 根據 StatusLine 的開頭判斷並填入協議的型別(可能是 HTTP/1.0 或 HTTP/1.1)
  2. 填入響應碼
  3. 填入 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,分別返回了 newFixedLengthSourcenewChunkedSource 的返回值。

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);
}
複製程式碼

這裡實際上就是構建了一個繼承了 AbstractSourceFixedLengthSource 類,我們看到其 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);
}
複製程式碼

這裡實際上也是構建了一個繼承 AbstractSourceChunkedSource 類,我們看到其 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 中存在著流量控制機制,它的目標是在不改變協議的情況下允許使用多種流量控制演算法。

它具有如下的特徵(摘自參考資料):

  1. 流量控制是特定於一個連線的。每種型別的流量控制都是在單獨的兩個端點之間的。
  2. 流量控制基於 WINDOW_UPDATE 幀。接收方公佈自己打算在每個 Stream 以及整個連線上分別接收多少位元組。
  3. 流量控制是有方向的,由接收者全面控制。接收方可以為每個流和整個連線設定任意的視窗大小。傳送方必須尊重接收方設定的流量控制限制。
  4. 無論是新流還是整個連線,流量控制視窗的初始值是65535位元組。
  5. 幀的型別決定了流量控制是否適用於幀。只有 DATA 幀服從流量控制,所有其它型別的幀並不消耗流量控制視窗的空間,從而保證重要的控制幀不會因流量控制阻塞。
  6. 流量控制不能被禁用。
  7. 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,它是一個繼承自 SinkFramingSink 物件,我們看看其 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 中,之後不斷呼叫 emitFramesendBuffer 中的資料以資料幀的形式發出。讓我們看看 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,如果有興趣的讀者可以期待後續的博文。

參考資料

PUSH_PROMISE幀

理解HTTP/2流量控制(一)

相關文章