Linux中的TCP编程接口基本利用

打印 上一主题 下一主题

主题 1022|帖子 1022|积分 3066

TCP编程接口基本利用

本篇先容

在UDP编程接口基本利用已经先容过UDP编程相关的接口,本篇开始先容TCP编程相关的接口。有了UDP编程的基础,明白TCP相关的接口会更加容易,下面将按照两个方向利用TCP编程接口:

  • 基本利用TCP编程接口实现服务端和客户端通讯
  • 利用TCP编程实现客户端控制服务器执行相关命令的步调
创建并封装服务端

创建服务器类

与UDP一样,起首创建服务器类的基本框架,本次计划的服务器一旦启动就不再关闭,除非手动关闭,所以可以提供两个接口:

  • start:启动服务器
  • stop:制止服务器
基本结构如下:
  1. class TcpServer
  2. {
  3. public:
  4.     TcpServer()
  5.     {
  6.     }
  7.     // 启动服务器
  8.     void start()
  9.     {
  10.     }
  11.     // 停止服务器
  12.     void stop()
  13.     {
  14.     }
  15.     ~TcpServer()
  16.     {
  17.     }
  18. };
复制代码
创建服务器套接字

创建方式与UDP基本同等,只是在socket接口的第二个参数利用SOCK_STREAM而不再是SOCK_DGRAM,代码如下:
  1. class TcpServer
  2. {
  3. public:
  4.     TcpServer()
  5.         : _socketfd(-1)
  6.     {
  7.         // 创建服务器套接字
  8.         _socketfd = socket(AF_INET, SOCK_STREAM, 0);
  9.         if (_socketfd < 0)
  10.         {
  11.             LOG(LogLevel::FATAL) << "Server initiated error: " << strerror(errno);
  12.             exit(static_cast<int>(ErrorNumber::SocketFail));
  13.         }
  14.         LOG(LogLevel::INFO) << "Server initated: " << _socketfd;
  15.     }
  16.     // ...
  17. private:
  18.     int _socketfd;  // 服务器套接字
  19. };
复制代码
绑定服务器IP地址和端口

绑定方式与UDP基本同等,先利用原生的方式而不是直接利用封装后的sockaddr_in结构。在UDP编程接口基本利用部分已经提到过服务器不需要指定IP地址,所以本次一步到位,代码如下:
  1. // 默认端口
  2. const uint16_t default_port = 8080;
  3. class TcpServer
  4. {
  5. public:
  6.     TcpServer(uint16_t port = default_port)
  7.         : // ...
  8.         , _port(port)
  9.     {
  10.         // ...
  11.         struct sockaddr_in server;
  12.         server.sin_family = AF_INET;
  13.         server.sin_port = htons(_port);
  14.         server.sin_addr.s_addr = INADDR_ANY;
  15.         int ret = bind(_socketfd, reinterpret_cast<const struct sockaddr *>(&server), sizeof(server));
  16.         if (ret < 0)
  17.         {
  18.             LOG(LogLevel::FATAL) << "Bind error:" << strerror(errno);
  19.             exit(static_cast<int>(ErrorNumber::BindSocketFail));
  20.         }
  21.         LOG(LogLevel::INFO) << "Bind Success";
  22.     }
  23.     // ...
  24. private:
  25.     int _socketfd;  // 服务器套接字
  26.     uint16_t _port; // 服务器端口
  27. };
复制代码
开启服务器监听

在UDP部分,走完上面的步骤就已经完成了基本工作,一旦服务器启动就会等候连接。但是在TCP部分则不可,由于TCP是面向连接的,也就是说,利用客户端需要连接利用TCP的客户端必须先建立连接,只有连接建立完成了才可以开始通讯。为了可以让客户端和服务端乐成建立连接,起首需要让服务器处于监听状态,此时服务器只会一直等候客户端发起连接请求
在Linux中,实现服务器监听可以利用listen接口,其原型如下:
  1. int listen(int sockfd, int backlog);
复制代码
该接口的第一个参数表现当前需要作为传输的套接字,第二个参数表现等候中的客户端的最大个数。之所以会有第二个参数是由于一旦请求连接的客户端太多但是服务器又无法快速得做出响应就会导致用户一直处于等候连接状态从而造成不必要的损失。一般情况下第二个参数不发起设置比较大,而是由于应该根据实际情况决定,但是一定不能为0,本次大小定为8
当监听乐成,该接口会返回0,否则返回-1并设置对应的错误码
在TCP中,服务器一旦被创建那么久意味着其需要开始进行监听,所以本次考虑将监听放在构造中:
  1. // 默认最大支持排队等待连接的客户端个数
  2. const int max_backlog = 8;
  3. class TcpServer
  4. {
  5. public:
  6.     TcpServer(uint16_t port = default_port)
  7.         : _socketfd(-1), _port(port)
  8.     {
  9.         // ...
  10.         ret = listen(_socketfd, max_backlog);
  11.         if (ret < 0)
  12.         {
  13.             LOG(LogLevel::ERROR) << "Listen error:" << strerror(errno);
  14.             exit(static_cast<int>(ErrorNumber::ListenFail));
  15.         }
  16.         LOG(LogLevel::INFO) << "Listen Success";
  17.     }
  18.     // ...
  19. };
复制代码
启动服务器

在TCP中,启动服务器的逻辑和UDP的逻辑有一点差别,由于TCP服务器在启动之前先要进行监听,所以实际上此时服务器并没有进入IO状态,所以一旦启动服务器后,起首要做的就是一旦乐成建立连接就需要进入收发消息的状态
起首判断服务器是否启动,假如服务器本身已经启动就不需要再次启动,所以还是利用一个_isRunning变量作为判断条件,基本逻辑如下:
  1. // 启动服务器
  2. void start()
  3. {
  4.     if (!_isRunning)
  5.     {
  6.         _isRunning = true;
  7.         while (true)
  8.         {
  9.         }
  10.     }
  11. }
复制代码
接着就是在监听乐成的情况下进入IO状态,这里利用的接口就是accept,其原型如下:
  1. int accept(int sockfd, struct sockaddr * addr, socklen_t * addrlen);
复制代码
该接口的第一个参数表现需要绑定的服务器套接字,第二个参数表现对方的套接字结构,第二个参数表现对方套接字结构的大小,此中第二个参数和第三个参数均为输出型参数
需要注意的是该接口的返回值,当函数执行乐成时,该接口会返回一个套接字,这个套接字与前面通过socket接口获取到的套接字差别。在UDP中,只有一个套接字,就是socket的返回值,但是在TCP中,由于起首需要先监听,此时需要用到的实际上是监听套接字,一旦监听乐成,才会给定用于IO的套接字。所以实际上,在TCP中,socket接口的返回值对应的是listen用的套接字,而accept的套接字就是用于IO的套接字
基于上面的概念,现在对前面的代码进行一定的修正:对于前面的成员_socketfd,应该修改为_listen_socketfd:
  1. class TcpServer
  2. {
  3. public:
  4.     TcpServer(uint16_t port = default_port)
  5.         : _listen_socketfd(-1), _port(port), _isRunning(false)
  6.     {
  7.         // 创建服务器套接字
  8.         _listen_socketfd = socket(AF_INET, SOCK_STREAM, 0);
  9.         if (_listen_socketfd < 0)
  10.         {
  11.             LOG(LogLevel::FATAL) << "Server initiated error: " << strerror(errno);
  12.             exit(static_cast<int>(ErrorNumber::SocketFail));
  13.         }
  14.         LOG(LogLevel::INFO) << "Server initated: " << _listen_socketfd;
  15.         
  16.         int ret = bind(_listen_socketfd, reinterpret_cast<const struct sockaddr *>(&server), sizeof(server));
  17.         // ...
  18.         ret = listen(_listen_socketfd, max_backlog);
  19.         // ...
  20.     }
  21.     // ...
  22. private:
  23.     int _listen_socketfd; // 服务器监听套接字
  24.     // ...
  25. };
复制代码
接着,对于吸收乐成也可以创建一个成员变量_ac_socketfd,并用其吸收accept接口的返回值:
  1. class TcpServer
  2. {
  3. public:
  4.     TcpServer(uint16_t port = default_port)
  5.         : // ...
  6.         , _ac_socketfd(-1)
  7.     {
  8.         // ...
  9.     }
  10.     // 启动服务器
  11.     void start()
  12.     {
  13.         if (!_isRunning)
  14.         {
  15.             _isRunning = true;
  16.             while (true)
  17.             {
  18.                 struct sockaddr_in peer;
  19.                 socklen_t length = sizeof(peer);
  20.                 _ac_socketfd = accept(_listen_socketfd, reinterpret_cast<struct sockaddr *>(&peer), &length);
  21.                 if (_ac_socketfd < 0)
  22.                 {
  23.                     LOG(LogLevel::WARNING) << "Accept failed:" << strerror(errno);
  24.                     exit(static_cast<int>(ErrorNumber::AcceptFail));
  25.                 }
  26.                 LOG(LogLevel::INFO) << "Accept Success: " << _ac_socketfd;
  27.             }
  28.         }
  29.     }
  30.    
  31.     // ...
  32. private:
  33.     // ...
  34.     int _ac_socketfd;     // 服务器接收套接字
  35.     // ...
  36. };
复制代码
后续的代码与UDP思绪类似,但是具体实现有些差别。由于UDP是面向数据包的,所以只能「整发整取」,但是TCP是面向字节省的,所以可以「按照需求读取」而不需要「一定完整读取」,而在文件部分,读取和写入文件也是面向字节省的,所以在TCP中,读取和写入就可以直接利用文件的读写接口。但是需要注意,由于读写不是一次性的,所以需要一个循环控制持续读和写:
  1. // 启动服务器
  2. void start()
  3. {
  4.     if (!_isRunning)
  5.     {
  6.         while (true)
  7.         {
  8.             // ...
  9.             while (true)
  10.             {
  11.                 // 读取客户端消息
  12.                 char buffer[4096] = {0};
  13.                 ssize_t ret = read(_ac_socketfd, buffer, sizeof(buffer) - 1);
  14.                 if (ret > 0)
  15.                 {
  16.                     LOG(LogLevel::INFO) << "Client: " << inet_ntoa(peer.sin_addr) << ":" << std::to_string(ntohs(peer.sin_port)) << " send: " << buffer;
  17.                     // 向客户端回消息
  18.                     ret = write(_ac_socketfd, buffer, sizeof(buffer));
  19.                 }
  20.             }
  21.         }
  22.     }
  23. }
复制代码
制止服务器

制止服务器和UDP思绪同等,但是需要注意,除了要关闭吸收套接字以外还需要关闭监听套接字,此处不再赘述:
=== “制止服务器函数”
  1. // 停止服务器
  2. void stop()
  3. {
  4.     if (_isRunning)
  5.     {
  6.         close(_listen_socketfd);
  7.         close(_ac_socketfd);
  8.     }
  9. }
复制代码
=== “析构函数”
  1. ~TcpServer()
  2. {
  3.     stop();
  4. }
复制代码
创建并封装客户端

创建客户端类

与UDP同等,代码如下:
  1. class TcpClient
  2. {
  3. public:
  4.     TcpClient()
  5.     {
  6.     }
  7.     // 启动客户端
  8.     void start()
  9.     {
  10.     }
  11.     // 停止客户端
  12.     void stop()
  13.     {
  14.     }
  15.     ~TcpClient()
  16.     {
  17.     }
  18. };
复制代码
创建客户端套接字

与UDP同等,此处不再赘述:
  1. class TcpClient
  2. {
  3. public:
  4.     TcpClient()
  5.         : _socketfd(-1)
  6.     {
  7.         _socketfd = socket(AF_INET, SOCK_STREAM, 0);
  8.         if (_socketfd < 0)
  9.         {
  10.             LOG(LogLevel::FATAL) << "Client initiated error:" << strerror(errno);
  11.             exit(static_cast<int>(ErrorNumber::SocketFail));
  12.         }
  13.         LOG(LogLevel::INFO) << "Client initiated";
  14.     }
  15.     // ...
  16. private:
  17.     int _socketfd;
  18. };
复制代码
启动客户端

由于当前是TCP,所以客户端必须先与服务端建立连接才可以进行数据传输。在Linux中,让客户端连接服务端的接口是connect,其原型如下:
  1. int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
复制代码
该接口的第一个参数表现传送数据需要的套接字,第二个参数表现服务器的套接字结构,第三个参数表现第二个参数的大小
假如该接口连接乐成大概绑定乐成,则返回0,否则返回-1并且设置错误码
   需要注意,该接口会在乐成连接后自动绑定端口和IP地址,与UDP一样不需要用户手动设置客户端的IP地址和端口
  由于需要用到服务器的端口和IP地址,所以在创建客户端对象时需要让用户转达IP地址和端口,所以基本代码如下:
  1. // 默认服务器端口和IP地址
  2. const std::string default_ip = "127.0.0.1";
  3. const uint16_t default_port = 8080;
  4. class TcpClient
  5. {
  6. public:
  7.     TcpClient(const std::string &ip = default_ip, uint16_t port = default_port)
  8.         : // ...
  9.         , _isRunning(false), _ip(ip), _port(port)
  10.     {
  11.         // ...
  12.     }
  13.     // 启动客户端
  14.     void start()
  15.     {
  16.         if (!_isRunning)
  17.         {
  18.             _isRunning = true;
  19.             // 启动后就进行connect
  20.             struct sockaddr_in server;
  21.             server.sin_family = AF_INET;
  22.             server.sin_addr.s_addr = inet_addr(_ip.c_str());
  23.             server.sin_port = htons(_port);
  24.             int ret = connect(_socketfd, reinterpret_cast<const struct sockaddr *>(&server), sizeof(server));
  25.             if (ret < 0)
  26.             {
  27.                 LOG(LogLevel::WARNING) << "Connect failed" << strerror(errno);
  28.                 exit(static_cast<int>(ErrorNumber::ConnectFail));
  29.             }
  30.             LOG(LogLevel::INFO) << "Connect Success: " << _socketfd;
  31.             while (true)
  32.             {
  33.                 // ...
  34.             }
  35.         }
  36.     }
  37.     // ...
  38. private:
  39.     // ...
  40.     std::string _ip; // 服务器IP地址
  41.     uint16_t _port;  // 服务器端口
  42.     bool _isRunning; // 判断是否正在运行
  43. };
复制代码
在上面的代码中需要注意,不要把connect放在循环里,由于建立连接需要一次而不需要每一次发送都建立连接
接着就是写入和读取消息,基本思绪与UDP相同,代码如下:
  1. // 启动客户端
  2. void start()
  3. {
  4.     if (!_isRunning)
  5.     {
  6.         // ...
  7.         while (true)
  8.         {
  9.             // 向服务器写入
  10.             std::string message;
  11.             std::cout << "请输入消息:";
  12.             std::getline(std::cin, message);
  13.             ssize_t ret = write(_socketfd, message.c_str(), message.size());
  14.             // 收到消息
  15.             char buffer[4096] = {0};
  16.             ret = read(_socketfd, buffer, sizeof(buffer));
  17.             if (ret > 0)
  18.                 LOG(LogLevel::INFO) << "收到服务器消息:" << buffer;
  19.         }
  20.     }
  21. }
复制代码
制止客户端

制止客户端的思绪与UDP同等,此处不再赘述:
=== “制止客户端函数”
  1. // 停止客户端
  2. void stop()
  3. {
  4.     if (_isRunning)
  5.         close(_socketfd);
  6. }
复制代码
=== “析构函数”
  1. ~TcpClient()
  2. {
  3.     stop();
  4. }
复制代码
本地通讯测试

测试步骤:

  • 先启动服务端,再启动客户端
  • 客户端向服务器端发送信息
测试目的:

  • 客户端可以正常向服务器端发送信息
  • 服务端可以正常显示客户端信息并正常向客户端返回客户端发送的信息
  • 客户端可以正常显示服务端回复的信息
测试代码如下:
=== “客户端”
  1. #include "tcp_client.hpp"
  2. #include <memory>
  3. using namespace TcpClientModule;
  4. int main(int argc, char *argv[])
  5. {
  6.      std::shared_ptr<TcpClient> tcp_client;
  7.      if (argc == 1)
  8.      {
  9.          // 使用默认端口和IP地址
  10.          tcp_client = std::make_shared<TcpClient>();
  11.      }
  12.      else if (argc == 3)
  13.      {
  14.          std::string ip = argv[1];
  15.          std::uint16_t port = std::stoi(argv[2]);
  16.          // 使用自定义端口和IP地址
  17.          tcp_client = std::make_shared<TcpClient>(ip, port);
  18.      }
  19.      else
  20.      {
  21.          LOG(LogLevel::ERROR) << "错误使用,正确使用为:" << argv[0] << " IP地址 端口号(或者二者都不存在)";
  22.          exit(7);
  23.      }
  24.      
  25.      tcp_client->start();
  26.      tcp_client->stop();
  27.      return 0;
  28. }
复制代码
=== “服务端”
  1. #include "tcp_server.hpp"
  2. #include <memory>
  3. using namespace TcpServerModule;
  4. int main(int argc, char *argv[])
  5. {
  6.      std::shared_ptr<TcpServer> tcp_server;
  7.      if (argc == 1)
  8.      {
  9.          // 使用默认的端口
  10.          tcp_server = std::make_shared<TcpServer>();
  11.      }
  12.      else if (argc == 2)
  13.      {
  14.          // 使用自定义端口
  15.          std::string port = argv[1];
  16.          tcp_server = std::make_shared<TcpServer>(port);
  17.      }
  18.      else
  19.      {
  20.          LOG(LogLevel::ERROR) << "错误使用,正确方式:" << argv[0] << " 端口(可以省略)";
  21.          exit(6);
  22.      }
  23.      tcp_server->start();
  24.      tcp_server->stop();
  25.      return 0;
  26. }
复制代码
本次计划的客户端支持用户从命令行输入端口和IP地址,否则就直接利用默认,下面是一种结果:

客户端退出但服务端没有退出的问题

在UDP中,假如客户端退出但服务端没有退出,下一次客户端再连接该服务端时不会出现问题。但是在TCP中就并不是这样,例如:

从上图可以看到,假如客户端连接后断开再连接就会出现第二次连接发送消息无法得到回应。之所以出现这个问题就是由于服务器卡在了读写死循环中,办理这个问题的方式很简单,只需要判断read接口返回值是否为0,假如为0,阐明当前服务器并没有读取到任何内容,直接退出即可:
  1. // 启动服务器
  2. void start()
  3. {
  4.     if (!_isRunning)
  5.     {
  6.         // ...
  7.         while (true)
  8.         {
  9.             // ...
  10.             
  11.             while(true)
  12.             {
  13.                 // 读取客户端消息
  14.                 char buffer[4096] = {0};
  15.                 ssize_t ret = read(_ac_socketfd, buffer, sizeof(buffer) - 1);
  16.                 if (ret > 0)
  17.                 {
  18.                     // ...
  19.                 }
  20.                 else if (ret == 0)
  21.                 {
  22.                     LOG(LogLevel::INFO) << "Client disconnected: " << _ac_socketfd;
  23.                     break;
  24.                 }
  25.             }
  26.         }
  27.     }
  28. }
复制代码
此时便可以办理上面的问题:

文件描述符走漏问题

在上面的测试结果中可以发现,当客户端退出后再重新连接服务端,此时的文件描述符由4变成了5,但是实际上文件描述符是非常有限的,对于一般的用户机来说,文件描述符最大为1024,而服务器一般为65535,利用下面的指令可以查看:
  1. ulimit -a
复制代码
在结果中的open files一栏即可看到值
既然客户端已经退出了,那么对应的文件描述符就应该关闭而不是持续被占用着,此时就出现了文件描述符走漏问题。办理这个问题很简答,只需要在判断读取结果小于0时关闭文件描述符再退出即可:
  1. // 启动服务器
  2. void start()
  3. {
  4.     if (!_isRunning)
  5.     {
  6.         // ...
  7.         while (true)
  8.         {
  9.             // ...
  10.             // ...
  11.             close(_ac_socketfd);
  12.         }
  13.     }
  14. }
复制代码
测试云服务器与本地进行通讯

相同操作体系(客户端和服务端均为Linux)

测试云服务器与本地进行通讯最直接的步骤如下:

  • 将服务端步调拷贝到云服务器
  • 本地作为客户端,通过云服务器的公网IP地址连接云服务器的服务端
  • 客户端向云服务器发送信息
具体操作步骤与UDP类似,下面直接展示结果:

   与UDP一样需要注意安全组的问题,以阿里云为例,设置结果如下:

  差别操作体系(客户端为Windows,服务端为Linux)

由于Windows中利用接口和Linux中差不多,所以不会再详细先容,下面直接给出Windows客户端代码:
  1. #include <winsock2.h>
  2. #include <iostream>
  3. #include <string>
  4. #pragma warning(disable : 4996)
  5. #pragma comment(lib, "ws2_32.lib")
  6. std::string serverip = "47.113.217.80";  // 填写云服务器IP地址
  7. uint16_t serverport = 8888; // 填写云服务开放的端口号
  8. int main()
  9. {
  10.     WSADATA wsaData;
  11.     int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
  12.     if (result != 0)
  13.     {
  14.         std::cerr << "WSAStartup failed: " << result << std::endl;
  15.         return 1;
  16.     }
  17.     SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  18.     if (clientSocket == INVALID_SOCKET)
  19.     {
  20.         std::cerr << "socket failed" << std::endl;
  21.         WSACleanup();
  22.         return 1;
  23.     }
  24.     sockaddr_in serverAddr;
  25.     serverAddr.sin_family = AF_INET;
  26.     serverAddr.sin_port = htons(serverport);                  // 替换为服务器端口
  27.     serverAddr.sin_addr.s_addr = inet_addr(serverip.c_str()); // 替换为服务器IP地址
  28.     result = connect(clientSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr));
  29.     if (result == SOCKET_ERROR)
  30.     {
  31.         std::cerr << "connect failed" << std::endl;
  32.         closesocket(clientSocket);
  33.         WSACleanup();
  34.         return 1;
  35.     }
  36.     while (true)
  37.     {
  38.         std::string message;
  39.         std::cout << "Please Enter@ ";
  40.         std::getline(std::cin, message);
  41.         if(message.empty()) continue;
  42.         send(clientSocket, message.c_str(), message.size(), 0);
  43.         char buffer[1024] = {0};
  44.         int bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
  45.         if (bytesReceived > 0)
  46.         {
  47.             buffer[bytesReceived] = '\0'; // 确保字符串以 null 结尾
  48.             std::cout << "Received from server: " << buffer << std::endl;
  49.         }
  50.         else
  51.         {
  52.             std::cerr << "recv failed" << std::endl;
  53.         }
  54.     }
  55.     closesocket(clientSocket);
  56.     WSACleanup();
  57.     return 0;
  58. }
复制代码
运行结果如下:

多个客户端同时连接服务器

在上面已经测试过一个客户端连接一个服务端,接下来测试多个客户端连接服务端
基本现象

利用本地虚拟机和云服务器的客户端本地连接云服务器的服务端:
先利用虚拟机大概云服务器的客户端连接服务端:

可以看到正常连接,但是此时假如云服务器本地客户端连接云服务器的服务端:

此时就会发现,尽管云服务器客户端提示连接乐成,但是服务器却没有显示吸收。假如云服务器的客户端向服务器发送消息也不回得到回应:

假如终断虚拟机的连接,此时服务器又会显示连接乐成:

之所以会出现这个问题就是由于在上面的逻辑中:只有吸收乐成了才会发送消息,而一旦吸收乐成后,就在写入和读取中死循环,此时就导致accept不能继承吸收。办理这个问题就需要考虑到利用子进程大概新线程,将吸收和读写分别放在两个执行进程大概执行流中,根据这个思绪下面提供三种办理方案:

  • 子进程版本
  • 新线程版本
  • 线程池版本
子进程版本

计划子进程版本的本质就是让子进程执行读写方法,先将读写逻辑抽离到一个函数中:
=== “读写函数”
  1. // 读写函数
  2. void read_write_msg(struct sockaddr_in peer)
  3. {
  4.     while (true)
  5.     {
  6.         // 读取客户端消息
  7.         char buffer[4096] = {0};
  8.         ssize_t ret = read(_ac_socketfd, buffer, sizeof(buffer) - 1);
  9.         if (ret > 0)
  10.         {
  11.             LOG(LogLevel::INFO) << "Client: " << inet_ntoa(peer.sin_addr) << ":" << std::to_string(ntohs(peer.sin_port)) << " send: " << buffer;
  12.             // 向客户端回消息
  13.             ret = write(_ac_socketfd, buffer, sizeof(buffer));
  14.         }
  15.         else if (ret == 0)
  16.         {
  17.             LOG(LogLevel::INFO) << "Client disconnected: " << _ac_socketfd;
  18.             break;
  19.         }
  20.     }
  21.     close(_ac_socketfd);
  22. }
复制代码
=== “启动服务器函数”
  1. // 启动服务器
  2. void start()
  3. {
  4.     if (!_isRunning)
  5.     {
  6.         _isRunning = true;
  7.         while (true)
  8.         {
  9.             struct sockaddr_in peer;
  10.             socklen_t length = sizeof(peer);
  11.             _ac_socketfd = accept(_listen_socketfd, reinterpret_cast<struct sockaddr *>(&peer), &length);
  12.             if (_ac_socketfd < 0)
  13.             {
  14.                 LOG(LogLevel::WARNING) << "Accept failed:" << strerror(errno);
  15.                 exit(static_cast<int>(ErrorNumber::AcceptFail));
  16.             }
  17.             LOG(LogLevel::INFO) << "Accept Success: " << _ac_socketfd;
  18.             // 读写逻辑
  19.             read_write_msg(peer);
  20.         }
  21.     }
  22. }
复制代码
接着,为了让子进程执行对应的任务,起首就是创建一个子进程,此处直接利用原生接口:
  1. // 启动服务器
  2. void start()
  3. {
  4.     if (!_isRunning)
  5.     {
  6.         _isRunning = true;
  7.         while (true)
  8.         {
  9.             // ...
  10.             // 创建子进程
  11.             pid_t pid = fork();
  12.             if (pid == 0)
  13.             {
  14.                 // 子进程
  15.                 // 读写逻辑
  16.                 read_write_msg(peer);
  17.                 exit(0);
  18.             }
  19.         }
  20.     }
  21. }
复制代码
但是,这样写还不敷以办理问题,在Linux进程间通讯提到子进程会拷贝父进程描述符表,此时同样会导致文件描述符走漏问题,所以父进程和子进程都需要关闭本身不需要的文件描述符:对于父进程来说,其需要关闭读写用的文件描述符,由于写入和读取交给了子进程;对于子进程来说,其需要关闭监听用的文件描述符,由于继承监听其他客户端的连接由父进程进行
基于上面的思绪,代码如下:
  1. // 启动服务器
  2. void start()
  3. {
  4.     if (!_isRunning)
  5.     {
  6.         _isRunning = true;
  7.         while (true)
  8.         {
  9.             // ...
  10.             // 创建子进程
  11.             pid_t pid = fork();
  12.             if (pid == 0)
  13.             {
  14.                 // 子进程
  15.                 // 关闭监听文件描述符
  16.                 close(_listen_socketfd);
  17.                 // ...
  18.             }
  19.             // 父进程关闭读写描述符
  20.             close(_ac_socketfd);
  21.         }
  22.     }
  23. }
复制代码
一旦创建了子进程,父进程就需要对其进行等候并回收,假如不回收就会导致内存走漏问题,回收子进程的方式目前有下面两种:

  • 利用wait和waitpid接口进行等候
  • 借助子进程退出时发送的SIGCHILD信号,利用SIG_IGN行为
但是本次不利用上面的任意一种,而是考虑让子进程再创建一个子进程,一旦创建乐成就让当前子进程退出,而让新创建的子进程(孙子进程)继承执行后续的代码,由于当前子进程已经退出并且退出前并没有回收新创建的子进程(孙子进程),所以当前孙子进程就会被操作体系托管变成孤儿进程,一旦孙子进程走到了读写逻辑下面的exit(0)就会退出,此时操作体系就会自动回收这个孙子进程。这个思绪也被称为「双重fork(大概保卫进程化))」。所以,代码如下:
  1. // 启动服务器
  2. void start()
  3. {
  4.     if (!_isRunning)
  5.     {
  6.         _isRunning = true;
  7.         while (true)
  8.         {
  9.             // ...
  10.             // 创建子进程
  11.             pid_t pid = fork();
  12.             if (pid == 0)
  13.             {
  14.                 // 子进程
  15.                 // ...
  16.                 // 创建孙子进程
  17.                 if (fork())
  18.                     exit(0); // 当前子进程执行exit(0)
  19.                 // 孙子进程从此处继续向后执行
  20.                 // 读写逻辑
  21.                 read_write_msg(peer);
  22.                 exit(0);
  23.             }
  24.             // ...
  25.         }
  26.     }
  27. }
复制代码
现在,再进行上面的测试可以发现问题已包办理:


新线程版本

由于所有线程共享一个文件描述符表,所以不需要手动关闭一些文件描述符,下面利用前面封装的线程进行演示:
  1. // 启动服务器
  2. void start()
  3. {
  4.     if (!_isRunning)
  5.     {
  6.         _isRunning = true;
  7.         while (true)
  8.         {
  9.             // ...
  10.             // // 父进程关闭读写描述符
  11.             // close(_ac_socketfd);
  12.             // 创建新线程
  13.             Thread t(std::bind(&TcpServer::read_write_msg, this, peer));
  14.             t.start();
  15.         }
  16.     }
  17. }
复制代码
测试之后可以发现和子进程测试的效果一样,此处不再展示
线程池版本

线程池版本和新线程版本的思绪非常类似,给出代码不再演示:
  1. using task_t = std::function<void()>;
  2. // ...
  3. class TcpServer
  4. {
  5. public:
  6.     TcpServer(uint16_t port = default_port)
  7.         : _listen_socketfd(-1), _port(port), _isRunning(false), _ac_socketfd(-1)
  8.     {
  9.         // 创建服务器套接字
  10.         // ...
  11.         // 绑定
  12.         // ...
  13.         // 创建线程池
  14.         _tp = ThreadPool<task_t>::getInstance();
  15.         // 启动线程池
  16.         _tp->startThreads();
  17.         // 监听
  18.         // ...
  19.     }
  20.     // ...
  21.     // 启动服务器
  22.     void start()
  23.     {
  24.         if (!_isRunning)
  25.         {
  26.             _isRunning = true;
  27.             while (true)
  28.             {
  29.                 // ...
  30.                 // version-3
  31.                 _tp->pushTasks(std::bind(&TcpServer::read_write_msg, this, peer));
  32.             }
  33.         }
  34.     }
  35.     // 停止服务器
  36.     void stop()
  37.     {
  38.         if (_isRunning)
  39.         {
  40.             _tp->stopThreads();
  41.             // ...
  42.         }
  43.     }
  44.     ~TcpServer()
  45.     {
  46.         stop();
  47.     }
  48. private:
  49.     // ...
  50.     std::shared_ptr<ThreadPool<task_t>> _tp;
  51.     // ...
  52. };
复制代码
客户端控制服务器执行相关命令的步调

思绪分析

既然需要客户端控制服务器执行命令就必须要履历下面的步骤:

  • 客户端将命令字符串发送给服务端
  • 服务端创建子进程,利用进程间通讯将分析后的命令交给子进程,子进程调用exec家族函数将命令执行的结果通过服务器发送给客户端
实现

由于服务端本身就是进行吸收和返回结果,所以考虑将命令执行单独作为一个类来描述,本次为了执行的安全,考虑只允许用户执行部分命令,并且提供判断命令是否是合法命令,所以少不了需要查询的接口,为了更快速的查询,可以利用set集合。另外,由于要执行命令,所以需要一个成员函数executeCommand执行对应的命令,所以基本结构如下:
  1. class Command
  2. {
  3.     Command()
  4.     {
  5.         // 构造可以执行的一些命令
  6.         _commands.insert("ls");
  7.         _commands.insert("pwd");
  8.         _commands.insert("ll");
  9.         _commands.insert("touch");
  10.         _commands.insert("who");
  11.         _commands.insert("whoami");
  12.     }
  13.     // 判断命令是否合法
  14.     bool isValid(std::string cmd)
  15.     {
  16.         auto pos = _commands.find(cmd);
  17.         if (pos == _commands.end())
  18.             return false;
  19.         return true;
  20.     }
  21.     // 执行命令
  22.     std::string executeCommand(const std::string &cmd)
  23.     {
  24.     }
  25.     ~Command()
  26.     {
  27.     }
  28. private:
  29.     std::set<std::string> _commands;
  30. };
复制代码
接着,改变服务端的读写任务的接口,此处不再利用文件的read和write接口,而是利用recv和send接口,这两个接口只是比read和write多了flags,其余都一样,并且目前情况下flags设置为0即可:
  1. // 读写函数
  2. void read_write_msg(struct sockaddr_in peer)
  3. {
  4.     while (true)
  5.     {
  6.         // 读取客户端消息
  7.         char buffer[4096] = {0};
  8.         ssize_t ret = recv(_ac_socketfd, buffer, sizeof(buffer) - 1, 0);
  9.         if (ret > 0)
  10.         {
  11.             LOG(LogLevel::INFO) << "Client: " << inet_ntoa(peer.sin_addr) << ":" << std::to_string(ntohs(peer.sin_port)) << " send: " << buffer;
  12.             // 向客户端回消息
  13.             Command cmd;
  14.             if (cmd.isValid(buffer))
  15.             {
  16.                 // 命令合法可以执行命令
  17.                 std::string ret = cmd.executeCommand(buffer);
  18.                 send(_ac_socketfd, ret.c_str(), ret.size(), 0);
  19.             }
  20.             else
  21.             {
  22.                 send(_ac_socketfd, "错误指令", sizeof("错误指令"), 0);
  23.             }
  24.         }
  25.         // ...
  26.     }
  27.     // ...
  28. }
复制代码
接下来就是实现执行命令函数,根据前面的分析需要创建子进程调用exec家族函数执行对应的命令,但是在尺度库中有对应的接口已经实现了这个功能:popen,其原型如下:
  1. FILE *popen(const char *command, const char *type);
复制代码
对应的接口就是pclose接口,原型如下:
  1. int pclose(FILE *stream);
复制代码
对于popen接口来说,其会对传入的命令进行分析并创建子进程执行,将执行结果放到返回值中,由于FILE是文件结构,所以只需要利用文件的读写接口即可读取到此中的内容,这个接口第二个参数表现读模式大概写模式,由于是执行命令,所以只需要填入"r"即可
结合上面的接口即可完成对应的执行命令函数:
  1. std::string executeCommand(const std::string &cmd)
  2. {
  3.     FILE *fp = popen(cmd.c_str(), "r");
  4.     if (fp == nullptr)
  5.         return std::string();
  6.     char buffer[1024];
  7.     std::string result;
  8.     while (fgets(buffer, sizeof(buffer), fp))
  9.     {
  10.         result += buffer;
  11.     }
  12.     pclose(fp);
  13.     return result;
  14. }
复制代码
!!! note
fgets会自动添加\0,不需要预留\0的位置
测试

服务端主函数代码和客户端主函数代码稳定,下面是测试结果:


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

钜形不锈钢水箱

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表