在服务器客户端通信的时候,并不是像简单的echo服务器那样,还是发送特定的数据包协议。
数据包的协议可以用自定义结构体定义。
自定义协议分简单分为消息头, 和内容,消息头具有消息的长度,和类型,使用继承的方法,简单实现
#ifndef _MessageHeader_hpp_
#define _MessageHeader_hpp_
enum CMD
{
CMD_LOGIN,
CMD_LOGIN_RESULT,
CMD_LOGOUT,
CMD_LOGOUT_RESULT,
CMD_NEW_USER_JOIN,
CMD_ERROR
};
struct DataHeader
{
short dataLength;
short cmd;
};
//DataPackage
struct Login : public DataHeader
{
Login()
{
dataLength = sizeof(Login);
cmd = CMD_LOGIN;
}
char userName[32];
char PassWord[32];
};
struct LoginResult : public DataHeader
{
LoginResult()
{
dataLength = sizeof(LoginResult);
cmd = CMD_LOGIN_RESULT;
result = 0;
}
int result;
char data[1024];
};
struct Logout : public DataHeader
{
Logout()
{
dataLength = sizeof(Logout);
cmd = CMD_LOGOUT;
}
char userName[32];
};
struct LogoutResult : public DataHeader
{
LogoutResult()
{
dataLength = sizeof(LogoutResult);
cmd = CMD_LOGOUT_RESULT;
result = 0;
}
int result;
};
struct NewUserJoin : public DataHeader
{
NewUserJoin()
{
dataLength = sizeof(NewUserJoin);
cmd = CMD_NEW_USER_JOIN;
scok = 0;
}
int scok;
};
#endif // !_MessageHeader_hpp_
服务器使用C++ 面向对象的方式封装,主函数代码只有以下几行,很好理解
#include "EasyTcpServer.hpp"
int main()
{
EasyTcpServer server;
server.InitSocket();
server.Bind(nullptr, 4567);
server.Listen(5);
while (server.isRun())
{
server.OnRun();
//printf("空闲时间处理其它业务..\n");
}
server.Close();
printf("已退出。\n");
getchar();
return 0;
}
EasyTCPServer这个类封装了 服务器的基本设置函数,同时使用了一个STL vector<int> 数组来保存连接的客户端
class EasyTcpServer
{
private:
SOCKET _sock;
std::vector<SOCKET> g_clients;
}
这里最重要的逻辑函数是
每次调用select函数之前,都要重新设置fd_set
bool OnRun()
{
if (isRun())
{
//伯克利套接字 BSD socket
fd_set fdRead;//描述符(socket) 集合
fd_set fdWrite;
fd_set fdExp;
//清理集合
FD_ZERO(&fdRead);
FD_ZERO(&fdWrite);
FD_ZERO(&fdExp);
//将描述符(socket)加入集合
FD_SET(_sock, &fdRead);
FD_SET(_sock, &fdWrite);
FD_SET(_sock, &fdExp);
SOCKET maxSock = _sock;
for (int n = (int)g_clients.size() - 1; n >= 0; n--)
{
FD_SET(g_clients[n], &fdRead);
if (maxSock < g_clients[n])
{
maxSock = g_clients[n];
}
}
///nfds 是一个整数值 是指fd_set集合中所有描述符(socket)的范围,而不是数量
///既是所有文件描述符最大值+1 在Windows中这个参数可以写0
//timeval t = { 1,0 };
int ret = select(maxSock + 1, &fdRead, &fdWrite, &fdExp, nullptr); //&t
//printf("select ret=%d count=%d\n", ret, _nCount++);
if (ret < 0)
{
printf("select任务结束。\n");
Close();
return false;
}
//判断描述符(socket)是否在集合中
if (FD_ISSET(_sock, &fdRead))
{
FD_CLR(_sock, &fdRead);
Accept();
}
for (int n = (int)g_clients.size() - 1; n >= 0; n--)
{
if (FD_ISSET(g_clients[n], &fdRead))
{
if (-1 == RecvData(g_clients[n]))
{
auto iter = g_clients.begin() + n;//std::vector<SOCKET>::iterator
if (iter != g_clients.end())
{
g_clients.erase(iter);
}
}
}
}
return true;
}
return false;
}
其中RecvData函数的实现也特别关键
一次读取 1024 * 400 = 400K数据,再拆分处理
//缓冲区
char szRecv[409600] = {};
//接收数据 处理粘包 拆分包
int RecvData(SOCKET _cSock)
{
// 5 接收客户端数据
int nLen = (int)recv(_cSock, szRecv, 409600, 0);
//printf("nLen=%d\n", nLen);
if (nLen <= 0)
{
printf("客户端<Socket=%d>已退出,任务结束。\n", _cSock);
return -1;
}
LoginResult ret;
SendData(_cSock, &ret);
/*
DataHeader* header = (DataHeader*)szRecv;
recv(_cSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
OnNetMsg(_cSock, header);
*/
return 0;
}
服务器回送请求
//响应网络消息
virtual void OnNetMsg(SOCKET _cSock, DataHeader* header)
{
switch (header->cmd)
{
case CMD_LOGIN:
{
Login* login = (Login*)header;
printf("收到客户端<Socket=%d>请求:CMD_LOGIN,数据长度:%d,userName=%s PassWord=%s\n", _cSock, login->dataLength, login->userName, login->PassWord);
//忽略判断用户密码是否正确的过程
LoginResult ret;
SendData(_cSock, &ret);
}
break;
case CMD_LOGOUT:
{
Logout* logout = (Logout*)header;
printf("收到客户端<Socket=%d>请求:CMD_LOGOUT,数据长度:%d,userName=%s \n", _cSock, logout->dataLength, logout->userName);
//忽略判断用户密码是否正确的过程
LogoutResult ret;
SendData(_cSock, &ret);
}
break;
default:
{
DataHeader header = { 0,CMD_ERROR };
send(_cSock, (char*)&header, sizeof(header), 0);
}
break;
}
}
客户端的实现比较简单, 开一线程用来接受命令,退出
#include "EasyTcpClient.hpp"
#include<thread>
void cmdThread(EasyTcpClient* client)
{
while (true)
{
char cmdBuf[256] = {};
scanf("%s", cmdBuf);
if (0 == strcmp(cmdBuf, "exit"))
{
client->Close();
printf("退出cmdThread线程\n");
break;
}
else if (0 == strcmp(cmdBuf, "login"))
{
Login login;
strcpy(login.userName, "lyd");
strcpy(login.PassWord, "lydmm");
client->SendData(&login);
}
else if (0 == strcmp(cmdBuf, "logout"))
{
Logout logout;
strcpy(logout.userName, "lyd");
client->SendData(&logout);
}
else {
printf("不支持的命令。\n");
}
}
}
int main()
{
EasyTcpClient client1;
client1.Connect("127.0.0.1", 4567);
//启动UI线程
std::thread t1(cmdThread, &client1);
t1.detach();
Login login;
strcpy(login.userName, "lyd");
strcpy(login.PassWord, "lydmm");
while (client1.isRun())
{
client1.OnRun();
client1.SendData(&login);
//printf("空闲时间处理其它业务..\n");
//Sleep(1000);
}
client1.Close();
printf("已退出。\n");
getchar();
return 0;
}
客户端也是用select接受数据
//处理网络消息
int _nCount = 0;
bool OnRun()
{
if (isRun())
{
fd_set fdReads;
FD_ZERO(&fdReads);
FD_SET(_sock, &fdReads);
timeval t = { 0,0 };
int ret = select(_sock + 1, &fdReads, 0, 0, &t);
//printf("select ret=%d count=%d\n", ret, _nCount++);
if (ret < 0)
{
printf("<socket=%d>select任务结束1\n", _sock);
Close();
return false;
}
if (FD_ISSET(_sock, &fdReads))
{
FD_CLR(_sock, &fdReads);
if (-1 == RecvData(_sock))
{
printf("<socket=%d>select任务结束2\n", _sock);
Close();
return false;
}
}
return true;
}
return false;
}
这里接受消息包括了粘包的处理
//处理网络消息
int _nCount = 0;
bool OnRun()
{
if (isRun())
{
fd_set fdReads;
FD_ZERO(&fdReads);
FD_SET(_sock, &fdReads);
timeval t = { 0,0 };
int ret = select(_sock + 1, &fdReads, 0, 0, &t);
//printf("select ret=%d count=%d\n", ret, _nCount++);
if (ret < 0)
{
printf("<socket=%d>select任务结束1\n", _sock);
Close();
return false;
}
if (FD_ISSET(_sock, &fdReads))
{
FD_CLR(_sock, &fdReads);
if (-1 == RecvData(_sock))
{
printf("<socket=%d>select任务结束2\n", _sock);
Close();
return false;
}
}
return true;
}
return false;
}