詳解TCP網路協定棧的工作原理

2023-07-24 12:00:21

本文分享自華為雲社群《網路通訊的神奇之旅:解密Linux TCP網路協定棧的工作原理》,作者: Lion Long 。

一、TCP網路開發API

TCP,全稱傳輸控制協定(Transmission Control Protocol),是一種面向連線的、可靠的、基於位元組流的傳輸層通訊協定。

1.1、TCP伺服器呼叫的API

#include <sys/types.h> /* See NOTES */

#include <sys/socket.h>

// 1

int socket(int domain, int type, int protocol);

// 2

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// 3

int listen(int sockfd, int backlog);

// 4

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

// 5

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

// 6

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

// 7

int close(int fd);

// 8

int shutdown(int sockfd, int how);

1.2、TCP使用者端呼叫的API

#include <sys/types.h> /* See NOTES */

#include <sys/socket.h>

// 1

int socket(int domain, int type, int protocol);

// 2

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// 3

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// 4

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

// 5

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

// 6

int close(int fd);

// 7

int shutdown(int sockfd, int how);

1.3、API函數的作用

(1)int socket(int domain, int type, int protocol)

在檔案系統中分配一個fd,並建立TCB資料結構。

(2)int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

為TCP的socket繫結本地IP地址和埠。

(3)int listen(int sockfd, int backlog)

將TCP置於LISTEN狀態。

(4)int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)

從全連線佇列中取出一個節點,並分配一個fd。

(5)ssize_t recv(int sockfd, void *buf, size_t len, int flags)

在對應fd中,從讀緩衝區中拷貝出資料。

(6)ssize_t send(int sockfd, const void *buf, size_t len, int flags)

把fd對應的TCB資料拷貝到寫緩衝區中。

(7)int close(int fd)

準備一個FIN包,放到寫緩衝區,是否fd。

(8)int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

準備一個SYN包,交給協定棧傳送出去,等待三次握手完成後才返回。

二、TCP的三個階段

2.1 TCP建立連線

TCP連線的建立主要依靠socket()、bind()、listen()、connect()、accept()這幾個函數。

2.1.1、TCP的三次握手

示意圖:

cke_122.png

三次握手在kernel協定棧中進行,那麼三次握手是在哪幾個函數中傳送的呢?

第一次,由connect()函數觸發 發起握手,也就是傳送syn包到伺服器端;

第二次,在listen()之後accept()之前,伺服器接收到syn包後傳送syn&&ack包到使用者端;

第三次,使用者端傳送ack包到伺服器端完成連線的建立。

TCP報頭:

0 |1 |2 |3

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1

+-------------------------------+-------------------------------+

| Source Port | Destination Port |

+---------------------------------------------------------------+

| Sequence Number |

+---------------------------------------------------------------+

| Acknowledgment Number |

+-------+-----------+-+-+-+-+-+-+-------------------------------+

| Header| Reserve |U|A|P|R|S|F| Window |

| Length| |R|C|S|S|Y|I| |

| | |G|K|H|T|N|N| |

+-------------------------------+-------------------------------+

| Checksum | Urgent Pointer |

+---------------------------------------------------------------+

| Option |

+---------------------------------------------------------------+

| Data |

| ... |

+---------------------------------------------------------------+
  • SYN:即synchronous,同步。
  • ACK:即acknowledgement,確認。
  • PSH:即push,推播。
  • FIN :即finish,結束。
  • RST:即reset,重置。
  • URG:即urgent,緊急。
  • Sequence Number:是封包本身第一個位元組的序列號。
  • Acknowledge Number:是期望對方繼續傳送的那個確認封包的序列號其值一般為接收到的Sequence Number加1。

從報文中可以看出,SYN包最重要的是將SYN位設為1,設定Sequence Number;ACK包最重要的是將ACK位設為1,設定Acknowledgment Number。

半連線佇列和全連線佇列:

在三次握手中,Linux kener 協定棧會維護兩個佇列:半連線佇列和全連線佇列。

半連線佇列(也叫SYN佇列): 半連線佇列在第一握手中,當用戶端傳送SYN包到伺服器端時,伺服器端的半連線佇列會加入一個節點,表示此連線處於半連線狀態。

全連線佇列(也叫ACCEPT佇列): 全連線佇列在第三握手中,當用戶端傳送ACK包到伺服器端時,伺服器端會檢查半連線佇列中是否存在此連線節點(通過五元組進行查詢),如果存在就將此連線節點加入全連線佇列中;否則將拋棄此連線。

accpt()函數在三次握手完成後,從全連線佇列中取出連線節點,為節點分配socket fd,返回到使用者態。

那麼,accept()函數如何知道全連線佇列中有節點呢?

當三次握手完成後,全連線佇列建立節點的同時會釋放一個有連線接入的訊號(single或號誌),這個訊號決定了accept()函數是否可以從全連線佇列中取節點;也決定epoll等IO多路複用器能不能檢查這個連線fd是否可讀。

在阻塞模式下,accept()函數一直等待訊號,直到全連線佇列中有節點才返回。

在非阻塞模式下,全連線佇列為空accept()函數就返回-1,否則返回socket fd。

在listen()函數有,有一個backlog引數,這個參數列示的是全連線佇列的大小還是半連線佇列的大小呢?

隨著TCP協定的不斷迭代,backlog引數在不同的版本中代表的含義也不相同;它可以是半連線佇列大小,也可以是全連線佇列大小,也可以是半連線佇列+全連線佇列的大小總和。不過,效果不會有太大差異。目前版本中主要表示全連線佇列的大小。

DDOS攻擊:

根據三次握手原理,產生一種對伺服器的攻擊方式:DDOS攻擊。所謂DDOS攻擊,就是使用者端偽造一些不存在的IP,一直傳送SYN包,使伺服器的半連線佇列不斷增大,當半連線佇列的大小達到極限時,造成網路阻塞就會導致伺服器無法再接受連線,從而使伺服器奔潰。

2.1.2、TCP狀態轉換

TCP狀態轉換圖:

cke_123.png

(1)從狀態轉換圖看出,LISTEN狀態可以通過傳送SYN和資料轉換到SYN_SEND狀態;也就是LISTEN狀態可以傳送資料。

(2)SYN_SEND狀態可以收到SYN,並行送SYN和ACK轉換到SYN_RECV狀態;也就是兩個裝置可以互發SYN包,建立連線。

2.2 TCP傳輸資料

TCP傳輸資料主要依靠send()和recv()兩個函數。

使用send()函數傳送資料時,返回正數不一定代表傳送成功。因為send()函數僅僅只是將資料拷貝到協定棧的寫緩衝區,由協定棧傳送;傳送過程中會經過N個閘道器,可能存在丟包或鏈路斷開導致未能傳送到目的地。如果要知道資料是否傳送成功,需要加上確認機制(ACK)。

2.2.1、傳輸控制塊TCB

為了保證資料能正確分發,TCP使用一種TCB(傳輸控制塊)的資料結構,把傳送給不同裝置的資料封裝起來。這個TCB會存在整個TCP週期,知道斷開連線。

一個TCB資料塊包含資料傳送雙方對應的socket資訊以及擁有存放資料的緩衝區。建立連線連線傳送資料之前,通訊雙方必須做一個準備工作:分配記憶體建立TCB資料塊。當雙方準備好自己的socket和TCB資料結構後,就可以進入「三次握手」建立連線。

2.2.2、TCP分包

TCP分包就是要傳輸的資料很大,超出傳送快取區剩餘空間,將會進行分包;待傳送的資料大於最大報文長度,TCP在傳輸前將進行分包。

分包在應用程式的處理一般是傳送回圈send(),接收方迴圈recv()。

2.2.3、TCP粘包及解決方案

TCP粘包就是傳送方傳送的若干封包到接收方接收時粘成一個包,從接收緩衝區看就是後封包的頭緊接著前封包的尾。

常見解決方案:

(1)(推薦)應用層協定頭前面新增包長度。分兩次接收資料;第一次先接收包的長度,然後根據包的長度一次性讀取或迴圈讀取資料。

例如:

// ...

ssize count=0;

ssize size=0;

while(count<tcpHdr->length)

{

size=recv(fd,buffer,buffersize,0);

count+=size;

}

// ...

(2)為每個包新增分隔符。在資料末尾新增分隔符,這會導致解資料可能需要有合包操作;因為分割封包後,需要記錄後一個封包,用於與該包後面部分資料進行合併。

cke_124.png

2.3 TCP四次揮手

斷開連線是比建立連線和傳輸資料還複雜的一個過程,斷開連線主要分為主動關閉和被動關閉兩種。

四次揮手示意圖:

cke_125.png

需要注意的是,呼叫close()不是立即完成斷開,而是關閉了資料傳輸,進入了四次揮手階段,TCB資料結構還沒有釋放。四次揮手結束才真正把TCB釋放。

根據四次揮手流程,可以思考一些問題:

(1)傳輸資料過程中,網線斷了之後立刻連線,TCP如何知道?

網線掉線網路卡會停止供電,再次連線後網路卡恢復供電,網路卡服務重啟,網路連線重連。應用程式設計通過心跳包檢測。

(2)伺服器如何知道使用者端是否宕機?

一樣需要通過心跳包機制來檢測。

(3)伺服器如何甄別網路阻塞和宕機?

伺服器傳送心跳包時,不僅僅發一次,而是要傳送多次的;如果是網路阻塞,那麼在一定時間內一定有回覆資訊;如果是宕機,無論多長時間都沒有使用者端的回覆。

(4)如果出現大量的CLOSING狀態,如何處理?

出現大量CLOSING狀態,基本上業務上要處理的邏輯過多,導致一直在CLOSING狀態;可以使用非同步,將網路層和業務層分離,單獨處理。

(5)四次揮手中,為什麼存在TIME_WAIT狀態?

防止沒有LAST_ACK或LAST_ACK丟失,導致一直重發已經不存在的socket。

總結

需要掌握TCP三次握手和四次揮手的過程,熟悉TCP狀態轉換。清楚什麼是SYN包和ACK包。

(1)三次握手是 由使用者端發起SYN,伺服器端收到SYN後傳送SYN和ACK,使用者端回覆ACK;完成連線的建立。

(2)斷開連線主要有主動斷開和被動斷開。

(3)四次揮手是 由發起方呼叫close(),同時傳送FIN包;接收端接收到FIN包返回ACK包,接收端傳送FIN包;發起方接收到FIN包返回ACK包;完成斷開。

(4)理解TCP的狀態轉換圖。LISTEN狀態到SYN_RCVD狀態和SYN_SEND狀態,如何進入ESTABLISHED狀態;四次揮手FIN_WAIT_1、FIN_WAIT_2、TIME_WAIT、CLOSING直接的轉換,CLOSE_WAIT和LAST_ACK的處理等。

(5)理解API的底層原理,以及全連線佇列和半連線佇列。

(6)TCP的分包場景以及TCP粘包的處理方式。

TCP通訊完整過程:

cke_126.png

 

點選關注,第一時間瞭解華為雲新鮮技術~