傳輸控制協議(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 包,這個過程被稱為三次握手:
- 服務端 bind 埠並開始 listen
- 客戶端呼叫 connect 開始建立連線:客戶端傳送 SYN 包並帶上初始序列號, 並進入 SYN_SENT 狀態
- 服務端傳送 ACK 確認收到了客戶端的SYN, 並且傳送自己的 SYN 以及自己的初始序列號,並進入 SYN_RCVD 狀態。 這裡的ACK 和 SYN 是在同一個 TCP 包中傳送的
- 客戶端確認服務端的SYN。 至此雙方都獲得了對方的序列號,連線成功建立
四次揮手
TCP 連線斷開過程需要傳送四個 TCP 包, 這個過程被稱為四次揮手:
- 主動方呼叫 close 開始關閉連線(客戶端和服務端都可以主動斷開連線, 下圖以客戶端主動斷開為例): 主動方傳送 FIN 包並進入 FIN_WAIT_1,表示己方資料傳送完成。此後主動方還可以繼續接收資料,但是無法繼續傳送資料。
- 被動方對 FIN 包傳送 ACK 並進入 CLOSE_WAIT 狀態。此狀態下,被動方可以繼續傳送資料。
- 被動方資料傳送完成,呼叫 close 傳送 FIN 包, 並進入 LAST_ACK 狀態等待對 FIN 包的 ACK.
- 主動方對 FIN 包傳送 ACK 並進入 TIME_WAIT 狀態, 在此狀態等待 2 MSL 後連線關閉
- 被動方收到 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)
- 要求接收方每收到一個失序的報文段後就立即發出重複確認而不是等待自己傳送資料時才捎帶確認
- 傳送方只要一連收到三個重複確認就立即重傳對方尚未收到的報文段,而不必等待設定的重傳計時器到期
快重傳使得傳送方迅速重傳丟失的資料包減少等待時間。
快恢復
當傳送方連續收到三個重複確認時,就執行“乘法減小”演算法,把 ssthresh 減半(為了預防網路發生擁塞), 但是接下來並不執行慢開始演算法。
考慮到如果網路出現擁塞的話就不會收到好幾個重複的確認,所以傳送方現在認為網路可能沒有出現擁塞。所以此時不執行慢開始演算法,而是將 CWND 設定為ssthresh減半後的值,然後執行擁塞避免演算法,使 CWND 緩慢增大。
TCP Reno 版本引入了快恢復與快重傳機制
流量控制
若傳送過快導致超出了接收方處理能力同樣會導致丟包重傳,因此我們需要控制傳送方的傳送速率避免傳送速率超過接收方處理能力。對傳送方傳送速率的控制,我們稱之為流量控制。
接收方會在返回的 ACK 包的 WIN 欄位中告知自己接收視窗(Receiver Window, RWND) 大小, 傳送方會取接收視窗 RWND 和擁塞視窗 CWND 中的最小值(min(CWND, RWND))作為自己的傳送視窗,當未確認的資料量到達傳送視窗規定的上限時便暫停傳送。
當傳送者收到了一個視窗為0的應答,傳送者便停止傳送,等待接收者的下一個應答。但是如果這個視窗不為0的應答在傳輸過程丟失,傳送者一直等待下去,而接收者以為傳送者已經收到該應答,等待接收新資料,這樣雙方就相互等待,從而產生死鎖。
為了避免流量控制引發的死鎖,TCP使用了持續計時器。每當傳送者收到一個零視窗的應答後就啟動該計時器。時間一到便主動傳送報文詢問接收者的視窗大小。若接收者仍然返回零視窗,則重置該計時器繼續等待;若視窗不為0,則表示應答報文丟失了,此時重置傳送視窗後開始傳送,這樣就避免了死鎖的產生。