今天和大家講一下socket網絡編程中粘包和拆包的問題。
1、出現粘包拆包的原因
假設一個這樣的場景,客戶端要利用send()函數發送字符“asd”到服務端,連續發送3次,但是服務端休眠10秒之后再去緩沖池中接收。那么請問10秒之后服務端從緩沖區接收到的信息是“asd”還是“asdasdasd”呢?如果大家有去做實驗的話,可以知道服務端收到的是“asdasdasd”,為什么會這樣呢?按正常的話,服務端收到的應該是“asd”,剩下的兩個asd要不就是收不到要不就是下次循環收到,怎么會一次性收到“asdasdasd”呢?如果要說罪魁禍首的話就是那個休眠10秒,導致數據粘包了!
服務端代碼:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 512 #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while (0) int main() { //創建套接字 int m_sockfd = socket(AF_INET, SOCK_STREAM, 0); if (m_sockfd < 0) { ERR_EXIT("create socket fail"); } //初始化socket元素 struct sockaddr_in server_addr; int server_len = sizeof(server_addr); memset(&server_addr, 0, server_len); server_addr.sin_family = AF_INET; //server_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //用這個寫法也可以 server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(39002); //綁定文件描述符和服務器的ip和端口號 int m_bindfd = bind(m_sockfd, (struct sockaddr *)&server_addr, server_len); if (m_bindfd < 0) { ERR_EXIT("bind ip and port fail"); } //進入監聽狀態,等待用戶發起請求 int m_listenfd = listen(m_sockfd, 20); if (m_listenfd < 0) { ERR_EXIT("listen client fail"); } //定義客戶端的套接字,這里返回一個新的套接字,后面通信時,就用這個m_connfd進行通信 struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len); //這里休眠了10秒 sleep(10); //接收客戶端數據 char buffer[BUF_SIZE]; recv(m_connfd, buffer, sizeof(buffer)-1, 0); printf("server recv:%s\n", buffer); strcat(buffer, "+ACK"); send(m_connfd, buffer, strlen(buffer), 0); //關閉套接字 close(m_connfd); close(m_sockfd); return 0; }
客戶端代碼:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 512 #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while (0) int main() { //創建套接字 int m_sockfd = socket(AF_INET, SOCK_STREAM, 0); if (m_sockfd < 0) { ERR_EXIT("create socket fail"); } //服務器的ip為本地,端口號 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr("81.68.140.74"); server_addr.sin_port = htons(39002); //向服務器發送連接請求 int m_connectfd = connect(m_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); if (m_connectfd < 0) { ERR_EXIT("connect server fail"); } //發送並接收數據 char buffer[BUF_SIZE] = "asd"; int datasize = strlen(buffer); send(m_sockfd, buffer, datasize, 0); send(m_sockfd, buffer, datasize, 0); send(m_sockfd, buffer, datasize, 0); recv(m_sockfd, buffer, sizeof(buffer)-1, 0); printf("client recv:%s\n", buffer); //斷開連接 close(m_sockfd); return 0; }
以上代碼在Linux平台上運行之后就會出現粘包現象,大家可以把以上代碼復制去驗證看看。
2、粘包拆包的幾種情況
這個問題在socket網絡編程中非常的常見,數據不僅會粘包,還會被拆包,就是一段數據被拆成兩部分。那么拆包、粘包問題產生的原因都有哪些呢
- 要發送的數據大於TCP發送緩沖區剩余空間大小,將會發生拆包。
- 待發送數據大於MSS(最大報文長度),TCP在傳輸前將進行拆包。
- 要發送的數據小於TCP發送緩沖區的大小,TCP將多次寫入緩沖區的數據一次發送出去,將會發生粘包。
- 接收數據端的應用層沒有及時讀取接收緩沖區中的數據,將發生粘包。
而數據之所以會發送粘包拆包的根本原因是TCP的數據包是流的方式傳輸的,就像水流一樣,沒有一個分界的東西。
3、處理粘包拆包的方法
處理拆包、粘包問題的方法:
那么最關鍵的就是我們該怎么處理粘包拆包問題呢?因為這個問題在socket無法很好的處理,所以必須要在應用層上面處理,所以就需要要求大家在封裝網絡通信接口的時候要自己實現粘包拆包的處理方法。解決問題的關鍵在於如何給每個數據包添加邊界信息,常用的方法有如下幾個:
- 可以在數據包之間設置邊界,如添加特殊符號,這樣,接收端通過這個邊界就可以將不同的數據包拆分開。
- 發送端將每個數據包封裝為固定長度(不夠的可以通過補0填充),這樣接收端每次從接收緩沖區中讀取固定長度的數據就自然而然的把每個數據包拆分開來。
- 發送端給每個數據包添加包首部,首部中應該至少包含數據包的長度,這樣接收端在接收到數據后,通過讀取包首部的長度字段,便知道每一個數據包的實際長度了。
第1種和第2種方法都會存在一些誤差,沒有辦法很好處理好粘包拆包,所以一般的方法都是采用第3種。以下我先給出代碼,然后再結合代碼分析第3種粘包拆包的處理方式。
3.1、服務端代碼
#include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdlib.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include "protocol.h" #define BUF_SIZE 512 #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while (0) int main() { //創建套接字 int m_sockfd = socket(AF_INET, SOCK_STREAM, 0); if (m_sockfd < 0) { ERR_EXIT("create socket fail"); } //初始化socket元素 struct sockaddr_in server_addr; int server_len = sizeof(server_addr); memset(&server_addr, 0, server_len); server_addr.sin_family = AF_INET; //server_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //用這個寫法也可以 server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(39002); //綁定文件描述符和服務器的ip和端口號 int m_bindfd = bind(m_sockfd, (struct sockaddr *)&server_addr, server_len); if (m_bindfd < 0) { ERR_EXIT("bind ip and port fail"); } //進入監聽狀態,等待用戶發起請求 int m_listenfd = listen(m_sockfd, 20); if (m_listenfd < 0) { ERR_EXIT("listen client fail"); } //定義客戶端的套接字,這里返回一個新的套接字,后面通信時,就用這個m_connfd進行通信 struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len); //接收客戶端數據 char recv_buffer[10000]; //接收數據的buffer memset(recv_buffer, 0, sizeof(recv_buffer)); //初始化接收buffer while (1) { if (m_connfd < 0) { m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len); printf("client accept success again!!!\n"); } //休眠10秒才能有粘包現象出現 sleep(10); int nrecvsize = 0; //一次接收到的數據大小 int sum_recvsize = 0; //總共收到的數據大小 int packersize; //數據包長度 int disconn = false; //先從緩存池取出包頭 while (sum_recvsize != sizeof(NetPacketHeader)) { nrecvsize = recv(m_connfd, recv_buffer + sum_recvsize, sizeof(NetPacketHeader) - sum_recvsize, 0); if (nrecvsize == 0) { close(m_connfd); m_connfd = -1; printf("client lose connection!!!\n"); disconn = true; break; } sum_recvsize += nrecvsize; } if (disconn) { continue; } NetPacketHeader *phead = (NetPacketHeader *)recv_buffer; packersize = phead->wDataSize; //客戶端發過來的數據包長度(包含包頭) //從緩沖池中取出數據(不包含包頭) while (sum_recvsize != packersize) { nrecvsize = recv(m_connfd, recv_buffer + sum_recvsize, packersize - sum_recvsize, 0); if (nrecvsize == 0) { close(m_connfd); m_connfd = -1; printf("client lose connection!!!\n"); disconn = true; break; } else if (nrecvsize < 0) { ERR_EXIT("recv fail"); } printf("server recv:%s, size:%d\n", recv_buffer + sum_recvsize, nrecvsize); sum_recvsize += nrecvsize; } if (disconn) { continue; } } //關閉套接字 close(m_connfd); close(m_sockfd); return 0; }
3.2、客戶端代碼
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include "protocol.h" #define BUF_SIZE 512 #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while (0) int main() { //創建套接字 int m_sockfd = socket(AF_INET, SOCK_STREAM, 0); if (m_sockfd < 0) { ERR_EXIT("create socket fail"); } //服務器的ip為本地,端口號 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr("81.68.140.74"); server_addr.sin_port = htons(39002); //向服務器發送連接請求 if (connect(m_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { ERR_EXIT("connect server fail"); } //發送並接收數據 char data_buffer[BUF_SIZE] = "asd"; int datasize = strlen(data_buffer); NetPacket send_packet; //數據包 send_packet.Header.wDataSize = datasize + sizeof(NetPacketHeader); //數據包大小 memcpy(send_packet.Data, data_buffer, datasize); //數據拷貝 send(m_sockfd, &send_packet, send_packet.Header.wDataSize, 0); send(m_sockfd, &send_packet, send_packet.Header.wDataSize, 0); send(m_sockfd, &send_packet, send_packet.Header.wDataSize, 0); //斷開連接 close(m_sockfd); return 0; }
3.3、公用的部分
//protocol.h #ifndef _PROTOCOL_H #define _PROTOCOL_H #define NET_PACKET_DATA_SIZE 5000 /// 網絡數據包包頭 struct NetPacketHeader { unsigned short wDataSize; ///< 數據包大小,包含包頭的長度和數據長度 }; /// 網絡數據包 struct NetPacket { NetPacketHeader Header; /// 包頭 unsigned char Data[NET_PACKET_DATA_SIZE]; /// 數據 }; #endif
首先定義一個新的文件protocol.h,主要是客戶端和服務端共用的部分,包含數據包和包頭的結構體定義。
然后客戶端發送的時候記得發送數據體的長度是數據加包頭的長度。
而在接收端的代碼則稍微要花點心思了。首先接收端需要分兩次來從緩沖池中接收數據,先取出長度為包頭的數據,然后去取數據體的部分的時候一定要記得每次從緩沖區取數據的偏移量。
這樣子就可以正確的處理好粘包拆包的問題了。當然從服務端向客戶端發送數據的話,兩者則是顛倒過來,這里就不在說明了。最后希望大家可以從這邊文章獲得一點收獲,有什么疑問歡迎在下方評論說明。
更多精彩內容,請關注同名公眾:一點月光(alittle-moon)