在我們所處的互聯網世界中,HTTP協議算得上是使用最廣泛的網絡協議。最近http2.0的誕生使得它再次互聯網技術圈關注的焦點。任何事物的消退和新生都有其背后推動的力量。對于HTTP來說,這力量復雜來說是各種技術細節的演進,簡單來說是用戶體驗和感知的進化。用戶總是希望網絡上的信息能盡可能快的抵達眼球,越快越好,正是這種對“快”對追逐催生了今天的http2.0。
1. HTTP2.0的前世
http2.0的前世是http1.0和http1.1這兩兄弟。雖然之前僅僅只有兩個版本,但這兩個版本所包含的協議規范之龐大,足以讓任何一個有經驗的工程師為之頭疼。http1.0誕生于1996年,協議文檔足足60頁。之后第三年,http1.1也隨之出生,協議文檔膨脹到了176頁。不過和我們手機端app升級不同的是,網絡協議新版本并不會馬上取代舊版本。實際上,1.0和1.1在之后很長的一段時間內一直并存,這是由于網絡基礎設施更新緩慢所決定的。今天的http2.0也是一樣,新版協議再好也需要業界的產品錘煉,需要基礎設施逐年累月的升級換代才能普及。
1.1 HTTP站在TCP之上
理解http協議之前一定要對TCP有一定基礎的了解。HTTP是建立在TCP協議之上,TCP協議作為傳輸層協議其實離應用層并不遠。HTTP協議的瓶頸及其優化技巧都是基于TCP協議本身的特性。比如TCP建立連接時三次握手有1.5個RTT(round-trip time)的延遲,為了避免每次請求的都經歷握手帶來的延遲,應用層會選擇不同策略的http長鏈接方案。又比如TCP在建立連接的初期有慢啟動(slow start)的特性,所以連接的重用總是比新建連接性能要好。
1.1 HTTP應用場景
http誕生之初主要是應用于web端內容獲取,那時候內容還不像現在這樣豐富,排版也沒那么精美,用戶交互的場景幾乎沒有。對于這種簡單的獲取網頁內容的場景,http表現得還算不錯。但隨著互聯網的發展和web2.0的誕生,更多的內容開始被展示(更多的圖片文件),排版變得更精美(更多的css),更復雜的交互也被引入(更多的js)。用戶打開一個網站首頁所加載的數據總量和請求的個數也在不斷增加。今天絕大部分的門戶網站首頁大小都會超過2M,請求數量可以多達100個。另一個廣泛的應用是在移動互聯網的客戶端app,不同性質的app對http的使用差異很大。對于電商類app,加載首頁的請求也可能多達10多個。對于微信這類IM,http請求可能僅限于語音和圖片文件的下載,請求出現的頻率并不算高。
1.2 因為延遲,所以慢
影響一個網絡請求的因素主要有兩個,帶寬和延遲。今天的網絡基礎建設已經使得帶寬得到極大的提升,大部分時候都是延遲在影響響應速度。http1.0被抱怨最多的就是連接無法復用,和head of line blocking這兩個問題。理解這兩個問題有一個十分重要的前提:客戶端是依據域名來向服務器建立連接,一般PC端瀏覽器會針對單個域名的server同時建立6~8個連接,手機端的連接數則一般控制在4~6個。顯然連接數并不是越多越好,資源開銷和整體延遲都會隨之增大。
連接無法復用會導致每次請求都經歷三次握手和慢啟動。三次握手在高延遲的場景下影響較明顯,慢啟動則對文件類大請求影響較大。
head of line blocking會導致帶寬無法被充分利用,以及后續健康請求被阻塞。假設有5個請求同時發出,如下圖:
對于http1.0的實現,在第一個請求沒有收到回復之前,后續從應用層發出的請求只能排隊,請求2,3,4,5只能等請求1的response回來之后才能逐個發出。網絡通暢的時候性能影響不大,一旦請求1的request因為什么原因沒有抵達服務器,或者response因為網絡阻塞沒有及時返回,影響的就是所有后續請求,問題就變得比較嚴重了。
1.3 解決連接無法復用
http1.0協議頭里可以設置Connection:Keep-Alive。在header里設置Keep-Alive可以在一定時間內復用連接,具體復用時間的長短可以由服務器控制,一般在15s左右。到http1.1之后Connection的默認值就是Keep-Alive,如果要關閉連接復用需要顯式的設置Connection:Close。一段時間內的連接復用對PC端瀏覽器的體驗幫助很大,因為大部分的請求在集中在一小段時間以內。但對移動app來說,成效不大,app端的請求比較分散且時間跨度相對較大。所以移動端app一般會從應用層尋求其它解決方案,長連接方案或者偽長連接方案:
方案一:基于tcp的長鏈接
現在越來越多的移動端app都會建立一條自己的長鏈接通道,通道的實現是基于tcp協議。基于tcp的socket編程技術難度相對復雜很多,而且需要自己制定協議,但帶來的回報也很大。信息的上報和推送變得更及時,在請求量爆發的時間點還能減輕服務器壓力(http短連接模式會頻繁的創建和銷毀連接)。不止是IM app有這樣的通道,像淘寶這類電商類app都有自己的專屬長連接通道了。現在業界也有不少成熟的方案可供選擇了,google的protobuf就是其中之一。
方案二:http long-polling
long-polling可以用下圖表示:
客戶端在初始狀態就會發送一個polling請求到服務器,服務器并不會馬上返回業務數據,而是等待有新的業務數據產生的時候再返回。所以連接會一直被保持,一旦結束馬上又會發起一個新的polling請求,如此反復,所以一直會有一個連接被保持。服務器有新的內容產生的時候,并不需要等待客戶端建立一個新的連接。做法雖然簡單,但有些難題需要攻克才能實現穩定可靠的業務框架:
long-polling方式還有一些缺點是無法克服的,比如每次新的請求都會帶上重復的header信息,還有數據通道是單向的,主動權掌握在server這邊,客戶端有新的業務請求的時候無法及時傳送。
方案三:http streaming
http streaming流程大致如下:
同long-polling不同的是,server并不會結束初始的streaming請求,而是持續的通過這個通道返回最新的業務數據。顯然這個數據通道也是單向的。streaming是通過在server response的頭部里增加”Transfer Encoding: chunked”來告訴客戶端后續還會有新的數據到來。除了和long-polling相同的難點之外,streaming還有幾個缺陷:
有些代理服務器會等待服務器的response結束之后才會將結果推送到請求客戶端。對于streaming這種永遠不會結束的方式來說,客戶端就會一直處于等待response的過程中。
業務數據無法按照請求來做分割,所以客戶端沒收到一塊數據都需要自己做協議解析,也就是說要做自己的協議定制。
streaming不會產生重復的header數據。
方案四:web socket
WebSocket和傳統的tcp socket連接相似,也是基于tcp協議,提供雙向的數據通道。WebSocket優勢在于提供了message的概念,比基于字節流的tcp socket使用更簡單,同時又提供了傳統的http所缺少的長連接功能。不過WebSocket相對較新,2010年才起草,并不是所有的瀏覽器都提供了支持。各大瀏覽器廠商最新的版本都提供了支持。
1.4 解決head of line blocking
Head of line blocking(以下簡稱為holb)是http2.0之前網絡體驗的最大禍源。正如圖1中所示,健康的請求會被不健康的請求影響,而且這種體驗的損耗受網絡環境影響,出現隨機且難以監控。為了解決holb帶來的延遲,協議設計者設計了一種新的pipelining機制。
http pipelining
pipelining的流程圖可以用下圖表示:
和圖一相比最大的差別是,請求2,3,4,5不用等請求1的response返回之后才發出,而是幾乎在同一時間把request發向了服務器。2,3,4,5及所有后續共用該連接的請求節約了等待的時間,極大的降低了整體延遲。下圖可以清晰的看出這種新機制對延遲的改變:
不過pipelining并不是救世主,它也存在不少缺陷:
正是因為有這么多的問題,各大瀏覽器廠商要么是根本就不支持pipelining,要么就是默認關掉了pipelining機制,而且啟用的條件十分苛刻??梢詤⒖糲hrome對于pipeling的問題描述。
1.5 其它奇技淫巧
為了解決延遲帶來的苦惱,永遠都會有聰明的探索者找出新的捷徑來。互聯網的蓬勃興盛催生出了各種新奇技巧,我們來依次看下這些“捷徑”及各自的優缺點。
Spriting(圖片合并)
Spriting指的是將多個小圖片合并到一張大的圖片里,這樣多個小的請求就被合并成了一個大的圖片請求,然后再利用js或者css文件來取出其中的小張圖片使用。好處顯而易見,請求數減少,延遲自然低。壞處是文件的粒度變大了,有時候我們可能只需要其中一張小圖,卻不得不下載整張大圖,cache處理也變得麻煩,在只有一張小圖過期的情況下,為了獲得最新的版本,不得不從服務器下載完整的大圖,即使其它的小圖都沒有過期,顯然浪費了流量。
Inlining(內容內嵌)
Inlining的思考角度和spriting類似,是將額外的數據請求通過base64編碼之后內嵌到一個總的文件當中。比如一個網頁有一張背景圖,我們可以通過如下代碼嵌入:
background: url(data:image/png;base64,)
data部分是base64編碼之后的字節碼,這樣也避免了一次多余的http請求。但這種做法也有著和spriting相同的問題,資源文件被綁定到了其它文件,粒度變得難以控制。
Concatenation(文件合并)
Concatenation主要是針對js這類文件,現在前端開發交互越來越多,零散的js文件也在變多。將多個js文件合并到一個大的文件里在做一些壓縮處理也可以減小延遲和傳輸的數據量。但同樣也面臨著粒度變大的問題,一個小的js代碼改動會導致整個js文件被下載。
Domain Sharding(域名分片)
前面我提到過很重要的一點,瀏覽器或者客戶端是根據domain(域名)來建立連接的。比如針對www.example.com只允許同時建立2個連接,但mobile.example.com被認為是另一個域名,可以再建立兩個新的連接。依次類推,如果我再多建立幾個sub domain(子域名),那么同時可以建立的http請求就會更多,這就是Domain Sharding了。連接數變多之后,受限制的請求就不需要等待前面的請求完成才能發出了。這個技巧被大量的使用,一個頗具規模的網頁請求數可以超過100,使用domain sharding之后同時建立的連接數可以多到50個甚至更多。
這么做當然增加了系統資源的消耗,但現在硬件資源升級非常之快,和用戶寶貴的等待時機相比起來實在微不足道。
domain sharding還有一大好處,對于資源文件來說一般是不需要cookie的,將這些不同的靜態資源文件分散在不同的域名服務器上,可以減小請求的size。
不過domain sharding只有在請求數非常之多的場景下才有明顯的效果。而且請求數也不是越多越好,資源消耗是一方面,另一點是由于tcp的slow start會導致每個請求在初期都會經歷slow start,還有tcp 三次握手,DNS查詢的延遲。這一部分帶來的時間損耗和請求排隊同樣重要,到底怎么去平衡這二者就需要取一個可靠的連接數中間值,這個值的最終確定要通過反復的測試。移動端瀏覽器場景建議不要使用domain sharding,具體細節參考這篇文章。
2. 開拓者SPDY
http1.0和1.1雖然存在這么多問題,業界也想出了各種優化的手段,但這些方法手段都是在嘗試繞開協議本身的缺陷,都有種隔靴搔癢,治標不治本的感覺。直到2012年google如一聲驚雷提出了SPDY的方案,大家才開始從正面看待和解決老版本http協議本身的問題,這也直接加速了http2.0的誕生。實際上,http2.0是以SPDY為原型進行討論和標準化的。為了給http2.0讓路,google已決定在2016年不再繼續支持SPDY開發,但在http2.0出生之前,SPDY已經有了相當規模的應用,作為一個過渡方案恐怕在還將一段時間內繼續存在?,F在不少app客戶端和server都已經使用了SPDY來提升體驗,http2.0在老的設備和系統上還無法使用(iOS系統只有在iOS9+上才支持),所以可以預見未來幾年spdy將和http2.0共同服務的情況。
2.1 SPDY的目標
SPDY的目標在一開始就是瞄準http1.x的痛點,即延遲和安全性。我們上面通篇都在討論延遲,至于安全性,由于http是明文協議,其安全性也一直被業界詬病,不過這是另一個大的話題。如果以降低延遲為目標,應用層的http和傳輸層的tcp都是都有調整的空間,不過tcp作為更底層協議存在已達數十年之久,其實現已深植全球的網絡基礎設施當中,如果要動必然傷經動骨,業界響應度必然不高,所以SPDY的手術刀對準的是http。
降低延遲,客戶端的單連接單請求,server的FIFO響應隊列都是延遲的大頭。
http最初設計都是客戶端發起請求,然后server響應,server無法主動push內容到客戶端。
壓縮HTTP Header,http1.x的header越來越膨脹,cookie和user agent很容易讓header的size增至1kb大小,甚至更多。而且由于http的無狀態特性,header必須每次request都重復攜帶,很浪費流量。
為了增加業界響應的可能性,聰明的google一開始就避開了從傳輸層動手,而且打算利用開源社區的力量以提高擴散的力度,對于協議使用者來說,也只需要在請求的header里設置user agent,然后在server端做好支持即可,極大的降低了部署的難度。SPDY的設計如下:
SPDY位于HTTP之下,TCP和SSL之上,這樣可以輕松兼容老版本的HTTP協議(將http1.x的內容封裝成一種新的frame格式),同時可以使用已有的SSL功能。SPDY的功能可以分為基礎功能和高級功能兩部分,基礎功能默認啟用,高級功能需要手動啟用。
SPDY基礎功能
多路復用(multiplexing)。多路復用通過多個請求stream共享一個tcp連接的方式,解決了http1.x holb(head of line blocking)的問題,降低了延遲同時提高了帶寬的利用率。
請求優先級(request prioritization)。多路復用帶來一個新的問題是,在連接共享的基礎之上有可能會導致關鍵請求被阻塞。SPDY允許給每個request設置優先級,這樣重要的請求就會優先得到響應。比如瀏覽器加載首頁,首頁的html內容應該優先展示,之后才是各種靜態資源文件,腳本文件等加載,這樣可以保證用戶能第一時間看到網頁內容。
header壓縮。前面提到過幾次http1.x的header很多時候都是重復多余的。選擇合適的壓縮算法可以減小包的大小和數量。SPDY對header的壓縮率可以達到80%以上,低帶寬環境下效果很大。
SPDY高級功能
server推送(server push)。http1.x只能由客戶端發起請求,然后服務器被動的發送response。開啟server push之后,server通過X-Associated-Content header(X-開頭的header都屬于非標準的,自定義header)告知客戶端會有新的內容推送過來。在用戶第一次打開網站首頁的時候,server將資源主動推送過來可以極大的提升用戶體驗。
server暗示(server hint)。和server push不同的是,server hint并不會主動推送內容,只是告訴有新的內容產生,內容的下載還是需要客戶端主動發起請求。server hint通過X-Subresources header來通知,一般應用場景是客戶端需要先查詢server狀態,然后再下載資源,可以節約一次查詢請求。
2.2 SPDY的成績
SPDY的成績可以用google官方的一個數字來說明:頁面加載時間相比于http1.x減少了64%。而且各大瀏覽器廠商在SPDY誕生之后的1年多里都陸續支持了SPDY,不少大廠app和server端框架也都將SPDY應用到了線上的產品當中。
google的官網也給出了他們自己做的一份測試數據。測試對象是25個訪問量排名靠前的網站首頁,家用網絡%1的丟包率,每個網站測試10次取平均值。結果如下: