
🌈 個人主頁:Zfox_
🔥 系列專欄:Linux

目錄
- 一:🔥 前置復盤
- 🦋 傳輸層
- 🦋 再談端口號
- 🦋 端口號范圍劃分
- 🦋 認識知名端口號 (Well-Know Port Number)
- 二:🔥 UDP 協議
- 🦋 UDP 協議段格式
- 🦋 UDP 的特點
- 🦋 面向數據報
- 🦋 UDP 的緩沖區
- 🦋 UDP 使用注意事項
- 🦋 基于 UDP 的應用層協議
- 三:🔥 傳輸層協議 TCP (重點)
- 🦋 TCP 協議
- 🦋 TCP 協議段格式
- 🦋 確認應答 (ACK) 機制
- 🦋 超時重傳機制
- 🦋 連接管理機制
- 🦋 MSL(Maximum Segment Lifetime)
- 🦋 理解 TIME_WAIT 狀態
- 🦋 解決 TIME_WAIT 狀態引起的 bind 失敗的方法 (作業)
- 🦋 理解 CLOSE_WAIT 狀態
- 🦋 滑動窗口
- 滑動窗口如何滑動?
- 滑動窗口丟包問題
- 滑動窗口的特點
- 🦋 流量控制
- 🦋 擁塞控制
- 🦋 發送窗口的最終上限由誰決定?
- 🦋 延遲應答
- 🦋 捎帶應答
- 🦋 面向字節流
- 🦋 粘包問題
- 五:🔥 TCP 異常情況
- 七:🔥 TCP 小結
- 🦋 基于 TCP 應用層協議
- 🦋 TCP/UDP 對比
- 🦋 用 UDP 實現可靠傳輸(經典面試題)
- 八:🔥 共勉
一:🔥 前置復盤
🦋 傳輸層
🧑?💻 負責數據能夠從發送端傳輸到接收端.
🦋 再談端口號
💻 端口號 (Port) 標識了一個主機上進行通信的不同的應用程序;
🛜 在 TCP/IP 協議中, 用 “源 IP”, “源端口號”, “目的 IP”, “目的端口號”, “協議號” 這樣一個五元組來標識一個通信 (可以通過 netstat -n 查看);
🦋 端口號范圍劃分
- 🌾 🦁 0 - 1023: 知名端口號, HTTP, FTP, SSH 等這些廣為使用的應用層協議, 他們的端口號都是固定的.
- 🌾 1024 - 65535: 操作系統動態分配的端口號. 客戶端程序的端口號, 就是由操作系統從這個范圍分配的.
🦋 認識知名端口號 (Well-Know Port Number)
?? 有些服務器是非常常用的, 為了使用方便, 人們約定一些常用的服務器, 都是用以下這些固定的端口號:
ssh
服務器, 使用 22 端口ftp
服務器, 使用 21 端口telnet
服務器, 使用 23 端口http
服務器, 使用 80 端口https
服務器, 使用 443
🖱? 執行下面的命令, 可以看到知名端口號
cat /etc/services
🎯 我們自己寫一個程序使用端口號時, 要避開這些知名端口號.
二:🔥 UDP 協議
🦋 UDP 協議段格式
- 16 位 UDP 長度: 表示整個數據報 (UDP 首部+UDP 數據) 的最大長度;
- 16 位 UDP 校驗和: 用于檢測報文在傳輸過程中是否出錯。如果校驗和出錯, 就會直接丟棄;
- 如果校驗和為 0,表示未啟用校驗(不推薦)
🦋 UDP 的特點
🧑?💻 UDP 傳輸的過程類似于寄信
- 🌰 無連接: 知道對端的 IP 和端口號就直接進行傳輸, 不需要建立連接;
- 🌰 不可靠: 沒有確認機制, 沒有重傳機制; 如果因為網絡故障該段無法發到對方, UDP 協議層也不會給應用層返回任何錯誤信息;
- 🌰 面向數據報: 不能夠靈活的控制讀寫數據的次數和數量;
🦋 面向數據報
🛜 應用層交給 UDP 多長的報文, UDP 原樣發送, 既不會拆分, 也不會合并; 用 UDP 傳輸 100 個字節的數據.
- 如果發送端調用一次 sendto, 發送 100 個字節, 那么接收端也必須調用對應的一次 recvfrom, 接收 100 個字節; 而不能循環調用 10 次 recvfrom, 每次接收 10 個字節。
🦋 UDP 的緩沖區
- UDP 沒有真正意義上的 發送緩沖區. 調用 sendto 會直接交給內核, 由內核將數據傳給網絡層協議進行后續的傳輸動作;
- UDP 具有接收緩沖區. 但是這個接收緩沖區不能保證收到的 UDP 報的順序和發送 UDP 報的順序一致; 如果緩沖區滿了, 再到達的 UDP 數據就會被丟棄;
🦁 UDP 的 socket 既能讀, 也能寫, 這個概念叫做 全雙工
🦋 UDP 使用注意事項
?? 我們注意到, UDP 協議首部中有一個 16 位的最大長度. 也就是說一個 UDP 能傳輸的數據最大長度是 64K(包含 UDP 首部).
📊 然而 64K 在當今的互聯網環境下, 是一個非常小的數字.
📊 如果我們需要傳輸的數據超過 64K, 就需要在應用層手動的分包, 多次發送, 并在接收端手動拼裝;
🦋 基于 UDP 的應用層協議
- NFS: 網絡文件系統
- TFTP: 簡單文件傳輸協議
- DHCP: 動態主機配置協議
- BOOTP: 啟動協議(用于無盤設備啟動)
- DNS: 域名解析協議
💤 當然, 也包括你自己寫 UDP 程序時自定義的應用層協議;
三:🔥 傳輸層協議 TCP (重點)
🦋 TCP 協議
🧑?💻 TCP
全稱為 "傳輸控制協議(Transmission Control Protocol")
. 人如其名, 要對數據的傳輸進行一個詳細的控制;
🦋 TCP 協議段格式
源/目的端口號
: 表示數據是從哪個進程來, 到哪個進程去;32 位序號
: 當前報文段數據的第一個字節的序列號;
用于保證數據的有序性和可靠性;后面詳細講。32 位確認序號
: 期望收到的下一個報文段的序列號;僅在 ACK 標志位為 1 時有效;后面詳細講。4 位 TCP 報頭長度
: 表示該 TCP 頭部有多少個 32 位 bit (有多少個 4 字節); 所以TCP 頭部最大長度是 15 * 4 = 606 位標志位
:URG (Urgent)
: 緊急指針是否有效ACK(Acknowledgment)
: 確認序號是否有效PSH(Push)
: 提示接收端應用程序立刻從 TCP 緩沖區把數據讀走RST(Reset)
: 對方要求重新建立連接; 我們把攜帶 RST 標識的稱為復位報文段SYN(Synchronize)
: 請求建立連接; 我們把攜帶 SYN 標識的稱為同步報文段FIN(Finish)
: 通知對方, 本端要關閉了, 我們稱攜帶 FIN 標識的為結束報文段
16 位窗口大小
: 接收方當前可接收的數據量(以字節為單位)16 位校驗和
: 發送端填充, CRC 校驗. 接收端校驗不通過, 則認為數據有問題. 此處的檢驗和不光包含 TCP 首部, 也包含 TCP 數據部分.16 位緊急指針
: 標識哪部分數據是緊急數據;40 字節頭部選項
: 可選部分(Options,最多 40 字節)。接收方需要通過 “4 位 TCP 報頭長度” 字段動態確定:
1?? 頭部結束位置
2?? 載荷數據(Payload)的起始位置;
🦋 確認應答 (ACK) 機制

🌲 TCP 將每個字節的數據都進行了編號. 即為序列號(這里我們可以想像成字節數組。
?? 每一個 ACK 都帶有對應的確認序列號, 意思是告訴發送者, 確認序列號之前的報文我已經全部收到了; 下一次你從哪一個序號開始發.
🦋 超時重傳機制
- 🫧 主機 A 發送數據給 B 之后, 可能因為網絡擁堵等原因, 數據無法到達主機 B;
- 🫧 如果主機 A 在一個特定時間間隔內沒有收到 B 發來的確認應答, 就會進行重發;
🎯 但是, 主機 A 未收到 B 發來的確認應答, 也可能是因為 ACK 丟失了;
- 對于主機 A 來說,無論是數據丟了還是應答丟了,都是一樣的
🌊 因此主機 B 會收到很多重復數據. 那么 TCP 協議需要能夠識別出那些包是重復的包, 并且把重復的丟棄掉.
這時候我們可以利用前面提到的序列號, 就可以很容易做到去重的效果.
那么, 如果超時的時間如何確定?
最理想的情況下, 找到一個最小的時間, 保證 “確認應答一定能在這個時間內返回”.
- 但是這個時間的長短, 隨著網絡環境的不同, 是有差異的.
- 如果超時時間設的太長, 會影響整體的重傳效率;
- 如果超時時間設的太短, 有可能會頻繁發送重復的包;
- TCP 為了保證無論在任何環境下都能比較高性能的通信, 因此會動態計算這個最大超時時間.
- Linux 中 (BSD Unix 和 Windows 也是如此), 超時以 500ms 為一個單位進行控制, 每次判定超時重發的超時時間都是 500ms 的整數倍.
- 如果重發一次之后, 仍然得不到應答, 等待 2*500ms 后再進行重傳.
- 如果仍然得不到應答, 等待 4*500ms 進行重傳. 依次類推, 以指數形式遞增.
- 累計到一定的重傳次數, TCP 認為網絡或者對端主機出現異常, 強制關閉連接。
🦋 連接管理機制
?? 在正常情況下, TCP 要經過三次握手建立連接, 四次揮手斷開連接
💻 服務端狀態轉化:
[CLOSED -> LISTEN]
服務器端調用 listen 后進入 LISTEN 狀態, 等待客戶端連接;[LISTEN -> SYN_RCVD]
一旦監聽到連接請求(同步報文段), 就將該連接放入內核等待隊列中, 并向客戶端發送 SYN 確認報文.[SYN_RCVD -> ESTABLISHED]
服務端一旦收到客戶端的確認報文, 就進入 ESTABLISHED 狀態, 可以進行讀寫數據了.[ESTABLISHED -> CLOSE_WAIT]
當客戶端主動關閉連接 (調用 close), 服務器會收到結束報文段, 服務器返回確認報文段并進入 CLOSE_WAIT;[CLOSE_WAIT -> LAST_ACK]
進入 CLOSE_WAIT 后說明服務器準備關閉連接(需要處理完之前的數據); 當服務器真正調用 close 關閉連接時, 會向客戶端發送 FIN, 此時服務器進入 LAST_ACK 狀態, 等待最后一個 ACK 到來(這個 ACK 是客戶端確認收到了 FIN)[LAST_ACK -> CLOSED]
服務器收到了對 FIN 的 ACK, 徹底關閉連接.
💻 客戶端狀態轉化:
[CLOSED -> SYN_SENT]
客戶端調用 connect, 發送同步報文段;[SYN_SENT -> ESTABLISHED]
connect 調用成功, 則進入 ESTABLISHED 狀態, 開始讀寫數據;[ESTABLISHED -> FIN_WAIT_1]
客戶端主動調用 close 時, 向服務器發送結束報文段, 同時進入 FIN_WAIT_1;[FIN_WAIT_1 -> FIN_WAIT_2]
客戶端收到服務器對結束報文段的確認, 則進入 FIN_WAIT_2, 開始等待服務器的結束報文段;[FIN_WAIT_2 -> TIME_WAIT]
客戶端收到服務器發來的結束報文段, 進入TIME_WAIT, 并發出 LAST_ACK;[TIME_WAIT -> CLOSED]
客戶端要等待一個 2MSL (Max Segment Life, 報文最大生存時間) 的時間, 才會進入 CLOSED 狀態.
🦋 MSL(Maximum Segment Lifetime)
- 定義:
- MSL 是 TCP 報文段在網絡中能夠存活的最長時間。超過這個時間后,報文段會被丟棄。
- 作用:
- 確保網絡中舊的、重復的 TCP 報文段不會干擾新的連接。
- 在 TCP 連接關閉時,MSL 用于確定 TIME_WAIT 狀態的持續時間。
- 典型值:
- MSL 的默認值通常是 30 秒 到 2 分鐘,具體取決于操作系統實現。
- 因此,TIME_WAIT 狀態通常持續 1 分鐘 到 4 分鐘。
- TIME_WAIT 狀態:
🦋 理解 TIME_WAIT 狀態
📚 防止舊連接的報文干擾新連接(游離報文):
- 如果客戶端在關閉連接后立即建立新連接,網絡中可能還有舊連接的延遲報文,這會導致數據混亂。
📚 確保服務器收到最后一個 ACK:
- 如果服務器沒有收到最后一個 ACK,會重傳 FIN 報文,TIME_WAIT 狀態允許客戶端重新發送 ACK。
現在做一個測試,首先啟動 server,然后啟動 client,然后用 Ctrl-C 使 server 終止,這時馬上再運行 server, 結果是:
🐮 這是因為,雖然 server 的應用程序終止了,但 TCP 協議層的連接并沒有完全斷開,因此不能再次監聽同樣的 server 端口. 我們用 netstat 命令查看一下:
root# netstat -apn | grep 8080
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:8080 127.0.0.1:17256 TIME_WAIT -
- TCP 協議規定,主動關閉連接的一方要處于 TIME_ WAIT 狀態,等待兩個 MSL (maximum segment lifetime) 的時間后才能回到 CLOSED 狀態.
我們使用 Ctrl-C 終止了 server, 所以 server 是主動關閉連接的一方, 在 TIME_WAIT 期間仍然不能再次監聽同樣的 server 端口;
- MSL 在 RFC1122 中規定為兩分鐘,但是各操作系統的實現不同, 在 Centos7 上默認配置的值是 60s;
- 可以通過
cat /proc/sys/net/ipv4/tcp_fin_timeout
查看MSL
的值;
root# cat /proc/sys/net/ipv4/tcp_fin_timeout
60
🤔 想一想, 為什么是 TIME_WAIT 的時間是 2MSL?
- MSL 是 TCP 報文的最大生存時間, 因此 TIME_WAIT 持續存在 2MSL 的話。
- 就能保證在兩個傳輸方向上的尚未被接收或遲到的報文段都已經消失 (
否則服務器立刻重啟, 可能會收到來自上一個進程的遲到的數據, 但是這種數據很可能是錯誤的
);- 同時也是在理論上保證最后一個報文可靠到達 (
假設最后一個 ACK 丟失, 那么服務器會再重發一個 FIN. 這時雖然客戶端的進程不在了, 但是 TCP 連接還在, 仍然可以重發 LAST_ACK
);
🦋 解決 TIME_WAIT 狀態引起的 bind 失敗的方法 (作業)
🧑?💻 在 server 的 TCP 連接沒有完全斷開之前不允許重新監聽, 某些情況下可能是不合理的:
- 服務器需要處理非常大量的客戶端的連接 (每個連接的生存時間可能很短, 但是每秒都有很大數量的客戶端來請求).
- 這個時候如果由服務器端主動關閉連接 (比如某些客戶端不活躍, 就需要被服務器端主動清理掉), 就會產生大量 TIME_WAIT 連接.
- 由于我們的請求量很大, 就可能導致 TIME_WAIT 的連接數很多, 每個連接都會占用一個通信五元組 (源 ip, 源端口, 目的 ip, 目的端口, 協議). 其中服務器的 ip 和端口和協議是固定的. 如果新來的客戶端連接的 ip 和端口號和 TIME_WAIT 占用的鏈接重復了, 就會出現問題.
📚 使用 setsockopt()設置 socket 描述符的 選項 SO_REUSEADDR 為 1, 表示允許創建端口號相同但 IP 地址不同的多個 socket 描述符:
// 保證服務器,異常斷開之后,可以立即重啟,不會有bind問題
int opt = 1;
int n = ::setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
(void)n;
🦋 理解 CLOSE_WAIT 狀態
以之前寫過的 TCP 服務器為例, 我們稍加修改將 套接字的文件描述符關閉的 close(); 這個代碼去掉.
我們編譯運行服務器. 啟動客戶端鏈接, 查看 TCP 狀態, 客戶端服務器都為 ESTABLELISHED 狀態, 沒有問題.
然后我們關閉客戶端程序, 觀察 TCP 狀態
tcp 0 0 0.0.0.0:9090 0.0.0.0:* LISTEN 5038/./server
tcp 0 0 127.0.0.1:49958 127.0.0.1:9090 FIN_WAIT2 -
tcp 0 0 127.0.0.1:9090 127.0.0.1:49958 CLOSE_WAIT 5038/./server
🧑?💻 小結: 對于服務器上出現大量的 CLOSE_WAIT 狀態, 原因就是服務器沒有正確的關閉 socket, 導致四次揮手沒有正確完成. 這是一個 BUG. 只需要加上對應的 close 即可解決問題.
🦋 滑動窗口
💻 剛才我們討論了確認應答策略, 對每一個發送的數據段, 都要給一個 ACK 確認應答. 收到 ACK 后再發送下一個數據段. 這樣做有一個比較大的缺點, 就是性能較差. 尤其是數據往返的時間較長的時候.
?? 既然這樣一發一收的方式性能較低, 那么我們一次發送多條數據, 就可以大大的提高性能(其實是將多個段的等待時間重疊在一起了).
- 窗口大小指的是無需等待確認應答而可以繼續發送數據的最大值(由接收方的緩沖區剩余空間決定). 上圖的窗口大小就是 4000 個字節(四個段).
- 發送前四個段的時候,
不需要等待任何 ACK
, 直接發送;- 收到第一個 ACK 后, 滑動窗口向后移動, 繼續發送第五個段的數據; 依次類推;
- 操作系統內核為了維護這個滑動窗口, 需要開辟 發送緩沖區 來記錄當前還有哪些數據沒有應答; 只有確認應答過的數據, 才能從緩沖區刪掉;
- 窗口越大, 則網絡的吞吐率就越高
- 滑動窗口的大小實際上就是對方接收緩沖區剩余空間的大小,左側是已經發送完且 ACK 完畢的
滑動窗口如何滑動?
- 窗口的邊界:
- Start:窗口的起始位置,通常是接收方已確認的最后一個字節的序號(ACK 序號)。
- End:窗口的結束位置,計算公式為:
- End = Start + 接收方緩沖區剩余空間的大小
- 窗口的滑動:
- 窗口會根據接收方的確認信息(ACK)和緩沖區剩余空間動態調整。
- 窗口只會向右滑動(即序號遞增),但窗口的大小可以不變、變大、變小,甚至變為 0。
- 環形緩沖區:
- 在邏輯上,TCP 的序號空間是環形的(32 位序號,范圍為0 到 )。
- 當序號達到最大值時,會回繞到 0。
滑動窗口丟包問題
🎯 那么如果出現了丟包, 如何進行重傳? 這里分兩種情況討論.
-
情況一: 數據包已經抵達, ACK 被丟了.
這種情況下, 部分 ACK 丟了并不要緊, 因為可以通過后續的 ACK 進行確認; -
情況二: 數據包就直接丟了
- 當某一段報文段丟失之后, 發送端會一直收到 1001 這樣的 ACK, 就像是在提醒發送端 “我想要的是 1001” 一樣;
- 如果發送端主機連續三次收到了同樣一個 “1001” 這樣的應答, 就會將對應的數據 1001 - 2000 重新發送;
- 這個時候接收端收到了 1001 之后, 再次返回的 ACK 就是 7001 了(因為 2001 - 7000) 接收端其實之前就已經收到了, 被放到了接收端操作系統內核的接收緩沖區中;
📚 這種機制被稱為 “高速重發控制” (也叫 “快重傳”).
在這種機制下,如果滑動窗口最左側的數據丟失,接收方會觸發快速重傳機制,要求發送方重新發送丟失的數據包。如果滑動窗口中間的數據丟失,接收方會通過確認應答(ACK)指出丟失數據包的起始位置,此時問題會轉化為滑動窗口最左側數據丟失的情況,從而同樣觸發快速重傳。類似地,如果滑動窗口最右側的數據丟失,問題也會被轉換為最左側數據丟失的情況,最終通過快速重傳機制解決。
🧑?💻 通過這種設計,滑動窗口機制能夠高效處理不同位置的數據丟失問題,確保數據傳輸的可靠性和連續性。
滑動窗口的特點
動態調整:
- 窗口大小根據接收方的緩沖區剩余空間動態調整。
流量控制:
- 通過調整窗口大小,防止發送方發送過多數據導致接收方緩沖區溢出。
可靠性:
- 通過確認序號和窗口滑動,確保數據按序到達且不丟失。
🦋 流量控制
接收端處理數據的速度是有限的. 如果發送端發的太快, 導致接收端的緩沖區被打滿, 這個時候如果發送端繼續發送, 就會造成丟包, 繼而引起丟包重傳等等一系列連鎖反應.
因此 TCP 支持根據接收端的處理能力, 來決定發送端的發送速度. 這個機制就叫做流量控制 (Flow Control);
- 接收端將自己可以接收的緩沖區大小放入 TCP 首部中的 “窗口大小” 字段, 通過 ACK 端通知發送端;
- 窗口大小字段越大, 說明網絡的吞吐量越高;
- 接收端一旦發現自己的緩沖區快滿了, 就會將窗口大小設置成一個更小的值通知給發送端;
- 發送端接受到這個窗口之后, 就會減慢自己的發送速度;
- 如果接收端緩沖區滿了, 就會將窗口置為 0; 這時發送方不再發送數據, 但是需要定期發送一個窗口探測數據段, 使接收端把窗口大小告訴發送端;
接收端如何把窗口大小告訴發送端呢? 回憶我們的 TCP 首部中, 有一個 16 位窗口字段,就是存放了窗口大小信息;
那么問題來了, 16 位數字最大表示 65535, 那么 TCP 窗口最大就是 65535 字節么?
實際上, TCP 首部 40 字節選項中還包含了一個窗口擴大因子 M, 實際窗口大小是窗口字段的值左移 M 位;
🦋 擁塞控制
雖然 TCP 有了滑動窗口這個大殺器, 能夠高效可靠的發送大量的數據. 但是如果在剛開始階段就發送大量的數據, 仍然可能引發問題.
因為網絡上有很多的計算機, 可能當前的網絡狀態就已經比較擁堵. 在不清楚當前網絡狀態下, 貿然發送大量的數據, 是很有可能引起雪上加霜的.
🧑?💻 TCP 引入 慢啟動
機制, 先發少量的數據, 探探路, 摸清當前的網絡擁堵狀態, 再決定按照多大的速度傳輸數據;
- 此處引入一個概念稱為擁塞窗口
- 發送開始的時候, 定義擁塞窗口大小為 1;
- 每次收到一個 ACK 應答, 擁塞窗口加 1;
每次發送數據包的時候, 將擁塞窗口和接收端主機反饋的窗口大小做比較, 取較小的值作為實際發送的窗口;
💻 像上面這樣的擁塞窗口增長速度, 是指數級別的. “慢啟動” 只是指初使時慢, 但是增長速度非常快.
- 為了不增長的那么快, 因此不能使擁塞窗口單純的加倍.
- 此處引入一個叫做慢啟動的閾值
當擁塞窗口超過這個閾值的時候, 不再按照指數方式增長, 而是按照線性方式增長
- 當 TCP 開始啟動的時候, 慢啟動閾值等于窗口最大值;
- 在 每次超時重發的時候, 慢啟動閾值會變成原來的一半, 同時擁塞窗口置回 1; 少量的丟包, 我們僅僅是觸發超時重傳; 大量的丟包, 我們就認為網絡擁塞; 當 TCP 通信開始后, 網絡吞吐量會逐漸上升; 隨著網絡發生擁堵, 吞吐量會立刻下降;
- 擁塞控制, 歸根結底是 TCP 協議想盡可能快的把數據傳輸給對方, 但是又避免給網絡造成太大壓力的折中方案.
🦋 發送窗口的最終上限由誰決定?
🧑?💻 發送方的實際發送窗口大小 發送窗口 = min(rwnd, cwnd)。
- rwnd(接收窗口):由接收方通過 ACK 報文通告的剩余緩沖區大小。
- cwnd(擁塞窗口):由發送方根據網絡擁塞狀態動態調整。
-
網絡擁塞時
- 若網絡出現擁塞(如丟包、延遲激增),擁塞控制會減小 cwnd。
此時發送窗口由 cwnd 限制,即 發送窗口 = cwnd。
擁塞控制主導流量限制。
- 若網絡出現擁塞(如丟包、延遲激增),擁塞控制會減小 cwnd。
-
網絡狀態良好時
- 若網絡無擁塞,擁塞控制會逐步增大 cwnd(如慢啟動、擁塞避免階段)。
當 cwnd 增長到與 rwnd 相等甚至更大時,發送窗口由 rwnd 限制,即 發送窗口 = rwnd。
流量控制(滑動窗口)主導流量限制。
- 若網絡無擁塞,擁塞控制會逐步增大 cwnd(如慢啟動、擁塞避免階段)。
🦋 延遲應答
💻 如果接收數據的主機立刻返回 ACK 應答, 這時候返回的窗口可能比較小.
- 假設接收端緩沖區為 1M. 一次收到了 500K 的數據; 如果立刻應答, 返回的窗口就是 500K;
- 但實際上可能處理端處理的速度很快, 10ms 之內就把 500K 數據從緩沖區消費掉了;
- 在這種情況下, 接收端處理還遠沒有達到自己的極限, 即使窗口再放大一些, 也能處理過來;
- 如果接收端稍微等一會再應答, 比如等待 200ms 再應答, 那么這個時候返回的窗口大小就是 1M;
?? 一定要記得, 窗口越大, 網絡吞吐量就越大, 傳輸效率就越高. 我們的目標是在保證網絡不擁塞的情況下盡量提高傳輸效率;
那么所有的包都可以延遲應答么? 肯定也不是;
- 數量限制: 每隔 N 個包就應答一次;
- 時間限制: 超過最大延遲時間就應答一次;
🍻 具體的數量和超時時間, 依操作系統不同也有差異; 一般 N 取 2, 超時時間取 200ms;
🦋 捎帶應答
🧑?💻 在延遲應答的基礎上, 我們發現, 很多情況下, 客戶端服務器在應用層也是 “一發一收” 的. 意味著客戶端給服務器說了 “How are you”, 服務器也會給客戶端回一個 “Fine, thank you”;
那么這個時候 ACK 就可以搭順風車, 和服務器回應的 “Fine, thank you” 一起回給客戶端
🦋 面向字節流
💦 創建一個 TCP 的 socket, 同時在內核中創建一個 發送緩沖區 和一個 接收緩沖區;
調用 write 時, 數據會先寫入發送緩沖區中;
- 如果發送的字節數太長, 會被拆分成多個 TCP 的數據包發出;
- 如果發送的字節數太短, 就會先在緩沖區里等待, 等到緩沖區長度差不多了, 或者其他合適的時機發送出去;
- 接收數據的時候, 數據也是從網卡驅動程序到達內核的接收緩沖區;
然后應用程序可以調用 read 從接收緩沖區拿數據;
- 另一方面, TCP 的一個連接, 既有發送緩沖區, 也有接收緩沖區, 那么對于這一個連接, 既可以讀數據, 也可以寫數據. 這個概念叫做
全雙工
💻 由于緩沖區的存在, TCP 程序的讀和寫不需要一一匹配, 例如:
- 寫 100 個字節數據時, 可以調用一次 write 寫 100 個字節, 也可以調用 100 次write, 每次寫一個字節;
- 讀 100 個字節數據時, 也完全不需要考慮寫的時候是怎么寫的, 既可以一次 read 100 個字節, 也可以一次 read 一個字節, 重復 100 次;
🦋 粘包問題
[八戒吃饅頭例子]
- 首先要明確, 粘包問題中的 “包” , 是指的應用層的數據包.
- 在 TCP 的協議頭中, 沒有如同 UDP 一樣的 “報文長度” 這樣的字段, 但是有一個序號這樣的字段.
- 站在傳輸層的角度, TCP 是一個一個報文過來的. 按照序號排好序放在緩沖區中.
- 站在應用層的角度, 看到的只是一串連續的字節數據.
- 那么應用程序看到了這么一連串的字節數據, 就不知道從哪個部分開始到哪個部分, 是一個完整的應用層數據包.
那么如何避免粘包問題呢? 歸根結底就是一句話, 明確兩個包之間的邊界.
- 對于定長的包, 保證每次都按固定大小讀取即可; 例如上面的 Request 結構, 是固定大小的, 那么就從緩沖區從頭開始按 sizeof(Request)依次讀取即可;
- 對于變長的包, 可以在包頭的位置, 約定一個包總長度的字段, 從而就知道了包的結束位置;
- 對于變長的包, 還可以在包和包之間使用明確的分隔符(應用層協議, 是程序猿自己來定的, 只要保證分隔符不和正文沖突即可);
思考: 對于 UDP 協議來說, 是否也存在 “粘包問題” 呢?
- 對于 UDP, 如果還沒有上層交付數據, UDP 的報文長度仍然在. 同時, UDP 是一個一個把數據交付給應用層. 就有很明確的數據邊界。
- 站在應用層的站在應用層的角度, 使用 UDP 的時候, 要么收到完整的 UDP 報文, 要么不收. 不會出現"半個"的情況。
五:🔥 TCP 異常情況
-
進程終止: 進程終止會釋放文件描述符, 仍然可以發送 FIN. 和正常關閉沒有什么區別.
-
機器重啟: 和進程終止的情況相同.
-
機器掉電/網線斷開: 接收端認為連接還在, 一旦接收端有寫入操作, 接收端發現連接已經不在了, 就會進行 reset. 即使沒有寫入操作, TCP 自己也內置了一個保活定時器, 會定期詢問對方是否還在. 如果對方不在, 也會把連接釋放.
-
另外, 應用層的某些協議, 也有一些這樣的檢測機制. 例如 HTTP 長連接中, 也會定期檢測對方的狀態. 例如 QQ, 在 QQ 斷線之后, 也會定期嘗試重新連接.
七:🔥 TCP 小結
為什么 TCP 這么復雜? 因為要保證可靠性, 同時又盡可能的提高性能.
可靠性:
- 校驗和
- 序列號(按序到達)
- 確認應答
- 超時重發
- 連接管理
- 流量控制
- 擁塞控制
提高性能:
- 滑動窗口
- 快速重傳
- 延遲應答
- 捎帶應答
其他:
- 定時器(超時重傳定時器, 保活定時器, TIME_WAIT 定時器等)
🦋 基于 TCP 應用層協議
- HTTP
- HTTPS
- SSH
- Telnet
- FTP
- SMTP
當然, 也包括你自己寫 TCP 程序時自定義的應用層協議;
🦋 TCP/UDP 對比
🦁 我們說了 TCP 是可靠連接, 那么是不是 TCP 一定就優于 UDP 呢? TCP 和 UDP 之間的優點和缺點, 不能簡單, 絕對的進行比較。
- TCP 用于可靠傳輸的情況, 應用于文件傳輸, 重要狀態更新等場景;
- UDP 用于對高速傳輸和實時性要求較高的通信領域, 例如, 早期的 QQ, 視頻傳輸等. 另外 UDP 可以用于廣播;
🧑?💻 歸根結底, TCP 和 UDP 都是程序員的工具, 什么時機用, 具體怎么用, 還是要根據具體的需求場景去判定.
🦋 用 UDP 實現可靠傳輸(經典面試題)
📚 參考 TCP 的可靠性機制, 在應用層實現類似的邏輯;
🧑?💻 例如:
- 引入序列號, 保證數據順序和完整性;
- 引入確認應答, 確保對端收到了數據;
- 引入超時重傳, 如果隔一段時間沒有應答, 就重發數據;
- …
八:🔥 共勉
😋 以上就是我對 傳輸層協議 UDP 與 TCP
的理解, 覺得這篇博客對你有幫助的,可以點贊收藏關注支持一波~ 😉