探索HTTP/2: HTTP/2協(xié)議簡述
HTTP/2的協(xié)議包含著兩個RFC:Hypertext Transfer Protocol Version 2 (RFC7540),即HTTP/2;HPACK: Header Compression for HTTP/2 (RFC7541),即HPACK。RFC7540描述了HTTP/2的語義,RFC7541則描述了用于HTTP/2的頭部壓縮的格式。本文只涉及HTTP/2協(xié)議,本系列的后續(xù)文章將會涉及HPACK協(xié)議。(2016.10.13最后更新)
1. HTTP/2要解決的問題
HTTP/1.0只允許在一個TCP連接中出現(xiàn)一個請求。后來的HTTP/1.1雖然引入了請求流水線,以允許在一個連接中發(fā)送多個請求,但這只是部分地解決了請求并發(fā)的問題。服務器端在返回響應時,還是必須要按照它接收到的請求的順序進行返回。如果排在前面的響應要消耗較長的時間,那依然會對后面的響應的造成阻塞,亦即線頭阻塞(Head-of-line blocking)。所以,客戶端必須要使用多條連接去發(fā)起多個的請求以實現(xiàn)并發(fā),并進而減小延遲。更大的并發(fā)會增大服務器的負載,也會占用更大的網(wǎng)絡帶寬。另外,頭部通常會包含有大量的信息,如cookie,而這也會增加網(wǎng)絡傳輸?shù)拈_銷。
HTTP/2允許在同一個TCP連接中交錯地出現(xiàn)多個請求與響應,亦即多工(Multiplex)。同時,它使用了一個高效的編碼方法對頭部進行壓縮。HTTP/2還允許對請求進行優(yōu)先級排序,以便讓更為重要的請求得以更快的完成,這會進一步提高性能。HTTP/2還改變了服務器端只能被動地向客戶端返回響應的定式,允許服務器端主動地向客戶端推送數(shù)據(jù),這就可以減少客戶端發(fā)起請求的數(shù)量。
總之,HTTP/2主要是解決性能問題。
2. 發(fā)起HTTP/2
HTTP/2會使用與HTTP/1相同的URI scheme,即http和https。而且實現(xiàn)HTTP/2的服務器端也不會使用不同的端口去分別支持HTTP/1和HTTP/2。這樣有利于平滑地從HTTP/1升級到HTTP/2。畢竟目前已部署的絕大部分網(wǎng)絡服務都只支持HTTP/1,當未來它們升級到HTTP/2時,如果換用了不同URI scheme或端口,那么肯定會對客戶端產生極大的影響。但是HTTP/2協(xié)議為運行在http和https上的HTTP/2分別定義了兩個不同的標識符:h2c和h2。h2c中的"c"指的是cleartext,即明文。本文后面會使用h2c指代運行在http2上(直接使用TCP)的HTTP/2,而用h2指代運行在https上(使用TLS)的HTTP/2。
那么,支持HTTP/2的客戶端如何知道它所連接的服務器端是否也支持HTTP/2呢?
對于h2c,支持HTTP/2的客戶端可以在發(fā)起的請求中使用HTTP/1.1的Upgrade頭部去嘗試要求服務器升級到HTTP/2。該請求的格式如下:
GET / HTTP/1.1
Host: server.example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
HTTP2-Settings是一個經(jīng)由BASE64編碼過的字符串,其原始內容是客戶端將要發(fā)送的SETTINGS幀的載荷,即一些配置參數(shù)。
如果服務器端支持HTTP/2,它就響應"101 Switching Protocols",表示可以進行升級。該響應的格式如下:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c
如果服務器端不支持HTTP/2,則會忽略Upgrade請求頭部,后續(xù)依然使用HTTP/1.1。
對于h2,會使用到協(xié)議Transport Layer Security (TLS) Application-Layer Protocol Negotiation Extension (RFC7301),即TLS-ALPN。該協(xié)議允許客戶端和服務器端就使用何種版本的HTTP進行協(xié)商。如果TLS-ALPN在現(xiàn)實中運行良好的話,也許某天還會使用該方法去協(xié)商使用別的協(xié)議。
當客戶端與服務器端都同意使用HTTP/2時,雙方都需要各自發(fā)出一個連接序言(Connection Preface)以進行最后的確認。
客戶端在接收到服務器端的"101 Switching Protocols"響應(針對h2c)或TLS連接的第一個應用數(shù)據(jù)字節(jié)(針對h2)之后會立即發(fā)出連接序言。該序言的開頭是"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"(其十六進制形式為"0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a")(1),后面必須再跟一個SETTINGS幀,哪怕這個幀是空的。
服務器端的連接序言則由一個SETTINGS幀構成,該幀必須是服務器端在HTTP/2連接中發(fā)送的第一個幀。這個SETTINGS幀可以為空,也可以包含一些希望客戶端如何與自己進行通信的必要配置信息。
3. 幀(Frame)
HTTP/2消息使用二進制格式(實際編碼時使用十六進制書寫),相比于文本格式,這樣可以提高消息處理的效率。HTTP/2消息的最小單元為幀,它由頭部與載荷(Payload)組成。每個幀的長度必須是一個或多個8比特位字節(jié)(octet,下文將其簡寫為"字節(jié)")。
幀頭部依次包含有如下的5個字段:
長度(Length):該字段占用24個比特位,代表幀載荷的長度。該長度是一個24位的無符號整數(shù)。
類型(Type):該字段占用8個比特位,代表幀的類型。
標志(Flags):該字段占用8個比特位,代表幀所定義的一個或多個標志。并不是所有的幀都定義了標志。
保留位(R):該字段占用1個比特位,其語義尚未被定義。在讀取幀時,該位需要被忽略;但在發(fā)送幀時,該位需要保持為0(0x0)。
流標識符(Stream Identifier):該字段占用31個比特位,代表該幀所在流的標識符。
在頭部之后,緊接著的就是載荷。載荷的結構與內容完全由幀的類型決定,它的長度也是不定的。
HTTP/2定義了如下10種不同類型的幀。
DATA:用于攜帶一組長度不定的字節(jié)。一個或多個DATA可作為請求或響應的載荷。
HEADERS:用于開啟一個流,并可攜帶一個頭部塊片斷。頭部塊指由一個HEADERS/PUSH_PROMISE幀和緊隨它的零到多個CONTINUATION幀組成的集合,因為只有它們才可能攜帶頭部信息。這個集合可被分割成一個或一組字節(jié),這樣的字節(jié)被稱為頭部塊片斷。頭部塊中各個特定類型的幀必須緊緊相鄰,不能出現(xiàn)其它類型的幀。
PRIORITY:用于指定發(fā)送端建議的流優(yōu)先級。
RST_STREAM:用于立即終止流。當希望取消一個流或發(fā)生錯誤時,就可發(fā)送RST_STREAM幀。
SETTINGS:用于攜帶可以影響兩端之間通信方式的配置參數(shù)。SETTINGS幀定義了一個ACK標志,用于指示該幀所設置的參數(shù)是否已被接收端獲知。當收到一個SETTINGS且其中的ACK標志為0時,接收端必須盡可能快的應用其中已被更新的參數(shù)。
PUSH_PROMISE:用于向接收端通知發(fā)送端將要創(chuàng)建的流。當接收端接收到該幀時,新的流尚未被發(fā)送端創(chuàng)建,但發(fā)送端承諾會創(chuàng)建該流。該幀用于實現(xiàn)HTTP/2的重要特性"服務器端推送(Server Push)"。
PING:用于測量發(fā)送端與接收端之間的最小往返時間。這與使用眾所周知的ping命令的目的相似,是為了測試某個空閑的連接是否還可用。
GOAWAY:用于發(fā)起對連接的關閉,或觸發(fā)嚴重的錯誤條件。該幀允許一端,在完成對之前已創(chuàng)建的流的處理的同時,優(yōu)雅地停止接收新的流。一端在創(chuàng)建新的流,另一端在發(fā)送GOAWAY,這兩者之間天然存在著競爭關系。為了就對這種情況,發(fā)送端在發(fā)送GOAWAY時會讓它攜帶上(該發(fā)送端所知曉的)接收端最后創(chuàng)建的流的標識符,當該GOAWAY被發(fā)送之后,發(fā)送端將會忽視由接收端創(chuàng)建的任何一個標識符比該標識符大的流。
WINDOW_UPDATE:用于流量控制。該幀的載荷由一個單比特保留位和一個31比特位的無符號整數(shù)組成。該整數(shù)向該幀的接收端指示了其向當前流量控制窗口所能增加傳輸量的值。
CONTINUATION:用于繼續(xù)發(fā)送頭部塊片斷。只要同一個流中前面的幀是HEADERS,PUSH_PROMISE或CONTINUATION,并且該幀沒有設置END_HEADERS標志,那么可無限量地發(fā)送CONTINUATION幀。
部分幀,DATA,HEADERS和PUSH_PROMISE,的載荷中可能包含填白(Padding)。填白在業(yè)務上沒有實際的用處,它的出現(xiàn)是基于安全目的。比如,可以用它來擾亂實際數(shù)據(jù)的長度,以減輕特定的HTTP攻擊。
發(fā)送端發(fā)送的幀的最大長度要尊重接收端設定的SETTINGS_MAX_FRAME_SIZE的值。但該值的范圍要介于2^14至2^24-1個字節(jié)之間。
4. 流(Stream)
流是用于在客戶端與服務器端之間進行幀傳送的通道,同一個TCP連接中可以同時有多個流,如下圖所示,
┌────────┐ Connection ┌────────┐
│ │ ============================= │ │
│ │ --------------------- <-- Stream │
│ │ ┌─────┐┌─────────┐┌─┐ │ │
│ │ └─────┘└─────────┘└─┘ <-- Frame │
│ │ --------------------- │ │
│ Client │ │ Server │
│ │ ---------- │ │
│ │ ┌──┐┌────┐ │ │
│ │ └──┘└────┘ │ │
│ │ ---------- │ │
│ │ ============================= │ │
└────────┘ └────────┘
服務器端和客戶端可以交錯地向同一個連接中的不同流中傳送幀??梢园岩粋€流看作HTTP/1中的一個連接??蛻舳伺c服務器端在同一個流中的交互依然遵循發(fā)送請求-等待響應模式。兩端都可以創(chuàng)建新的流,共享對方創(chuàng)建的流,也可以關閉對方創(chuàng)建的流。幀在流中的順序是有意義的,接收端會以接收到的順序去處理幀。
每個流都有一個標識符,是一個31比特位的無符合整數(shù)。在同一個連接中,流標識符是唯一的。由客戶端創(chuàng)建的流的標識符為奇數(shù),由服務器創(chuàng)建的流的標識符為偶數(shù)。但標識符為0的流可看作連接,用于連接控制信息,創(chuàng)建新的流時不可使用該標識符。同一個連接中的任何一個流的標識符都不可重用,即便這個流已被關閉了。對于長時間沒有中斷的連接,可能會出現(xiàn)標識符不夠用的情況,那時就必須強制創(chuàng)建一個新的連接。
HTTP/2協(xié)議為流的生命周期定義了7種狀態(tài)(2):idle,reserved(local),reserved(remote),open,half closed(local),half closed(remote)和closed。當一端接收或發(fā)送頭部塊或(幀DATA和HEADERS的)標志RST_STREAM后可使流的狀態(tài)發(fā)生轉變。
使用流來實現(xiàn)多工就會引起對TCP連接使用的競爭,這會造成流的阻塞?;趲琖INDOW_UPDATE的流量控制方案可以確保相同連接中的流相互之間不會產生破壞性干擾。流量控制可以作用于兩個層面,即單個流或整個連接。只有幀DATA需要遵守流量控制,所有其它的幀所有消耗的空間均不會占用流量控制窗口。HTTP/2協(xié)議只是定義了WINDOW_UPDATE幀的結構和語義,協(xié)議的實現(xiàn)可以選擇任何適用自己的流量控制算法。
流可以有優(yōu)先級??蛻舳嗽趧?chuàng)建一個新的流時,可在HEADERS中指定優(yōu)先級權重。在后續(xù)任何時間,通過PRIORITY可以改變流的優(yōu)先級權重。在并發(fā)能力有限的情況下,高權重流的幀會被優(yōu)先傳送。權重的值必須介于1至256之間,默認權重為16。流與流之間還可以有依賴關系,這種關系會組成一棵依賴關系樹。一個流能夠指定自己成為另一個流的子流。這一過程,可以是非排他的,也可以是排他的。非排他性依賴,是指一個流在將自己變成另一個流的子流的過程中,允許另一個流還有別的子流,即允許有自己的兄弟流存在。排他性依賴,指在前述過程中,不允許另一個流還有別的子流。如果另一個流已經(jīng)有子流了,那么該流會把所有潛在的兄弟流先變成自己的子流,然后再使自己成為另一個流的唯一子流。其實,排他性依賴的作用就是為了能夠打破已有的關系樹,在既成的父子節(jié)點中插入新的節(jié)點。否則,只能為已有節(jié)點添加子節(jié)點,那么關系樹將不可能進行重構。所有的流在被創(chuàng)建時,默認成為標識符為0x0的流的子流。在"服務器端推送"中生成的"推送"流將自動地成為生成該推送流的流的子流,其默認權重也為16。
5. 消息交換
5.1 請求/響應交換
HTTP/2沿襲了HTTP/1的語義,即所有的請求與響應語義均得到了保留,盡管傳遞這些語義的語法已經(jīng)改變了。
一個HTTP/2消息由如下幾個部分組成:
[1]僅對于響應消息,可以包含一個攜帶有1xx響應頭部的頭部塊。該頭部塊由一個HEADERS幀和緊隨它的零到多個CONTINUATION幀組成。
[2]一個頭部塊。該頭部塊由一個HEADERS幀和緊隨它的零到多個CONTINUATION幀組成。
[3]零到多個攜帶有體部(Body)消息的DATA幀。HTTP/1中使用的"分塊(chunked)"體部將不適用于HTTP/2。因為一個體部可由多個DATA幀組成,所以HTTP/2的體部天然就是可分塊的。
[4]一個可能存在的包含著尾部消息的頭部塊。該頭部塊由一個HEADERS幀和緊隨它的零到多個CONTINUATION幀組成。
HTTP/2仍然沿用HTTP/1中的頭部字段,但字段名稱中的字母必須全部為小寫。另外,還將HTTP/1消息開始行(請求中的請求行與響應中的狀態(tài)行)中的消息,分解成了若干偽頭部字段,此類字段均以冒號(:)開頭。
HTTP/1請求行格式為"method request-target HTTP-version",對應的HTTP/2偽頭部字段有:method=method和:path=request-target,但HTTP-version無對應字段,默認為HTTP/2。
HTTP/1狀態(tài)行格式為"HTTP-version status-code reason-phrase",對應的HTTP/2偽頭部字段有:status=status-code。但HTTP-version無對應字段,默認為HTTP/2;reason-phrase也無對應字段,因為可以通過狀態(tài)代碼查找到其對應的reason-phrase。HTTP/2協(xié)議是在盡量減少冗余消息。
HTTP/2協(xié)議還為請求頭部定義了另外兩個偽字段:
:scheme:URI中的scheme部分。它可以不僅僅是http或https,因為有時候可能會與非HTTP服務進行交互。
:authority:URI中的授權部分。即,scheme://user:password@host:port/path?query#fragment中的"user:password@host:port"。
HTTP/2協(xié)議8.1.3節(jié)中給出一些簡單示例,展示了如何將HTTP/1消息對應到HTTP/2消息。
5.2 服務器端推送
HTTP/2的服務器端推送是傳統(tǒng)的請求/響應模式的一種特殊形式。服務器端在收到客戶端的請求(主請求)之后,為了主動向客戶端推送更多的內容,會自動地生成若干新的請求(推送請求)。服務器向客戶端發(fā)送的響應中,不僅包含對主請求的響應(主響應),還包含對推送請求的響應(推送響應)。
客戶端可以通過發(fā)送包含有SETTINGS_MAX_CONCURRENT_STREAMS參數(shù)的SETTINGS幀去禁用服務器端推送,也可以通過發(fā)送RST_STREAM幀去取消已經(jīng)發(fā)起的服務器端推送,但不能發(fā)送包含有END_STREAM標志的幀。
(1)"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"中的"PRI"與"SM"合起來就是"RRISM(棱鏡)"。呵呵,HTTPbis工作組這是想表達什么意思呢 ;-)
(2)本系列的后續(xù)文章解讀了流的狀態(tài)。