HTTP/2 簡介
最近閱讀了一下RFC7540和一部分HTTP/2的Go語言支援實現,故作此記錄。
HTTP/1 的問題在哪
回想一下作為一個瀏覽器,請求HTTP/1網站的過程。瀏覽器經過一系列操作之後,和伺服器建立了通訊,並且傳送請求,例如:
GET / HTTP/1.1 Host: jiajunhuang.com User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Encoding: gzip, deflate
此時伺服器收到請求,並且返回/
對應的內容,此處,是一個網頁。網頁中包含了一堆的圖片,css和js的連結,瀏覽器解析出來之後,
為了獲取這些內容,瀏覽器必須新建一些和伺服器的連線,請求圖片,css和js等內容。例如請求common.min.css
,會發起一個這樣的
報文:
GET /static/css/common.min.css HTTP/1.1 Host: jiajunhuang.com User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Encoding: gzip, deflate
可以發現,如果有n個這樣的請求,請求中header裡大部分的內容都是相同的例如Host
,User-Agent
等。多次請求中包含同樣的內容,
是一種資源浪費,而且,如果有多個資源需要請求,則需要建立多個TCP連線,請求完之後這個連線就廢掉了,沒法重複利用,對於TCP連線
的利用率實在是很低。
HTTP/1.1 中有 request pipelining,但是有缺陷,例如只能對GET請求進行pipelinning,詳見:ofollow,noindex" target="_blank">https://en.wikipedia.org/wiki/HTTP_pipelining
而HTTP/2就是為了解決上述問題而存在的。
HTTP/2
首先得說明,HTTP/2不再像HTTP/1那樣是明文協議,HTTP/2是二進位制協議,也就意味著,我們沒法再簡單的通過telnet jiajunhuang.com 80
這樣去請求一些內容。為什麼HTTP/2要做成二進位制協議呢?主要原因在https://http2.github.io/faq/#why-is-http2-binary:
- 使用TLS之後,也沒法直接telnet這樣進行除錯
- 相比明文協議(可以手工輸入),二進位制協議必須使用庫或者工具(當然也可以自己寫),相對來說更不容易出錯
- 二進位制協議解析起來更加高效(再也不用逐個字元去判斷哪裡是頭部結束了,直接偏移多少就可以知道對應的位元組表示什麼意思)
frame(幀)
frame 是HTTP/2中最小的傳輸單位。HTTP/2 相對HTTP/1來說,把原來的頭部和body分開來了,統一都丟到frame裡,頭部對應的frame的型別 是HEADERS,而body對應的frame的型別是DATA。除此之外,frame的型別還有例如:GOAWAY, WINDOW_UPDATE等等。可以這麼理解,HTTP/2 連線開始之後,所有的資料都是一個frame的內容,直到開始下一個frame。因此,Go語言RPC/">gRPC實現中有這麼一段程式碼:
func readFrameHeader(buf []byte, r io.Reader) (FrameHeader, error) { _, err := io.ReadFull(r, buf[:frameHeaderLen]) if err != nil { return FrameHeader{}, err } return FrameHeader{ Length:(uint32(buf[0])<<16 | uint32(buf[1])<<8 | uint32(buf[2])), Type:FrameType(buf[3]), Flags:Flags(buf[4]), StreamID: binary.BigEndian.Uint32(buf[5:]) & (1<<31 - 1), valid:true, }, nil }
第一行,frameHeaderLen 的定義是const frameHeaderLen = 9
,為什麼呢?我們來看看RFC中對frame的格式的定義:
+-----------------------------------------------+ |Length (24)| +---------------+---------------+---------------+ |Type (8)|Flags (8)| +-+-------------+---------------+-------------------------------+ |R|Stream Identifier (31)| +=+=============================================================+ |Frame Payload (0...)... +---------------------------------------------------------------+
數一下 frame payload 之前一共是多少個bit,24 + 8 + 8 + 1 + 31 = 72
,72 bit,也就是9byte了。既然提到了frame的格式,
那我們還是要看看frame中各個欄位的用處:
-
Length: The length of the frame payload expressed as an unsigned 24-bit integer. Values greater than 2^14 (16,384) MUST NOT be sent unless the receiver has set a larger value for SETTINGS_MAX_FRAME_SIZE. 這24bit是一個24bit的無符號整數, 因此最大可以表示2^14=16384,所以通常來說,frame的最大長度是16384 byte, 也就是16k。當接收者設定了 SETTINGS_MAX_FRAME_SIZE 之後,才可以傳輸超過這個大小的frame。
-
Type: The 8-bit type of the frame. The frame type determines the format and semantics of the frame. 這8bit用來明確 所在frame的型別。
-
Flags: An 8-bit field reserved for boolean flags specific to the frame type. 這8bit用來標誌當前型別的frame的一些其他 特徵,例如DATA這種型別的frame有定義 END_STREAM(0x1) 表示當前frame是這個stream中最後的一個frame。
-
R: A reserved 1-bit field. The semantics of this bit are undefined, and the bit MUST remain unset (0x0) when sending and MUST be ignored when receiving. 保留位,沒用。
-
Stream Identifier: A stream identifier (see Section 5.1.1) expressed as an unsigned 31-bit integer. The value 0x0 is reserved for frames that are associated with the connection as a whole as opposed to an individual stream. 當前frame 所在的stream的id。stream的概念在下一小節會介紹。
stream(流)
上一節我們看到了每個frame的定義中都有31bit用來標識當前frame所在stream的id。那麼stream是個什麼鬼?stream是一個抽象概念, 因為HTTP/2把原本HTTP/1中一個請求中的頭部和body打散了,分成了HEADERS和DATA兩個frame。那當伺服器收到一堆的frame之後,他如何 知道哪個frame和哪個frame是一起的,組合起來是一個完整的請求呢?所以我們需要stream這個抽象概念,而且由於一個HTTP/2連線可以 同時傳輸多個stream,所以我們可以通過下面的圖片來理解stream:
- 這是stream 1的內容:
- 這是stream 2的內容:
- 這是實際傳輸流程中的樣子:
也就是這樣:
從HTTP/1升級
接下來我們看看,由於現實世界中大部分網站還是HTTP/1的,HTTP/2在實現細節上與HTTP/1相差太大,是如何做到相容的。
首先可以肯定的是,http://
和https://
這兩個scheme不能改,要不然估計HTTP/2別想推廣開來,不信可以看看ipv6今天的覆蓋
程度,ipv6可是推出二十好幾年了呢。
那我們怎麼判斷伺服器是不是支援HTTP/2呢?HTTP/1中有一個狀態碼,叫做101 Switching Protocols 。從HTTP/1升級到HTTP/2連線,就靠它了。
https://tools.ietf.org/html/rfc7540#section-3.2
首先發起HTTP/1請求,並且給出如下報文:
GET / HTTP/1.1 Host: jiajunhuang.com Connection: Upgrade, HTTP2-Settings Upgrade: h2c HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
如果伺服器不支援HTTP/2,就該怎麼返回怎麼返回,例如返回:
HTTP/1.1 200 OK Content-Length: 243 Content-Type: text/html ...
但是如果伺服器支援HTTP/2,那就返回101狀態碼,然後隨即開始的內容就是HTTP/2的二進位制內容了:
HTTP/1.1 101 Switching Protocols Connection: Upgrade Upgrade: h2c [ HTTP/2 connection ...
沒有講到的東西
- HPACK HTTP/2 頭部壓縮
- Stream prioritization
- Server Push
- Flow Control
說穿了,HTTP/2就是把HTTP/1建立在多個TCP之上的這一整套流程,建立在一個TCP之上,所以有這麼一坨的概念,例如multiplexing, 優先順序等等。
頭部壓縮我準備在讀完 RFC 7541 之後再單獨介紹。