一、粘包处理
1.什么是粘包
首先要了解一些概念,Send函数是将指定缓冲区的内容流到协议栈的发送缓冲区中。至于数据何时通过互联网从本机发送给另外一台主机,这是操作系统要负责的事情,操作系统的数据发送方式比较繁琐,所以,当数据发送频率较快,或网络比较复杂,容易出现重发的情况时,就容易出现操作系统一次发送了多个数据结构,半个数据结构的情况。
对于数据接收端也是同理,recv函数是将协议栈接收缓冲区中的内容流到指定的缓冲区中,至于是不是一个完整的数据结构根本不管,于是便很容易出现一次接收了多个数据结构或半个数据结构的情况。
对于这种问题,因为常常几个数据结构粘连在一起,于是我们给它起了个形象的名字:粘包
2.如何解决客户端粘包
至于处理方法,就是创建一个第二缓冲区(又名消息缓冲区)就可以了,第一缓冲区接收数据,然后将数据复制到第二缓冲区中。所有的处理部分都在第二缓冲区中进行。(这样就可以解决数据不全,多个数据粘连的问题)
下面的Header表示传输的数据结构
数据结构
#pragma once
#include<cstring>
enum class CMD
{
LOGIN,
LOGOUT,
LOGIN_RESULT,
LOGOUT_RESULT,
NO_CMD
};
struct Header
{
CMD cmd;
unsigned dataLength;
Header() :cmd(CMD::NO_CMD), dataLength(0) {}
Header(CMD cmd_, unsigned dataLength_) : cmd(cmd_), dataLength(dataLength_) {}
};
struct Login : public Header
{
char usrName[32];
char passwd[32];
Login(const char* usrName_, const char* passwd_) :Header(CMD::LOGIN, sizeof(Login))
{
strcpy_s(usrName, 32, usrName_);
strcpy_s(passwd, 32, passwd_);
}
Login() :Header(CMD::LOGIN, sizeof(Login)), usrName{ 0 }, passwd{ 0 } {}
};
struct Logout : public Header
{
unsigned logoutNum;
Logout(unsigned logoutNum_) :Header(CMD::LOGOUT, sizeof(Logout)), logoutNum(logoutNum_) {}
Logout() :Header(CMD::LOGOUT, sizeof(Logout)), logoutNum(0) {}
};
struct LoginResult : public Header
{
unsigned loginResultNum;
LoginResult(unsigned loginResultNum_) : Header(CMD::LOGIN_RESULT, sizeof(LoginResult)), loginResultNum(loginResultNum_) {}
LoginResult() : Header(CMD::LOGIN_RESULT, sizeof(LoginResult)), loginResultNum(0) {}
};
struct LogoutResult : public Header
{
unsigned logoutResultNum;
LogoutResult(unsigned logoutResultNum_) :Header(CMD::LOGOUT_RESULT, sizeof(LogoutResult)), logoutResultNum(logoutResultNum_) {}
LogoutResult() :Header(CMD::LOGOUT_RESULT, sizeof(LogoutResult)), logoutResultNum(0) {}
};
客户端类定义
#pragma once
#include<WinSock2.h>
#include"Message.hpp"
#include<iostream>
const unsigned RECV_BUF_SIZE = 1024;
class Client
{
public:
Client(SOCKET clntSock_);
void setMsgBUfcount(unsigned msgBufCount_);
unsigned getMsgBufcount();
char* getMsgBuf();
SOCKET getSocket();
private:
SOCKET clntSock;
char szMsgBuf[RECV_BUF_SIZE * 10]{};
//第二缓冲区长度
unsigned msgBufCount = 0;
};
客户端粘包处理
//这里只写客户端的recvMsg函数
//其余部分与select模型相同
void Client::recvMsg()
{
while (1)
{
//recv函数
int recvRet = recv(servSock, szBuf, RECV_BUF_SIZE, 0);
if (recvRet <= 0)
{
std::cout << "Client" << __LINE__ << "recv func error ,error num is " << WSAGetLastError() << std::endl;
return;
}
//将第一缓冲区的数据 复制到 第二缓冲区
memcpy_s(szMsgBuf + msgBufLength, RECV_BUF_SIZE * 10 - msgBufLength, szBuf, (unsigned)recvRet);
//第二缓冲区数据长度 加上 recv到的数据大小
msgBufLength += (unsigned)recvRet;
//保证 第二缓冲区大小 大于 数据结构的大小
while (msgBufLength >= sizeof(Header))
{
//创建一个临时变量接收第二缓冲区的信息
Header* pHeader = (Header*)szMsgBuf;
//保证 第二缓冲区大小 大于 数据结构的大小
if (pHeader->dataLength <= msgBufLength)
{
//第二缓冲区变化后的长度
unsigned tmpLength = msgBufLength - pHeader->dataLength;
//处理信息
conductMsg(pHeader);
//复制覆盖到第一缓冲区
memcpy(szMsgBuf, szMsgBuf + pHeader->dataLength, tmpLength);
//更改第二缓冲区长度
msgBufLength = tmpLength;
}
else
{
std::cout << "Client" << __LINE__ << "recvMsg func error " << std::endl;
break;
}
}
}
}
处理数据
bool Client::conductMsg(Header* pHeader)
{
//这里处理数据 用打印代替
switch (pHeader->cmd)
{
case CMD::LOGIN_RESULT:
{
LoginResult* pLoginResult = (LoginResult*)pHeader;
std::cout << pLoginResult->loginResultNum << std::endl;
break;
}
case CMD::LOGOUT_RESULT:
{
LogoutResult* pLogoutResult = (LogoutResult*)pHeader;
std::cout << pLogoutResult->logoutResultNum << std::endl;
break;
}
default:
break;
}
return true;
}
这里还要注意一下
启动客户端的时候要创建一个线程执行recvMsg函数
bool Client::start()
{
while (1)
{
Login login("a", "b");
if (send(servSock, (char*)&login, sizeof(Login), 0) < 0)
{
std::cout << "Client"<<__LINE__<< "send func error, error num is : " << WSAGetLastError() << std::endl;
return false;
}
//创建一个线程执行recvMsg函数
std::thread recvThread(&Client::recvMsg, this);
//将线程与调用其的线程分离,彼此独立执行
recvThread.detach();
Sleep(100);
}
return true;
}
2.如何解决服务端粘包
这里操作思路与解决客户端一致,但服务端相对要麻烦一些,因为服务端维护多个客户端协议栈,每个协议栈都有一个第二接受缓冲区。
代码演示:
服务端类定义
class Server
{
public:
Server();
~Server();
void close();
bool start();
bool init(const char* ip,unsigned short port);
bool recvMsg(Client* pClient);
SOCKET getSocket();
private:
SOCKET servSock;
bool coreFunc();
bool conductMsg(Header* pHeader, Client* pClient);
//第一缓冲区
char szBuf[RECV_BUF_SIZE]{};
//主要要用Client指针,防止栈溢出
/*
还有要注意的点 这里使用了map存储
关闭的时候要遍历clntmap
closesocket(clntMap.first)
delete clntmap.second
*/
std::map<SOCKET, Client*> clntMap;
};
主要区别在recvMsg函数 这里只写该函数的实现
bool Server::recvMsg(Client* pClient)
{
int recvRes = recv(pClient->getSocket(), szBuf, RECV_BUF_SIZE, 0);
if (recvRes <= 0)
{
std::cout << "Server" << __LINE__ << "recv func error, error num is : " << WSAGetLastError() << std::endl;
return false;
}
//先对客户端进行粘包处理,将数据拷贝到第二缓冲区
memcpy_s(pClient->getMsgBuf() + pClient->getMsgBufcount(),
RECV_BUF_SIZE * 10 - pClient->getMsgBufcount(), szBuf, (unsigned)recvRes);
//第二缓冲区大小增长
pClient->setMsgBUfcount(pClient->getMsgBufcount() + (unsigned)recvRes);
//判断缓冲区数据长度大于消息头Header长度
while (pClient->getMsgBufcount() >= sizeof(Header))
{
Header* pHeader = (Header*)pClient->getMsgBuf();
if (pClient->getMsgBufcount() >= pHeader->dataLength)
{
//缓冲区未处理的剩余长度
unsigned tmpMsgBufLength = pClient->getMsgBufcount() - pHeader->dataLength;
//处理信息
conductMsg(pHeader, pClient);
//将未处理信息再放到第二缓冲区
memcpy(pClient->getMsgBuf(), pClient->getMsgBuf() + pHeader->dataLength, tmpMsgBufLength);
//标记缓冲区长度
pClient->setMsgBUfcount(tmpMsgBufLength);
}
else
{
//不够一条 跳出循环
break;
}
}
return true;
}
二、服务端的生产者消费者模型
1.概念
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
生产者消费者模型是一个非常经典的多线程并发协作的模型
生产者消费者的核心就是一个线程生产数据,然后发送到一个公共缓冲区中,另外一个线程从该公共缓冲区中取出数据进行处理。
因为要使用多线程,这里补充一下数据保护的概念,当两个线程对同一数据进行写,或一读,一写的操作时,会触发数据保护的问题,主线程的结束会导致子线程的强制结束,为了让子线程能正常运行,我们要使用mutex即锁来解决这一问题。
2.思路
我们单独创建一个类来解决接收和处理信息,主线程先将Client这个变量发送到vector缓冲区中,子线程再从vector中取出这个变量,后续操作只需要注意数据保护问题,在适当的位置加锁即可。
一个线程负责接收客户端,另一个线程负责处理客户端发送过来的数据,这个公共缓冲区就在recvServer中
3.实现
recvServer类定义
#pragma once
#include"Client.h"
#include"Message.hpp"
#include<map>
#include<vector>
#include<mutex>
//缓冲区
class RecvServer
{
public:
RecvServer();
~RecvServer();
void close();
void start();
void addClientToBuf(Client* pClient);
unsigned getClientNum();
private:
void coreFunc();
bool recvMsg(Client* pCLient);
bool conductMsg(Header* pHeader, Client* pClient);
//客户端缓冲区
//主线程将信息发送到子线程
//子线程接收信息处理
std::vector<Client*> clntBufVec;
std::map<SOCKET, Client*> clntMap;
//第一缓冲区
char szBuf[RECV_BUF_SIZE]{};
//涉及到数据保护问题 使用锁
std::mutex _mutex;
};
addClientToRecvServer函数的实现:
void Server::addClientToRecvServer(Client* pClient)
{
//记录每个接收线程的客户端数量
RecvServer* minClientRecvServer = recvServerVec[0];
//让每个线程处理的客户端数量尽量均衡可以更加合理的分配系统资源
for (unsigned i = 1; i < RECV_SERVER_SIZE; i++)
{
if (recvServerVec[i]->getClientNum() < minClientRecvServer->getClientNum())
{
minClientRecvServer = recvServerVec[i];
}
}
minClientRecvServer->addClientToBuf(pClient);
}
Start函数的实现:
这里Start函数和之前有所不用
bool Server::start()
{
//主线程对应四个消息接收线程
for (unsigned i = 0; i < RECV_SERVER_SIZE; i++)
{
RecvServer* pRecvServer = new RecvServer();
pRecvServer->start();
recvServerVec.push_back(pRecvServer);
}
if (!coreFunc())
{
return false;
}
return true;
}
接下来看corefunc核心部分
void RecvServer::coreFunc()
{
while (1)
{
fd_set fdRead;
FD_ZERO(&fdRead);
//存取过程需要考虑数据保护问题
{
//主线程对clntBufVec的扩充很可能对这个if产生影响 所以我们要上锁
_mutex.lock();
if (!clntBufVec.empty())
{
for (auto Client : clntBufVec)
{
clntMap.insert(std::make_pair(Client->getSocket(), Client));
}
}
_mutex.unlock();
}
for (auto client : clntMap)
{
FD_SET(client.first, &fdRead);
}
timeval tv{ 1, 0 };
//优化 防止recvMsg传无效参数
if (clntMap.empty())
continue;
int selectRes = select(0, &fdRead, nullptr, nullptr, &tv);
if (selectRes > 0)
{
for (unsigned i = 0; i < fdRead.fd_count; ++i)
{
if (!recvMsg(clntMap[fdRead.fd_array[i]]))
{
std::cout << "recvServer" << __LINE__ << "recvMsg func error" << std::endl;
return;
}
}
}
else if (selectRes == 0)
{
std::cout << "do something else" << std::endl;
continue;
}
else
{
std::cout << "Server" << __LINE__ << "select func error, error num : " << WSAGetLastError() << std::endl;
return;
}
}
return;
}