5. 网络数据传输中的粘包阻塞等问题

至此,客户端代码和服务端代码都可以正常运行,收发数据:

image-20210228171627554

但是,程序在网络中快速的收发大量数据时,会出现什么问题呢?

现在,我们对程序做一些修改,进行一些测试:

将client输入命令线程屏蔽,自动发送数据:

//启动线程函数
	//thread t1(cmdThread, &client);
	//t1.detach();

	Login login;
	strcpy(login.UserName, "anthony");
	strcpy(login.passWord, "chen");

	while (client.isRun())
	{
		client.onRun();
		client.sendData(&login);
	}
image-20210228182258463

可以看到,在数据量不大的情况下,数据收发正常,那么下面加大数据量:

struct LoginSucceed : public DataHeader
{
	LoginSucceed()
	{
		lenth = sizeof(LoginSucceed);
		cmd = CMD_LOGIN_SU;
		result = 0;
	}
	char data[1024];
	int result;
};//在LoginSucceed中新加入一个大的数据

客户端与服务端连接并收发数据时,运行一段时间后就卡住了

image-20210228182517180

这是因为每次传输的数据的大小提高,并且由程序循环发送和接收数据时,发送数据的速度大于接收或处理数据的速度,使得系统的接收缓冲区溢出,导致网络阻塞。

在这里插入图片描述

因此,我们可以自行设置一个比较大的数据缓冲区,尽可能将每一次接受到的数据都放入缓冲区中,相当于使用第二缓冲区快速清空系统的接收缓冲区。

在此过程中,可能会有多个消息进入第二缓冲区,粘在一起,即粘包,这时候我们就需要将其分开。

客户端更改如下:

int TcpClient::recvData()
{
	int nLen = recv(sock, recvBuf, RECV_BUF_SIZE, 0);	
	if (nLen <= 0)
	{
		cout << "与服务器连接终端..." << endl;
		return -1;
	}
	//将收到的数据拷贝到第二缓冲区中
	memcpy(msgBuf + lastPos, recvBuf, nLen);
	//消息缓冲区尾部位置后移
	lastPos += nLen;
	//判断消息缓冲区中的数据长度是否大于消息头DataHeader
	while (lastPos >= sizeof(DataHeader))
	{
		//这时就可以知道消息体的长度
		DataHeader* header = (DataHeader*)msgBuf;
		//判断消息缓冲区的数据长度是否大于消息体长度
		if (lastPos >= header->cmd)
		{
			//消息缓冲区未处理消息的长度
			int nSize = lastPos - header->lenth;
			//处理网络下消息
			onNetMsg(header);
			//将消息缓冲区中未处理数据前移
			memcpy(msgBuf, msgBuf + header->lenth, nSize);
			//消息缓冲区尾部位置后移
			lastPos = nSize;
		}
		else
		{
			//消息缓冲区剩余数据不足一条完整数据
			break;
		}
	}
	
	return 0;
}

服务端recvData函数修改:

int TcpServer::recvData(ClientSocket* pClient)
{
	int nLen = recv(pClient->GetSocket(), recvBuf, RECV_BUF_SIZE, 0);
	if (nLen <= 0)
	{
		cout << "客户端" << (int)pClient->GetSocket() << "退出..." << endl;
		return -1;
	}
	//将收到的数据拷贝到第二缓冲区中
	memcpy(pClient->GetMsgBuf() + pClient->GetLast(), recvBuf, nLen);
	//消息缓冲区尾部位置后移
	pClient->setLastPos(pClient->GetLast() + nLen);
	//判断消息缓冲区中的数据长度是否大于消息头DataHeader
	while (pClient->GetLast() >= sizeof(DataHeader))
	{
		//这时就可以知道消息体的长度
		DataHeader* header = (DataHeader*)pClient->GetMsgBuf();
		//判断消息缓冲区的数据长度是否大于消息体长度
		if (pClient->GetLast() >= header->lenth)
		{
			//消息缓冲区未处理消息的长度
			int nSize = pClient->GetLast() - header->lenth;
			//处理网络下消息
			onNetMsg(pClient->GetSocket(), header);
			//将消息缓冲区中未处理数据前移
			memcpy(pClient->GetMsgBuf(), pClient->GetMsgBuf() + header->lenth, nSize);
			//消息缓冲区尾部位置后移
			pClient->setLastPos(nSize);
		}
		else
		{
			//消息缓冲区剩余数据不足一条完整数据
			break;
		}
	}
	return 0;
}

头文件及消息结构定义(客户端同步修改):

#ifndef _TCPSERVER_H_
#define _TCPSERVER_H_
#define WIN32_LEAN_AND_MEAN
#include <vector>
#include <WinSock2.h>

#ifndef RECV_BUF_SIZE
//缓冲区最小单元大小
#define RECV_BUF_SIZE 10240
#endif // !RECV_BUF_SIZE

#pragma comment(lib, "ws2_32.lib")
using std::vector;

enum CMD
{
	CMD_LOGIN,
	CMD_LogOut,
	CMD_LOGIN_SU,
	CMD_LOGOUT_SU,
	CMD_NEW_USER,
	CMD_ERROR
};
struct DataHeader
{
	DataHeader()
	{
		lenth = sizeof(DataHeader);
		cmd = CMD_ERROR;
	}
	short lenth;
	short cmd;
};
// Data Package
struct Login : public DataHeader
{
	Login()
	{
		lenth = sizeof(Login);
		cmd = CMD_LOGIN;
	}
	char UserName[32] = "";
	char passWord[32] = "";
	char data[932];
};

struct LoginSucceed : public DataHeader
{
	LoginSucceed()
	{
		lenth = sizeof(LoginSucceed);
		cmd = CMD_LOGIN_SU;
		result = 0;
	}
	char data[992];
	int result;
};
struct LogOut : public DataHeader
{
	LogOut()
	{
		lenth = sizeof(LogOut);
		cmd = CMD_LogOut;
	}
	char UserName[32] = "";
};
struct LogOutSucceed : public DataHeader
{
	LogOutSucceed()
	{
		lenth = sizeof(LogOutSucceed);
		cmd = CMD_LOGOUT_SU;
		result = 0;
	}
	int result;
};

struct NewUser : public DataHeader
{
	NewUser()
	{
		lenth = sizeof(NewUser);
		cmd = CMD_NEW_USER;
		sock = 0;
	}
	int sock;
};

class ClientSocket
{
public:
	ClientSocket(SOCKET s = INVALID_SOCKET);
	SOCKET GetSocket();
	char* GetMsgBuf();
	int GetLast();
	void setLastPos(int pos);
private:
	SOCKET sock;
	//第二缓冲区 消息缓冲区
	char msgBuf[RECV_BUF_SIZE * 10];
	//消息缓冲区尾部位置
	int lastPos;
};

class TcpServer
{
public:
	TcpServer();
	virtual ~TcpServer();

	//初始化socket
	SOCKET initSocket();
	//绑定端口号
	int Bind(const char* ip, unsigned short port);
	//监听端口号
	int Listen(int n);
	//接收客户端连接
	SOCKET Accept();
	//关闭socket
	void Close();
	//处理网络消息
	bool onRun();
	//是否工作中
	bool isRun();
	//接收数据
	int recvData(ClientSocket* pClient);
	//响应网络消息
	virtual void onNetMsg(SOCKET cSock, DataHeader* cmdBuf);
	//给指定socket发送数据
	int sendData(SOCKET cSock, DataHeader* header);
	void sendDataToAll(DataHeader* header);
private:
	SOCKET sock;
	vector<ClientSocket* >clients;
	//接收缓冲区
	char recvBuf[RECV_BUF_SIZE];
};

#endif // !_TCPSERVER_H_

现在,即使开启多个客户端连接服务器,同时收发数据,也基本不会发生粘包和少包等问题。

之后,我们可以给服务端加上一个高分辨率的定时器/计时器,可以得到网络中每秒可以传输多少个数据包。

封装的CELLTimestamp类:

#ifndef _CELLTIMESTAMP_HPP_
#define _CELLTIMESTAMP_HPP_

#include <chrono>
using namespace std::chrono;
class CELLTimestamp
{
public:
	CELLTimestamp()
	{
		update();
	}
	~CELLTimestamp()
	{

	}
	void update()
	{
		begin = high_resolution_clock::now();
	}
	double getElapsedSecond()
	{
		return this->getElapsedTimeInMicroSec() * 0.000001;
	}
	double getElapsedTimeInMilliSec()
	{
		return this->getElapsedTimeInMicroSec() * 0.001;
	}
	long long getElapsedTimeInMicroSec()
	{
		return duration_cast<microseconds>(high_resolution_clock::now() - begin).count();
	}

private:
	time_point<high_resolution_clock> begin;
};

#endif // !_CELLTIMESTAMP_HPP_

//这里使用的是C++11标准里的高精度计时器,所以可以实现跨平台

TcpServer类中的变量:

private:
	SOCKET sock;
	vector<ClientSocket* >clients;
	//接收缓冲区
	char recvBuf[RECV_BUF_SIZE];
	CELLTimestamp tTime;    //加入定时器
	int recvCount;          //加上一个计算每秒发送了多少个数据包的计数器,构造函数中初始化为0

修改onNetMsg函数实现:

void TcpServer::onNetMsg(SOCKET cSock, DataHeader* header)
{
    //加入定时器和数据包计时器
	recvCount++;
	auto t1 = tTime.getElapsedSecond();
	if (t1 >= 1.0)
	{
		cout << setiosflags(ios::left);
		cout << "time<" << setw(8) << t1 << ">	socket<" << setw(5) << cSock
			<< ">	clients<" << setw(5) << clients.size() << ">	recvCount<" << setw(8) << recvCount << ">" << endl;
		recvCount = 0;
		tTime.update();
	}
	//处理请求
	switch (header->cmd)
	{
	case CMD_LOGIN:
	{
		/*Login* login = (Login*)header;
		cout << "接收到命令:CMD_LOGIN" << "	数据长度:" << login->lenth
			<< "    UserName: " << login->UserName << "    PassWord: " << login->passWord << endl;*/

		LoginSucceed ret;
		//send(cSock, (char*)&ret, sizeof(ret), 0);
		//sendData(cSock, &ret);
	}
	break;
	case CMD_LogOut:
	{
		/*LogOut* logout = (LogOut*)header;
		cout << "接收到命令:CMD_LogOut" << "	数据长度:" << logout->lenth
			<< "    UserName: " << logout->UserName << endl;*/
		LogOutSucceed ret;
		send(cSock, (char*)&ret, sizeof(ret), 0);
	}
	break;
	default:
		cout << "<socket=" << socket << ">收到未定义消息,数据长度:" << header->lenth << endl;
		/*DataHeader head;
		send(cSock, (char*)&head, sizeof(head), 0);*/
		break;
	}
}

具体代码下载地址:https://gitee.com/hongwei2021/cppsocket.git

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值