實用TCP協議(1):TCP 協議簡介

傳輸控制協議(TCP,Transmission Control Protocol)是一種面向連接的、可靠的、基於字節流的傳輸層通信協議。TCP 協議假設下層協議可以提供簡單的不可靠數據報, 並在此基礎上構建可靠的端到端字節流服務。TCP 協議通常工作在 IP 協議上,依賴 IP 協議提供的地址和路由機制。

本文將介紹 TCP 協議的握手、揮手、流量控制、擁塞控制等基本機制。

TCP 包結構

  • 發送方端口
  • 接收方端口
  • 序列號(SEQ)
  • 確認號碼(Acknowledge Number):設置了 ACK 標誌位後有效,表示期待要收到下一個數據包的 SEQ
  • 資料偏移(offset): 表示數據段開始位置相對於 TCP 數據包開頭的偏移量,也是 TCP Header 的長度
  • 保留位: 目前不使用
  • 標誌位(Flag): 一共有 9bit, 對應位置1表示標誌位有效
    • ACK: 表示確認收到了發送方發送的數據, ACK=1 時 TCP Header 中的 ACK Number 字段有效。
    • PSH: 優先推送。接收方 TCP 應該儘快推送給接收應用程序,而不用等到 TCP 緩存填滿後再交付
    • RST: 重置連接。表示 TCP 連接中出現嚴重錯誤,需要釋放並重新建立連接。
    • SYN: 表示請求建立連接,SYN 意爲同步(synchronize), 即請求同步序列號。
    • FIN: 表示此報文段的發送方的數據已經發送完畢,並要求釋放 TCP 連接
  • 校驗和: 根據 TCP 包的頭部和數據段計算的校驗和,用於保證傳輸完整無誤

IP 協議的數據包大小有限、不保證送達也不保證送達的順序,如果需要發送大量數據就必須分爲多個數據包。發送方 會爲自己的每個 TCP 數據包分配一個序列號sequence number,SEQ)。

接收方收到數據包後會按照 SEQ 將數據包去重並排序,然後接收方對已成功收到的包發回一個相應的確認包(ACK)。如果發送方在合理的往返時延(RTT)內未收到確認,那麼對應的數據包就被假設爲已丟失並進行重傳。

acknowledge number 表示期望收到的下一個數據包的序列號,換句話說 acknowledge number 之前的數據包已經全部收到。這種確認方法稱爲累積確認。累積確認在丟包時效率很低,假設通過10個分組發出了1萬個字節的數據。如果第一個分組丟失,在純粹的累計確認協議下,接收方不能說它成功收到了1,000到9,999字節,但未收到包含0到999字節的第一個分組。因而,發送方可能必須重傳所有1萬個字節。

因此,RFC 2018 中引入了選擇確認機制(selective acknowledgment,SACK),允許接收方向發送方返回多個 SACK block, 每個 SACK block 表示一段已經成功收到的連續範圍的開始與結束字節序號。

三次握手

TCP 連接建立過程需要發送三個 TCP 包,這個過程被稱爲三次握手:

  1. 服務端 bind 端口並開始 listen
  2. 客戶端調用 connect 開始建立連接:客戶端發送 SYN 包並帶上初始序列號, 並進入 SYN_SENT 狀態
  3. 服務端發送 ACK 確認收到了客戶端的SYN, 並且發送自己的 SYN 以及自己的初始序列號,並進入 SYN_RCVD 狀態。 這裏的ACK 和 SYN 是在同一個 TCP 包中發送的
  4. 客戶端確認服務端的SYN。 至此雙方都獲得了對方的序列號,連接成功建立

四次揮手

TCP 連接斷開過程需要發送四個 TCP 包, 這個過程被稱爲四次揮手:

  1. 主動方調用 close 開始關閉連接(客戶端和服務端都可以主動斷開連接, 下圖以客戶端主動斷開爲例): 主動方發送 FIN 包並進入 FIN_WAIT_1,表示己方數據發送完成。此後主動方還可以繼續接收數據,但是無法繼續發送數據。
  2. 被動方對 FIN 包發送 ACK 並進入 CLOSE_WAIT 狀態。此狀態下,被動方可以繼續發送數據。
  3. 被動方數據發送完成,調用 close 發送 FIN 包, 並進入 LAST_ACK 狀態等待對 FIN 包的 ACK.
  4. 主動方對 FIN 包發送 ACK 並進入 TIME_WAIT 狀態, 在此狀態等待 2 MSL 後連接關閉
  5. 被動方收到 ACK 後連接關閉

在握手過程中服務端可以將 ACK 和 SYN 在同一個包中發送, 因此握手過程中的兩對 SYN-ACK 只需要三次傳輸即可。在揮手過程中,被動方收到主動方的 FIN 包後可能仍有數據需要發送,所以不能將 FIN 和 ACK 在同一個包中發出使得揮手過程必須要經過四次傳輸。

TIME WAIT

上文中提到的 MSL 是指 Max Segment Lifetime,它是一個 TCP 包在網絡中最大的生存時間超過 MSL 的 TCP 包會被丟棄,MSL 的推薦值爲兩分鐘。

被動方在收到 LAST ACK 會一直嘗試重傳 FIN 包直到到達最大重試次數。 若主動方在 TIME WAIT 狀態等待時間過短, 在收到重傳的 FIN 包時連接已經關閉,則主動方會向被動方返回 RST,此時被動方會認爲遇到了錯誤,因此無法正常關閉連接。

TIME_WAIT至少需要持續 2MSL 時長,這2個MSL中的第一個MSL是爲了等主動方發出去的 LAST ACK從網絡中消失,而第二MSL是爲了等在被動方收到ACK之前的一剎那可能重傳的FIN報文從網絡中消失。

2MSL 並不能絕對保證屬於本連接的 TCP 包在網絡中消失,比如我們利用防火牆攔截主動方發送的所有 LAST ACK 包,那麼被動方會一直重傳 FIN 包。最後一個 FIN 包在網絡中消失的時間只取決於被動方何時停止重傳,與主動方 TIME WAIT 狀態持續時間無關。

Linux 系統中 TIME_WAIT 的時間爲固定的 60 秒,由內核代碼裏的 TCP_TIMEWAIT_LEN 宏定義, 只有重新編譯內核纔可以修改。

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT state, about 60 seconds  */

TIME WAIT 狀態的連接會佔用端口, 導致系統無法建立新的 TCP 連接。在本系列的後續文章中我們將介紹如何避免出現過多 TIME WAIT 狀態的連接。

擁塞控制

發送數據時當然是越快越好,但是發送速度超過了網絡的最大承載能力就會發生丟包。TCP 的目標是儘可能的利用網絡承載能力,一方面不浪費帶寬,另一方面儘量避免丟包。TCP 協議中控制如何合理利用網絡的機制被稱爲擁塞控制,接下來我們來了解一下擁塞控制所涉及的四個算法:慢開始、擁塞避免、快重傳和快恢復。

慢開始 - 擁塞避免

發送方維持一個叫做擁塞窗口 CWND(congestion window)的狀態變量,當在網絡中傳輸的數據量(未ACK的數據量)到達 CWND 時就暫停發送。

慢開始算法(SlowStart)將 CWND 的初始值設置的非常小,每一輪成功發送-確認都會使得 CWND 加倍,直到 CWND 達到慢開始算法的閾值 SSThresh 後轉爲擁塞避免。

慢開始算法的慢是指初始傳輸速度很慢,但是傳輸速度會以指數快速增長。

在達到 SSThresh 之後轉爲使用擁塞避免算法使 CWND 線性增長(加法增大), 避免繼續快速增長導致網絡擁塞。

無論是在慢開始階段還是在擁塞避免階段,只要發送方沒有及時收到 ACK 都會判斷爲出現了網絡擁塞。遇到網絡擁塞後,發送方會把 SSThresh 設爲當前 CWND 的一半, 把 CWND 設爲初始值重新執行慢開始算法,這個操作稱爲“乘法減少”。

乘法減少做的目的就是要迅速減少發送到網絡中的數據,使得發生擁塞的路由器有足夠時間把隊列中積壓的數據處理完畢。

快重傳

快重傳(Fast Retransmit)

  1. 要求接收方每收到一個失序的報文段後就立即發出重複確認而不是等待自己發送數據時才捎帶確認
  2. 發送方只要一連收到三個重複確認就立即重傳對方尚未收到的報文段,而不必等待設置的重傳計時器到期

快重傳使得發送方迅速重傳丟失的數據包減少等待時間。

快恢復

當發送方連續收到三個重複確認時,就執行“乘法減小”算法,把 ssthresh 減半(爲了預防網絡發生擁塞), 但是接下來並不執行慢開始算法。

考慮到如果網絡出現擁塞的話就不會收到好幾個重複的確認,所以發送方現在認爲網絡可能沒有出現擁塞。所以此時不執行慢開始算法,而是將 CWND 設置爲ssthresh減半後的值,然後執行擁塞避免算法,使 CWND 緩慢增大。

TCP Reno 版本引入了快恢復與快重傳機制

流量控制

若發送過快導致超出了接收方處理能力同樣會導致丟包重傳,因此我們需要控制發送方的發送速率避免發送速率超過接收方處理能力。對發送方發送速率的控制,我們稱之爲流量控制。

接收方會在返回的 ACK 包的 WIN 字段中告知自己接收窗口(Receiver Window, RWND) 大小, 發送方會取接收窗口 RWND 和擁塞窗口 CWND 中的最小值(min(CWND, RWND))作爲自己的發送窗口,當未確認的數據量到達發送窗口規定的上限時便暫停發送。

當發送者收到了一個窗口爲0的應答,發送者便停止發送,等待接收者的下一個應答。但是如果這個窗口不爲0的應答在傳輸過程丟失,發送者一直等待下去,而接收者以爲發送者已經收到該應答,等待接收新數據,這樣雙方就相互等待,從而產生死鎖。

爲了避免流量控制引發的死鎖,TCP使用了持續計時器。每當發送者收到一個零窗口的應答後就啓動該計時器。時間一到便主動發送報文詢問接收者的窗口大小。若接收者仍然返回零窗口,則重置該計時器繼續等待;若窗口不爲0,則表示應答報文丟失了,此時重置發送窗口後開始發送,這樣就避免了死鎖的產生。

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