HTTP 2.0
是對1.x的擴展而非替代,之因此是「2.0」,是由於它改變了客戶端與服務器之間交換數據的方式。HTTP 2.0
增長了新的二進制分幀數據層,而這一層並不兼容以前的HTTP 1.x
服務器及客戶端——是謂2.0。 在正式介紹HTTP 2.0
以前,咱們須要先了解幾個概念。html
Request
、Response
)對應的完整的一系列數據幀。HTTP 2.0
通訊的最小單位,如Header
幀(存儲的是Header
)、DATA
幀(存儲的是發送的內容或者內容的一部分)。 總所周知,HTTP 1.x
擁有隊首阻塞、不支持多路複用、Header
沒法壓縮等諸多缺點。儘管針對這些缺點也提出了不少解決方案,如長鏈接、鏈接與合併請求、HTTP管道等,但都治標不治本,直到HTTP 2.0
的出現,它新增的如下設計從根本上解決了HTTP 1.x
所面臨的諸多問題。java
HTTP 2.0
性能加強的核心,改變了客戶端與服務器之間交互數據的方式,將傳輸的信息(Header
、Body
等)分割爲更小的消息和幀,並採用二進制格式的編碼。HTTP
消息分解爲互不依賴的幀,而後亂序發送,最後再在另外一端把這些消息組合起來。HTTP 2.0
可讓全部數據流共用一個鏈接,從而更有效的使用TCP
鏈接TCP
的流量控制實現是如出一轍的。HTTP 2.0
能夠對一個客戶端請求發送多個響應,即除了最初請求響應外,服務器還能夠額外的向客戶端推送資源,而無需客戶端明確地請求。HTTP 2.0
會在客戶端及服務器使用「首部表」來跟蹤和存儲以前發送的鍵-值對,對於相同的數據,不會再經過每次請求和響應發送。首部表在鏈接存續期間始終存在,由客戶端及服務器共同漸進的更新。每一個新的首部鍵-值對要麼追加到當前表的末尾,要麼替換表中的值。 雖然HTTP 2.0
解決了1.x中的諸多問題,但它也存在如下問題。linux
HTTP
隊首阻塞現象,但TCP
層次上仍然存在隊首阻塞現象。要想完全解決這個問題,就須要完全拋棄TCP
,本身來定義協議。能夠參考谷歌的QUIC。TCP
窗口縮放被禁用,那寬帶延遲積效應可能會限制鏈接的吞吐量。TCP
擁塞窗口會縮小。 HTTP 2.0
的根本改進仍是新增的二進制分幀層。與HTTP 1.x
使用換行符分割純文本不一樣,二進制分幀層更加簡介,經過代碼處理起來更簡單也更有效。 web
創建了HTTP 2.0
鏈接後,客戶端與服務器會經過交換幀來通訊,幀也是基於這個新協議通訊的最小單位。全部幀都共享一個8字節的首部,其中包括幀的長度、類型、標誌,還有一個保留位和一個31位的流標識符。 算法
HTTP 2.0
的流 HTTP 2.0
規定了如下的幀類型。promise
HTTP
消息體Header
) 在發送應用數據以前,必須建立一個新流並隨之發送相應的元數據,好比流的優先級、HTTP首部等。HTTP 2.0
協議規定客戶端和服務器均可以發起新流,所以有如下兩種可能。服務器
HEADERS
幀來發起新流,這個幀裏包含帶有新流ID的公用首部、可選的31位優先值,以及一組HTTP
鍵值對首部PUSH_PROMISE
幀來發起推送流,這個幀與HEADER
幀等效,但它包含「要約流ID」,沒有優先值 應用數據能夠分爲多個DATA幀,最後一幀要翻轉幀首部的END_STREAM
字段。 網絡
數據淨荷不會被另行編碼或壓縮。DATA幀的編碼方式取決於應用或者服務器,純文本、gzip壓縮、圖片或者視頻壓縮格式均可以。整個幀由公用的8字節首部及HTTP淨荷組成。 從技術上說,DATA幀的長度字段決定了每幀的數據淨荷最多可達-1(65535)字節。但是,爲了減小隊首阻塞,
HTTP 2.0
標準要求DATA幀不能超過 (16383)字節。長度超過這個閥值的數據,就得分幀發送。併發
HTTP 2.0
是經過RealConnection
的startHttp2
方法開啓的,在該方法中會建立一個Http2Connection
對象,而後調用Http2Connection
的start
方法。app
private void startHttp2(int pingIntervalMillis) throws IOException {
socket.setSoTimeout(0); // HTTP/2 connection timeouts are set per-stream.
//建立Http2Connection對象
http2Connection = new Http2Connection.Builder(true)
.socket(socket, route.address().url().host(), source, sink)
.listener(this)
.pingIntervalMillis(pingIntervalMillis)
.build();
//開啓HTTP 2.0
http2Connection.start();
}
複製代碼
在start
方法中會首先給服務器發送一個字符串PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n來進行協議的最終肯定,並用於創建 HTTP/2 鏈接的初始設置。而後給服務器發送一個SETTINGS
類型的Header
幀,該幀主要是將客戶端每一幀的最大容量、Header
表的大小、是否開啓推送等信息告訴給服務器。若是Window
的大小發生改變,就還須要更新Window
的大小(HTTP 2.0
的默認窗口大小爲64KB,而客戶端則須要將該大小改成16M,從而避免頻繁的更新)。最後開啓一個子線程來讀取從服務器返回的數據。
public void start() throws IOException {
start(true);
}
void start(boolean sendConnectionPreface) throws IOException {
if (sendConnectionPreface) {
//發送一個字符串PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n來進行協議的最終肯定,即序言幀
writer.connectionPreface();
//告訴服務器本地的配置信息
writer.settings(okHttpSettings);
//okHttpSetting中Window的大小是設置爲16M
int windowSize = okHttpSettings.getInitialWindowSize();
//默認是64kb,但若是在客戶端則須要從新設置爲16M
if (windowSize != Settings.DEFAULT_INITIAL_WINDOW_SIZE) {
//更新窗口大小
writer.windowUpdate(0, windowSize - Settings.DEFAULT_INITIAL_WINDOW_SIZE);
}
}
//子線程監聽服務器返回的消息
new Thread(readerRunnable).start(); // Not a daemon thread.
}
複製代碼
從ReaderRunnable
的名稱就能夠看出它是用來讀取從服務器返回的各類類型數據。
class ReaderRunnable extends NamedRunnable implements Http2Reader.Handler {
...
@Override protected void execute() {
ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR;
ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR;
try {
//讀取服務器返回的序言幀
reader.readConnectionPreface(this);
//不斷的讀取下一幀,全部消息從這裏開始分發
while (reader.nextFrame(false, this)) {
}
connectionErrorCode = ErrorCode.NO_ERROR;
streamErrorCode = ErrorCode.CANCEL;
} catch (IOException e) {
...
} finally {
...
}
}
//讀取返回的DATA類型數據
@Override public void data(boolean inFinished, int streamId, BufferedSource source, int length) throws IOException {...}
//讀取返回的HEADERS類型數據
@Override public void headers(boolean inFinished, int streamId, int associatedStreamId, List<Header> headerBlock) {...}
//讀取返回的RST_TREAM類型數據
@Override public void rstStream(int streamId, ErrorCode errorCode) {...}
//讀取返回的SETTINGS類型數據
@Override public void settings(boolean clearPrevious, Settings newSettings) {...}
//回覆服務器返回的ackSettings
private void applyAndAckSettings(final Settings peerSettings) ...} //恢復客戶端發送的SETTING數據,客戶端默認不實現 @Override public void ackSettings() {...}
//讀取返回的PING類型數據
@Override public void ping(boolean reply, int payload1, int payload2) {...}
//讀取服務器返回的GOAWAY類型數據
@Override public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {...}
//讀取服務器返回的WINDOW_UPDATE類型數據
@Override public void windowUpdate(int streamId, long windowSizeIncrement) {...}
//讀取服務器返回的PRIORITY類型數據
@Override public void priority(int streamId, int streamDependency, int weight, boolean exclusive) {...}
//讀取返回的PUSH_PROMISE類型數據
@Override
public void pushPromise(int streamId, int promisedStreamId, List<Header> requestHeaders) {... }
//備用Service
@Override public void alternateService(int streamId, String origin, ByteString protocol, String host, int port, long maxAge) {...}
}
複製代碼
上面簡述了在OkHttp
中如何開啓HTTP 2.0
協議。下面就來介紹客戶端與服務器經過HTTP 2.0
協議來進行數據讀寫操做。
向服務器寫入Header
是經過httpCodec.writeRequestHeaders(request)
來實現的,httpCodec
在HTTP 2.0
協議下的實現類是Http2Codec
。writeRequestHeaders
方法主要是建立一個新流Http2Stream
,在這個流建立成功後就會向服務器發送Headers
類型數據。
boolean hasRequestBody = request.body() != null;
List<Header> requestHeaders = http2HeadersList(request);
//建立新流
stream = connection.newStream(requestHeaders, hasRequestBody);
//咱們可能在建立新流併發送Headers時被要求取消,但仍然沒有要關閉的流。
if (canceled) {
stream.closeLater(ErrorCode.CANCEL);
throw new IOException("Canceled");
}
...
}
//如下方法在Http2Connection類中
public Http2Stream newStream(List<Header> requestHeaders, boolean out) throws IOException {
return newStream(0, requestHeaders, out);
}
private Http2Stream newStream( int associatedStreamId, List<Header> requestHeaders, boolean out) throws IOException {
...
synchronized (writer) {
synchronized (this) {
//每一個TCP鏈接的流數量不能超過Integer.MAX_VALUE
if (nextStreamId > Integer.MAX_VALUE / 2) {
shutdown(REFUSED_STREAM);
}
if (shutdown) {
throw new ConnectionShutdownException();
}
//每一個流的ID
streamId = nextStreamId;
//下一個流的ID是在當前流ID基礎上加2
nextStreamId += 2;
//建立新流
stream = new Http2Stream(streamId, this, outFinished, inFinished, null);
flushHeaders = !out || bytesLeftInWriteWindow == 0L || stream.bytesLeftInWriteWindow == 0L;
if (stream.isOpen()) {
streams.put(streamId, stream);
}
}
if (associatedStreamId == 0) {
//向服務器寫入Headers
writer.headers(outFinished, streamId, requestHeaders);
} else if (client) {
throw new IllegalArgumentException("client streams shouldn't have associated stream IDs");
} else {//用於服務器
writer.pushPromise(associatedStreamId, streamId, requestHeaders);
}
}
//刷新
if (flushHeaders) {
writer.flush();
}
return stream;
}
複製代碼
在客戶端,流的ID是從3開始的全部奇數,在服務器,流的ID則是全部偶數。在Http2Connection
的構造函數中定義了定義了流ID的初始值。
Http2Connection(Builder builder) {
....
//若是是客戶端,流的ID則從1開始
nextStreamId = builder.client ? 1 : 2;
if (builder.client) {
//在HTTP2中,1保留,用於升級
nextStreamId += 2;
}
...
}
複製代碼
readResponseHeaders
是從服務器讀取Headers
數據,該方法在Http2Codec
中。
@Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
//從流中拿到Headers信息,
Headers headers = stream.takeHeaders();
Response.Builder responseBuilder = readHttp2HeadersList(headers, protocol);
if (expectContinue && Internal.instance.code(responseBuilder) == HTTP_CONTINUE) {
return null;
}
return responseBuilder;
}
//該方法在Http2Stream中
public synchronized Headers takeHeaders() throws IOException {
readTimeout.enter();
try {
//若是隊列中沒有數據就等待
while (headersQueue.isEmpty() && errorCode == null) {
waitForIo();
}
} finally {
readTimeout.exitAndThrowIfTimedOut();
}
//從隊列中拿到Headers數據
if (!headersQueue.isEmpty()) {
return headersQueue.removeFirst();
}
throw new StreamResetException(errorCode);
}
複製代碼
headersQueue
是一個雙端隊列,它主要是存儲服務器返回的Headers
。當服務器返回Headers
時,就會更新該鏈表。
在建立流的時候,都會建立一個FramingSink
及FramingSource
對象。FramingSink
用來向服務器寫入數據,FramingSource
則讀取服務器返回的數據。所以關於讀/寫Body
其實就是對Okio
的運用,不熟悉Okio
的能夠先去了解一下Okio的知識。
//向服務器寫數據
final class FramingSink implements Sink {
private static final long EMIT_BUFFER_SIZE = 16384;
...
@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);
}
}
//
private void emitFrame(boolean outFinished) throws IOException {
...
try {
//向服務器寫入DATA類型數據
connection.writeData(id, outFinished && toWrite == sendBuffer.size(), sendBuffer, toWrite);
} finally {
writeTimeout.exitAndThrowIfTimedOut();
}
}
...
}
//從服務器讀取數據
private final class FramingSource implements Source {
//將從網絡讀取的數據寫入該Buffer,僅供讀線程訪問
private final Buffer receiveBuffer = new Buffer();
//可讀buffer
private final Buffer readBuffer = new Buffer();
//緩衝的最大字節數
private final long maxByteCount;
...
//從receiveBuffer中讀取數據
@Override public long read(Buffer sink, long byteCount) throws IOException {...}
...
//接收服務器傳遞的數據,僅在ReaderRunnable中調用
void receive(BufferedSource in, long byteCount) throws IOException {...}
...
}
複製代碼
前面介紹了從服務器讀寫數據,但不管如何都離不開Http2Reader
與Http2Writer
這兩個類,畢竟這兩個類纔是真正向服務器執行讀寫操做的。先來看向服務器寫數據。
final class Http2Writer implements Closeable {
...
//寫入序言幀,來進行協議的最終肯定
public synchronized void connectionPreface() throws IOException {...}
//發送PUSH_PROMISE類型數據
public synchronized void pushPromise(int streamId, int promisedStreamId, List<Header> requestHeaders) throws IOException {...}
...
//發送RST_TREAM類型數據
public synchronized void rstStream(int streamId, ErrorCode errorCode) throws IOException {...}
//發送DATA類型數據
public synchronized void data(boolean outFinished, int streamId, Buffer source, int byteCount) throws IOException {...}
//發送SETTINGS類型數據
public synchronized void settings(Settings settings) throws IOException {...}
//發送PING類型數據
public synchronized void ping(boolean ack, int payload1, int payload2) throws IOException {...}
//發送GOAWAY類型數據
public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData) throws IOException {...}
//發送WINDOW_UPDATE類型數據,進行Window更新
public synchronized void windowUpdate(int streamId, long windowSizeIncrement) throws IOException {...}
//發送HEADERS類型數據
public void frameHeader(int streamId, int length, byte type, byte flags) throws IOException {...}
@Override public synchronized void close() throws IOException {
closed = true;
sink.close();
}
...
//寫入CONTINUATION類型數據
private void writeContinuationFrames(int streamId, long byteCount) throws IOException {...}
//寫入headers
void headers(boolean outFinished, int streamId, List<Header> headerBlock) throws IOException {...}
}
複製代碼
下面再來看看從服務器讀數據,基本上就是根據數據的類型來進行分發。
final class Http2Reader implements Closeable {
...
//讀取數據
public boolean nextFrame(boolean requireSettings, Handler handler) throws IOException {
try {
source.require(9); // Frame header size
} catch (IOException e) {
return false; // This might be a normal socket close.
}
// 0 1 2 3
// 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 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Length (24) |
// +---------------+---------------+---------------+
// | Type (8) | Flags (8) |
// +-+-+-----------+---------------+-------------------------------+
// |R| Stream Identifier (31) |
// +=+=============================================================+
// | Frame Payload (0...) ...
// +---------------------------------------------------------------+
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));
//這裏的handler是ReaderRunnable對象
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;
}
...
}
複製代碼
在Http2Reader
與Http2Writer
中都是以幀的形式(二進制)來讀取或者寫入數據的,這樣相對字符串效率會更高,固然,咱們還能夠用哈夫曼算法(OkHttp
支持哈夫曼算法)來對幀進行壓縮,從而得到更好的性能。 記得在HTTP 1.x
協議下的網絡優化就有用Protocol Buffer
(二進制)來替代字符串傳遞這一個選擇,而若是用HTTP 2.0
則無需使用Protocol Buffer
。
到這裏,相必對HTTP 2.0
有了一個大概的瞭解,更多的就須要去實踐了。固然若是要使用HTTP 2.0
協議,就須要客戶端及服務器一塊兒才能搞定。注意:目前OKHttp
僅支持在https
請求下使用HTTP 2.0
。
【參考資料】 《Web性能權威指南》 科普:QUIC協議原理分析 初識HTTP2.0協議 HTTP 2.0 協議詳解 HTTP協議探究(六):H2幀詳解和HTTP優化 HTTP/2筆記之鏈接創建