標籤:

HTTP/2 學習筆記

Why HTTP/2

  • HTTP/1.1 是文本協議
    • 對用戶友好,但對計算機不友好 (parse 不高效)
  • TCP 連接管理
    • TCP 慢啟動,因此儘可能的復用連接 (keep-alive)
    • 一個 TCP 連接上只能有一個 request/response,可以使用 pipeline 實現並發請求,但會有頭部阻塞問題,現代瀏覽器默認不啟用 pipeline,而是通過對一個域名同時建立多個連接 workaround
  • 頭部 field 重複,造成資源浪費
  • 資源沒有優先順序

HTTP/2 特性:

HTTP/2 沒有改變 HTTP/1.1 的語義 (method, status code, URI, header 均保留),其改變了數據是如何傳輸的。

  1. 多路復用: 為了更高效地利用多路復用,另外實現了 Flow Control 和 Prioritization
  2. Server Push
  3. 頭部壓縮

HTTP2 基本概念

HTTP/1.1 是文本協議,而 HTTP/2 是二進位協議,其通過發送不同的二進位幀(frame)來進行傳輸

先來看看 HTTP/2 frame 的格式

+-----------------------------------------------+ | Length (24) | +---------------+---------------+---------------+ | Type (8) | Flags (8) | +-+-------------+---------------+-------------------------------+ |R| Stream Identifier (31) | +=+=============================================================+ | Frame Payload (0...) ... +---------------------------------------------------------------+

所有的 frame 都有一個 9 byte 的 header 和不定長的 payload 組成。header 中的 Length 指 payload 的長度,因為可以很方便的 parse。stream identifier 是每個 stream 唯一的標誌,client 發起的 stream stream identifier 為奇數,server 發起的 stream stream identifier ,stream identifier 為 0x0 表示這個 frame 針對整個 connection 而不是單個 stream。

上面的 Frame 是一個很容易理解的物理概念,而 HTTP/2 在 Frame 的基礎上抽象出了 Stream。Stream 其實就是一段包含客戶端和服務端交換 frame 的序列。

HTTP/2 中的多路復用其實指的就是一個 HTTP/2 連接上可以同時存在多個 stream。

Stream 狀態機

+--------+ send PP | | recv PP ,--------| idle |--------. / | | v +--------+ v +----------+ | +----------+ | | | send H / | | ,------| reserved | | recv H | reserved |------. | | (local) | | | (remote) | | | +----------+ v +----------+ | | | +--------+ | | | | recv ES | | send ES | | | send H | ,-------| open |-------. | recv H | | | / | | | | | v v +--------+ v v | | +----------+ | +----------+ | | | half | | | half | | | | closed | | send R / | closed | | | | (remote) | | recv R | (local) | | | +----------+ | +----------+ | | | | | | | | send ES / | recv ES / | | | | send R / v send R / | | | | recv R +--------+ recv R | | | send R / `----------->| |<----------- send R / | | recv R | closed | recv R | `----------------------->| |<---------------------- +--------+ send: endpoint sends this frame recv: endpoint receives this frame H: HEADERS frame (with implied CONTINUATIONs) PP: PUSH_PROMISE frame (with implied CONTINUATIONs) ES: END_STREAM flag R: RST_STREAM frame

理解 HTTP/2 必須理解 stream 的狀態以及如何通過發送不同的 frame 使 stream 狀態發生轉換。上面的圖比較複雜,接下來會逐個來看。

首先來介紹最基本的兩種 frame,HEADER 和 DATA。可以簡單的看做:HEADER 用來傳輸 HTTP/1.1 的 header,DATA 用來傳輸 HTTP/1.1 的 body。這兩種 frame 用來傳遞真正對上層應用有用的信息,其他 frame 都是用來控制 HTTP/2 stream / connection。

+---------------+ |Pad Length? (8)| +-+-------------+-----------------------------------------------+ |E| Stream Dependency? (31) | +-+-------------+-----------------------------------------------+ | Weight? (8) | +-+-------------+-----------------------------------------------+ | Header Block Fragment (*) ... +---------------------------------------------------------------+ | Padding (*) ... +---------------------------------------------------------------+ +---------------+ |Pad Length? (8)| +---------------+-----------------------------------------------+ | Data (*) ... +---------------------------------------------------------------+ | Padding (*) ...

注意上面的圖都只是對應 frame 的 payload 部分。

HEADER 和 DATA 有個 flag END_STREAM 值得提一下,當這個 flag 被設置後,說明這是最後一個 HEADER/DATA,數據都已經傳完了。

另外還有個 frame 先提下,RST_STREAM 用來關閉 stream,常常用在當 error 發生的時候。

+---------------------------------------------------------------+| Error Code (32) |+---------------------------------------------------------------+

有了這幾種 Stream,我們可以很容易的畫出最基本的 stream 狀態機。

+--------+ | | | idle | | | +--------+ | | send H | recv H | v +--------+ | | | open | | | +--------+ | | | send R | recv R | | | v +--------+ | | | closed | | | +--------+

首先每個 stream 開始都處於 idle 狀態。當 client 發起 request 時,client 首先發送 一個Header frame ,client 進入 open 狀態。當 server 收到這個 Header frame 後,server 也進入 open 狀態。client / server 進入 open 狀態後,就可以發送/接受數據了。server 發起 response 也是同理,只不過是把 client 和 server 的位置互換罷了。

當任何一方認為這個 stream 需要終止時,可以發送 RST_STREAM frame 給對方,發送後發送方的狀態變為 closed,接收方接收到後的狀態也變為 closed。在 closed 的狀態下,雙方不能交換 frame,這個 stream 的生命周期也就結束了。

上面的流程有一個很大的問題,當某一方認為自己的數據已經發完,可以關閉 stream 的時候,但他不知道對方的數據是否發完沒有,如果這時候貿然的關閉 stream 會導致接下來的數據收不到。在 HTTP/2 中,更常規的做法(上面的 RST_STREAM 只有發生錯誤的情況下才會使用,屬於非常規做法)是在發送數據最後的一個 frame 中,設置 END_STREAM flag。發送帶 END_STREAM flag frame 端的狀態變為 half closed(local), 表明數據已經發完了,不會再發數據,但可以接受對方的數據。收到帶 END_STREAM flag frame 端的狀態變為 half closed(remote),表明還可以發數據,對方會接受,但對方已經不會再發數據了。

+--------+ | | | idle | | | +--------+ | | send H | recv H | v +--------+ recv ES | | send ES ,-------| open |-------. / | | v +--------+ v +----------+ | +----------+ | half | | | half | | closed | | send R / | closed | | (remote) | | recv R | (local) | +----------+ | +----------+ | | | | send ES / | recv ES / | | send R / v send R / | | recv R +--------+ recv R | `----------->| |<----------- | closed | | | +--------+

Server Push

常見的web請求是先拿一個 html,然後 client 在去請求這個 html 中包括的 css,js。但是server 在返回 html 給 client 的時候是知道 html 中內容的,如果支持 server push,可以直接把需要的資源推給 client,減少了 client 解析再請求的時間。

HTTP/2 的 Server Push 是通過在上一個 stream 中插入 PUSH_PROMISE frame 實現的。

+---------------+ |Pad Length? (8)| +-+-------------+-----------------------------------------------+ |R| Promised Stream ID (31) | +-+-----------------------------+-------------------------------+ | Header Block Fragment (*) ... +---------------------------------------------------------------+ | Padding (*) ... +---------------------------------------------------------------+

上一個 stream 是指 client 請求導致 server push 的 stream。按上面的例子來說,就是請求你 html 的那個 stream。具體把這個 PUSH_PROMISE 插入在哪裡是個比較 trick 的地方,假設我們放在上一個 stream 的最後,client 先收到 html(此時 server push 的內容還沒收到),然後發起對 css,js 的請求,這樣 server push 就沒有意義了。因此,HTTP/2 規定:

The server SHOULD send PUSH_PROMISE frames prior to sending any frames that reference the promised responses. This avoids a race where clients issue requests prior to receiving any PUSH_PROMISE frames.

發送 PUSH_PROMISE 會把一個 idle stream (PUSH_PROMISE 中 Promised Stream ID 對應的 Stream)的狀態置為 reversed(local) (對server而言,對 client 而言是 reversed(remote))。開始 server push 後,會先發送 HEADER frame, 然後狀態置為 half closed(remote) (對 client 而言是 half closed(local))

+--------+ send PP | | recv PP ,--------| idle |--------. / | | v +--------+ v +----------+ | +----------+ | | | send H / | | ,------| reserved | | recv H | reserved |------. | | (local) | | | (remote) | | | +----------+ v +----------+ | | | +--------+ | | | | recv ES | | send ES | | | send H | ,-------| open |-------. | recv H | | | / | | | | | v v +--------+ v v | | +----------+ | +----------+ | | | half | | | half | | | | closed | | send R / | closed | | | | (remote) | | recv R | (local) | | | +----------+ | +----------+ | | | | | | | | send ES / | recv ES / | | | | send R / v send R / | | | | recv R +--------+ recv R | | | send R / `----------->| |<----------- send R / | | recv R | closed | recv R | `----------------------->| |<---------------------- +--------+

這裡有個商榷的地方,為何需要把帶推送的 stream 的狀態設置為 reversed(local) 而不是 half closed(remote),畢竟從語義上來講,Server Push 時客戶端不會發送 frame,整個 stream 就應該是 half closed 狀態。在RFC 5.1.2 節中,HTTP 規定了一個 HTTP/2 連接中的並發 stream 數目,而只有能夠發送 DATA frame 的 stream 才算做並發 stream 裡面,而在 reversed 下,由於還沒有發送 HEADER,這個 STREAM 不能發送 DATA,自然也就不能算作並發 stream 裡面,因此必須在 idlehalf closed 之間顯式的另外引入一個狀態來表明某個 stream 是之後要進行 server push,但還沒有開始 (即沒有發送 HEADER),而這個狀態就是 reversed。雖然 RFC 裡面是這麼規定的,但實現上貌似並不是特別遵守,Golang 的 HTTP/2 實現就是沒有 reversed 狀態的。

Flow Control

因為引入了 Stream Multiplex,各個 stream 之間有了資源競爭,為了更好的分配資源,需要在應用層實現 Flow Control (TCP 的 flow control 是針對單個 connection,沒法對 stream 這個更上層的抽象做)

HTTP/2 只提供了一種機制來實現流量控制,而沒有具體指定使用哪種演算法(由實現者自己決定)。而這種機制是通過發送 WINDOW_UPDATE 這種 frame 來實現的。

+-+-------------------------------------------------------------+|R| Window Size Increment (31) |+-+-------------------------------------------------------------+

具體實現是 connection 和 每個 stream 都會有一個 flow-control window, 發送的 DATA payload 大小不能超過這個 window。每發送一個 DATA,connection window 和對應的 stream window 都會減去 DATA payload 長度大小,當收到 connection / stream WINDOW_UPDATE ,connection / stream window 會增大對應 Window Size Increment 大小。

Reference

  • httpwg.org/specs/rfc754
  • hpbn.co/http2/
  • imququ.com/post/http2-n
  • laike9m.com/blog/rfc754

推薦閱讀:

在公司用免費的Wi-Fi被人監控了如何處理?
華為路由器無線橋接水星路由器
「茴」字的五種寫法
未來十年,人類還需要400億個感測器

TAG:計算機網路 |