一、概述
- 在前面的文章中,服务端的代码都是以面向过程的形式展现,本文将之前服务端的代码封装为一个class
二、代码如下
MessageHeader.hpp
#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 cmd; //命令的类型
short dataLength; //数据的长度
};
//登录消息体
struct Login :public DataHeader
{
Login() {
cmd = CMD_LOGIN;
dataLength = sizeof(Login); //消息长度=消息头(父类)+消息体(子类)
}
char userName[32]; //账号
char PassWord[32]; //密码
};
//登录结果
struct LoginResult :public DataHeader
{
LoginResult() :result(0) {
cmd = CMD_LOGIN_RESULT;
dataLength = sizeof(LoginResult);
}
int result; //登录的结果,0代表正常
};
//退出消息体
struct Logout :public DataHeader
{
Logout() {
cmd = CMD_LOGOUT;
dataLength = sizeof(Logout);
}
char userName[32]; //账号
};
//退出结果
struct LogoutResult :public DataHeader
{
LogoutResult() :result(0) {
cmd = CMD_LOGOUT_RESULT;
dataLength = sizeof(LogoutResult);
}
int result; //退出的结果,0代表正常
};
//新的客户端加入,服务端给其他所有客户端发送此报文
struct NewUserJoin :public DataHeader
{
NewUserJoin(int _cSocket = 0) :sock(_cSocket) {
cmd = CMD_NEW_USER_JOIN;
dataLength = sizeof(LogoutResult);
}
int sock; //新客户端的socket
};
#endif
EasyTcpServer.hpp
- 这个头文件为客户端的代码封装
- 相关方法有:
- 判断当前服务端是否在运行:isRun()
- 初始化socket:InitSocket()
- 绑定端口号:Bind(const char* ip, unsigned short port)
- 监听端口号:Listen(int n)
- 接收客户端连接:Accept()
- 关闭socket:CloseSocket()
- 处理网络消息:Onrun()
- 接收数据:RecvData(SOCKET _cSock)
- 响应网络消息:OnNetMessage(SOCKET _cSock, DataHeader* header)
- 发送数据:SendData(SOCKET _cSock, DataHeader* header)
- 群发数据:SendDataToAll( DataHeader* header)
#ifndef _EasyTcpClient_hpp_
#define _EasyTcpClient_hpp_
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS //for inet_pton()
#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#else
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>
//在Unix下没有这些宏,为了兼容,自己定义
#define SOCKET int
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR (-1)
#endif
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <vector>
#include "MessageHeader.hpp"
using namespace std;
class EasyTcpServer
{
public:
EasyTcpServer() :_sock(INVALID_SOCKET) {}
virtual ~EasyTcpServer() { CloseSocket(); }
public:
//判断当前服务端是否在运行
bool isRun() { return _sock != INVALID_SOCKET; }
//初始化socket
void InitSocket();
//绑定端口号
int Bind(const char* ip, unsigned short port);
//监听端口号
int Listen(int n);
//接收客户端连接
SOCKET Accept();
//关闭socket
void CloseSocket();
//处理网络消息
bool Onrun();
/*
使用RecvData接收任何类型的数据,
然后将消息的头部字段传递给OnNetMessage()函数中,让其响应不同类型的消息
*/
//接收数据,参数:客户端的套接字
int RecvData(SOCKET _cSock);
//响应网络消息
virtual void OnNetMessage(SOCKET _cSock, DataHeader* header);
//发送数据,单发(参数1为指定的客户端的socket)
int SendData(SOCKET _cSock, DataHeader* header);
//群发数据
void SendDataToAll( DataHeader* header);
private:
SOCKET _sock;
std::vector<SOCKET> g_clients;//存放客户端的套接字
SOCKET maxSock = _sock; //select的参数1要使用,当前最大的文件描述符值
};
void EasyTcpServer::InitSocket()
{
#ifdef _WIN32
WORD ver = MAKEWORD(2, 2);
WSADATA dat;
WSAStartup(ver, &dat);
#endif
//建立socket
_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == _sock) {
std::cout << "Server:创建socket成功" << std::endl;
}
else {
std::cout << "Server:创建socket成功" << std::endl;
}
}
int EasyTcpServer::Bind(const char* ip, unsigned short port)
{
if (!isRun())
InitSocket();
//初始化服务端地址
struct sockaddr_in _sin = {};
#ifdef _WIN32
if (ip)
_sin.sin_addr.S_un.S_addr = inet_addr(ip);
else
_sin.sin_addr.S_un.S_addr = INADDR_ANY;
#else
if (ip)
_sin.sin_addr.s_addr = inet_addr(ip);
else
_sin.sin_addr.s_addr = INADDR_ANY;
#endif
_sin.sin_family = AF_INET;
_sin.sin_port = htons(port);
//绑定服务端地址
int ret = bind(_sock, (struct sockaddr*)&_sin, sizeof(_sin));
if (SOCKET_ERROR == ret) {
if (ip)
std::cout << "Server:绑定地址(" << ip << "," << port << ")失败!" << std::endl;
else
std::cout << "Server:绑定地址(INADDR_ANY," << port << ")失败!" << std::endl;
}
else {
if (ip)
std::cout << "Server:绑定地址(" << ip << "," << port << ")成功!" << std::endl;
else
std::cout << "Server:绑定地址(INADDR_ANY," << port << ")成功!" << std::endl;
}
return ret;
}
void EasyTcpServer::CloseSocket()
{
if (_sock != INVALID_SOCKET)
{
#ifdef _WIN32
//将所有的客户端套接字关闭
for (int n = (int)g_clients.size() - 1; n >= 0; --n)
{
closesocket(g_clients[n]);
}
//关闭服务端套接字
closesocket(_sock);
WSACleanup();
#else
for (int n = (int)g_clients.size() - 1; n >= 0; --n)
{
close(g_clients[n]);
}
close(_sock);
#endif
_sock = INVALID_SOCKET;
}
}
int EasyTcpServer::Listen(int n)
{
//监听网络端口
int ret = listen(_sock, n);
if (SOCKET_ERROR == ret)
std::cout << "Server:监听网络端口失败!" << std::endl;
else
std::cout << "Server:监听网络端口成功!" << std::endl;
return ret;
}
SOCKET EasyTcpServer::Accept()
{
//用来保存客户端地址
struct sockaddr_in _clientAddr = {};
int nAddrLen = sizeof(_clientAddr);
SOCKET _cSock = INVALID_SOCKET;
//接收客户端连接
#ifdef _WIN32
_cSock = accept(_sock, (struct sockaddr*)&_clientAddr, &nAddrLen);
#else
_cSock = accept(_sock, (struct sockaddr*)&_clientAddr, (socklen_t*)&nAddrLen);
#endif
if (INVALID_SOCKET == _cSock) {
std::cout << "Server:接收到无效客户端!" << std::endl;
}
else {
//通知其他已存在的所有客户端,有新的客户端加入
NewUserJoin newUserInfo(static_cast<int>(_cSock));
SendDataToAll(&newUserInfo);
//将客户端的套接字存入vector内
g_clients.push_back(_cSock);
std::cout << "Server:接受到新的客户端连接,IP=" << inet_ntoa(_clientAddr.sin_addr)
<< ",Socket=" << static_cast<int>(_cSock) << std::endl;
}
return _cSock;
}
bool EasyTcpServer::Onrun()
{
if (isRun())
{
fd_set fdRead;
fd_set fdWrite;
fd_set fdExp;
FD_ZERO(&fdRead);
FD_ZERO(&fdWrite);
FD_ZERO(&fdExp);
FD_SET(_sock, &fdRead);
FD_SET(_sock, &fdWrite);
FD_SET(_sock, &fdExp);
//每次select之前,将所有客户端加入到读集中(此处为了演示,只介绍客户端读的情况)
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];
}
struct timeval t = { 3,0 };
int ret = select(maxSock + 1, &fdRead, &fdWrite, &fdExp, &t);
if (ret < 0)
{
std::cout << "Server:select出错!" << std::endl;
return false;
}
if (FD_ISSET(_sock, &fdRead))//如果一个客户端连接进来,那么服务端的socket就会变为可读的,此时我们使用accept来接收这个客户端
{
FD_CLR(_sock, &fdRead);
Accept();
}
//遍历vector数组中所有的客户端套接字,如果某个客户端的套接字在读集中,
//那么说明相应的客户端有数据来,那么就执行processor()函数
for (int n = (int)g_clients.size() - 1; n >= 0; --n)
{
if (FD_ISSET(g_clients[n], &fdRead))
{
if (-1 == RecvData(g_clients[n]))
{
//如果processor出错,那么就将该客户端从全局vector中移除
//首先获取该套接字在vector中的迭代器位置,然后通过erase()删除
auto iter = g_clients.begin() + n;
if (iter != g_clients.end())
{
g_clients.erase(iter);
}
}
}
}
return true;
}
return false;
}
int EasyTcpServer::RecvData(SOCKET _cSock)
{
/*接收数据规则:对于接收到的数据,先接收头部部分,
然后再接收实体部分,最后调用OnNetMessage()函数判断接收到的数据的类型
*/
char szRecv[1024];//设置接收缓冲区,并接收命令
//先接收头部部分
int _nLen = recv(_cSock, szRecv, sizeof(DataHeader), 0);
if (_nLen < 0) {
std::cout << "recv函数出错!" << std::endl;
return -1;
}
else if (_nLen == 0) {
std::cout << "客户端<Socket=" << _cSock << ">:已退出!" << std::endl;
return -1;
}
//在此处还应该判断少包黏包的问题,但是现在处于单机处理状态,后面介绍到复杂的消息通信时再介绍
DataHeader* header = (DataHeader*)szRecv;
//接收实体部分
recv(_cSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
OnNetMessage(_cSock, header);
return 0;
}
void EasyTcpServer::OnNetMessage(SOCKET _cSock, DataHeader* header)
{
switch (header->cmd)
{
case CMD_LOGIN: //如果是登录
{
Login *login = (Login*)header;
std::cout << "客户端<Socket=" << _cSock << ">:CMD_LOGIN,用户名:" << login->userName << ",密码:" << login->PassWord << std::endl;
//此处可以判断用户账户和密码是否正确等等(省略)
//返回登录的结果给客户端
LoginResult ret;
SendData(_cSock, &ret);
}
break;
case CMD_LOGOUT: //如果是退出
{
Logout *logout = (Logout*)header;
std::cout << "客户端<Socket=" << _cSock << ">:CMD_LOGOUT,用户名:" << logout->userName << std::endl;
//返回退出的结果给客户端
LogoutResult ret;
SendData(_cSock, &ret);
}
break;
default: //如果有错误
{
DataHeader header = { CMD_ERROR,0 };
SendData(_cSock, &header);
}
break;
}
}
int EasyTcpServer::SendData(SOCKET _cSock, DataHeader* header)
{
if (isRun() && header)
{
return send(_cSock, (const char*)header, header->dataLength, 0);
}
return SOCKET_ERROR;
}
void EasyTcpServer::SendDataToAll(DataHeader* header)
{
//通知其他已存在的所有客户端,有新的客户端加入
for (int n = 0; n < g_clients.size(); ++n)
{
SendData(g_clients[n], header);
}
}
#endif
三、测试:运行单个服务端
测试程序如下
#include "EasyTcpServer.hpp"
#include "MessageHeader.hpp"
int main()
{
EasyTcpServer server;
//server.InitSocket();
server.Bind("192.168.0.106", 4567);
server.Listen(5);
while (server.isRun())
{
server.Onrun();
//std::cout << "空闲时间,处理其他业务..." << std::endl;
}
server.CloseSocket();
std::cout << "服务端停止工作!" << std::endl;
getchar(); //防止程序一闪而过
return 0;
}
演示效果
- 服务端的IP为192.168.0.106。使用单个客户端去连接,并且输入数据,一切正常
四、测试:运行多个服务端
测试程序如下
- 运行多个服务端的时候,将服务端的select()函数设置为非阻塞的
struct timeval t = { 0,0 };
select(maxSock + 1, &fdRead, &fdWrite, &fdExp, &t);
#include "EasyTcpServer.hpp"
#include "MessageHeader.hpp"
int main()
{
EasyTcpServer server1;
//server1.InitSocket();
server1.Bind("192.168.0.106", 4567);
server1.Listen(5);
EasyTcpServer server2;
server2.Bind("192.168.0.106", 4568);
server2.Listen(5);
while (server1.isRun() || server2.isRun())
{
server1.Onrun();
server2.Onrun();
//std::cout << "空闲时间,处理其他业务..." << std::endl;
}
server1.CloseSocket();
server2.CloseSocket();
std::cout << "所有服务端停止工作!" << std::endl;
getchar(); //防止程序一闪而过
return 0;
}
演示效果
- 程序中运行了两个服务端,一个监听端口为4567,一个监听端口为4568
- 运行两个客户端,第一个(Ubuntu)连接4567,第二个(Windows)连接4568,显示成功,数据交互正常
- 这里有一个问题未解决:新客户端加入的时候,服务器会通知其他所有客户端有新用户加入,但是在测试的时候,客户端位于Ubuntu和Windows不同系统之间时,这个消息没有收到,位于同一系统的其它客户收到了,可能跟数据报的传送有关(后面看看能不能解决)