手把手教你了解并解决TCP粘包问题

2023-03-26 14:54:39 2682人已围观 61已点赞 26人已收藏

简介本文向大家介绍一个C++实战项目:手把手教你了解并解决TCP粘包问题。通过该实战项目可以了解TCP粘包问题产生的原因及解决方式,具有一定的C++实战价值,感兴趣的朋友可以参考一下。

什么时候需要考虑粘包问题

1).如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题(因为只有一种包结构,类似于http协议)。关闭连接主要要双方都发送close连接(参考tcp关闭协议)。如:A需要发送一段字符串给B,那么A与B建立连接,然后发送双方都默认好的协议字符如"hello give me sth abour yourself",然后B收到报文后,就将缓冲区数据接收,然后关闭连接,这样粘包问题不用考虑到,因为大家都知道是发送一段字符;

2).如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包;

3).如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构:

 1) "hello give me sth abour yourself" 
 2) "Don't give me sth abour yourself" 

那这样的话,如果发送方连续发送这个两个包出去,接收方一次接收可能会是

hello give me sth abour yourselfDon't give me sth abour yourself

这样接收方就傻了,到底是要干嘛?不知道,因为协议没有规定这么诡异的字符串,所以要处理把它分包,怎么分也需要双方组织一个比较好的包结构,所以一般可能会在头加一个数据长度之类的包,以确保接收。

粘包问题出现原因

在流传输中出现,UDP不会出现粘包,因为它有消息保护边界。

  • 1 发送端需要等缓冲区满才发送出去,造成粘包
  • 2 接收方不及时接收缓冲区的包,造成多个包接收

粘包问题解决

为了避免粘包现象,可采取以下几种措施:

一是对于发送方引起的粘包现象,用户可通过编程设置来避免, TCP提供了强制数据立即传送的操作指令push ,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;

二是对于接收方引起的粘包,则可 通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据 ,从而尽量避免出现粘包现象;

三是由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。

以上提到的三种措施,都有其不足之处。

第一种编程设置方法虽然可以避免发送方引起的粘包,但它关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使用。

第二种方法只能减少出现粘包的可能性,但 并不能完全避免粘包 ,当发送频率较高时,或由于网络突发可能使某个时间段数据包到达接收方较快,接收方还是有可能来不及接收,从而导致粘包。

第三种方法虽然避免了粘包,但 应用程序的效率较低,对实时应用的场合不适合

更为简洁的说法:

定包长

包尾加\r\n

包头加包体长度

网上说法:

个人比较喜欢的一种做法是给一帧数据加帧头帧尾,然后接收方不断接受并缓存收到的数据,根据帧头帧尾分离出一帧完整的数据,再分离各字段得到数据。

TCP粘包模拟程序

首先,我们先写个简单的TCP服务器端,实现方式可参考本站文章: 《C++多线程实现TCP服务器端同时和多个客户端通信》 ,也可在文末下载源码,核心代码如下:

// 客户端通讯处理线程
DWORD WINAPI ThreadFun(LPVOID lpThreadParameter)
{
	SOCKET sock = (SOCKET)lpThreadParameter;
	cout << "客户端 " << sock << " 连接成功!" << endl;
	
	::Sleep(1000);
	char buf[] = "服务器:C++实战网(www.cppszw.com)\n";

	// 这里多次发送以便实现产生TCP粘包问题
	for (int i = 1; i < 11; ++i)
	{
		if (SendMsg(sock, buf))
		{
			printf("%d:发送数据成功......\n", i);		
		}
		else
		{
			printf("%d:发送数据失败......\n", i);
		}
	}

	while (1)
	{
		char recvBuf[100];
		memset(recvBuf, 0, sizeof(recvBuf));
		// 接收数据
		int ret = recv(sock, recvBuf, sizeof(recvBuf), 0);
		if (ret == SOCKET_ERROR || ret == 0)
		{
			cout << sock << "断开了连接!" << endl;
			break;
		}
		else
		{
			cout << sock << "  收到数据: " << recvBuf << endl;
		}
	}
	return 0;
}

// 发送数据
bool SendMsg(SOCKET sock, char* pMsg)
{
	if (INVALID_SOCKET == sock)
	{
		return false;
	}
	if (pMsg[0] == '\0')
	{
		return false;
	}

	bool bRet = true;
	if (SOCKET_ERROR == send(sock, pMsg, strlen(pMsg) + 1, 0))
	{
		bRet = false;
	}

	return bRet;
}

运行后截图如下:

C++TCP粘包问题解决,C++网络编程

然后,我们写个简单的TCP客户端,实现方式可参考本站文章: 《MFC实现一个简单TCP服务端及客户端》 ,也可在文末下载源码,接收部分代码如下:

// 接收TCP消息
void CTCPClientDemoDlg::TCPReceiveService()
{
	char recv_buf[MSG_BUF_SIZE];
	while (m_tcpClientHelper.IsRunning())
	{
		memset(recv_buf, 0, MSG_BUF_SIZE);
		if (m_tcpClientHelper.RecvData(recv_buf, MSG_BUF_SIZE))
		{
			WriteLog("TCP消息 接收:%s", recv_buf);
		}
		else
		{
			WriteLog("系统检测到TCP异常断开...");
			DisConnect();
		}
	}
}

运行后截图如下:

C++TCP粘包问题解决,C++网络编程

点击连接,可以看到如下结果:

C++TCP粘包问题解决,C++网络编程

服务端显示已成功发送了10次数据,而客户端只接收显示了2次,原因就是 TCP出现了粘包 ,服务端代码中模拟产生了TCP粘包问题:

// 这里多次发送以便实现产生TCP粘包问题
for (int i = 1; i < 11; ++i)
{
	if (SendMsg(sock, buf))
	{
		printf("%d:发送数据成功......\n", i);		
	}
	else
	{
		printf("%d:发送数据失败......\n", i);
	}
}

为了验证确实是客户端因为 TCP出现了粘包 导致数据收取错误,我们采用Wireshark抓包工具抓包结果如下:

C++TCP粘包问题解决,C++网络编程

可见,该模拟程序中TCP确实出现了粘包问题。

TCP粘包解决

只需要在TCP包头部增加一个结构来标识消息的长度即可,这里定义包头结构如下:

C++TCP粘包问题解决,C++网络编程

这样,我们修改TCP服务端发送消息代码如下:

// 发送数据
bool SendMsg(SOCKET sock, char* pMsg)
{
	if (INVALID_SOCKET == sock)
	{
		return false;
	}
	if (pMsg[0] == '\0')
	{
		return false;
	}

	// 消息体字节长度
	int iMsgLen = strlen(pMsg) + 1;
	BYTE sendMsgLen[4];
	memset(sendMsgLen, 0, 4);
	sendMsgLen[0] = (BYTE)(iMsgLen);
	sendMsgLen[1] = (BYTE)(iMsgLen >> 8);
	sendMsgLen[2] = (BYTE)(iMsgLen >> 16);
	sendMsgLen[3] = (BYTE)(iMsgLen >> 24);
	int iBytesLen = MSG_HEAD_SIZE + iMsgLen;
	// 字节拼接
	BYTE* pBData = new BYTE[iBytesLen];
	for (int i = 0; i < 6; ++i)
	{
		pBData[i] = m_sendHead[i];
	}
	for (int i = 0; i < 4; ++i)
	{
		pBData[6 + i] = sendMsgLen[i];
	}
	for (int i = 0; i < iMsgLen; ++i)
	{
		pBData[MSG_HEAD_SIZE + i] = pMsg[i];
	}

	bool bRet = true;
	if (SOCKET_ERROR == send(sock, (char*)pBData, iBytesLen, 0))
	{
		bRet = false;
	}
	delete[] pBData;
	return bRet;
}

客户端接收部分代码如下:

if (recv_buf[0] == 'c'&&recv_buf[1] == 'p'&&recv_buf[2] == 'p'&&recv_buf[3] == 's'&&recv_buf[4] == 'z'&&recv_buf[5] == 'w')
{
	int iPos = 0;
	while (iPos <= MSG_BUF_SIZE)
	{
		// 获取当前消息长度
		int iMsgLen = 0;
		memcpy(&iMsgLen, &recv_buf[iPos + 6], 4);
		int iPacketSize = iMsgLen + MSG_HEAD_SIZE;
		if (iPacketSize < MSG_BUF_SIZE - iPos)
		{
			ParseTCPMsg(recv_buf, iPos, iMsgLen);
			// 数据完整
			if (recv_buf[iPos + iPacketSize] == '\0')
			{
				break;
			}
			// 粘包 作拆包处理
			else
			{
				iPos += iPacketSize;
			}
		}
		else
		{
			// 拷贝到不完整包
			memcpy(&packet[iLen], &recv_buf[iPos], MSG_BUF_SIZE - iPos);
			iLen += (MSG_BUF_SIZE - iPos);
			break;
		}
	}
}

运行结果如下:

C++TCP粘包问题解决,C++网络编程

可见,TCP粘包问题就完美解决了。

源码下载
  • 最近更新:   2022-07-20开发环境:   Visual Studio 2015
  • 源码大小:   294.73KB下载次数:  20 

更多为你推荐