目錄
- 一、再談端口號
- 1.1 端口號
- 1.2 端口號的范圍劃分
- 1.3 常見知名端口號
- 1.4 netstat 命令
- 1.5 進程與端口號的關系
- 1.6 pidof 命令
- 二、UDP協議
- 2.1 UDP協議段格式
- 2.2 如何理解UDP報頭和UDP報文
- 2.2.1 UDP報頭
- 2.2.2 UDP報文和UDP報文的管理
- 2.2.3 UDP封裝過程
- 2.3 UDP的特點
- 2.4 UDP的緩沖區
- 2.5 UDP使用注意事項
- 2.6 基于UDP的應用層協議
- 三、TCP協議
- 3.1 TCP協議段格式
- 3.1.1 16位緊急指針(標記位URG)
- 3.1.2 標記位PSH
- 3.1.3 標記位RST
- 3.2 流量控制(16位窗口大小)
- 3.3 確認應答機制(32位序號和32確認序號)
- 3.4 捎帶應答(32位序號和32確認序號)
- 3.5 超時重傳機制
- 3.6 三次握手和四次揮手
- 3.6.1 三次握手中服務器和客戶端狀態變化
- 3.6.2 三次握手(建立連接)
- 3.6.2.1 為什么要進行三次握手?
- 3.6.3 四次揮手(斷開連接)
- 3.6.3.1 為什么要進行四次揮手
- 3.6.3.2 CLOSE_WAIT 和 TIME_WAIT
- 3.7 滑動窗口
- 3.7.1 滑動窗口在哪里?
- 3.7.2 如何理解滑動窗口?
- 3.7.3 滑動窗口的大小變化嗎?
- 3.7.4 報文/響應報文丟失了怎么辦?(快重傳)
- 3.8 擁塞控制
- 3.9 面相字節流
- 3.10 粘包問題
- 3.11 TCP異常情況
- 3.12 半連接隊列和全連接隊列(listen 的第二個參數)
- 3.13 文件、Socket、系統和網絡之間的關系
- 四、UDP與TCP的對比
- 4.1 UDP與TCP的特點對比
- 4.2 用UDP實現可靠傳輸
- 結尾
一、再談端口號
1.1 端口號
端口號(Port)標識了一個主機上進行通信的不同的應用程序
在TCP/IP協議中,用 “源IP”,“源端口號”,“目的IP”,“目的端口號”,“協議號” 這樣一個五元組來標識一個通信。
1.2 端口號的范圍劃分
- 0 - 1023:知名端口號,HTTP、FTP、SSH等這些廣為使用的應用層協議,它們的端口號都是固定的
- 1024 - 65535:操作系統動態分配的端口號。客戶端程序的端口號,就是由操作系統從這個范圍分配的
1.3 常見知名端口號
- ssh服務器,使用22端口
- ftp服務器,使用21端口
- telnet服務器,使用23端口
- http服務器,使用80端口
- https服務器,使用443端口
執行下面的命令,可以看到知名端口號
cat /etc/services
所以在我們自己程序中需要使用端口號時,需要避開這些知名端口號
1.4 netstat 命令
語法:netstat [選項]
功能:netstat 是網絡管理中常用的命令行工具
常見選項:
-
-n:以數字的形式顯示IP地址和端口號
-
-a:顯示所有端口
-
-l:僅僅顯示監聽狀態的端口
-
-t:僅僅顯示TPC連接
-
-u:僅僅顯示UDP連接
-
-p:顯示與每個連接關聯的進程 ID(需要root權限)
1.5 進程與端口號的關系
- 一個進程是否可以bind多個端口號?不可以,端口號只能標識唯一的進程。
- 一個端口號是否可以被多個進程bind?可以。
1.6 pidof 命令
語法:pidof [進程名]
功能:通過進程名,查看進程id,在查看服務器的進程id時非常方便。
二、UDP協議
2.1 UDP協議段格式
- 16位源端口號:標識發送方端口號
- 16位目的端口號:標識接收方端口號
- 16位UDP長度:標識整個UDP報文的長度
- 16位UDP校驗和:用于檢測UDP數據報在傳輸過程中是否發生錯誤。
UDP協議由UDP報頭和有效載荷(數據)構成,談到協議就有下面兩個問題需要解決:
- 如何解決報頭與有效載荷分離的問題
UDP報頭采用的是8字節固定報頭長度,剩下的就是有效載荷 - 如何解決有效載荷向上交付的問題
UDP的報頭中有一個字段叫做目的端口號,UDP就是通過目的端口號將有效載荷交付給上層進程的
對于UDP協議還有下面一個問題:UDP是面向數據報的,發送方發什么,接收方就收到什么,并且接收方不需要解決拆分報文,總需要有誰來解決這個問題,那它是如何知道報文是否被收齊的呢?
這個工作實際上是由接收方的操作系統來完成的,UDP報文中有一個字段叫做UDP長度,它記錄的是整個UDP報文的長度,包括報頭和有效載荷,去除報頭部分的長度就是有效載荷的長度,若實際收到有效載荷的長度比它小,則說明沒有收齊,就需要將該報文丟棄。
2.2 如何理解UDP報頭和UDP報文
2.2.1 UDP報頭
UDP報頭實際上就是一個結構體:
struct udphdr
{uint32_t srcport:16;uint32_t dstport:16;uint32_t length:16;uint32_t checksum:16;
}
Linux操作系統內核就是這樣表示的。
2.2.2 UDP報文和UDP報文的管理
在客戶端和服務器中,一定存在同時收到很多UDP報文的情況,所以客戶端和服務器需要對這些UDP報文進行管理,管理就需要提到先描述再組織了,先使用一個結構體描述UDP報文,再使用鏈表將所有的結構體管理起來。
對于UDP報文來說,它也是一個結構化字段(結構體):
struct sk_buff
{char* data; // 指向報文開頭char* tail; // 指向報文結尾struct sk_buff* next; // 指向下一個結構體
}
Linux操作系統內核中對UDP報文的描述更加詳細。
當發生方的應用層向傳輸層中的UDP交付數據后,此時UDP就需要對該數據進行封裝,操作系統會為其創建一個sk_buff結構體和緩沖區,將數據和UDP報頭依次保存好后,再將結構體連接到鏈表中,再將結構體向下交付給數據鏈路層繼續進行封裝。
當接收方在接收到報文后,會向上進行解包和分用,到傳輸層時,由于發送方和接收方使用的都是UDP協議,也就是同樣的結構化字段,就可以根據UDP報文對應的結構體和UDP報頭所對應的結構體,對UDP報文做出解釋。
2.2.3 UDP封裝過程
首先緩沖區中沒有任何數據,所以data和tail最開始指向的同一個位置。
然后根據數據的大小,data指針向前移動數據大小的位置,將數據存放在data與tail指向的區間。
最后再添加報頭,data指針向前移動報頭大小的位置,將UDP報頭存存放在data與tail指向的區間。
2.3 UDP的特點
UDP傳輸的過程類似于寄信。
- 無連接:知道對端的IP和端口號就直接進行傳輸,不需要建立連接
- 不可靠:沒有確認機制,沒有重傳機制;如果因為網絡故障該段無法發到對方,UDP協議層也不會給應用層返回任何錯誤信息
- 面向數據報:不能夠靈活的控制讀寫數據的次數和數量,將每一個獨立的報文作為一個整體發送,保留報文邊界
面向數據報:應用層交給UDP多長的報文,UDP原樣發送,既不會拆分,也不會合并
用UDP傳輸100個字節的數據:如果發送端調用一次sendto,發送100個字節,那么接收端也必須調用對應的一次recvfrom,接收100個字節;而不能循環調用10次recvfrom,每次接收10個字節。
2.4 UDP的緩沖區
-
UDP沒有真正意義上 顯示的發送緩沖區。調用sendto會直接交給內核,由內核將數據傳給網絡層協議進行后續的傳輸動作。
-
UDP具有真正意義上顯示的接收緩沖區。但是這個接收緩沖區不能保證收到的UDP報的順序和發送UDP報的順序一致,如果緩沖區滿了,再到達的UDP數據就會被丟棄。
UDP的socket既能讀,也能寫,這個概念叫做 全雙工。
2.5 UDP使用注意事項
我們注意到,UDP協議首部中有一個16位的最大長度。也就是說一個UDP能傳輸的數據最大長度是64K(包含UDP首部)。
然而64K在當今的互聯網環境下,是一個非常小的數字。所以當用戶需要傳輸的數據超過64K時,用戶需要在應用層手動進行分包,多次發送,并在接收端需要將這些數據進行手動拼裝。
2.6 基于UDP的應用層協議
- NFS協議:網絡文件系統
- TFTP協議:簡單文件傳輸協議
- DHCP協議:動態主機配置協議
- BOOTP協議:啟動協議(用于無盤設備啟動)
- DNS協議:域名解析協議
三、TCP協議
3.1 TCP協議段格式
- 16位源端口號:標識發送方端口號
- 16位目的端口號:標識接收方端口號
- 32位序號:數據段第一個字節的序列號(用于排序和去重)
- 32位確認序號:用于檢測UDP數據報在傳輸過程中是否發生錯誤
- 4位首部長度:TCP頭部的長度(單位4字節)
- 6位標志位:控制TCP行為(URG、ACK等)
- URG:緊急指針是否有效
- ACK:確認號是否有效
- PSH:提示接收端應用程序立刻從TCP緩沖區把數據讀走
- RST:對方要求重新建立連接,我們把攜帶RST標識的稱為復位報文段
- SYN:請求建立連接,我們把攜帶SYN標識的稱為同步報文段
- FIN:通知對方,本端要斷開連接
- 16位窗口大小:接收方當前可接收的字節數(用于流量控制)
- 16位校驗和:頭部和數據的校驗和
- 16位緊急指針:緊急數據的偏移量(僅在URG標志位為1時有效)。
TCP協議由TCP報頭和有效載荷(數據)構成,談到協議就有下面兩個問題需要解決:
- 如何解決報頭與有效載荷分離的問題
TCP報頭中,有20字節固定長度,其中包含一個字段4位首部長度,它的單位為4字節,4位比特位能表示[0,15],也就是首部長度最大為60字節,減去20字節的固定長度,剩下的就是選項,這樣就完整的找到了報頭,剩下的就是有效載荷了 - 如何解決有效載荷向上交付的問題
TCP報頭中有一個字段叫做目的端口號,TCP就是通過目的端口號將有效載荷交付給上層進程的
3.1.1 16位緊急指針(標記位URG)
TCP會將接收到的報文保存在以隊列形式組織起來的接收緩沖區中,如果說隊列中已經存在了很多報文,此時有一個緊急任務需要處理,由于緩沖區的存在,這個任務也需要在緩沖區中排隊,這顯然是不合理的,緊急任務就是需要被盡快處理的,所以這個報文需要插隊。
將報文中的URG標記位置為1,則說明該報文時緊急任務,報頭中16位緊急指針表示的就是緊急數據在有效載荷的偏移量。僅僅數據的大小為1字節,也就是說TCP允許插隊,但是不允許大量的插隊。
那么什么是緊急任務呢?舉個例子:假設用戶正在想服務器中上傳數據,但是此時用戶發現自己想要上傳的數據不是這個,所以用戶需要終止上傳行為,有了URG和緊急指針,即使服務器的接收緩沖區中有大量的報文,服務器已經可以優先處理這個緊急任務。
3.1.2 標記位PSH
在流量控制中會講到報文中窗口大小表示的就是接收方當前可用的接收緩沖區的大小,假設TCP對應的上層執行任務非常消耗時間,導致其接收緩沖區被填滿,此時接收方就需要通過窗口大小來告訴發送方自己的接收緩沖區已經不能接收數據了,此時發送到就需要等待接收方上層讀取緩沖區的數據。
但是發送方一直這么等下去也不是辦法,所以發送方會發送探測報文,以檢查接收方是否已恢復接收能力,等待接收方報文中窗口大小,如果窗口大小不足時,發送方依舊會停止發送數據,發送方就會每過一段時間就重復這個過程,如果重復的次數多了,報文就會將PSH標記位置為1,告訴接收方盡快將接收緩沖區中的內容交付給上層。
還有一種方式就是發送方在等待的過程中,接收方上層讀取了接收緩沖區內的數據,接收緩沖區就可以接收發送方的數據,接收方就會向發送方發送報文,報文中更新了自己的窗口大小。
無論是發送方發送的探測報文報文后,接收方回復報文,還是接收方直接發送報文,這兩種方式都是告訴發送方,接收方已經能夠接收數據,發送方可以繼續發送數據了。
3.1.3 標記位RST
TCP是保證可靠性的,那么是否就能保證TCP的三次握手一定是成功的呢?
并不是,TCP保證可靠性并不是指TCP的三次握手一定是成功的,它指的是接收方收到了發送方發送的哪些數據,發送方是知道的,發送方發送的哪些數據是有問題的,發送方是知道的。
所以,TCP的三次握手是有可能失敗的,當TCP第一次和第二次失敗的時候,由于有響應的存在,客戶端和服務器是報文是否被對方接收了的,但是第三次握手是沒有響應的,所以客戶端時無法保證服務器接收到了報文。
第三次握手時,客戶端認為自己發送出報文,三次握手就完成了,而服務器需要接收到客戶端的報文,服務器才認為三次握手完成。假設第三次握手的報文丟失了,客戶端認為三次握手完成了,服務器認為三次握手沒有完成。
客戶端認為三次握手完成了,然后開始向服務器發送數據,然后服務器就感到疑惑,沒有完成三次握手,客戶端怎么就向我發送數據了,然后服務器就向客戶端發送報文,報文中將RST標記位置為1,表示
3.2 流量控制(16位窗口大小)
TCP協議是擁有顯示意義上的發送緩沖區和接收緩沖區的,當發送方應用層通過write/send函數向接收方發送數據時,由于write/send函數本質上就是拷貝函數,它會將應用層的數據拷貝到TCP的發送緩沖區中,通過一系列操作,將數據拷貝到接收方的接收緩沖區,最終接收方的應用層通過read/recv函數將數據讀取上去。
在下圖中,我就將這過程進行簡單的演示,對于處在同一層協議棧的雙方,發送方在同一層發出的數據,就是接收方收到的數據,并且下面的協議我們還沒講到,就先跳過一下。
這里假設發送方瘋狂的向接收方發送數據,導致接收方來不及讀取,那么接收方存儲不下的報文應該怎么辦?
丟棄嗎?這里并不是報文的問題,并且報文傳輸也是消耗資源了的,所以丟棄報文顯然是不合理的。
所以不能丟棄,那么接收方在自己接收緩沖區保存不下時,就需要告訴發送方不要再發送數據了。這就是我這里要將的流量控制,流量控制是誰做的?用戶好像并沒有管過,實際上流量控制是發送方的TCP做的,本質上就是操作系統做的。
發送方的操作系統如何進行流量控制呢?發送方一定需要知道接收方的當前可用的接收緩沖區的大小。
發送方怎么知道接收方當前可用的接收緩沖區的大小呢?一般來說發送方發送一個TCP報文,接收方就需要返回一個TCP報文,這就是確認應答機制(后面會講),又TCP報頭中有一個字段16位窗口大小,它就是記錄接收方當前可用的接收緩沖區的大小。
有了流量控制,當接收方可用的接收緩沖區的大小不足時,發送方就會減少或停止發送數據,但是用戶在一直發,這就會導致發送方的發送緩沖區被填滿,最終導致進程阻塞,只有當操作系統將發送緩沖區中的數據發送出去時,進程才會被移除阻塞隊列。
同樣當接收方的接收緩沖區為空時,進程也會被阻塞,只有發送方有數據發送到接收緩沖區中,進程才會被移出阻塞隊列。
但是用戶并不需要關心這個過程,TCP(操作系統)會自己管理進程是否發送的問題。
這與我們在操作系統中文件讀寫相關知識很類似,當我們向文件中寫入數據時,實際上是向的文件的內核基本緩沖區中,數據什么時候刷新到磁盤中,一次刷新多少,都不需要用戶操心。
而這里同樣如此,用戶將數據發送出去,實際上就是將數據拷貝到發送緩沖區中,什么時候發,一次發多少,這都是TCP(操作系統)需要操心的事,用戶就不需要管了。
3.3 確認應答機制(32位序號和32確認序號)
對于TCP而言,當客戶端發送了一個報文以后,服務器就需要回應一個報文,當客戶端接收到了服務器的報文就知道服務器收到了客戶端的報文,但是服務器卻不知道自己發送的報文客戶端是否收到,所以客戶端就又向服務器發送一個報文,當服務器收到了客戶端發的報文以后,服務端就知道它發送的報文客戶端收到了,此時客戶端又不知道自己發送的報文服務器是否收到,一直重復這樣的操作,最終最后發生報文的一方,無法知道對方是否接受到報文。所以,TCP暫時做不到100%可靠的網絡通信,但是可以做到局部上的可靠通信,當一方收到了響應,就能100%保證歷史上最近的一個報文是被對方收到了的。
下圖是為了大家方便理解而畫的圖,這是TCP發送數據的一種方式,下圖中客戶端發送報文時,是串行發送的,但是這樣有一個缺點就是慢。
在TCP中,TCP是允許同時發送多個TCP報文的。由于報文在網絡傳輸中各種因素的影響,可能會導致服務器收到的報文順序是亂序的,亂序是一直不可靠的一種,TCP報頭中的32位序號就可以解決報文順序亂序的問題,保證報文的按序到達(按序到達是對TCP的上層來說的)。
TCP會將數據中的每一個字節進行編號,并將數據中第一個序號保存在TCP報頭中的32位序號中,當客戶端同時發送多個報文時,經過網絡的傳輸可能導致服務器接收報文的順序是亂序的,但是由于有報頭中序號的存在,TCP就可以通過序號的大小進行排序,從而使報文變得有序。
而報頭的32位確認序號表示的是確認序號之前的所有報文都被對方全部接收了。以下圖為例,服務器收到四個報文,就需要響應四個報文,其中四個報文中的確認序號為101、201、301和401,假設除了確認序號為401的報文,其他的報文全部丟失,只要客戶端收到確認序號為401的報文,就可以確定客戶端發送的四個報文已經被服務器收到了。這樣做了以后,TCP協議就能夠允許少量報文的丟失。
3.4 捎帶應答(32位序號和32確認序號)
上面我們講到了請求報文中的32序號能夠讓接收方接收到的報文按序到達,響應報文中的32位確認序號可以讓發送方知道自己發送的哪些報文已經被接收方收到了。
這時候就有一個問題了,請求報文和響應報文,是兩個分開的報文,為什么報文中需要存在序號和確認序號呢?為什么不可以將它們合并在一起呢?
這是因為上面我們講到的只有服務器確認的情況,實際上服務器也會發送數據。例如,當客戶端發送請求報文以后,服務器需要對客戶端發送ACK,表示自己已經收到了報文,然后服務器也想給客戶端發送數據,這樣服務器就對客戶端的一個報文,響應了兩個報文,為什么不將這兩個報文合并呢?將服務器想要發送數據的報文的ACK置為1,就可以將兩個報文合并為一個報文了,這就是捎帶應答。因為序號和確認序號可能會被同時使用,所以不能合并。
捎帶應答就是通過將確認信息與待發送的數據結合在一起傳輸,從而減少通信開銷和降低延遲。
3.5 超時重傳機制
超時重傳機制就是主機A向主機B發送數據,在特定的時間間隔中沒有收到主機B的確認應答,主機A就會向主機B重新發送未被確認的數據。
主機A向主機B發送數據,但是可能由于網絡中的各種原因,導致數據沒有送達到主機B,主機A在一定時間內沒有收到主機B的應答,就會將未被確認的數據進行重發。
由于主機A需要收到主機B的應答才能夠確定,自己發送的數據被主機B收到了,所以除了上面的情況,還有主機B發送的確認應答由于網絡的各種原因,導致主機A沒有收到主機B的應答,主機A在一段時間內沒有收到主機B的應答,就會將未被確認的數據進行重發。
在該情況下,主機B就會收到主機A相同的報文,主機B就會根據報文中的序號,對報文進行去重,保留新的還是舊的就需要看操作系統了。
在上面兩種情況中,無論是主機A發送的請求報文丟失了,還是主機B發送的應答報文丟失了,最終結果都是主機A進行超時重傳。
由于主機A需要得到主機B的確認應答,才能保證數據沒主機B接收了,所以主機A發送數據后,不能立即從發送緩沖區移除,需要暫時保留一段時間,直到主機B發送確認應答后,才能夠移除。
暫時保存在發送緩沖區的數據是在滑動窗口的區域中,收到應答后,通過滑動窗口的移動,移除指定的數據(滑動窗口在后面講解)。
那么超時時間是多久呢?
- 如果超時時間太長會導致效率降低
- 如果超時時間太短會導致過于頻繁的進行重傳
并且由于網絡環境是動態變化的,所以超時時間也需要是浮動的。
TCP為了保證無論在任何環境下都能比較高性能的通信,因此會動態計算這個最大超時時間。
Linux中超時以500ms為一個單位進行控制,每次判定超時重發的超時
時間都是500ms的整數倍。
如果重發一次之后,仍然得不到應答,等待 2*500ms 后再進行重傳。
如果仍然得不到應答,等待 4*500ms 進行重傳。依次類推,以指數形式遞增。
累計到一定的重傳次數,TCP認為網絡或者對端主機出現異常,強制關閉連接。
3.6 三次握手和四次揮手
主機可能在同一時間內存在不同狀態的連接,所以操作系統就需要對連接進行管理,管理的方式就是先描述再組織,先通過連接的各種屬性,將連接描述為一個結構體,再使用特定的數據結構將所有的結構體管理起來,當建立一個連接以后,操作系統就為這個連接創建一個結構體對象,在將對方放入到數據結構中,最終對連接的管理就轉變為了對數據結構的管理。
3.6.1 三次握手中服務器和客戶端狀態變化
服務端狀態轉化:
- [CLOSED -> LISTEN],服務器端調用listen后進入LISTEN狀態,等待客戶端連接。
- [LISTEN -> SYN_RCVD] ,一旦監聽到連接請求,就將該連接放入到內核的等待隊列中,并向客戶端發送SYN+ACK確認報文。
- [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狀態。
3.6.2 三次握手(建立連接)
3.6.2.1 為什么要進行三次握手?
TCP為什么需要三次握手,為什么不是一次、兩次和四次呢?
-
TCP為什么不是一次握手?
- 如果TCP一次握手就建立連接,只需要客戶端向服務器發送SYN就能建立連接,客戶端無法得知服務器是否收到SYN,若是客戶端發送的報文丟失,就會造成雙方建立不一致的問題。
- 攻擊者可以偽造SYN,會導致SYN洪水的問題,并且服務器維護連接是需要成本的,會造成資源浪費。
-
TCP為什么不是兩次握手?
- 兩次握手不能避免舊的重復連接初始化造成混亂的問題。若客戶端發送的SYN因網絡延遲滯留,隨后重發SYN并建立新連接,而滯留的SYN稍后到達服務器,服務器會誤認為是新連接請求并響應SYN+ACK。此時客戶端已忽略舊SYN,導致服務器資源被無效占用。
- 攻擊者可以偽造SYN,會導致SYN洪水的問題,并且服務器維護連接是需要成本的,會造成資源浪費。
- 無法保證雙方的通信能力,客戶端向服務器發送SYN,證明客戶端的發送能力,服務器收到SYN,并向客戶端發送SYN+ACK,證明了服務器的接收能力和發送能力。所以兩次握手不能證明客戶端的接收能力。
- 兩次握手不能保證雙方初始序號同步
-
TCP為什么是三次握手?
- 三次握手可以避免舊的重復連接初始化造成混亂的問題。若客戶端發送的SYN因網絡延遲滯留,隨后重發SYN并建立新連接,而滯留的SYN稍后到達服務器,服務器會誤認為是新連接請求并響應SYN+ACK,并向。此時客戶端已忽略舊SYN,導致服務器資源被無效占用。
- 三次握手以最小成本驗證全雙工,保證雙方的通信能力。客戶端向服務器發送SYN,證明客戶端的發送能力,服務器收到SYN,并向客戶端發送SYN+ACK,證明了服務器的接收能力和發送能力,客戶端接收到SYN+ACK,并向客戶端發送ACK,就能證明客戶端的接收能力。
- 奇數次握手,服務器能夠減少資源的浪費,最后一個報文時客戶端發出的,客戶端發出報文認為連接建立成功,服務器則是收到了報文才認為連接建立成功。建立連接是需要消耗資源的,客戶端先認為連接建立成功,所以資源是需要客戶端先消費的,當服務器收到報文后才會消費資源,假設報文在傳輸過程中丟失了,服務器就任務連接建立失敗,就不會消耗資源。
- 三次握手可以保證雙方序號同步
-
TCP為什么不是四次握手?
- 四次握手的過程:
- 客戶端向服務器發送SYN
- 服務器收到SYN,向客戶端發送ACK
- 服務器向客戶端發送SYN
- 客戶端收到SYN,向服務器發送ACK
- 服務器收到客戶端的SYN,就必須向客戶端發送ACK和SYN,所以服務器為了提高效率和節省資源,將ACK和SYN合并為一個報文。本質上三次握手就是四次握手+捎帶應答。
- 四次握手的過程:
3.6.3 四次揮手(斷開連接)
3.6.3.1 為什么要進行四次揮手
- 為什么要進行四次揮手
- 四次揮手的過程
- 由于TCP雙方是平等的,所以斷開連接需要雙方同意
- 客戶端向服務器發送FIN,表示客戶端已經發送完數據,不會再向服務器發送數據了(此時客戶端還可以接收數據)
- 服務器收到FIN,向客戶端發送ACK,表示自己已經收到報文(此時服務器還可以向服務器發送數據)
- 服務器向客戶端發送FIN,表示服務器已經發送完數據,不會再向客戶端發送數據了(由于服務器收到FIN后,此時服務器還沒將數據發送完畢,所以通常不將ACK與FIN進行合并)
- 客戶端收到FIN,向服務器發送ACK,表示自己已經收到報文
- 四次揮手可以變為三次揮手嗎?
- 可以的,但是現實中四次揮手更常見。四次揮手可以通過延遲應答機制,將ACK和FIN合并。首先服務器收到客戶端的FIN,準備向客戶端發送ACK,此時服務器會等待一段時間,若此時服務器需要向客戶端發送FIN,就將ACK和FIN合并為一個報文發送給客戶端,否則就先將ACK先發送給客戶端。(延遲應答會在后面講解)
- 可以的,但是現實中四次揮手更常見。四次揮手可以通過延遲應答機制,將ACK和FIN合并。首先服務器收到客戶端的FIN,準備向客戶端發送ACK,此時服務器會等待一段時間,若此時服務器需要向客戶端發送FIN,就將ACK和FIN合并為一個報文發送給客戶端,否則就先將ACK先發送給客戶端。(延遲應答會在后面講解)
- 四次揮手的過程
3.6.3.2 CLOSE_WAIT 和 TIME_WAIT
CLOSE_WAIT狀態只有被動斷開連接的一方才會擁有的狀態,當被動斷開連接一方收到FIN并返回ACK后,會將狀態變為CLOSE_WAIT狀態,直到它調用close函數,并向主動斷開連接的一方發送FIN后,才會變為LAST_ACK狀態。
服務器出現大量 CLOSE_WAIT 狀態的連接原因有哪些?
- 服務器的代碼存在問題,服務器在處理完數據以后,并沒有調用關閉連接的函數(close()函數或shutdown()函數),導致服務器中存在大量CLOSE_WAIT狀態的連接。
- 服務器在處理客戶端發送的數據時,可能由于某些復雜的業務邏輯導致處理過程阻塞。在這種情況下,服務器無法及時處理完數據并發送FIN報文關閉連接,從而使連接長時間處于CLOSE_WAIT狀態。
服務器中存在大量 CLOSE_WAIT 狀態的連接,會導致服務器網絡應用越來越卡。
TIME_WAIT是只有主動斷開連接的一方才會擁有的狀態。
進入TIME_WAIT狀態的連接,此時它綁定的IP和端口號并未被徹底釋放,通常此時重啟服務器綁定同樣的IP和端口號就會報錯。
下圖我關閉服務器后,立刻重啟服務器并綁定相同IP和端口號,此時服務器并未啟動,錯誤原因為IP和端口號已經被其他進程綁定了。
解決這個問題的方法就是使用setsockopt函數,設置套接字選項以允許地址重用。
TIME_WAIT的時間是2MSL,MSL是報文的最長存活時間。TIME_WAIT為兩倍MSL,能夠保證兩個方向上未被接收到的報文和遲到的報文已經消失。
同時也保證了最后一個報文的可靠到達,假設最后一個ACK丟失了,服務器會重發FIN,此時客戶端已經不在了,但是操作系統以及維護著TCP連接,依舊可以重復ACK。
TIME_WAIT存在的意義:
- 防止網絡中歷史報文對新連接的影響
- 報文三次握手時,初始的序號是隨機的,所以并不是判斷報文是新的還是舊的,如果是沒有TIME_WAIT,舊報文會對新連接有影響。
- 報文的最長存活時間MSL,TIME_WAIT能夠等待歷史報文在網絡中消散,就沒有舊報文影響。
- TIME_WAIT能夠保證最后一個報文的可靠到達,當最后一個報文丟失后
- 沒有TIME_WAIT的情況,客戶端(主動斷開連接一方)發送ACK后就進入CLOSE狀態,由于服務器(被動斷開連接一方)沒有收到ACK,就會重發FIN,由于客戶端連接已經關閉,則會發送RST給服務器,這樣服務器就會將連接強制關閉,強制關閉連接并不是很好。
- 有TIME_WAIT的情況,客戶端(主動斷開連接一方)發送ACK后就進入CLOSE狀態,由于服務器(被動斷開連接一方)沒有收到ACK,就會重發FIN,由于客戶端的連接并沒有關閉,所以收到FIN以后會重置TIME_WAIT時間,并向服務器發送ACK,這樣能夠保證服務器正常關閉連接。
3.7 滑動窗口
滑動窗口就是TC并發多個數據暫時不需要ACK的解決方案,從而提高TCP的效率,滑動窗口還是流量控制的解決方案。
在確認應答部分我就講到過,對于每一個報文,都要收到它對應的ACK,假設發送一個報文,就要等待收到ACK再發送下一個報文,這樣TCP的效率就比較低了。
那么一次發送多個報文,就可以大大提高效率(多個報文的等待時間重疊了)。
3.7.1 滑動窗口在哪里?
滑動窗口就是發送緩沖區中的一個區域。
3.7.2 如何理解滑動窗口?
發送緩沖區我們可以理解為一個數組,滑動窗口則可以理解為兩個指針,兩個指針指向的區間就是滑動窗口,滑動窗口向右滑動就是指針向右移動。
需要注意的是,滑動窗口只能向右滑動,不能向左滑動。
這時候就有人問了,按照數組形式理解,滑動窗口越界了怎么辦?
操作系統中對其有復雜的處理方式,大家可以將滑動窗口為環形結構即可。
- 滑動窗口的大小是由誰決定呢?
- 滑動窗口的大小是由接收方可用的接收緩沖區的大小決定的。
- 滑動窗口的大小還與擁塞窗口有關。
- 滑動窗口的大小就是接收方窗口大小與擁塞窗口中小的一個。
- 滑動窗口是如何更新的呢?
- 滑動窗口是根據確認序號和接收方窗口大小決定的
- win_start = 確認序號,win_end = win_start + win(窗口大小)
3.7.3 滑動窗口的大小變化嗎?
滑動窗口的大小是可以變化的,滑動窗口的大小是根據對方可用的接收緩沖區大小來決定的,滑動窗口既可以變大,也可以變小,甚至可以為0。
當主機A將數據發送給主機B后,假設主機B的上層并未將接收緩沖區中讀走,此時可以緩沖區就會變小,主機A的滑動窗口也會變小。主機A多次向發送數據,假設主機B上層一直沒有讀走數據,主機B的可以緩沖區就為0,主機A的滑動窗口也會變為0。
當主機A再次將數據發送給主機B后,假設主機B的上層一次將接收緩沖區中的數據全部讀走,此時可以緩沖區就會變大,對應主機A的滑動窗口也會變大。
3.7.4 報文/響應報文丟失了怎么辦?(快重傳)
-
最左側的報文/響應報文丟失了
- 當最左側的報文丟失時,其他的報文被主機B接收到,主機B會對所有的報文進行響應,但由于主機B并未收到數據1001~2000,所以所有相應的報文的確認序號都為1001,當主機A收到3個及以上的響應報文的確認序號相同時,就會立即對該數據進行重發,這就是快重傳。如果主機A沒有收到3個及以上的響應報文的確認序號相同時,主機A則會等到超時時間,將對應數據進行超時重傳。
- 當最左側的報文對應的相應報文丟失了,但由于其他的響應報文并未丟失,確認序號的定義是確認序號之前的數據都收到了,所以并未有影響。
-
中間的報文丟失了
- 中間的報文丟失了,由于前面的報文已經被主機B接收到了,那說明滑動窗口就可以向右滑動,將已經被接收到的數據移除,則中間報文就變成了最左側報文,中間報文丟失的問題就轉變為了最左側報文丟失的問題了。
- 中間報文對應的響應報文丟失,會轉化為最左側報文對應的響應報文丟失的問題。
-
最右側報文/響應報文丟失了
- 最右側報文丟失了,由于前面的報文已經被主機B接收到了,那說明滑動窗口就可以向右滑動,將已經被接收到的數據移除,則中間報文就變成了最左側報文,最右側報文丟失的問題就轉變為了最左側報文丟失的問題了。
- 最右側報文對應的響應報文丟失,會被轉化為最左側報文對應的響應報文丟失的問題。
3.8 擁塞控制
上面講述的機制都是與雙方主機相關的,但是主機A向主機B發送數據需要通過網絡,所以TCP還考慮了網絡的狀態,當網絡中少量報文丟失,發送方就會將報文進行重傳,但網絡中大量的報文丟失了,發送方則會判斷出是網絡出現了問題 。
TCP引入 慢啟動 機制,先發少量的數據,探探路,摸清當前的網絡擁堵狀態,再決定按照多大的速度傳輸數據。少量數據探路,有響應后,說明網絡狀態已經好了一些,就需要盡快的恢復網絡通信。
此處引入一個概念為擁塞窗口
- 發送開始的時候,定義擁塞窗口大小為1
- 每次收到一個ACK應答,擁塞窗口加1
- 也就是先發送一個報文,收到應答后,下次就可以發送兩個。發送兩個報文,兩個報文都收到了應答,則下次可以發送四個,以此類推
- 每次發送報文的時候,將擁塞窗口和接收端窗口大小做比較,取較小的值作為滑動窗口的大小
像上面這樣的擁塞窗口增長速度,是指數級別的。“慢啟動” 只是指初使時慢,但是增長速度非常快。
- 為了不增長的那么快,因此不能使擁塞窗口單純的加倍
- 此處引入一個叫做慢啟動的閾值
- 當擁塞窗口超過這個閾值的時候,不再按照指數方式增長,而是按照線性方式增長
- 當TCP開始啟動的時候,以慢啟動的方式增加擁塞窗口,慢啟動閾值等于窗口最大值
- 當擁塞窗口達到慢啟動的閾值后,以線性方式增加擁塞窗口
- 在每次超時重發的時候,慢啟動閾值會變成原來的一半,同時擁塞窗口置回1
擁塞窗口的過程:
- 慢啟動
- 發送方以指數方式逐漸增加擁塞窗口的大小。
- 擁塞避免
- 當擁塞窗口超過閾值后,發送方以線性方式緩慢增加擁塞窗口,避免網絡過載。
- 快重傳
- 當發送方收到3個重復的ACK時,立即重傳丟失的數據包,而不必等待超時。
- cwnd = cwnd /2,ssthresh = cwnd
- 快恢復
- 在快重傳后,發送方不進入慢啟動階段,而是進入擁塞避免階段,調整擁塞窗口。主機還能收到 3 個重復 ACK 說明網絡也不那么糟糕,所以沒有必要像超時重傳那么強烈。
- cwnd = ssthresh + 3
- 如果再收到重復的 ACK,那么 cwnd 增加 1
- 如果收到新的的 ACK,把 cwnd 設置為第一步中的 ssthresh 的值
- 超時重傳
- 發送方的重傳定時器超時,說明可能發生了嚴重的網絡擁塞
- ssthresh = cwnd / 2
- cwnd = 1
3.9 面相字節流
創建一個TCP的socket,同時在內核中創建一個 發送緩沖區 和一個 接收緩沖區
- 調用write時,數據會先寫入發送緩沖區中
- 如果發送的字節數太長,會被拆分成多個TCP的數據包發出
- 如果發送的字節數太短,就會先在緩沖區里等待,等到緩沖區長度差不多了,或者其他合適的時機發送出去
- 接收數據的時候,數據也是從網卡驅動程序到達內核的接收緩沖區
- 然后應用程序可以調用read從接收緩沖區拿數據
- 另一方面,TCP的一個連接,既有發送緩沖區,也有接收緩沖區,那么對于這一個連接,既可以讀數據,也可以寫數據,這個概念叫做 全雙工
由于緩沖區的存在,TCP程序的讀和寫不需要一一匹配,例如:
- 寫100個字節數據時,可以調用一次write寫100個字節,也可以調用100次write,每次寫一個字節
- 讀100個字節數據時,也完全不需要考慮寫的時候是怎么寫的,既可以一次read 100個字節,也可以一次read一個字節,重復100次
如何理解面向字節流呢?
-
發送緩沖區
- 發送方實際發送的數據是 “滑動窗口” 內的字節,已發送但未確認的字節會保留在緩沖區中,當收到接收方的 ACK 就會通過將滑動窗口向右滑動,將已被接收的數據移除,看上去發送緩沖區中的數據就像是“流動的”。
-
接收緩沖區
- 發送方的發送的數據會按照順序保存在接收緩沖區,而接收方的上層可以按順序從接收緩沖區中讀取數據,將數據讀走后,接收緩沖區中的數據看起來就是“流動的”。
3.10 粘包問題
粘包問題中的 “包”,是指的應用層的數據包
- 在TCP的協議頭中,沒有如同UDP一樣的 “報文長度” 這樣的字段,但是有一個序號這樣的字段
- 站在傳輸層的角度,TCP是一個一個報文過來的,按照序號排好序放在緩沖區中
- 站在應用層的角度,看到的只是一串連續的字節數據
- 那么應用程序看到了這么一連串的字節數據,就不知道從哪個部分開始到哪個部分,是一個完整的應用層數據包
那么如何避免粘包問題呢?歸根結底就是一句話,明確兩個包之間的邊界
- 對于定長的包,保證每次都按固定大小讀取即可。例如上面的Request結構,是固定大小的,那么就從緩沖區從頭開始按sizeof(Request)依次讀取即可
- 對于變長的包,可以在包頭的位置,約定一個包總長度的字段,從而就知道了包的結束位置
- 對于變長的包,還可以在包和包之間使用明確的分隔符
思考:對于UDP協議來說,是否也存在 “粘包問題” 呢?
- 對于UDP,如果還沒有上層交付數據,UDP的報文長度仍然在。同時,UDP是一個一個把數據交付給應用層,就有很明確的數據邊界
- 站在應用層的站在應用層的角度,使用UDP的時候,要么收到完整的UDP報文,要么不收,不會出現"半個"的情況
3.11 TCP異常情況
- 進程終止
- 進程終止會釋放文件描述符,仍然可以發送FIN,和正常關閉沒有什么區別
- 機器重啟
- 機器重啟時,操作系統會詢問是否終止進程,所以和進程終止的情況相同
- 機器掉電(無法發送FIN)
- 發送方機器掉電
- 發送方所有未發送的數據和連接狀態丟失,無法繼續傳輸
- 接收方長時間未收到數據或 FIN,可能默認關閉連接
- 接收方機器掉電
- 發送方會啟動重傳定時器。若在定時器超時后仍未收到數據,發送方會持續重傳確認報文,在多次重傳無果后,發送方會認為連接出現問題,最終會選擇關閉連接。
- 接收方所有數據和連接狀態丟失,無法繼續傳輸
- 發送方機器掉電
3.12 半連接隊列和全連接隊列(listen 的第二個參數)
- 半鏈接隊列(用來保存處于SYN_SENT和SYN_RECV狀態的請求)
- 全連接隊列(用來保存處于 ESTABLISHED 狀態,但是應用層沒有調用accept取走的請求)
半連接隊列工作流程
- 客戶端向服務器發送 SYN 包,請求建立連接。
- 服務器收到 SYN 包后,為這個連接創建一個新的條目,并將其放入半連接隊列中。
- 服務器向客戶端發送 SYN + ACK 包,等待客戶端的 ACK 確認。
- 如果在一定時間內沒有收到客戶端的 ACK 確認,服務器會重發 SYN + ACK 包,達到一定重傳次數后仍未收到 ACK,則會從半連接隊列中移除該條目。
全連接隊列工作流程
- 客戶端收到服務器的 SYN + ACK 包后,向服務器發送 ACK 確認包。
- 服務器收到 ACK 確認包后,將連接信息從半連接隊列移除,并加入到全連接隊列中。
- 服務器進程調用 accept() 函數從全連接隊列中取出一個連接進行處理。
- 如果全連接隊列已滿,新的連接將被暫時拒絕,客戶端可能會收到連接超時的錯誤,服務器也可能會采取一些策略(如丟棄連接請求等)來處理這種情況。
全連接隊列的容量 = listen 的第二個參數(backlog)+ 1。
3.13 文件、Socket、系統和網絡之間的關系
進程在啟動后,操作系統會為進程創建對應的PCB和文件描述符表,當進程創建套接字后,會返回一個文件描述符并保存在文件描述符表中。
數據通過網絡后,最先到達網卡,網卡會向CPU對應的針腳發送中斷信號,CPU中的寄存器會將其轉化為中斷號,操作系統可以通過中斷號找到對應中斷向量表中的方法,操作系統可以根據對應的方法,將網卡中的數據讀取到傳輸層的接收緩沖區中,再通過文件的操作方法表中的方法將接收緩沖區中的數據讀取到應用層。
每一個文件描述符指向一個 file 結構體, file 結構體中的一個字段指向 socket 結構體。socket 結構體中的一個字段指向 file 結構體,還有一個字段指向相關套接字函數。socket 結構體中有一個 struct sock* 字段,指向 sock 結構體(實際上指向的是 udp_sock 結構體或 tcp_sock 結構體)。
- tcp_sock 嵌套了 inet_connection_sock,inet_connection_sock 嵌套了 inet_sock,inet_sock 又嵌套了 sock。所以通過 tcp_sock 可以依次訪問到其內部嵌套的各層結構體
- 同理,udp_sock 嵌套了 inet_sock,inet_sock 嵌套了 sock,通過 udp_sock 可以訪問 sock 和 inet_sock
struct sock * 字段,既可以指向的是 udp_sock 結構體也可以指向 tcp_sock 結構體。將 struct sock * 進行強制類型轉換,就可以分別訪問 tcp_sock 和 udp_sock。所以Linux內核中,使用指針操作和結構體嵌套實現了多態,這樣就使用一種方式就實現了TCP和UDP。
四、UDP與TCP的對比
4.1 UDP與TCP的特點對比
可靠傳輸/不可靠傳輸不是優缺點,而是協議的特點。
UDP特點:
- 面向數據報:將每一個獨立的報文作為一個整體發送,保留報文邊界
- 不建立連接:知道對方的IP和端口號就可以直接傳輸,不需要建立鏈接
- 不可靠傳輸:沒有確認機制,沒有重傳機制,數據傳輸發生錯誤的時候,沒有如何反饋
TCP特點:
- 面向字節流:將數據設置為連續的字節流,不保留報文邊界
- 建立連接:在傳輸數據之前需要建立連接,連接過程我們稱之為三次握手
- 可靠傳輸:通過確認應對、超時重傳、擁塞控制、數據校驗和、流量控制等機制保證了數據的可靠傳輸
4.2 用UDP實現可靠傳輸
參考TCP的可靠性機制,在應用層實現類似的邏輯
- 引入序號,保證數據按序到達
- 引入確認應對機制,確認數據是否對方收到
- 引入超時重傳機制,無法保證對方收到數據,則將數據進行重傳,盡可能的保證數據被對方收到
- 引入滑動窗口與流量控制,使用滑動窗口機制控制數據傳輸速率,避免接收方緩存溢出,導致數據丟失
- 引入擁塞控制,通過擁塞控制避免網絡擁塞。
結尾
如果有什么建議和疑問,或是有什么錯誤,大家可以在評論區中提出。
希望大家以后也能和我一起進步!!🌹🌹
如果這篇文章對你有用的話,希望大家給一個三連支持一下!!🌹🌹