聊聊HTTP連接的那些事-讀權威HTTP筆記

本文多圖,流量打開需謹慎!
HTTP協議作爲我們平時開發過程中使用最爲廣泛的網絡協議,相信大家至少對它都有簡單的瞭解,它也是很多互聯網公司平時面試時經常問的點,比如各種HTTP方法、狀態碼字、持久連接等。今天我們就來聊聊HTTP協議在客戶端和服務器連接過程中那些有意思的點。

大家都知道,HTTP協議是基於TCP協議的協議,所以我們如果要建立一個HTTP連接首先需要建立一根TCP連接。爲了更加直觀的展示這個過程,我這裏使用Telnet工具來模擬一下HTTP請求和響應的過程:

  • 連接到目標主機(www.maoyan.com)的HTTP端口80

    我們看到這個時候TCP連接已經建立,服務器在等待我們往TCP管道中寫入數據。
  • 按照HTTP規範,寫入請求

    我們可以看到,寫入了一個GET請求,客戶端HTTP版本是1.1,請求的url是/index.html,輸入之後,後臺直接返回了我們這個請求的結果,結果的狀態碼是301,表示這個頁面已經重定向了,注意一點,服務器返回了重定向的頁面之後,我們的TCP連接並沒有結束。

一 尷尬的Host頭部

剛學習HTTP協議的時候,我就對Host首部很疑惑,存在的意義是啥?
在上面的栗子中,可能有些同學已經發現了www.maoyan.com這個主機地址寫了兩次,一次是是建立TCP的時候,一次是我們寫入HTTP請求時,放到了Host頭部。

在HTTP 1.0協議裏面不寫Host是沒有任何問題的,大家可以自己去試一下,但是在HTTP 1.1裏面如果不寫Host首部,會直接報400錯誤。

我們在發起TCP連接的時候已經輸入了www.maoyan.com,爲什麼還需要用一個Host首部再寫入一次呢?原來我們考慮一下HTTP被設置代理的情況,比如我們設置了路由代理,我們的HTTP客戶端的流量都會被髮送到代理服務器。客戶端會直接和代理服務器建立TCP連接,如果在HTTP層面不提供目標主機的地址,代理服務器蒙圈了,這個請求我TM發給誰?所以在HTTP 1.1中規定所有的HTTP請求都需要指定Host首部。

二 強大的持久化連接

在栗子中我們看到服務器返回一個重定向的響應之後,並沒有關閉TCP連接,如果我們接着寫入HTTP請求,這條連接還是會返回數據:


這就是HTTP協議的持久化連接,對同一個主機的多個HTTP請求可以複用同一個TCP連接。相信大家都瞭解HTTP的持久化連接的作用,它可以減少TCP連接的重複創建,特別是對於那種通信量非常小的HTTP請求和響應,大部分的傳輸流量都浪費在建立和斷開連接上面了。
相信簡單瞭解過HTTP協議的同學對這個理解都沒有問題,那我們聊聊另外一個持久連接的優勢:TCP擁塞窗口冷卻

2.1 TCP擁塞窗口

我們在學習計算機網絡時,肯定都學過在TCP層存在兩個叼炸天的控制策略:流量控制和擁塞控制。所謂流量控制指的是發送方需要調整自己的發送窗口,以免接收方來不及處理數據而導致數據丟失和重傳,典型的就是滑動窗口來做流量控制。而擁塞控制考慮的更加複雜,它主要用於依據當前網絡環境的狀況來動態調整我們發送數據的速度,從而避免過多數據注入到網絡中,導致整個網絡性能下降。所以在TCP層面進行數據傳輸的窗口大小是由發送窗口和擁塞窗口共同決定的。

我們這裏暫時只討論擁塞控制對於HTTP持久連接的影響(因爲發送窗口和當時服務器的負載有關係,和協議層面關係不大)。
目前最常見TCP的擁塞控制算法是慢啓動算法+擁塞避免算法,一圖勝千言:


相信學過計算機網絡(謝希仁版)的同學對於這個圖真是百感交集,哈哈,老師喜歡考,沒想到工作之後還有人貼出來..

所謂慢啓動,就是擁塞窗口開始非常小,依據對收到ACK報文來預測網絡狀態的性能,如果網絡較好,擁塞窗口才會慢慢增加。然而如果HTTP不啓動持久連接,你發現問題了麼?
是的,每次連接都需要重新走一邊慢啓動,來逐步增加窗口,這不是坑爹麼。如果使用持久連接,那麼擁塞窗口在網絡較好的情況下,每次新的HTTP報文在TCP層面直接有了非常合適的擁塞窗口。
雖然持久連接有這麼多好處,但是HTTP其實一開始是不支持的。一直到HTTP 1.0版本的時很多人都覺得不能忍,於是把它寫入了擴展協議,實際上大部分HTTP 1.0 Web服務器都支持了持久連接。然而在1.0和1.1版本對於持久連接支持還略有區別,下面分開來說。

2.2 1.0 VS 1.1 持久連接

在HTTP1.0協議中是沒有支持持久連接的說明,但是大家自覺的擴展了一個名爲Connection,如果客戶端往裏面寫入了keep-alive值,那麼服務器如果能夠理解這個語意,回返回Connection:keep-alive來告訴客戶端我們可以保持TCP連接。但是在1.0中默認是不支持持久連接的,我們看看效果:


可以看到服務器返回了數據之後,TCP連接就被關閉了。

然而在HTTP 1.1中,持久連接被默認支持,參考我的第一個栗子。如果你不想維持長連接,那麼可以手動往頭部中加入Connection:close來關閉TCP連接。

2.3 鬱悶的Proxy-Connection頭部

由於1.0版本中依賴於Connection擴展頭部來進行持久化,但是在網絡中存在很多運行舊版本的HTTP應用,它們不一定認識Connection字段的含義,比如下圖中的笨代理(圖片來自網絡,如有侵權,馬上刪除):


客戶端支持持久連接,然而中間的代理使用的舊版本的HTTP協議,所以它並不知道這個含義,然後直接將所有的頭部都轉發給服務器,服務器看到客戶端要建立持久連接,當然欣然同意,客戶端看到服務器返回了Connection:keep-alive,也不會去關閉持久連接。然而此時的代理服務器認爲這次通信已經結束了,以後客戶端發送給服務器的任何數據它都不會轉發給服務器了。這種情況就比較尷尬了,只有等着客戶端 或者 服務器有一方TCP超時,才能斷開連接,然而大好資源已經被浪費了。
爲了解決這個問題,網景公司的大神們只得屁顛屁顛的出補丁,這就是我們今天說的Proxy-Connection,它能工作的前提是如果一個代理不知道Connection:keep-alive,自然也就不理解Proxy-Connection,所以它會直接把請求發送給服務器,服務器看到的Proxy-Connection,所以不會創建長連接。
如果代理知道Connection:keep-alive語意,那麼它會把Proxy-Connection轉換成Connection:keep-alive。COOL!

雖然上面的方案看起來是解決問題了,但是你可以想想如果客戶端和服務器直接的通道是這樣的:
Client--->聰明代理--->笨代理-->Server
gg了,問題依舊~~

三 傳輸邊界

剛接觸HTTP時,經常想兩個問題:

  • 建立連接之後,客戶端開始往TCP連接中寫入數據,那什麼時候算輸入的結束,Service可以開始響應?
  • 持久化連接之後,服務器返回了數據,客戶端怎麼知道服務器數據已經寫完了?
    原來在HTTP協議已經對這種傳輸邊界進行了定義,本質上就是通信雙方需要知道傳輸的數據一共有多大,最傳統的做法就是加一個Content-Length頭部用來描述傳輸中的主體大小,那麼接收方只有在收到頭部描述的這麼多字節之後纔會開始進行請求解析:


我們看到和第一個栗子不同的時候,這裏我多輸入了5個CRLF回車換行,服務器才認爲數據已經寫入完畢,開始解析客戶端的請求。

但是Content-Length其實有侷限性,就是在傳輸數據之前我需要知道需要傳輸的東西大小,對於傳輸靜態文件或者已知數據來說,這通常來說沒什麼問題。但是考慮一種場景,比如服務器需要對一個視頻文件轉碼之後傳送給客戶端,這個文件有10G,轉碼之後的大小肯定之前不知道,如果需要把文件完全轉換之後纔給客戶端傳送延遲就會很長同時也會浪費服務器資源,最理想的情況是服務器一邊轉碼一邊給客戶端進行傳輸。

chunked編碼就應運而生,它將數據進行分塊,每塊都包含數據大小和數據本身,每個chunk的格式如下:

長度 (十六進制表示)CRLF
數據
比如我們有如下HTTP報文:

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

25 //注意,這裏是十六進制,表示37個字節
This is the data in the first chunk

1A
and this is the second one
0  //0字節表示結束

Java構建這個響應的代碼如下:

StringBuilder sb = new StringBuilder();
sb.append("HTTP/1.1 200 OK\r\n");
sb.append("Content-Type: text/plain\r\n");
sb.append("Transfer-Encoding: chunked\r\n\r\n");
sb.append("25\r\n");        
sb.append("This is the data in the first chunk\r\n"); // 37 bytes of payload
        // (conveniently consisting of ASCII characters only)
sb.append("\r\n1A\r\n");
sb.append("and this is the second one"); // 26 bytes of payload
        // (conveniently consisting of ASCII characters only)
sb.append("\r\n0\r\n\r\n");

注意,關於傳輸邊界的問題對於請求實體和響應實體都有效。

最後插播一條廣告,貓眼電影正在招Android中高級工程師,有興趣的同學可以戳拉勾JD,也可以簡歷發我郵箱 "pengliang".concat("02").concat"@".concat("maoyan.com")內推,公司目前正處於快速上升期,有不少技術項目可以挑戰。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章