飞道的博客

项目(百万并发网络通信架构)6.1---将客户端封装为class

341人阅读  评论(0)

一、概述

  • 在前面的文章中,客户端的代码都是以面向过程的形式展现,本文将之前客户端的代码封装为一个class

二、代码如下

MessageHeader.hpp

  • 这个头文件包含所有的数据包的格式定义

   
  1. #ifndef _MessageHeader_hpp_
  2. #define _MessageHeader_hpp_
  3. enum CMD
  4. {
  5. CMD_LOGIN,
  6. CMD_LOGIN_RESULT,
  7. CMD_LOGOUT,
  8. CMD_LOGOUT_RESULT,
  9. CMD_NEW_USER_JOIN,
  10. CMD_ERROR
  11. };
  12. struct DataHeader
  13. {
  14. short cmd;
  15. short dataLength;
  16. };
  17. struct Login : public DataHeader
  18. {
  19. Login() {
  20. cmd = CMD_LOGIN;
  21. dataLength = sizeof(Login);
  22. }
  23. char userName[ 32];
  24. char PassWord[ 32];
  25. };
  26. struct LoginResult : public DataHeader
  27. {
  28. LoginResult() :result( 0) {
  29. cmd = CMD_LOGIN_RESULT;
  30. dataLength = sizeof(LoginResult);
  31. }
  32. int result;
  33. };
  34. struct Logout : public DataHeader
  35. {
  36. Logout() {
  37. cmd = CMD_LOGOUT;
  38. dataLength = sizeof(Logout);
  39. }
  40. char userName[ 32];
  41. };
  42. struct LogoutResult : public DataHeader
  43. {
  44. LogoutResult() :result( 0) {
  45. cmd = CMD_LOGOUT_RESULT;
  46. dataLength = sizeof(LogoutResult);
  47. }
  48. int result;
  49. };
  50. struct NewUserJoin : public DataHeader
  51. {
  52. NewUserJoin( int _cSocket = 0) :sock(_cSocket) {
  53. cmd = CMD_NEW_USER_JOIN;
  54. dataLength = sizeof(LogoutResult);
  55. }
  56. int sock;
  57. };
  58. #endif

EasyTcpClient.hpp

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

   
  1. #ifndef _EasyTcpClient_hpp_
  2. #define _EasyTcpClient_hpp_
  3. #ifdef _WIN32
  4. #define WIN32_LEAN_AND_MEAN
  5. #define _WINSOCK_DEPRECATED_NO_WARNINGS //for inet_pton()
  6. #define _CRT_SECURE_NO_WARNINGS
  7. #include <windows.h>
  8. #include <WinSock2.h>
  9. #pragma comment(lib, "ws2_32.lib")
  10. #else
  11. #include <unistd.h>
  12. #include <sys/socket.h>
  13. #include <sys/types.h>
  14. #include <arpa/inet.h>
  15. #include <netinet/in.h>
  16. #include <sys/select.h>
  17. //在Unix下没有这些宏,为了兼容,自己定义
  18. #define SOCKET int
  19. #define INVALID_SOCKET (SOCKET)(~0)
  20. #define SOCKET_ERROR (-1)
  21. #endif
  22. #include <iostream>
  23. #include <string.h>
  24. #include <stdio.h>
  25. #include "MessageHeader.hpp"
  26. using namespace std;
  27. class EasyTcpClient
  28. {
  29. public:
  30. EasyTcpClient() :_sock(INVALID_SOCKET) {}
  31. virtual ~EasyTcpClient() {
  32. CloseSocket();
  33. }
  34. public:
  35. //判断当前客户端是否在运行
  36. bool isRun() { return _sock != INVALID_SOCKET; }
  37. //初始化socket
  38. void InitSocket();
  39. //连接服务器
  40. int ConnectServer(const char* ip, unsigned int port);
  41. //关闭socket
  42. void CloseSocket();
  43. //处理网络消息
  44. bool Onrun();
  45. /*
  46. 使用RecvData接收任何类型的数据,
  47. 然后将消息的头部字段传递给OnNetMessage()函数中,让其响应不同类型的消息
  48. */
  49. //接收数据
  50. int RecvData();
  51. //响应网络消息
  52. virtual void OnNetMessage(DataHeader* header);
  53. //发送数据
  54. int SendData(DataHeader* header);
  55. private:
  56. SOCKET _sock;
  57. };
  58. void EasyTcpClient::InitSocket()
  59. {
  60. //如果之前有连接了,关闭旧连接,开启新连接
  61. if (isRun())
  62. {
  63. std:: cout << "<Socket=" << ( int)_sock << ">:关闭旧连接,建立了新连接" << std:: endl;
  64. CloseSocket();
  65. }
  66. #ifdef _WIN32
  67. WORD ver = MAKEWORD( 2, 2);
  68. WSADATA dat;
  69. WSAStartup(ver, &dat);
  70. #endif
  71. _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  72. if (INVALID_SOCKET == _sock) {
  73. std:: cout << "ERROR:建立socket失败!" << std:: endl;
  74. }
  75. else {
  76. std:: cout << "<Socket=" << ( int)_sock << ">:建立socket成功!" << std:: endl;
  77. }
  78. }
  79. int EasyTcpClient::ConnectServer( const char* ip, unsigned int port)
  80. {
  81. if (!isRun())
  82. {
  83. InitSocket();
  84. }
  85. //声明要连接的服务端地址(注意,不同平台的服务端IP地址也不同)
  86. struct sockaddr_in _sin = {};
  87. #ifdef _WIN32
  88. _sin.sin_addr.S_un.S_addr = inet_addr(ip);
  89. #else
  90. _sin.sin_addr.s_addr = inet_addr(ip);
  91. #endif
  92. _sin.sin_family = AF_INET;
  93. _sin.sin_port = htons(port);
  94. //连接服务端
  95. int ret = connect(_sock, (struct sockaddr*)&_sin, sizeof(_sin));
  96. if (SOCKET_ERROR == ret) {
  97. std:: cout << "<Socket=" << ( int)_sock << ">:连接服务端(" << ip << "," << port << ")失败!" << std:: endl;
  98. }
  99. else {
  100. std:: cout << "<Socket=" << ( int)_sock << ">:连接服务端(" << ip << "," << port << ")成功!" << std:: endl;
  101. }
  102. return ret;
  103. }
  104. void EasyTcpClient::CloseSocket()
  105. {
  106. if (_sock != INVALID_SOCKET)
  107. {
  108. #ifdef _WIN32
  109. closesocket(_sock);
  110. WSACleanup();
  111. #else
  112. close(_sock);
  113. #endif
  114. _sock = INVALID_SOCKET;
  115. }
  116. }
  117. bool EasyTcpClient::Onrun()
  118. {
  119. if (isRun())
  120. {
  121. fd_set fdRead;
  122. FD_ZERO(&fdRead);
  123. FD_SET(_sock, &fdRead);
  124. struct timeval t = { 1, 0 };
  125. int ret = select(_sock + 1, &fdRead, NULL, NULL, &t);
  126. if (ret < 0)
  127. {
  128. std:: cout << "<Socket=" << _sock << ">:select出错!" << std:: endl;
  129. return false;
  130. }
  131. if (FD_ISSET(_sock, &fdRead)) //如果服务端有数据发送过来,接收显示数据
  132. {
  133. FD_CLR(_sock, &fdRead);
  134. if ( -1 == RecvData())
  135. {
  136. std:: cout << "<Socket=" << _sock << ">:数据接收失败,或服务端已断开!" << std:: endl;
  137. return false;
  138. }
  139. }
  140. return true;
  141. }
  142. return false;
  143. }
  144. int EasyTcpClient::RecvData()
  145. {
  146. /*接收数据规则:对于接收到的数据,先接收头部部分,
  147. 然后再接收实体部分,最后调用OnNetMessage()函数判断接收到的数据的类型
  148. */
  149. char szRecv[ 1024]; //设置接收缓冲区,并接收命令
  150. //先接收头部部分
  151. int _nLen = recv(_sock, szRecv, sizeof(DataHeader), 0);
  152. if (_nLen < 0) {
  153. std:: cout << "<Socket=" << _sock << ">:recv函数出错!" << std:: endl;
  154. return -1;
  155. }
  156. else if (_nLen == 0) {
  157. std:: cout << "<Socket=" << _sock << ">:接收数据失败,服务端已关闭!" << std:: endl;
  158. return -1;
  159. }
  160. //在此处还应该判断少包黏包的问题,但是现在处于单机处理状态,后面介绍到复杂的消息通信时再介绍
  161. DataHeader* header = (DataHeader*)szRecv;
  162. //接收实体部分
  163. recv(_sock, ( char*)szRecv + sizeof(DataHeader), header->dataLength + sizeof(DataHeader), 0);
  164. //判断接收到的数据的类型
  165. OnNetMessage(header);
  166. return 0;
  167. }
  168. void EasyTcpClient::OnNetMessage(DataHeader* header)
  169. {
  170. switch (header->cmd)
  171. {
  172. case CMD_LOGIN_RESULT: //如果返回的是登录的结果
  173. {
  174. LoginResult* loginResult = (LoginResult*)header;
  175. std:: cout << "<Socket=" << _sock << ">,收到服务端数据:CMD_LOGIN_RESULT,数据长度:" << loginResult->dataLength << ",结果为:" << loginResult->result << std:: endl;
  176. }
  177. break;
  178. case CMD_LOGOUT_RESULT: //如果是退出的结果
  179. {
  180. LogoutResult* logoutResult = (LogoutResult*)header;
  181. std:: cout << "<Socket=" << _sock << ">,收到服务端数据:CMD_LOGOUT_RESULT,数据长度:" << logoutResult->dataLength << ",结果为:" << logoutResult->result << std:: endl;
  182. }
  183. break;
  184. case CMD_NEW_USER_JOIN: //有新用户加入
  185. {
  186. NewUserJoin* newUserJoin = (NewUserJoin*)header;
  187. std:: cout << "<Socket=" << _sock << ">,收到服务端数据:CMD_NEW_USER_JOIN,数据长度:" << newUserJoin->dataLength << ",新用户Socket为:" << newUserJoin->sock << std:: endl;
  188. }
  189. break;
  190. }
  191. }
  192. int EasyTcpClient::SendData(DataHeader* header)
  193. {
  194. if (isRun() && header)
  195. {
  196. return send(_sock, ( const char*)header, header->dataLength, 0);
  197. }
  198. return SOCKET_ERROR;
  199. }
  200. #endif // !_EasyTcpClient_hpp_

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

测试程序如下


   
  1. #include "EasyTcpClient.hpp"
  2. #include <thread>
  3. void cmdThread(EasyTcpClient *client);
  4. int main()
  5. {
  6. EasyTcpClient client;
  7. //client.InitSocket();
  8. client.ConnectServer( "192.168.0.106", 4567);
  9. //启动线程,线程执行函数的参数传入client的指针
  10. std:: thread t1(cmdThread, &client);
  11. t1.detach(); //分离线程
  12. while (client.isRun())
  13. {
  14. client.Onrun();
  15. //Sleep(1000); 可以让发送与接受速度延迟1秒
  16. //std::cout << "空闲时间,处理其他业务..." << std::endl;
  17. }
  18. client.CloseSocket();
  19. std:: cout << "客户端停止工作!" << std:: endl;
  20. getchar(); //防止程序一闪而过
  21. return 0;
  22. }
  23. void cmdThread(EasyTcpClient *client)
  24. {
  25. char cmdBuf[ 256] = {};
  26. while ( true)
  27. {
  28. std:: cin >> cmdBuf;
  29. if ( 0 == strcmp(cmdBuf, "exit"))
  30. {
  31. std:: cout << "客户端退出" << std:: endl;
  32. client->CloseSocket();
  33. break;
  34. }
  35. else if ( 0 == strcmp(cmdBuf, "login"))
  36. {
  37. Login login;
  38. strcpy(login.userName, "dongshao");
  39. strcpy(login.PassWord, "123456");
  40. client->SendData(&login);
  41. }
  42. else if ( 0 == strcmp(cmdBuf, "logout"))
  43. {
  44. Logout logout;
  45. strcpy(logout.userName, "dongshao");
  46. client->SendData(&logout);
  47. }
  48. else {
  49. std:: cout << "命令不识别,请重新输入" << std:: endl;
  50. }
  51. }
  52. }
  • 编译如下:
g++ -g -o client client.cpp -std=c++11 -pthread

 

测试结果如下

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

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

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

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

测试程序如下

  • 使用多个客户端时,需要将客户端的代码和服务端的代码中的select参数4设置为非阻塞的形式

   
  1. struct timeval t = { 0, 0 };
  2. select(_sock + 1, &fdRead, NULL, NULL, &t);
  • 测试程序如下: 

   
  1. #include "EasyTcpClient.hpp"
  2. #include <thread>
  3. void cmdThread(EasyTcpClient *client);
  4. int main()
  5. {
  6. EasyTcpClient client1;
  7. //client1.InitSocket();
  8. client1.ConnectServer( "192.168.0.106", 4567);
  9. EasyTcpClient client2;
  10. client2.ConnectServer( "192.168.0.106", 4567);
  11. EasyTcpClient client3;
  12. client3.ConnectServer( "192.168.0.106", 4567);
  13. std:: thread t1(cmdThread, &client1);
  14. t1.detach();
  15. std:: thread t2(cmdThread, &client2);
  16. t2.detach();
  17. std:: thread t3(cmdThread, &client3);
  18. t3.detach();
  19. while (client1.isRun() || client2.isRun() || client3.isRun())
  20. {
  21. client1.Onrun();
  22. client2.Onrun();
  23. client3.Onrun();
  24. }
  25. client1.CloseSocket();
  26. client2.CloseSocket();
  27. client3.CloseSocket();
  28. std:: cout << "客户端全部工作!" << std:: endl;
  29. getchar(); //防止程序一闪而过
  30. return 0;
  31. }
  32. void cmdThread(EasyTcpClient *client)
  33. {
  34. char cmdBuf[ 256] = {};
  35. while ( true)
  36. {
  37. std:: cin >> cmdBuf;
  38. if ( 0 == strcmp(cmdBuf, "exit"))
  39. {
  40. std:: cout << "客户端退出" << std:: endl;
  41. client->CloseSocket();
  42. break;
  43. }
  44. else if ( 0 == strcmp(cmdBuf, "login"))
  45. {
  46. Login login;
  47. strcpy(login.userName, "dongshao");
  48. strcpy(login.PassWord, "123456");
  49. client->SendData(&login);
  50. }
  51. else if ( 0 == strcmp(cmdBuf, "logout"))
  52. {
  53. Logout logout;
  54. strcpy(logout.userName, "dongshao");
  55. client->SendData(&logout);
  56. }
  57. else {
  58. std:: cout << "命令不识别,请重新输入" << std:: endl;
  59. }
  60. }
  61. }
  • 编译如下:
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
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场