粘包的概念
粘包: 多次数据发送, 收尾相连, 接收端接收的时候不能正确区分第一次发 送多少, 第二次发送多少.
粘包原因:
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
• 发送端原因: 由于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;
}
总结
粘包问题,最重要的是理解什么是粘包。粘包这个概念。