在EasyTCP服务器发送数据的时候,不是直接调用send函数发送,而是设计一个发送缓冲区,每次从这个发送缓冲区中发送
//发送数据
int SendData(DataHeader* header)
{
int ret = SOCKET_ERROR;
//要发送的数据长度
int nSendLen = header->dataLength;
//要发送的数据
const char* pSendData = (const char*)header;
while (true)
{
if (_lastSendPos + nSendLen >= SEND_BUFF_SZIE)
{
//计算可拷贝的数据长度
int nCopyLen = SEND_BUFF_SZIE - _lastSendPos;
//拷贝数据
memcpy(_szSendBuf + _lastSendPos, pSendData, nCopyLen);
//计算剩余数据位置
pSendData += nCopyLen;
//计算剩余数据长度
nSendLen -= nSendLen;
//发送数据
ret = send(_sockfd, _szSendBuf, SEND_BUFF_SZIE, 0);
//数据尾部位置清零
_lastSendPos = 0;
//发送错误
if (SOCKET_ERROR == ret)
{
return ret;
}
}else {
//将要发送的数据 拷贝到发送缓冲区尾部
memcpy(_szSendBuf + _lastSendPos, pSendData, nSendLen);
//计算数据尾部位置
_lastSendPos += nSendLen;
break;
}
}
return ret;
}
服务器接收数据也是先通过缓冲区接收,在拷贝到消息缓冲区
//接收数据 处理粘包 拆分包
int RecvData(ClientSocket* pClient)
{
//接收客户端数据
char* szRecv = pClient->msgBuf() + pClient->getLastPos();
int nLen = (int)recv(pClient->sockfd(), szRecv, (RECV_BUFF_SZIE)- pClient->getLastPos(), 0);
_pNetEvent->OnNetRecv(pClient);
//printf("nLen=%d\n", nLen);
if (nLen <= 0)
{
//printf("客户端<Socket=%d>已退出,任务结束。\n", pClient->sockfd());
return -1;
}
//将收取到的数据拷贝到消息缓冲区
//memcpy(pClient->msgBuf() + pClient->getLastPos(), _szRecv, nLen);
//消息缓冲区的数据尾部位置后移
pClient->setLastPos(pClient->getLastPos() + nLen);
//判断消息缓冲区的数据长度大于消息头DataHeader长度
while (pClient->getLastPos() >= sizeof(DataHeader))
{
//这时就可以知道当前消息的长度
DataHeader* header = (DataHeader*)pClient->msgBuf();
//判断消息缓冲区的数据长度大于消息长度
if (pClient->getLastPos() >= header->dataLength)
{
//消息缓冲区剩余未处理数据的长度
int nSize = pClient->getLastPos() - header->dataLength;
//处理网络消息
OnNetMsg(pClient, header);
//将消息缓冲区剩余未处理数据前移
memcpy(pClient->msgBuf(), pClient->msgBuf() + header->dataLength, nSize);
//消息缓冲区的数据尾部位置前移
pClient->setLastPos(nSize);
}
else {
//消息缓冲区剩余数据不够一条完整消息
break;
}
}
return 0;
}
发送任务类
//网络消息发送任务
class CellSendMsg2ClientTask:public CellTask
{
ClientSocket* _pClient;
DataHeader* _pHeader;
public:
CellSendMsg2ClientTask(ClientSocket* pClient, DataHeader* header)
{
_pClient = pClient;
_pHeader = header;
}
//执行任务
void doTask()
{
_pClient->SendData(_pHeader);
delete _pHeader;
}
};
再来梳理一下主函数
int main()
{
MyServer server;
server.InitSocket();
server.Bind(nullptr, 4567);
server.Listen(5);
server.Start(4);
//启动UI线程
std::thread t1(cmdThread);
t1.detach();
while (g_bRun)
{
server.OnRun();
//printf("空闲时间处理其它业务..\n");
}
server.Close();
printf("已退出。\n");
getchar();
return 0;
}
看server.Start方法
void Start(int nCellServer)
{
for (int n = 0; n < nCellServer; n++)
{
auto ser = new CellServer(_sock);
_cellServers.push_back(ser);
//注册网络事件接受对象
ser->setEventObj(this);
//启动消息处理线程
ser->Start();
}
}
这里Start开启了nCellServer个CellServer,所以要继续看CellServer的Start函数,其中涉及到客户端的断开处理。
void Start()
{
_thread = std::thread(std::mem_fn(&CellServer::OnRun), this);
_taskServer.Start();
}
这里启动了线程执行CellServer::OnRun函数
这里OnRun从任务队列中,取出任务,在使用select IO多路复用,单独处理客户端
void OnRun()
{
_clients_change = true;
while (isRun())
{
if (!_clientsBuff.empty())
{//从缓冲队列里取出客户数据
std::lock_guard<std::mutex> lock(_mutex);
for (auto pClient : _clientsBuff)
{
_clients[pClient->sockfd()] = pClient;
}
_clientsBuff.clear();
_clients_change = true;
}
//如果没有需要处理的客户端,就跳过
if (_clients.empty())
{
std::chrono::milliseconds t(1);
std::this_thread::sleep_for(t);
continue;
}
//伯克利套接字 BSD socket
fd_set fdRead;//描述符(socket) 集合
//清理集合
FD_ZERO(&fdRead);
if (_clients_change)
{
_clients_change = false;
//将描述符(socket)加入集合
_maxSock = _clients.begin()->second->sockfd();
for (auto iter : _clients)
{
FD_SET(iter.second->sockfd(), &fdRead);
if (_maxSock < iter.second->sockfd())
{
_maxSock = iter.second->sockfd();
}
}
memcpy(&_fdRead_bak, &fdRead, sizeof(fd_set));
}
else {
memcpy(&fdRead, &_fdRead_bak, sizeof(fd_set));
}
///nfds 是一个整数值 是指fd_set集合中所有描述符(socket)的范围,而不是数量
///既是所有文件描述符最大值+1 在Windows中这个参数可以写0
int ret = select(_maxSock + 1, &fdRead, nullptr, nullptr, nullptr);
if (ret < 0)
{
printf("select任务结束。\n");
Close();
return;
}
else if (ret == 0)
{
continue;
}
#ifdef _WIN32
for (int n = 0; n < fdRead.fd_count; n++)
{
auto iter = _clients.find(fdRead.fd_array[n]);
if (iter != _clients.end())
{
if (-1 == RecvData(iter->second))
{
if (_pNetEvent)
_pNetEvent->OnNetLeave(iter->second);
_clients_change = true;
_clients.erase(iter->first);
}
}else {
printf("error. if (iter != _clients.end())\n");
}
}
#else
std::vector<ClientSocket*> temp;
for (auto iter : _clients)
{
if (FD_ISSET(iter.second->sockfd(), &fdRead))
{
if (-1 == RecvData(iter.second))
{
if (_pNetEvent)
_pNetEvent->OnNetLeave(iter.second);
_clients_change = false;
temp.push_back(iter.second);
}
}
}
for (auto pClient : temp)
{
_clients.erase(pClient->sockfd());
delete pClient;
}
#endif
}
}
实际上这里相当于分出了两个线程,一个接收,一个发送。
void Start()
{
//线程
std::thread t(std::mem_fn(&CellTaskServer::OnRun),this);
t.detach();
}
//工作函数
void OnRun()
{
while (true)
{
//从缓冲区取出数据
if (!_tasksBuf.empty())
{
std::lock_guard<std::mutex> lock(_mutex);
for (auto pTask : _tasksBuf)
{
_tasks.push_back(pTask);
}
_tasksBuf.clear();
}
//如果没有任务
if (_tasks.empty())
{
std::chrono::milliseconds t(1);
std::this_thread::sleep_for(t);
continue;
}
//处理任务
for (auto pTask : _tasks)
{
pTask->doTask();
delete pTask;
}
//清空任务
_tasks.clear();
}
}
};
这里任务又是一个虚函数,所以继续看下面
//网络消息发送任务
class CellSendMsg2ClientTask:public CellTask
{
ClientSocket* _pClient;
DataHeader* _pHeader;
public:
CellSendMsg2ClientTask(ClientSocket* pClient, DataHeader* header)
{
_pClient = pClient;
_pHeader = header;
}
//执行任务
void doTask()
{
_pClient->SendData(_pHeader);
delete _pHeader;
}
};
这个MyServer类是继承自EasyTcpServer类,只重写了几个虚函数,这里暂时不管
那就看EasyTcpServer的OnRun函数
//处理网络消息
bool OnRun()
{
if (isRun())
{
time4msg();
//伯克利套接字 BSD socket
fd_set fdRead;//描述符(socket) 集合
//清理集合
FD_ZERO(&fdRead);
//将描述符(socket)加入集合
FD_SET(_sock, &fdRead);
///nfds 是一个整数值 是指fd_set集合中所有描述符(socket)的范围,而不是数量
///既是所有文件描述符最大值+1 在Windows中这个参数可以写0
timeval t = { 0,10};
int ret = select(_sock + 1, &fdRead, 0, 0, &t); //
if (ret < 0)
{
printf("Accept Select任务结束。\n");
Close();
return false;
}
//判断描述符(socket)是否在集合中
if (FD_ISSET(_sock, &fdRead))
{
FD_CLR(_sock, &fdRead);
Accept();
return true;
}
return true;
}
return false;
}
可以发现,OnRun函数用了select模型,调用了Accept函数
//接受客户端连接
SOCKET Accept()
{
// 4 accept 等待接受客户端连接
sockaddr_in clientAddr = {};
int nAddrLen = sizeof(sockaddr_in);
SOCKET cSock = INVALID_SOCKET;
#ifdef _WIN32
cSock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);
#else
cSock = accept(_sock, (sockaddr*)&clientAddr, (socklen_t *)&nAddrLen);
#endif
if (INVALID_SOCKET == cSock)
{
printf("socket=<%d>错误,接受到无效客户端SOCKET...\n", (int)_sock);
}
else
{
//将新客户端分配给客户数量最少的cellServer
addClientToCellServer(new ClientSocket(cSock));
//获取IP地址 inet_ntoa(clientAddr.sin_addr)
}
return cSock;
}
Accept函数将新客户端分配给客户数量最少的CellServer
void addClientToCellServer(ClientSocket* pClient)
{
//查找客户数量最少的CellServer消息处理对象
auto pMinServer = _cellServers[0];
for(auto pCellServer : _cellServers)
{
if (pMinServer->getClientCount() > pCellServer->getClientCount())
{
pMinServer = pCellServer;
}
}
pMinServer->addClient(pClient);
OnNetJoin(pClient);
}
CellServer使用了OnNetJoin函数处理新客户端的连接,注意这里OnNetJoin是一个虚函数,调用的是这个结果
virtual void OnNetJoin(ClientSocket* pClient)
{
EasyTcpServer::OnNetJoin(pClient);
}
//只会被一个线程触发 安全
virtual void OnNetJoin(ClientSocket* pClient)
{
_clientCount++;
//printf("client<%d> join\n", pClient->sockfd());
}
简单来说, 这个服务器的架构这这样的。
首先开启4个工作线程,工作线程又分程两个线程,接收和发送,接收和发送是分开的,接收到数据又可能回调发送数据,接收从客户端数组中使用IO多路复用的select函数监听。发送/任务处理从任务队列中处理
服务器主循环中,只负责监听客户端连接,然后将客户端加入到客户端队列中。
整个程序大致就是这样。