漫談 HTTP 連接的相關(guān)知識
本文首先會 HTTP 的特點和優(yōu)缺點,然后會詳細介紹 HTTP 長連接和短連接的連接管理,通過閱讀本文能夠?qū)?HTTP 連接有個深入的認識。

通過前面的 HTTP 系列文章,想必大家已經(jīng)知道 HTTP 協(xié)議的基本知識,了解它的報文結(jié)構(gòu),請求頭、響應(yīng)頭等細節(jié)。
HTTP 的特點
所以接下來先是聊聊 HTTP 協(xié)議的特點、優(yōu)點和缺點。既要看到它好的一面,也要正視它不好的一面,只有全方位、多角度了解 HTTP,才能實現(xiàn)“揚長避短”,更好地利用 HTTP。
靈活可擴展
首先, HTTP 協(xié)議是一個“靈活可擴展”的傳輸協(xié)議。
HTTP 協(xié)議最初誕生的時候就比較簡單,本著開放的精神只規(guī)定了報文的基本格式,比如用空格分隔單詞,用換行分隔字段,“header+body”等,報文里的各個組成部分都沒有做嚴格的語法語義限制,可以由開發(fā)者任意定制。
所以,HTTP 協(xié)議就隨著互聯(lián)網(wǎng)的發(fā)展一同成長起來了。在這個過程中,HTTP 協(xié)議逐漸增加了請求方法、版本號、狀態(tài)碼、頭字段等特性。而 body 也不再限于文本形式的 TXT 或 HTML,而是能夠傳輸圖片、音頻視頻等任意數(shù)據(jù),這些都是源于它的“靈活可擴展”的特點。
而那些 RFC 文檔,實際上也可以理解為是對已有擴展的“承認和標準化”,實現(xiàn)了“從實踐中來,到實踐中去”的良性循環(huán)。
也正是因為這個特點,HTTP 才能在三十年的歷史長河中“屹立不倒”,從最初的低速實驗網(wǎng)絡(luò)發(fā)展到現(xiàn)在的遍布全球的高速互聯(lián)網(wǎng),始終保持著旺盛的生命力。
可靠傳輸
第二個特點, HTTP 協(xié)議是一個“可靠”的傳輸協(xié)議。
這個特點顯而易見,因為 HTTP 協(xié)議是基于 TCP/IP 的,而 TCP 本身是一個“可靠”的傳輸協(xié)議,所以 HTTP 自然也就繼承了這個特性,能夠在請求方和應(yīng)答方之間“可靠”地傳輸數(shù)據(jù)。
它的具體做法與 TCP/UDP 差不多,都是對實際傳輸?shù)臄?shù)據(jù)(entity)做了一層包裝,加上一個頭,然后調(diào)用 Socket API,通過 TCP/IP 協(xié)議棧發(fā)送或者接收。
不過我們必須正確地理解“可靠”的含義,HTTP 并不能 100% 保證數(shù)據(jù)一定能夠發(fā)送到另一端,在網(wǎng)絡(luò)繁忙、連接質(zhì)量差等惡劣的環(huán)境下,也有可能收發(fā)失敗。“可靠”只是向使用者提供了一個“承諾”,會在下層用多種手段“盡量”保證數(shù)據(jù)的完整送達。
當然,如果遇到光纖被意外挖斷這樣的極端情況,即使是神仙也不能發(fā)送成功。所以,“可靠”傳輸是指在網(wǎng)絡(luò)基本正常的情況下數(shù)據(jù)收發(fā)必定成功,借用運維里的術(shù)語,大概就是“3 個 9”或者“4 個 9”的程度吧。
應(yīng)用層協(xié)議
第三個特點,HTTP 協(xié)議是一個應(yīng)用層的協(xié)議。
這個特點也是不言自明的,但卻很重要。
在 TCP/IP 誕生后的幾十年里,雖然出現(xiàn)了許多的應(yīng)用層協(xié)議,但它們都僅關(guān)注很小的應(yīng)用領(lǐng)域,局限在很少的應(yīng)用場景。例如 FTP 只能傳輸文件、SMTP 只能發(fā)送郵件、SSH 只能遠程登錄等,在通用的數(shù)據(jù)傳輸方面“完全不能打”。
所以 HTTP 憑借著可攜帶任意頭字段和實體數(shù)據(jù)的報文結(jié)構(gòu),以及連接控制、緩存代理等方便易用的特性,一出現(xiàn)就“技壓群雄”,迅速成為了應(yīng)用層里的“明星”協(xié)議。只要不太苛求性能,HTTP 幾乎可以傳遞一切東西,滿足各種需求,稱得上是一個“萬能”的協(xié)議。
套用一個網(wǎng)上流行的段子,HTTP 完全可以用開玩笑的口吻說:“不要誤會,我不是針對 FTP,我是說在座的應(yīng)用層各位,都是垃圾。”
請求 - 應(yīng)答
第四個特點,HTTP 協(xié)議使用的是請求 - 應(yīng)答通信模式。
這個請求 - 應(yīng)答模式是 HTTP 協(xié)議最根本的通信模型,通俗來講就是“一發(fā)一收”“有來有去”,就像是寫代碼時的函數(shù)調(diào)用,只要填好請求頭里的字段,“調(diào)用”后就會收到答復(fù)。
請求 - 應(yīng)答模式也明確了 HTTP 協(xié)議里通信雙方的定位,永遠是請求方先發(fā)起連接和請求,是主動的,而應(yīng)答方只有在收到請求后才能答復(fù),是被動的,如果沒有請求時不會有任何動作。
當然,請求方和應(yīng)答方的角色也不是絕對的,在瀏覽器 - 服務(wù)器的場景里,通常服務(wù)器都是應(yīng)答方,但如果將它用作代理連接后端服務(wù)器,那么它就可能同時扮演請求方和應(yīng)答方的角色。
HTTP 的請求 - 應(yīng)答模式也恰好契合了傳統(tǒng)的 C/S(Client/Server)系統(tǒng)架構(gòu),請求方作為客戶端、應(yīng)答方作為服務(wù)器。所以,隨著互聯(lián)網(wǎng)的發(fā)展就出現(xiàn)了 B/S(Browser/Server)架構(gòu),用輕量級的瀏覽器代替笨重的客戶端應(yīng)用,實現(xiàn)零維護的“瘦”客戶端,而服務(wù)器則擯棄私有通信協(xié)議轉(zhuǎn)而使用 HTTP 協(xié)議。
此外,請求 - 應(yīng)答模式也完全符合 RPC(Remote Procedure Call)的工作模式,可以把 HTTP 請求處理封裝成遠程函數(shù)調(diào)用,導(dǎo)致了 WebService、RESTful 和 gPRC 等的出現(xiàn)。
無狀態(tài)
第五個特點,HTTP 協(xié)議是無狀態(tài)的。這個所謂的“狀態(tài)”應(yīng)該怎么理解呢?
“狀態(tài)”其實就是客戶端或者服務(wù)器里保存的一些數(shù)據(jù)或者標志,記錄了通信過程中的一些變化信息。
你一定知道,TCP 協(xié)議是有狀態(tài)的,一開始處于 CLOSED 狀態(tài),連接成功后是 ESTABLISHED 狀態(tài),斷開連接后是 FIN-WAIT 狀態(tài),最后又是 CLOSED 狀態(tài)。
這些“狀態(tài)”就需要 TCP 在內(nèi)部用一些數(shù)據(jù)結(jié)構(gòu)去維護,可以簡單地想象成是個標志量,標記當前所處的狀態(tài),例如 0 是 CLOSED,2 是 ESTABLISHED 等等。
再來看 HTTP,那么對比一下 TCP 就看出來了,在整個協(xié)議里沒有規(guī)定任何的“狀態(tài)”,客戶端和服務(wù)器永遠是處在一種“無知”的狀態(tài)。建立連接前兩者互不知情,每次收發(fā)的報文也都是互相獨立的,沒有任何的聯(lián)系。收發(fā)報文也不會對客戶端或服務(wù)器產(chǎn)生任何影響,連接后也不會要求保存任何信息。
“無狀態(tài)”形象地來說就是“沒有記憶能力”。比如,瀏覽器發(fā)了一個請求,說“我是小明,請給我 A 文件。”,服務(wù)器收到報文后就會檢查一下權(quán)限,看小明確實可以訪問 A 文件,于是把文件發(fā)回給瀏覽器。接著瀏覽器還想要 B 文件,但服務(wù)器不會記錄剛才的請求狀態(tài),不知道第二個請求和第一個請求是同一個瀏覽器發(fā)來的,所以瀏覽器必須還得重復(fù)一次自己的身份才行:“我是剛才的小明,請再給我 B 文件。”
我們可以再對比一下 UDP 協(xié)議,不過它是無連接也無狀態(tài)的,順序發(fā)包亂序收包,數(shù)據(jù)包發(fā)出去后就不管了,收到后也不會順序整理。而 HTTP 是有連接無狀態(tài),順序發(fā)包順序收包,按照收發(fā)的順序管理報文。
但不要忘了 HTTP 是“靈活可擴展”的,雖然標準里沒有規(guī)定“狀態(tài)”,但完全能夠在協(xié)議的框架里給它“打個補丁”,增加這個特性。
其他特點
除了以上的五大特點,其實 HTTP 協(xié)議還可以列出非常多的特點,例如傳輸?shù)膶嶓w數(shù)據(jù)可緩存可壓縮、可分段獲取數(shù)據(jù)、支持身份認證、支持國際化語言等。但這些并不能算是 HTTP 的基本特點,因為這都是由第一個“靈活可擴展”的特點所衍生出來的。
小結(jié)
- HTTP 是靈活可擴展的,可以任意添加頭字段實現(xiàn)任意功能;
- HTTP 是可靠傳輸協(xié)議,基于 TCP/IP 協(xié)議“盡量”保證數(shù)據(jù)的送達;
- HTTP 是應(yīng)用層協(xié)議,比 FTP、SSH 等更通用功能更多,能夠傳輸任意數(shù)據(jù);
- TTP 使用了請求 - 應(yīng)答模式,客戶端主動發(fā)起請求,服務(wù)器被動回復(fù)請求;
- HTTP 本質(zhì)上是無狀態(tài)的,每個請求都是互相獨立、毫無關(guān)聯(lián)的,協(xié)議不要求客戶端或服務(wù)器記錄請求相關(guān)的信息。
HTTP的連接管理
HTTP 的連接管理也算得上是個“老生常談”的話題了,你一定曾經(jīng)聽說過“短連接”“長連接”之類的名詞,今天讓我們一起來把它們弄清楚。
短連接
HTTP 協(xié)議最初(0.9/1.0)是個非常簡單的協(xié)議,通信過程也采用了簡單的“請求 - 應(yīng)答”方式。
它底層的數(shù)據(jù)傳輸基于 TCP/IP,每次發(fā)送請求前需要先與服務(wù)器建立連接,收到響應(yīng)報文后會立即關(guān)閉連接。
因為客戶端與服務(wù)器的整個連接過程很短暫,不會與服務(wù)器保持長時間的連接狀態(tài),所以就被稱為“短連接”(short-lived connections)。早期的 HTTP 協(xié)議也被稱為是“無連接”的協(xié)議。
短連接的缺點相當嚴重,因為在 TCP 協(xié)議里,建立連接和關(guān)閉連接都是非常“昂貴”的操作。TCP 建立連接要有“三次握手”,發(fā)送 3 個數(shù)據(jù)包,需要 1 個 RTT;關(guān)閉連接是“四次揮手”,4 個數(shù)據(jù)包需要 2 個 RTT。
而 HTTP 的一次簡單“請求 - 響應(yīng)”通常只需要 4 個包,如果不算服務(wù)器內(nèi)部的處理時間,最多是 2 個 RTT。這么算下來,浪費的時間就是“3÷5=60%”,有三分之二的時間被浪費掉了,傳輸效率低得驚人。

單純地從理論上講,TCP 協(xié)議你可能還不太好理解,我就拿打卡考勤機來做個形象的比喻吧。
假設(shè)你的公司買了一臺打卡機,放在前臺,因為這臺機器比較貴,所以專門做了一個保護罩蓋著它,公司要求每次上下班打卡時都要先打開蓋子,打卡后再蓋上蓋子。
可是偏偏這個蓋子非常牢固,打開關(guān)閉要費很大力氣,打卡可能只要 1 秒鐘,而開關(guān)蓋子卻需要四五秒鐘,大部分時間都浪費在了毫無意義的開關(guān)蓋子操作上了。
可想而知,平常還好說,一到上下班的點在打卡機前就會排起長隊,每個人都要重復(fù)“開蓋 - 打卡 - 關(guān)蓋”的三個步驟,你說著急不著急。
在這個比喻里,打卡機就相當于服務(wù)器,蓋子的開關(guān)就是 TCP 的連接與關(guān)閉,而每個打卡的人就是 HTTP 請求,很顯然,短連接的缺點嚴重制約了服務(wù)器的服務(wù)能力,導(dǎo)致它無法處理更多的請求。
長連接
針對短連接暴露出的缺點,HTTP 協(xié)議就提出了“長連接”的通信方式,也叫“持久連接”(persistent connections)、“連接?;?rdquo;(keep alive)、“連接復(fù)用”(connection reuse)。
其實解決辦法也很簡單,用的就是“成本均攤”的思路,既然 TCP 的連接和關(guān)閉非常耗時間,那么就把這個時間成本由原來的一個“請求 - 應(yīng)答”均攤到多個“請求 - 應(yīng)答”上。
這樣雖然不能改善 TCP 的連接效率,但基于“分母效應(yīng)”,每個“請求 - 應(yīng)答”的無效時間就會降低不少,整體傳輸效率也就提高了。
這里我畫了一個短連接與長連接的對比示意圖。

在短連接里發(fā)送了三次 HTTP“請求 - 應(yīng)答”,每次都會浪費 60% 的 RTT 時間。而在長連接的情況下,同樣發(fā)送三次請求,因為只在第一次時建立連接,在最后一次時關(guān)閉連接,所以浪費率就是“3÷9≈33%”,降低了差不多一半的時間損耗。顯然,如果在這個長連接上發(fā)送的請求越多,分母就越大,利用率也就越高。
繼續(xù)用剛才的打卡機的比喻,公司也覺得這種反復(fù)“開蓋 - 打卡 - 關(guān)蓋”的操作太“反人類”了,于是頒布了新規(guī)定,早上打開蓋子后就不用關(guān)上了,可以自由打卡,到下班后再關(guān)上蓋子。
這樣打卡的效率(即服務(wù)能力)就大幅度提升了,原來一次打卡需要五六秒鐘,現(xiàn)在只要一秒就可以了,上下班時排長隊的景象一去不返,大家都開心。
連接相關(guān)的頭字段
由于長連接對性能的改善效果非常顯著,所以在 HTTP/1.1 中的連接都會默認啟用長連接。不需要用什么特殊的頭字段指定,只要向服務(wù)器發(fā)送了第一次請求,后續(xù)的請求都會重復(fù)利用第一次打開的 TCP 連接,也就是長連接,在這個連接上收發(fā)數(shù)據(jù)。
當然,我們也可以在請求頭里明確地要求使用長連接機制,使用的字段是 Connection,值是 “keep-alive”。
不過不管客戶端是否顯式要求長連接,如果服務(wù)器支持長連接,它總會在響應(yīng)報文里放一個 “Connection: keep-alive” 字段,告訴客戶端:“我是支持長連接的,接下來就用這個 TCP 一直收發(fā)數(shù)據(jù)吧”。
不過長連接也有一些小缺點,問題就出在它的“長”字上。
因為 TCP 連接長時間不關(guān)閉,服務(wù)器必須在內(nèi)存里保存它的狀態(tài),這就占用了服務(wù)器的資源。如果有大量的空閑長連接只連不發(fā),就會很快耗盡服務(wù)器的資源,導(dǎo)致服務(wù)器無法為真正有需要的用戶提供服務(wù)。
所以,長連接也需要在恰當?shù)臅r間關(guān)閉,不能永遠保持與服務(wù)器的連接,這在客戶端或者服務(wù)器都可以做到。
在客戶端,可以在請求頭里加上“Connection: close”字段,告訴服務(wù)器:“這次通信后就關(guān)閉連接”。服務(wù)器看到這個字段,就知道客戶端要主動關(guān)閉連接,于是在響應(yīng)報文里也加上這個字段,發(fā)送之后就調(diào)用 Socket API 關(guān)閉 TCP 連接。
服務(wù)器端通常不會主動關(guān)閉連接,但也可以使用一些策略。拿 Nginx 來舉例,它有兩種方式:
- 使用“keepalive_timeout”指令,設(shè)置長連接的超時時間,如果在一段時間內(nèi)連接上沒有任何數(shù)據(jù)收發(fā)就主動斷開連接,避免空閑連接占用系統(tǒng)資源。
- 使用“keepalive_requests”指令,設(shè)置長連接上可發(fā)送的最大請求次數(shù)。比如設(shè)置成 1000,那么當 Nginx 在這個連接上處理了 1000 個請求后,也會主動斷開連接。
另外,客戶端和服務(wù)器都可以在報文里附加通用頭字段“Keep-Alive: timeout=value”,限定長連接的超時時間。但這個字段的約束力并不強,通信的雙方可能并不會遵守,所以不太常見。
隊頭阻塞
看完了短連接和長連接,接下來就要說到著名的“隊頭阻塞”(Head-of-line blocking,也叫“隊首阻塞”)了。
“隊頭阻塞”與短連接和長連接無關(guān),而是由 HTTP 基本的“請求 - 應(yīng)答”模型所導(dǎo)致的。
因為 HTTP 規(guī)定報文必須是“一發(fā)一收”,這就形成了一個先進先出的“串行”隊列。隊列里的請求沒有輕重緩急的優(yōu)先級,只有入隊的先后順序,排在最前面的請求被最優(yōu)先處理。
如果隊首的請求因為處理的太慢耽誤了時間,那么隊列里后面的所有請求也不得不跟著一起等待,結(jié)果就是其他的請求承擔了不應(yīng)有的時間成本。
還是用打卡機做個比喻。
上班的時間點上,大家都在排隊打卡,可這個時候偏偏最前面的那個人遇到了打卡機故障,怎么也不能打卡成功,急得滿頭大汗。等找人把打卡機修好,后面排隊的所有人全遲到了。
性能優(yōu)化
因為“請求 - 應(yīng)答”模型不能變,所以“隊頭阻塞”問題在 HTTP/1.1 里無法解決,只能緩解,有什么辦法呢?
公司里可以再多買幾臺打卡機放在前臺,這樣大家可以不用擠在一個隊伍里,分散打卡,一個隊伍偶爾阻塞也不要緊,可以改換到其他不阻塞的隊伍。
這在 HTTP 里就是“并發(fā)連接”(concurrent connections),也就是同時對一個域名發(fā)起多個長連接,用數(shù)量來解決質(zhì)量的問題。
但這種方式也存在缺陷。如果每個客戶端都想自己快,建立很多個連接,用戶數(shù)×并發(fā)數(shù)就會是個天文數(shù)字。服務(wù)器的資源根本就扛不住,或者被服務(wù)器認為是惡意攻擊,反而會造成“拒絕服務(wù)”。
所以,HTTP 協(xié)議建議客戶端使用并發(fā),但不能“濫用”并發(fā)。RFC2616 里明確限制每個客戶端最多并發(fā) 2 個連接。不過實踐證明這個數(shù)字實在是太小了,眾多瀏覽器都“無視”標準,把這個上限提高到了 6~8。后來修訂的 RFC7230 也就“順水推舟”,取消了這個“2”的限制。
但“并發(fā)連接”所壓榨出的性能也跟不上高速發(fā)展的互聯(lián)網(wǎng)無止境的需求,還有什么別的辦法嗎?
公司發(fā)展的太快了,員工越來越多,上下班打卡成了迫在眉睫的大問題。前臺空間有限,放不下更多的打卡機了,怎么辦?那就多開幾個打卡的地方,每個樓層、辦公區(qū)的入口也放上三四臺打卡機,把人進一步分流,不要都往前臺擠。
這個就是“域名分片”(domain sharding)技術(shù),還是用數(shù)量來解決質(zhì)量的思路。
HTTP 協(xié)議和瀏覽器不是限制并發(fā)連接數(shù)量嗎?好,那我就多開幾個域名,比如 shard1.chrono.com、shard2.chrono.com,而這些域名都指向同一臺服務(wù)器 www.chrono.com,這樣實際長連接的數(shù)量就又上去了,真是“美滋滋”。不過實在是有點“上有政策,下有對策”的味道。
小結(jié)
這一講中我們學(xué)習(xí)了 HTTP 協(xié)議里的短連接和長連接,簡單小結(jié)一下今天的內(nèi)容:
- 早期的 HTTP 協(xié)議使用短連接,收到響應(yīng)后就立即關(guān)閉連接,效率很低;
- HTTP/1.1 默認啟用長連接,在一個連接上收發(fā)多個請求響應(yīng),提高了傳輸效率;
- 服務(wù)器會發(fā)送“Connection: keep-alive”字段表示啟用了長連接;
- 報文頭里如果有“Connection: close”就意味著長連接即將關(guān)閉;
- 過多的長連接會占用服務(wù)器資源,所以服務(wù)器會用一些策略有選擇地關(guān)閉長連接;
- “隊頭阻塞”問題會導(dǎo)致性能下降,可以用“并發(fā)連接”和“域名分片”技術(shù)緩解。