最近在閱讀Go語言的Http庫,以前總覺得這些基礎代碼,會很高大上很難懂,所以連去挑戰下的勇氣都沒有。工作了幾年之後,總算是有信心來挑戰下了,不過目前進展還是很慢,說明目前的實力還不足。

本來想著用Java照著Go的代碼仿寫一份的,寫著寫著,就感到越來越難了。因為對整體代碼都沒有認知,就直接開始,一邊閱讀代碼,一邊仿寫,加上Go和Java都不能說熟到遂心應手的程度,寫著寫著,就變成純粹用Java模擬Go的語法了。遇到愈來愈多無法直接模仿的代碼和類庫,仿寫也就進行不下去了。

雖然是放棄了仿寫,不過我還是覺得用另一門語言照著實現一份代碼,進步會非常快。譬如之前自己寫代碼,就很少會用那麼複雜的多線程同步。

雖說放棄了仿寫,但是單純的閱讀代碼,其實並沒有什麼意義,或者說,會覺得自己看懂了就一帶而過了,事後也就忘了。於是決定重操舊業,邊閱讀代碼,邊寫文章。

寫文章讓我心中倍感踏實,每次學點什麼,如果我沒有寫成一篇總結文章,就覺得自己並沒有學到,感覺也記不住,很快就忘了,譬如TCP的基礎知識,我已經閱讀過兩本書了,現在還是很不懂。

如果後面自己還能堅持的話,我還是打算用Java把這份Go的http庫給仿寫出來。

下面開始正文:

首先第一個問題,http的keep-alive和tcp的keepalive,有沒有關係?

答案自然是沒有關係。

問題二: TCP 的 keepalive 是什麼意思,用途是什麼?

TCP實現中包含一個keepalive定時器,當一條TCP沒有數據流通時,定時器開始活動,直至為0時,服務端這邊會向客戶端這邊發送一個不帶數據的ACK請求,如果收到回復,則表明這條連接是活的。如果沒有收到回復,伺服器端這邊會發送多次ack,到一定次數之後還沒有收到回復,就認定這條連接已經死掉了,直接關閉掉。

具體作用就兩個:探測連接的死活,以及保持連接的活動,保證不被防火牆之類的服務殺死(比如防火牆認為一條連接超過一定時間沒有活動就要殺死掉,只要設置keepalive短於這個時間即可保活)。

Linux的TCP默認是沒有開啟keepalive的,因為大量的keepalive會浪費伺服器資源。keepalive也可以在代碼里建立TCP的時候開啟,具體可以看這篇 TCP KeepAlive How To

問題三:HTTP的keep-alive是什麼意思?用途是什麼?

http的keep-alive其實是HTTP 1.0的產物,HTTP 1.1 之後,所有的http連接都默認是keep-alive的, 所謂的keep-alive,就是一條tcp連接,在處理完一次http事務之後,不關閉此tcp連接,用於下一次的http事務。譬如客戶端和 伺服器端建立一個連接,用於客戶端向伺服器端發送圖片。如果不是keep-alive的http連接,那麼在發送完一張圖片之後,就關閉 掉這條連接了。而keep-alive的連接,則在發送完一張圖片後,放回到連接池裡,客戶端要再次發送圖片到伺服器了,就直接從連接池拿出這條連接,而不是從零開始建立一條連接(http的底層是TCP,從零建立一條tcp連接,三次握手,慢啟動等導致非常耗時)。

對於HTTP 1.1,並不需要指定keep-alive了。但是,假如你希望這條http連接完成一次事務之後(發送完這張圖片),就關閉這條連接(實際就是關閉底層的tcp連接),那麼可以在http請求的頭部加入 Connection: close,而且可以在任何時候加入這個頭部,比如發第一張圖片不加,第二張圖片加這個頭部。

不發送 Connection:close, 並不意味著伺服器承諾永遠不關閉這條連接,實際上空閑超過一定時間一定數量之後,就會關閉掉。

問題四:重用tcp連接不是很正常的事情么,為什麼http的keep-alive會有那麼複雜的歷史,要廢那麼多口水來解釋呢?

重用連接之後,對於一次http事務是否完成了,就需要稍微複雜點的判斷了。同一條tcp通道,你灌進來一張圖片的數據之後又再灌進來一張圖片,對於另一端的服務,它如何確定第一張圖片的數據讀到哪個位置就完成了呢?如果連接不是keep-alive的,那麼只要把tcp通道里的所有數據都讀完就可以了。

目前http在持久化連接上,區分兩次http事務的方式有兩種,一個是通過Content-Length這個header來判斷主體內容的長度,比如我通過http請求發送了一個 Hello, 那麼這個Content-Length就是5。讀完之後,就是下次事務的開始了。另一種則是通過chunk, 即分塊編碼來實現。

如果Content-Length設置錯誤或者沒有設置,那麼接收端就應該質疑這個長度的正確性。用Go的http庫測試了下,發現用戶是 無法設置Content-Length這個header的。

但有一種情況可以不設置Content-Length,即使用傳輸編碼Transfer-Encoding:chunked的時候,歷史上Transfer-Encoding支持 多種編碼,但是目前最新的規範里,只支持chunked編碼,即分塊編碼一種。

當在報文的頭部加入Transfer-Encoding:chunked的時候,報文就會被分塊,每塊包含長度值,數據,以及分隔符CRLF,最後一個塊長度必須為0,如下圖:

接收端就可以根據長度值讀取數據,以及最後一個長度為0的塊來判定接收結束。

分塊傳輸有許多的用途和好處,譬如有些內容是持續產生的,事先並不知道長度是多長,用分塊傳輸就可以邊傳輸邊讀取新數據發送了。還有一些大文件,如果先將整個文件壓縮好了再傳輸,對用戶很不友好(感覺等待的時間長),就可以採用分塊傳輸和內容 編碼結合(Content-Encoding),一塊塊的壓縮,一塊塊的傳輸。


推薦閱讀:
相关文章