项目(百万并发网络通信架构)6.2---将服务端端封装为class

一、概述

  • 在前面的文章中,服务端的代码都是以面向过程的形式展现,本文将之前服务端的代码封装为一个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不同系统之间时,这个消息没有收到,位于同一系统的其它客户收到了,可能跟数据报的传送有关(后面看看能不能解决)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

董哥的黑板报

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值