? ? ? ? ?我們用戶是處在應用層的,根據不同的場景和業務需求,傳輸層就要為我們應用層提供不同的傳輸協議,常見的就是TCP協議和UDP協議,二者各自有不同的特點,網絡中的數據的傳輸其實就是兩個進程間的通信,兩個進程在通信時,傳輸層使用TCP協議將一方進程的應用層的數據傳輸給另一方進程的應用層,我們這一節就是基于TCP協議講解網絡數據的傳輸。
目錄
一、主機字節序列和網絡字節序列
1.1 概念
1.2 接口函數
二、套接字地址結構
2.0 套接字
2.1?通用 socket 地址結構
2.2?專用 socket 地址結構
2.3?IP 地址轉換函數
三、網絡編程接口
3.1 創建套接字(買個手機)
3.2? 套接字地址綁定(為手機辦卡,電話號碼相當于地址)
3.3?從監聽隊列中接收一個連接(開機)
3.4?接受客戶端連接請求并創建新的套接字 (接聽電話)
3.5?客戶端主動與服務器建立連接
3.6 從已連接的套接字中接收數據(TCP數據讀)
3.6 發送數據到已連接的套接字(TCP數據寫)
3.7?從已連接或未連接的套接字接收數據(UDP數據讀)
3.8?通過套接字發送數據到指定目標地址((UDP數據寫)
3.9 關閉套接字
四、TCP 編程流程
五、三次握手和四次揮手(重點面試題)
5.1 三次握手
5.2?可以將三次握手改成兩次握手嗎?
5.3?四次揮手
5.4?可以將四次揮手改成三次揮手嗎?
六、tcp協議服務器-客戶端編程流程實驗(掌握)
6.1 服務器端
6.2 客戶端
七、實驗改進
7.1 服務器端
7.2 客戶端
一、主機字節序列和網絡字節序列
1.1 概念
? ? ? ? 主機字節序列分為大端字節序和小端字節序,不同的主機采用的字節序列可能不同。在兩臺使用不同字節序的主機之間傳遞數據時,可能會出現沖突。所以,在將數據發送到網絡時規定數據使用大端字節序,所以也把大端字節序成為網絡字節序列。對方接收到數據后,可以根據自己的字節序進行轉換。這是為了確保不同系統之間的數據傳輸一致性。無論主機字節序列是什么,數據在網絡上傳輸時都需要轉換為網絡字節序。
- 大端字節序是指:一個整數的高位字節存儲在內存的低地址處,低位字節存儲在內存的高地址 處。
- 小端字節序是指:整數的高位字節存儲在內存的高地址處,而低位字節則存儲在內存的 低地址處。
1.2 接口函數
? ? ? ? ? 為了在主機字節序列和網絡字節序列之間進行轉換,編程語言通常提供了一些標準函數。Linux 系統提供如下 4 個函數來完成主機字節序和網絡字節序之間的轉換:
#include <netinet/in.h>
uint32_t htonl(uint32_t hostlong); // 長整型的主機字節序轉網絡字節序
uint32_t ntohl(uint32_t netlong); // 長整型的網絡字節序轉主機字節序
uint16_t htons(uint16_t hostshort); // 短整形的主機字節序轉網絡字節序
uint16_t ntohs(uint16_t netshort); // 短整型的網絡字節序轉主機字節序
二、套接字地址結構
2.0 套接字
? ? ? ? 套接字(Socket)是網絡編程中用于描述IP地址和端口的通信端點。它是網絡通信中的一個抽象概念,通常用于描述兩個程序之間的雙向通信鏈路。套接字是網絡編程的基石,允許應用程序通過網絡發送和接收數據。
套接字的類型
套接字主要有兩種類型:
流式套接字(Stream Socket):
- 使用TCP(傳輸控制協議)。
- 提供面向連接的、可靠的、基于字節流的通信。
- 典型應用場景包括HTTP、FTP、SMTP等協議。
數據報套接字(Datagram Socket):
- 使用UDP(用戶數據報協議)。
- 提供無連接的、不可靠的、基于數據報的通信。
- 適用于需要快速傳輸且允許丟包的場景,如視頻流、在線游戲等。
2.1?通用 socket 地址結構
? ? ? ?在網絡編程中,通用的 socket 地址結構(Socket Address Structure)用于存儲網絡地址信息。不同的協議族(如 IPv4、IPv6 等)有不同的地址結構,但都遵循一個通用的框架。socket 網絡編程接口中表示 socket 地址的是結構體 sockaddr,這是所有地址結構的通用基礎,定義在 <sys/socket.h>
頭文件中。它是一個通用的地址結構,包含了地址族信息。
struct sockaddr {sa_family_t sa_family; // 地址族(Address Family)char sa_data[14]; // 協議地址(Protocol Address)
};
地址族類型通常與協議族類型對應。常見的協議族和對應的地址族如下圖所示:
2.2?專用 socket 地址結構
? ? ? ? TCP/IP 協議族有 sockaddr_in 和 sockaddr_in6 兩個專用 socket 地址結構體,它們分別用于 IPV4 和 IPV6:
sockaddr_in
結構體(用于 IPv4)
? ?sockaddr_in
結構體用于存儲 IPv4 地址信息。它定義在 <netinet/in.h>
頭文件中。結構體定義如下:
struct sockaddr_in {sa_family_t sin_family; // 地址族(必須為 AF_INET)in_port_t sin_port; // 端口號(使用 `htons` 將主機字節序轉換為網絡字節序)struct in_addr sin_addr; // IP 地址(使用 `inet_pton` 等函數進行賦值)char sin_zero[8]; // 填充字段,使得結構體大小與 `sockaddr` 一致
};
sin_family
:地址族,必須設置為AF_INET
。sin_port
:端口號,必須使用htons
函數將主機字節序轉換為網絡字節序。sin_addr
:IP 地址,通常使用inet_pton
或inet_aton
函數進行設置。sin_zero
:填充字段,使得結構體大小與sockaddr
結構體一致,通常設置為 0。
sockaddr_in6
結構體(用于 IPv6)
? ?sockaddr_in6
結構體用于存儲 IPv6 地址信息。它也定義在 <netinet/in.h>
頭文件中。結構體定義如下:
struct sockaddr_in6 {sa_family_t sin6_family; // 地址族(必須為 AF_INET6)in_port_t sin6_port; // 端口號(使用 `htons` 將主機字節序轉換為網絡字節序)uint32_t sin6_flowinfo; // 流信息(通常設置為 0)struct in6_addr sin6_addr; // IPv6 地址uint32_t sin6_scope_id; // 范圍 ID(用于本地鏈路地址,通常設置為 0)
};
sin6_family
:地址族,必須設置為AF_INET6
。sin6_port
:端口號,必須使用htons
函數將主機字節序轉換為網絡字節序。sin6_flowinfo
:流信息,通常設置為 0。sin6_addr
:IPv6 地址,使用inet_pton
函數進行設置。sin6_scope_id
:范圍 ID,主要用于本地鏈路地址,通常設置為 0。
2.3?IP 地址轉換函數
? ? ? 通常,人們習慣用點分(用點分隔)十進制字符串表示 IPV4 地址,但編程中我們需要先把它們轉化為整數(4個字節32位)方能使用,下面函數可用于點分十進制字符串表示的 IPV4 地址和網絡字節序整數表示的 IPV4 地址之間的轉換(字符串轉整型函數接口):
需要引入的頭文件 #include <arpa/inet.h>in_addr_t inet_addr(const char *cp); //字符串表示的 IPV4 地址轉化為無符號整型
char* inet_ntoa(struct in_addr in); // IPV4 地址的網絡字節序(無符號整型)轉化為字符串表示
三、網絡編程接口
3.1 創建套接字(買個手機)
int socket(int domain, int type, int protocol);
?參數解釋
domain: 指定套接字所使用的協議族,也稱為地址族。常見的值包括:
AF_INET
:IPv4 Internet 協議AF_INET6
:IPv6 Internet 協議AF_UNIX
或AF_LOCAL
:本地通信(UNIX 域套接字)AF_PACKET
:低級別的套接字接口,用于直接訪問網絡層type: 指定套接字的服務類型。常見的類型包括:
SOCK_STREAM
:提供面向連接的可靠字節流服務(如 TCP)SOCK_DGRAM
:提供數據報服務(如 UDP)SOCK_RAW
:提供原始網絡協議訪問SOCK_SEQPACKET
:提供序列包服務,類似于SOCK_STREAM
,但每個消息邊界保留protocol: 指定使用的協議。通常設置為 0,以選擇默認協議。可以明確指定特定協議:
IPPROTO_TCP
:如果 type 是SOCK_STREAM
IPPROTO_UDP
:如果 type 是SOCK_DGRAM
IPPROTO_ICMP
:如果 type 是SOCK_RAW
(用于原始套接字)返回值
? ? ? ?成功時返回一個非負整數,即套接字文件描述符。 失敗時返回 -1,并設置
errno
以指示錯誤。
3.2? 套接字地址綁定(為手機辦卡,電話號碼相當于地址)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
? ? ? ? 它是一個用于將套接字綁定到特定的本地地址和端口的系統調用。在網絡編程中,
bind
函數通常用于服務器端套接字,以指定它們將在哪個地址和端口上監聽傳入連接。參數解釋
sockfd:
- 這是由
socket
函數創建的套接字文件描述符。addr:
- 這是一個指向
struct sockaddr
的指針(通用套接字地址結構體指針),包含要綁定到的地址信息。實際的地址結構根據使用的協議族不同而不同:
- 對于 IPv4,使用
struct sockaddr_in
。- 對于 IPv6,使用
struct sockaddr_in6
。- 對于本地通信(UNIX 域套接字),使用
struct sockaddr_un
。addrlen:
- 這是地址結構的長度(以字節為單位)。利用sizeof求得即可,對于 IPv4 地址,通常是
sizeof(struct sockaddr_in)
;對于 IPv6 地址,通常是sizeof(struct sockaddr_in6)
。返回值
- 成功時返回 0。
- 失敗時返回 -1,并設置
errno
以指示錯誤。
3.3?從監聽隊列中接收一個連接(開機)
int listen(int sockfd, int backlog);
? ? ? 它是一個用于在指定的套接字上監聽連接請求的系統調用。它通常用于服務器端的套接字,以便將套接字轉換為被動模式,準備接受來自客戶端的連接請求。
參數解釋
sockfd:
- 這是由
socket
函數創建并綁定了地址(通過bind
函數)的套接字文件描述符。backlog:
- 在
accept
函數被調用之前可以排隊的連接請求數量。在Linux系統上指的是已經完成三次握手的客戶端的數量,在unix系統上指的是未完成加已完成的客戶端數量。- 如果連接請求的數量超過了此限制,新來的連接請求將被拒絕。
返回值
- 成功時返回 0。
- 失敗時返回 -1,并設置
errno
以指示錯誤。
? ? ? ?監聽隊列可以理解為:客戶端向服務器端發送連接請求時,首先,先將它放到監聽隊列中,讓它等著,然后服務器一個一個的從監聽隊列進行連接,相當于銀行大廳的等待區,監聽隊列的大小就是為等待客戶提供的凳子的數量。?
3.4?接受客戶端連接請求并創建新的套接字 (接聽電話)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
? ? ? ?它是一個用于接受傳入連接請求的系統調用。它通常用于服務器端套接字,用于接受客戶端連接請求并創建新的套接字用于與客戶端通信。
參數解釋
sockfd:
- 這是由
socket
函數創建并綁定了地址(通過bind
函數)的套接字文件描述符。addr:
- 這是一個指向
struct sockaddr
類型的指針,用于存儲連接的遠程地址信息(客戶端的套接字地址信息)。可以將其設置為NULL
,如果不關心連接的遠程地址信息。addrlen:
- 這是一個指向
socklen_t
類型的指針,指示傳入的地址結構的長度。在調用accept
函數之前,應該將其設置為struct sockaddr
結構的大小。返回值
- 如果成功,返回一個新的套接字文件描述符,也就是連接套接字,用于與客戶端通信。
- 如果失敗,返回 -1,并設置
errno
以指示錯誤。
3.5?客戶端主動與服務器建立連接
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
? ? ? 它是一個用于連接到遠程服務器的系統調用。它通常用于客戶端套接字,用于與服務器建立連接。
參數解釋
sockfd:
- 這是由
socket
函數創建的套接字文件描述符。serv_addr:
- 這是一個指向
struct sockaddr
類型的指針,包含遠程服務器的地址信息。addrlen:
- 這是傳入地址結構的長度(以字節為單位)。
返回值
- 如果成功建立連接,則返回 0。
- 如果失敗,返回 -1,并設置
errno
以指示錯誤。
3.6 從已連接的套接字中接收數據(TCP數據讀)
ssize_t recv(int sockfd, void *buff, size_t len, int flags);
? ? ?它是一個用于從套接字接收數據的系統調用。它通常用于在網絡編程中從已連接的套接字中接收數據。recv的返回值如果等于0,說明對方關閉了!!!這是循環收發判斷的唯一條件!
參數解釋
sockfd:
- 這是由
socket
函數創建的套接字文件描述符。buff:
- 這是一個指向接收數據緩沖區的指針,用于存儲接收到的數據。
len:
- 這是接收數據緩沖區的長度,即
buff
所指向的緩沖區的大小。flags:
- 這是一組控制接收行為的標志,可以為 0 或以下之一的按位或:
MSG_WAITALL
:阻塞直到接收到指定長度的數據。MSG_DONTWAIT
:非阻塞接收數據。返回值
- 如果成功接收到數據,則返回接收到的字節數。
- 如果連接已關閉,則返回 0。
- 如果發生錯誤,則返回 -1,并設置
errno
來指示錯誤。
3.6 發送數據到已連接的套接字(TCP數據寫)
ssize_t send(int sockfd, const void *buff, size_t len, int flags);
? ? ? ?它是一個用于將數據通過套接字發送到遠程端的系統調用。通常在網絡編程中,它被用于發送數據到已連接的套接字上。
參數解釋
sockfd:
- 這是由
socket
函數創建的套接字文件描述符。buff:
- 這是一個指向要發送數據的緩沖區的指針。
len:
- 這是要發送的數據的字節數。
flags:
- 這是一組控制發送行為的標志,可以為 0 或以下之一的按位或:
MSG_CONFIRM
:要求數據發送得到確認。MSG_DONTROUTE
:數據不路由,僅限于本地接收。MSG_EOR
:數據結束標志。MSG_MORE
:還有更多數據等待發送。MSG_NOSIGNAL
:忽略 SIGPIPE 信號,如果連接已關閉,則不會引發信號。返回值
- 如果成功發送數據,則返回實際發送的字節數。
- 如果發送過程中出現錯誤,則返回 -1,并設置
errno
來指示錯誤。
3.7?從已連接或未連接的套接字接收數據(UDP數據讀)
ssize_t recvfrom(int sockfd, void *buff, size_t len, int flags, struct sockaddr* src_addr, socklen_t *addrlen);
? ? ? 它是一個用于從已連接或未連接的套接字接收數據的系統調用。與
recv
不同的是,recvfrom
可以從任意地址接收數據,而不僅僅是連接到套接字的對等方。參數解釋
sockfd:
- 這是由
socket
函數創建的套接字文件描述符。buff:
- 這是一個指向接收數據緩沖區的指針,用于存儲接收到的數據。
len:
- 這是接收數據緩沖區的長度,即
buff
所指向的緩沖區的大小。flags:
- 這是一組控制接收行為的標志,可以為 0 或以下之一的按位或:
MSG_WAITALL
:阻塞直到接收到指定長度的數據。MSG_DONTWAIT
:非阻塞接收數據。MSG_TRUNC
:截斷超出緩沖區大小的數據。src_addr:
- 這是一個指向存儲發送端地址信息的
struct sockaddr
結構體的指針。addrlen:
- 這是傳入地址結構的長度(以字節為單位)。在調用
recvfrom
函數之前,應該將其設置為struct sockaddr
結構的大小。返回值
- 如果成功接收到數據,則返回接收到的字節數。
- 如果連接已關閉,則返回 0。
- 如果發生錯誤,則返回 -1,并設置
errno
來指示錯誤。
3.8?通過套接字發送數據到指定目標地址((UDP數據寫)
ssize_t sendto(int sockfd, void *buff, size_t len, int flags, struct sockaddr* dest_addr, socklen_t addrlen);
? ? ? ?它是一個用于通過套接字發送數據到指定目標地址的系統調用。與
send
不同的是,sendto
允許指定目標地址,因此適用于無連接的 UDP 套接字以及有連接的套接字。參數解釋
sockfd:
- 這是由
socket
函數創建的套接字文件描述符。buff:
- 這是一個指向要發送數據的緩沖區的指針。
len:
- 這是要發送的數據的字節數。
flags:
- 這是一組控制發送行為的標志,可以為 0 或以下之一的按位或:
MSG_CONFIRM
:要求數據發送得到確認。MSG_DONTROUTE
:數據不路由,僅限于本地發送。MSG_EOR
:數據結束標志。MSG_MORE
:還有更多數據等待發送。MSG_NOSIGNAL
:忽略 SIGPIPE 信號,如果連接已關閉,則不會引發信號。dest_addr:
- 這是一個指向包含目標地址信息的
struct sockaddr
結構體的指針。addrlen:
- 這是傳入目標地址結構的長度(以字節為單位)。在調用
sendto
函數之前,應該將其設置為struct sockaddr
結構的大小。返回值
- 如果成功發送數據,則返回實際發送的字節數。
- 如果發生錯誤,則返回 -1,并設置
errno
來指示錯誤。
3.9 關閉套接字
int close(int sockfd);
它是一個用于關閉套接字的系統調用。關閉套接字后,不再可以使用該套接字進行數據傳輸或接收。
參數解釋
- sockfd:
- 這是要關閉的套接字的文件描述符。
返回值
- 如果成功關閉套接字,則返回 0。
- 如果發生錯誤,則返回 -1,并設置
errno
來指示錯誤。
四、TCP 編程流程
TCP 提供的是面向連接的、可靠的、字節流服務。TCP 的服務器端和客戶端編程流程如 下:
1、socket()方法是用來創建一個套接字,有了套接字就可以通過網絡進行數據的收發。
? ? ? ? ?這也是為什么進行網絡通信的程序首先要創建一個套接字。創建套接字時要指定使用的服務類型,使用 TCP 協議選擇流式服務(SOCK_STREAM)。
2、bind()方法是用來指定套接字使用的 IP 地址和端口。
? ? ? ? ?IP 地址就是自己主機的地址,如果主機沒有接入網絡,測試程序時可以使用回環地址“127.0.0.1”。端口是一個 16 位的整型值, 一般 0-1024 為知名端口,如 HTTP 使用的 80 號端口。這類端口一般用戶不能隨便使用。其 次,1024-4096 為保留端口,用戶一般也不使用。4096 以上為臨時端口,用戶可以使用。在 Linux 上,1024 以內的端口號,只有 root 用戶可以使用。
3、listen()方法是用來創建監聽隊列。
? ? ? ? ?監聽隊列有兩種,一個是存放未完成三次握手的連接, 一種是存放已完成三次握手的連接。listen()第二個參數就是指定已完成三次握手隊列的長度。
? ? ? ? ?在網絡編程中,服務器端通過監聽指定的網絡地址和端口來等待客戶端的連接請求。
監聽隊列就像是一個等待區,它存放著已經發送連接請求但還沒有得到服務器響應的客戶端連接請求。當一個客戶端請求連接時,服務器將其放入監聽隊列中,然后按照一定的順序逐個處理這些請求。通俗地說,你可以把監聽隊列想象成是一個餐廳的等候區。當你到達餐廳時,可能會看到一個等候區,里面坐滿了等待就座的人。服務員會按照先來后到的順序逐個安排客人入座,就像服務器按照監聽隊列中連接請求的順序逐個處理客戶端的連接請求一樣。
4、accept()處理存放在 listen 創建的已完成三次握手的隊列中的連接。
? ? ? ?每處理一個連接,則 accept()返回該連接對應的套接字描述符。如果該隊列為空,則 accept 阻塞。
5、connect()方法一般由客戶端程序執行,需要指定連接的服務器端的 IP 地址和端口。
? ? ? 該方法執行后,會進行三次握手, 建立連接。
6、send()方法用來向 TCP 連接的對端發送數據。
? ? ? send()執行成功,只能說明將數據成功寫入到發送端的發送緩沖區中,并不能說明數據已經發送到了對端。send()的返回值為實際寫入到發送緩沖區中的數據長度。
7、recv()方法用來接收 TCP 連接的對端發送來的數據。
? ? ? ? recv()從本端的接收緩沖區中讀取數 據,如果接收緩沖區中沒有數據,則 recv()方法會阻塞。返回值是實際讀到的字節數,如果 recv()返回值為 0, 說明對方已經關閉了 TCP 連接。
8、close()方法用來關閉 TCP 連接。
? ? ?此時,會進行四次揮手。
五、三次握手和四次揮手(重點面試題)
5.1 三次握手
? ? ? ? ? 客戶端在進行connect()開始建立連接 之后就會進行三次握手!
? ? ? ? 三次握手是TCP/IP協議中用于建立可靠連接的過程。在進行通信之前,客戶端和服務器之間需要通過三次握手來確認彼此的通信能力和參數設置。這個過程包括以下步驟:
客戶端發送同步(SYN)報文:客戶端首先向服務器發送一個帶有SYN標志的TCP報文段,表示客戶端想要建立連接,并且指定初始序列號(sequence number)。
服務器確認同步(SYN-ACK)報文:服務器收到客戶端的SYN報文后,會向客戶端發送一個帶有SYN和ACK標志的TCP報文段作為確認。該報文段中也包含服務器選擇的初始序列號。
客戶端確認(ACK)報文:最后,客戶端收到服務器的SYN-ACK報文后,會向服務器發送一個帶有ACK標志的TCP報文段作為確認。這個報文段不攜帶SYN標志。
完成了這三次握手之后,客戶端和服務器之間的連接就建立起來了,雙方可以開始進行數據傳輸。這個過程確保了雙方都能夠收到彼此的確認,從而建立了可靠的通信連接。
下面為面試內容!!? ? ?
? ? ? ? 在完成握手時,有兩個隊列,一個是未完三次握手隊列,一個是已完成三次握手隊列,客戶端請求連接,首先會放到未完三次握手隊列,然后等他完成三次握手隊列,也就是建立好連接以后,就會將它放到已完成三次握手隊列,(注意:listen(socked,5) 在linux里這里的5是代表已完成三次握手隊列的大小,在unix里代表未完成和已完成隊列之和。這里的5,不是說只能完成5次握手,而是完成握手隊列里能放5個鏈接,第六個就放在未完成隊列里,等到完成握手隊列里有空位了,在挪下來 ),然后在進行accept()的時候,它會去已完成三次握手隊列的里面去看,如果有已經完成三次握手隊列的客戶端請求,那么他就會與該客戶端建立連接,產生一個連接套接字,否則,他會一直阻塞住!(accept只處理已完成握手隊列中的鏈接)
三次握手發生在客戶端執行 connect()的時候,該方法返回成功,則說明三次握手已經建 立。三次握手示例圖如下:?
?
現在解釋這個圖:
-
客戶端首先向服務器發送一個帶有 SYN(同步)標志的報文,表示客戶端想要建立連接,并指定初始序列號。這是第一次握手。
-
服務器收到客戶端的 SYN 報文后,會發送一個帶有 SYN 和 ACK(確認)標志的報文給客戶端,表示服務器收到了客戶端的請求,并同意建立連接,同時服務器也指定了自己的初始序列號。這是第二次握手。
-
客戶端收到服務器的 SYN-ACK 報文后,發送一個帶有 ACK 標志的報文給服務器,表示客戶端確認收到了服務器的確認,并同意建立連接。這是第三次握手。
5.2?可以將三次握手改成兩次握手嗎?
? ? ? 不可以!根據TCP協議的設計,三次握手是必需的,并且是建立可靠連接的基礎。在標準的TCP實現中,無法將三次握手簡化為兩次握手。這是因為第三次握手中客戶端必須發送ACK包來確認連接建立,以確保雙方都能夠收到對方的確認信息。
5.3?四次揮手
? ? ? ?執行close()之后就會進行四次揮手操作,服務器端和客服端那一端先close()都可以!
三次揮手是TCP/IP協議中用于關閉連接的過程。與建立連接時的三次握手相似,關閉連接時需要進行四次揮手以確保雙方都能夠完成數據傳輸并關閉連接,這個過程包括以下步驟:
客戶端發送關閉請求(FIN):當客戶端決定關閉連接時,它會發送一個帶有FIN標志的TCP報文段給服務器,表示它不再發送數據了,但仍然可以接收數據。
服務器確認關閉請求(ACK):服務器收到客戶端的關閉請求后,會發送一個帶有ACK標志的TCP報文段作為確認,表示它已經收到了客戶端的關閉請求。
服務器發送關閉請求(FIN):當服務器確定不再發送數據時,它也會向客戶端發送一個帶有FIN標志的TCP報文段,表示它也準備關閉連接。
客戶端確認關閉請求(ACK):客戶端收到服務器的關閉請求后,會發送一個帶有ACK標志的TCP報文段作為確認。此時,雙方的連接就被完全關閉了。
通過這個四次揮手的過程,客戶端和服務器都有機會告知對方它們不再發送數據,并且確認對方的關閉請求,從而安全地關閉連接,避免數據丟失或不完整的傳輸。
四次揮手發生在客戶端或服務端執行 close()關閉連接的時候,示例圖如下:?
?
這里是四次揮手的解釋:
-
客戶端首先發送一個帶有 FIN(關閉請求)標志的報文給服務器,表示客戶端不再發送數據,但仍然能接收數據。這是第一次揮手。
-
服務器收到客戶端的 FIN 報文后,發送一個帶有 ACK(確認)標志的報文給客戶端,表示服務器已經收到了客戶端的關閉請求,但服務器還可以向客戶端發送數據。這是第二次揮手。
-
服務器在確定不再發送數據后,發送一個帶有 FIN(關閉請求)標志的報文給客戶端,表示服務器也準備關閉連接。這是第三次揮手。
-
客戶端收到服務器的 FIN 報文后,發送一個帶有 ACK(確認)標志的報文給服務器,表示客戶端確認收到了服務器的關閉請求。這是第四次揮手。
5.4?可以將四次揮手改成三次揮手嗎?
? ? ? ?可以,四次揮手可以演化成三次揮手 當一端close 發送報文過來,此時我也要close了,回復報文,和通知對方關閉的報文一起發送。
- 第一次揮手(FIN): 客戶端發送一個FIN報文,表示它要關閉到服務器的數據傳送。
- 第二次揮手(FIN): 服務器收到FIN后,直接發送一個FIN報文,表示它也要關閉到客戶端的數據傳送。
- 第三次揮手(ACK): 客戶端收到FIN后,發送一個ACK報文,確認收到關閉請求,連接關閉。
六、tcp協議服務器-客戶端編程流程實驗(掌握)
6.1 服務器端
? ? ? 簡單的TCP服務器,它監聽6000端口,接收來自客戶端的消息,回復“ok”并關閉連接。服務器無限運行,一次處理一個客戶端。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int main()
{int sockfd = socket(AF_INET,SOCK_STREAM,0); //創建套接字 ----->監聽套接字(相當于飯店的接待員)if( sockfd == -1 ){exit(1);}struct sockaddr_in saddr,caddr; //定義服務器套接字地址,客戶端套接字地址memset(&saddr,0,sizeof(saddr)); //清零套接字地址結構體的第四個成員/*為服務器套接字地址結構體初始化*/saddr.sin_family = AF_INET; //地址族 IPV4 saddr.sin_port = htons(6000); //端口號 6000saddr.sin_addr.s_addr = inet_addr("43.138.164.79"); //服務器IP地址;回環地址(用的是測試的本機)int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr)); //套接字地址綁定if ( res == -1 ){printf("bind err\n"); //這里容易失敗,所以要打印觀察exit(1);}if (listen(sockfd,5) == -1 ) //從監聽隊列中接收一個連接{exit(1); }//服務器無限運行,一次處理一個客戶端。while( 1 ) //循環接收連接{int len = sizeof(caddr);int c = accept(sockfd,(struct sockaddr*)&caddr,&len); //返回值為連接套接字(得到新的套接字描述符),沒有人連接時,可能會阻塞!(沒有客人來)if (c < 0 ) //c是套接字文件描述符,相當于服務員,連接失敗{continue;}printf("accept c =%d\n",c );char buff[128] = {0}; int n = recv(c,buff,127,0); //接收客戶端發送過來的數據,如果客戶端未發送數據,此時便會阻塞! 讀取最多127字節,以留出一個字節用于\0終止符printf("recv=%s\n",buff); send(c,"ok",2,0); //服務器給客戶端發送數據close(c); //關閉本次與客戶端連接的套接字描述符}
}
6.2 客戶端
? ? ? ?實現了一個簡單的TCP客戶端,它連接到IP地址為
43.138.164.79
、端口為6000
的服務器,讀取用戶輸入,將輸入發送到服務器,接收服務器的響應并打印,然后關閉連接。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int main()
{int sockfd = socket(AF_INET,SOCK_STREAM,0); //創建套接字---->監聽套接字if (sockfd == -1 ){exit(1);}/**注意:客戶端不需要綁定套接字地址(調用bind()函數)端口號會隨機分配,IP地址就直接用*//*客戶端需要連接服務器端,因此需要指定連接的服務器的套接字地址,下面的都是服務器的套接字地址信息*/struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family = AF_INET; //地址族saddr.sin_port = htons(6000); //服務器的端口號saddr.sin_addr.s_addr = inet_addr("43.138.164.79"); //服務器IP地址int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr)); //連接服務器,失敗概率高,打印顯示if (res == -1 ){printf("connect err\n");exit(1);}char buff[128] = {0};printf("input:\n");fgets(buff,128,stdin);send(sockfd,buff,strlen(buff)-1,0); //客戶端發送數據給服務器,客戶端不分監聽套接字和連接套接字memset(buff,0,128);recv(sockfd,buff,127,0); //客戶端接收服務器發送過來的數據printf("buff=%s\n",buff);close(sockfd);exit(0);
}
運行有先后順序,先運行服務器端,在運行客戶端。
七、實驗改進
7.1 服務器端
一旦有客戶端連接成功,便會一直建立連接,循環收發數據!
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int main()
{int sockfd = socket_init();if (sockfd == -1){printf("create socket failed\n");exit(1);}while( 1 ){struct sockaddr_in caddr;int len = sizeof(caddr);int c = accept(sockfd,(struct sockaddr*)&caddr,&len);//阻塞if (c < 0 ){continue;}//上面一旦有客戶端連接成功,便會進行下面的循環數據收發/****改動之處:一直建立連接,可以循環收發數據****/while ( 1 ){char buff[128] = {0};int n = recv(c,buff,127,0); //可能會阻塞if(n<=0) //n等于0說明對方關閉,n小于0說明出錯了{ break; //對方關閉后,不需要進行通信了}printf("recv(c=%d)=%s\n",c,buff);send(c,"ok",2,0);}close(c); //服務器端也應該關閉與該客戶端進行通信}
}//封裝創建套接字并綁定,進行監聽的函數
int socket_init()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);//tcp if (sockfd == -1){exit(1);}struct sockaddr_in saddr;memset(&saddr, 0, sizeof(saddr));saddr.sin_family = AF_INET; //地址族 ipv4saddr.sin_port = htons(6000);saddr.sin_addr.s_addr = inet_addr("0.0.0.0");int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));if (res == -1){printf("bind err\n");exit(1);}if (listen(sockfd, 5) == -1){exit(1);}return sockfd;}
7.2 客戶端
客戶端連接成功,便會一直建立連接,循環收發數據!
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int main()
{//創建套接字int sockfd = socket(AF_INET,SOCK_STREAM,0);if (sockfd == -1 ){exit(1);}//連接服務器struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family = AF_INET;saddr.sin_port = htons(6000);//ser 6000saddr.sin_addr.s_addr = inet_addr("43.138.164.79");int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));if (res == -1 ){printf("connect err\n");exit(1);}printf("連接成功!\n");//****客戶端可以循環進行發送***/while(1){ char buff[128] = {0};printf("input:\n");fgets(buff,128,stdin);if (strncmp(buff, "end", 3) == 0){break;}send(sockfd,buff,strlen(buff)-1,0);memset(buff,0,128);recv(sockfd,buff,127,0);printf("buff=%s\n",buff);}close(sockfd);exit(0);
}
至此,已經講解完畢!篇幅較長,慢慢消化,以上就是全部內容!請務必掌握,創作不易,歡迎大家點贊加關注評論,您的支持是我前進最大的動力!下期再見!