当前位置:   article > 正文

Linux/C++:基于TCP协议实现网络版本计算器(自定义应用层协议)_linuxc++ tcpserver

linuxc++ tcpserver

目录

Sock.hpp

TcpServer.hpp

Protocol.hpp

CalServer.cc

CalClient.cc

分析


因为,TCP面向字节流,所以TCP有粘包问题,故我们需要应用层协议来区分每一个数据包。防止读取到半个,一个半数据包的情况。

Sock.hpp

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. #include <cstring>
  5. #include <unistd.h>
  6. #include <sys/types.h>
  7. #include <sys/socket.h>
  8. #include <arpa/inet.h>
  9. #include <netinet/in.h>
  10. #include "log.hpp"
  11. // 对于一些TCP相关调用的封装
  12. class Sock
  13. {
  14. private:
  15. const static int gback_log = 20;
  16. public:
  17. int Socket()
  18. {
  19. // 1. 创建套接字,成功返回对应套接字,失败直接进程exit
  20. int listen_sock = socket(AF_INET, SOCK_STREAM, 0); // 网络套接字, 面向字节流(tcp)
  21. if (listen_sock < 0)
  22. {
  23. logMessage(FATAL, "create listen socket error, %d:%s", errno, strerror(errno));
  24. exit(2);
  25. }
  26. logMessage(NORMAL, "create listen socket success: %d", listen_sock); // 1111Log
  27. return listen_sock;
  28. }
  29. void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
  30. {
  31. // 2. bind,注意云服务器不能绑定公网IP,不允许。
  32. // 成功bind则成功bind,失败进程exit(bind不在循环语句内,故失败直接进程退出。)
  33. struct sockaddr_in local;
  34. memset(&local, 0, sizeof local);
  35. local.sin_family = AF_INET;
  36. local.sin_port = htons(port);
  37. local.sin_addr.s_addr = inet_addr(ip.c_str());
  38. if (bind(sock, (struct sockaddr *)&local, sizeof local) < 0)
  39. {
  40. logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
  41. exit(3);
  42. }
  43. }
  44. void Listen(int sock)
  45. {
  46. // 3. listen监听: 因为TCP是面向连接的,在我们正式通信之前,需要先建立连接
  47. // listen: 将套接字状态设置为监听状态。服务器要一直处于等待状态,这样客户端才能随时随地发起连接。
  48. // 成功则成功,失败则exit
  49. if (listen(sock, gback_log) < 0) // gback_log后面讲,全连接队列的长度。我猜测就是这个服务器同一时刻允许连接的客户端的数量最大值?也不太对呀,这么少么?
  50. {
  51. logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
  52. exit(4);
  53. }
  54. logMessage(NORMAL, "listen success");
  55. }
  56. // 一般经验
  57. // const std::string &: 输入型参数
  58. // std::string *: 输出型参数
  59. // std::string &: 输入输出型参数
  60. int Accept(int sock, uint16_t *port, std::string *ip)
  61. {
  62. // accept失败进程不退出,返回-1
  63. // 成功则返回对应的通信套接字
  64. struct sockaddr_in client;
  65. socklen_t len = sizeof client;
  66. // 其实accept是获取已经建立好的TCP连接。建立好的连接在一个内核队列中存放,最大数量的第二个参数+1
  67. int service_sock = accept(sock, (struct sockaddr *)&client, &len); // 返回一个用于与客户端进行网络IO的套接字,不同于listen_sock
  68. // On success, these system calls return a nonnegative integer that is a descriptor for the accepted socket. On error, -1 is returned, and errno is set appropriately.
  69. if (service_sock < 0)
  70. {
  71. logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
  72. return -1; // accept失败不直接exit,而是返回-1。因为在循环语句内部。
  73. }
  74. if (port)
  75. *port = ntohs(client.sin_port);
  76. if (ip)
  77. *ip = inet_ntoa(client.sin_addr);
  78. logMessage(NORMAL, "link(accept) success, service socket: %d | %s:%d", service_sock,
  79. (*ip).c_str(), *port);
  80. return service_sock;
  81. }
  82. int Connect(int sock, const std::string &ip, const uint16_t &port)
  83. {
  84. // 惯例写一下:失败返回-1,成功则客户端与服务端连接成功,返回0
  85. struct sockaddr_in server;
  86. memset(&server, 0, sizeof server);
  87. server.sin_family = AF_INET;
  88. server.sin_addr.s_addr = inet_addr(ip.c_str());
  89. server.sin_port = htons(port);
  90. if (connect(sock, (struct sockaddr *)&server, sizeof server) < 0)
  91. {
  92. return -1;
  93. }
  94. return 0;
  95. }
  96. public:
  97. Sock() = default;
  98. ~Sock() = default;
  99. };

TcpServer.hpp

  1. #ifndef _TCP_SERVER_HPP_
  2. #define _TCP_SERVER_HPP_
  3. #include "Sock.hpp"
  4. #include <vector>
  5. #include <functional>
  6. // 说实话,这个TcpServer类实现的非常棒,真的很棒,网络和服务进行了解耦。
  7. // 使用者直接BindServer, 然后start即可
  8. namespace ns_tcpserver
  9. {
  10. using func_t = std::function<void(int socket)>; // 服务器提供的服务方法类型void(int),可变
  11. class TcpServer;
  12. class ThreadData
  13. {
  14. public:
  15. ThreadData(int sock, TcpServer *server)
  16. : _sock(sock), _server(server)
  17. {}
  18. ~ThreadData() {}
  19. public:
  20. int _sock;
  21. TcpServer *_server; // 因为静态成员函数呀
  22. };
  23. class TcpServer
  24. {
  25. // 不关心bind的ip和port,因为用不到啊,保留一个listen_sock用于accept就够了。
  26. private:
  27. int _listen_sock;
  28. Sock _sock;
  29. std::vector<func_t> _funcs; // 服务器提供的服务
  30. private:
  31. static void *threadRoutine(void *args)
  32. {
  33. pthread_detach(pthread_self()); // 线程分离(避免类似于僵尸进程状态)
  34. ThreadData *td = (ThreadData *)args;
  35. td->_server->excute(td->_sock); // 提供服务
  36. close(td->_sock); // 保证四次挥手正常结束
  37. delete td;
  38. return nullptr;
  39. }
  40. public:
  41. TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0")
  42. {
  43. // 创建监听套接字,bind,listen
  44. _listen_sock = _sock.Socket();
  45. _sock.Bind(_listen_sock, port, ip);
  46. _sock.Listen(_listen_sock);
  47. }
  48. void start()
  49. {
  50. for (;;)
  51. {
  52. // 开始accept,然后执行任务
  53. std::string ip;
  54. uint16_t port; // 这两个东西,也并没有传给线程。
  55. int sock = _sock.Accept(_listen_sock, &port, &ip); // 后面是输出型参数
  56. if (sock == -1)
  57. continue; // 本次accept失败,循环再次accept。目前来看几乎不会
  58. // 连接客户端成功,ip port已有。但是这里没用...
  59. pthread_t tid;
  60. ThreadData *td = new ThreadData(sock, this);
  61. pthread_create(&tid, nullptr, threadRoutine, (void *)td); // 新线程去提供service,主线程继续accept
  62. }
  63. }
  64. void bindService(func_t service) // 暴露出去的接口,用于设置该服务器的服务方法
  65. {
  66. _funcs.push_back(service);
  67. }
  68. void excute(int sock)
  69. {
  70. for (auto &func : _funcs)
  71. {
  72. func(sock);
  73. }
  74. }
  75. ~TcpServer()
  76. {
  77. if (_listen_sock >= 0)
  78. close(_listen_sock);
  79. }
  80. };
  81. }
  82. #endif

Protocol.hpp

  1. #ifndef _PROTOCOL_HPP_
  2. #define _PROTOCOL_HPP_
  3. #include <iostream>
  4. #include <string>
  5. #include <cstring>
  6. #include <sys/types.h>
  7. #include <sys/socket.h>
  8. #include <arpa/inet.h>
  9. #include <netinet/in.h>
  10. #include <jsoncpp/json/json.h>
  11. // important and new
  12. namespace ns_protocol
  13. {
  14. // #define MYSELF 1 // 自己实现序列化反序列化还是使用json库
  15. #define SPACE " "
  16. #define SPACE_LENGTH strlen(SPACE)
  17. #define SEP "\r\n"
  18. #define SEP_LENGTH strlen(SEP)
  19. // 请求和回复,都需要序列化和反序列化的成员函数
  20. // 序列化和反序列化双方都不同。但是添加报头和去报头是相同的,"Length\r\nxxxxx\r\n";
  21. // 客户端生成请求,序列化之后发送给服务端
  22. class Request
  23. {
  24. public:
  25. Request() = default;
  26. Request(int x, int y, char op)
  27. : _x(x), _y(y), _op(op)
  28. {
  29. }
  30. ~Request() {}
  31. public:
  32. int _x;
  33. int _y;
  34. char _op;
  35. public:
  36. std::string serialize()
  37. {
  38. // 序列化为"_x _op _y" (注意,序列化和添加报头是分开的,反序列化和去掉报头是分开的
  39. #ifdef MYSELF
  40. std::string s = std::to_string(_x);
  41. s += SPACE;
  42. s += _op;
  43. s += SPACE;
  44. s += std::to_string(_y);
  45. return s;
  46. #else
  47. Json::Value root;
  48. root["x"] = _x;
  49. root["y"] = _y;
  50. root["op"] = _op;
  51. Json::FastWriter writer;
  52. return writer.write(root);
  53. #endif
  54. }
  55. bool deserialize(const std::string &s)
  56. {
  57. #ifdef MYSELF
  58. // "_x _op _y"
  59. std::size_t left = s.find(SPACE);
  60. if (left == std::string::npos)
  61. return false;
  62. std::size_t right = s.rfind(SPACE);
  63. if (right == left)
  64. return false;
  65. _x = atoi(s.substr(0, left).c_str());
  66. _op = s[left + SPACE_LENGTH];
  67. _y = atoi(s.substr(right + SPACE_LENGTH).c_str());
  68. #else
  69. Json::Value root;
  70. Json::Reader reader;
  71. reader.parse(s, root);
  72. _x = root["x"].asInt();
  73. _y = root["y"].asInt();
  74. _op = root["op"].asInt();
  75. #endif
  76. return true;
  77. }
  78. };
  79. // 服务端收到请求,反序列化,业务处理生成response,序列化后发送给客户端
  80. class Response
  81. {
  82. public:
  83. Response(int result = 0, int code = 0)
  84. : _result(result), _code(code)
  85. {
  86. }
  87. ~Response() {}
  88. public:
  89. std::string serialize()
  90. {
  91. // 序列化为"_code _result" (注意,序列化和添加报头是分开的,反序列化和去掉报头是分开的
  92. #ifdef MYSELF
  93. std::string s = std::to_string(_code);
  94. s += SPACE;
  95. s += std::to_string(_result);
  96. return s;
  97. #else
  98. Json::Value root;
  99. root["code"] = _code;
  100. root["result"] = _result;
  101. Json::FastWriter writer;
  102. return writer.write(root);
  103. #endif
  104. }
  105. bool deserialize(const std::string &s)
  106. {
  107. #ifdef MYSELF
  108. // "_code _result"
  109. std::size_t pos = s.find(SPACE);
  110. if (pos == std::string::npos)
  111. return false;
  112. _code = atoi(s.substr(0, pos).c_str());
  113. _result = atoi(s.substr(pos + SPACE_LENGTH).c_str());
  114. #else
  115. Json::Value root;
  116. Json::Reader reader;
  117. reader.parse(s, root);
  118. _result = root["result"].asInt();
  119. _code = root["code"].asInt();
  120. #endif
  121. return true;
  122. }
  123. public:
  124. int _result;
  125. int _code; // 状态码, 防止除零,模零,和其他错误(比如非法运算符运算符)。code == 0时,result有效。
  126. };
  127. // 进行去报头,报文完整则去报头,并返回有效载荷,不完整则代表失败返回空字符串。
  128. std::string deCode(std::string &s) // 输入型输出型参数
  129. {
  130. // "Length\r\nx op y\r\n" 成功返回有效载荷,失败返回空串
  131. std::size_t left = s.find(SEP);
  132. if (left == std::string::npos)
  133. return "";
  134. std::size_t right = s.rfind(SEP);
  135. if (right == left)
  136. return "";
  137. int length = atoi(s.substr(0, left).c_str());
  138. if (length > s.size() - left - 2 * SEP_LENGTH)
  139. return ""; // 有效载荷长度不足,不是一个完整报文,其实经过上面两次的if判断已经够了可能。
  140. // 是一个完整报文,进行提取
  141. std::string ret;
  142. s.erase(0, left + SEP_LENGTH);
  143. ret = s.substr(0, length);
  144. s.erase(0, length + SEP_LENGTH);
  145. return ret;
  146. }
  147. std::string enCode(const std::string &s)
  148. {
  149. // "Length\r\n1+1\r\n"
  150. std::string retStr = std::to_string(s.size());
  151. retStr += SEP;
  152. retStr += s;
  153. retStr += SEP;
  154. return retStr;
  155. }
  156. // 我真的很想用引用,但是好像传统规则是输出型参数用指针...
  157. // 其实这个Recv就是一个单纯的读数据的函数,将接收缓冲区数据读到应用层缓冲区中,也就是*s中。存储的是对端发来的应用层报文。
  158. bool Recv(int sock, std::string *s)
  159. {
  160. // 仅仅读取数据到*s中
  161. char buff[1024];
  162. ssize_t sz = recv(sock, buff, sizeof buff, 0);
  163. if (sz > 0)
  164. {
  165. buff[sz] = '\0';
  166. *s += buff;
  167. return true;
  168. }
  169. else if (sz == 0)
  170. {
  171. std::cout << "peer quit" << std::endl;
  172. return false;
  173. }
  174. else
  175. {
  176. std::cout << "recv error" << std::endl;
  177. return false;
  178. }
  179. }
  180. bool Send(int sock, const std::string &s)
  181. {
  182. ssize_t sz = send(sock, s.c_str(), s.size(), 0);
  183. if (sz > 0)
  184. {
  185. return true;
  186. }
  187. else
  188. {
  189. std::cout << "send error!" << std::endl;
  190. return false;
  191. }
  192. }
  193. }
  194. #endif

CalServer.cc

  1. #include "TcpServer.hpp"
  2. #include "Protocol.hpp"
  3. #include <memory>
  4. using namespace ns_tcpserver;
  5. using namespace ns_protocol;
  6. Response calculatorHelp(const Request &req)
  7. {
  8. // "1+1"???
  9. Response resp;
  10. int x = req._x;
  11. int y = req._y;
  12. switch (req._op)
  13. {
  14. case '+':
  15. resp._result = x + y;
  16. break;
  17. case '-':
  18. resp._result = x - y;
  19. break;
  20. case '*':
  21. resp._result = x * y;
  22. break;
  23. case '/':
  24. if (y == 0)
  25. resp._code = 1;
  26. else
  27. resp._result = x / y;
  28. break;
  29. case '%':
  30. if (y == 0)
  31. resp._code = 2;
  32. else
  33. resp._result = x % y;
  34. break;
  35. default:
  36. resp._code = 3;
  37. break;
  38. }
  39. return resp;
  40. }
  41. void calculator(int sock)
  42. {
  43. std::string s;
  44. for (;;)
  45. {
  46. if (Recv(sock, &s) <= 0) // 输出型参数
  47. break; // 大概率对端退出,则服务结束。一般不会读取失败recv error
  48. std::string package = deCode(s);
  49. if (package.empty())
  50. continue; // 不是一个完整报文,继续读取(因为TCP面向字节流!!!)
  51. // 读取到一个完整报文,且已经去了应用层报头,有效载荷在package中。如"1 + 2"
  52. Request req;
  53. req.deserialize(package);
  54. Response resp = calculatorHelp(req);
  55. std::string backStr = resp.serialize();
  56. backStr = enCode(backStr);
  57. if (!Send(sock, backStr)) // 发送失败就退出
  58. break;
  59. }
  60. }
  61. // ./cal_server port
  62. int main(int argc, char **argv)
  63. {
  64. // std::cout << "test remake" << std::endl; // success
  65. if (argc != 2)
  66. {
  67. std::cout << "\nUsage: " << argv[0] << " port\n"
  68. << std::endl;
  69. exit(1);
  70. }
  71. std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));
  72. server->bindService(calculator); // 给服务器设置服务方法,将网络服务和业务逻辑进行解耦
  73. server->start(); // 服务器开始进行accept,连接一个client之后就提供上方bind的服务
  74. return 0;
  75. }

CalClient.cc

  1. #include "Protocol.hpp"
  2. #include "Sock.hpp"
  3. #include <memory>
  4. using namespace ns_protocol;
  5. // ./client serverIp serverPort
  6. int main(int argc, char **argv)
  7. {
  8. if (argc != 3)
  9. {
  10. std::cout << "\nUsage: " << argv[0] << " serverIp serverPort\n"
  11. << std::endl;
  12. exit(1);
  13. }
  14. Sock sock;
  15. int sockfd = sock.Socket();
  16. // 客户端不需要显式bind, 老生常谈了。
  17. if (sock.Connect(sockfd, argv[1], atoi(argv[2])) == -1)
  18. {
  19. std::cout << "connect error" << std::endl;
  20. exit(3);
  21. }
  22. std::string backStr; //
  23. bool quit = false;
  24. while (!quit)
  25. {
  26. Request req;
  27. std::cout << "Please enter# ";
  28. std::cin >> req._x >> req._op >> req._y;
  29. std::string reqStr = req.serialize();
  30. reqStr = enCode(reqStr); // 添加应用层报头,此处添加报头(制定协议)是为了解决TCP粘包问题,因为TCP是面向字节流的。
  31. if (!Send(sockfd, reqStr))
  32. break;
  33. while (true)
  34. {
  35. if (!Recv(sockfd, &backStr))
  36. {
  37. quit = true;
  38. break;
  39. }
  40. std::string package = deCode(backStr);
  41. if(package.empty())
  42. continue; // 这次不是一个完整的应用层报文,继续读取
  43. // 读取到一个完整的应用层报文,且已经去报头,获取有效载荷成功,在package中。(这个有效载荷是server发来的,计算结果)
  44. Response resp;
  45. resp.deserialize(package);
  46. switch (resp._code)
  47. {
  48. case 1:
  49. std::cout << "除零错误" << std::endl;
  50. break;
  51. case 2:
  52. std::cout << "模零错误" << std::endl;
  53. break;
  54. case 3:
  55. std::cout << "其他错误" << std::endl;
  56. break;
  57. default:
  58. std::cout << req._x << " " << req._op << " " << req._y << " = " << resp._result << std::endl;
  59. break;
  60. }
  61. break; // 退出防止TCP粘包问题的循环。
  62. }
  63. // 进行下一次获取用户输入,进行计算。
  64. }
  65. close(sockfd);
  66. return 0;
  67. }

分析

可以分为两个模块:网络通信模块,应用层模块(包括应用层协议,以及应用层计算器逻辑)。

网络模块中,Sock.hpp就是一个简单的对于系统调用的封装,TcpServer.hpp的设计很优雅,内部有一个std::vector<func_t> _funcs;即这个server提供的服务。对外提供一个BindServer的方法,可以指定这个服务器提供的服务。Start,为服务器开始方法,先accept获取与客户端建立好的连接,然后创建新线程给客户端提供服务,服务就是BindServer绑定的方法,类型为void(int)。

应用层协议:一个Request,一个Response。分别是客户端的请求(x,y,运算符)和服务端的响应(计算结果)。这两个类,都有序列化和反序列化的方法,便于网络传输。还有一个Encode添加报头和Decode去报头的方法,这个其实就是应用层协议的报头,大体格式为 Length\r\nxxxx\r\n。目的就是解决TCP面向字节流所引起的粘包问题。
Recv内部就是一个recv调用,将读取的网络数据添加到一个输出型参数string*指向string的结尾。因为TCP粘包问题,所以可能读取的不是一个完整报文(半个?),故,在Decode方法内部,也就是去报头时,会检测此时是否有至少一个完整应用层报文。若有,则去报头,获取有效载荷。若没有则返回一个空串。上层可以通过判断是否为空串。判断是否读到了一个完整应用层报文,若没有,则再次Recv,直到读到一个完整应用层报文为止。所以,server和client在读取网络数据并去报头时,都是在while循环内部进行的。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/空白诗007/article/detail/978221
推荐阅读
相关标签
  

闽ICP备14008679号