当前位置: 首页 > article >正文

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

TCP编程接口基本使用

本篇介绍

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

  1. 基本使用TCP编程接口实现服务端和客户端通信
  2. 使用TCP编程实现客户端控制服务器执行相关命令的程序

创建并封装服务端

创建服务器类

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

  1. start:启动服务器
  2. stop:停止服务器

基本结构如下:

class TcpServer
{
public:TcpServer(){}// 启动服务器void start(){}// 停止服务器void stop(){}~TcpServer(){}
};

创建服务器套接字

创建方式与UDP基本一致,只是在socket接口的第二个参数使用SOCK_STREAM而不再是SOCK_DGRAM,代码如下:

class TcpServer
{
public:TcpServer(): _socketfd(-1){// 创建服务器套接字_socketfd = socket(AF_INET, SOCK_STREAM, 0);if (_socketfd < 0){LOG(LogLevel::FATAL) << "Server initiated error: " << strerror(errno);exit(static_cast<int>(ErrorNumber::SocketFail));}LOG(LogLevel::INFO) << "Server initated: " << _socketfd;}// ...private:int _socketfd;  // 服务器套接字
};

绑定服务器IP地址和端口

绑定方式与UDP基本一致,先使用原生的方式而不是直接使用封装后的sockaddr_in结构。在UDP编程接口基本使用部分已经提到过服务器不需要指定IP地址,所以本次一步到位,代码如下:

// 默认端口
const uint16_t default_port = 8080;class TcpServer
{
public:TcpServer(uint16_t port = default_port): // ..., _port(port){// ...struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(_port);server.sin_addr.s_addr = INADDR_ANY;int ret = bind(_socketfd, reinterpret_cast<const struct sockaddr *>(&server), sizeof(server));if (ret < 0){LOG(LogLevel::FATAL) << "Bind error:" << strerror(errno);exit(static_cast<int>(ErrorNumber::BindSocketFail));}LOG(LogLevel::INFO) << "Bind Success";}// ...private:int _socketfd;  // 服务器套接字uint16_t _port; // 服务器端口
};

开启服务器监听

在UDP部分,走完上面的步骤就已经完成了基本工作,一旦服务器启动就会等待连接。但是在TCP部分则不行,因为TCP是面向连接的,也就是说,使用客户端需要连接使用TCP的客户端必须先建立连接,只有连接建立完成了才可以开始通信。为了可以让客户端和服务端成功建立连接,首先需要让服务器处于监听状态,此时服务器只会一直等待客户端发起连接请求

在Linux中,实现服务器监听可以使用listen接口,其原型如下:

int listen(int sockfd, int backlog);

该接口的第一个参数表示当前需要作为传输的套接字,第二个参数表示等待中的客户端的最大个数。之所以会有第二个参数是因为一旦请求连接的客户端太多但是服务器又无法快速得做出响应就会导致用户一直处于等待连接状态从而造成不必要的损失。一般情况下第二个参数不建议设置比较大,而是因为应该根据实际情况决定,但是一定不能为0,本次大小定为8

当监听成功,该接口会返回0,否则返回-1并设置对应的错误码

在TCP中,服务器一旦被创建那么久意味着其需要开始进行监听,所以本次考虑将监听放在构造中:

// 默认最大支持排队等待连接的客户端个数
const int max_backlog = 8;class TcpServer
{
public:TcpServer(uint16_t port = default_port): _socketfd(-1), _port(port){// ...ret = listen(_socketfd, max_backlog);if (ret < 0){LOG(LogLevel::ERROR) << "Listen error:" << strerror(errno);exit(static_cast<int>(ErrorNumber::ListenFail));}LOG(LogLevel::INFO) << "Listen Success";}// ...
};

启动服务器

在TCP中,启动服务器的逻辑和UDP的逻辑有一点不同,因为TCP服务器在启动之前先要进行监听,所以实际上此时服务器并没有进入IO状态,所以一旦启动服务器后,首先要做的就是一旦成功建立连接就需要进入收发消息的状态

首先判断服务器是否启动,如果服务器本身已经启动就不需要再次启动,所以还是使用一个_isRunning变量作为判断条件,基本逻辑如下:

// 启动服务器
void start()
{if (!_isRunning){_isRunning = true;while (true){}}
}

接着就是在监听成功的情况下进入IO状态,这里使用的接口就是accept,其原型如下:

int accept(int sockfd, struct sockaddr * addr, socklen_t * addrlen);

该接口的第一个参数表示需要绑定的服务器套接字,第二个参数表示对方的套接字结构,第二个参数表示对方套接字结构的大小,其中第二个参数和第三个参数均为输出型参数

需要注意的是该接口的返回值,当函数执行成功时,该接口会返回一个套接字,这个套接字与前面通过socket接口获取到的套接字不同。在UDP中,只有一个套接字,就是socket的返回值,但是在TCP中,因为首先需要先监听,此时需要用到的实际上是监听套接字,一旦监听成功,才会给定用于IO的套接字。所以实际上,在TCP中,socket接口的返回值对应的是listen用的套接字,而accept的套接字就是用于IO的套接字

基于上面的概念,现在对前面的代码进行一定的修正:对于前面的成员_socketfd,应该修改为_listen_socketfd

class TcpServer
{
public:TcpServer(uint16_t port = default_port): _listen_socketfd(-1), _port(port), _isRunning(false){// 创建服务器套接字_listen_socketfd = socket(AF_INET, SOCK_STREAM, 0);if (_listen_socketfd < 0){LOG(LogLevel::FATAL) << "Server initiated error: " << strerror(errno);exit(static_cast<int>(ErrorNumber::SocketFail));}LOG(LogLevel::INFO) << "Server initated: " << _listen_socketfd;int ret = bind(_listen_socketfd, reinterpret_cast<const struct sockaddr *>(&server), sizeof(server));// ...ret = listen(_listen_socketfd, max_backlog);// ...}// ...
private:int _listen_socketfd; // 服务器监听套接字// ...
};

接着,对于接收成功也可以创建一个成员变量_ac_socketfd,并用其接收accept接口的返回值:

class TcpServer
{
public:TcpServer(uint16_t port = default_port): // ..., _ac_socketfd(-1){// ...}// 启动服务器void start(){if (!_isRunning){_isRunning = true;while (true){struct sockaddr_in peer;socklen_t length = sizeof(peer);_ac_socketfd = accept(_listen_socketfd, reinterpret_cast<struct sockaddr *>(&peer), &length);if (_ac_socketfd < 0){LOG(LogLevel::WARNING) << "Accept failed:" << strerror(errno);exit(static_cast<int>(ErrorNumber::AcceptFail));}LOG(LogLevel::INFO) << "Accept Success: " << _ac_socketfd;}}}// ...private:// ...int _ac_socketfd;     // 服务器接收套接字// ...
};

后续的代码与UDP思路类似,但是具体实现有些不同。因为UDP是面向数据包的,所以只能「整发整取」,但是TCP是面向字节流的,所以可以「按照需求读取」而不需要「一定完整读取」,而在文件部分,读取和写入文件也是面向字节流的,所以在TCP中,读取和写入就可以直接使用文件的读写接口。但是需要注意,因为读写不是一次性的,所以需要一个循环控制持续读和写:

// 启动服务器
void start()
{if (!_isRunning){while (true){// ...while (true){// 读取客户端消息char buffer[4096] = {0};ssize_t ret = read(_ac_socketfd, buffer, sizeof(buffer) - 1);if (ret > 0){LOG(LogLevel::INFO) << "Client: " << inet_ntoa(peer.sin_addr) << ":" << std::to_string(ntohs(peer.sin_port)) << " send: " << buffer;// 向客户端回消息ret = write(_ac_socketfd, buffer, sizeof(buffer));}}}}
}

停止服务器

停止服务器和UDP思路一致,但是需要注意,除了要关闭接收套接字以外还需要关闭监听套接字,此处不再赘述:

=== “停止服务器函数”

// 停止服务器
void stop()
{if (_isRunning){close(_listen_socketfd);close(_ac_socketfd);}
}

=== “析构函数”

~TcpServer()
{stop();
}

创建并封装客户端

创建客户端类

与UDP一致,代码如下:

class TcpClient
{
public:TcpClient(){}// 启动客户端void start(){}// 停止客户端void stop(){}~TcpClient(){}
};

创建客户端套接字

与UDP一致,此处不再赘述:

class TcpClient
{
public:TcpClient(): _socketfd(-1){_socketfd = socket(AF_INET, SOCK_STREAM, 0);if (_socketfd < 0){LOG(LogLevel::FATAL) << "Client initiated error:" << strerror(errno);exit(static_cast<int>(ErrorNumber::SocketFail));}LOG(LogLevel::INFO) << "Client initiated";}// ...private:int _socketfd;
};

启动客户端

因为当前是TCP,所以客户端必须先与服务端建立连接才可以进行数据传输。在Linux中,让客户端连接服务端的接口是connect,其原型如下:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

该接口的第一个参数表示传送数据需要的套接字,第二个参数表示服务器的套接字结构,第三个参数表示第二个参数的大小

如果该接口连接成功或者绑定成功,则返回0,否则返回-1并且设置错误码

需要注意,该接口会在成功连接后自动绑定端口和IP地址,与UDP一样不需要用户手动设置客户端的IP地址和端口

因为需要用到服务器的端口和IP地址,所以在创建客户端对象时需要让用户传递IP地址和端口,所以基本代码如下:

// 默认服务器端口和IP地址
const std::string default_ip = "127.0.0.1";
const uint16_t default_port = 8080;class TcpClient
{
public:TcpClient(const std::string &ip = default_ip, uint16_t port = default_port): // ..., _isRunning(false), _ip(ip), _port(port){// ...}// 启动客户端void start(){if (!_isRunning){_isRunning = true;// 启动后就进行connectstruct sockaddr_in server;server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(_ip.c_str());server.sin_port = htons(_port);int ret = connect(_socketfd, reinterpret_cast<const struct sockaddr *>(&server), sizeof(server));if (ret < 0){LOG(LogLevel::WARNING) << "Connect failed" << strerror(errno);exit(static_cast<int>(ErrorNumber::ConnectFail));}LOG(LogLevel::INFO) << "Connect Success: " << _socketfd;while (true){// ...}}}// ...private:// ...std::string _ip; // 服务器IP地址uint16_t _port;  // 服务器端口bool _isRunning; // 判断是否正在运行
};

在上面的代码中需要注意,不要把connect放在循环里,因为建立连接需要一次而不需要每一次发送都建立连接

接着就是写入和读取消息,基本思路与UDP相同,代码如下:

// 启动客户端
void start()
{if (!_isRunning){// ...while (true){// 向服务器写入std::string message;std::cout << "请输入消息:";std::getline(std::cin, message);ssize_t ret = write(_socketfd, message.c_str(), message.size());// 收到消息char buffer[4096] = {0};ret = read(_socketfd, buffer, sizeof(buffer));if (ret > 0)LOG(LogLevel::INFO) << "收到服务器消息:" << buffer;}}
}

停止客户端

停止客户端的思路与UDP一致,此处不再赘述:

=== “停止客户端函数”

// 停止客户端
void stop()
{if (_isRunning)close(_socketfd);
}

=== “析构函数”

~TcpClient()
{stop();
}

本地通信测试

测试步骤:

  1. 先启动服务端,再启动客户端
  2. 客户端向服务器端发送信息

测试目标:

  1. 客户端可以正常向服务器端发送信息
  2. 服务端可以正常显示客户端信息并正常向客户端返回客户端发送的信息
  3. 客户端可以正常显示服务端回复的信息

测试代码如下:

=== “客户端”

 #include "tcp_client.hpp"#include <memory>using namespace TcpClientModule;int main(int argc, char *argv[]){std::shared_ptr<TcpClient> tcp_client;if (argc == 1){// 使用默认端口和IP地址tcp_client = std::make_shared<TcpClient>();}else if (argc == 3){std::string ip = argv[1];std::uint16_t port = std::stoi(argv[2]);// 使用自定义端口和IP地址tcp_client = std::make_shared<TcpClient>(ip, port);}else{LOG(LogLevel::ERROR) << "错误使用,正确使用为:" << argv[0] << " IP地址 端口号(或者二者都不存在)";exit(7);}tcp_client->start();tcp_client->stop();return 0;}

=== “服务端”

 #include "tcp_server.hpp"#include <memory>using namespace TcpServerModule;int main(int argc, char *argv[]){std::shared_ptr<TcpServer> tcp_server;if (argc == 1){// 使用默认的端口tcp_server = std::make_shared<TcpServer>();}else if (argc == 2){// 使用自定义端口std::string port = argv[1];tcp_server = std::make_shared<TcpServer>(port);}else{LOG(LogLevel::ERROR) << "错误使用,正确方式:" << argv[0] << " 端口(可以省略)";exit(6);}tcp_server->start();tcp_server->stop();return 0;}

本次设计的客户端支持用户从命令行输入端口和IP地址,否则就直接使用默认,下面是一种结果:

在这里插入图片描述

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

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

从上图可以看到,如果客户端连接后断开再连接就会出现第二次连接发送消息无法得到回应。之所以出现这个问题就是因为服务器卡在了读写死循环中,解决这个问题的方式很简单,只需要判断read接口返回值是否为0,如果为0,说明当前服务器并没有读取到任何内容,直接退出即可:

// 启动服务器
void start()
{if (!_isRunning){// ...while (true){// ...while(true){// 读取客户端消息char buffer[4096] = {0};ssize_t ret = read(_ac_socketfd, buffer, sizeof(buffer) - 1);if (ret > 0){// ...}else if (ret == 0){LOG(LogLevel::INFO) << "Client disconnected: " << _ac_socketfd;break;}}}}
}

此时便可以解决上面的问题:

在这里插入图片描述

文件描述符泄漏问题

在上面的测试结果中可以发现,当客户端退出后再重新连接服务端,此时的文件描述符由4变成了5,但是实际上文件描述符是非常有限的,对于一般的用户机来说,文件描述符最大为1024,而服务器一般为65535,使用下面的指令可以查看:

ulimit -a

在结果中的open files一栏即可看到值

既然客户端已经退出了,那么对应的文件描述符就应该关闭而不是持续被占用着,此时就出现了文件描述符泄漏问题。解决这个问题很简答,只需要在判断读取结果小于0时关闭文件描述符再退出即可:

// 启动服务器
void start()
{if (!_isRunning){// ...while (true){// ...// ...close(_ac_socketfd);}}
}

测试云服务器与本地进行通信

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

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

  1. 将服务端程序拷贝到云服务器
  2. 本地作为客户端,通过云服务器的公网IP地址连接云服务器的服务端
  3. 客户端向云服务器发送信息

具体操作步骤与UDP类似,下面直接展示结果:

在这里插入图片描述

与UDP一样需要注意安全组的问题,以阿里云为例,设置结果如下:
在这里插入图片描述

不同操作系统(客户端为Windows,服务端为Linux)

因为Windows中使用接口和Linux中差不多,所以不会再详细介绍,下面直接给出Windows客户端代码:

#include <winsock2.h>
#include <iostream>
#include <string>#pragma warning(disable : 4996)#pragma comment(lib, "ws2_32.lib")std::string serverip = "47.113.217.80";  // 填写云服务器IP地址
uint16_t serverport = 8888; // 填写云服务开放的端口号int main()
{WSADATA wsaData;int result = WSAStartup(MAKEWORD(2, 2), &wsaData);if (result != 0){std::cerr << "WSAStartup failed: " << result << std::endl;return 1;}SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (clientSocket == INVALID_SOCKET){std::cerr << "socket failed" << std::endl;WSACleanup();return 1;}sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(serverport);                  // 替换为服务器端口serverAddr.sin_addr.s_addr = inet_addr(serverip.c_str()); // 替换为服务器IP地址result = connect(clientSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr));if (result == SOCKET_ERROR){std::cerr << "connect failed" << std::endl;closesocket(clientSocket);WSACleanup();return 1;}while (true){std::string message;std::cout << "Please Enter@ ";std::getline(std::cin, message);if(message.empty()) continue;send(clientSocket, message.c_str(), message.size(), 0);char buffer[1024] = {0};int bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);if (bytesReceived > 0){buffer[bytesReceived] = '\0'; // 确保字符串以 null 结尾std::cout << "Received from server: " << buffer << std::endl;}else{std::cerr << "recv failed" << std::endl;}}closesocket(clientSocket);WSACleanup();return 0;
}

运行结果如下:

在这里插入图片描述

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

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

基本现象

使用本地虚拟机和云服务器的客户端本地连接云服务器的服务端:

先使用虚拟机或者云服务器的客户端连接服务端:

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

之所以会出现这个问题就是因为在上面的逻辑中:只有接收成功了才会发送消息,而一旦接收成功后,就在写入和读取中死循环,此时就导致accept不能继续接收。解决这个问题就需要考虑到使用子进程或者新线程,将接收和读写分别放在两个执行进程或者执行流中,根据这个思路下面提供三种解决方案:

  1. 子进程版本
  2. 新线程版本
  3. 线程池版本

子进程版本

设计子进程版本的本质就是让子进程执行读写方法,先将读写逻辑抽离到一个函数中:

=== “读写函数”

// 读写函数
void read_write_msg(struct sockaddr_in peer)
{while (true){// 读取客户端消息char buffer[4096] = {0};ssize_t ret = read(_ac_socketfd, buffer, sizeof(buffer) - 1);if (ret > 0){LOG(LogLevel::INFO) << "Client: " << inet_ntoa(peer.sin_addr) << ":" << std::to_string(ntohs(peer.sin_port)) << " send: " << buffer;// 向客户端回消息ret = write(_ac_socketfd, buffer, sizeof(buffer));}else if (ret == 0){LOG(LogLevel::INFO) << "Client disconnected: " << _ac_socketfd;break;}}close(_ac_socketfd);
}

=== “启动服务器函数”

// 启动服务器
void start()
{if (!_isRunning){_isRunning = true;while (true){struct sockaddr_in peer;socklen_t length = sizeof(peer);_ac_socketfd = accept(_listen_socketfd, reinterpret_cast<struct sockaddr *>(&peer), &length);if (_ac_socketfd < 0){LOG(LogLevel::WARNING) << "Accept failed:" << strerror(errno);exit(static_cast<int>(ErrorNumber::AcceptFail));}LOG(LogLevel::INFO) << "Accept Success: " << _ac_socketfd;// 读写逻辑read_write_msg(peer);}}
}

接着,为了让子进程执行对应的任务,首先就是创建一个子进程,此处直接使用原生接口:

// 启动服务器
void start()
{if (!_isRunning){_isRunning = true;while (true){// ...// 创建子进程pid_t pid = fork();if (pid == 0){// 子进程// 读写逻辑read_write_msg(peer);exit(0);}}}
}

但是,这样写还不足以解决问题,在Linux进程间通信提到子进程会拷贝父进程描述符表,此时同样会导致文件描述符泄漏问题,所以父进程和子进程都需要关闭自己不需要的文件描述符:对于父进程来说,其需要关闭读写用的文件描述符,因为写入和读取交给了子进程;对于子进程来说,其需要关闭监听用的文件描述符,因为继续监听其他客户端的连接由父进程进行

基于上面的思路,代码如下:

// 启动服务器
void start()
{if (!_isRunning){_isRunning = true;while (true){// ...// 创建子进程pid_t pid = fork();if (pid == 0){// 子进程// 关闭监听文件描述符close(_listen_socketfd);// ...}// 父进程关闭读写描述符close(_ac_socketfd);}}
}

一旦创建了子进程,父进程就需要对其进行等待并回收,如果不回收就会导致内存泄漏问题,回收子进程的方式目前有下面两种:

  1. 使用waitwaitpid接口进行等待
  2. 借助子进程退出时发送的SIGCHILD信号,使用SIG_IGN行为

但是本次不使用上面的任意一种,而是考虑让子进程再创建一个子进程,一旦创建成功就让当前子进程退出,而让新创建的子进程(孙子进程)继续执行后续的代码,因为当前子进程已经退出并且退出前并没有回收新创建的子进程(孙子进程),所以当前孙子进程就会被操作系统托管变成孤儿进程,一旦孙子进程走到了读写逻辑下面的exit(0)就会退出,此时操作系统就会自动回收这个孙子进程。这个思路也被称为「双重fork(或者守护进程化))」。所以,代码如下:

// 启动服务器
void start()
{if (!_isRunning){_isRunning = true;while (true){// ...// 创建子进程pid_t pid = fork();if (pid == 0){// 子进程// ...// 创建孙子进程if (fork())exit(0); // 当前子进程执行exit(0)// 孙子进程从此处继续向后执行// 读写逻辑read_write_msg(peer);exit(0);}// ...}}
}

现在,再进行上面的测试可以发现问题已经解决:

在这里插入图片描述

在这里插入图片描述

新线程版本

因为所有线程共享一个文件描述符表,所以不需要手动关闭一些文件描述符,下面使用前面封装的线程进行演示:

// 启动服务器
void start()
{if (!_isRunning){_isRunning = true;while (true){// ...// // 父进程关闭读写描述符// close(_ac_socketfd);// 创建新线程Thread t(std::bind(&TcpServer::read_write_msg, this, peer));t.start();}}
}

测试之后可以发现和子进程测试的效果一样,此处不再展示

线程池版本

线程池版本和新线程版本的思路非常类似,给出代码不再演示:

using task_t = std::function<void()>;// ...class TcpServer
{
public:TcpServer(uint16_t port = default_port): _listen_socketfd(-1), _port(port), _isRunning(false), _ac_socketfd(-1){// 创建服务器套接字// ...// 绑定// ...// 创建线程池_tp = ThreadPool<task_t>::getInstance();// 启动线程池_tp->startThreads();// 监听// ...}// ...// 启动服务器void start(){if (!_isRunning){_isRunning = true;while (true){// ...// version-3_tp->pushTasks(std::bind(&TcpServer::read_write_msg, this, peer));}}}// 停止服务器void stop(){if (_isRunning){_tp->stopThreads();// ...}}~TcpServer(){stop();}private:// ...std::shared_ptr<ThreadPool<task_t>> _tp;// ...
};

客户端控制服务器执行相关命令的程序

思路分析

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

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

实现

因为服务端本身就是进行接收和返回结果,所以考虑将命令执行单独作为一个类来描述,本次为了执行的安全,考虑只允许用户执行部分命令,并且提供判断命令是否是合法命令,所以少不了需要查询的接口,为了更快速的查询,可以使用set集合。另外,因为要执行命令,所以需要一个成员函数executeCommand执行对应的命令,所以基本结构如下:

class Command
{Command(){// 构造可以执行的一些命令_commands.insert("ls");_commands.insert("pwd");_commands.insert("ll");_commands.insert("touch");_commands.insert("who");_commands.insert("whoami");}// 判断命令是否合法bool isValid(std::string cmd){auto pos = _commands.find(cmd);if (pos == _commands.end())return false;return true;}// 执行命令std::string executeCommand(const std::string &cmd){}~Command(){}private:std::set<std::string> _commands;
};

接着,改变服务端的读写任务的接口,此处不再使用文件的readwrite接口,而是使用recvsend接口,这两个接口只是比readwrite多了flags,其余都一样,并且目前情况下flags设置为0即可:

// 读写函数
void read_write_msg(struct sockaddr_in peer)
{while (true){// 读取客户端消息char buffer[4096] = {0};ssize_t ret = recv(_ac_socketfd, buffer, sizeof(buffer) - 1, 0);if (ret > 0){LOG(LogLevel::INFO) << "Client: " << inet_ntoa(peer.sin_addr) << ":" << std::to_string(ntohs(peer.sin_port)) << " send: " << buffer;// 向客户端回消息Command cmd;if (cmd.isValid(buffer)){// 命令合法可以执行命令std::string ret = cmd.executeCommand(buffer);send(_ac_socketfd, ret.c_str(), ret.size(), 0);}else{send(_ac_socketfd, "错误指令", sizeof("错误指令"), 0);}}// ...}// ...
}

接下来就是实现执行命令函数,根据前面的分析需要创建子进程调用exec家族函数执行对应的命令,但是在标准库中有对应的接口已经实现了这个功能:popen,其原型如下:

FILE *popen(const char *command, const char *type);

对应的接口就是pclose接口,原型如下:

int pclose(FILE *stream);

对于popen接口来说,其会对传入的命令进行分析并创建子进程执行,将执行结果放到返回值中,因为FILE是文件结构,所以只需要使用文件的读写接口即可读取到其中的内容,这个接口第二个参数表示读模式或者写模式,因为是执行命令,所以只需要填入"r"即可

结合上面的接口即可完成对应的执行命令函数:

std::string executeCommand(const std::string &cmd)
{FILE *fp = popen(cmd.c_str(), "r");if (fp == nullptr)return std::string();char buffer[1024];std::string result;while (fgets(buffer, sizeof(buffer), fp)){result += buffer;}pclose(fp);return result;
}

!!! note
fgets会自动添加\0,不需要预留\0的位置

测试

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

在这里插入图片描述

相关文章:

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

TCP编程接口基本使用 本篇介绍 在UDP编程接口基本使用已经介绍过UDP编程相关的接口&#xff0c;本篇开始介绍TCP编程相关的接口。有了UDP编程的基础&#xff0c;理解TCP相关的接口会更加容易&#xff0c;下面将按照两个方向使用TCP编程接口&#xff1a; 基本使用TCP编程接口…...

系统部署【信创名录】及其查询地址

一、信创类型 &#xff08;一&#xff09;服务器&#xff1a; 1.华为云 2.腾讯云 3.阿里云 &#xff08;二&#xff09;中央处理器&#xff08;CPU&#xff09;&#xff1a; 1.海思&#xff0c;鲲鹏920服务器 &#xff08;三&#xff09;中间件 1.人大金仓 &#xff0…...

JavaWeb后端基础(7)AOP

AOP是Spring框架的核心之一&#xff0c;那什么是AOP&#xff1f;AOP&#xff1a;Aspect Oriented Programming&#xff08;面向切面编程、面向方面编程&#xff09;&#xff0c;其实说白了&#xff0c;面向切面编程就是面向特定方法编程。AOP是一种思想&#xff0c;而在Spring框…...

Python 中多种方式获取屏幕的 DPI值

在 Python 中&#xff0c;可以通过多种方式获取屏幕的 DPI&#xff08;每英寸点数&#xff09;。以下是几种常见的方法&#xff1a; 方法 1&#xff1a;使用 tkinter 模块 tkinter 是 Python 的标准 GUI 库&#xff0c;可以通过它获取屏幕的 DPI。 import tkinter as tkdef …...

高效数据分析实战指南:Python零基础入门

高效数据分析实战指南 —— 以Python为基石&#xff0c;构建您的数据分析核心竞争力 大家好&#xff0c;我是kakaZhui&#xff0c;从事数据、人工智能算法多年&#xff0c;精通Python数据分析、挖掘以及各种深度学习算法。一直以来&#xff0c;我都发现身边有很多在传统行业从…...

Unity DOTS从入门到精通之EntityCommandBufferSystem

文章目录 前言安装 DOTS 包ECBECB可以执行的指令示例&#xff1a; 前言 DOTS&#xff08;面向数据的技术堆栈&#xff09;是一套由 Unity 提供支持的技术&#xff0c;用于提供高性能游戏开发解决方案&#xff0c;特别适合需要处理大量数据的游戏&#xff0c;例如大型开放世界游…...

开放充电点协议(OCPP)技术解析:架构演进与通信机制 - 慧知开源充电桩平台

开放充电点协议&#xff08;OCPP&#xff09;技术解析&#xff1a;架构演进与通信机制 引言 开放充电点协议&#xff08;Open Charge Point Protocol, OCPP&#xff09;作为电动汽车充电基础设施的核心通信标准&#xff0c;其技术架构与实现逻辑直接影响充电桩与中央管理系统&…...

MySQL 索引的数据结构(详细说明)

6. MySQL 索引的数据结构(详细说明) 文章目录 6. MySQL 索引的数据结构(详细说明)1. 为什么使用索引2. 索引及其优缺点2.1 索引概述 3. InnoDB中索引的推演3.1 索引之前的查找3.2 设计索引3.3 常见索引概念1. 聚簇索引2. 二级索引&#xff08;辅助索引、非聚簇索引&#xff09;…...

初学者快速入门Python爬虫 (无废话版)

全篇大概 5000 字(含代码)&#xff0c;建议阅读时间 40min 一、Python爬虫简介 1.1 什么是网络爬虫&#xff1f; 定义&#xff1a; 网络爬虫&#xff08;Web Crawler&#xff09;是自动浏览互联网并采集数据的程序&#xff0c;就像电子蜘蛛在网页间"爬行"。 分类&…...

【git】ssh配置提交 gitcode-ssh提交

【git】ssh配置提交 gitcode-ssh提交 之前一直用的是gitee和阿里云的仓库&#xff0c;前两天想在gitcode上面备份一下我的打洞代码和一些资料 就直接使用http克隆了下来 。 在提交的时候他一直会让我输入账号和密码&#xff0c;但是我之前根本没有设置过这个&#xff0c;根本没…...

【二】JavaScript能力提升---this对象

目录 this的理解 this的原理 事件绑定中的this 行内绑定 动态绑定 window定时器中的this 相信小伙伴们看完这篇文章&#xff0c;对于this的对象可以有一个很大的提升&#xff01; this的理解 对于this指针&#xff0c;可以先记住以下两点&#xff1a; this永远指向一个…...

C++————类和对象(一)

1.类定义格式 在C中&#xff0c;类&#xff08;class&#xff09;是封装数据和操作这些数据的函数的构造。类的定义包含成员变量和成员函数。 类的基本定义格式如下&#xff1a; class ClassName {// 访问修饰符public:// 公有成员DataType memberVariable; // 成员变量voi…...

SpringBoot参数校验:@Valid 与 @Validated 详解

SpringBoot参数校验&#xff1a;Valid 与 Validated 详解 一、案例&#xff08;参数校验的必要性&#xff09; 传统方式&#xff08;无注解&#xff09;的缺点&#xff1a; // 需要手动校验每个字段&#xff0c;代码冗余且易出错 public String register(User user) {// 手动…...

<论文>MiniCPM:利用可扩展训练策略揭示小型语言模型的潜力

一、摘要 本文跟大家一起阅读的是清华大学的论文《MiniCPM: Unveiling the Potential of Small Language Models with Scalable Training Strategies》 摘要&#xff1a; 对具有高达万亿参数的大型语言模型&#xff08;LLMs&#xff09;的兴趣日益增长&#xff0c;但同时也引发…...

SpringCloud系列教程(十三):Sentinel流量控制

SpringCloud中的注册、发现、网关、服务调用都已经完成了&#xff0c;现在就剩下最后一部分&#xff0c;就是关于网络控制。SpringCloud Alibaba这一套中间件做的非常好&#xff0c;把平时常用的功能都集成进来了&#xff0c;而且非常简单高效。我们下一步就完成最后一块拼图Se…...

Codeforces Round 502 E. The Supersonic Rocket 凸包、kmp

题目链接 题目大意 平面上给定两个点集&#xff0c;判定两个点集分别形成的凸多边形能否通过旋转、平移重合。 点集大小 ≤ \leq ≤ 1 0 5 10^{5} 105&#xff0c;坐标范围 [0, 1 0 8 10^{8} 108 ]. 思路 题意很明显&#xff0c;先求出凸包再判断两凸包是否同构。这里用…...

论文阅读方法

文章目录 步骤一&#xff1a;对论文进行自我判断阅读题目和关键词。阅读摘要阅读总结要点 步骤二&#xff1a;阅读文章阅读图表和图表的注释阅读引言阅读实验部分阅读结果和作者对结果的讨论&#xff08;创新点&#xff09;要点 步骤三&#xff1a;精度论文回答问题1回答问题2回…...

ArcGIS操作:15 计算点的经纬度,并添加到属性表

注意&#xff1a;需要转化为地理坐标系 1、打开属性表&#xff0c;添加字段 2、计算字段&#xff08;以计算纬度为例 !Shape!.centroid.Y ) 3、效果...

蓝桥杯历年真题题解

1.轨道炮&#xff08;数学模拟&#xff09; #include <iostream> #include <map> using namespace std; const int N1010; int x[N],y[N],v[N]; char d[N]; int main() {int n;int ans-100;cin>>n;for(int i1;i<n;i)cin>>x[i]>>y[i]>>v…...

IP-地址

主机号&#xff08;Host ID&#xff09; IP地址简介&#xff1a;IP地址是每台接入互联网的设备所拥有的唯一标识符&#xff0c;类似于电话号码的分层结构&#xff0c;由网络号和主机号组成。为了便于记忆&#xff0c;32位二进制的IP地址通常以点分十进制表示。 网络号&#xf…...

MoonSharp 文档一

目录 1.Getting Started 步骤1&#xff1a;在 IDE 中引入 MoonSharp 步骤2&#xff1a;引入命名空间 步骤3&#xff1a;调用脚本 步骤4&#xff1a;运行代码 2.Keeping a Script around 步骤1&#xff1a;复现前教程所有操作 步骤2&#xff1a;改为创建Script对象 步骤…...

2025-03-08 学习记录--C/C++-PTA 习题10-1 判断满足条件的三位数

合抱之木&#xff0c;生于毫末&#xff1b;九层之台&#xff0c;起于累土&#xff1b;千里之行&#xff0c;始于足下。&#x1f4aa;&#x1f3fb; 一、题目描述 ⭐️ 裁判测试程序样例&#xff1a; #include <stdio.h> #include <math.h>int search( int n );int…...

三星首款三折叠手机被曝外屏6.49英寸:折叠屏领域的新突破

在智能手机的发展历程中,折叠屏手机的出现无疑是一次具有里程碑意义的创新。它打破了传统手机屏幕尺寸的限制,为用户带来了更加多元和便捷的使用体验。而三星,作为手机行业的巨头,一直以来都在折叠屏技术领域积极探索和创新。近日,三星首款三折叠手机的诸多细节被曝光,其…...

大白话Vue Router 中路由守卫(全局守卫、路由独享守卫、组件内守卫)的种类及应用场景

大白话Vue Router 中路由守卫&#xff08;全局守卫、路由独享守卫、组件内守卫&#xff09;的种类及应用场景 答题思路 明确要介绍的内容&#xff1a;需要分别介绍 Vue Router 中全局守卫、路由独享守卫和组件内守卫这三种路由守卫的种类&#xff0c;详细说明它们的定义、使用…...

CUDA编程之OpenCV与CUDA结合使用

OpenCV与CUDA的结合使用可显著提升图像处理性能。 一、版本匹配与环境配置 CUDA与OpenCV版本兼容性‌ OpenCV各版本对CUDA的支持存在差异&#xff0c;例如OpenCV 4.5.4需搭配CUDA 10.0‌2&#xff0c;而较新的OpenCV 4.8.0需使用更高版本CUDA‌。 需注意部分模块&#xff08;…...

Educational Codeforces Round 7 F. The Sum of the k-th Powers 多项式、拉格朗日插值

题目链接 题目大意 求 ( ∑ i 1 n i k ) (\sum_{i1}^{n} i^k) (∑i1n​ik) m o d ( 1 0 9 7 ) mod(10^97) mod(1097) . 数据范围 &#xff1a; 1 ≤ n ≤ 1 0 9 1 \leq n \leq 10^9 1≤n≤109 , 0 ≤ k ≤ 1 0 6 0 \leq k \leq 10^6 0≤k≤106 . 思路 令 f ( n ) ∑ …...

LINUX网络基础 [五] - HTTP协议

目录 HTTP协议 预备知识 认识 URL 认识 urlencode 和 urldecode HTTP协议格式 HTTP请求协议格式 HTTP响应协议格式 HTTP的方法 HTTP的状态码 ​编辑HTTP常见Header HTTP实现代码 HttpServer.hpp HttpServer.cpp Socket.hpp log.hpp Makefile Web根目录 H…...

WPS Word中英文混杂空格和行间距不一致调整方案

文章目录 问题1&#xff1a;在两端对齐的情况下&#xff0c;如何删除参考文献&#xff08;英文&#xff09;的空格问题2&#xff1a;中英文混杂行间距不一致问题问题3&#xff1a;设置中文为固定字体&#xff0c;设置西文为固定字体参考 问题1&#xff1a;在两端对齐的情况下&a…...

C++ Qt创建计时器

在Qt中&#xff0c;可以使用QTimer来创建一个简单的计时器。QTimer是一个用于定时触发事件的类&#xff0c;通常与QObject的子类&#xff08;如QWidget&#xff09;一起使用。以下是一个完整的示例&#xff0c;展示如何使用Qt创建一个带有计时器的窗口应用程序。 示例&#xff…...

CSDN博客:Markdown编辑语法教程总结教程(中)

❤个人主页&#xff1a;折枝寄北的博客 Markdown编辑语法教程总结 前言1. 列表1.1 无序列表1.2 有序列表1.3 待办事项列表1.4 自定义列表 2. 图片2.1 直接插入图片2.2 插入带尺寸的图片2.3 插入宽度确定&#xff0c;高度等比例的图片2.4 插入高度确定宽度等比例的图片2.5 插入居…...