一、概述
- 在前面的文章中,客户端的代码都是以面向过程的形式展现,本文将之前客户端的代码封装为一个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系统的原因,导致输入的结果不一样(这里应该跟线程的执行有关),此处输入的数据都是第一个客户端的数据
转载:https://blog.csdn.net/qq_41453285/article/details/105367366
查看评论