🔥個人主頁🔥:孤寂大仙V
🌈收錄專欄🌈:計算機網絡
🌹往期回顧🌹: 【計算機網絡】傳輸層UDP協議
🔖流水不爭,爭的是滔滔不息
一、TCP協議
UDP(User Datagram Protocol)是一種無連接的傳輸層協議,提供簡單的不可靠數據傳輸服務。與TCP不同,UDP不保證數據包的順序、可靠性或重復性,但具有低延遲和低開銷的特點,適用于實時性要求高的場景。
UDP的特點
無連接:通信前無需建立連接,直接發送數據。
不可靠:不保證數據包的到達、順序或重復性。
高效:頭部開銷小(僅8字節),無流量控制、擁塞控制機制。
支持廣播和多播:可向多個目標同時發送數據。
TCP協議是面向字節流的沒有邊界的概念,不像UDP中還規定好的一個報文大大小,當有多個報文的時候也能區分出每個不同的報文。所以TCP中會出現黏包的問題,即這個報文和下一個報文黏在一起,為解決這一問題可以在應用程手動添加邊界。
確認應答機制
確認應答機制(ACK機制)是可靠傳輸協議(TCP)中的核心特征之一,用于確保數據資網絡中可靠、無誤的到達對方。
你發一條消息,我必須給你回個“收到”。你不回我就一直發,直到你回。
比如客戶端發送一個信息給服務端,服務端必須要給客戶端應答,有了服務端的應答,客戶端才能確定信息被服務端接收到了。具有應答可以保證歷史消息的可靠性。TCP協議中保證可靠性,處于核心地位的就是確認應答機制。
不需要對應答做應答
二、TCP協議段格式
4位首部長度
TCP首部的長度字段是4位,表示TCP首部的長度,以4字節(32位)為單位。最大值為15,因此TCP首部最大長度為15 × 4 = 60字節。TCP報頭的長度不是固定的,通過上圖TCP協議段格式圖,發現沒有TCP長度不像UDP那樣有UDP長度,所以TCP報文也不是固定的。這也是TCP粘包問題的根源,需要應用層自行處理邊界問題。
序號和確認序號(確認應答機制)
發送方把一段數據放進TCP報文段,打上序號發送出去,接收方收到數據后,反回一個確認報文,其中帶上期望的下一個序號。比如收到序號100的包,數據長1字節,就返回101,表示已經收到了100到101了。收到確認序號表明制定報文之前的所有信息,已經全部收到了,下一次發送從確認序號開始。
服務器不是只做應答,做的是捎帶應答(服務器應答的時候把要發送的報文加上)。所以服務器也需要對方的應答。不管是客戶端還是服務端都需要給對方發送報文報文中有序號然后得到確認序號。
16位窗口大小
發送端發送數據,取決于接收端接收緩沖區剩余空間的大小。16位窗口大小告訴發送方在未收到確認(ACK)的情況下,可以發送的最大數據量,從而避免發送方發送過多數據導致接收方緩沖區溢出。16位窗口大小作為報文中的一個字段,在報文在網絡中傳輸的時候將自己的16位窗口大小信息給對方。
6個保留位
六個保留位是六個比特位,URG、ACK、PSH、RST、SYN、FIN、。六個保留位是標志位,本質就是報頭中的比特位。注意:網絡中傳輸的是整個報文信息,報文中有報頭和有效載荷,這些字段在有效載荷中。
**設計保留位是為了讓接收方收到不同的TCP報文,針對不同報文的類型,接收方要有不同的行為做法。
**
從這里先引入三次握手四次揮手,后面細聊
雖然好多圖都這么畫,但是在網絡中傳輸的是報文,報頭里面有字段是表示連接表示應答等等。
SYN
SYN是同步標志位,建立連接用的,握手過程使用的標志位。前兩次握手,不能帶數據只有報頭發送給對方,三次握手沒有完成就不能發送數據。這三次握手就是發送端和接收端的協商。
ACK
確認號是否有效,表明報文是一個應答報文,ACK標志位幾乎常設位1。
FIN
通知對方,本端要關閉了,我們稱帶FIN標識的位結束報文。
PSH
要求接收端應用程序立刻從TCP緩沖區把數據讀走。
當發送方設置PSH=1,接收方的TCP協議棧會盡快將緩沖區中的數據(包括該報文段)推送到應用層。
RST
對方要求重新建立連接,把攜帶RST標識的稱為復位報文段。
舉一個極端例子,發送端和客戶端建立連接進行三次握手,發送端把對服務端的應答發送完了那么發送端就認為自己就與服務端建立了連接。服務端接收到發送端的應答,這個過程是有時間差的,如果應答報文沒有被服務端接收到,那么服務端認為沒有與發送端建立連接。發送端與服務端連接建立是否成功認知不一致。這時候服務端要給發送端發送RST,讓連接重置。
其實在通信過程中連接出現任何問題,都可以進行重置。
URG 緊急指針
URG標志位是表示緊急指針是否有效。URG標志位為1表示緊急指針生效。
緊急指針的工作過程:tcp傳輸比如說在接收端緩沖區,接收到的數據是按字節流式的接收隊列,可以理解為報文在緩沖區中是有序排列的。如果有數據想被優先讀取優先處理就要用到緊急指針,例如接收端緩沖區的數據中,用戶想終止這些數據的傳輸,最后傳進來的報文在隊列的最后,這個報文也就終止的緊急數據,緊急指針就會開始運作了。
緊急指針這種行為不是主流。
三、超時重傳理解丟包
發送端給接收端發送數據,如果沒有得到應答,數據就一定丟了嗎?分為兩種情況,一種主機A向主機B發送報文確實丟包了主機B沒有收到報文,另一種主機A向主機B發送報文主機B收到了主機A的報文,但是應答報文丟失了。無法百分百保證對方是否收到信息,就無法保證可靠性。
**為了應對上述無法百分百保證對方是否收到信息的情況,發送端如果特定的時間間隔內沒有收到接受端的應答,就會觸發超時重傳,重新給接收端發送報文。**有一種情況如果發送端給接收端重傳了報文信息,但是接收端也接收到了之前第一次的報文信息,這是會出現報文重復的問題嗎?不會,因為接收端會根據序號進行去重。
這個特定的時間間隔是多少呢?
最理想的情況下,找到一個最小的時間,確保確認應答一定能在這個時間內返回。但是這個時間的長短,隨著網絡環境的不同,是有差異的。如果超時時間設的太長,會影響整體的重傳效率,如果超時時間設的太短,有可能會頻繁發送重復的包。
TCP 為了保證無論在任何環境下都能比較高性能的通信, 因此會動態計算這個最大超
時時間。Linux 中(BSD Unix 和 Windows 也是如此), 超時以 500ms 為一個單位進行控
制, 每次判定超時重發的超時時間都是 500ms 的整數倍,如果重發一次之后, 仍然得不到應答, 等待 2500ms 后再進行重傳,如果仍然得不到應答, 等待 4500ms 進行重傳. 依次類推, 以指數形式遞增,累計到一定的重傳次數, TCP 認為網絡或者對端主機出現異常, 強制關閉連接。
四、三次握手四次揮手
客戶端和服務端建立的連接,也是要通過先描述再組織管理起來的。進行管理的也是一個結構體struct_link
,結構體中包含連接的信息。所以建立連接是有成本的,時間成本和空間成本。一個臺主機操作系統建立大量的連接就會變卡,甚至開始殺建立好的連接進程。
之前寫TCP通信的代碼,客戶端需要寫connect去連接服務器,三次握手也是connect發起的。connect發起后,建立連接的過程由客戶端和服務端的操作系統自動完成。注意accept不參加三次握手,寫代碼的時候我們也發現,accept創建的套接字是用來干活的,connect才是用來建立連接的。connect() 是三次握手的起點,客戶端通過它向服務端發起 SYN 報文。三次握手完成之后,服務端的 accept() 才返回一個新的 socket 文件描述符,這個 socket 就是用來“干活”的。所以我們在代碼里看到:服務端 listen 的 socket 是用來“等人”的,accept() 拿到的 socket 是用來和客戶端通信的。
為什么三次握手?
外部條件:以最短的方式,進行驗證發送端和服務端是全雙工的,本質是兩個所處的網絡是暢通的的能支持全雙工。
通信雙方:百分百確認雙方通信意愿。
三次握手,本來就是通信雙方想要進行通信才建立鏈接的。所以在第二次握手的時候,連SYN和ACK一起發出去了,這就是捎帶應答。
為什么四次握手不進行捎帶應答
因為斷開連接時,雙方不是"同時想斷",而是一方先想斷開的。
客戶端說:我要斷了(FIN),服務端說:好,我知道了(ACK),但我還有話沒說完,等我說完再斷。服務端說:好了,我也說完了(FIN)。客戶端說:收到,咱們徹底斷了(ACK)。
四次揮手不能捎帶的本質原因是:服務端不能立即發FIN,它可能還有數據沒發完,得等緩沖區清空后再說,所以不能捎帶兩個動作,只能形成四次揮手。
狀態
CLOSE_WAIT
如圖如果客戶端與服務端之前是建立連接的,現在客戶端斷掉連接,但是服務端沒有斷掉連接,服務端就處于CLOSE_WAIT的狀態,服務端依舊占用文件描述符,連接沒有釋放。此時服務端可以給客戶端發消息。與上述四次揮手為什么不進行捎帶應答對應。
TIME_WAIT
主動斷開連接的一方會進入TIME_WAIT狀態,即使四次握手已經完成(就是如上圖客戶端認為自己發送了ACK就完成四次揮手了)。發送完最后一個 ACK 后,才進入 TIME_WAIT 狀態,并開始等待 2MSL(最大報文段生存時間)。等待2MSL讓兩個傳輸方向上尚未被接收到的或遲到的報文消散。
假設一個極端情況——客戶端發送了數據包后立刻關閉連接,結果數據包因為網絡擁堵延遲漂在半路。此時客戶端又快速重啟,并復用了原來的端口號。如果沒有 TIME_WAIT,那個“幽靈數據包”一旦抵達服務器,可能會被誤認為是新的連接數據,導致數據混亂甚至安全問題。
所以為什么要等 2MSL?
確保對方收到了剛才你發的那個最后的 ACK。如果對方沒收到,會重發 FIN;那你還在 TIME_WAIT 狀態中,可以再發一次 ACK。
同時避免舊連接殘留的數據影響之后新建立的連接。
我們發現我們的代碼連接服務器用的端口號,用完一次短時間內不能復用。 TCP 的 TIME_WAIT 狀態限制了端口的短時間復用,就是為了保證連接的安全性和協議的嚴謹性,不是你斷了連接馬上就能拿這個端口再用一遍的。還是上述那個極端情況,重新建立連接后,因為綁定了新的端口號,源端口和目的端口匹配不上,"幽靈數據包👻"就自動丟棄了。
五、滑動窗口
滑動窗口是 TCP 協議中實現“流量控制”和“可靠傳輸”的核心機制之一。可以把它想成一個動態變化的“可發送/可接收數據范圍”,幫助發送方和接收方協調通信節奏,防止“你發我收不過來”的情況。發送方向對方發送多少數據是由滑動窗口決定的。
滑動窗口就是一個數組,由兩個指針維護,就像算法中的那個滑動窗口一下。已經發送的那個區域,就是這部分空間的數據已經被利用了這部分數據已經無效了,不需要刻意清空緩沖區,前面聊過序號隨著發送序號會依次增大,滑動窗口未來是向右滑動的。
滑動窗口的大小=對方的接收能力。發送方滑動窗口,維護滑動窗口的start相當于是對方的確認序號,end相當于start+對方的整個窗口大小。**注意:接收方也有滑動窗口!這個窗口是 整個接收緩沖區中還沒有被填滿的那一部分,每次接收到新的數據并交付給上層應用,緩沖區就釋放了,窗口也就向前滑動。**滑動窗口不是整個緩沖區,而是“緩沖區中還空著”的部分。
滑動窗口的本質是流量控制的具體實現方案。
發送方的滑動窗口肯定不能向左滑動。滑動窗口是可以變大、變小或者不變的,因為滑動窗口的大小=對方的接收能力,如果接收方緩沖區內的未向上交付的數據太多,這些數據都在緩沖區中,那么接收方的滑動窗口就變小了,發送方的滑動窗口相對應肯定也就會變小。如果接收方緩沖區內數據沒有堆積,那么接收方的滑動窗口就變大了,發送方的滑動窗口相對應也就會變大。不變就是比較穩定的情況。
如果滑動窗口交付數據丟包怎么辦?
這里把滑動窗口報完丟失歸為三類,最左側報文丟失、中間報文丟失、最右側報文丟失。
最左側報文丟失,分為最左側報文對應的應答丟失了和最左側報文真的丟失了。
這種情況下, 部分 ACK 丟了并不要緊, 因為可以通過后續的 ACK 進行確認。確認序號表示這個確認序號之前的報文全都收到了。也就是說,如果1-1000的確認序號應該是1001但是沒有收到,下一個10001-2000的確認序號2001收到了,根據確認序號表示確認序號之前的報文全部收到了,就可以確認2001之前的報文全部收到了。這樣發送方就可以確認1-1000的報文沒有丟。
如果報文真丟了,發送端會一直收到 1001 這樣的 ACK, 就像是在提醒發送端 “我想要的是 1001” 一樣。如果發送端主機連續三次收到了同樣一個 “1001” 這樣的應答, 就會將對應的數據 1001 - 2000 重新發送。這個時候接收端收到了 1001 之后, 再次返回的 ACK 就是 7001 了(因為 2001 -7000)接收端其實之前就已經收到了, 被放到了接收端操作系統內核的接收緩沖區中。這個機制叫做快重傳。
上面說的是最左側報文丟失,其實中間報文丟失和最右側報文丟失,都會轉化為最左側報文丟失。因為如果是中間報文丟失,左邊沒有丟失的話,左邊就正常發送報文,相當于滑動窗口的start指向了中間報文的開頭,中間報文就成了最左側報文。
滑動窗口不會跳過報文進行應答,確認序號定義決定,確認消息一定要連續確認,所以要連續發送。所以滑動窗口發報文肯定是連續的,不能跳躍。由于 TCP 使用累計確認機制,滑動窗口內的數據必須按序連續發送與確認,不能跳躍發送或跳躍確認,否則窗口無法前移。
滑動窗口總是向右滑動,不會“向左滑”或“跳躍確認”。雖然序號值是有限的(32 位),但當達到最大值后會回繞,從 0 重新開始,因此可以將整個序號空間理解為一個環形結構,滑動窗口在其中不斷推進。
快重傳 vs 超時重傳
要想進行快重傳得滿足一個條件,收到三個同樣的確認應答時則進行重發。超時重傳上面聊過,即使一個報文在一段時間間隔內沒有收到應答,會進行重傳。在這里超時重傳更像是快重傳的一種兜底策略。
六、流量控制
接收端處理數據的速度是有限的. 如果發送端發的太快, 導致接收端的緩沖區被打滿, 這個時候如果發送端繼續發送, 就會造成丟包, 繼而引起丟包重傳等等一系列連鎖反應。
因此 TCP 支持根據接收端的處理能力, 來決定發送端的發送速度. 這個機制就叫做流量控制。
接收端將自己可以接收的緩沖區大小放入 TCP 首部中的 “窗口大小” 字段, 通過 ACK 端通知發送端。
窗口大小字段越大, 說明網絡的吞吐量越高。
接收端一旦發現自己的緩沖區快滿了, 就會將窗口大小設置成1一個更小的值通知給發送端;
發送端接受到這個窗口之后, 就會減慢自己的發送速度。
如果接收端緩沖區滿了, 就會將窗口置為 0; 這時發送方不再發送數據, 但是需要定期發送一個窗口探測數據段, 使接收端把窗口大小告訴發送端。
接收端把窗口大小告訴發送端,CP 首部中, 有一個 16 位窗口字段就是存放了窗口大小信息。16 位數字最大表示 65535, 那么 TCP 窗口最大就是 65535 字節么?實際上, TCP 首部 40 字節選項中還包含了一個窗口擴大因子 M, 實際窗口大小是 窗口
字段的值左移 M 位。
七、擁塞控制
網絡通訊中不只和發送方和接收方有關,也需要考慮網絡的問題。TCP不僅僅考慮了雙方主機的問題,還考慮了網絡本身的問題。當網絡大面積丟包,發送方判定出現了網路擁塞的問題。網絡擁塞不能理解重發,所有發送方都立即重發不就成了堵上加堵了嗎。這里要理解在網絡通信中,有許多發送端主機和許多接收端主機,都需要通過這一個網絡進行通信。
當網絡擁塞時,這一個網絡下的,多個發送端的多個主機都采用擁塞控制這種策略。TCP 引入 慢啟動 機制, 先發少量的數據, 探探路, 摸清當前的網絡擁堵狀態, 再決定按照多大的速度傳輸數據。
**擁塞窗口是一個臨界值,值以內網絡大概率不擁塞,值以上,網絡可能擁塞。**擁塞窗口是在計算機內衡量網絡是否會擁堵的一個指標。網絡好壞是會變化的,所以也就決定了擁塞窗口也一定到進行更新變化。
發送方的滑動窗口其實不單單是由對端滑動窗口大小決定的,擁塞窗口和對方滑動窗口誰小誰是發送端的滑動窗口大小。發送開始的時候, 定義擁塞窗口大小為 1。每次收到一個 ACK 應答, 擁塞窗口加 1。每次發送數據包的時候, 將擁塞窗口和接收端主機反饋的窗口大小做比較, 取較小的值作為實際發送的窗口。
慢啟動開始,如上圖擁塞窗口增速非常快,指數級增長,但是不是一直指數級增長的。為了盡快恢復網絡通信所以一開始要快一點,解決擁塞問題(網絡擁塞就是網絡中報文太多造成堵塞,所以一開始當前網絡所有發送方發送報文慢慢發送)。等到線性增長的階段,本質是探測網絡的擁塞窗口的值。
慢啟動開始,擁塞窗口指數級增長,到達一定閾值就會開始線性增長。擁塞窗口在增加,發送的數據量不一定是在增加的,上面也說過每次發送數據包的時候, 將擁塞窗口和接收端主機反饋的窗口大小做比較, 取較小的值作為實際發送的窗口,所以是不一定的。線性增長到一定程度,這時候網絡又開始堵了,又重新從1開始慢啟動,但是這次慢啟動到線性增長的閾值是上一次網絡擁塞窗口大小的一半。
擁塞控制, 歸根結底是 TCP 協議想盡可能快的把數據傳輸給對方, 但是又要避免給網絡造成太大壓力的折中方案
八、延遲應答
如果接收數據的主機立刻返回 ACK 應答, 這時候返回的窗口可能比較小。
假設接收端緩沖區為 1M, 一次收到了 500K 的數據;。如果立刻應答, 返回的窗口就是 500K。但實際上可能處理端處理的速度很快, 10ms 之內就把 500K 數據從緩沖區消費掉了。在這種情況下, 接收端處理還遠沒有達到自己的極限, 即使窗口再放大一些, 也能處理過來。如果接收端稍微等一會再應答, 比如等待 200ms 再應答, 那么這個時候返回窗口大小就是 1M。
一定要記得, 窗口越大, 網絡吞吐量就越大, 傳輸效率就越高. 我們的目標是在保證網絡不擁塞的情況下盡量提高傳輸效率。
九、捎帶應答
“捎帶應答”(piggybacking)是網絡通信中的一個術語,通常出現在面向連接的協議(如TCP)中,指的是在發送確認(ACK)報文的同時,順便攜帶要發送的數據,從而提高通信效率。
十、TCP異常
進程終止: 進程終止會釋放文件描述符, 仍然可以發送 FIN和正常關閉沒有什么區別。
機器重啟: 和進程終止的情況相同。
機器掉電/網線斷開: 接收端認為連接還在, 一旦接收端有寫入操作, 接收端發現連接已經不在了, 就會進行 reset. 即使沒有寫入操作, TCP 自己也內置了一個保活定時器, 會定期詢問對方是否還在. 如果對方不在, 也會把連接釋放。
另外, 應用層的某些協議, 也有一些這樣的檢測機制. 例如 HTTP 長連接中, 也會定期檢測對方的狀態. 例如 QQ, 在 QQ 斷線之后, 也會定期嘗試重新連接。