??📚?博主的專欄
🐧?Linux???|?? 🖥??C++???|?? 📊?數據結構??|?💡C++ 算法?|?🅒?C 語言? |?🌐?計算機網絡
上篇文章:傳輸層協議-UDP
下篇文章: 網絡層
我們的講解順序是:通過前面的學習,理解能理解的TCP字段、學習TCP的策略,并且理解報頭字段、學習其他的可靠性策略
文章摘要
本文全面剖析TCP協議的核心機制,深入探討其如何保障網絡通信的可靠性。從協議段格式、確認應答、超時重傳、連接管理(三次握手與四次揮手)等基礎原理出發,詳解流量控制、滑動窗口、擁塞控制等性能優化策略。同時解析粘包問題、延遲應答、捎帶應答等實際場景中的關鍵技術,并通過對比TCP與UDP的差異,闡明兩者的適用場景。文章結合代碼示例與網絡狀態分析,揭示TCP異常處理與資源管理的底層邏輯,為理解高效可靠的數據傳輸提供系統性視角。無論是網絡初學者還是開發者,均可從中獲得理論與實踐的深度洞見。
目錄
TCP 協議段格式
確認應答(ACK)機制
如何保證服務器端到客戶端的可靠性?
真正的網絡發送:
TCP 將每個字節的數據都進行了編號. 即為序列號
為什么需要有兩種序號:序號和確認序號
捎帶應答:TCP中提高效率的重要機制
6 位標志位:
如何理解序號?
超時重傳機制
序號可以提供確認應答機制、能夠去重、還能夠保證按序交付。來保證可靠性
那么,如果超時,超時的時間如何確定?
連接管理機制
建立連接:三次握手、由一方主動發起,過程是由雙方OS自主完成的
?編輯建立連接的本質就是在賭,賭最后一個發送的ACK對方(接收方)一定收到了
如果最后發送的ACK丟失了,會出現什么結果?
四次揮手:最小的通信成本,建立了斷開連接的共識
TCP 狀態轉換的一個匯總圖:
建立連接,為什么要三次握手(重要)
再談四次揮手
驗證TCP四次揮手的兩種狀態:
理解CLOSE_WAIT狀態:
文件描述符泄漏現象:
理解 TIME_WAIT 狀態
setsockopt
流量控制
PSH標記位
URG標記位、緊急指針是否有效
滑動窗口
問題1
問題2
如何理解滑動窗口?
最左側報文丟失:解決辦法
滑動窗口問題解答
實際上滑動窗口 = min(應答窗口, 擁塞窗口)
擁塞控制
"慢啟動" 只是指初使時慢, 但是增長速度非常快.
后線性是為了精細化探測出最新擁塞窗口的值、線性探測
延遲應答
捎帶應答
第三次握手可以捎帶應答?
***面向字節流***重要***粘包問題***
面向字節流
粘包問題:
那么如何避免粘包問題呢? 歸根結底就是一句話, 明確兩個包之間的邊界
思考: 對于 UDP 協議來說, 是否也存在 "粘包問題" 呢?
TCP異常情況
TCP 小結
可靠性與提高性能
基于 TCP 應用層協議
TCP/UDP 對比
學習每一個協議時都要理解的兩個問題:
1.如何解包(如何分裝)
2.如何分用(如何從上層向下層交付)
1.首先,宏觀上:需要知道報文從應用層向下層交付的時候,交付的數據部分,而在TCP(TCP的上一層就是應用層)中,會將數據給拷貝到緩沖區里,從緩沖區拿出數據,會自動添加報頭(添加sk_buff結構)后再發送。?
2.對方收到報文前,TCP這里會處理報文,將報頭分離開,提取出有效載荷放入接收緩沖區,讓用戶讀取。
TCP 協議段格式
TCP 全稱為 "傳輸控制協議(Transmission Control Protocol"). 人如其名, 要對數據的傳輸進行一個詳細的控制;
如何解包:?
1.讀取前20個字節的固定首部
2.提取4位首部長度(單位是4字節)(范圍:[0, 15] -> [0, 60])
以上就是,先理解能理解的報文字段,接下來,我們邊學習TCP的策略,并且理解報頭字段
確認應答(ACK)機制
在發送消息和接收消息的時候,總有最新的消息沒有應答。
例如:
對于客戶端:客戶端給服務器端發送消息后,并不能確認自己的消息成功被服務器端接收到,但,只要服務器給客戶端響應回來,就能確認服務器端收到了客戶端歷史發送的消息,如果沒有收到應答,就認為報文丟失。
要保證從左到右的可靠性:就要保證我發的歷史的消息數據(而不是最新的),收到了應答,就能保證歷史數據100%被對方收到了。
再次理解可靠性:
如果只考慮客戶端給服務器端發消息(單向),客戶端發一次消息,服務器端做一次應答,正常通信時,客戶端不需要對服務器端的應答做應答。只保證自己發的歷史消息,服務器做了應答,客戶端就再發下一次的數據消息。
因此可靠性實際上并不是指客戶端所發送的消息100%被服務器端所收到,這是理想情況。
所謂的可靠性,是指客戶端給服務器端所發送的消息,發送方需要知道接收方是否收到。
如何保證服務器端到客戶端的可靠性?
同上。服務器端給客戶端發送消息,也需要收到客戶端的應答,才能保證發送的歷史消息被收到。沒收到應答,就確認未收到,就重發。
結論:
可靠:重點是保證對發送的數據的進行可靠,應答是否可靠不考慮。只要收到應答,就能保證歷史數據是可靠的。
雙方都采用確認應答機制,就能保證兩個朝向上數據通信的可靠性。
真正的網絡發送:
是雙方的OS(TCP協議)自動做的,所謂的發消息,是發送方的TCP協議自動發的,由對方的TCP協議來自動接受做ACK應答的。
TCP通信模式:(兩種)
1.在不做說明的時候,我們一般是指一個朝向的通信,例如現在所說的就是客戶端(發送端)向服務器端(接收端)發送消息。兩個朝向的通信,也就是發送端和接收端一直做身份互換,客戶端給服務器端發,服務器端作為發送端又給客戶端發。
2.實際上,正常情況下,做的串行發送,是在發送第一個報文后收到應答,再發下一個,這樣的效率低。(第二種通信方式)(滑動窗口),發送端一次發送多條報文給接收端,因為TCP要做確認應答,理論上,每一批發送的消息,接收端都需要做分批的應答,因為處理時間很快,這樣可以做到發消息和收應答的時間發生重疊,可以提高發送效率。而如果收到的一批應答并不是發送的個數,這就不知道應答的是那一條消息,因此對應答需要做唯一性標識,引入序號和確認序號。
TCP 將每個字節的數據都進行了編號. 即為序列號
理解序號:
收到1001的確認應答:則表示前1000個報文數據接收方都收到了,下次要接受的是從1001開始的數據。
注意:我們的發送和響應的都是TCP報文,無論他是什么數字,要么只有報頭,要么含有數據帶報頭的報文。發送的指的不是1000、2000這些數字,這些是32位序號,真實的發送和響應的是要么只有報頭,要么含有數據帶報頭的報文、總之,都含有報頭。這里的數據1000、2000等本質就是給32位序號填值。所謂的應答就是裸的報頭,被設置了32位序號(確認應答)。沒在特定時間收到應答,會進行超時重傳。如果沒收到3001,而是收到了 4001的確認應答,能表示接收方收到了4001之前的所有報文,能支持應答的少量丟失。
為什么需要有兩種序號:序號和確認序號
因為TCP是全雙工的,在客戶端給服務器端發送消息的時候,服務器端也有可能給客戶端發送消息,并且也有可能做出對接收到消息的響應。發的數據是報頭帶數據,做出的響應是只含報頭。
捎帶應答:TCP中提高效率的重要機制
在TCP當中,為了提高效率:如果服務器端既要對接收到消息做應答,也需要發送消息,這時候服務器端有可能會將應答和要發送的數據的兩個不同的報文合并成一個報文。做應答只需要設置32位確認序號,發送消息則是設置32位序號,帶上數據字段。因此服務器需要同時使用序號和確認序號兩種。
客戶端如何確認所接收到的消息,是確認應答還是發送過來的消息,甚至是同時具有應答和發送過來的消息的報文。引入TCP報頭中的6個標志位。
注意:我們需要意識到,實際情況中,是多個客戶端向服務器端發送消息,未來服務器端回收到多種類型的報文,有可能客戶端所發送的請求時請求連接、斷開連接、確認報文、正常數據、確認+數據(捎帶應答)。這就說明,服務器作為TCP協議的接收方時,TCP協議要有處理不同類型報文的能力,即:TCP的報文是有不同的類型的。
6 位標志位:
URG: 緊急指針是否有效
ACK: 確認號是否有效
PSH: 提示接收端應用程序立刻從 TCP 緩沖區把數據讀走
RST: 對方要求重新建立連接; 我們把攜帶 RST 標識的稱為復位報文段
SYN: 請求建立連接; 我們把攜帶 SYN 標識的稱為同步報文段
FIN: 通知對方, 本端要關閉了, 我們稱攜帶 FIN 標識的為結束報文段
從之前文章的學習中,我們可以知道無論是UDP報頭、還是TCP報頭,本質上都是結構體:?
typedef struct _tcp_hdr { unsigned short src_port; //源端口號 unsigned short dst_port; //目的端口號 unsigned int seq_no; //序列號 unsigned int ack_no; //確認號 #if LITTLE_ENDIAN unsigned char reserved_1:4; //保留6位中的4位首部長度 unsigned char thl:4; //tcp頭部長度 unsigned char flag:6; //6位標志 unsigned char reseverd_2:2; //保留6位中的2位 #else unsigned char thl:4; //tcp頭部長度 unsigned char reserved_1:4; //保留6位中的4位首部長度 unsigned char reseverd_2:2; //保留6位中的2位 unsigned char flag:6; //6位標志 #endif unsigned short wnd_size; //16位窗口大小 unsigned short chk_sum; //16位TCP檢驗和 unsigned short urgt_p; //16為緊急指針 }tcp_hdr;
如果發的是含有應答的報文,就需要將ACK置為1,就要關注確認序號、如果是捎帶應答,就也要關注攜帶的數據。
如何理解序號?
TCP 將每個字節的數據都進行了編號. 即為序列號.
將數據從應用層以字節流的方式拷貝到發送緩沖區,就相當于把數據放在數組當中,在發送緩沖區中的數據,天然的就具有了序號,約定從第一個字節,開始發,發送100個字節,那么發送序號就是1~100,101~200。序號就是該char endbuffer[65535]緩沖區的數組下標。
而真實情況會更復雜,因為剛開始的發送序號可能是隨機的。
每一個 ACK 都帶有對應的確認序列號, 意思是告訴發送者, 我已經收到了哪些數據; 下一次你從哪里開始發。
超時重傳機制
- 主機 A 發送數據給 B 之后, 可能因為網絡擁堵等原因, 數據無法到達主機 B;
- 如果主機 A 在一個特定時間間隔內沒有收到 B 發來的確認應答, 就會進行重發;
我收到ACK,對方收到數據(客觀事實);我沒有收到ACK,對方沒有收到數據(規定);
為什么是規定,這是因為 主機 A 未收到 B 發來的確認應答, 也可能是因為 ACK 丟失了;
因此主機 B 會收到很多重復數據. 那么 TCP 協議需要能夠識別出那些包是重復的包, 并且把重復的丟棄掉。
這時候我們可以利用前面提到的序列號, 就可以很容易做到去重的效果。
按序到達
對于接受方,收到的報文的順序,一定是發送時的順序嗎,不一定,因此,需要按序號排列報文再向上層交付。
序號可以提供確認應答機制、能夠去重、還能夠保證按序交付。來保證可靠性
那么,如果超時,超時的時間如何確定?
- 最理想的情況下, 找到一個最小的時間, 保證 "確認應答一定能在這個時間內返回".
- 但是這個時間的長短, 隨著網絡環境的不同, 是有差異的.
- 如果超時時間設的太長, 會影響整體的重傳效率;
- 如果超時時間設的太短, 有可能會頻繁發送重復的包;
TCP 為了保證無論在任何環境下都能比較高性能的通信, 因此會動態計算這個最大超時時間.
- Linux 中(BSD Unix 和 Windows 也是如此), 超時以 500ms 為一個單位進行控制, 每次判定超時重發的超時時間都是 500ms 的整數倍.
- 如果重發一次之后, 仍然得不到應答, 等待 2*500ms 后再進行重傳.
- 如果仍然得不到應答, 等待 4*500ms 進行重傳. 依次類推, 以指數形式遞增.
- 累計到一定的重傳次數, TCP 認為網絡或者對端主機出現異常, 強制關閉連接.
連接管理機制
在正常情況下, TCP 要經過三次握手建立連接, 四次揮手斷開連接
建立連接:三次握手、由一方主動發起,過程是由雙方OS自主完成的
1.listen狀態:隨時等待連接
2.connect狀態:要求客戶端構建一個TCP報文(報頭),將SYN(同步標志位)置1,代表,發送連接建立的請求。服務器會應答(只含報頭)SYN設置為1,ACK表示確認收到請求。
3.緊接著,客戶端會在發送一個TCP報文,將ACK置1,表示對服務器發送的ACK做應答。
應用層調connect系統調用,嚴格意義上來講:只是發起了三次握手,我們可以理解為connect就是指向服務器發送SYN,發起握手,從此往后,三次握手的過程由雙方的OS自動協商。
三次握手完成,accept會返回新的連接套接字描述符,accept不參與三次握手的過程。
套接字狀態發生變化:
服務器從CLOSED狀態開啟后處于LISTEN狀態,在發送端客戶端處于CLOSED狀態到開啟發送一個SYN的時候,發送方的套接字會變成SYN_SENT(同步發送),服務器收到SYN標志位,服務器就會變成SYN_RCVD狀態,同時會發送應答,一旦客戶端收到ACK,就會發送ACK,并且處于ESTABUSHED狀態(保證不了這個ACK是否發送給了服務器),客戶端最后發送的ACK沒有應答,但是我們規定,一旦客戶端最后發出ACK就代表,客戶端的三次握手完成了,客戶端就認為自己就建立好了連接。服務器端認為建立好連接,是在收到ACK,處于ESTABUSHED狀態。
因此,客戶端和服務端雙方確認好連接,會有一個短暫的時間差。
建立連接的本質就是在賭,賭最后一個發送的ACK對方(接收方)一定收到了
TCP保證可靠性能保證:建立連接一定會成功嗎,不能保證、但是不用擔心。
如果最后發送的ACK丟失了,會出現什么結果?
就會超時,就會補發,當發送端認為已經建立好連接,就會馬上發送數據DATA,當服務器端收到DATA,會查看該報文中的源端口與目的端口,確認是否與該客戶端建立好了連接(是否收到了最后一次的ACK),服務器就會給客戶端做應答。如果確認沒有收到最后一次的ACK,也就是沒有建立好連接,應答的報頭中就會將RST(連接重置、要求重新建立連接)置為1。客戶端收到就會重新和服務器端再次三次握手(收到該標志位的主機,要對異常連接釋放,重新建立連接)。
四次揮手:最小的通信成本,建立了斷開連接的共識
一旦雙方通信結束,需要關閉連接。雙方都不和對方通信了,并且也知道對方也不和我通信了。
客戶端會將報頭的標志位FIN置1,在服務器收到FIN后,就會相應ACK,表示兩次握手完成,服務器端完成服務后才會和客戶端斷開連接,就會給客戶端發送FIN,當客戶端收到FIN后就會給服務器發送ACK的響應,客戶端關閉連接,服務端收到ACK后,關閉連接,完成4次握手。
斷開連接是要獲取到雙方的確認、同意。所以需要四次揮手。
前兩次揮手:
客戶端和服務器端斷開連接本質是:客戶端要給服務器發送的數據發送完畢(應用層將數據發送完,并且調用了close)。
中間狀態:服務器端繼續給客戶端發送消息,客戶端必須繼續接收消息,并且給出應答ACK。OS維護正常的可靠性的ACK,客戶端還需要保證。
后兩次揮手:
服務器端和客戶端斷開連接的本質是,服務器端要給客戶端發送的數據發送完了。
調用close是關閉讀寫端全部關閉。只有客戶端close、服務器端才會close。
shutdown(屬于套接字中的系統調用接口、可以指定關閉讀、關閉寫、同時關閉讀寫)
TCP 狀態轉換的一個匯總圖:
? 較粗的虛線表示服務端的狀態變化情況;
? 較粗的實線表示客戶端的狀態變化情況;
? CLOSED 是一個假想的起始點, 不是真實狀態
因此為什么在我們剛關閉服務器端,又再次用相同端口開啟服務器端的時候,會開啟失敗呢?
進程已經退了,但是連接還在,這個連接在和瀏覽器在進行四次揮手。連接的端口號被占用,因此開啟失敗。
建立連接,為什么要三次握手(重要)
因為將會有多個客戶端和服務器建立連接,并且還會有正在趕來建立連接的客戶端。
在OS上有多種狀態的連接,因此在OS上需要對連接進行管理。先描述、再組織。
所謂的連接本質也是內核數據結構的對象。建立好連接,就需要創建好一個內核數據結構對象。建立連接就需要在服務器端,malloc內核數據結構的空間,套接字信息,誰建立的、什么時候、源IP,目的IP,建立好的連接要有對應的緩沖區,還要維護好狀態。
一個連接既要有內存空間,也要花時間初始化連接。維護連接、是有成本的(時間+空間)。3次握手,一旦成功,就是在雙方維護對應的連接結構體。四次揮手結束,連接才會釋放。
一個客戶端給服務器發送大量的SYN,稱作SYN洪水。并且不做任何處理。會導致,服務器出現問題。為什么三次握手就可以呢?三次握手也存在SYN洪水供給問題,但是細節就是最后一次報文一定是客戶端給服務器發的。所以要建立好連接,需要客戶端先3次握手完成(有其他策略保證握手時有SYN洪水問題)。但1、2次有明顯的問題,出現問題容易被利用。
因此,為什么要有三次握手、是三次握手?重要
1.驗證全雙工、驗證雙方網絡的連通性。網絡狀態是雙方進行通信的前提條件。最小的次數驗證了客戶端能發消息,能收消息,也驗證了服務器端既能收也能發。
2.建立雙方通信的共識意愿。由于服務器一般無條件同意客戶端的連接請求,三次握手本質上就是四次握手,是四次握手將中間的ACK、SYN進行捎帶應答了,因此稱作三次握手。
再談四次揮手
深度理解下:
如果能做到:在服務器端收到客戶端發來的FIN時也恰好想要關閉連接,此時可以將ACK和FIN捎帶應答成一次嗎,可以將四次揮手稱之為三次揮手?如果恰好遇到這種情況,是可以的、這是其中的特殊情況。但是由于客戶端給服務器發消息,服務器不一定將數據發完。而建立連接,客戶端給服務器請求連接,服務器是一定要同意的,因此合并在一起毫無問題,四次握手也沒有問題。
驗證TCP四次揮手的兩種狀態:
理解CLOSE_WAIT狀態:
以之前寫過的 TCP 服務器為例, 我們稍加修改,可以看前兩篇博客編寫HTTP服務器
服務器不關閉文件描述符,他的狀態就一直處于CLOSE_WAIT的狀態
運行程序,并且,使用telnet命令與服務器建立一個連接,再使用netstat -nlap查看所有的網絡服務:可以觀察到,服務器狀態為ESTABLISHD、telnet也就是我們的客戶端狀態為ESTABLISHD,我們查到了兩個連接(因為這是在本主機上建立的)。
關閉telnet連接
此時的服務器就處于CLOSE_WAIT狀態
做這個實驗需要量多一些的客戶端,當多個客戶端斷開連接后,并且服務端也斷開連接,我們再看網絡狀態:(使用的端口號是8889),這是服務器在等待客戶端回復ACK,當收到之后就變成CLOSED狀態。
文件描述符泄漏現象:
如果我們的服務器卡頓,查一下是不是存在大量的CLOSE_WAIT
理解 TIME_WAIT 狀態
主動開始斷開連接,自己最終要處于TIME_WAIT的狀態,三次握手一般是滯后于進程退出的,因為進程退出了,連接還會被OS維護。這里又體現了雙方的TCP連接是由雙方的OS自主管理完成的(通信細節)。
主動斷開連接的一方,會在第四次揮手完成,等待一定的時長:2*MSL
現在做一個測試,首先啟動 httpserver,然后啟動 client,然后用 Ctrl-C 使 httpserver 終止,結果是:
此時再退出客戶端,四次揮手完成,從服務器到客戶端的一方存在TIME_WAIT狀態
做一個測試,首先啟動 httpserver,然后啟動 client,然后用 Ctrl-C 使 httpserver 終止,再馬上打開httpserver:
狀態也是TIME_WAIT的狀態,直到大概過了30~60s(系統配置)之后,才能再次開啟統一端口的服務器。
引入一個系統調用接口:
setsockopt
setsockopt
?是用于設置套接字選項的系統調用函數,允許開發者對套接字的行為進行細粒度控制。它在網絡編程中非常關鍵,常用于優化性能、處理異常或調整協議細節。以下是詳細講解:#include <sys/socket.h>int setsockopt(int sockfd, // 套接字描述符int level, // 選項的協議層(如 SOL_SOCKET、IPPROTO_TCP)int optname, // 選項名稱(如 SO_REUSEADDR)const void *optval, // 指向選項值的指針socklen_t optlen // 選項值的長度 );
返回值:
成功返回?
0
,失敗返回?-1
?并設置?errno
。關鍵參數說明
1.?協議層(
level
)
SOL_SOCKET
:通用套接字層選項。
IPPROTO_TCP
:TCP 協議層選項。
IPPROTO_IP
:IP 協議層選項(如多播、廣播配置)。2.?選項名稱(
optname
)
常見選項:
SO_REUSEADDR
:允許地址重用(解決?bind
?地址占用問題)。
SO_KEEPALIVE
:啟用 TCP 心跳檢測(自動探測連接是否存活)。
SO_RCVBUF
/SO_SNDBUF
:調整接收/發送緩沖區大小。
SO_LINGER
:控制?close()
?關閉時的行為(立即關閉或等待數據發送)。
TCP_NODELAY
:禁用 Nagle 算法(減少小數據包的延遲)。更新套接字類中代碼:
再次實施之前的操作,就能一直重啟服務器了
?通過指令查MSL時間長度?
pupu@VM-8-15-ubuntu:~/computer-network/class_67/http$ cat /proc/sys/net/ipv4/tcp_fin_timeout
60
想一想, 為什么是 TIME_WAIT 的時間是 2MSL?
? MSL 是 TCP 報文的最大生存時間, 因此 TIME_WAIT 持續存在 2MSL 的話
? 就能保證在兩個傳輸方向上的尚未被接收或遲到的報文段都已經消失(否則服務器立刻重啟, 可能會收到來自上一個進程的遲到的數據, 但是這種數據很可能是錯誤的);
? 同時也是在理論上保證最后一個報文可靠到達(假設最后一個 ACK 丟失, 那么服務器會再重發一個 FIN. 這時雖然客戶端的進程不在了, 但是 TCP 連接還在, 仍然可以重發 LAST_ACK);
流量控制
接收端處理數據的速度是有限的. 如果發送端發的太快, 導致接收端的緩沖區被打滿, 這個時候如果發送端繼續發送, 就會造成丟包, 繼而引起丟包重傳等等一系列連鎖反應.
因此 TCP 支持根據接收端的處理能力(接受緩沖區中剩余空間的大小 、根據16位窗口大小(填寫的都是自己的)、填的都是自己的接收能力), 來決定發送端的發送速度.
這個機制就叫做流量控制(Flow Control);
接收端如何把窗口大小告訴發送端呢? 回憶我們的 TCP 首部中, 有一個 16 位窗口字段,就是存放了窗口大小信息;
注意細節:我們需要知道,在三次握手期間(不僅建立連接,也能協商通信數據),雙方就已經協商交換過了雙方的接收能力
? 接收端將自己可以接收的緩沖區大小放入 TCP 首部中的 "窗口大小" 字段, 通過 ACK 端通知發送端;
? 窗口大小字段越大, 說明網絡的吞吐量越高;
? 接收端一旦發現自己的緩沖區快滿了, 就會將窗口大小設置成一個更小的值通知給發送端;
? 發送端接收到這個窗口之后, 就會減慢自己的發送速度;
? 如果接收端緩沖區滿了, 就會將窗口置為 0; 這時發送方 不再發送數據, 但是需要定期發送一個窗口探測數據段, 使接收端把窗口大小告訴發送端.
那么問題來了, 16 位數字最大表示 65535, 那么 TCP 窗口最大就是 65535 字節么?
實際上, TCP 首部 40 字節選項中還包含了一個窗口擴大因子 M, 實際窗口大小是 窗口
字段的值左移 M 位;
PSH標記位
如果我們發的數據想讓對方盡快處理、交付(交給上層),就可以設置PSH(PUSH)。盡快將緩沖區騰出空位。
URG標記位、緊急指針是否有效
只要URG標記位為0,16位緊急指針就無意義,一旦設置為1,16位緊急指針才有意義
因此16位緊急指針是什么?標識數據部分,哪部分是緊急數據,緊急數據相對于報文的數據部分的偏移量。在工作場景,實際當中凡事具有指向、標識功能的都可以稱之為指針。C語言中指地址。
TCP中緊急數據只有一個字節、緊急指針一般很少用,對TCP通信做管理,規定一些狀態數字(1字節就夠了)(0:正常、1:暫停、2:取消)
在之前講過的recv、send系統調用接口函數中都有一個flag標志位,可以設置為:MSG_OOB、攜帶緊急指針。
out-of-band:帶外數據、很少使用
滑動窗口
問題1
1.流量控制:在發送方如何根據對方的接收能力,發送數據?
2.超時重傳:超時時間以內,已經發送的報文不能被丟棄,而是要保存起來,保存在哪里?
前面我們討論了確認應答策略, 對每一個發送的數據段, 都要給一個 ACK 確認應答. 收到 ACK 后再發送下一個數據段. 這樣做有一個比較大的缺點, 就是性能較差. 尤其是數據往返的時間較長的時候。
既然這樣一發一收的方式性能較低, 那么我們一次發送多條數據, 就可以大大的提高性能(其實是將多個段的等待時間重疊在一起了)、前面講過:
發送方:規定一個概念,滑動窗口,在滑動窗口以內的數據,可以直接發送,暫時不用收到應答。
滑動窗口的本質:發送緩沖區當中的一部分。
? 窗口大小指的是無需等待確認應答而可以繼續發送數據的最大值. 上圖的窗口大小就是 4000 個字節(四個段).
? 發送前四個段的時候, 不需要等待任何 ACK, 直接發送;
? 收到第一個 ACK 后, 滑動窗口向后移動, 繼續發送第五個段的數據; 依次類推;
? 操作系統內核為了維護這個滑動窗口, 需要開辟 發送緩沖區 來記錄當前還有哪些數據沒有應答; 只有確認應答過的數據, 才能從緩沖區刪掉;
? 窗口越大, 則網絡的吞吐率就越高;
在滑動窗口以內的數據:暫時可以不用應答,可以直接發送
在滑動窗口左邊的數據:已發送,已確認
在滑動窗口右側的數據:待發送
問題2
1.滑動窗口只能向右滑動嗎?是的,能不能向左滑動?不能
2.滑動窗口是一直不變的嗎?可以變大、變小嗎?滑動窗口是能夠變化的(流量控制)
3.滑動窗口可以為0嗎?可以
如何理解滑動窗口?
滑動窗口的本質就是發送窗口的下標
可以為滑動窗口建立一個模型,由兩個指針指向滑動窗口的邊界
滑動窗口的模型
窗口向右移動,也就是讓win_start++、win_end++,改變滑動窗口的大小,也就是讓滑動窗口的左右邊界指針誰++的更快(例如,窗口增大:win_start增長的慢、win_end增長的快、極端情況也就是win_start不變。win_end一直++)、滑動窗口為0,則是兩個指針指向同一個位置。
滑動窗口是如何更新的?
應答里面一定有一個確認序號、因此win_start = ack_seq 、win_end = win_start +win(win是對方的接收能力)。
那么如果出現了丟包, 如何進行重傳?????????這里分兩種情況討論
數據包已經抵達, ACK 被丟了
?這種情況下, 部分 ACK 丟了并不要緊, 因為可以通過后續的 ACK 進行確認;
情況二: 數據包就直接丟了
? 當某一段報文段丟失之后, 發送端會一直收到 1001 這樣的 ACK, 就像是在提醒發送端 "我想要的是 1001" 一樣;(win_start不會更新,仍然指向1001)
? 如果發送端主機連續三次收到了同樣一個 "1001" 這樣的應答, 就會將對應的數據 1001 - 2000 重新發送;
? 這個時候接收端收到了 1001 之后, 再次返回的 ACK 就是 7001 了(因為 2001 - 7000)接收端其實之前就已經收到了, 被放到了接收端操作系統內核的接收緩沖區中;
這種機制被稱為 "高速重發控制"(也叫 "快重傳").
也有可能收到兩個同樣的“1001”應答,這個時候就會由超時重傳來接管,這可能是因為,剩余的報文不滿足三個了,發回來的應答不足三個。
以上的情況就是最左側報文丟失:
最左側報文丟失:解決辦法
1.確認序號規定的約束,滑動窗口左側不動
2.快重傳&&超時重傳,對最左側報文進行補發
中間報文丟失?
如果是2001~3001的報文丟失了,填2001確認序號、win_start更新到2001。這時候就變成新窗口的最左側報文丟失的問題。
最右側報文丟失?
如果是4001~5001的報文丟失了,填4001確認序號、win_start更新到4001。這時候就變成新窗口的最左側報文丟失的問題。
當上層接收了數據,就會更新接收能力,win_end更新。
滑動窗口問題解答
1.流量控制:在發送方如何根據對方的接收能力,發送數據?
對方的接收能力為0,那么滑動窗口為0,5000,5000以內的數據都可以由TCP直接發。
流量控制就是通過滑動窗口實現的。
2.超時重傳:超時時間以內,已經發送的報文不能被丟棄,而是要保存起來,保存在哪里?
保存在滑動窗口中,丟包問題都會被轉化成最左側報文丟失。
滑動窗口一直向右滑動,會越界嗎?
不會、將發送緩沖區想象成環形隊列(本質物理上發送緩沖區也是一個隊列),就不會出現溢出越界問題。
是否需要清除已發送已確認的數據?
不用,代表的就是廢棄數據,因為是環狀隊列,之后滑動窗口再滑動的時候,數據拷貝下來的時候,會將這些廢棄數據覆蓋。將滑動窗口移到了最左側就相當于丟棄報文、因為這些報文已經被發送被確認。
實際上滑動窗口 = min(應答窗口, 擁塞窗口)
擁塞控制
雖然 TCP 有了滑動窗口這個大殺器, 能夠高效可靠的發送大量的數據. 但是如果在剛開始階段就發送大量的數據, 仍然可能引發問題.
因為網絡上有很多的計算機, 可能當前的網絡狀態就已經比較擁堵. 在不清楚當前網絡狀態下, 貿然發送大量的數據, 是很有可能引起雪上加霜的.
TCP 引入 慢啟動 機制, 先發少量的數據, 探探路, 摸清當前的網絡擁堵狀態, 再決定按照多大的速度傳輸數據;
? 此處引入一個概念稱為擁塞窗口
因為網絡的狀況是浮動的,因此擁塞窗口的大小,也必然是浮動的,主機應該怎么才能得知,擁塞窗口的接近大小應該是多大?必須經過多輪嘗試,才能知道。
? 發送開始的時候, 定義擁塞窗口大小為 1;
? 每次收到一個 ACK 應答, 擁塞窗口加 1;
? 每次發送數據包的時候, 將擁塞窗口和接收端主機反饋的窗口大小做比較, 取較小的值作為實際發送的窗口; 滑動窗口 = min(應答窗口,擁塞窗口)
"慢啟動" 只是指初使時慢, 但是增長速度非常快.
像上面這樣的擁塞窗口增長速度, 是指數級別的.
前期慢,可以慢慢減少網絡發送,讓網絡恢復,網絡恢復,我們的通信過程也要恢復起來,中后期增長就快。因此指數增長
? 為了不增長的那么快, 因此不能使擁塞窗口單純的加倍.
? 此處引入一個叫做慢啟動(2^n)的閾值
? 當擁塞窗口超過這個閾值的時候, 不再按照指數方式增長, 而是按照線性方式增長
后線性是為了精細化探測出最新擁塞窗口的值、線性探測
? 當 TCP 開始啟動的時候, 慢啟動閾值等于窗口最大值;
? 在每次超時重發的時候, 慢啟動閾值會變成原來擁塞窗口的一半, 同時擁塞窗口置回 1;
少量的丟包, 我們僅僅是觸發超時重傳; 大量的丟包, 我們就認為網絡擁塞;
當 TCP 通信開始后, 網絡吞吐量會逐漸上升; 隨著網絡發生擁堵, 吞吐量會立刻下降;
擁塞控制, 歸根結底是 TCP 協議想盡可能快的把數據傳輸給對方, 但是又要避免給網絡造成太大壓力的折中方案。
擁塞窗口的值是有上限的,根據OS決定
延遲應答
如果接收數據的主機立刻返回 ACK 應答, 這時候返回的窗口可能比較小
? 假設接收端緩沖區為 1M. 一次收到了 500K 的數據; 如果立刻應答, 返回的窗口就是 500K;
? 但實際上可能處理端處理的速度很快, 10ms 之內就把 500K 數據從緩沖區消費掉了;
? 在這種情況下, 接收端處理還遠沒有達到自己的極限, 即使窗口再放大一些, 也能處理過來;
? 如果接收端稍微等一會再應答, 比如等待 200ms 再應答, 那么這個時候返回的窗口大小就是 1M;
一定要記得, 窗口越大, 網絡吞吐量就越大, 傳輸效率就越高. 我們的目標是在保證網絡不擁塞的情況下盡量提高傳輸效率;
那么所有的包都可以延遲應答么? 肯定也不是;
? 數量限制: 每隔 N 個包就應答一次;
? 時間限制: 超過最大延遲時間就應答一次;
具體的數量和超時時間, 依操作系統不同也有差異; Linux一般 N 取 2, 超時時間取 200ms;
捎帶應答
在延遲應答的基礎上, 我們發現, 很多情況下, 客戶端服務器在應用層也是 "一發一收"的. 意味著客戶端給服務器說了 "How are you", 服務器也會給客戶端回一個 "Fine, thank you";
那么這個時候 ACK 就可以搭順風車, 和服務器回應的 "Fine, thank you" 一起回給客戶端
第三次握手可以捎帶應答?
注意:三次握手完成才意味著連接建立成功,連接建立成功才能開始通信。而對于客戶端來說,由于第二次握手收到了服務器發來的肯定,第二次握手成功后,客戶端已經認為連接建立成功,因此在第三次握手的時候客戶端 可以攜帶數據
***面向字節流***重要***粘包問題***
面向字節流
? 調用 write 時, 數據會先寫入發送緩沖區中;
? 如果發送的字節數太長, 會被拆分成多個 TCP 的數據包發出;
? 如果發送的字節數太短, 就會先在緩沖區里等待, 等到緩沖區長度差不多了, 或者其他合適的時機發送出去;
? 接收數據的時候, 數據也是從網卡驅動程序到達內核的接收緩沖區;
? 然后應用程序可以調用 read 從接收緩沖區拿數據;
? 另一方面, TCP 的一個連接, 既有發送緩沖區, 也有接收緩沖區, 那么對于這一個連接, 既可以讀數據, 也可以寫數據. 這個概念叫做 全雙工
由于緩沖區的存在, TCP 程序的讀和寫不需要一一匹配, 例如:
? 寫 100 個字節數據時, 可以調用一次 write 寫 100 個字節, 也可以調用 100 次write, 每次寫一個字節;
? 讀 100 個字節數據時, 也完全不需要考慮寫的時候是怎么寫的, 既可以一次read 100 個字節, 也可以一次 read 一個字節, 重復 100 次;
粘包問題:
可以看http從0開始實現的博客
? 首先要明確, 粘包問題中的 "包" , 是指的應用層的數據包.
? 在 TCP 的協議頭中, 沒有如同 UDP 一樣的 "報文長度(首部長度)" 這樣的字段, 但是有一個序號這樣的字段.
? 站在傳輸層的角度, TCP 是一個一個報文過來的. 按照序號排好序放在緩沖區中.
? 站在應用層的角度, 看到的只是一串連續的字節數據.
? 那么應用程序看到了這么一連串的字節數據, 就不知道從哪個部分開始到哪個部分, 是一個完整的應用層數據包.
那么如何避免粘包問題呢? 歸根結底就是一句話, 明確兩個包之間的邊界
? 對于定長的包, 保證每次都按固定大小讀取即可; 例如上面的 Request 結構, 是固定大小的, 那么就從緩沖區從頭開始按 sizeof(Request)依次讀取即可;
? 對于變長的包, 可以在包頭的位置, 約定一個包總長度的字段, 從而就知道了包的結束位置;
? 對于變長的包, 還可以在包和包之間使用明確的分隔符(應用層協議, 是由我們自己來定的, 只要保證分隔符不和正文沖突即可);
思考: 對于 UDP 協議來說, 是否也存在 "粘包問題" 呢?
? 對于 UDP, 如果還沒有上層交付數據, UDP 的報文長度仍然在. 同時, UDP 是一個一個把數據交付給應用層. 就有很明確的數據邊界.
? 站在應用層的站在應用層的角度, 使用 UDP 的時候, 要么收到完整的 UDP 報文, 要么不收. 不會出現"半個"的情況
數據庫軟件:Redis、MySQL會存在許多序列化和反序列化,也需要有格式存取,這也就是協議。
網絡通信和本地文件流,沒有區別。
TCP異常情況
進程終止: 進程終止會釋放文件描述符, 仍然可以發送 FIN. 和正常關閉沒有什么區別.
機器重啟: 和進程終止的情況相同.OS會做,其他進程都結束了OS才會退出。都會四次揮手
機器掉電/網線斷開: 接收端認為連接還在, 一旦接收端有寫入操作, 接收端發現連接已經不在了, 就會進行 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 都是程序員的工具, 什么時機用, 具體怎么用, 還是要根據具體的需求場景去判定.
?結語:
? ? ? ?隨著這篇博客接近尾聲,我衷心希望我所分享的內容能為你帶來一些啟發和幫助。學習和理解的過程往往充滿挑戰,但正是這些挑戰讓我們不斷成長和進步。我在準備這篇文章時,也深刻體會到了學習與分享的樂趣。 ? ?
? ? ? ? ?在此,我要特別感謝每一位閱讀到這里的你。是你的關注和支持,給予了我持續寫作和分享的動力。我深知,無論我在某個領域有多少見解,都離不開大家的鼓勵與指正。因此,如果你在閱讀過程中有任何疑問、建議或是發現了文章中的不足之處,都歡迎你慷慨賜教。
? ? ? ? 你的每一條反饋都是我前進路上的寶貴財富。同時,我也非常期待能夠得到你的點贊、收藏,關注這將是對我莫大的支持和鼓勵。當然,我更期待的是能夠持續為你帶來有價值的內容