??作者:一只大喵咪1201
??專欄:《網(wǎng)絡(luò)》
??格言:你只管努力,剩下的交給時間!
現(xiàn)在是傳輸層,在應(yīng)用層中的報文(報頭 + 有效載荷)就不能被叫做報文了,而是叫做數(shù)據(jù)段(報頭 + 有效載荷),傳輸層的有效載荷就是應(yīng)用層的完整報文。
??再談端口號
- 端口號(port):標(biāo)識了一個主機上進行通信的不同的應(yīng)用程序。
如上圖所示,F(xiàn)TP,SSH,SMTP,HTTP,F(xiàn)TP等類型的服務(wù)器,其實就是在一臺機器上運行著的不同進程,這些是他們的名字。
每一個進程都有一個端口號,如上圖中的,TCP21,TCP22等等,TCP后面的數(shù)字就是端口號,當(dāng)數(shù)據(jù)從網(wǎng)絡(luò)中傳輸?shù)綉?yīng)用層后,根據(jù)端口號將數(shù)據(jù)交給對應(yīng)的進程。
- 在TCP/IP協(xié)議中, 用 “源IP”, “源端口號”, “目的IP”, “目的端口號”, “協(xié)議號” 這樣一個五元組來標(biāo)識一個通信。
如上圖網(wǎng)絡(luò)通信中的數(shù)據(jù)1和2,是客戶端A的兩個瀏覽器頁面(兩個進程)向服務(wù)器發(fā)送的數(shù)據(jù),所以TCP/IP協(xié)議中,源IP地址都是客戶端A的IP地址,目標(biāo)IP地址都是服務(wù)器的IP地址,源端口號分別是2001和2002,表示兩個頁面的兩個進程,目標(biāo)端口號也都是服務(wù)端的HTTP進程,端口號是80。
數(shù)據(jù)3是客戶端B的一個瀏覽器頁面(一個進程)向服務(wù)器發(fā)送的數(shù)據(jù),源IP地址就是客戶端B的IP地址,目標(biāo)IP是服務(wù)端的IP地址,源端口號是這個頁面進程的端口號,目標(biāo)端口號是服務(wù)器HTTP進程的端口號80。
- 協(xié)議號:這些通信協(xié)議的編號,通過指定的協(xié)議號可以指定使用哪個協(xié)議。
IP地址在后面本喵講解網(wǎng)絡(luò)層中的IP協(xié)議時再詳談。
??端口號劃分
在前面編寫套接字代碼的時候,本喵代碼中指定的端口號是一個uint16_t
類型的數(shù)據(jù),是一個16位的無符號整數(shù),它的范圍是0~65535
,也就意味著端口號的范圍是0~65535
。
知名端口號:
- 端口號為
0~1023
的是知名端口號,如HTTP,F(xiàn)TP,SSH等這些廣為使用的應(yīng)用層協(xié)議,他們的端口都是固定的。
應(yīng)用層協(xié)議本質(zhì)上就是進程,協(xié)議規(guī)定是通過進程的執(zhí)行來體現(xiàn)的,所以應(yīng)用層協(xié)議 = 進程 = 端口號
,他們可以看成是等價的。
就好比,110對應(yīng)的是報警電話,120對應(yīng)的是急救電話,119是火警電話…。
同樣的:
- ssh服務(wù), 使用22端口
- ftp服務(wù), 使用21端口
- telnet服務(wù), 使用23端口
- http服務(wù), 使用80端口
- https服務(wù), 使用443
- …
0~1023
這些端口號都有對應(yīng)的協(xié)議,不能隨便更改,就像報警電話不會隨便更改一樣。
在文件/etc/services
中存設(shè)定了具體端口號對應(yīng)的服務(wù)協(xié)議,如上圖紅色框中所示,http的端口號是80。
我們自己寫程序的時候,如果使用到端口號,要避開0~1023
這些知名端口號。
操作系統(tǒng)動態(tài)分配的端口號:
1024~65535
這些端口號可以由我們指定去綁定某個進程,也可以讓操作系統(tǒng)從這些端口號中為某個進程隨機分配一個。
在前面寫套接字代碼的時候,本喵服務(wù)器上的進程經(jīng)常都是綁定的端口號是8080,需要使用bind
系統(tǒng)調(diào)用去綁定。
客戶端的套接字中,雖然也需要為進程綁定端口號,但是本喵并沒有顯式綁定(調(diào)用bind
綁定),而是在調(diào)用send
時,讓操作系統(tǒng)自己從1024~65535
中隨機指定一個端口號綁定。
端口號與進程之間的關(guān)系:
如上圖所示,從端口號到進程必須是唯一的,也就是說根據(jù)一個端口號只能找到一個進程。
并且一個進程可以有多個端口號,如上圖所示的8080,80801…,這些端口號都表示一個進程。
- 但是一個端口號不能綁定多個進程。
netstat:
netstat
指令前面本喵用過好幾次,就是用來查看當(dāng)前機器上網(wǎng)絡(luò)狀態(tài)的。
- 語法
netstat [選項]
- 功能:查看網(wǎng)絡(luò)狀態(tài)
- 常用選項:
- n 拒絕顯示別名,能顯示數(shù)字的全部轉(zhuǎn)化成數(shù)字
- l 僅列出有在 Listen (監(jiān)聽) 的服務(wù)狀態(tài)
- p 顯示建立相關(guān)鏈接的程序名
- t (tcp)僅顯示tcp相關(guān)選項
- u (udp)僅顯示udp相關(guān)選項
- a (all)顯示所有選項,默認(rèn)不顯示LISTEN相關(guān)
這里本喵就不演示了,大家自行組合使用即可。
還有一個指令pidof [進程名]
使用起來非常的方便,可以直接得到進程名對應(yīng)的pid
值,不用再使用ps ajx
來查了。
??UDP
??協(xié)議格式
如上圖所示就是UDP協(xié)議的格式,前八個字節(jié)屬于協(xié)議報頭部分,之后的才是有效載荷。
UDP協(xié)議其實就是前八個字節(jié),也就是兩個int
類型的數(shù)據(jù),其中第一個int
類型數(shù)據(jù)的前16位0~15
存放的是源端口號,后16位16~31
是目的端口號,第二個int
類型數(shù)據(jù)的前16位0~15
存放的是UDP長度,后16位16~31
存放的是UDP校驗和。
UDP協(xié)議是傳輸層協(xié)議,是由操作系統(tǒng)維護的,而Linux操作系統(tǒng)又是由C語言寫的,所以UDP協(xié)議報頭的結(jié)構(gòu)化數(shù)據(jù)偽代碼是:
struct udp_hdr
{
unsigned int src_port:16; //16位源端口號
unsigned int dest_port:16; //16位目的端口號
unsigned int length:16; //16位UDP長度
unsigned int check:16; //16位UDP校驗和
};
UDP協(xié)議報頭本質(zhì)上就是一個結(jié)構(gòu)體,如上面代碼所示,使用位段將兩個unsigned int
類型的數(shù)據(jù)分成四部分。
-
16位源端口號:表示發(fā)送端(客戶端)的端口號。
-
16位目的端口號:表示接收端(服務(wù)端)的端口號。
-
16位UDP長度:整個UDP數(shù)據(jù)段(報文)的長度,包括報頭和有效載荷,UDP數(shù)據(jù)段最大就是216 = 64KB。
-
16位校驗和:由操作系統(tǒng)進行校驗,如果發(fā)送方和接收方的校驗和不一致,說明出錯,就會丟棄整個UDP數(shù)據(jù)段。
如上圖,將用戶層的數(shù)據(jù)(字符串)使用sendto
發(fā)送到UDP套接字中時,其本質(zhì)上就是將用戶層buffer
中的字符串復(fù)制到UDP套接字的內(nèi)核緩沖區(qū)中。
- hdr指針指向內(nèi)核緩沖區(qū)最起始位置(UDP報頭),start指針指向內(nèi)核中有效載荷的起始位置。
上面過程的偽代碼:
char* hdr = malloc(XXX);//操作系統(tǒng)創(chuàng)建內(nèi)核緩沖區(qū)
char* start = hdr + sizeof(struct udr_hdr);//指向有效載荷
strcpy(start,buffer,len);//將用戶緩沖區(qū)中數(shù)據(jù)復(fù)制到內(nèi)核緩沖區(qū)有效載荷處。
(struct udp_hdr*)hdr->src_port = xxx;//賦值源端口
(struct udp_hdr*)hdr->dest_port = xxx;//賦值源端口
(struct udp_hdr*)hdr->length = xxx;//賦值源端口
(struct udp_hdr*)hdr->check = xxx;//賦值源端口
操作系統(tǒng)會在內(nèi)核中使用malloc
創(chuàng)建一個內(nèi)核緩沖區(qū),這個緩沖區(qū)最大是64KB,前8個字節(jié)屬于UDP的報頭,也就是struct udp_hdr
類型對象,再偏移報頭字節(jié)數(shù)(sizeof(udp_hdr)
),用start
指向有效載荷存儲的起始位置。
再將內(nèi)核緩沖區(qū)中的字符串使用strcpy
復(fù)制到內(nèi)核中存放有效載荷的位置,再將內(nèi)核緩沖區(qū)強轉(zhuǎn)成struct udp_hdr
類型,將報頭的4部分填充,最后將整個緩沖區(qū)的數(shù)據(jù)發(fā)送到對端。
??解包和分用
UDP的解包和分用比較簡單,對端收到UDP數(shù)據(jù)段以后,直接將有效載荷拿到,并且放入到接收緩沖區(qū)中,只需要從整個數(shù)據(jù)段的頭部偏移sizeof(struct udp_hdr)
個字節(jié)即可拿到有效載荷。
分用時,將綁定的端口號作為key
值,在操作系統(tǒng)維護的網(wǎng)絡(luò)通信進程哈希表中查找到對應(yīng)的進程,將有效載荷交給進程即可。
??特點
- 無連接:只需要指定目的IP和目的端口就可以直接進行傳輸,不需要建立連接。
- 不可靠:沒有確認(rèn)機制,沒有重傳機制,如果因為網(wǎng)絡(luò)故障該數(shù)據(jù)段無法發(fā)到對方,UDP協(xié)議層也不會給應(yīng)用層返回任何錯誤信息。
- 面向數(shù)據(jù)報:不能夠靈活控制讀寫數(shù)據(jù)的次數(shù)和數(shù)量。
UDP發(fā)送數(shù)據(jù)的方式就像寄信一樣,只管發(fā)出去,至于對方能不能收到完全不關(guān)心。
面向數(shù)據(jù)報:
應(yīng)用層交給UDP多長的報文,UDP原樣發(fā)送,既不拆分,也不合并。
- UDP發(fā)送數(shù)據(jù)段時,是一次性將內(nèi)核緩沖區(qū)中的數(shù)據(jù)全部發(fā)送出去。
- 接收端會一次性接收發(fā)送端發(fā)送的所有數(shù)據(jù),不能分多次接收。
就像快遞,你發(fā)快遞只能一個一個發(fā),不能先發(fā)半個,然后再發(fā)半個,收快遞也是,不能先收半個,然后再收半個,只能一個個完整的快遞進行收發(fā)。
UDP緩沖區(qū):
如上圖所示,UDP協(xié)議沒有真正意義上的發(fā)送緩沖區(qū),當(dāng)客戶端的用戶層將數(shù)據(jù)使用sendto
發(fā)送的時候,操作系統(tǒng)將數(shù)據(jù)拷貝到上面?zhèn)未a中的那個數(shù)組中,然后將整個數(shù)組的數(shù)據(jù)全部發(fā)送。
但是UDP協(xié)議存在接收緩沖區(qū),服務(wù)端的操作系統(tǒng)將客戶端發(fā)送來的數(shù)據(jù)存放在接收緩沖區(qū)中,如上圖綠色框所示,當(dāng)接收緩沖區(qū)滿了,或者在適當(dāng)?shù)臅r候,操作系統(tǒng)會將數(shù)據(jù)再交給用戶層。
- 因為沒有發(fā)送緩沖區(qū),所以UDP協(xié)議發(fā)送數(shù)據(jù)時,用戶層一調(diào)用
sendto
數(shù)據(jù)就被發(fā)送出去了,沒有停留。
根據(jù)上圖,雖然沒有發(fā)送緩沖區(qū),但是雙方都有接收緩沖區(qū),客戶端發(fā)送數(shù)據(jù),服務(wù)端接收數(shù)據(jù)的同時,并不妨礙服務(wù)端也發(fā)送數(shù)據(jù),客戶端接收數(shù)據(jù)。
- 這樣的雙方同時發(fā)送并且接收數(shù)據(jù)的模式叫做全雙工。
注意事項:
前面本喵說過,UDP數(shù)據(jù)段最長不能超過64KB,所以如果傳輸?shù)臄?shù)據(jù)超過了64KB,就需要在應(yīng)用層手動的分包,將數(shù)據(jù)分成多個64KB大小的數(shù)據(jù)包,然后多次發(fā)送,并且在接收端手動拼裝。
常見的基于UDP的應(yīng)用層協(xié)議:
- NFS: 網(wǎng)絡(luò)文件系統(tǒng)
- TFTP: 簡單文件傳輸協(xié)議
- DHCP: 動態(tài)主機配置協(xié)議
- BOOTP: 啟動協(xié)議(用于無盤設(shè)備啟動)
- DNS: 域名解析協(xié)議
這些協(xié)議在傳輸層都是使用的UDP協(xié)議。當(dāng)然也包括我們自己寫UDP程序時自定義的應(yīng)用層協(xié)議。
??TCP
- TCP全稱為 “傳輸控制協(xié)議(Transmission Control Protocol”).。
- 人如其名, 要對數(shù)據(jù)的傳輸進行一個詳細(xì)的控制。
??協(xié)議格式
如上圖所示,TCP協(xié)議報頭的長度固定20個字節(jié),不包括選項和有效載荷,本喵也不會講解選項部分,有興趣的小伙伴可以自行了解。
TCP協(xié)議報頭同樣也是一個結(jié)構(gòu)化數(shù)據(jù),和UDP一樣,只是結(jié)構(gòu)體中的成員變量大小和類型不同而已。
- 16位源端口號:發(fā)送方進程的端口號。
- 16位目的端口號:接收方進程的端口號。
- 32位序號/32位確認(rèn)號: 后面詳細(xì)講。
- 4位TCP報頭長度: 表示該TCP頭部有多少個32位bit(有多少個4字節(jié)),也就是一個
unsigned int
類型,所以TCP頭部長度是TCPval * 4 。最大頭部長度是15 * 4 = 60字節(jié)。
- TCP中的4位首部長度包含報頭和選項,報頭部分固定20字節(jié)。
- 頭部長度 - 20 = 選項長度。
- 6位標(biāo)志位:標(biāo)識UDP數(shù)據(jù)段的類型。
- URG:緊急指針是否有效。
- ACK:確認(rèn)號是否有效。
- PSH:提示接收端應(yīng)用程序立即從TCP緩沖區(qū)把數(shù)據(jù)拿走。
- RST:對端要求重新建立連接,所以把攜帶RST標(biāo)識的數(shù)據(jù)段稱為復(fù)位報文段。
- SYN:請求建立連接,所以把攜帶SYN標(biāo)識的數(shù)據(jù)段稱為同步報文段。
- FIN:通知對端,本段關(guān)閉了,稱攜帶FIN標(biāo)識的為結(jié)束報文段。
- 16位窗口大?。罕硎净瑒哟翱诘拇笮?,后面詳細(xì)講解。
- 16位校驗和:發(fā)送端填充CRC校驗. 接收端校驗不通過, 則認(rèn)為數(shù)據(jù)有問題,此處的檢驗和不僅包含TCP首部,也包含TCP數(shù)據(jù)部分。
- 16位緊急指針:標(biāo)識哪部分?jǐn)?shù)據(jù)是緊急數(shù)據(jù)(帶外數(shù)據(jù))。
這些報頭中的內(nèi)容,本質(zhì)上也是一個結(jié)構(gòu)體的成員變量,偽代碼:
struct tcp_hdr
{
uint32_t src_port:16;
uint32_t dest_port:16;
uint32_t seq;
uint32_t ack_seq;
uint32_t header_length:4;
......
};
??解包和分用
緩沖區(qū):
如上圖所示,TCP協(xié)議中,通信雙方都既有發(fā)送緩沖區(qū),又有接收緩沖區(qū),用戶層在send
數(shù)據(jù)之后,操作系統(tǒng)會自動拼接TCP報頭形成數(shù)據(jù)段,然后放入到發(fā)送緩沖區(qū)中。
對端收到數(shù)據(jù)段后先放入接收緩沖區(qū)中,等待用戶層讀取數(shù)據(jù),通信雙方既可以發(fā)送數(shù)據(jù),也可以接收數(shù)據(jù),而且互不影響,同樣是全雙工的方式。
- 至于什么時候?qū)?shù)據(jù)段從發(fā)送緩沖區(qū)發(fā)出去,什么時候?qū)?shù)據(jù)從接收緩沖區(qū)交給應(yīng)用層。
- 一次從緩沖區(qū)發(fā)送多少個字節(jié)的數(shù)據(jù),一次又接受多少數(shù)據(jù)。
- 這些全部由操作系統(tǒng)自行控制,也就是由TCP協(xié)議自行決定。
所以說,TCP是傳輸控制協(xié)議,它完全有自主決定權(quán),而且是面向字節(jié)流的,發(fā)送數(shù)據(jù)接收數(shù)據(jù)以字節(jié)為單位,在發(fā)送接收過程中,完全不考慮是不是一個完整的數(shù)據(jù)段。
- TCP不像UDP那樣,一次必須發(fā)送或者接收一個完整的數(shù)據(jù)段,而是以字節(jié)為單位的,發(fā)送或接收一個完整的數(shù)據(jù)段,需要很多個字節(jié)。
- 這些流動的字節(jié)像河流一樣,所以說TCP是面向字節(jié)流的。
在對端收到TCP數(shù)據(jù)段以后,根據(jù)數(shù)據(jù)段中的TCP首部長度,得到整個報頭長度,偏移sizeof(struct tcp_hdr)
后,得到有效載荷的首地址,直到下一個TCP數(shù)據(jù)段的起始全部都是有效載荷,將這部有效載荷提取到接收緩沖區(qū)中就完成了解包。
那么又是怎么完成分用的呢?也就是說該如何找到曾經(jīng)bind
端口號的特定進程呢?
網(wǎng)絡(luò)協(xié)議和協(xié)議棧:
如上圖所示,應(yīng)用層協(xié)議本質(zhì)上就是一個一個的進程,每一個進程都有pid
,網(wǎng)絡(luò)協(xié)議還有一個port
,操作系統(tǒng)將網(wǎng)絡(luò)通信進程的PCB用一個哈希表來維護。
- 以【port :pid】鍵值對的形式存放在哈希表中,其中
port
是key值,pid
是value。
在操作系統(tǒng)完成數(shù)據(jù)段的解包以后,會從這個哈希表中,根據(jù)TCP協(xié)議中的目的port
查找對應(yīng)pid
值的PCB,這個PCB維護的文件描述符表中有一個fd
指向的是一個套接字,如上圖所示的3。
- 套接字本質(zhì)上也是一個文件,所以也存在一個
struct file
結(jié)構(gòu)體,該結(jié)構(gòu)體中的讀寫緩沖區(qū)其實就是TCP協(xié)議的接收和發(fā)送緩沖區(qū)。
然后將TCP層解包后的有效載荷放入找到的struct file
的接收緩沖區(qū)中,如上圖帶箭頭紅色線條所示。此時就完成了分用,應(yīng)用層像讀取文件內(nèi)容一樣從fd = 3
的文件中讀取有效載荷(報文)即可。
發(fā)送的過程只是反過來而已,本喵不再詳細(xì)講解。
??可靠性
確認(rèn)應(yīng)答(ACK)機制
- TCP將每個字節(jié)的數(shù)據(jù)都進行了編號,即為序列號。
TCP數(shù)據(jù)段char* udp[N]
本質(zhì)上就是一個char*
類型的數(shù)組,每個元素大小是一字節(jié),TCP數(shù)據(jù)段就放在這個數(shù)組中,包括報頭,選項以及有效載荷,所以數(shù)據(jù)段中的每個數(shù)據(jù)都有一個編號,就是數(shù)組的下標(biāo)。
如上圖所示,主機A和主機B使用TCP協(xié)議進行通信,主機A發(fā)送數(shù)據(jù)(1~1000),主機B收到后回復(fù)一個應(yīng)答信號(AKC),其值是1001。
上面通信過程中,發(fā)送的數(shù)據(jù)編號范圍是1~1000,所以主機A在發(fā)送數(shù)據(jù)的時候,將數(shù)字1000填入到報頭中的32位序號中。
主機B在收到數(shù)據(jù)段以后,將1001作為確認(rèn)序號填入到報頭中的32位確認(rèn)序號中,然后發(fā)送給主機A,此時發(fā)送的數(shù)據(jù)段中沒有有效載荷,只有報頭。
- 確認(rèn)應(yīng)答信號中只有TCP報頭部分,沒有有效載荷。
- 確認(rèn)序號中的1001,表示主機B已經(jīng)收到編號1001之前的所有數(shù)據(jù)。
- 主機A接收到確認(rèn)應(yīng)答后,下次發(fā)送數(shù)據(jù)就從編號位1001處開始。
主機A收到確認(rèn)應(yīng)答信號后,就可以保證剛剛發(fā)送的數(shù)據(jù)主機B收到了,并且下次發(fā)送從確認(rèn)序號處開始發(fā)送即可。
ACK標(biāo)志位:
那么主機A在收到主機B的應(yīng)答信號后,如何確定這就是一個確認(rèn)應(yīng)答信號呢?它有沒有可能是一個正常的數(shù)據(jù)段,但是并沒有有效載荷呢?
如上圖所示,主機B在給主機A發(fā)送應(yīng)答信號之前,除了會填充32位的確認(rèn)序號外,還會將6個標(biāo)志位的AKC
置一。
當(dāng)主機A收到確認(rèn)應(yīng)答信號后,發(fā)現(xiàn)ACK
的狀態(tài)是1,就知道這是一個確認(rèn)應(yīng)答信號,而不是一個普通的數(shù)據(jù)段,然后再區(qū)查看確認(rèn)序號中的值,來判斷剛剛發(fā)送的數(shù)據(jù)主機B是否完全收到了。
跟前前面主機A和主機B的通信示意圖可以看到,在主機B給主機A發(fā)送了確認(rèn)應(yīng)答信號后,主機A沒有任何表示,但是此時主機B就不知道它發(fā)送的應(yīng)答信號主機A到底有沒有收到。
- 確認(rèn)應(yīng)答機制只保證歷史數(shù)據(jù)的可靠性,最新數(shù)據(jù)(確認(rèn)應(yīng)答)無法保證可靠性。
當(dāng)主機A給主機B發(fā)送數(shù)據(jù)后,主機B收到了,并且也有數(shù)據(jù)給主機A發(fā)送,就可以將數(shù)據(jù)和確認(rèn)應(yīng)答信號作為一個數(shù)據(jù)段發(fā)送給主機A,如上圖所示。
此時除了要將報頭中的確認(rèn)序號,ACK標(biāo)志位填充外,還要將有效載荷拼接數(shù)據(jù)段中。
當(dāng)主機A收到數(shù)據(jù)段以后,除了發(fā)現(xiàn)它是一個應(yīng)答信號,確定對方收到了字節(jié)剛剛發(fā)送的數(shù)據(jù),又發(fā)現(xiàn)它同樣有有效載荷,此時的有效載荷就是主機B發(fā)送過了的數(shù)據(jù)。
為什么要有兩組序號呢?一個是32位序號,一個是32位確認(rèn)序號,只用一個不行嗎?
答案是為了實現(xiàn)全雙工,像上面本喵講解的一樣,主機B給主機A發(fā)送的數(shù)據(jù)段中不僅起確認(rèn)應(yīng)答作用,還有數(shù)據(jù),如果此時只有一個組序號,主機A接收到數(shù)據(jù)段后就不能知道這個序號是確認(rèn)序號還是數(shù)據(jù)的編號。
使用兩組序號就將確認(rèn)應(yīng)答和數(shù)據(jù)的序號分離開了,互不影響,通信雙方可以實現(xiàn)全雙工通信。
超時重傳機制
丟包:
如上圖所示,如果主機A給主機B發(fā)送數(shù)據(jù)之后,由于網(wǎng)絡(luò)擁堵等原因,發(fā)生了丟包,導(dǎo)致數(shù)據(jù)無法到底主機B,所以主機B也不會給主機A一個應(yīng)答信號。
- 如果主機A在一個特定時間間隔內(nèi)沒有收到B發(fā)來的確認(rèn)應(yīng)答,就會進行重發(fā),這就是超時重傳機制。
還有一種情況:丟應(yīng)答
如上圖所示,主機A未收到B發(fā)來的確認(rèn)應(yīng)答, 也可能是因為ACK丟失了,主機B其實是收到了數(shù)據(jù)的,但是此時主機A認(rèn)為主機B沒有收到,所以就會觸發(fā)超時重傳機制,主機A會再次重新發(fā)送剛剛數(shù)據(jù)。
因此主機B會收到很多重復(fù)數(shù)據(jù),所以TCP協(xié)議需要能夠識別出哪些包是重復(fù)的包,并且把重復(fù)的丟棄掉。
- 利用前面提到的序列號,就可以很容易做到去重的效果,這一切都是操作系統(tǒng)在做。
超時重傳的中的超時又是如何確定呢?多長時間算是超時呢?
最理想的情況下,找到一個最小的時間,保證 “確認(rèn)應(yīng)答一定能在這個時間內(nèi)返回”,但是這個時間的長短,隨著網(wǎng)絡(luò)環(huán)境的不同是有差異的。
如果超時時間設(shè)的太長,會影響整體的重傳效率,如果超時時間設(shè)的太短, 有可能會頻繁發(fā)送重復(fù)的包。
- TCP為了保證無論在任何環(huán)境下都能比較高性能的通信,因此會動態(tài)計算這個最大超時時間。
Linux中(BSD Unix和Windows也是如此),超時以500ms為一個單位進行控制,每次判定超時重發(fā)的超時時間都是500ms的整數(shù)倍。
如果重發(fā)一次之后,仍然得不到應(yīng)答, 等待 2 * 500ms 后再進行重傳,如果仍然得不到應(yīng)答,等待 4 * 500ms 再進行重傳。
以此類推,以指數(shù)形式遞增,累計到一定的重傳次數(shù),TCP認(rèn)為網(wǎng)絡(luò)或者對端主機出現(xiàn)異常,強制關(guān)閉連接。
連接管理機制
這就是傳說中的“三次握手,四次揮手”。
三次握手:
如上圖所示就是使用TCP協(xié)議通信的整個流程。客戶端使用socket
創(chuàng)建套接字,服務(wù)端使用socket
創(chuàng)建套接字,bind
了端口號,并且listen
了套接字,設(shè)置為監(jiān)聽狀態(tài)。
-
第一次揮手:客戶端給服務(wù)端發(fā)送數(shù)據(jù)段,沒有數(shù)據(jù),只有是將報頭中的
SYN
標(biāo)志位置一,然后將只有報頭的數(shù)據(jù)段發(fā)送給服務(wù)端,表示請求建立連接。
如上圖所示,報頭中六個標(biāo)志位中的SYN
被置一,表示發(fā)起連接請求。所以當(dāng)服務(wù)端收到客戶端這個請求報文后,就知道三次揮手開始了。 -
第二次揮手:服務(wù)端在收到客戶端的連接請求后,返回應(yīng)答信號和連接請求,表示同意并且建立連接。
此時的數(shù)據(jù)段中同樣沒有數(shù)據(jù),只有報頭,并且六個標(biāo)志位中的ACK
和SYN
被置一,其中ACK
是作為客戶端發(fā)起連接請求的確認(rèn)應(yīng)答信號,SYN
是服務(wù)端向客戶端發(fā)起的連接請求。
- 第三次握手:客戶端收到服務(wù)器端的數(shù)據(jù)段后,將套接字的狀態(tài)設(shè)為
ESTABLISHED
,正式建立連接,并且再給服務(wù)端一個ACK
信號。
在客戶端收到服務(wù)器的數(shù)據(jù)段后,根據(jù)ACK
標(biāo)志位知道了服務(wù)器收到了自己的連接請求,再根據(jù)SYN
標(biāo)志位知道了服務(wù)器向自己也發(fā)起了連接請求,那么自己就可以正式建立連接了,然后再給服務(wù)端一個應(yīng)答信號,讓服務(wù)端也建立連接。
服務(wù)端收到這個最后應(yīng)答信號后,便知道客戶端已經(jīng)建立好連接了,自己也將套接字設(shè)置為ESTABLISHED
狀態(tài),表示連接建立。
- 當(dāng)客戶端使用了
connect
系統(tǒng)調(diào)用后,三次揮手就被發(fā)起了。- 當(dāng)服務(wù)端使用了
accept
系統(tǒng)調(diào)用后,三次揮手的請求就被接收到了,并且開始進行后續(xù)動作。- 三次揮手的過程完全由操作系統(tǒng)來維護。
此時服務(wù)端和客戶端雙方的套接字都是ESTABLISHED
狀態(tài),雙方正式連接連接。
- 根據(jù)三次揮手示意圖中可以看到,雙方建立連接的過程中,客戶端比服務(wù)端先建立連接。
- 雙方建立連接存在一個時間差,申請者先建立。
為什么就必須得是三次握手?一次握手行不行,兩次呢?四次呢?
假設(shè)現(xiàn)在一次握手就可以建立連接:
如上圖所示,當(dāng)客戶端發(fā)出帶有SYN
標(biāo)志位的數(shù)據(jù)端后,服務(wù)端收到后便建立連接,將套接字設(shè)置為ESTABLISHED
狀態(tài)。
如果此時客戶端發(fā)送了連接請求,并且自己沒有建立連接,自己的系統(tǒng)中并沒有建立用來連接套接字,那么服務(wù)端就相當(dāng)于維護著一個完全沒有用處的套接字,白白占用系統(tǒng)資源。
如果此時的客戶端是一個不法份子,他就可以頻繁的向服務(wù)端發(fā)起三握手手請求,并且自己不建立連接,只讓服務(wù)端建立連接,那么服務(wù)端的系統(tǒng)資源很快就被占用完,服務(wù)端也就奔潰了。
- 這種現(xiàn)象被叫做SYN洪水。
再假設(shè)現(xiàn)在兩次握手建立連接:
如上圖所示,客戶端向服務(wù)端發(fā)起三次握手請求,服務(wù)端收到后建立了連接,并且給客戶端發(fā)送了ACK
應(yīng)答信號和SYN
連接請求,讓客戶端也建立連接。
但是客戶端在收到服務(wù)端的數(shù)據(jù)端后選擇了忽略,并不建立連接。
如果客戶端同樣頻繁發(fā)起三次握手請求,并且忽略服務(wù)端返回的數(shù)據(jù)端,那么同樣會發(fā)送SYN洪水。
三次握手:
當(dāng)客戶端發(fā)起三次握手請求后,服務(wù)端收到以后并沒有第一時間建立連接,而是給客戶端確認(rèn)應(yīng)答,并且也向客戶端發(fā)起連接請求。
客戶端在收到服務(wù)端的數(shù)據(jù)段后,需要先建立連接,然后再向服務(wù)端發(fā)送確認(rèn)應(yīng)答,表示自己已經(jīng)建立好了連接。
服務(wù)端在收到應(yīng)答信號后,知道客戶端已經(jīng)建立了連接,自己再建立連接。
- 三次握手能夠保證,服務(wù)端在連接連接的時候,客戶端已經(jīng)建立好了連接。
所以如果是單一的客戶端發(fā)起SYN洪水
,那么客戶端需要和服務(wù)端付出同等的代價,都需要建立連接。一般情況下,服務(wù)端的配置比客戶端會高,所以客戶端耗不過服務(wù)端自己就先奔潰了。
如果有多臺客戶端向同一臺服務(wù)端發(fā)起SYN洪水攻擊,那么服務(wù)端也是扛不住的,但是這種安全問題就不是TCP協(xié)議該管的事情,TCP僅能做的事情就是讓自己的機制沒有漏洞。
- 三次握手可以有效防止單一客戶端向服務(wù)端發(fā)起攻擊。
在三次握手的過程中,客戶端證明了自己有發(fā)送和接收數(shù)據(jù)的能力,服務(wù)端也證明了自己有發(fā)送和接收數(shù)據(jù)的能力。
- 三次握手可以用最小的成本驗證客戶端和服務(wù)端之間的全雙工通信信道是通暢的。
這一點,一次握手和兩次握手都無法驗證。
至于四次握手,五次握手等等更多次的握手根本就沒有必要去用,因為三次握手能解決問題就沒有必要用更多次的握手。
- 三次握手不一定非得成功,最擔(dān)心的其實是最后一個
ACK
丟失,但是有配套的解決方案,就是超時重傳機制。- 三次握手最重要的是建立了連接結(jié)構(gòu)體,這些結(jié)構(gòu)體同樣被操作系統(tǒng)采用先描述再組織的方式管理了起來。
四次揮手:
當(dāng)客戶端使用close
系統(tǒng)調(diào)用后,就向服務(wù)端發(fā)起了四次揮手請求,四次揮手的作用是斷開連接。
-
第一次揮手:客戶端向服務(wù)端發(fā)送數(shù)據(jù)段,同樣沒有數(shù)據(jù),只是將
FIN
標(biāo)志位置一,表示要斷開連接。 -
第二次揮手:服務(wù)端收到客戶端斷開連接的請求后,將自己的套接字設(shè)置為
CLOSE_WAIT
狀態(tài),然后給客戶端發(fā)送確認(rèn)應(yīng)答信號。
客戶端收到服務(wù)端的確認(rèn)應(yīng)答信號后,知道了服務(wù)端已經(jīng)收到了自己四次揮手?jǐn)嚅_連接的請求,然后開始等待服務(wù)器的FIN
信號。
- 第三次揮手:服務(wù)器的套接字處于
CLOSE_WAIT
狀態(tài)時,同樣要向客戶端發(fā)送斷開連接的請求,所以向客戶端發(fā)送帶有FIN
信號的數(shù)據(jù)段。
通信是雙方的事,不能客戶端說斷開連接就斷開連接,只有雙方都發(fā)起過斷開連接的請求FIN
,連接才能斷開。
- 第四次揮手:客戶端收到服務(wù)端發(fā)來的
FIN
數(shù)據(jù)后,知道了服務(wù)端也要斷開連接,此時將自己的套接字設(shè)置為TIME_WAIT
狀態(tài),然后給服務(wù)端發(fā)送確認(rèn)應(yīng)答。
客戶端收到服務(wù)端發(fā)來的斷開連接請求后,并沒有第一時間斷開,而是先將狀態(tài)設(shè)置為TIME_CLOSE
狀態(tài),等待一段時間后再斷開連接。
服務(wù)端收到客戶端的第四次揮手應(yīng)答信號后,知道了客戶端已經(jīng)收到了自己斷開連接的請求,所以就將自己的套接字關(guān)閉了。
- 發(fā)起四次揮手的一方,最后的狀態(tài)是
TIME_WAIT
。- 被動斷開連接的一方,兩次揮手完成后會進入
CLOSE_WAIT
狀態(tài)。
- TCP協(xié)議中,通信雙方的地方是對等的,發(fā)起四次揮手以及三次握手有可能是任何一方。
在三次握手和四次揮手之間,就是雙方在進行通信,采用的是確認(rèn)應(yīng)答機制,如上圖所示。
理解TIME_WAIT狀態(tài)
為什么客戶端在發(fā)起四次揮手請求到完成后并沒有第一時間斷開連接而是等待一段時間呢?
- 保證最后一個ACK盡可能的被對方收到,如果對應(yīng)沒有收到會觸發(fā)超時重傳機制,此時未完全關(guān)閉的套接字還可以重寫發(fā)送ACK信號。
- 雙方在斷開連接的時候,網(wǎng)絡(luò)中還有滯留的報文,要保證滯留報文進行撤銷(教材中的理由)。
從TIME_WAIT
到CLOSED
之間維持的時間是多久呢?
- MSL:通信數(shù)據(jù)段從一方到另一方花費的最長時間稱為MSL。
這個維持的時間是 2 * MSL,這段時間能夠保證服務(wù)端觸發(fā)一次超時重傳,并且客戶端做出一次應(yīng)答,正好是兩次傳送數(shù)據(jù)的時間。
如上圖,在寫前面寫套接字代碼的時候,服務(wù)端綁定了一個端口后,進行網(wǎng)絡(luò)通信,然后使用ctrl + c
將服務(wù)端服務(wù)進程結(jié)束。
然后再運行,也就是重啟服務(wù)端,綁定相同的端口,但是此時就會出現(xiàn)bind error
錯誤,無法綁定,服務(wù)器也就重啟失敗。
- 服務(wù)端進程結(jié)束,服務(wù)端主動斷開連接,它會有一段時間處于
TIME_WAIT
狀態(tài),套接字沒有完全關(guān)閉。- 再次運行時無法綁定還沒有關(guān)閉的套接字。
在綁定失敗后,查看失敗端口號8080
的網(wǎng)絡(luò)狀態(tài),可以看到,該套接字的狀態(tài)是TIME_CLOSE
狀態(tài),這個狀態(tài)一般會維持4分鐘,也就是兩個MSL
時間。
這樣其實也存在很大危害,如果在618當(dāng)天,淘寶服務(wù)器突然奔潰了,需要立刻重啟,但是需要等待兩個MSL
時間(大概4分鐘),在這段時間內(nèi),已經(jīng)和服務(wù)器建立連接的客戶端就會因為超時而斷開連接,那么造成的損失是不可估量的。
解決方法:
使用系統(tǒng)調(diào)用setsockopt()
設(shè)置socket描述符的選項SO_REUSEADDR
為1,此時就可以立刻重新綁定之前的端口號了。
如上圖紅色框中代碼所示,在bind
套接字之前,使用setsockopt
將SO_REUSEADDR
設(shè)置為1。
如上圖所示,使用ctrl + c
結(jié)束服務(wù)端進程后,可以立刻重新啟動,并且綁定之前的端口號8080
。
但是這樣就有可能導(dǎo)致數(shù)據(jù)丟失等問題,所以還要結(jié)合具體情況來使用。
理解CLOSE_WAIT狀態(tài)
如上圖代碼所示,在前面寫的使用TCP協(xié)議的HTTP應(yīng)用程序中,服務(wù)端不執(zhí)行close
,也就是說,即使客戶端斷開連接,服務(wù)端也不會關(guān)閉套接字。
- 在TCP協(xié)議中,對應(yīng)著客戶端發(fā)起四次揮手請求,服務(wù)端給確認(rèn)應(yīng)答信號。
-
但是服務(wù)沒有發(fā)出
FIN
信號,沒有請求斷開連接,也就是從第三次和第四次揮手沒有了。
根據(jù)上面通信流程示意圖,此時服務(wù)端的套接字就會維持在CLOSE_WAIT
狀態(tài)。
如上圖所示,運行服務(wù)端進程后,客戶端發(fā)起HTTP請求,服務(wù)端收到了客戶端的請求報文。
此時查看當(dāng)前機器上httpserver
套接字的狀態(tài),可以看到,一個是監(jiān)聽狀態(tài)LISTEN
,一個是建立連接狀態(tài)ESTABLISHED
,連接狀態(tài)的套接字是用來進行通信的。
- 此時將客戶端的套接字關(guān)閉,也就是退出。
此時再次查看服務(wù)器套接字狀態(tài),可以看到,用來進行通信的套接字狀態(tài)未CLOSE_WAIT
。
根據(jù)上面現(xiàn)象我們知道,當(dāng)一段發(fā)起四次揮手?jǐn)嚅_連接后,另一端必須也要發(fā)起FIN
斷開連接請求,也就是調(diào)用close
系統(tǒng)調(diào)用,否則無法斷開連接,就會存在CLOSE_WAIT
狀態(tài)的套接字,占用系統(tǒng)資源。文章來源:http://www.zghlxwxcb.cn/news/detail-642117.html
??總結(jié)
這篇文章介紹了UDP的全部內(nèi)容,比較簡單,還有TCP的部分內(nèi)容,其他維護可靠性的機制再下篇文章中本喵再詳細(xì)介紹。文章來源地址http://www.zghlxwxcb.cn/news/detail-642117.html
到了這里,關(guān)于【網(wǎng)絡(luò)】傳輸層——UDP | TCP(協(xié)議格式&&確認(rèn)應(yīng)答&&超時重傳&&連接管理)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!