怎么解决TCP网络传输「粘包」问题?

21 篇文章 1 订阅

粘包的概念

粘包: 多次数据发送, 收尾相连, 接收端接收的时候不能正确区分第一次发 送多少, 第二次发送多少.

粘包原因:

所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
发送端原因: 由于TCP协议本身的机制(面向连接的可靠地协议-三次握手机制)客户端与服务器会维持一个连接(Channel),数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包.
接收端原因: 服务器在接收到数据库后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象。

粘包问题分析和解决?

  • 方案1: 包头+数据

    如4位的数据长度+数据 -----------> 00101234567890

    其中0010表示数据长度, 1234567890表示10个字节长度的数据.

    另外, 发送端和接收端可以协商更为复杂的报文结构, 这个报文结 构就相当于双方约定的一个协议.

  • 方案2: 添加结尾标记.

    如结尾最后一个字符为\n或者 $等.

  • 方案3: 数据包定长
    如发送方和接收方约定, 每次只发送128个字节的内容, 接收方接收定长128个字节就可以了.

代码

memcpy函数

C 库函数

void *memcpy(void *str1, const void *str2, size_t n)

从存储区 str2 复制 n 个字节到存储区 str1。

参数

  • str1 – 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。
  • str2 – 指向要复制的数据源,类型强制转换为 void* 指针。
  • n – 要被复制的字节数。

返回值

该函数返回一个指向目标存储区 str1 的指针。

实例

下面的实例演示了 memcpy() 函数的用法。

// 将字符串复制到数组 dest 中
#include <stdio.h>
#include <string.h>
 
int main ()
{
   const char src[50] = "http://www.runoob.com";
   char dest[50];
 
   memcpy(dest, src, strlen(src)+1);
   printf("dest = %s\n", dest);
   
   return(0);
}

发送

int TcpSocket::sendMsg(char * sendData, int dataLen, int timeout)
{
	int 	ret = 0;

	if (sendData == NULL || dataLen <= 0)
	{
		ret = ParamError;
		return ret;
	}

	ret = writeTimeout(timeout);
	if (ret == 0)
	{
		int writed = 0;
		unsigned char *netdata = (unsigned char *)malloc(dataLen + 4);
		if (netdata == NULL)
		{
			ret = MallocError;
			printf("func sckClient_send() mlloc Err:%d\n ", ret);
			return ret;
		}
		int netlen = htonl(dataLen);
		memcpy(netdata, &netlen, 4);
		memcpy(netdata + 4, sendData, dataLen);

		writed = writen(netdata, dataLen + 4);
		if (writed < (dataLen + 4))
		{
			if (netdata != NULL)
			{
				free(netdata);
				netdata = NULL;
			}
			return writed;
		}

		if (netdata != NULL)  //wangbaoming 20150630 modify bug
		{
			free(netdata);
			netdata = NULL;
		}
	}

	if (ret < 0)
	{
		//失败返回-1,超时返回-1并且errno = ETIMEDOUT
		if (ret == -1 && errno == ETIMEDOUT)
		{
			ret = TimeoutError;
			printf("func sckClient_send() mlloc Err:%d\n ", ret);
			return ret;
		}
		return ret;
	}

	return ret;
}

接收

int TcpSocket::recvMsg(char ** recvData, int & recvLen, int timeout)
{
	int		ret = 0;

	if (recvData == NULL || recvLen == NULL)
	{
		ret = ParamError;
		printf("func sckClient_rev() timeout , err:%d \n", TimeoutError);
		return ret;
	}

	ret = readTimeout(timeout); //bugs modify bombing
	if (ret != 0)
	{
		if (ret == -1 || errno == ETIMEDOUT)
		{
			ret = TimeoutError;
			return ret;
		}
		else
		{
			return ret;
		}
	}

	int netdatalen = 0;
	ret = readn(&netdatalen, 4); //读包头 4个字节
	if (ret == -1)
	{
		//printf("func readn() err:%d \n", ret);
		return ret;
	}
	else if (ret < 4)
	{
		ret = PeerCloseError;
		//printf("func readn() err peer closed:%d \n", ret);
		return ret;
	}

	int n;
	n = ntohl(netdatalen);
	char* tmpBuf = (char *)malloc(n + 1);
	if (tmpBuf == NULL)
	{
		ret = MallocError;
		return ret;
	}


	ret = readn(tmpBuf, n); //根据长度读数据
	if (ret == -1)
	{
		//printf("func readn() err:%d \n", ret);
		return ret;
	}
	else if (ret < n)
	{
		ret = PeerCloseError;
		//printf("func readn() err peer closed:%d \n", ret);
		return ret;
	}

	*recvData = tmpBuf;
	recvLen = n;
	tmpBuf[n] = '\0'; //多分配一个字节内容,兼容可见字符串 字符串的真实长度仍然为n

	return 0;
}

总结

粘包问题,最重要的是理解什么是粘包。粘包这个概念。

参考

完整代码
TCP粘包概念、产生原因及解决方法
知乎

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值