計算機通訊之謎,帶你徹底理解socket網路程式設計(五)

_kiwi_發表於2020-11-05

目錄

1、出現粘包拆包的原因

2、粘包拆包的幾種情況

3、處理粘包拆包的方法

3.1、服務端程式碼

3.2、客戶端程式碼

3.3、公用的部分


今天和大家講一下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

 

 

相關文章