HTTP,是Web工程師每天打交道最多的一個基本協議。很多工作流程、性能優化都圍繞HTTP協議來進行,但是我們對HTTP的理解是否全面呢?如果前端工程師和后臺工程師坐在一起玩捉鬼游戲,他們對HTTP的描述可能會截然不同,從這兩個角色的視角看過去,HTTP呈現出截然不同的形態。
HTTP簡介
超文本傳輸協議(HyperText Transfer Protocol,HTTP)是互聯網上應用最為廣泛的一種網絡協議。設計HTTP的最初目的是提供一種發布和接收html頁面的方法。OSI模型1義了整個世界計算機相互連接的標準,總共分為7層,其中最上層(也就是第7層)就是應用層,HTTP、HTTPS、FTP、TELNET、SSH、SMTP和POP3都屬于應用層。這是軟件工程師最關心的一層。
SI模型越靠近底層,就越接近硬件。在HTTP協議中,并沒有規定必須使用它或它支持的層。事實上,HTTP可以在任何互聯網協議或其他網絡上實現。HTTP假定其下層協議提供可靠的傳輸,因此,任何能夠提供這種保證的協議都可以被其使用,也就是其在TCP/IP協議族使用TCP作為其傳輸層。
OSI模型,圖片來自維基百科。
關于HTTP版本
HTTP已經演化出了很多版本,它們中的大部分都是向下兼容的??蛻舳嗽谡埱蟮拈_始告訴服務器它采用的協議版本號,而后者則在響應中采用相同或者更早的協議版本。
當前應用最廣泛的HTTP版本為HTTP/1.1,它自從1999年發布以來,距寫作本書時已有16年的時間。比起HTTP/1,它增加了幾個重要特性,比如緩存處理(在下一章介紹)和持續連接,以及其他一些性能優化。
2015年2月,HTTP/2正式發布。新的HTTP版本有一些重大更新,除了一如既往地向下兼容HTTP/1以外,還有一些優化,比如減小網絡傳輸延遲,并簡化服務器向瀏覽器傳輸內容的過程。主流的服務器(Apache、Nginx等)和瀏覽器(Firefox、Chrome、Safari以及IOS和Android的瀏覽器等)的最新版都已經支持HTTP/2,剩下的就需要網站管理員把服務器升級到最新版了。
例子
下面是一個HTTP客戶端與服務器之間會話的例子,運行于www.google.com,端口80。
客戶端首先發出請求。
GET / HTTP/1.1
Host:www.google.com
第一行指定方法、資源路徑、協議版本。當然這是一個簡化后的例子,實際請求中還會有當前Google登錄賬戶的cookie、HTTPS頭、瀏覽器接受何種類型的壓縮格式和UA2代碼等。
服務器隨之應答。
HTTP/1.1 200 OK
Content-Length: 3059
Server: GWS/2.0
Date: Mon, 20 Apr 2015 20:30:45 GMT
Content-Type: text/html
Cache-control: private
Set-cookie: PREF=ID=73d4aef52e57bae9:TM=1042253044:LM=1042253044:S= SMCc_HRPCQiqy
X9j; expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com
Connection: keep-alive
在這一串HTTPS頭之后,會緊跟著一個空行,然后是HTML格式的文本組成的Google主頁。
介紹完關于HTTP的基本知識,我們來分別看看前端工程師和后臺工程師分別是怎樣看待這個最熟悉的小伙伴的。
前端視角
前端工程師的職責之一是,讓網站又快又好地展現在用戶的瀏覽器中。
從這個角度來說,對HTTP的理解是這樣的:打開HttpWatch3,然后隨意訪問一個網站。HttpWatch會按照瀏覽器請求的次序,列出打開這個網站的時候發生的請求細節。
- 發出的請求列表。
- 每個請求的開始時間。
- 每個請求從開始到結束花費的時間。
- 每個請求的類型(比如是文本、css、JS,還是圖片或者字體等)。
- 每個請求的狀態碼(比如是200、還是from cache、304、404等)。
- 每個請求產生的流量消耗。
- 每個請求gzip4壓縮前的體積,以及在本地gzip解壓后的體積。
通過查看站點的HTTP請求信息,可以得到很多優化信息。每一個前端工程師都知道的基本優化方法是:盡量減少同一域下的HTTP請求數,以及盡量減少每一個資源的體積。5
盡量減少同一域下的HTTP請求數
瀏覽器常常限定了對同一域名發起的并發連接數的上限。IE6/7和Firefox2的設計規則是,同時只能對一個域名發起兩個并發連接。新版本的各種瀏覽器普遍把這一上限設定為4至8個。如果瀏覽器需要對某個域進行更多的連接,則需要在用完了當前連接之后,重復使用或者重新建立TCP連接。
QQ空間的CSS貼圖由程序自動生成,保證最佳的圖片質量、最合理的圖片擺放和最小的體積。
由于瀏覽器針對資源的域名限制并發連接數,而不是針對瀏覽器地址欄中的頁面域名,所以很多靜態資源可以放在其他域名下(不同的子域名也被認為是不同的域名)。如果您只有一臺服務器,可以把這些不同的域名同時指向一個IP,也就提高了對這臺服務器的并發連接數限制(不過要小心服務器壓力過大)。
把靜態資源放在非主域名下,這種做法除了可以增加瀏覽器并發,還有一個好處是,減少HTTP請求中攜帶的不必要的cookie數據。cookie是某些網站為了辨別用戶身份而儲存在用戶瀏覽器中的數據。cookie的作用域是整個域名,也就是說如果某個cookie存放在google.com域名下,那么對于google.com域名下的所有HTTP請求頭都會帶上cookie數據。如果Google把所有的資源都放在google.com下,那么所有資源的請求都會帶上cookie數據。對于靜態資源來說,這是毫無必要的,因為這對帶寬和鏈接速度都造成了影響。所以我們一般把靜態資源放在單獨的域名下。
除此之外,前端工程師經常做的優化是合并同一域名下的資源,比如把多個CSS合并為一個CSS,或者將圖片組合為CSS貼圖。6
還有一些優化建議是省掉不必要的HTTP請求,比如內嵌小型CSS、內嵌小型JAVAScript、設置緩存,以及減少重定向。這些做法雖然各不相同,但是如果了解HTTP請求的過程,就知道這些優化方法的最終目的都是最大化利用有限的請求數。
盡量減少每一個資源的體積
我們不光要限制請求數,還要盡量減少每一個資源的體積。因為資源的體積越大,在傳輸中消耗的流量就越多,等待時間也越久。
在面試應聘者的時候,我會問的一個基礎題目是“常用的圖片格式有哪些,它們的使用場景是什么”。如果能選擇合適的圖片格式,就能夠用更小的體積,達到更好的顯示效果。對圖片格式的敏感,能反映出工程師對帶寬和速度的不懈追求。
此外,對于比較大的文本資源,必須開啟gzip壓縮。因為gzip對于含有重復“單詞”的文本文件,壓縮率非常高,能有效提高傳輸過程。
對于一個CSS資源的請求耗時,我想說明兩個細節。
- 這個CSS資源請求的體積是36.4KB(這是gzip壓縮過的體積),解壓縮之后,CSS內容實際上是263KB,可以算出壓縮后體積是原來的13.8%。
- 整個連接的建立花費了30%的時間,發出請求到等待收到第一個字節回復花費了20%的時間,下載CSS資源的內容花費了50%的時間。
如果沒有設置gzip,下載這個CSS文件會需要好幾倍的時間。
后臺視角
前端工程師對HTTP的關注點在于盡量減少同一域下的HTTP請求數,以及盡量減少每一個資源的體積。與之不同,后臺工程師對于HTTP的關注在于讓服務器盡快響應請求,以及減少請求對服務器的開銷。
后臺工程師知道,瀏覽器限定對某個域的并發連接數,很大程度上是瀏覽器對服務器的一種保護行為。瀏覽器作為一種善意的客戶端,為了保護服務器不被大量的并發請求弄得崩潰,才限定了對同一個域的最大并發連接數。而一些“惡意”的客戶端,比如一些下載軟件,它作為一個HTTP協議客戶端,不考慮到服務器的壓力,而發起大量的并發請求(雖然用戶感覺到下載速度很快),但是由于它違反了規則,所以經常被服務器端“防范”和屏蔽。
那么為什么服務器對并發請求數這么敏感?
雖然服務器的多個進程看上去是在同時運行,但是對于單核CPU的架構來說,實際上是計算機系統同一段時間內,以進程的形式,將多個程序加載到存儲器中,并借由時間共享,以在一個處理器上表現出同時運行的感覺。由于在操作系統中,生成進程、銷毀進程、進程間切換都很消耗CPU和內存,因此當負載高時,性能會明顯降低。
提高服務器的請求處理能力
在早期系統中(如linux 2.4以前),進程是基本運作單位。在支持線程的系統(Linux2.6)中,線程才是基本的運作單位,而進程只是線程的容器。由于線程開銷明顯小于進程,而且部分資源還可以共享,因此效率較高。
Apache是市場份額最大的服務器,超過50%的網站運行在Apache上。Apache 通過模塊化的設計來適應各種環境,其中一個模塊叫做多處理模塊(MPM),專門用來處理多請求的情況。Apache安裝在不同系統上的時候會調用不同的默認MPM,我們不用關心具體的細節,只需要了解Unix上默認的MPM是prefork。為了優化,我們可以改成worker模式。
prefork和worker模式的最大區別就是,prefork的一個進程維持一個連接,而worker的一個線程維持一個連接。所以prefork更穩定但內存消耗也更大,worker沒有那么穩定,因為很多連接的線程共享一個進程,當一個線程崩潰的時候,整個進程和所有線程一起死掉。但是worker的內存使用要比prefork低得多,所以很適合用在高HTTP請求的服務器上。
近年來Nginx越來越受到市場的青睞。在高連接并發的情況下,Nginx是Apache服務器不錯的替代品或者補充:一方面是Nginx更加輕量級,占用更少的資源和內存;另一方面是Nginx 處理請求是異步非阻塞的,而Apache 則是阻塞型的,在高并發下Nginx 能保持低資源、低消耗和高性能。
由于Apache和Nginx各有所長,所以經常的搭配是Nginx處理前端并發,Apache處理后臺請求。
值得一提的是,新秀Node.js也是采用基于事件的異步非阻塞方式處理請求,所以在處理高并發請求上有天然的優勢。
DDoS攻擊
DDoS是Distributed Denial of Service的縮寫,DDoS攻擊翻譯成中文就是“分布式拒絕服務”攻擊。
簡單來說,就是黑客入侵并控制了大量用戶的計算機(俗稱“肉雞”),然后在這些計算機上安裝了DDoS攻擊軟件。我們知道瀏覽器作為一種“善意”的客戶端,限制了HTTP并發連接數。但是DDoS就沒有這樣的道德準則,每一個DDoS攻擊客戶端都可以自由設置TCP/IP并發連接數,并且連接上服務器之后,它不會馬上斷開連接,而是保持這個連接一段時間,直到同時連接的數量大于最大連接數,才斷開之前的連接。
就這樣,攻擊者通過海量的請求,讓目標服務器癱瘓,無法響應正常的用戶請求,以此達到攻擊的效果。
對于這樣的攻擊,幾乎沒有什么特別好的防護方法。除了增加帶寬和提高服務器能同時接納的客戶數,另一種方法就是讓首頁靜態化。DDoS攻擊者喜歡攻擊的頁面一般是會對數據庫進行寫操作的頁面,這樣的頁面無法靜態化,服務器更容易宕機。DDoS攻擊者一般不會攻擊靜態化的頁面或者圖片,因為靜態資源對服務器壓力小,而且能夠部署在CDN上。
這里介紹的只是最簡單的TCP/IP攻擊,而DDoS是一個概稱,具體來說,有各種攻擊方式,比如CC攻擊、SYN攻擊、NTP攻擊、TCP攻擊和DNS攻擊等。
BigPipe
前端跟后端在HTTP上也能有交集,BigPipe就是一個例子。
現有的HTTP數據請求流程是:客戶端建立連接,服務器同意連接,客戶端發起請求,服務器返回數據,客戶端接受并處理數據。這個處理流程有兩個問題。
現有的阻塞模型,黃色代表服務器生成頁面,白色代表網絡傳輸,紫色代表瀏覽器渲染頁面。
第一,HTTP協議的底層是TCP/IP,而TCP/IP規定3次握手才建立一次連接。每一個新增的請求都要重新建立TCP/IP連接,從而消耗服務器的資源,并且浪費連接時間。對于幾種不同的服務器程序(Apache、Nginx和Node.js等),所消耗的內存和CPU資源也不太一樣,但是新的連接無法避免,沒有從本質上解決問題。
第二個問題是,在現有的阻塞模型中,服務器計算生成頁面需要時間。等服務器完全生成好整個頁面,才開始網絡傳輸,網絡傳輸也需要時間。整個頁面都完全傳輸到瀏覽器中之后,在瀏覽器中最后渲染還是需要時間。三者是阻塞式的,每一個環節都在等上一個環節100%完成才開始。頁面作為一個整體,需要完整地經歷3個階段才能出現在瀏覽器中,效率很低。
BigPipe是Facebook公司科學家Changhao Jiang發明的一種非阻塞式模型,這種模型能完美解決上面的兩個問題。
通俗來解釋,BigPipe首先把HTML頁面分為很多部分,然后在服務器和瀏覽器之間建立一條管道(BigPipe就是“大管道”的意思),HTML的不同部分可以源源不斷地從服務器傳輸到瀏覽器。BigPipe首先輸送的內容是框架性HTML結構,這個框架結構可能會定義每個Pagelet模塊的位置和寬高,但是這些pagelet都是空的,就像只有鋼筋混泥土骨架的毛坯房。
BigPipe頁面的渲染流程。
服務器傳輸完框架性HTML結構之后,對瀏覽器說:“我這個請求還沒結束,我們保持這個連接不要斷開,不過您可以先用我給您的這部分來渲染。”
所以瀏覽器就開始渲染這個“不完整的HTML”,毛坯房頁面很快出現在用戶眼前,具體的頁面模塊都顯示“正在加載”。
接下來管道里源源不斷地傳輸過來很多模塊,這時候最開始加載在服務器中的JS代碼開始工作,它會負責把每一個模塊依次渲染到頁面上。
在用戶的感知上,頁面非??斓爻霈F在眼前,但是所有的模塊都顯示正在加載中,然后主要的區域(比如重要的用戶動態)優先出現,接下來是logo、邊欄和各種掛件等。
為什么BigPipe能夠讓服務器對瀏覽器說“我這個請求還沒結束,我們保持這個連接不要斷開”呢?答案是HTTP1.1的分塊傳輸編碼。
HTTP 1.1引入分塊傳輸編碼,允許服務器為動態生成的內容維持HTTP持久鏈接。如果一個HTTP消息(請求消息或應答消息)的Transfer-Encoding消息頭的值為chunked,那么消息體由數量不確定的塊組成——也就是說想發送多少塊就發送多少塊——并以最后一個大小為0的塊為結束。
實現這個架構需要深刻理解HTTP 1.1的規則,而且要有前端的知識。在我看來,這就是一個極佳的全棧工程師改變世界的例子。
截止寫書時,Chrome、Safari和Opera已經支持HTTP/2并默認開啟,它允許服務器向瀏覽器“推送”內容。也就是說,返回的條目數可以比請求的條目數多,這樣服務器可以在一開始就推送所有它認為瀏覽器“應該需要”的資源,而不需要瀏覽器接受并解析完HTML頁面才開始請求下載CSS、JavaScript等。而且,后面的請求可以復用之前已經建立的底層連接。