HTTP2總結及簡單實踐總結
HTTP發展歷史
在總結http2之前先來回顧下http的發展歷史。以下三張圖片來自 Jerry Qu
HTTP/0.9 (1991)
HTTP/1.0 (1996)
HTTP/1.1 (1999)
HTTP通信過程
眾所周知,http是基于tcp之上的應用層協議,即在tcp連接建立之后,在tcp的鏈路上傳送數據。
- 首先進行TCP連接,三次握手, C --(SYN{k})--> S , S --(ACK{k+1}&SYN{j})--> C , C --ACK{j+1}--> S
- 客戶端發送ACK后,就會發送一個HTTP請求
- 服務端接受到ACK,確認TCP連接建立,再接著收到HTTP請求,進行解析并將結果返回客戶端。
- 客戶端收到HTTP請求結果。
在 HTTP/0.9 和 HTTP/1.0 中,第3步之后,服務端就會關閉連接,也就是TCP的四次揮手,但是在 HTTP/1.1 后,客戶端在發送HTTP請求時頭部可以帶上 Connection:Keep-Alive ,就是告訴服務器保持連接,不要關閉TCP。當 Connection:Close 時,服務器會關閉連接。
HTTP2 的通信過程無外乎這是這個流程,但是通過TCP傳輸的數據會有不同,客戶端和服務器的行為也有了新的規則。引入了Connection、Stream、Message、Frame這四個概念,從下圖大概可以看出他們之間的關系。
- Connection: 其實就是一個TCP連接
- Stream:已建立的連接上的雙向字節流
- Message:請求或者響應,由一個或多個幀組合而成
- Frame: Message中的二進制幀,HTTP/2通信的最小單位,后面會詳細解釋
HTTP/2 新特性
- 二進制分幀(Binary framing layer)
- 多路復用 (Multiplexing)
- 單一連接(One connection per origin)
- 數據流優先級(Stream prioritization)
- 首部壓縮(Header Compression)
- 流控 (Flow control)
- 服務端推送(Server Push)
這些新特性的產生,主要是為了解決之前的問題,我們來對比下之前的 HTTP/1.1 ,看看解決了哪些問題
二進制分幀(Binary framing layer)
二進制分幀就是把http的數據按照規定的格式進行封裝,類似IP和TCP的數據包, 簡單畫了個承載HTTP2數據的以太幀結構,方便理解。
通過wireshark抓包可以看到http2的結構
- Length: 無符號的自然數,24個比特表示,僅表示幀負載所占用字節數,不包括幀頭所占用的9個字節。默認大小區間為為0~16,384(2^14),一旦超過默認最大值2^14(16384),發送方將不再允許發送,除非接收到接收方定義的SETTINGS_MAX_FRAME_SIZE(一般此值區間為2^14 ~ 2^24)值的通知。
- Type: 8個比特表示,定義了幀負載的具體格式和幀的語義,HTTP/2規范定義了10個幀類型,這里不包括實驗類型幀和擴展類型幀
- Flags: 8個比特表示,服務于具體幀類型,默認值為0x0。有一個小技巧需要注意,一般來講,8個比特可以容納8個不同的標志,比如,PADDED值為0x8,二進制表示為00001000;END_HEADERS值為0x4,二進制表示為00000100;END_STREAM值為0X1,二進制為00000001。可以同時在一個字節中傳達三種標志位,二進制表示為00001101,即0x13。因此,后面的幀結構中,標志位一般會使用8個比特表示,若某位不確定,使用問號?替代,表示此處可能會被設置標志位
- R: 在HTTP/2語境下為保留的比特位,固定值為0X0
- Stream Identifier: 無符號的31比特表示無符號自然數。0x0值表示為幀僅作用于連接,不隸屬于單獨的流。
HTTP2幀中的類型如下:
想了解每一個類型的詳細數據結構可以參考我的另一篇文章http2幀類型詳解
通過Google Developers中的一個圖,我們可以更好的理解,HTTP2的分幀在網絡數據中所處的位置,以及和HTTP/1.1的不同之處。
HTTP/1.1中的頭部變成HEADERS類型的幀,請求體/回應體變成DATA類型的幀,通過二進制分幀,將傳輸的數據使用二進制方式,對比文本方式減少數據量;通過不同類型的幀實現流控、服務器推送等功能。
多路復用 (Multiplexing) & 單一連接(One connection per origin)
我們知道在HTTP2之前,我們如果想加快網頁資源的加載速度,會采用同時建立多條連接的方式,但是這樣每次建立TCP連接效率比較低,并且瀏覽器往往會限制最大連接數(例如chrome的最大連接數為6)。另外在HTTP/1.1中引入了Pipeline,可以在一個TCP連接中連續發送多個請求,不用關心前面的響應是否到達,但是服務器必須要按照收到請求的順序來進行響應,這樣一旦前面的請求阻塞,后來的請求也將不能及時回應。
HTTP2中,因為新的二進制幀的使用,使得可以輕松復用單個TCP連接??蛻舳撕头掌骺梢詫?HTTP 消息分解為互不依賴的幀,然后交錯發送,最后再在另一端把它們重新組裝起來。
還是 Google Developers的圖:
可以看到我們可以并行交錯的發送多個響應和請求,并且使用同一個TCP連接,沒有先后順序,每個幀中攜帶有如何組裝的信息,客戶端會等某項工作所需要的所有的資源都就緒之后再執行。
數據流優先級(Stream prioritization)
由于可以進行單連接復用,服務器和客戶端的幀都是交錯發送,對于發送給服務器的幀,為了解決哪些該先處理,哪些該后處理,因此引入了數據流的優先級,服務器根據優先級來分配資源。例如優先級高的獲得更多的CPU和帶寬資源。那么優先級是如何標示的呢?還記得前面的幀類型中有一個Type為PRIORITY,這種類型的幀就是為了告訴服務器這個stream的優先級,此外HEADERS幀中也包含了優先級信息。
HTTP/2通過父依賴和權重來標示優先級,每一個stream會標示一個父stream id,沒有標示的默認為虛擬的root stream,這樣按照這種依賴關系構建一個依賴樹,樹上層的stream權重較高,同一層的stream會有一個weight來區分資源分配比。。
上圖是依賴樹的一些示例,從左到右,共四棵樹。
- 第一個兩個stream A 和 B,沒有標明父stream,默認依賴虛擬的root節點,A、B處于同一層,優先級相同,根據權重分配資源,A分到 12/(12+4)=3/4 資源,B分到 1/4 資源。
- 第二個D和C有層級結構,C的父級是D,那么服務器拿完整資源優先處理D,然后再處理C。
- 第三個,服務器先處理D,再處理C,然后處理A和B,A分到 3/4 資源,B分到 1/4 資源。
- 第四個,先處理D,再講資源對半分處理E和C,之后再按照權重處理A和B
需要注意的一點是,流優先級并不是強制約束,當優先級高的流阻塞時,并不能不讓服務器處理優先級低的流
首部壓縮 (Header Compression)
由于當前網站內容越來越復雜,單個頁面的請求數基本都是幾十個甚至上百,每個請求都要帶上客戶端或者用戶的標識,例如:UA,cookie等頭部數據,請求數量多了以后,傳輸http頭部消耗的流量也非??捎^,并且頭部數據中大部分都是相同的,這就是赤裸裸的浪費呀。于是產生了頭部壓縮技術來節省流量。
- 維護一份相同的靜態字典(Static Table),包含常見的頭部名稱,以及特別常見的頭部名稱與值的組合
- 維護一份相同的動態字典(Dynamic Table),可以動態地添加內容
- 支持基于靜態哈夫曼碼表的哈夫曼編碼(Huffman Coding)
靜態字典
靜態字典就是把常用的頭部映射為字節較短的索引序號,如下圖所示,截取了前面幾個映射,全部定義可以看 Static Table Definition
例如當頭部有個字段是 :method: GET ,那么查表可知,可以用序號2標識,于是這個字段的數據就是 0000010 (2的二進制表示)
動態字典
靜態字典能表示的頭部數據畢竟有限,壓縮率也不會高。但是對于一個站點來講,和某個用戶交互時會發生非常多的請求,但是每次請求頭部差別不大,會有很多重復數據,因為用戶和瀏覽器的標識是不變的。那么我們可以針對一次HTTP2的連接生成一個可添加映射的動態字典,這樣再后面的連接中就可以使用動態字典中的序號。動態字典的生成過程其實就是通知對方添加映射,客戶端可以通知服務端添加,反之亦可。
具體的通知方式就是按照協議規定的格式傳輸數據。
Huffman Coding
哈弗曼編碼的特性是出現頻率越高,編碼長度越短。HTTP2協議中根據大量的請求頭部數據樣本生成了一種canonical Huffman code,具體在 Huffman Code 列出。
流控 (Flow control)
HTTP/2 流量控制的目標,在流量窗口初始值的約束下,給予接收端以全權,控制當下想要接受的流量大小。
算法:
- 兩端(收發)保有一個流量控制窗口(window)初始值。
- 發送端每發送一個DATA幀,就把window遞減,遞減量為這個幀的大小,要是window小于幀大小,那么這個幀就必須被拆分。如果window等于0,就不能發送任何幀
- 接收端可以發送 WINDOW_UPDATE幀給發送端,發送端以幀內指定的Window Size Increment作為增量,加到window上
服務端推送 (Server Push)
流程:
- 客戶端在交換 SETTINGS 幀時,設置字段 SETTINGS_ENABLE_PUSH(0x2) 為1顯式允許服務器推送
- 服務器在接受到請求時,分析出要推送的資源,先發個 PUSH_PROMISE 幀給瀏覽器
- 然后再發送各個response header和response body
- 瀏覽器收到 PUSH_PROMISE 幀時,根據header block fragment字段里的url,可以知道當前有沒有緩存,從而判斷是否要接收。如果不要,瀏覽器就要發送個 RST_STREAM 來終止服務器推送
問題:
- 流量浪費。若瀏覽器有緩存,不要這個推送,就會出現浪費流量的現象,因為整個過程都是異步的,在服務器接收到RST_STREAM時,響應很有可能部份發出或者全部發出了。
HTTP/2簡單實踐
Okhttp是一個java生態中有名的的http client,由于其簡單易用,性能較好,支持http2。下面用這個工具來實踐下,因為本人博客已經在nginx上配置了http2,就拿本博客來實驗下。
- public class Http2Example {
- final static OkHttpClient client = new OkHttpClient.Builder().build();
- public static void main(String[] args) {
- Request request = new Request.Builder()
- .url("https://blog.fliaping.com")
- .build();
- try {
- Response response = client.newCall(request).execute();
- System.out.println(JSON.toJSONString(response.protocol()));
- System.out.println(response.headers().toString());
- System.out.println(response.body().string());
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
用過Okhttp的同學就會發現,這跟平時用的方法一樣啊,沒有任何區別,是的沒錯,就是沒有任何區別。別的不多說,執行下看看,不幸的是你會發現protocol還是http1.1,并不是h2,這是怎么回事?這是因為HTTP2新加入了ALPN(Application Layer Protocol Negotiation),從字面意思理解就是應用層協議協商,即雙方商量下用哪個協議。不幸的是jdk8是在2014年發布的,當時HTTP2協議還沒出生,幸運的是通過第三方jar包就可以支持ALPN。另外jdk9已經支持了HTTP2,雖然還沒正式發布,但是我們可以試用下JDK 9 Early-Access Builds。
jdk7和jdk8通過添加jvm參數加入第三方alpn支持包,注意版本不能搞錯,jdk7使用 alpn-boot-7.*.jar ,jdk8使用 alpn-boot-8.*.jar ,這里有版本對應關系 alpn-versions
- # jdk8
- -Xbootclasspath/p:/home/payne/Downloads/alpn-boot-8.1.11.v20170118.jar
- # jdk7
- -Xbootclasspath/p:/home/payne/Downloads/alpn-boot-7.1.3.v20150130.jar
- # jdk9
- # 使用jdk9平臺時,注意okhttp版本大于3.3.0
- # https://mvnrepository.com/artifact/org.mortbay.jetty.alpn/alpn-boot