项目(百万并发网络通信架构)7.2---解决客户端粘包现象(设置接收缓冲区)

一、客户端粘包现象

  • 在前面一篇文章中我们解决了客户端与服务端网络缓冲区可能会溢出而导致的网络阻塞现象,但是仍然以后一个问题没有解决,就是粘包现象。我们的数据接收端一次性从网络缓冲区中接收上万字节的数据,这些数据是由不同的数据包组成的,因此包与包之间粘在一起。本文主要讲解修改客户端代码,使其能够正常处理粘包现象

二、解决粘包现象:为程序设置双缓冲

  • 在前面文章中我们只为程序设置了一个接收缓冲区,现在我们为程序设置两个接收缓冲区:
    • 第一缓冲区(接收缓冲区):_recvBuff,用来存储从网络缓冲区中接收的数据
    • 第二缓冲区(消息缓冲区):_recvMsgBuff,将第一缓冲区中的数据拷贝到这个缓冲区中,并在这个缓冲区中对数据进行处理(粘包拆包处理)
    • _lastPos:用来标识_recvMsgBuff中数据末尾的位置,当对_recvMsgBuff中添加或删除数据时,其的值会相应的变化

项目(百万并发网络通信架构)7.2---解决客户端粘包现象(设置接收缓冲区)_第1张图片

三、客户端代码修订

  • 修改EasyTcpClient的定义,为其添加两个三个成员变量:
class EasyTcpClient
{
//其余代码同上
public:
	EasyTcpClient() :_sock(INVALID_SOCKET), _lastPos(0) {
		memset(_recvBuff, 0, sizeof(_recvBuff));
		memset(_recvMsgBuff, 0, sizeof(_recvMsgBuff));
	}
private:
	SOCKET _sock;
#define RECV_BUFF_SIZE 10240
	char _recvBuff[RECV_BUFF_SIZE];        //第一缓冲区(接收缓冲区)
	char _recvMsgBuff[RECV_BUFF_SIZE * 10];//第二缓冲区(消息缓冲区)
	int _lastPos;
};
  • 然后是对RecvData()函数的重新定义:
int EasyTcpClient::RecvData()
{
	int _nLen = recv(_sock, _recvBuff, RECV_BUFF_SIZE, 0);
	if (_nLen < 0) {
		std::cout << ":recv函数出错!" << std::endl;
		return -1;
	}
	else if (_nLen == 0) {
		std::cout << ":接收数据失败,服务端已关闭!" << std::endl;
		return -1;
	}
	std::cout << "_nLen=" << _nLen << std::endl;
	
	//将获取的数据拷贝到消息缓冲区
	memcpy(_recvMsgBuff + _lastPos, _recvBuff, _nLen);
	_lastPos += _nLen;

	//如果_recvMsgBuff中的数据长度大于等于DataHeader
	while (_lastPos >= sizeof(DataHeader))
	{
		DataHeader* header = (DataHeader*)_recvMsgBuff;
		//如果_lastPos的位置大于等于一个数据包的长度,那么就会这个数据包进行处理
		if (_lastPos >= header->dataLength)
		{
			//剩余未处理消息缓冲区的长度
			int nSize = _lastPos - header->dataLength;
			//处理网络消息
			OnNetMessage(header);
			//处理完成之后,将_recvMsgBuff中剩余未处理部分的数据前移
			memcpy(_recvMsgBuff, _recvMsgBuff + header->dataLength, nSize);
			_lastPos = nSize;
		}
		else {
			//消息缓冲区剩余数据不够一条完整消息
			break;
		}
	}
	return 0;
}

四、客户端最终完整代码

  • 客户端头文件EasyTcpClient.hpp最终完整代码如下:
#ifndef _EasyTcpClient_hpp_
#define _EasyTcpClient_hpp_

#ifdef _WIN32
	#define WIN32_LEAN_AND_MEAN
	#define _WINSOCK_DEPRECATED_NO_WARNINGS //for inet_pton()
	#define _CRT_SECURE_NO_WARNINGS
	#include 
	#include 
	#pragma comment(lib, "ws2_32.lib")
#else
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	//在Unix下没有这些宏,为了兼容,自己定义
	#define SOCKET int
	#define INVALID_SOCKET  (SOCKET)(~0)
	#define SOCKET_ERROR            (-1)
#endif
#include 
#include 
#include 
#include "MessageHeader.hpp"

using namespace std;

class EasyTcpClient
{
public:
	EasyTcpClient() :_sock(INVALID_SOCKET), _lastPos(0) {
		memset(_recvBuff, 0, sizeof(_recvBuff));
		memset(_recvMsgBuff, 0, sizeof(_recvMsgBuff));
	}
	virtual ~EasyTcpClient() {
		CloseSocket();
	}
public:
	//判断当前客户端是否在运行
	bool isRun() { return _sock != INVALID_SOCKET; }
	//初始化socket
	void InitSocket();
	//连接服务器
	int ConnectServer(const char* ip, unsigned int port);
	//关闭socket
	void CloseSocket();
	//处理网络消息
	bool Onrun();

	/*
		使用RecvData接收任何类型的数据,
		然后将消息的头部字段传递给OnNetMessage()函数中,让其响应不同类型的消息
	*/
	//接收数据
	int RecvData();
	//响应网络消息
	virtual void OnNetMessage(DataHeader* header);

	//发送数据
	int SendData(DataHeader* header);
private:
	SOCKET _sock;
#define RECV_BUFF_SIZE 10240
	char _recvBuff[RECV_BUFF_SIZE];        //第一缓冲区(接收缓冲区),用来存储从网络缓冲区中接收的数据
	char _recvMsgBuff[RECV_BUFF_SIZE * 10];//第二缓冲区(消息缓冲区),将第一缓冲区中的数据存储在这个缓冲区中,并在这个缓冲区中对数据进行处理(粘包拆包处理)
	int _lastPos;//用来标识当前消息缓冲区中数据的结尾位置
};

void EasyTcpClient::InitSocket()
{
	//如果之前有连接了,关闭旧连接,开启新连接
	if (isRun())
	{
		std::cout << ":关闭旧连接,建立了新连接" << std::endl;
		CloseSocket();
	}

#ifdef _WIN32
	WORD ver = MAKEWORD(2, 2);
	WSADATA dat;
	WSAStartup(ver, &dat);
#endif

	_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (INVALID_SOCKET == _sock) {
		std::cout << "ERROR:建立socket失败!" << std::endl;
	}
	else {
		std::cout << ":建立socket成功!" << std::endl;
	}
}

int EasyTcpClient::ConnectServer(const char* ip, unsigned int port)
{
	if (!isRun())
	{
		InitSocket();
	}

	//声明要连接的服务端地址(注意,不同平台的服务端IP地址也不同)
	struct sockaddr_in _sin = {};
#ifdef _WIN32
	_sin.sin_addr.S_un.S_addr = inet_addr(ip);
#else
	_sin.sin_addr.s_addr = inet_addr(ip);
#endif
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(port);

	//连接服务端
	int ret = connect(_sock, (struct sockaddr*)&_sin, sizeof(_sin));
	if (SOCKET_ERROR == ret) {
		std::cout << ":连接服务端(" << ip << "," << port << ")失败!" << std::endl;
	}
	else {
		std::cout << ":连接服务端(" << ip << "," << port << ")成功!" << std::endl;
	}
	
	return ret;
}

void EasyTcpClient::CloseSocket()
{
	if (_sock != INVALID_SOCKET)
	{
#ifdef _WIN32
		closesocket(_sock);
		WSACleanup();
#else
		close(_sock);
#endif
		_sock = INVALID_SOCKET;
	}
}

bool EasyTcpClient::Onrun()
{
	if (isRun())
	{
		fd_set fdRead;
		FD_ZERO(&fdRead);
		FD_SET(_sock, &fdRead);

		struct timeval t = { 0,0 };
		int ret = select(_sock + 1, &fdRead, NULL, NULL, &t);
		if (ret < 0)
		{
			std::cout << ":select出错!" << std::endl;
			return false;
		}
		if (FD_ISSET(_sock, &fdRead)) //如果服务端有数据发送过来,接收显示数据
		{
			FD_CLR(_sock, &fdRead);
			if (-1 == RecvData())
			{
				std::cout << ":数据接收失败,或服务端已断开!" << std::endl;
				CloseSocket();
				return false;
			}
		}
		return true;
	}
	return false;
}

int EasyTcpClient::RecvData()
{
	int _nLen = recv(_sock, _recvBuff, RECV_BUFF_SIZE, 0);
	if (_nLen < 0) {
		std::cout << ":recv函数出错!" << std::endl;
		return -1;
	}
	else if (_nLen == 0) {
		std::cout << ":接收数据失败,服务端已关闭!" << std::endl;
		return -1;
	}
	std::cout << "_nLen=" << _nLen << std::endl;
	
	//将获取的数据拷贝到消息缓冲区
	memcpy(_recvMsgBuff + _lastPos, _recvBuff, _nLen);
	_lastPos += _nLen;

	//如果_recvMsgBuff中的数据长度大于等于DataHeader
	while (_lastPos >= sizeof(DataHeader))
	{
		DataHeader* header = (DataHeader*)_recvMsgBuff;
		//如果_lastPos的位置大于等于一个数据包的长度,那么就会这个数据包进行处理
		if (_lastPos >= header->dataLength)
		{
			//剩余未处理消息缓冲区的长度
			int nSize = _lastPos - header->dataLength;
			//处理网络消息
			OnNetMessage(header);
			//处理完成之后,将_recvMsgBuff中剩余未处理部分的数据前移
			memcpy(_recvMsgBuff, _recvMsgBuff + header->dataLength, nSize);
			_lastPos = nSize;
		}
		else {
			//消息缓冲区剩余数据不够一条完整消息
			break;
		}
	}
	return 0;
}

void EasyTcpClient::OnNetMessage(DataHeader* header)
{
	switch (header->cmd)
	{
	case CMD_LOGIN_RESULT:   //如果返回的是登录的结果
	{
		LoginResult* loginResult = (LoginResult*)header;
		std::cout << ",收到服务端数据:CMD_LOGIN_RESULT,数据长度:" << loginResult->dataLength << ",结果为:" << loginResult->result << std::endl;
	}
	break;
	case CMD_LOGOUT_RESULT:  //如果是退出的结果
	{
		LogoutResult* logoutResult = (LogoutResult*)header;
		std::cout << ",收到服务端数据:CMD_LOGOUT_RESULT,数据长度:" << logoutResult->dataLength << ",结果为:" << logoutResult->result << std::endl;
	}
	break;
	case CMD_NEW_USER_JOIN:  //有新用户加入
	{
		NewUserJoin* newUserJoin = (NewUserJoin*)header;
		std::cout << ",收到服务端数据:CMD_NEW_USER_JOIN,数据长度:" << newUserJoin->dataLength << ",新用户Socket为:" << newUserJoin->sock << std::endl;
	}
	break;
	case CMD_ERROR:  //错误消息
	{

		std::cout << ",收到服务端数据:CMD_ERROR,数据长度:" << header->dataLength << std::endl;
	}
	break;
	default:
	{
		std::cout << ",收到服务端数据:未知类型的消息,数据长度:" << header->dataLength << std::endl;
	}
	}
}

int EasyTcpClient::SendData(DataHeader* header) 
{
	if (isRun() && header)
	{
		return send(_sock, (const char*)header, header->dataLength, 0);
	}
	return SOCKET_ERROR;
}

#endif // !_EasyTcpClient_hpp_

五、测试

  • 客户端代码如下,一直向服务端发送Login类型的消息,因此服务端会给自己回送LoginResult类型(1032KB)的消息
#include "EasyTcpClient.hpp"

int main()
{
	EasyTcpClient client1;
	client1.ConnectServer("192.168.0.106", 4567); //IP地址随服务端IP地址而变

	Login login;
	strcpy(login.userName, "dongshao");
	strcpy(login.PassWord, "123456");
	while (client1.isRun())
	{
		client1.Onrun();
		client1.SendData(&login);
	}

	client1.CloseSocket();
	std::cout << "客户端停止工作!" << std::endl;
	getchar();  //防止程序一闪而过
	return 0;
}
  • 服务端测试测试代码如下,一直接收客户端的数据
#include "EasyTcpServer.hpp"
#include "MessageHeader.hpp"

int main()
{
	EasyTcpServer server1;
	server1.Bind("192.168.0.105", 4567);
	server1.Listen(5);

	while (server1.isRun())
	{
		server1.Onrun();
	}

	server1.CloseSocket();
	std::cout << "服务端停止工作!" << std::endl;

	getchar();  //防止程序一闪而过
	return 0;
}
  • 运行程序,左侧为服务端,右侧开启了两个客户端,可以看到数据交互正常,客户端收到了LoginResult类型的数据包,并且在程序中正确的对缓冲区中的数据包进行了拆包解析

项目(百万并发网络通信架构)7.2---解决客户端粘包现象(设置接收缓冲区)_第2张图片

  • 看到网络IO,可以看到网络速率大概为14Mbps左右

项目(百万并发网络通信架构)7.2---解决客户端粘包现象(设置接收缓冲区)_第3张图片

你可能感兴趣的:(项目(百万并发网络通信架构))