TCP/IP

TCP粘包拆包处理

Posted by Liangjf on April 10, 2019

TCP粘包拆包处理

TCP是可靠的吗?可能你马上会回答:TCP是面向连接的,是可靠的。这个回答可能出现在TCP和UDP的区别问题中回答最多了。

这句话是没错的。但是这句话的可靠是指TCP协议会保证传输的有序性,尽量不丢包。

但是因为TCP是无边界,也就是以流的形式在网络中传输的。而且因为需要为其保证上面说的可靠性,最小单元(MTU)Nagle算法接收端应用层没及时读取接收缓冲区中的数据等都会导致接收的TCP包是不正常的

接收的消息有几种情况:

  • 1.正常

  • 2.拆包

  • 2.粘包

算法实现

	/**
	 * 基于固定消息头的粘包拆包处理算法
	 */
	
	//接收缓存大小
	#define RECV_BUFF_SZIE 102400
	
	//接收缓存
	char g_szRecv[RECV_BUFF_SZIE];
	
	//命令类型
	enum CMD
	{
		CMD_LOGIN,
		CMD_ERROR
	};

	//消息头
	struct DataHeader
	{
		DataHeader()
		{
			dataLength = sizeof(DataHeader);
			cmd = CMD_ERROR;
		}
		short dataLength;
		short cmd;
	};
	
	//真正的数据消息
	struct DataLogin : DataHeader
	{
		DataLogin()
		{
			dataLength = sizeof(DataLogin);
			cmd = CMD_LOGIN;
		}
	
		char data[100];
	};
	
	/**
	 * 封装一个客户端fd数据类型
	 * socket fd+接收缓冲区
	 */
	class ClientSocket 
	{
	public:
		ClientSocket(int sockfd = -1):m_sockfd(sockfd), m_lastPos(0)
		{
			memset(m_szMsgBuf, 0, sizeof(m_szMsgBuf));
		}
	
	public:
		int sockfd()
		{
			return m_sockfd;
		}
	
		char* msgBuf()
		{
			return m_szMsgBuf;
		}
	
		int getLastPos()
		{
			return m_lastPos;
		}
		void setLastPos(int pos)
		{
			m_lastPos = pos;
		}
	
	private:
		int m_sockfd;	//socket fd
		char m_szMsgBuf[RECV_BUFF_SZIE * 10];	//每个客户端socket fd有单独的接收缓冲区
		int m_lastPos;	//当前接收缓冲区的最后位置
	};
	
	/**
	 * 接受数据函数
	 * @param  pClient 客户端结构体
	 * @return         0-正常  -1-异常
	 */
	int RecvData(ClientSocket* pClient)
	{
		//接收数据
		int nLen = (int)recv(pClient->sockfd(), g_szRecv, RECV_BUFF_SZIE, 0);
		if (nLen <= 0)
		{
			printf("recv error Socket=%d\n", pClient->sockfd());
			return -1;
		}
	
		//把接收到的数据拷贝到客户端缓存区,以便判断处理和不影响接收缓冲区
		memcpy(pClient->msgBuf() + pClient->getLastPos(), g_szRecv, nLen);
	
		//重设客户端接收缓存区指向位置
		pClient->setLastPos(pClient->getLastPos() + nLen);
	
		//只要接收缓冲区的长度大于消息头就一直处理数据(固定共4字节。数据长度变量[short]+命令变量[short])
		while (pClient->getLastPos() >= sizeof(DataHeader))
		{
			//判断接收缓冲区的长度是否大于消息头附带的数据体的长度
			DataHeader* header = (DataHeader*)pClient->msgBuf();
			if (pClient->getLastPos() >= header->dataLength)
			{
				//到这里表明肯定是一个完整的数据包(消息头+数据体)
				//计算出接受缓冲区比对应接收类型(消息头附带了)的完整数据包多余的字节
				int nSize = pClient->getLastPos() - header->dataLength;
	
				//为了演示,这里仅仅是对命令做处理,没有对数据做处理
				OnNetMsg(pClient->sockfd(),header);
	
				//因已消费客户端缓冲区的一个完整数据包,把后面的数据往前挪对应数据包大小的位置
				memcpy(pClient->msgBuf(), pClient->msgBuf() + header->dataLength, nSize);
	
				//重设新的客户端接收缓冲区最后数据的位置
				pClient->setLastPos(nSize);
			}
			else 
			{
				break;
			}
		}
		return 0;
	}