至此,客户端代码和服务端代码都可以正常运行,收发数据:
但是,程序在网络中快速的收发大量数据时,会出现什么问题呢?
现在,我们对程序做一些修改,进行一些测试:
将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);
}
可以看到,在数据量不大的情况下,数据收发正常,那么下面加大数据量:
struct LoginSucceed : public DataHeader
{
LoginSucceed()
{
lenth = sizeof(LoginSucceed);
cmd = CMD_LOGIN_SU;
result = 0;
}
char data[1024];
int result;
};//在LoginSucceed中新加入一个大的数据
客户端与服务端连接并收发数据时,运行一段时间后就卡住了
这是因为每次传输的数据的大小提高,并且由程序循环发送和接收数据时,发送数据的速度大于接收或处理数据的速度,使得系统的接收缓冲区溢出,导致网络阻塞。
因此,我们可以自行设置一个比较大的数据缓冲区,尽可能将每一次接受到的数据都放入缓冲区中,相当于使用第二缓冲区快速清空系统的接收缓冲区。
在此过程中,可能会有多个消息进入第二缓冲区,粘在一起,即粘包,这时候我们就需要将其分开。
客户端更改如下:
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