Linux系列
文章目錄
- Linux系列
- 一、TCP連接的建立與斷開
- 1.1 TCP 三次握手
- 1.2 TCP四次揮手
- 1. TCP連接的本質是應用層間的通信通道
- 2. 斷開連接的核心是終止應用層通信
- 3. 常見誤解澄清
- 二、TCP協議的機制
- 2.1 流量控制
- 2.2 滑動窗口
- 2.2.1 滑動窗口的工作原理
- 2.2.2 基于滑動窗口快重傳
- 2.3 擁塞控制
- 2.3.1 擁塞控制的定義
- 2.3.2 擁塞窗口機制的實現
- 2.3.3 慢啟動策略
- 2.4 延時應答
- 三、TCP協議的其他特點
- 3.1 面向字節流
- 3.2 粘包問題
- 總結
上篇文章:深入解析TCP:可靠傳輸的核心機制與實現邏輯
本篇我們接著上篇文章繼續介紹TCP協議,不說廢話,直接開始
一、TCP連接的建立與斷開
TCP協議在建立時,通信開始前需要先建立連接,通訊結束后會斷開連接,接下來我們就來學習一下具體的實現
TCP協議在建立時遵循以下流程:
接下來我會依據上圖,詳細的介紹TCP通訊的實現
1.1 TCP 三次握手
此處需要結合上篇文章介紹的標志為理解
客戶端側狀態
- CLOSED(初始關閉態)
客戶端 TCP 層、應用層尚未發起連接時的基礎狀態,無連接相關資源占用,代表“連接未建立”。- SYN_SENT(已發同步請求態)
客戶端調用connect()
發起連接,TCP 層向外發送SYN
報文后進入此狀態,等待服務端SYN+ACK
響應,標志“主動請求連接,等待確認”。- ESTABLISHED(連接建立態)
客戶端收到服務端返回的SYN+ACK
后,回發ACK
確認,TCP 層狀態轉為ESTABLISHED
,此時客戶端與服務端完成三次握手,雙方進入“可雙向傳輸數據”的就緒狀態,應用層connect()
調用也隨之返回 。
服務端側狀態
- CLOSED(初始關閉態)
服務端 TCP 層、應用層未監聽端口時的初始狀態,無連接相關資源,代表“無監聽、無連接”。- LISTEN(監聽態)
服務端應用層執行socket()
、bind()
、listen()
后,TCP 層進入LISTEN
狀態,開始監聽指定端口,準備接收客戶端連接請求,標志“主動開放端口,等待連接”。- SYN_RCVD(已收同步請求態)
服務端 TCP 層收到客戶端SYN
報文后進入此狀態,會回復SYN+ACK
報文,等待客戶端最終ACK
確認,是三次握手“中間確認環節”的服務端狀態。- ESTABLISHED(連接建立態)
服務端收到客戶端回發的ACK
后,TCP 層狀態轉為ESTABLISHED
,與客戶端共同進入“可雙向傳輸數據”狀態,此時服務端應用層accept()
調用返回,可通過新的connfd
與客戶端通信
在連接建立過程中:
- 第一次握手: Client 給Server發送請求連接,報文中攜帶SYN標記位來表明當前報文是和建立連接相關的。
- 第二次握手: Server接受Client的連接,并詢問何時建立連接,報文中也會攜帶SYN標記位,同時會攜帶ACK來回復上一條請求,此處使用了捎帶應答。
- 第三次握手: Client 回復 Server,立馬建立連接,這里只是單純的回復,只要設置ACK即可。
在連接建立流程里,connect
函數僅負責發起連接建立請求,一旦請求發出,后續連接建立的具體交互流程便不再由它參與;accept
函數也只是從系統底層獲取已完成三次握手、建立好的連接,本身不介入三次握手過程。實際上,兩臺主機間 TCP 連接的建立,完全依靠各自操作系統遵循 TCP 協議自主完成報文交互與狀態變遷,應用層的 connect
和 accept
更多是觸發和感知這一內核流程的“接口” 。
下面我們來學習連接建立過程中可能出現的問題及解決策略
可以看到在建立連接時第三次請求,一經發出客戶端的狀態就會由同步請求態變為連接建立態,而服務端則是在收到第二次請求的應答后,才會由已收同步請求態變為連接建立態,在第三次請求由客戶端到服務端接收的這段時間內,我們就會面臨著通信雙方對連接狀態認知不一致問題,或者換個更形象的例子:當第三次請求丟失了,服務端一直處于已收同步請求態,并且認為連接并沒有成功建立,而客戶端則認為連接建立成功了,客戶端就該給服務端發送報文了,當服務端接收到客戶端的請求就會感到很疑惑“連接都沒建立成功呢,你小子怎么就開始發信息了”,在出現這種情況的時候,我們就需要用到RST
標志位了,這時服務器就會發送一個TCP
報文,該報文的RST
標志位被置為1,表示重新建立連接(重新進行三次握手)。
三次握手的優點
為什么要進行三次握手,而不是一次握手或兩次握手呢?
通過對比我們可以看到三次握手,實現了兩個方向上的通信,可靠的驗證了全雙工, 但是全雙工也可以通過兩次握手驗證啊,顯然單憑這一點是不夠的。
要回答這個問題首先我們要清楚,通信雙方維護連接是需要成本的,而服務器與客戶端之間的通信時多對1的,若采用兩次握手建立連接,當服務器發出出發起第二次握手的信息后,會默認連接已建立,隨即開辟資源、等待接收數據。可一旦該握手信息丟包,服務端會因判定二次握手完成而維持“已連接”狀態;客戶端卻因收不到回應,觸發超時重傳機制。當服務端再次收到重傳的連接請求,會誤判為新客戶端發起連接,再次同意并分配資源。若此類情況大量出現,服務端資源將被快速耗盡,引發崩潰。此外,若客戶端惡意重復發送 SYN 請求,還會直接導致服務端遭遇 SYN 洪水攻擊,進一步加劇資源消耗與服務異常風險 。
那么三次握手的優點到底在哪里呢?
在三次握手協議中,服務端與客戶端成功建立連接的關鍵,在于服務端接收到客戶端發送的第三次ACK確認。當客戶端向服務端發送ACK時,客戶端單方面認為連接已建立并開始維護資源,但服務端此時尚未確認連接的有效性。若第三次ACK因網絡丟包未能到達服務端,客戶端會持續重傳SYN請求,而服務端因未收到ACK不會分配資源,從而將連接失敗的成本完全轉移至客戶端。這種機制確保了服務端僅在連接真正建立后才消耗資源,有效規避了無效連接的資源浪費。
三次握手的設計從根本上瓦解了SYN洪水攻擊的邏輯:攻擊方(客戶端)必須持續發送SYN請求,但服務端僅在收到ACK后才會分配資源。由于服務端硬件配置通常遠超普通客戶端,在資源消耗對抗中,客戶端會因資源耗盡率先崩潰。例如,單臺客戶端每秒發送1000個SYN請求,服務端只需處理1000個合法ACK即可維持正常運行,而客戶端自身可能在短時間內就因網絡帶寬或系統資源耗盡而無法繼續攻擊。
對于基數次握手,連接建立失敗的資源消耗都是由發起方承擔的,而過多次的握手是沒有必要的,所以我們選擇三次握手.
連接在建立的同時,雙方也協商了接收能力
1.2 TCP四次揮手
四次揮手,服務端和客戶端都可以主動發起,下面我們以客戶端主動發起展開介紹。
TCP通信雙方,均維持著連接因此,Client 單方面斷開連接需要兩次;Server端單方面斷開連接需要兩次,加起來就是四次(后面詳細介紹)。
客戶端側狀態
- FIN_WAIT_1:客戶端調用
close(fd)
發起主動關閉,TCP 層發送FIN
報文后進入此狀態,等待服務端對FIN
的ACK
確認,是“主動關閉第一步,等待首次確認”的狀態。- FIN_WAIT_2:客戶端收到服務端返回的
ACK
(確認客戶端FIN
)后進入該狀態,此時客戶端需等待服務端發送自己的FIN
,是“已確認對方收到關閉請求,等待對方發起關閉”的中間態。- TIME_WAIT:客戶端收到服務端的
FIN
并回復ACK
后進入此狀態,會停留 2MSL(報文最大生存時間)時長,作用是確保最后一個ACK
能被服務端收到,避免服務端因收不到ACK
重發FIN
,同時讓舊連接殘留報文在網絡中自然超時,不干擾新連接。- CLOSED:
TIME_WAIT
超時后,客戶端 TCP 層徹底釋放連接資源,回到初始無連接狀態,標志“連接完全關閉” 。
服務端側狀態
- CLOSE_WAIT:服務端通過
read(connfd)
返回 0(感知到客戶端FIN
),進入此狀態,代表“被動收到關閉請求,需應用層決定何時發起主動關閉” ,期間服務端仍可向客戶端發送數據,若應用層未及時處理(如未調用close(connfd)
),會長期滯留該狀態,引發資源泄漏。- LAST_ACK:服務端調用
close(connfd)
發起關閉,TCP 層發送FIN
報文后進入此狀態,等待客戶端對FIN
的ACK
確認,是“被動關閉方發起最后關閉請求,等待最終確認”的狀態。- CLOSED:服務端收到客戶端返回的
ACK
(確認服務端FIN
)后,徹底釋放連接資源,回到初始無連接狀態,標志“連接完全關閉” 。
在斷開連接過程中:
- 第一次揮手: Client 通知 Server 自己單方面斷開連接,Client發送的報文中就會攜帶FIN標記位。( 注意:Client單方面斷開連接以后,只是斷開Client ->Server方向上的數據傳輸,此時Server可以給Client發送數據,反過來不行。)
- 第二次揮手: Server收到Client的通知請求,并給Client發送應答報文,報文中攜帶ACK標記位。
- 第三次揮手: Server 通知 Client 自己單方面斷開連接,Server發送的報文中會攜帶FIN標記位。
- 第四次揮手: Client收到Server的通知請求,并給Server發送應答報文,報文中攜帶ACK標記位。
在繼續向下介紹之前需要補充幾點:
1. TCP連接的本質是應用層間的通信通道
TCP協議通過三次握手在客戶端與服務端的傳輸層(操作系統內核)之間建立邏輯連接,但最終目的是為應用層提供可靠的數據傳輸服務。只有當應用層,確認連接建立后,才能真正進行數據交換。若應用層未成功對接,即使傳輸層完成部分握手,也視為連接失敗。
2. 斷開連接的核心是終止應用層通信
當應用層調用close()
關閉socket時,會觸發TCP四次揮手流程:
- 應用層視角:客戶端/服務端的應用程序停止發送或接收數據(如HTTP請求響應結束);
- 傳輸層視角:操作系統內核通過發送FIN、ACK等管理報文,有序釋放TCP連接資源。
關鍵點:斷開連接后,應用層無法再通過socket發送數據,但傳輸層仍可能發送剩余的FIN/ACK以完成關閉流程。
斷開連接后,傳輸層仍可發送兩類報文:
- 連接管理報文:如FIN(請求關閉)、ACK(確認接收)、RST(強制復位),由操作系統內核自動生成,無需應用層干預;
- 應用層數據報文:需通過
socket API
(如send()
)主動調用,連接斷開后調用會失敗(如EPIPE錯誤)。
示例:
- 客戶端發送FIN后進入FIN_WAIT_1狀態,若此時應用層嘗試
send()
數據,會觸發RST強制關閉; - 服務端收到FIN后發送ACK并進入CLOSE_WAIT狀態,此時應用層仍可讀取緩沖區殘留數據,但無法發送新數據。
3. 常見誤解澄清
- ? “斷開連接后無法發送任何數據”:錯誤。傳輸層仍可發送FIN/ACK等管理報文;
- ? “TCP連接失敗僅影響傳輸層”:錯誤。連接失敗直接導致應用層無法通信;
- ? “連接狀態由傳輸層維護,但服務于應用層”:正確。TCP通過序列號、確認號等機制確保應用層數據的可靠傳輸。
TCP連接的成功與失敗,最終以應用層能否通過socket正常通信為判定標準。斷開連接后,傳輸層的“善后工作”(如發送FIN/ACK)由操作系統自動完成,與應用層的主動數據傳輸無關。這種分層設計使得TCP既能保證連接可靠性,又能在異常情況下快速釋放資源。
當我們知道了有了上面的知識儲備,再來理解接下來的問題就比較簡單了:
為什么稱為四次揮手,而不利用捎帶應答優化為三次揮手呢?
這是因為,當客戶端與服務器斷開連接時,只能說明客戶端已經沒有想要發送給服務端的請求了,但是服務端這時仍有可能存在需要發送給客戶端的數據,所以第二次揮手和第三次揮手不可合并。
客戶端收到服務發送的
FIN
為什么要先進入TIME_WAIT
狀態而不直接進入CLOSE
狀態?
- 第一個原因: 第四次揮手存在丟失的可能,需要客戶端進行補發。
- 第二個原因: 它正在等待一段時間,允許老的重復報文段在網絡中消逝,防止新的連接收到舊的報文段而導致數據錯亂。如:客戶端與服務端斷開連接后立刻重新建立,這時就一可能受到舊報文的干擾。
TIME_WAIT狀態也稱為 2MSL 等待狀態,在這個狀態下,TCP 將會等待兩倍于 MSL(最大段生存期)的時間,有時也被稱為加倍等待。每個實現都必須為 MSL 設定一個數值,它代表任何報文段在被丟棄前在網絡中被允許存在的最長時間
為什么TIME_WAIT狀態的持續時間是2倍的MSL?
為保證客戶端發送的最后一個ACK報文段能抵達服務器。因該ACK可能丟失,會讓處于LAST - ACK狀態的服務器收不到對FIN - ACK的確認報文,服務器會超時重傳此FIN - ACK,客戶端收到后會重傳確認、重啟時間等待計時器,最終雙方可正常關閉。若客戶端不等待2MSL,發送完ACK就直接釋放關閉,一旦ACK丟失,服務器便無法正常進入關閉連接狀態,具體如下:
- 保障客戶端最后一個ACK報文段抵達服務端
該ACK報文段有丟失可能,會使處于LAST - ACK狀態的服務端,收不到已發FIN + ACK報文段的確認。服務端超時重傳FIN + ACK報文段后,客戶端能在2MSL時間內收到此重傳報文,接著客戶端重傳確認、重啟2MSL計時器,最終客戶端和服務端都進入CLOSED狀態。若客戶端在TIME - WAIT狀態不等待,發完ACK就立即釋放連接,就無法收到服務端重傳的FIN + ACK報文段,不會再發確認,服務端也就無法正常進入CLOSED狀態。- 避免“已失效的連接請求報文段”混入本連接
客戶端發完最后一個ACK報文段,再過2MSL,可讓本連接存續期間產生的所有報文段從網絡消失,新連接就不會出現這類舊連接請求報文段。
網絡中可能存在多個源IP、目的IP相同的連接。若不等待就復用相同源端口和目的端口,之前連接的報文可能被誤認成新連接的一部分。等待2MSL時長,能確保之前連接的所有報文已在網絡消失,避免新、舊連接混淆。
二、TCP協議的機制
此處會在上篇介紹的基礎上再次理解
2.1 流量控制
通過之前了理解我們知道了TCP協議在進行通訊時,通訊雙方可以通過16位窗口
告訴對方自己的接收緩沖區還剩下多少空間資源,即自己的接收能力,而對方只需要根據獲得的接收能力,制定適合的發送方案,這樣當對方主機空間資源充足就多發一些,空間資源匱乏就少發一些,從而實現了雙方通訊的流量控制。
建立連接后首次發送數據時,TCP 三次握手已提前協商好接收能力:三次握手階段,前兩次(SYN、SYN + ACK 報文)雖不能攜帶應用層數據,但會交互關鍵信息,發送方發 SYN 報文時,攜帶自身初始序列號(ISN)與最大段大小(MSS);接收方回 SYN + ACK 報文,包含自己的 ISN、MSS ,還會告知「窗口」大小,體現當前可接收數據量。第三次握手的 ACK 報文可攜帶應用層數據,待三次握手完成,雙方已明確彼此序列號、段大小、窗口大小,基于這些協商好的參數,就能合理調整發送速度、控制數據量,保障首次及后續數據傳輸適配接收方能力 。
流量控制工作方式:
在 TCP 流量控制流程里,當主機 B 的接收緩沖區被占滿,會將窗口大小置為 0 ,主機 A 接收此狀態后,會立即暫停新數據發送。若主機 A 等待時長超出重發超時(RTO)閾值,卻仍未收到主機 B 的窗口更新通知,為避免因更新丟失陷入“永久停發”,主機 A 會主動發起窗口探測,通過發送探測包,強制獲取主機 B 當前實際可接收數據的窗口狀態 。
而從主機 B 角度,一旦其處理完緩沖區數據、窗口可用空間恢復,會主動發送窗口更新報文,將新的窗口大小告知主機 A 。這種“主動更新 + 被動探測”的雙機制協同運作,讓主機 A 能持續、精準掌握主機 B 的接收窗口動態,為基于滑動窗口的流量控制筑牢基礎,確保數據發送速率與接收端處理能力適配 。
窗口更新的兩種機制,誰先起作用就用誰,一定程度上提高了效率
2.2 滑動窗口
2.2.1 滑動窗口的工作原理
在介紹滑動窗口協議之前,我們先來想一想,如何來保證發送方與接收方之間,每個包都能被收到,并且是按次序的呢?
TCP協議的補發機制,當發送方發送的數據丟失時,需要具備重新發送的能力,也就是說發送方發送的數據,在未確認該數據已被對方主機成功接收的時間里,發送方就需要一直維護這些數據,這一點是不難理解的。
現在問題又來了,發送方該如何維護這些數據,而且在發送方不僅存放有已發送的數據,還有未發送的數據,該如何來區分他們呢?
TCP協議規定,發送緩沖區的數據可以分為以下四個部分:
- 已方并且收到確認ACK的數據,這些數據允許被覆蓋。
- 已發送未收到確認ACK的數據
- 未發送,但接收方準備接收的數據
- 未發送,且接收方未準備接收的數據
我們將處在,中間兩部分的數據稱為滑動窗口。
滑動窗口的大小是怎么設定的?未來大小又是怎么變化的?
- 滑動窗口的大小要始終和對方的接收能力掛鉤,因為滑動窗口的大小=一次批量化發送數據段的多少,我們知道TCP有流量控制,而一次批量化發送數據段的多少,其實是由對方的16位窗口大小和網絡的擁塞情況共同決定的(后面會介紹)共同決定的,所以滑動窗口的大小=min(16位窗口大小,擁塞窗口大小)。
滑動窗口的更維護:
我們只需要使用兩個指針start
和end
就可以將窗口描述出來
- start:表示滑動窗口起始位置下標,如圖中
start=16
- end:表示滑動窗口結束位置下標,如圖中
end=20
滑動窗口在更新前會收到處于“已發送未收到確認ACK的數據”區域的確認ACK
,如:收到的確認報文中,確認序號為19
(表示19之前的數據均收到了),這時start
指針就會由16
更新為19
,而end
指針則會根據該報文中的16位窗口大小
來決定如何調整。
在上一篇文章中我們介紹的這種機制就是基于滑動窗口優化的,將串行通信,優化為并行通信:
- 送前四個段的時候, 不需要等待任何ACK, 直接發送;
- 收到第一個ACK后, 滑動窗口向后移動, 繼續發送第五個段的數據;
- 依次類推只有確認應答過的數據, 才能移除滑動窗口;
- 窗口越大, 則網絡的吞吐率就越高;
在這種情況下:
即使出現,部分ACK丟了也不要緊, 因為可以通過后續的ACK進行確認(應答機制規定,收到的確認序號表示,該序號前的數據已全部被接收,如:對圖中1001、3001、4001應答丟失,只要我們收到了5001確認應答序號,那么5001之前的數據均被接收了)
現在我們有遇到了一個問題,如果1~1000這個數據包丟了,而處于滑動窗口的其他數據,被正確的接收了,那么應答是什么樣的呢,TCP又該如何處理呢?為解決這個問題TCP引入了快重傳機制。
補充:
能不能向左滑動呢?
滑動窗口一定不會向左滑動,左邊的數據都是已經被ACK的,而滑動窗口內的數據是未被ACK的,向左滑動是不合理的!
滑動窗口大小會保持不變嗎?會變大嗎?會變小嗎?變化的依據又是什么?
- 滑動窗口大小會保持不變,比如上面我們說丟包的情況,滑動窗口的大小和位置都會保持不變。
- 滑動窗口是會變大的,比如對方的接收能力變好了(空間資源充足,帶寬資源充足),那么滑動窗口就可以增大,發送數據時就可以一次批量化的發送更多的數據了。
- 滑動窗口也是會變小的,比如對方的接收能力下降(網絡變差、空間資源緊張),而此時滑動窗口也會跟著下降。
2.2.2 基于滑動窗口快重傳
當1001~2000
的數據包丟失后,即使此后的數據包都成功到達主機B,主機B回復的應答序號依然是1001
(表示期望收到的下一個報文是從1001開始的),直到主機A對1001~2000
數據包重發,確認序號才改變。
在上述過程中主機A一直沒有收到1001
的應答序號,所以主機A的滑動串口一直處于圖中狀態:
當主機A連續收到了三個相同確認應答序號,就觸發快重傳,此過程并不需要等到,等待超時,觸發超時重傳。
引入快重傳的優勢:
- 如果等待
1001~2000
應答超時,觸發超時重傳,這種情況下,不僅延長了等待,而且已經到達的數據,依然面臨著應答接收超時問題(要知道他們發送的時間間隔很短,而且主機B一直沒有對他們做出應答),而快重傳僅需對丟失的數據包進行補發即可。
補充:既然快重傳那么優秀,為什么還要有超時重傳呢?
快重傳是有觸發條件的,只有收到連續三個相同確認序號的確認應答時,才會觸發快重傳,當發送的最后一個數據包丟失后就只能使用超時重傳來兜底了。
2.3 擁塞控制
2.3.1 擁塞控制的定義
此前探討的各類TCP策略與機制,聚焦于通信兩端的交互邏輯,未涉及中間網絡的數據傳輸環節。實際場景中,若出現大量丟包問題,除兩端自身因素外,還可能源于中間網絡故障;當網絡因異常或壓力過大引發丟包時,就需要TCP借助擁塞控制機制來解決。
若客戶端向服務端發送一批數據段,僅丟失少量時,客戶端無需特殊處理,超時重傳即可;但丟失數量極大時,客戶端會判定網絡存在問題——畢竟在流量控制機制作用下,發送的數據段本就適配接收方能力,此時大面積丟包,大概率源于網絡環境異常,而遇此情況,TCP 需通過擁塞控制機制緩解網絡壓力 。
當發生大面積網絡丟包時,TCP 若仍單純采取超時重傳策略,會引發更嚴重問題:網絡中并非僅通信的兩臺主機,眾多主機同時通信,若都因丟包觸發超時重傳,會向本就擁塞的網絡瘋狂“塞”報文,進一步加劇帶寬窄、數據擁塞等網絡故障,導致大面積丟包反復出現。因此,合理做法是讓識別到網絡擁堵的主暫緩發送報文,待網絡恢復后再正常通信,這種在網絡大面積丟包時,讓主機暫止或少發報文、等待網絡恢復的機制,就是 TCP 的擁塞控制 。
可能有人覺得,在網絡擁塞時就算我停止發送報文,網絡就不擁塞了嗎?一臺主機能有那么大的分量嗎?在理解這一點時,我們要清楚,網絡資源屬于共享資源,識別到網絡異常的主機不單是我們的這臺,當其他主機識別到網絡擁堵時也會采取這種機制,那么當大部分主機都停止向網絡中塞數據了,網絡就有時間將擁堵的數據“疏通”了
2.3.2 擁塞窗口機制的實現
TCP 引入擁塞窗口 實現擁塞控制,核心邏輯為:
- 初始階段:連接建立后,擁塞窗口初始值設為 1(以報文段為單位,代表首輪可發 1 個報文段 );
- 增長規則:每收到一個對端的 ACK 確認,擁塞窗口 +1(體現“慢啟動”思路,逐步試探網絡承載能力 );
- 實際發送窗口:發送數據時,需將擁塞窗口 與接收端通告窗口(由接收方反饋的可接收窗口) 對比,取兩者最小值 作為實際可用的發送窗口(即
實際發送窗口 = min(擁塞窗口, 接收端通告窗口)
)。
簡單說,擁塞窗口是 TCP 感知網絡擁塞、動態調控發送速率的“閥門”,通過慢啟動(后面介紹)、與接收窗口協同,在網絡和接收端間找平衡,避免盲目發數據壓垮網絡 。
當網絡出現擁塞時,發送端會發送擁塞窗口大小的探測報文段,用于探測網絡狀況如何。
2.3.3 慢啟動策略
擁塞窗口調整算法:
- 慢啟動: 通信初始階段,使用極小的窗口探測網絡狀況。若一開始就發送大量流量,遇到網絡擁堵時,會加劇本就緊張的網絡帶寬壓力。
- 指數增長: 在網絡通暢不堵塞的傳輸過程中,擁塞窗口大小呈指數級增長。由于其增長速度極快,若不加以限制會導致窗口值過大,因此需要設定一個閾值(初始大小為接收端窗口大小)進行控制。
- 線性增長: 當窗口大小指數增長至設定閾值后,增長模式轉為線性增長。盡管線性增長速度慢于指數增長,但仍會使發送速度逐漸加快,當接近網絡傳輸極限時,可能出現丟包。
- 擁塞窗口回歸小窗口: 當傳輸中出現大量丟包,判定網絡發生擁堵時,會將窗口大小調整為初始的小窗口,并將閾值調整為上次出現擁塞時窗口大小的一半,隨后重新進入“指數增長+線性增長”的循環過程。
2.4 延時應答
在網絡傳輸過程中,需頻繁執行 I/O 操作來訪問網卡等外設,而過于頻繁的外設訪問會降低 TCP 通訊效率。若能一次性發送較多報文數據,可在一定程度上提升通訊效率,但兩主機通訊時,一次發送數據量受接收方窗口大小限制。因此,為提高效率,TCP 引入延時應答機制 ,通過適當延遲對接收窗口的應答反饋,爭取讓發送方一次發送更大量的數據。
延遲應答是 TCP 協議中一種用于提高傳輸效率的機制,它的誕生圍繞著滑動窗口展開。在 TCP 傳輸過程中,窗口大小與傳輸效率緊密相關,通常情況下,窗口越大,單位時間內能夠發送的數據量就越多,傳輸效率也就越高 。于是,一個關鍵問題擺在面前:在確保網絡不會出現擁堵的前提下,是否存在可行的方法,來盡可能地提升窗口大小呢?
接收端接收到數據后,并不一定立即處理數據,這些還未來得及處理的數據會暫存在接收緩沖區,如果接收數據的主機立刻返回 ACK 應答,此時返回的窗口可能較小;而若在返回 ACK 時適當拖延響應時間,就能利用這段時間給接收方留出更多消費數據的機會——接收緩沖區的剩余空間會隨之變大,窗口大小也能相應提升。需要注意的是,延遲應答并非必然讓上層在這段時間內取走緩沖區數據,這是一種概率性事件:大概率情況下上層會取走數據,從而恰巧提高傳輸效率;若未取走,也只能接受這一結果。畢竟,世界上沒有絕對的事情,任何事件都存在概率差異,延遲應答的優勢也在于它屬于較大概率能提升效率的機制。
顯然,并非所有數據包都適合延遲應答,TCP 對此設置了雙重限制:
- 數量限制:每接收 N 個數據包就必須應答一次,避免無限制延遲導致發送方超時重傳;
- 時間限制:若延遲時間超過最大閾值,無論接收了多少包,都必須立即應答。
具體的 N 值和超時時間因操作系統而異,通常 N 取 2(即每接收 2 個包應答一次),超時時間設為 200ms(超過此時長則強制應答)。
三、TCP協議的其他特點
3.1 面向字節流
兩主機通信時,發送緩沖區中的數據由TCP自主發送,其字節流大小會依據窗口大小、擁塞控制、流量控制等因素動態調整:若發送的字節數過長,TCP會將其拆分為多個數據包發送;若字節數過短,TCP可能先將數據暫存在發送緩沖區,待合適時機再一并發送。
接收數據時,數據經網卡驅動程序傳入內核的接收緩沖區,由TCP自主接收,接收的字節流大小則會根據窗口大小、確認機制等因素動態調整。
由于緩沖區的存在,TCP程序的讀寫無需一一匹配。例如:寫入100字節數據時,既可以調用一次write函數寫入100字節,也可以調用100次write函數每次寫入1字節;讀取100字節數據時,無需考慮寫入時的方式,既可以一次read100字節,也可以每次read1字節、重復100次。
對TCP而言,發送緩沖區中的數據僅是一個個字節,它會為每個字節分配序號,并通過序號和確認號保障字節流的順序與完整性。TCP的核心任務是將這些數據準確無誤地送達對方接收緩沖區,而數據的具體含義則由上層應用解釋——這就是所謂的“面向字節流”。操作系統同樣如此,它僅關注緩沖區的剩余空間,而不關心數據本身的內容。
3.2 粘包問題
首先要明確:
- 粘包問題中的 “包”,指的是應用層的數據包。
- 在 TCP 的協議頭中,沒有如同 UDP 一樣的 “報文長度” 這樣的字段。
- 站在傳輸層的角度,TCP 是一個一個報文過來的,按照序號排好序放在緩沖區中。
- 站在應用層的角度,看到的只是一串連續的字節數據。
- 那么應用程序看到了這么一連串的字節數據,就不知道從哪個部分開始到哪個部分,是一個完整的應用層數據包。
導致粘包問題的因素是報文之間的邊界不清晰:
粘包問題指的是發送方發送的多個數據包在接收方被合并為一個數據包的現象。這是因為 TCP 是面向字節流的協議,它不關心數據的邏輯結構,只負責將字節流按序和完整地傳輸給對方。TCP 在發送或接收數據時,都會通過緩沖區來進行優化,根據網絡狀況和窗口大小來動態調整發送或接收的字節流的大小。這樣就可能導致發送方發送的多個數據包被拼接在一起,或者一個數據包被拆分成多個部分。
解決辦法:
- 對于定長的包,保證每次都按固定大小讀取即可。
- 對于變長的包,可以在報頭的位置,約定一個包總長度的字段,從而就知道了包的結束位置。
- 對于變長的包,還可以在包和包之間使用明確的分隔符(該分隔符是由程序員自己定的)。
總結
本篇文章就分享到這里,由于TCP協議較為復雜,協議規則較多,文中有許多知識并未介紹完,后續文章會繼續補充。