项目(百万并发网络通信架构)6.1---将客户端封装为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;
};

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;
};

struct NewUserJoin :public DataHeader
{
	NewUserJoin(int _cSocket = 0) :sock(_cSocket) {
		cmd = CMD_NEW_USER_JOIN;
		dataLength = sizeof(LogoutResult);
	}
	int sock;
};

#endif

EasyTcpClient.hpp

  • 这个头文件为客户端的代码封装
  • 相关方法有:
    • 判断当前客户端是否在运行:isRun()
    • 初始化socket:InitSocket()
    • 连接服务器:ConnectServer(const char* ip, unsigned int port)
    • 关闭socket:CloseSocket()
    • 处理网络消息:Onrun()
    • 接收数据:RecvData()
    • 响应网络消息:OnNetMessage(DataHeader* header)
    • 发送数据:SendData(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 "MessageHeader.hpp"

using namespace std;

class EasyTcpClient
{
public:
	EasyTcpClient() :_sock(INVALID_SOCKET) {}
	virtual ~EasyTcpClient() {
		CloseSocket();
	}
public:
	//判断当前客户端是否在运行
	bool isRun() { return _sock != INVALID_SOCKET; }
	//初始化socket
	void InitSocket();
	//连接服务器
	int ConnectServer(const char* ip, unsigned int port);
	//关闭socket
	void CloseSocket();
	//处理网络消息
	bool Onrun();

	/*
		使用RecvData接收任何类型的数据,
		然后将消息的头部字段传递给OnNetMessage()函数中,让其响应不同类型的消息
	*/
	//接收数据
	int RecvData();
	//响应网络消息
	virtual void OnNetMessage(DataHeader* header);

	//发送数据
	int SendData(DataHeader* header);
private:
	SOCKET _sock;
};

void EasyTcpClient::InitSocket()
{
	//如果之前有连接了,关闭旧连接,开启新连接
	if (isRun())
	{
		std::cout << "<Socket=" << (int)_sock << ">:关闭旧连接,建立了新连接" << std::endl;
		CloseSocket();
	}

#ifdef _WIN32
	WORD ver = MAKEWORD(2, 2);
	WSADATA dat;
	WSAStartup(ver, &dat);
#endif

	_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (INVALID_SOCKET == _sock) {
		std::cout << "ERROR:建立socket失败!" << std::endl;
	}
	else {
		std::cout << "<Socket=" << (int)_sock << ">:建立socket成功!" << std::endl;
	}
}

int EasyTcpClient::ConnectServer(const char* ip, unsigned int port)
{
	if (!isRun())
	{
		InitSocket();
	}

	//声明要连接的服务端地址(注意,不同平台的服务端IP地址也不同)
	struct sockaddr_in _sin = {};
#ifdef _WIN32
	_sin.sin_addr.S_un.S_addr = inet_addr(ip);
#else
	_sin.sin_addr.s_addr = inet_addr(ip);
#endif
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(port);

	//连接服务端
	int ret = connect(_sock, (struct sockaddr*)&_sin, sizeof(_sin));
	if (SOCKET_ERROR == ret) {
		std::cout << "<Socket=" << (int)_sock << ">:连接服务端(" << ip << "," << port << ")失败!" << std::endl;
	}
	else {
		std::cout << "<Socket=" << (int)_sock << ">:连接服务端(" << ip << "," << port << ")成功!" << std::endl;
	}
	
	return ret;
}

void EasyTcpClient::CloseSocket()
{
	if (_sock != INVALID_SOCKET)
	{
#ifdef _WIN32
		closesocket(_sock);
		WSACleanup();
#else
		close(_sock);
#endif
		_sock = INVALID_SOCKET;
	}
}

bool EasyTcpClient::Onrun()
{
	if (isRun())
	{
		fd_set fdRead;
		FD_ZERO(&fdRead);
		FD_SET(_sock, &fdRead);

		struct timeval t = { 1,0 };
		int ret = select(_sock + 1, &fdRead, NULL, NULL, &t);
		if (ret < 0)
		{
			std::cout << "<Socket=" << _sock << ">:select出错!" << std::endl;
			return false;
		}
		if (FD_ISSET(_sock, &fdRead)) //如果服务端有数据发送过来,接收显示数据
		{
			FD_CLR(_sock, &fdRead);
			if (-1 == RecvData())
			{
				std::cout << "<Socket=" << _sock << ">:数据接收失败,或服务端已断开!" << std::endl;
				return false;
			}
		}
		return true;
	}
	return false;
}

int EasyTcpClient::RecvData()
{
	/*接收数据规则:对于接收到的数据,先接收头部部分,
		然后再接收实体部分,最后调用OnNetMessage()函数判断接收到的数据的类型
	*/

	char szRecv[1024];//设置接收缓冲区,并接收命令
	//先接收头部部分
	int _nLen = recv(_sock, szRecv, sizeof(DataHeader), 0);
	if (_nLen < 0) {
		std::cout << "<Socket=" << _sock << ">:recv函数出错!" << std::endl;
		return -1;
	}
	else if (_nLen == 0) {
		std::cout << "<Socket=" << _sock << ">:接收数据失败,服务端已关闭!" << std::endl;
		return -1;
	}

	//在此处还应该判断少包黏包的问题,但是现在处于单机处理状态,后面介绍到复杂的消息通信时再介绍

	DataHeader* header = (DataHeader*)szRecv;
	//接收实体部分
	recv(_sock, (char*)szRecv + sizeof(DataHeader), header->dataLength + sizeof(DataHeader), 0);

	//判断接收到的数据的类型
	OnNetMessage(header);

	return 0;
}

void EasyTcpClient::OnNetMessage(DataHeader* header)
{
	switch (header->cmd)
	{
	case CMD_LOGIN_RESULT:   //如果返回的是登录的结果
	{
		LoginResult* loginResult = (LoginResult*)header;
		std::cout << "<Socket=" << _sock << ">,收到服务端数据:CMD_LOGIN_RESULT,数据长度:" << loginResult->dataLength << ",结果为:" << loginResult->result << std::endl;
	}
	break;
	case CMD_LOGOUT_RESULT:  //如果是退出的结果
	{
		LogoutResult* logoutResult = (LogoutResult*)header;
		std::cout << "<Socket=" << _sock << ">,收到服务端数据:CMD_LOGOUT_RESULT,数据长度:" << logoutResult->dataLength << ",结果为:" << logoutResult->result << std::endl;
	}
	break;
	case CMD_NEW_USER_JOIN:  //有新用户加入
	{
		NewUserJoin* newUserJoin = (NewUserJoin*)header;
		std::cout << "<Socket=" << _sock << ">,收到服务端数据:CMD_NEW_USER_JOIN,数据长度:" << newUserJoin->dataLength << ",新用户Socket为:" << newUserJoin->sock << std::endl;
	}
	break;
	}
}

int EasyTcpClient::SendData(DataHeader* header) 
{
	if (isRun() && header)
	{
		return send(_sock, (const char*)header, header->dataLength, 0);
	}
	return SOCKET_ERROR;
}

#endif // !_EasyTcpClient_hpp_

三、测试:单个客户端和服务端之间的展示

测试程序如下

#include "EasyTcpClient.hpp"
#include <thread>

void cmdThread(EasyTcpClient *client);

int main()
{
	EasyTcpClient client;
	//client.InitSocket();
	client.ConnectServer("192.168.0.106", 4567);


	//启动线程,线程执行函数的参数传入client的指针
	std::thread t1(cmdThread, &client);
	t1.detach();//分离线程

	while (client.isRun())
	{
		client.Onrun();

		//Sleep(1000); 可以让发送与接受速度延迟1秒
		//std::cout << "空闲时间,处理其他业务..." << std::endl;
	}

	client.CloseSocket();
	std::cout << "客户端停止工作!" << std::endl;
	getchar();  //防止程序一闪而过
	return 0;
}

void cmdThread(EasyTcpClient *client)
{
	char cmdBuf[256] = {};
	while (true)
	{
		std::cin >> cmdBuf;
		if (0 == strcmp(cmdBuf, "exit"))
		{
			std::cout << "客户端退出" << std::endl;
			client->CloseSocket();
			break;
		}
		else if (0 == strcmp(cmdBuf, "login"))
		{
			Login login;
			strcpy(login.userName, "dongshao");
			strcpy(login.PassWord, "123456");
			client->SendData(&login);
		}
		else if (0 == strcmp(cmdBuf, "logout"))
		{
			Logout logout;
			strcpy(logout.userName, "dongshao");
			client->SendData(&logout);
		}
		else {
			std::cout << "命令不识别,请重新输入" << std::endl;
		}
	}
}
  • 编译如下:
g++ -g -o client client.cpp -std=c++11 -pthread

 

测试结果如下

  • 运行服务端程序,服务端的IP为192.168.0.106

  • 运行客户端程序,程序中只创建了1个客户端(代码见上),结果如下:
    • 可以看到服务端接收到了客户端的连接,输入数据交互正常

  • 可以看到服务端接收到了客户端的连接,输入数据交互正常

四、测试:多个客户端和服务端之间的展示

测试程序如下

  • 使用多个客户端时,需要将客户端的代码和服务端的代码中的select参数4设置为非阻塞的形式
struct timeval t = { 0,0 };
select(_sock + 1, &fdRead, NULL, NULL, &t);
  • 测试程序如下: 
#include "EasyTcpClient.hpp"
#include <thread>

void cmdThread(EasyTcpClient *client);

int main()
{
	EasyTcpClient client1;
	//client1.InitSocket();
	client1.ConnectServer("192.168.0.106", 4567);

	EasyTcpClient client2;
	client2.ConnectServer("192.168.0.106", 4567);

	EasyTcpClient client3;
	client3.ConnectServer("192.168.0.106", 4567);

	
	std::thread t1(cmdThread, &client1);
	t1.detach();

	std::thread t2(cmdThread, &client2);
	t2.detach();

	std::thread t3(cmdThread, &client3);
	t3.detach();

	while (client1.isRun() || client2.isRun() || client3.isRun())
	{
		client1.Onrun();
		client2.Onrun();
		client3.Onrun();
	}

	client1.CloseSocket();
	client2.CloseSocket();
	client3.CloseSocket();
	std::cout << "客户端全部工作!" << std::endl;
	getchar();  //防止程序一闪而过
	return 0;
}

void cmdThread(EasyTcpClient *client)
{
	char cmdBuf[256] = {};
	while (true)
	{
		std::cin >> cmdBuf;
		if (0 == strcmp(cmdBuf, "exit"))
		{
			std::cout << "客户端退出" << std::endl;
			client->CloseSocket();
			break;
		}
		else if (0 == strcmp(cmdBuf, "login"))
		{
			Login login;
			strcpy(login.userName, "dongshao");
			strcpy(login.PassWord, "123456");
			client->SendData(&login);
		}
		else if (0 == strcmp(cmdBuf, "logout"))
		{
			Logout logout;
			strcpy(logout.userName, "dongshao");
			client->SendData(&logout);
		}
		else {
			std::cout << "命令不识别,请重新输入" << std::endl;
		}
	}
}
  • 编译如下:
g++ -g -o client client.cpp -std=c++11 -pthread

 

在Windows中使用客户端

  • 开启一个服务端,服务端的IP为192.168.0.106

  • 运行客户端程序,程序中创建了3个客户端(代码见上),结果如下:
    • 我们在右边客户端进行输入的时候,因为我们创建的线程是顺序创建的,所以在控制台输入命令的时候是根据线程的创建顺序输入的。每次输入数据第一次数据的数据时client1的,第二次是client2的,第三次是client3的。退出顺序也一样

在Ubuntu中使用客户端

  • 开启一个服务端,服务端的IP为192.168.0.106

  • 运行客户端程序,程序中创建了3个客户端(代码见上),结果如下:
    • 与Windows下运行客户端一样,服务端也都收到了三个客户端的请求
    • 但是在输入数据的时候,由于Linux系统与Windows系统的原因,导致输入的结果不一样(这里应该跟线程的执行有关),此处输入的数据都是第一个客户端的数据

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

董哥的黑板报

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

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

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

打赏作者

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

抵扣说明:

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

余额充值