序言
目前HTTP/2.0(簡稱h2)已經在廣泛使用(截止2018年8月根據Alexa流行度排名的頭部1千萬網站中,h2占比約29%,https://w3techs.com/technologies/details/ce-http2/all/all)。寫此文章的目的是:h2作為較新的技術,並逐漸占有率廣泛,雖然目前有更新的QUIC,但其實現思路類似於h2。顛覆以往的HTTP/1.x,H2的創造性的技術值得我們細細品味。此篇文章根據筆者在h2開發經驗和思考,向你介紹全面的h2知識以及是非功過。本篇更注重於幫助讀者理解h2的設計思路、亦可作為一篇RFC導讀或者總結。
第一話、追蹤溯源
圖1、HTTP年鑒圖
早在1991年,伴隨WWW誕生之初,HTTP/0.9協議已經提出。HTTP0.9是簡單且應用受限的協議。支持去網絡主機獲取對應路徑的資源。但是沒有擴展屬性。其協議之簡單甚至只用下面一個訪問谷歌主機的例子概括了HTTP/0.9的全部。如下所示,協議只支持GET,沒有http頭;響應只能是超文本。
telnet google.com 80
Connected to x.x.x.x
GET /about
(Hyper text)
(Conection closed)
隨着人們對富媒體信息的渴望以及瀏覽器的普及,HTTP/1.0在1996年被提出來。HTTP/1.0的很多特性目前還被廣泛使用,但是仍然像HTTP/0.9一樣一次請求需要創建一次的tcp連接。隨即短短幾年時間內,HTTP/1.1以RFC標准形式再次展現在人們眼前。此時的HTTP協議1.1版本已經重新設計了長連接、options請求方法、cache頭、upgrade頭、range頭、transfer-encoding頭, 以及pieline(in order)等概念。
而我們另一個所熟知的HTTPS的SSL/TLS技術各個版本差不多在后來的十年間逐漸被提出。出於安全考慮,互聯網通信間的防火牆路由交換機等設備,這些設備一般僅會開發有限的端口(如80和443)。各種版本的通信協議只能復用這些端口。HTTP1.1的80端口設計了upgrade請求頭升級到更高級的協議,而443端口為了避免多消耗個網絡RTT,在tls握手過程中使用了NPN/ALPN技術直接在通信之前保持CS兩端的協議一致。NPN/ALPN是是TLS協議擴展,其中NPN是Google為實現spdy提出的。由服務端提供可支持的協議,供客戶端選擇。ALPN則是更接近於HTTP交互的方式,由客戶端先發出使用某種協議的請求,由服務端確認是否支持協議。ALPN為了HTTP2誕生做鋪墊。
另外一個不得不提的是spdy協議。Spdy旨在解決HTTP1.1的線頭阻塞問題(后面章節有詳細討論)於2009被google提出。同時分別於2012提出了spdy3.0實現了流控制,2013-2014期間提出了流優先級,server push等概念。Spdy的存在意義更像是http2.0的體驗服。它為探索HTTP繼續演進道路做了鋪墊。
第二話、人機交互
匯編是效率最高的語言之一,但是又是最晦澀難懂的語言之一。而人腦易懂的編程語言往往犧牲性能作為折衷。簡單說,HTTP/1.x協議就是為了人類語言習慣所設計的協議,但是轉換成機器執行協議並不是高效的。讓我們在回顧下計算機執行解析HTTP1.x的流程。
GET / HTTP/1.1<crlf>
Host: xxx.aa.com<crlf>
<crlf>
對應的解析偽代碼是
loop
while(! CRLF)
read bytes
end while
if line 1:
parse line as Request-Line
else if empty line:
Break out and We have done
else If start with non-whitespace
parse header
else if space
continue with last heade
end if
end loop
在偽代碼解析流程可以看到,肉眼看起來簡潔的協議解析起來是這么的費勁。而且在HTTP服務器中還要考慮這種問題:字節行的長度是未知的,也不知道預先分配多大內存。
HTTP/2.0使用了計算機易懂的二進制編碼信息,而且得向上兼容HTTP的涵義。具體我們來看下他是如何做到的。像大多數通信協議一樣,楨是傳輸最小單位。楨分為數據幀和控制楨。數據幀作為數據的載體,控制楨控制信道的信令。h2楨的通用格式為首部9字節+額外的字符。正如你能想到的那樣,楨的第一個部分是描述長度,第二個部分描述了楨的類型,第三個部分描述了標志Flag,第四個部分是唯一序列號。這是所有楨的通用頭。通用頭緊接的是楨的實體。圖4展示了楨的結構。
圖2、通用楨的格式
這樣設計有什么好處呢。再來看一下楨的解析流程,你就會發現對計算機來說更簡潔。
loop
read 9 byte
payload_Length=first 3 bytes
read payload
swith type:
Take action
end loop
HTTP2.0使用header楨表達HTTP header+request line,data楨表達Body。header楨和data楨使用相同的stream id組成一個完整的HTTP請求/響應包。這里的stream描述了一次請求和響應,相當於完成了一次HTTP/1.x的短連接請求和響應。
第三話、並行不悖
上節講到我們用h2楨完整表達了HTTP/1.x。但是h2協議抱負遠不止於此。它的真正目的是解決之前HTTP1.x的線頭阻塞問題、改善網絡延遲和頁面加載時間。
我們知道一個完整的網頁包含了主頁請求和數次或數十次的子請求。HTTP/1.1已經可以並行發出所有請求.但是HTTP本身是無狀態的協議,它依賴於時間的順序來識別請求和響應直接的對應關系。先來的請求必須先給響應。那么如果后面的響應資源對瀏覽器構建DOM或者CSSOM更重要。那它必須阻塞等待前者完成。當然這也難不倒我們,我們可以多開幾條tcp連接(瀏覽器規定一個origin(協議+host+port)最多6個)或者合並資源來減少不必要的阻塞。這是有代價的。首先tcp建連的開銷,其實合並資源帶來一小塊子資源過期導致整個合並資源的緩存過期。對此,h2有一攬子的解決方案,接下來一一道來。
h2在一個tcp連接創建多個流。每個流可以有從屬關系,比如說根據瀏覽器加載的優先級順序(主請求>CSS>能改變DOM結構的JS文件>圖片和字體資源文件)建立一條依賴關系鏈。處於同一等級的依賴關系中可以設置權重。權重用於分配傳輸信道資源多少。
圖6例子說明了有一次主頁請求index.html、一次main.css,一次jq.js以及一些image文件和字體文件qq.tff
圖3、h2請求的依賴樹
HTML的優先級最高,在HTML傳輸完成之前,其他文件不會被傳輸。HTML傳輸完成后,JS和CSS根據其分配的權重占比分配信息傳輸資源。如果CSS傳輸完成后,TFF和PNG如果是相同權重,那么他們將占有1/4的信道資源。
這里拋出3個問題和答案。
- 如果CSS被阻塞了,那么 JS 得到本屬於CSS的通信資源
- 如果CSS傳輸完成但沒有被移依賴樹, TFF和PNG繼承CSS的通信份額 (假設TFF和PNG權重一樣,那么各分得1/4通信資源).
- 如果CSS在依賴數被移除,JS, TFF, PNG平分通信資源(假設3個權重一樣,那么三者各分得1/3通信資源)
第四話、眾星捧月
HTTP2還設計了一系列方案來改善網絡的性能、包括流量控制,HPack壓縮,Server Push。
什么你說TCP已經有流量控制了,HTTP不是多此一舉嗎?沒錯,但是在單條TCP內部,各個流可是沒有流量控制。流量控制使用了Update Frame不斷告知發送方更新發送的窗口大小(上限)。流量控制一個現實用途是阻塞不重要的請求,以騰出更大的通信資源給重要的請求使用。流量控制是不可以被關閉的,流量大小可以設置2的31次方-1(2GB)。不同的中間網絡設備有不一樣的吞吐能力。流控的另一個用途在於同步所有的中間設備交換機最小的上限。流量窗口初始大小為65535(2的16次方-1)。
就像世界上的大多數財富聚集在少數人身上一樣。在一份對48,452,989個請求的統計中,以下11個頭占據了99%的數量,依名次遞減分別是user-agent、accetp-encoding、accept-language、accept、referer、host、connection、cookie、origin、upgrade-inseure-request、content-type。http header的值也有很大相似性,比如說”/index.html“, “gzip, deflate”。Cookie也攜帶冗余的信息。
這些都組成了http header大量可以壓縮的內容。
而在一份GET請求和一個304響應或者content-length很少的響應中,這些頭占據了很大比例的通信資源。2016年發布的一份HTTP報告中,請求頭大約在460bytes,對一個通常的網頁,平均會有140個請求對象。這些頭總共需要63KB。這些量很有可能會是首屏和頁面加載時間優化的瓶頸。
可能你會說用gzip等壓縮算法這些請求頭,不就完了嗎?的確spdy就這樣干過,直到2013年BREACH攻擊暴露了gzip壓縮在https應用的安全性,這種攻擊讓攻擊者很容易獲得session cookie等數據。於是才有了HPACK。
HPACK簡單來說就是索引表,包括靜態表和動態表。靜態表由RFC定義,從不改變,靜態表預留了62個表項。每個連接的通信雙方維護着動態表。
H2協議使用索引號代表http中的name、value或者name-value。假設被索引的是name,value沒有索引,那么value還可以用霍夫曼編碼壓縮。
- 在預定的頭字段靜態映射表 中已經有預定義的 Header Name 和 Header Value值,這時候的二進制數據格式如圖4, 第一位固定為1, 后面7位為映射的索引值。圖8的83就是這樣的,83的二進制字節標示1000 0011,抹掉首位就是 3 , 對應的靜態映射表中的method:POST。
圖4、index索引name和value
圖5、抓包示意
- 預定的頭字段靜態映射表中有 name,需要設置新值。圖6所示例子,一個指定 path的Header,首字符 為 44 ,對應的二進制位0100 0100。前兩個字符為 01 ,Index 為 4 ,即對應靜態映射表中的 path 頭。第二個字符為 95對應的二進制位 1001 0101,排除首字符對應的 Value Length 為 十進制的21。即 算上 44,一共23個字符來記錄這個信息。
圖6、index索引name和自定義value
圖7、抓包示意
- 預定的頭字段靜態映射表中沒有 name,需要設置新name和新值。40 的二進制是0100 0000,02 的二進制是0000 0010,后七位的十進制值是 2,86 的二進制是1000 0110,后7位的十進制值是6。
圖8、index索引自定義name和自定義value
圖9、抓包示意
- 明確要求該請求頭不做hpack的index
HTTP2.0還有個大殺器是Server Push,Server Push利用閑置的帶寬資源可以向瀏覽器預推送頁面展示的關鍵資源,Server push有效得降低了頁面加載時間。具體詳情參考筆者的另一篇文章https://cloud.tencent.com/developer/article/1159626。
第五話、庖丁解牛
HTTP2相比HTTP1更適合計算機執行。但是其二進制特性不易於人腦理解。這一話我們專門來講講關於http2的調試工具。
- Chrome(qq瀏覽器)可以按住F12查看h2協議。圖13所示為瀏覽器網絡時序圖,列出了具體協議名稱。chrome還可以在地址欄敲入chrome://net-internals/#http2查看到h2協議細節,如圖11所示。點擊相應的host就可以看到h2協商過程,如圖12所示。
圖10、瀏覽器的網絡時序圖
圖11、chrome調試h2
圖12、h2協商過程
- wireshark(需和chrome或firefox搭配使用)。設置環境變量SSLKEYLOGFILE=c:\temp\sslkeylog.log。然后在Wireshark->Preferences->Protocols->SSL配置key所在路徑。
圖13、wireshark配置
圖14、wireshark抓h2包
- Nghttp2是一個完整的http2協議實現的組件。作者也參與過spdy實現。目前nghttp2庫被很多知名軟件作為h2協議實現庫使用。另外nghttp2也自帶了h2協議的分析工具。圖18展示了在明文狀態使用upgrade頭升級到h2c。圖19展示了在https基礎上升級到h2。
圖15、明文狀態使用upgrade頭升級到h2c
圖16、展示了在https基礎上升級到h2
Curl的—http2選項(需要和nghttp2一起編譯)
圖17、支持h2的curl客戶端調試
- Github還有些實用的http2工具組件,諸如chrome-http2-log-parser、http2-push-manifest等組件,筆者后續會專門開篇文章介紹這些工具。對於移動端的調試,ios可以用charles proxy做代理,android需要開發者模式使用移動端的chrome,筆者在移動端使用較少,這里就不做展開。
第六話、雕欄玉砌
H2怎么部署呢,目前主流服務端像nginx、apache都已經支持http2,主流的客戶端curl和各種瀏覽器(包括移動端safari和chrome-android)基本也支持http2。代理服務器如ATS、Varnish,Akamai、騰訊雲等CDN服務也支持http2。那么怎么把一套網站部署到h2。或者說部署h2網站和之前h1網站有什么不一樣?
如果是自己的源站,那么請確保服務器支持TLS1.2已經RFC7540所要求的加密套件,h2需要保證支持alpn。你可以使用ssllabs等網站檢查。對於h2服務器的要求是h2必須了解如何設置流的優先級,h2服務器需要支持server push。h2客戶端需要盡量多的發送請求。
如果你的網站是從http1.x遷移過來的,那么之前對於http1.x所做的優化可能無任何幫助甚至更差。合並小文件不在需要,因為額外的小文件請求在h2看來只是開銷很少。並且如果大文件的局部更改使得整個大文件緩存失效。在http1.0時代使用多個域名來並發http連接,在http2也毫無必要,因為http2天生就是並發的。http1.x做的優化比如說圖片資源文件不使用cookie來減少請求大小,http2的header壓縮功能也減少了這種影響。即使不做這種優化也亦可。像合並css、小圖片帶來的增益在http2.0也是可忽略的。
如果網頁使用第三方網站組件,那么請盡可能減少使用第三方網站組件。第三方網站不能保證支持h2,所以它可能成為木桶理論的最大短板。
謹慎使用2.0-1.x的部署方案,h2流轉化成h1請求。因為這樣無法發揮h2性能。
圖18、2.0-1.x的部署方案
CDN代理服務器的h2支持,可以屏蔽h2強制走tls的代理服務器。如圖19,代理可以在與各種協議客戶端的網絡環境下,切斷和客戶端的tls連接,和服務器新建連接。也可以作為load balancer,相當於HTTP2.0用戶和HTTP2.x服務器直接通信。
圖19、帶tls客戶端功能的代理
圖20列舉如果繞過proxy到達h2服務器。此時的proxy相當於tcp轉發的load balance功能的設備。如果該proxy支持tls的alpn協議,那么它也可以選擇HTTP代理功能,和h2服務器可以建立加密連接。如果即不支持alpn,也不支持tcp轉發。那么proxy只能用upgrade升級成h2協議。
圖20、經過代理服務器的H2部署方案
第七話、十全九美
HTTP2.0是建立在TCP之上,所以TCP的所有缺點他都有,所以H2能發揮最大性能得益於調優的tcp協議棧。TCP的慢啟動特性,決定h2一開始的並發流量不會太大,TCP以及SSL的握手連接也會拖慢h2的首包網絡耗時。QUIC則完全地拋棄TCP,在UDP基礎上實現了HTTP2的一系列特性。同時做了應用層的如TCP的可靠性保障。同時這些TLS1.3傳輸更快更簡潔。這些都為HTTP2.0進化到HTTP3.0提供了一些思路。
總結
具體可以繼續參考RFC7540和RFC7541協議。