傳輸層協議TCP(下)

?上一篇https://blog.csdn.net/Small_entreprene/article/details/148193741?sharetype=blogdetail&sharerId=148193741&sharerefer=PC&sharesource=Small_entreprene&sharefrom=mp_from_link

接下來,我們來談論TCP具體的機制!

具體TCP機制

確認應答(ACK)機制

TCP 將每個字節的數據都進行了編號,即為序列號。

這個ACK的理解我們在上一篇的時候就已經談論清楚了!

不過要注意:確認應答必須將ACK標志位置為1。


下面,我們來好好理解一下序號!!!

我們知道TCP是有兩個緩沖區的:一個發送,一個接收!而且是面向字節流的,所以我們可以將發送緩沖區看成是一個char類型的字符數組!?

char outbuff[N];

那么操作系統看待緩沖區不就是看待一個字符數組了嘛,這不就是字節流了嗎?!流就是數組,那么從發送緩沖區拷貝下來的每一個字節,天然的不久有了編號了嗎!不就是在發送緩沖區對應的數組下標了嘛!

我們的這個理解并不嚴謹,我們在滑動窗口中會有更好的理解!

取數據不就是用確認應答的確認序號到序號所對應的下標的整體數組范圍嘛!這是我們第一版的理解!

每一個 ACK 都帶有對應的確認序列號, 意思是告訴發送者, 我已經收到了哪些數據,下一次你從哪里開始發!假設接收到的應答序號是1001,那是不是就表明下一次發送的序號就是1001呢?不是的!不然為什么還要有兩個序號,所以這個序號不是1001,是像上上圖的2000!!!

可靠性的本質就是我收到應答了,那么說明我剛發送的對方收到了!!!---ACK。

超時重傳機制

我們要談重傳,就需要先好好理解丟包!那么什么是丟包呢?如何理解?

我們以一個報文為例:丟包的情況就兩種:

1.應答前丟:真的是數據包丟了

主機A發送數據給B之后,可能因為網絡擁堵等原因,數據無法到達主機B;

如果主機A在一個特定時間間隔內沒有收到B發來的確認應答,就會進行重發;

2.應答后丟:數據包收到了,但是應答數據丟了

但是,主機A未收到B發來的確認應答,也可能是因為ACK丟失了:

結合上面的兩種情況,所以對于發送方,沒有收到應答ACK,意味著什么?意味著丟包嗎?不完全是,只能意味著數據可能丟失 ,無法100%確認對方收到消息,也就是無法保證可靠性,當前情況下,也就是看上圖是無法確認是數據丟還是應答丟的(后面會有解決辦法)!

這就需要等待特定的時間間隔,如果發送方在特定的時間間隔內沒有收到對端的ACK應答,那么發送發就會判定報文丟失!!!是主觀,不是客觀!

所以說:超時重傳的策略就出來了!

還有對于第二種丟包問題,不就是會導致重復問題嗎?

這個是可以甄別出來的!因為報文有序號的!!!主機B會收到很多重復數據。那么TCP協議需要能夠識別出那些包是重復的包,并且把重復的丟棄掉。序列號,就可以很容易做到去重的效果。

所以序號的作用:確認應答,按序達到,去重!!!

我們現在就可以根據特定的時間間隔,如果收不到應答,發送端就會判定報文丟失!所以,這個特定的時間間隔應該是多長啊?

發送端等的就是應答,那么我們是知道的:數據的傳輸是需要經過網絡的,如果網不好,那么一來一回的就會比較久,要什么排隊啥的,時間肯定變長了,如果等少了,就可能重復發生重傳,如果網絡特別好,那么時間間隔反而也是很長的話就會導致浪費時間了,所以因為網絡是變化的,因此:

等待的時長必須是變化的!那么具體是怎么變的呢?

TCP 為了保證無論在任何環境下都能比較高性能的通信,因此會動態計算這個最大超時時間。

  • Linux 中 (BSD Unix 和 Windows 也是如此),超時以 500ms 為一個單位進行控制,每次判定超時重發的超時時間都是 500ms 的整數倍。

  • 如果重發一次之后,仍然得不到應答,等待 2*500ms 后再進行重傳。

  • 如果仍然得不到應答,等待 4*500ms 進行重傳。依次類推,以指數形式遞增

  • 累計到一定的重傳次數,TCP 認為網絡或者對端主機出現異常,強制關閉連接。

不斷的改變時間進行重傳,就是在探索網絡當前的超時時長!如果下一次4*500ms后接收到了應答報文,那么下一次就直接使用4*500ms的首次等待時長。

連接管理機制

在正常情況下,TCP要經過三次握手建立連接,四次揮手斷開連接,我們上面為了更好理解相關概念,提前淺淺談了一下,下面我們來詳細說說!

三次握手:建立連接

圖中的SYN_SENT,SYN_RCVD等等,都是表示雙方主機的狀態,整個就是狀態變化,在內核層面上,所謂的狀態其實就是一個整數!也就是宏值!

多個客戶端可以向服務端建立連接,每個還處于不同狀態,這不就是在服務端需要進行管理嗎!?不就是先描述再組織嗎?!所以服務端需要管理這些連接!?

那不就是可以被結構體規范,管理起來嗎?在內核中,用于管理TCP連接的結構體通常是 struct tcp_sock(在Linux內核中)。這個結構體包含了TCP連接的所有相關信息,包括連接的狀態、發送和接收緩沖區、擁塞控制信息、定時器等。通過這個結構體,內核能夠有效地管理和跟蹤每個TCP連接的狀態和行為。

struct tcp_sock(簡化)
#include <linux/inet.h>
#include <linux/sk_buff.h>
#include <linux/tcp.h>// 定義TCP套接字結構體
struct tcp_sock {// 繼承自inet_connection_sock,包含通用的連接套接字字段struct inet_connection_sock inet_conn;// TCP頭部長度u16 tcp_header_len;// 快速路徑處理數據的頭部預測標志__be32 pred_flags;// 下一個期望接收的序列號u32 rcv_nxt;// 未讀數據的頭部u32 copied_seq;// 最后一次窗口更新發送時的rcv_nxtu32 rcv_wup;// 下一個要發送的序列號u32 snd_nxt;// 要發送的下一個確認號u32 snd_una;// 接收窗口大小u32 rcv_wnd;// 接收窗口左邊緣u32 rcv_wnd_left;// 發送窗口大小u32 snd_wnd;// 發送窗口左邊緣u32 snd_wnd_left;// 其他TCP相關字段...
};

正因為需要創建數據結構對象,花時間,花空間,所以TCP建立連接就會有成本!這也是為什么學校選課會卡,就是連接不斷的被建立,越來越多就會導致管理成本增加,甚至到內存不足,以至于操作系統殺進程(服務),也就服務器崩掉了!

由圖中的服務端:socket-bind-listen-accept到這就阻塞住了,然后到客戶端:connect發起連接請求!connect是發起三次握手的,也就是為什么需要傳入IP和端口!當然了,是發起三次握手,后續的三次握手具體的過程是由client OS自己完成的!跟connect沒有關系!等連接成功了,connect才會返回!

對于accept,我們之前的TCP編程的時候,其實不使用accept接口,就設置listen狀態,其實好像也是可以連接上服務器的。所以這說明了什么?就是accept不參與三次握手!!!三次握手依舊是由server OS和client OS雙方操作系統自動完成!?

accept直接將建立好的連接拿上去就行了!什么叫做把連接拿上去呢?我們后面會說!大概就是拿到文件描述符,這就相當于在accept的時候可以創建struct file對象,然后將來再讓這個struct file和獲得的這個連接建立某種關聯,這不就可以通過文件訪問這個連接了嘛!后面說!😜

三次握手是TCP進行通信之前必須要做的,為什么需要三次握手啊???---兩個原因

  • 三次握手是以最短的方式進行驗證全雙工的!客戶端能發能收就是客戶端的全雙工,服務端能發能收就是服務端的全雙工!(結合ACK機制體現,雙方地位是需要對等的)(驗證全雙工本質就是驗證我們兩個所處的網絡是通暢的,能夠支持全雙工)(父母等外部因素
  • 三次握手是四次握手中由捎帶應答壓縮帶來的產物,雙向的SYN其實就是再說你愿意嗎---我愿意,所以就是在以最小成本,100%確認雙方的通信意愿(你情我愿
  • 結婚!

三次握手的本質其實就是四次握手,因為服務端默認都會對客戶端做應答,還有捎帶應答的機制,所以可以壓縮為三次握手!

記下來,我們來談談四次揮手:斷開連接

斷開連接的本質就是建立雙方斷開連接的共識!具體的做法就是必須要保證客戶端向服務端100%發送對應的斷開連接請求,服務端向客戶端100%發送自己也要斷開的請求!因為TCP是全雙工的,也就意味著雙方之間發送消息是可以同時進行的,建立雙方斷開連接的共識,一個具象化的認識就是:

客戶端向服務器發送FIN,本質就是客戶端給服務器說:“我要發的數據已經發完了,我要和你斷開連接。”本質就是斷開全雙工的一條,即斷開client->server;

ACK就是確保客戶端向服務端說的話,100%收到了!

同樣的道理,TCP是全雙工的,此時服務端可能向客戶端的數據還沒有發完(這也是不能合并的原因,雙方的關閉時間不一定是同一時刻,不太好壓縮),就需要等發送完了,才會向客戶端發送FIN,:“我也要斷開連接了”!

ACK就是確保服務端向客戶端說的話,100%收到了!

這時候,雙方就建立了斷開連接的共識了,也就是四次揮手!

為什么需要四次揮手來斷開鏈接呢?

  • 四次揮手以最短次數,最小成本來建立了雙方在全雙工之下的斷開連接的請求!

不過,客戶端四次揮手的時候,將文件描述符close了,那么對端服務器還需要向客戶端發送數據的時候,此時客戶端的文件描述符不是已經關閉了嗎?客戶端不是讀不了了嗎???那該怎么辦?

所以Linux系統為了支持我們能夠對連接進行半關閉,也就是說客戶端想要斷開連接了,想要關閉自己的寫端,那就將寫端關閉,但是文件描述符不做釋放,后面還可以進行讀取數據!這就由全雙工退化成半/單雙工了!

在Linux中,shutdown 系統調用用于關閉一個套接字的特定端(讀端、寫端或兩者)。這個調用可以用于TCP連接,以優雅地關閉連接的一部分,從而允許數據的完全傳輸。

#include <sys/socket.h>int shutdown(int sockfd, int how);

sockfd:套接字文件描述符,必須是通過 socket() 調用創建的。

how:指定如何關閉套接字。可以是以下值之一:

  • SHUT_RD:關閉讀端。不再從套接字接收數據。

  • SHUT_WR:關閉寫端。不再向套接字發送數據。

  • SHUT_RDWR:關閉讀寫兩端。

其實一般我們在TCP網絡套接字編程的時候,我們使用close接口就行了,我們前面數據該發的我們都做了,另外四次揮手本質上也是有雙方的操作系統自己完成的。

需要注意的是:

當客戶端向服務端發送關閉連接請求,服務端就會處于CLOSE_WAIT的狀態,依舊占用文件描述符,連接也沒有釋放,這就帶來如果不關,可用的文件描述符就會越來越少,這就是文件描述符泄漏問題,文件描述符也是有限的資源的!所以fd用完了,就必須要關掉!!!

主動斷開連接的一方,在將最后一次發送ACK的時候,就代表著四次揮手完成了,但是不能說主動方就直接變為CLOSED狀態而是轉會為TIME_WAIT狀態,后續還需要等待一定的時間,才能設置為CLOSED狀態!

現在做一個測試,首先啟動 server,然后啟動 client,然后用 Ctrl-C 使 server 終止,這時馬 上再運行 server, 結果是:

這是因為,雖然 server 的應用程序終止了,但 TCP 協議層的連接并沒有完全斷開,因此不 能再次監 聽同樣的 server 端口. 我們用 netstat 命令查看一下:

那么TIME_WAIT的時間是多久?還有為什么需要TIME_WAIT?

TCP 協議規定,主動關閉連接的一方要處于 TIME_WAIT 狀態,等待兩個 MSL(maximum segment lifetime) (最大報文的存活時間)的時間后才能回到 CLOSED 狀態。說人話就是。我把一個報文從我的主機發送到網絡里,我歷史上發送了很多報文,發送出去的報文,其中有一個統計數據,是用來衡量一個報文在網絡里存放的最長時間,也就是將一個報文發送到網絡當中,歷史發了許多報文,基于歷史做統計,我能大概估算出來,我發出去的報文曾經在網絡存活的最長時間,這就是報文的最大存活時間。

為什么又是兩個MSL呢?MSL 是 TCP 報文的最大生存時間,因此 TIME_WAIT 持續存在 2MSL 的話,就能保證在兩個傳輸方向上的尚未被接收或遲到的報文段都已經消失(否則服務器立刻重啟,可能會收到來自上一個進程的遲到的數據,但是這種數據很可能是錯誤的);同時也是在理論上保證最后一個報文可靠到達(假設最后一個 ACK 丟失,那么服務器會再重發一個 FIN。這時雖然客戶端的進程不在了,但是 TCP 連接還在,仍然可以重發 LAST_ACK);因為是雙向的,兩個MSL就是保證兩個方向上的報文能夠在網絡當中消散!

“保證在兩個傳輸方向上的尚未被接收或遲到的報文段都已經消失”,其實我們就是可以理解為有一種情況:發送的數據DATA沒有丟,還在某一個路由器中排隊,而且發送方已經判定超時重傳了,后續甚至連接關閉了,報文還在網絡中存活,也就是說一旦我們將連接全部關閉了,不考慮什么TIME_WAIT,假設四次揮手完成了,雙方連接直接關閉,那么在網絡中可能會殘存里上上補發的數據,后來這個客戶端立即再重啟,端口和之前的一樣,那么在網絡中的報文就可能到服務端了,這個報文到來的時候,就可能會影響下一次連接的建立和通信的過程!

所以我們需要TIME_WAIT,本質就是需要兩個MSL的時間來確保安全性!

所以在TIME_WAIT期間,老端口就不會能夠理解使用了,如果對應的客戶端想要再次重啟,就需要強制更換端口號,就能保證歷史上沒有消散的報文,源端口和目的端口就和新建立的源端口和目的端口就配不上,所以客戶端和服務端就可以對這樣的報文進行自動丟棄了!?

在我們應用場景中,就比如PDD,在618,雙十一的時候,如果客戶一下子太多了,將PDD的服務器搞崩了的話?,那么PDD就是主動斷開連接的一方,那不就不能立即重啟了嗎?可是這不是無法重啟了嗎?一旦無法重啟就會導致服務器有一段時間沒有辦法提供服務了,耽誤一秒鐘就是幾千萬了!!!所以,雖然我們有TIME_WAIT,但是我們在編程應用角度,我們可以實現讓主動斷開的一方既可以進行TIME_WAIT,又要讓服務器立即重啟!不能理解重啟是因為歷史端口號可能被占用了,所以我們可以使用:系統調用:setsockpot

在創建socket套接字后立馬進行:使用setsockopt()設置socket描述符的選項SO_REUSEADDR為1,表示允許創建端口號相同但IP地址不同的多個socket描述符

#include <sys/types.h>
#include <sys/socket.h>int pot = 1;
int setsockopt(int listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

可是上面不是才剛剛說過嗎?可能會有歷史的報文會影響連接和通信?那肯定是可能的!所以TCP除了使用TIME_WAIT的兩個MSL時間的策略來保證對歷史遺留的報文做過略,客戶端和服務器通信的時候,還有一個大殺器:TCP報文當中的序號!!!?

當我們在進行通信的時候,對應的遺留的報文的序號和新建立連接時的序號不一定會對得上,就是作為服務器方,會收到很多報文,會做ACK,做ACK得時候,是知道下一次期望收到得序號的,其實三次握手之后,如果服務器的接收能力是5000個字節,那么假設對端客戶端想要從序號1000開始,因為其實起始序號是隨機的,那么服務端只能接收1000~6000序號的報文了,如果陳舊報文的序號是7000,那么不久丟棄掉了嘛!如果序號不匹配是可以被丟棄的!即便是這樣的策略,也有可能會出現問題,但是這時候的概率就已經很低了!當然還有針對這個問題設計了多種策略!我們其實再深一點就可以不用考慮了!

MSL 在 RFC1122 中規定為兩分鐘,但是各操作系統的實現不同,在 Centos7 上,默認配置的值是 60s。可以通過 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看 msl 的值:

lfz@hcss-ecs-ff0f:~$ cat /proc/sys/net/ipv4/tcp_fin_timeout
60

滑動窗口

對于滑動窗口的知識,我們前文做了鋪墊:下面我們來回顧一下:

通過確認應答策略,對每一個發送的數據段,都要給一個 ACK 確認應答。收到 ACK 后再發送下一個數據段。這樣做有一個比較大的缺點,就是性能較差,尤其是在數據往返時間較長的時候。

既然這樣一發一收的方式性能較低,那么我們一次發送多條數據,就可以大大地提高性能(其實是將多個段的等待時間重疊在一起了)。

為了能夠讓主機A能夠的向主機B發送批量消息,我們就需要在主機A中規定一個數字 --- 一個無需等待確認應答而可以繼續發送數據的最大值,這個值就是窗口大小,那么這個窗口就是滑動窗口!

那么現在主機A可以一次性批量的向主機B一次性發送多條報文了,那么主機A可以發送多少個報文是由什么決定的?

那就是由滑動窗口大小所決定了!?

那么什么是滑動窗口啊?在哪里體現的滑動窗口啊!?

在TCP協議中,我們之前也是認識到TCP的發送/接收緩沖區,數據的發送不是取決于用戶,因為我們調用的send/write只是將我們想要發送的數據傳輸到了下一層的TCP的接收緩沖區,什么時候發送是取決于操作系統的,滑動窗口是我們可以向對方發送數據,暫時可以不需要應答,在哪里呢?就是在主機A的發送緩沖區的一部分!!!

我們其實可以將緩沖區看成:char類型的一維數組:

char outbuffer[N];//邏輯上的認知

然后滑動窗口就有相對應的 start 和 end 的下標,【start,end】以內的就是直接可以向對端主機B發送的數據了!

正因為有滑動窗口的存在,就將發送緩沖區分成了三部分:

  1. 已發送已確認
  2. 可以直接發,暫時不要應答
  3. 待發送/未發送

左邊部分:已發送,收到ACK的,不就是這部分數據無效了,就是這部分空間可以再被利用了!所以在網絡通信中,我們不需要刻意的清空緩沖區,而只要讓數據無效就可以,只要對應的在滑動窗口的左側就可以體現無效性了!

我們知道,序號在發送的輪次中,數字是在不斷的增大的,也就是意味著滑動窗口未來需要向右滑動,那么滑動窗口的本質不就是讓滑動窗口的 start 和 end 下標增加嘛!

滑動窗口以內的數據是直接可以發送的,所以滑動窗口的大小是由什么來決定的?

滑動窗口的定義是可以直接發,暫時不需要應答,那么如果把滑動窗口的范圍擴得太大,就可能會導致給對方發送太多的數據,導致對方來不及接收,相反,如果我們給對方發送的數據量太小,就會導致單次發送數據量太小,效率降低!所以滑動窗口的大小最終是由對端的接受能力決定的!

所以!滑動窗口的本質是流量控制的具體實現方案!!!滑動窗口是決定發多少的依據!

我們之前認識到TCP協議中的窗口關鍵字,對端ACK會告訴發送端當前的接受能力,在收到對端對應的報文時,怎么調整滑動窗口對應的大小呢?

start = 對端ACK的報文中的確認序號end = start + 對端給我通告的窗口大小

記下來,我們來談談幾個問題:


滑動窗口可以向左滑動嗎?

滑動窗口是不會向左滑動的,因為從概念上講,滑動窗口的左側是已發送已確認數據,如果將滑動窗口向左滑動,就會將已發送已確認的數據再發送一遍,這是不合理的!還有上面的右移的計算方法,顯然是不能向左移動的!


滑動窗口,可以變大嗎?可以變小嗎?可以不變嗎?可以為0嗎?

滑動窗口的大小是一個數字,通常用字節(Bytes)來表示。它反映了接收方當前可用的緩沖區大小。窗口大小會根據接收方的緩沖區情況動態調整。

  • 窗口大小增大:表示接收方的緩沖區空間增加,發送方可以發送更多的數據。

  • 窗口大小減小:表示接收方的緩沖區空間減少,發送方需要減少發送的數據量。

  • 窗口大小不變:表示接收方的緩沖區空間沒變,發送方可以按照上一次的數據量進行發送。

  • 窗口大小為0:表示接收方的緩沖區已滿,發送方需要停止發送數據,直到接收方處理完部分數據并釋放緩沖區。(樣例:start++,end不變,直到start == end)


如果報文丟了怎么辦?滑動窗口,會不會跳過報文進行應答?(重點)

我們假設當前的滑動窗口的范圍是【1001,5001】,假設發送1001到2000的報文,沒有收到對應的應答,那么我們將來肯定是要將該報文進行重發了,那么沒有收到對應的應答,滑動窗口會進行更新嗎?

對于丟包問題,宏觀上無非就三種情形:實際丟包肯定更復雜,是下面三種情況的自由組合!

  1. 在滑動窗口的數據段中,最左側的對應報文丟失。
  2. 中間的報文數據丟失。
  3. 最右側的對應的報文丟失。

對于最左側的情形: (最左側就是要點)

情況一:?數據包丟了。

假設主機B收到了【2001,5000】的三個報文,然而【1001,2000】的最左側的報文數據丟失了,那么對沒有丟失的三個報文的應答的確認序號應該是多少呢?

填的都是1001!!!所以滑動窗口將來左側不做更新,即start的位置沒有發生改變,因為確認序號的意思是該確認序號之前的報文已經全部收到了!

  • 當1001-2000的數據包丟失后,發送端會一直收到確認序號為1001的響應報文,就是在提醒發送端“下一次應該從序號為1001的字節數據開始發送”。
  • 如果發送端連續收到三次確認序號為1001的響應報文,此時就會將1001-2000的數據包重新進行發送。
  • 此時當接收端收到1001-2000的數據包后,就會直接發送確認序號為6001的響應報文,因為2001-6000的數據接收端其實在之前就已經收到了。

這種機制被稱為“高速重發控制”,也叫做“快重傳”。

需要注意的是,快重傳需要在大量的數據重傳和個別的數據重傳之間做平衡,實際這個例子當中發送端并不知道是1001-2000這個數據包丟了,當發送端重復收到確認序號為1001的響應報文時,理論上發送端應該將1001-5000的數據全部進行重傳,但這樣可能會導致大量數據被重復傳送,所以發送端可以嘗試先把1001-2000的數據包進行重發,然后根據重發后的得到的確認序號繼續決定是否需要重發其它數據包。

快重傳和超時重傳都是對對應報文進行重傳的機制,快重傳觸發是需要有條件的,是需要收到3個同樣的確認應答!超時重傳的條件是超時了。但是如果只發了兩個報文的話,一個報文丟了,這樣只會收到一個ACK啊,那么收不到連續三個以上的相同確認應答的ACK,此時就必須要使用超時重傳了,所以超時重傳是用來兜底的!快重傳是在重傳角度提高效率的!!!?

情況二:?數據包已經抵達,ACK丟包。

如果【1001,2000】不是報文數據丟了,而是應答丟了,也就是說主機B這四個報文全部收到了,只不過在應答的時候,對于最左側的應答報文主機A沒有收到,那么對于另外三個報文的應答的確認序號應該是多少呢?

收到【2001,3000】填的是3001,收到【3001,4000】填的是4001,收到【4001,5000】填的是5001!所以即便是【1001,2000】丟失了,只要后面的報文是收到的,就會讓滑動窗口向右更新。

所以,對于最左側的丟失:有兩種情況:

數據真的丟了:滑動窗口左側不變

數據收到了,應答丟了:滑動窗口正常工作(因為確認序號的定義就是確認序號之前的已經全部收到了,難道不是嗎?只是說應答丟了,不影響!!!😝)(在發送端連續發送多個報文數據時,部分ACK丟包并不要緊,此時可以通過后續的ACK進行確認)

還有通過丟包問題,我們就可以理解說:TCP發出的報文,暫時還沒有應答的時候,必須讓對應的報文暫時保存起來,以方便后續的重傳!!!那么對已發送的報文保存起來,是保存到哪里?如何理解保存?

滑動窗口的定義不就是把報文數據統一發出去,可以暫時不需要應答,但是在將滑動窗口中的數據發送出去的時候,沒有任何應答時,滑動窗口不會向右滑動,當收到響應的應答的時候,才有可能向右滑動,就像上面丟了最左側的就start就不會動了😊所以向右滑動的本質就是把對應數據刪除!!!所以保存在滑動窗口當中!怎么理解這個保存,就是這個窗口不要動!

所以超時重傳和快重傳的底層支持是滑動窗口!!!不僅僅與流量控制有關,還與重傳有關!!!

如果中間報文丟失,意思就是左邊報文收到了,滑動窗口首先是會向右滑動的!!!本質就轉化為最左側報文丟失的情況了!!!最右側丟失何嘗不也是會轉化為最左側報文丟失了嘛!!!

報文丟失---》最左側丟失---》超時重傳/快重傳,我們注意的是:

確認序號一定是連續的!!!---》發送必須連續發送!!!---》滑動窗口不能跳躍!!!(支撐)


滑動窗口,一直向右,會不會溢出??

這肯定是不會的!重點是我們如何理解為什么不會溢出!我們對于緩沖區的數組認知其實是抽象出來的,我們可以將其再加上一層抽象認知:我們可將char類型的數組想象成一個環形區域!(TCP確實也是這么設計的,但是他的環形并不是基于數組的,底層的報文都是以隊列的形式呈現的,報文節點不連續。)?

這好像和上面的分成三部分(已發已收-待應答-待發)沖突,不過在環形中有一個分界點,一樣的還是左側已發送已應答,右邊待發送!


流量控制

其實之前的文章的內容中基本就已經將流量控制說完了,我們可以復習一下,順便補充幾點知識!

接收端處理數據的速度是有限的。如果發送端發的太快,導致接收端的緩沖區被打滿,這個時候如果發送端繼續發送,就會造成丟包,繼而引起丟包重傳等等一系列連鎖反應。在這里,其實并不影響可靠性本身,但是本質上這是對資源的一種浪費(讓費了那么多電力,帶寬...網絡資源!“泥他🐎不要了!?”),因此 TCP 支持根據接收端的處理能力,來決定發送端的發送速度。這個機制就叫做流量控制(Flow Control)。(UDP是沒有考慮這種問題的,只會無腦sendto)

  • 接收端將自己可以接收的緩沖區剩余空間的大小放入 TCP 首部中的“16位窗口大小”字段,通過 ACK 報文通知發送端。

  • 窗口大小字段越大,說明網絡的吞吐量越高。

  • 接收端一旦發現自己的緩沖區快滿了,就會將窗口大小設置成一個更小的值通知給發送端。

  • 發送端接收到這個窗口之后,就會減慢自己的發送速度。

  • 如果接收端緩沖區滿了,就會將窗口置為 0。這時發送方不再發送數據,但是需要定期發送一個窗口探測數據段,使接收端把窗口大小告訴發送端。

那么下面有幾個子問題需要解決:

在TCP三次握手的時候,第三次握手只發送ACK嗎,ACK可以攜帶數據嗎?

其實是可以的!因為合理!客戶端發送ACK出去的時候,就代表客戶端三次握手就已經完成了!那么客戶端可能就會發現自己還有數據,那么客戶端就可能將應答和用戶數據做捎帶應答!


第一和第二次可以攜帶數據嗎?

不可以!!!因為不管是第一次還是第二次,雙方三次握手都不算完成!在前兩次握手發送的是報頭,這樣也就可以知道了雙方的當前緩沖區的剩余空間情況,為流量控制做好了準備,后續發送數據的時候就可以正常進行流量控制了!!!


如果主機A給主機B發送消息,主機B上層一直沒有讀取對應的數據,知道將自己主機B的接收緩沖區打滿了,那么主句A就會停止向主句B發送數據,我們就可以將滑動窗口設置為0,主機A停止發送!

那么什么時候可以繼續進行數據包的傳送與接收呢?(對方上層什么時候才取走呀!取走了主機A他知道嗎?😭)

兩種辦法:

第一種策略:

主機A也沒有辦法,只能周期性的給主機B發送窗口探測,也就是發送一個攜帶序號的TCP報頭,是報頭,不攜帶正文數據,所以并不會放到主機B的接收緩沖區當中,所以不會導致丟包,根據TCP定義,沒有數據,但是也需要應答,只要應答,就會將主機B的緩沖區剩余空間大小通告給主機A!所以通過這種方式,就可以知道對端窗口有沒有更新!?

  • 窗口探測報文是一個特殊的TCP報文,它不攜帶數據(數據長度為0),但包含TCP報頭

  • 它的主要目的是觸發接收端發送一個ACK報文,從而獲取接收端當前的窗口大小。

第二種策略:

因為TCP通信中,雙方地位是對等的,雙方之間可以自行發送消息,不向HTTP,服務方要處理的是請求,服務方給對方做的是應答,雙方地位不對等,永遠都是客戶端請求服務器!所以主機B也可以自動給主機A發送消息,所以當主機B的對應窗口大小更新了,就會自動向主機A發送一個窗口更新的通知!

在真實的情況下,這兩種策略同時被采用!

如果單獨的一個第二種策略,就會導致:如下的類似死鎖的現象:

我們利用第一種策略!?

這時候,我們加上第一種策略的話, 就可以很好的相輔相成了!不僅保證了可靠性,還考慮了性能!!!

接收端如何把窗口大小告訴發送端呢?回憶我們的 TCP 首部中,有一個 16 位窗口字段,就是存放了窗口大小信息。

那么問題來了,16 位數字最大表示 65535,那么 TCP 窗口最大就是 65535 字節么?

實際上,TCP 首部 40 字節選項中還包含了一個窗口擴大因子 M,實際窗口大小是窗口字段的值左移 M 位。

擁塞控制

?一個例子:發送方向接收方一次發送1000個報文,報文丟了2,3個,那么很正常,發送方補發就可以了,但是如果1000個報文,就2,3個收到,其他全部丟了呢?!就好比大學期末考試,100個人掛科2,3個很正常,可能就是沒好好學,但是如果掛了98個人的話,那很可能就是閱卷老師的問題了。

所以,對于丟包數量,丟包相對多,丟包相對少是會給出不同的結論!

所以TCP不僅僅考慮了雙方主機的問題,還考慮了網絡本身的問題!但是請不要幻想TCP能將各種網絡問題怎么解決!如果網絡中的硬件設備,比如說路由器掛掉了,運營商大量機器掛掉了等等問題,能讓TCP干啥?!是干不了的!所以TCP說他考慮網絡問題,其實是只能考慮一些能恢復的網絡問題,這是在硬件無障礙的前提下!

在我們進行網絡通信時,如果出現了少量的數據包丟失,在我們看來,就是這幾個報文的問題,是不是路由器走錯了,是不是報文轉發的時候,到對端校驗和失敗了......反正就是這一兩個報文的問題,如果一旦丟包太多了,那么發送方就會判定網絡出現問題!網絡出現擁塞問題!

那么發送方判定出網絡擁塞,這些報文還需不需要重發呢?

是不能立即重發的!!! 就像一個十字路口已經堵得不成樣了,還讓進去嗎!!!如果立即重發就會增加網絡的壓力負載,讓網絡變得更加擁堵!就用客戶端-服務端,客戶端肯定是有多個的!就像我們在宿舍同一個網絡下訪問CSDN的時候,卡的不僅僅是自己,舍友肯定也卡了,那么所有的主機(客戶端),TCP就會將所有主機接管,大家會采用相同的策略處理!就需要進行擁塞控制!

想要理解擁塞控制,我們就需要時刻告訴自己,擁塞控制,會讓發送端的多個主機都采用擁塞控制的策略!--- 慢啟動

雖然滑動窗口能夠高效可靠的發送大量的數據,但如果在剛開始階段就發送大量的數據,就可能會引發某些問題。因為網絡上有很多的計算機,有可能當前的網絡狀態就已經比較擁塞了,因此在不清楚當前網絡狀態的情況下,貿然發送大量的數據,就可能會引起網絡擁塞問題。

因此TCP引入了慢啟動機制,在剛開始通信時先發少量的數據探探路,摸清當前的網絡擁堵狀態,再決定按照多大的速度傳輸數據。(剛開始大家都很慢)

當判定網絡擁堵了,就會引入慢啟動機制,前期慢一點,本質就是為了探測網絡是否慢慢恢復,一旦判定出網絡已經不怎么卡了,后期就會慢慢恢復網絡通信過程!

每收到一個ACK應答擁塞窗口的值就加一,此時擁塞窗口就是以指數級別進行增長的,如果先不考慮對方接收數據的能力,那么滑動窗口的大家就只取決于擁塞窗口的大小,此時擁塞窗口的大小變化情況如下:

擁塞窗口滑動窗口
$1=2^0$1
$1+1=2^1$2
$2+2=2^2$4
$4+4=2^3$8
......

可是上面不是說好了,發送多少數據,由滑動窗口決定啊!但是滑動窗口不是受對方當前的接受能力的影響嗎?

所以為了支持擁塞控制算法(慢啟動),我們需要再提出一個新的概念 --- 擁塞窗口

擁塞窗口是可能引起網絡擁塞的閾值,如果一次發送的數據超過了擁塞窗口的大小就可能會引起網絡擁塞。(一個臨界值,值以下,網絡較大概率不阻塞,值以上,網絡可能阻塞!)

網絡肯定是變化的,這也就決定了這個擁塞窗口一定要進行跟更新變化!

所以,我們對滑動窗口的認知就要更豐富,更細致了:

滑動窗口 = min(對方接收緩沖區剩余空間的大小,擁塞窗口);?
  • 剛開始發送數據的時候擁塞窗口大小定義以為1,每收到一個ACK應答擁塞窗口的值就加一。
  • 每次發送數據包的時候,將擁塞窗口和接收端主機反饋的窗口大小做比較,取較小的值作為實際發送數據的窗口大小,即滑動窗口的大小。

所以我們發送數據的時候,發送數據的量,不會一直進行指數級增長,即便是擁塞窗口再怎么增長,再大的話,主要矛盾就會轉化為對方目前的接受能力的大小了!

可是,擁塞窗口大小這個數字總不能一直指數增長吧?32位機器下會快就會溢出了!利用指數增長,前期慢,后期快,目的是為了盡快探測并恢復網絡通信,在我們當前計算機內,擁塞窗口的大小是衡量網絡是否會擁堵的一個指標!!!再說了,網絡是變化的,也就決定了這個擁塞窗口的大小是一定要進行更新變化的!然而指數增長是為了應對異常的,是用來恢復和處理網絡問題的,在擁塞窗口不做指數增長之后,還是需要變化的,這是取決于網絡變化!!!其實變化就是由指數增長轉化為線性增長了!這個線性變化本質就是在不斷探測新的擁塞窗口的值!我們將由指數增長轉為線性增長的轉折點稱為 "ssthresh"?

也就是說:

  • 為了避免短時間內再次導致網絡擁塞,因此不能一直讓擁塞窗口按指數級的方式進行增長。
  • 此時就引入了慢啟動的閾值,當擁塞窗口的大小超過這個閾值時,就不再按指數的方式增長,而按線性的方式增長。
  • 當TCP剛開始啟動的時候,慢啟動閾值設置為對方窗口大小的最大值。
  • 在每次超時重發的時候,慢啟動閾值會變成當前擁塞窗口的一半,同時擁塞窗口的值被重新置為1,如此循環下去。

擁塞窗口在增加,我們發送的數據量一定在增加嗎?不一定的哈?當擁塞窗口增加到一定層度的話,主要矛盾就會變為對端的接受能力了!上面才說過!是取min!擁塞窗口特別大不就是網絡特別好嘛!

所以,當觸發網絡擁塞時,TCP會進入慢啟動階段,擁塞窗口會重新開始指數增長。慢啟動階段的指數增長是為了快速探測網絡的可用帶寬,直到擁塞窗口達到一個閾值(ssthresh)。每一次都有應答,一旦達到閾值,TCP會進入擁塞避免階段,此時擁塞窗口的增長速度會變慢(通常是線性增長)。?因為發送了10輪,100輪,1000輪....都不發生丟包,時間越久,就代表網絡越好,那么擁塞窗口值就應該越大,擁塞窗口在線性增長本質就是在衡量當前網絡的通常程度的!但是,在發送數據的時候,突然發生了網絡擁塞,這時候就需要重置慢啟動,就是在下圖的24發生擁塞了,本質不就是我們探測出來了的當前的擁塞窗口了嘛!然后重新開始,除了是支持慢啟動,本質也是重新開始探測網絡健康!而且下一次從指數探測到線性探測的閾值根據算法規定,我們新的ssthresh值由上一次網絡擁堵時的窗口大小去" ÷ 2 "?

如下圖:

不過在極端情形下,就是網絡非常好,也就是說擁塞窗口在線性探測的過程中,會一直增大嗎?

連續正常發送一個月的數據,不就是網絡很好嘛!從邏輯上來說,就是需要不斷增大!要大就大到整型的最大值就不變唄!但是理論上是不會一直增大的!?從帶寬利用率的角度來看,即使網絡條件很好,TCP擁塞窗口也不會無限制增長。因為TCP的擁塞控制機制會動態調整窗口大小,使其在充分利用可用帶寬的同時,避免過度占用導致網絡擁塞,從而保持網絡的穩定性和高效性。其實再大也就沒有什么意義了!

延時應答

為什么要延遲?

每次接收方回復確認應答的時候,會在TCP頭部當中攜帶窗口大小, 來告知發送方自己的接收能力。
發送方是要通過接收方通告的窗口大小來調整發送窗口的
假設不考慮網絡的情況下:

  • 接收方通告的窗口大小越大,則發送窗口越大, 則發送方發送的數據越多
  • 接收方通告的窗口大小越小,則發送窗口越小, 則發送方發送的數據越少

如果接收方收到數據就立即返回ACK應答,這時候的緩沖區中接受的數據許多還沒能夠處理,緩沖區的剩余大小就是窗口大小,所以此時返回的窗口值會比較小。
在收到數據以后并不立即返回確認應答,延遲一小會,等待緩沖區中數據被處理,接收緩沖區空間變大一些,再進行應答(此時確認應答的窗口值會大一些)——這是延遲\延時應答

假設接收端緩沖區為1M。一次收到了500K的數據;如果立刻應答,返回的窗口就是500K;但實際上可能處理端處理的速度很快,10ms之內就把500K數據從緩沖區消費掉了;在這種情況下,接收端處理還遠沒有達到自己的極限,即使窗口再放大一些,也能處理過來;如果接收端稍微等一會再應答,比如等待200ms再應答,那么這個時候返回的窗口大小就是1M。

窗口越大,網絡吞吐量就越大,傳輸效率就越高,我們的目標是在保證網絡不擁堵的情況下盡量提高傳輸效率!?

那么所有的包都可以延遲應答嗎?? 不是的,與以下有關:

  • 數量限制:每隔N個包就延時應答一次;
  • 時間限制:超過最大延遲時間就應答一次;

具體的數量和超時時間,依操作系統不同也有差異;一般N取2,超時時間取200ms;

在系統中,有一個固定的定時器每隔200ms會來檢查是否需要發送ACK包,這樣做有兩個目的。

  1. 這樣做的目的是ACK是可以合并的,也就是指如果連續收到兩個TCP包,并不一定需要ACK兩次,只要回復最終的ACK就可以了(每一次數據都未必需要應答了),可以降低網絡流量;
  2. 如果接收方有數據要發送,那么就會在發送數據的TCP數據包里,帶上ACK信息,也就是捎帶應答,這樣做,可以避免大量的ACK以一個單獨的TCP包發送,減少了網絡流量。

捎帶應答?

我們之前對捎帶應答已經有了比較多的認識了,捎帶應答本質就是為了用來提高效率的!下面我們來稍微回顧一下:

在延遲應答的基礎上,我們發現,在很多情況下,客戶端服務器在應用層也是“一發一收”的。這意味著客戶端給服務器說了“How are you”,服務器也會給客戶端回一個“Fine, thank you”。那么這個時候 ACK 就可以搭順風車,和服務器回應的“Fine, thank you”一起回給客戶端。

TCP小結?

分類內容
可靠性校驗和、序列號(按序到達)、確認應答、超時重發、連接管理、流量控制、擁塞控制
提高性能滑動窗口、快速重傳、延遲應答、捎帶應答
其他定時器(超時重傳定時器、保活定時器、TIME_WAIT 定時器等)(在系統部分,信號專題,鬧鐘alarm就是很好的例子,說明操作系統本身就是可以計時的!)

下面我們來談談相關的其他話題:

面向字節流?

這個我們之前也講過了!?我們主要還是回顧一下:

創建一個 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。和正常關閉沒有什么區別。

雙方建立的兩個連接不就是雙方(客戶端和服務端的兩個進程建立的嘛,就好比服務器的accpet的文件描述符就是一個連接,相關話題我們后續的文章會說到!!!)(進程退出,進程打開的文件基本就不在了,引用計數到0的時候,這時候連接就會自動進行自動揮手)

機器重啟:和進程終止的情況相同。因為關機的話,操作系統會關閉所有的進程!

機器掉電/網線斷開:對于被拔掉網線的一方,因為操作系統是軟硬件資源的管理者,就會立馬意識到當前的網絡連接出現問題了!硬件上識別到之后,軟件上理所應當是對應的連接沒了!就像我們瀏覽器上,也就是客戶端的網絡斷開了,會出現:

可是這個客戶端是沒有機會和服務器發生四次揮手的!因為網線一拔,都來不及發送報文了!所以接收端也就是服務端還認為連接在,一旦接收端有寫入操作(服務端向客戶端發消息),接收端發現連接已經不在了,就會進行 reset(這個就是我拔掉網線后,重連了,可是通信之前需要進行三次握手呀,所以發reset進行連接重置!)。即使沒有寫入操作,TCP 自己也內置了一個保活定時器,會定期詢問對方是否還在。如果對方不在,也會把連接釋放。?

對于連接的保活機制其實是TCP協議報頭選項的一種功能!是TCP自帶的,是大幾十分鐘級別的,或者小時級的!但是對于保活機制,我們基本都是在應用層自己完成的!來符合應用層的需要與控制!

另外,應用層的某些協議,也有一些這樣的檢測機制。例如 HTTP 長連接場景中,也會定期檢測對方的狀態。例如 QQ,在 QQ 斷線之后,也會定期嘗試重新連接。

綜上:TCP對于連接異常,有很強的容錯性!

基于TCP應用層的協議

  • HTTP
  • HTTPS
  • SSH
  • Telnet
  • FTP
  • SMTP
  • 當然,我們自己寫的TCP程序時自定義的應用層協議!就如前面的網絡版本計算器!

TCP VS UDP

TCP和UDP的優缺點不能簡單絕對地進行比較。

  1. TCP用于可靠傳輸的情況,適用于文件傳輸、重要狀態更新等場景;
  2. UDP用于對高速傳輸和實時性要求較高的通信領域,例如早期的QQ、視頻傳輸等,另外UDP還可以用于廣播。

歸根結底,TCP和UDP都是程序員的工具,什么時機用,具體怎么用,還是要根據具體的需求場景去判定。

我們在選擇協議上,到底是選擇TCP還是UDP,基本的原則是:

只要在網絡通信時,要求的是足夠簡單,而且對報文丟包的容忍度比較高,這就可以選擇UDP,就像直播,允許點畫面糊掉了,沒有多大關系!除此之外,一律選擇TCP!就好比登錄注冊,支付轉賬等等!

用UDP實現可靠傳輸(經典面試題)

有一種特殊要求:TCP通信的時候稍微優點過重了!還需要建立連接之類的,我不想建立連接,而且我還要用UDP來保證基本的可靠性,在應用層,如何使用UDP來實現對應的可靠性?

在UDP協議的基礎上實現可靠傳輸是一個經典的面試題,通常考察面試者對TCP可靠性機制的理解以及對協議設計的思考。UDP本身是一種無連接、不可靠的傳輸層協議,不保證數據包的順序、完整性和可靠性。而TCP的可靠性機制包括序列號、確認應答、超時重傳、擁塞控制等。如果要在應用層基于UDP實現類似TCP的可靠傳輸,可以參考以下設計思路:

1.?引入序列號

序列號的作用是為每個發送的數據包分配一個唯一的編號,接收方可以根據序列號來判斷數據包是否丟失、重復或亂序。

  • 實現方式:在每個UDP數據報中添加一個序列號字段。發送方在發送數據時,為每個數據包依次分配序列號(例如從1開始遞增)。

  • 優勢:接收方可以通過序列號檢測數據包的順序,如果發現序列號不連續,可以判斷中間的數據包丟失。

2.?確認應答(ACK)

發送方需要接收方的確認應答來確認數據包是否被正確接收。如果沒有收到確認應答,發送方可以重傳數據。

  • 實現方式:接收方在收到數據包后,發送一個確認應答(ACK)消息,ACK消息中包含已成功接收的最高序列號。發送方收到ACK后,知道該序列號及之前的數據包已被接收。

  • 優勢:通過ACK機制,發送方可以明確知道哪些數據包需要重傳,從而提高傳輸的可靠性。

3.?超時重傳

如果發送方在一定時間內沒有收到確認應答,可以認為數據包可能丟失,需要重傳。

  • 實現方式:發送方為每個未確認的數據包設置一個計時器。如果在計時器超時后仍未收到ACK,就重傳該數據包。

  • 優勢:通過超時重傳機制,可以彌補UDP不保證數據包可靠傳輸的缺陷。

4.?滑動窗口機制

為了提高傳輸效率,可以引入滑動窗口機制,允許發送方在等待確認應答之前發送多個數據包。

  • 實現方式:發送方維護一個滑動窗口,窗口大小表示可以發送但尚未收到確認的數據包數量。接收方也維護一個滑動窗口,用于接收并緩存亂序到達的數據包。

  • 優勢:滑動窗口機制可以提高數據傳輸的效率,減少等待確認應答的時間。

5.?數據分片與重組

如果數據量較大,需要將數據分片后通過UDP發送,接收方需要將分片的數據重新組合。

  • 實現方式:在每個UDP數據報中添加分片標識和總分片數字段。接收方根據分片標識和總分片數將數據重新組合。

  • 優勢:通過分片與重組機制,可以支持大塊數據的可靠傳輸。

6.?擁塞控制

為了避免網絡擁塞,可以引入簡單的擁塞控制機制,動態調整發送速率。

  • 實現方式:根據網絡的擁塞情況動態調整滑動窗口大小。如果檢測到丟包,可以減小窗口大小;如果網絡狀況良好,可以適當增大窗口大小。

  • 優勢:擁塞控制可以避免發送方過度占用網絡資源,提高網絡的整體性能。

示例代碼(偽代碼)

以下是一個簡化的偽代碼示例,展示如何基于UDP實現可靠傳輸:

// 發送方
initialize sequence_number = 1
initialize timeout = 1 second
initialize window_size = 4while (data_to_send) {if (window_size > 0) {send_udp_packet(data, sequence_number)set_timer(sequence_number, timeout)sequence_number += 1window_size -= 1}if (timer_expired(sequence_number)) {resend_udp_packet(data, sequence_number)reset_timer(sequence_number, timeout)}if (receive_ack(ack_sequence_number)) {if (ack_sequence_number >= expected_ack) {window_size += 1expected_ack = ack_sequence_number + 1}}
}// 接收方
initialize expected_sequence_number = 1
initialize buffer = []while (true) {receive_udp_packet(data, sequence_number)if (sequence_number == expected_sequence_number) {deliver_data(data)expected_sequence_number += 1send_ack(expected_sequence_number - 1)while (buffer contains sequence_number == expected_sequence_number) {data = buffer.pop(sequence_number)deliver_data(data)expected_sequence_number += 1}} else if (sequence_number > expected_sequence_number) {buffer.append(data, sequence_number)send_ack(expected_sequence_number - 1)}
}

總結

通過引入序列號、確認應答、超時重傳、滑動窗口、分片與重組以及擁塞控制等機制,可以在應用層基于UDP實現類似TCP的可靠傳輸。在面試中,除了描述這些機制外,還可以結合實際場景討論如何優化和調整這些機制,以應對不同的網絡環境和應用需求。

從開始到現在,理論是理論,操作是操作,我們該如何將其有效的關聯起來呢?

Linux內核中Socket相關結構和流程

1. 用戶態到內核態的Socket調用流程

當用戶程序進行socket調用時,該調用會通過系統調用接口進入內核態,觸發內核中的socket創建流程。

2. 內核態中Socket的創建和關聯
  • sock_create:在內核中創建一個新的socket結構體。

  • 初始化等待隊列:為socket初始化等待隊列,用于處理同步操作。

  • sock_alloc_file:分配一個file結構體,用于用戶態和內核態之間的文件描述符(fd)關聯。

  • 建立雙向關聯

    • file->private_data 指向 socket 結構。

    • socket->file 指向 file 結構。

  • alloc_fd:分配文件描述符。

  • fd_install:將文件描述符與file結構體關聯。

3. 數據結構關系
  • task_struct:表示一個進程,包含進程的所有信息。

  • files_struct:每個進程都有一個files_struct結構,用于管理該進程打開的所有文件。

  • fd_array:文件描述符數組,存儲該進程打開的所有文件的指針。

  • struct file:表示一個打開的文件,包含文件操作相關的信息。

  • private_data:指向socket結構體。

  • struct socket:表示一個網絡socket,包含網絡連接的相關信息。

  • file指針:socket結構體中包含一個指向file結構體的指針。

  • sk_sleep等待隊列:socket的等待隊列,用于處理同步操作。

  • __wait_queue_head:等待隊列頭,包含鎖和等待任務列表。

結構圖

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/84524.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/84524.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/84524.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

洛谷B3612 【深進1.例1】求區間和

題目描述 給定 n 個正整數組成的數列 a1?,a2?,?,an? 和 m 個區間 [li?,ri?]&#xff0c;分別求這 m 個區間的區間和。 輸入格式 第一行&#xff0c;為一個正整數 n 。 第二行&#xff0c;為 n 個正整數 a1?,a2?,?,an? 第三行&#xff0c;為一個正整數 m 。 接下…

debian12 修改MariaDB數據庫存儲位置報錯

debian12 修改MariaDB數據庫存儲位置到home報錯 MariaDB 修改存儲路徑后啟動失敗問題解決 更改數據存儲位置 如果需要將數據存儲到其他位置&#xff08;如更大的分區&#xff09;&#xff1a; 停止 MariaDB 服務&#xff1a; bash sudo systemctl stop mariadb 創建新目錄并設…

【評測】flux-dev文生圖模型初體驗

回到目錄 【評測】flux-dev文生圖模型初體驗 1. 安裝基礎環境 參考 modelscope的Flux.1-dev頁面 2. 使用tongyi寫提示詞 幫我用英文寫3個&#xff0c;文生圖片1024*1024的提示詞&#xff0c;準備用flux.dev生成用 [pic03] 3. 運行代碼 4090D滿載運行&#xff0c; 1min左…

PHP7+MySQL5.6 雪里開簡易預約制訪客管理系統V1.0

# PHP7MySQL5.6 雪里開簡易預約制訪客管理系統 V1.0 ## 簡介 本系統是一個基于PHP7和MySQL5.6的封閉校區訪客管理系統&#xff0c;用于管理學生訪客的申請、核銷流程。 導入的賬號預先提交訪客信息(預約制)&#xff0c;無需審核&#xff0c;訪客提交匿名制訪客碼給門衛登記放行…

【深度學習:進階篇】--2.4.BN與神經網絡調優

學習目標 目標 知道常用的一些神經網絡超參數知道BN層的意義以及數學原理 應用 無 目錄 學習目標 1.神經網絡調優 1.1.調參技巧 1.2.運行 2.批標準化&#xff08;Batch Normalization&#xff09; 2.1.標準化公式 2.2.為什么可以優化簡單 2.3.BN總結 1.神經網絡調優 …

CMake指令: add_sub_directory以及工作流程

目錄 1.簡介 2.工作流程 3.示例場景 4.最佳實踐 5.注意事項 6.總結 相關鏈接 1.簡介 add_subdirectory 是 CMake 中用于添加子目錄參與構建的命令&#xff0c;允許將項目拆分為多個模塊或子項目&#xff0c;實現代碼的模塊化管理。 基本語法&#xff1a; add_subdirect…

【C++ 】智能指針:內存管理的 “自動導航儀”

目錄 一、引入 二、智能指針的兩大特性&#xff1a; 1、RAII 特點&#xff1a; 好處&#xff1a; 2、行為像指針 三、智能指針起初的缺陷&#xff1a;拷貝問題 四、幾種智能指針的介紹。 1、C98出現的智能指針——auto_ptr auto_ptr解決上述拷貝構造的問題&#xff1a…

Java多線程實現之線程池詳解

Java多線程實現之線程池詳解 一、線程池的基本概念1.1 為什么需要線程池1.2 線程池的核心思想 二、Java線程池的實現2.1 Executor框架2.2 ThreadPoolExecutor構造參數 三、常見線程池類型3.1 FixedThreadPool3.2 CachedThreadPool3.3 SingleThreadExecutor3.4 ScheduledThreadP…

解碼美元-黃金負相關:LSTM-Attention因果發現與黃金反彈推演

摘要&#xff1a;本文采用時間序列分析框架與自然語言處理&#xff08;NLP&#xff09;技術&#xff0c;對黃金與美元指數的負相關關系進行量化拆解。通過構建包含宏觀經濟因子、市場情緒指標及地緣風險的三維分析模型&#xff0c;揭示當前貴金屬市場的核心驅動邏輯&#xff0c…

Asp.Net Core SignalR導入數據

文章目錄 前言一、安裝包二、使用步驟1.實現SignalR Hub服務&#xff1a;2.實現CSV文件解析及數據導入服務3.控制器4.前端實現&#xff08;vue&#xff09; 三、關鍵技術點說明總結 前言 導入CSV文件中的數據到數據庫&#xff0c;使用CsvHelper解析CSV文件&#xff0c;SqlBulk…

Modern C++(四)聲明

4、聲明 聲明是將名字引入到cpp程序中&#xff0c;不是每條聲明都聲明實際的東西。定義是足以使該名字所標識的實體被使用的聲明。聲明包含以下幾種&#xff1a; 函數定義模板聲明模板顯式實例化模板顯式特化命名空間定義鏈接說明屬性聲明&#xff08;C11&#xff09;空聲明&…

目標檢測yolo算法

yolov5s&#xff1a; 從github官網下載yolov5的算法之后&#xff0c;配置好環境&#xff08;pycharm安裝包-CSDN博客&#xff09;&#xff0c;再下載權重文件&#xff0c;比如默認的yolov5s.pt&#xff1b; 運行當前文件&#xff08;detect.py&#xff09;&#xff0c;就能看…

一個超強的推理增強大模型,開源了,本地部署

大家好&#xff0c;我是 Ai 學習的老章 前幾天介紹了MOE 模型先驅 Mistral 開源的代碼 Agent 大模型——mistralai/Devstral-Small-2505 今天一起看看 Mistral 最新開源的推理大模型——Magistral Magistral 簡介 Mistral 公司推出了首個推理模型 Magistral 及自研可擴展強…

MySQL體系架構解析(五):讀懂MySQL日志文件是優化與故障排查的關鍵

MySQL文件 日志文件 在服務器運行過程中&#xff0c;會產生各種各樣的日志&#xff0c;比如常規的查詢日志&#xff0c;錯誤日志、二進制日志、 redo 日志和 Undo 日志等&#xff0c;日志文件記錄了影響 MySQL 數據庫的各種類型活動。 常見的日志文件有&#xff1a;錯誤日志…

湖南省網絡建設與運維賽項競賽規程及樣題

湖南省職業院校技能競賽樣題 賽題說明 一、競賽內容 “網絡建設與運維”競賽共分三個部分&#xff0c;其中&#xff1a; 第一部分&#xff1a;職業規范與素養 &#xff08; 5 分&#xff09; 第二部分&#xff1a;網絡搭建及安全部署項目 &#xff08; 50 分&#xff09…

華為云Flexus+DeepSeek征文 | 基于華為云ModelArts Studio搭建AnythingLLM聊天助手

華為云FlexusDeepSeek征文 | 基于華為云ModelArts Studio搭建AnythingLLM聊天助手 引言一、ModelArts Studio平臺介紹華為云ModelArts Studio簡介ModelArts Studio主要特點 二、AnythingLLM介紹AnythingLLM 簡介AnythingLLM主要特點AnythingLLM地址 三、安裝AnythingLLM應用下載…

板凳-------Mysql cookbook學習 (十--5)

6.11 計算年齡 2025年6月11日星期三 --創建表、初始化數據 drop table if exists sibling; create table sibling (name char(20),birth date );insert into sibling (name,birth) values(Gretchen,1942-04-14); insert into sibling (name,birth) values(Wilbur,1946-11-28)…

SAP RESTFUL接口方式發布SICF實現全路徑

其他相關資料帖可參考&#xff1a; https://blog.csdn.net/woniu_maggie/article/details/146210752 https://blog.csdn.net/SAPmatinal/article/details/134349125 https://blog.csdn.net/weixin_44382089/article/details/128283417 【業務場景】 外部系統不想通過RFC (需…

在windows中安裝或卸載nginx

首先在nginx的安裝目錄下cmd查看nginx的版本&#xff1a; 在看windows的服務中是否nginx注冊為服務了 如果注冊了服務就先將服務卸載了 在nginx的安裝目錄cmd執行命令 NginxService.exe uninstall “NginxService”是對應的注冊的服務名稱 關閉所有的相關nginx的服務這個也…

FaceFusion 技術深度剖析:核心算法與實現機制揭秘

在 AI 換臉技術蓬勃發展的浪潮中&#xff0c;FaceFusion 憑借其出色的換臉效果和便捷的操作&#xff0c;成為眾多用戶的首選工具。從短視頻平臺上的創意惡搞視頻&#xff0c;到影視制作中的特效合成&#xff0c;FaceFusion 都展現出強大的實用性。而這一切的背后&#xff0c;是…