網路編程-TCP長連接和短連接
TCP是一個面向連接的協議。無論哪一方向另一方發送數據之前,都必須先在雙方之間建立一條連接。下面會介紹一個TCP連接是如何建立的以及通訊結束後是如何終止的。
一、TCP連接的建立與終止
1.1 建立連接協議
- 請求端(通常稱為客戶)發送一個
SYN
段指明客戶打算連接的伺服器的埠,以及初始序號(ISN
,在這個例子中為1415531521)。這個SYN
段為報文段1。 - 伺服器發回包含伺服器的初始序號的
SYN
報文段(報文段2)作為應答。同時,將確認序號設置為客戶的ISN
加1以對客戶的SYN
報文段進行確認。一個SYN
將佔用一個序號。 - 客戶必須將確認序號設置為伺服器的
ISN
加1以對伺服器的SYN
報文段進行確認(報文段3)。
這三個報文段完成連接的建立。這個過程也稱為三次握手(three-way handshake)。
1.2 連接終止協議
建立一個連接需要三次握手,而終止一個連接要經過4次握手。這由TCP的半關閉(halfclose)造成的。既然一個TCP連接是全雙工(即數據在兩個方向上能同時傳遞),因此每個方向必須單獨地進行關閉。這原則就是當一方完成它的數據發送任務後就能發送一個FIN
來終止這個方向連接。當一端收到一個FIN
,它必須通知應用層另一端已經終止了那個方向的數據傳送。發送FIN
通常是應用層進行關閉的結果。
當伺服器收到這個FIN
,它發回一個ACK
,確認序號為收到的序號加1(報文段5)。和SYN一樣,一個FIN
將佔用一個序號。同時TCP伺服器還嚮應用程式(即丟棄伺服器)傳送一個文件結束符。接著這個伺服器程式就關閉它的連接,導致它的TCP端發送一個FIN
(報文段6),客戶必須發回一個確認,並將確認序號設置為收到序號加1(報文段7)。
1.3 序列號(seq)和確認號(ACK)
1.3.1 序列號(seq)
序列號(Sequence Number)欄位標識了TCP發送端到TCP接收端的數據流的一個位元組,該位元組代表著包含該序列號的報文段的數據中的第一個位元組
1.3.2 初始化序列號(ISN)
- 當建立一個新連接時,從客戶機發送至伺服器的第一個報文段的SYN位欄位被啟用。 這樣的報文段稱為SYN報文段,或簡單地稱為SYN。然後序列號欄位包含了在本次連接的這個方向上要使用的第一個序列號,後續序列號和返回的ACK號也在這個方向上(回想一 下,連接都是雙向的)。注意這個數字不是0和1,而是另一個數字,經常是隨機選擇的,稱為初始序列號(ISN)。ISN不是0和1,是因為這是一種安全措施(在後面「TCP連接管理」中介紹)。發送在本次連接的這個方向上的數據的第一個位元組的序列號是ISN加1,因為SYN位欄位會消耗一個序列號。正如我們稍後將見到的,消耗一個序列號也意味著使用重傳進行可靠傳輸。因此,SYN和應用程式位元組(還有FIN,稍後我們將會見到)是被可靠傳輸的。不消耗序列號的ACK則不是。
- 在發送用於建立連接的SYN之前,通訊雙方會選擇一個初始序列號。初始序列號會隨時間而改變,因此每一個連接都擁有不同的初始序列號
- [RFCO793]指出初始序列號可被視為一個32位的計數器。該計數器的數值每4微秒加1。此舉的目的在於為一個連接的報文段 安排序列號,以防止出現與其他連接的序列號重疊的情況。尤其對於同一連接的兩個不同實例而言,新的序列號也不能出現重疊的情況。
1.3.3 確認號(ACK)
- 確認號欄位(也簡稱ACK號或ACK欄位)包含的值是該確認號的發送方期待接收的下一個序列號。即最後被成功接收的數據位元組的序列號加1
- 這個欄位只有在ACK位欄位被啟用的情況下才有效,這個ACK位欄位通常用於除了初始和末尾報文段之外的所有報文段。發送一個ACK與發送任何一個TCP報文段的開銷是一樣的,因為那個32位的ACK號欄位一直都是頭部的一部分,ACK位欄位也一樣。
二、TCP長連接
-
長連接意味著進行一次數據傳輸後,不關閉連接,長期保持連通狀態。如果兩個應用程式之間有新的數據需要傳輸,則直接復用這個連接無需再建立一個新的連接。
-
長連接的優勢是在多次通訊中可以省去連接建立和關閉連接的開銷,並且從總體上來看,進行多次數據傳輸的總耗時更少。缺點是需要花費額外的精力來保持這個連接一直是可用的,因為網路抖動、伺服器故障等都會導致這個連接不可用,甚至是由於防火牆的原因。所以,一般我們會通過下面這幾種方式來做「保活」工作,確保連接在被使用的時候是可用狀態:
-
利用 TCP 自身的保活(Keepalive)機制來實現,保活機制會定時發送探測報文來識別對方是否可達。一般的默認定時間隔是2小時,可以根據自己的需要在作業系統層面去調整這個間隔,不管是 linux 還是 windows 系統。
-
上層應用主動的定時發送一個小數據包作為「心跳」,探測是否能成功送達到另外一端。 保活功能大多數情況下用於服務端探測客戶端的場景,一旦識別客戶端不可達,則斷開連接,緩解服務端壓力。
-
三、TCP短連接
-
短連接意味著每一次的數據傳輸都需要建立一個新的連接,用完再馬上關閉它。下次再用的時候重新建立一個新的連接,如此反覆。
它的優勢是由於每次使用的連接都是新建的,所以基本上只要能夠建立連接,數據就大概率能送達到對方。並且哪怕這次傳輸出現異常也不用擔心影響後續新的數據傳輸,因為屆時又是一個新的連接。缺點是每個連接都需要經過三次握手和四次握手的過程,耗時大大增加。 -
短連接還有一個致命的缺點。我們回到前面提到的維基百科對 socket 的定義,其中說到 socket 包含通訊協議、目標地址、狀
態等。實際當你在基於 socket 進行開發的時候,這些包含的具體資源主要就是這 5 個:源 IP、源埠、目的 IP、目的埠、協議,
有個專業的叫法稱之為「五元組」。在一台電腦上只要這五元組的值不重複,那麼連接就可以被建立。然而一台電腦最多只能開啟
65535個埠,如果現在兩個進程之間需要通訊,作為服務端的 IP 和埠必然是固定的,因此單個客戶端理論上最多只能與服務端同時建立65535個 socket 連接。如果除去作業系統和其它進程所佔用的埠,實際還會更少。所以,一旦使用不當,在很短的時間內建立了大量連接,埠很容易被佔用完。這不但會導致自身無法正常工作,還會影響到同一台電腦上的其它進程。
四、HTTP的長連接和短連接
-
HTTP1.1規定了默認保持長連接(HTTP persistent connection ,也有翻譯為持久連接),數據傳輸完成了保持TCP連接不斷開(
不發RST包、不四次握手),等待在同域名下繼續用這個通道傳輸數據;相反的就是短連接。HTTP首部的
Connection: Keep-alive
是HTTP1.0瀏覽器和伺服器的實驗性擴展,當前的HTTP1.1 RFC2616文檔沒有對它做說明,
因為它所需要的功能已經默認開啟,無須帶著它,但是實踐中可以發現,瀏覽器的報文請求都會帶上它。如果HTTP1.1版本的HTTP請求報文不希望使用長連接,則要在HTTP請求報文首部加上Connection: close。 -
長連接的過期時間
客戶端的長連接不可能無限期的拿著,會有一個超時時間,伺服器有時候會告訴客戶端超時時間,譬如:Keep-Alive: timeout=20,
表示這個TCP通道可以保持20秒。另外還可能有max=XXX,表示這個長連接最多接收XXX次請求就斷開。對於客戶端來說,如果伺服器沒有告訴客戶端超時時間也沒關係,服務端可能主動發起四次握手斷開TCP連接,客戶端能夠知道該TCP連接已經無效;另外TCP還有心跳包來檢測當前連接是否還活著,方法很多,避免浪費資源。 -
長連接的數據傳輸完成識別
使用長連接之後,客戶端、服務端怎麼知道本次傳輸結束呢?兩部分:1是判斷傳輸數據是否達到了Content-Length指示的大小;2動態生成的文件沒有Content-Length,它是分塊傳輸(chunked),這時候就要根據chunked編碼來判斷,chunked編碼的數據在最後有一個標明長度為0的chunk塊,表示本次傳輸數據結束。補充:
chunk編碼將數據分成一塊一塊的發生。Chunked編碼將使用若干個Chunk串連而成,由一個標明長度為0的chunk標示結束。每個Chunk分為頭部和正文兩部分,頭部內容指定正文的字元總數(十六進位的數字)和數量單位(一般不寫),正文部分就是指定長度的實際內容,兩部分之間用回車換行(CRLF)隔開。在最後一個長度為0的Chunk中的內容是稱為footer的內容,是一些附加的Header資訊(通常可以直接忽略)