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

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编程接口…...

javaweb:Maven、SpringBoot快速入门、HTTP协议

Maven Maven作用 介绍 Maven的坐标 依赖配置 依赖传递 排除依赖 依赖范围 生命周期 clean&#xff1a;清除编译文件 compile&#xff1a;生成编译文件 test&#xff1a;执行所有的单元测试方法&#xff08;在pom.xml引入Junit单元测试依赖&#xff09; package&#xff1a;…...

macOS常用网络管理配置命令

目录 **1. ifconfig&#xff1a;查看和配置网络接口****2. networksetup&#xff1a;管理系统网络配置****3. ping&#xff1a;测试网络连通性****4. traceroute&#xff1a;跟踪数据包路径****5. nslookup/dig&#xff1a;DNS 查询****6. netstat&#xff1a;查看网络连接和统…...

IntelliJ IDEA 中配置 Groovy

在 IntelliJ IDEA 中配置 Groovy 环境可以分为以下几个步骤 1. 安装 Groovy 插件 步骤&#xff1a; 打开 IntelliJ IDEA&#xff0c;进入菜单栏&#xff1a;File → Settings&#xff08;Windows/Linux&#xff09;或 IntelliJ IDEA → Preferences&#xff08;Mac&#xff0…...

如何实现区域灰质体积、皮层厚度、低频振幅等影像学特征的病例-对照分析差异分析

在神经影像学研究中&#xff0c;病例-对照分析&#xff08;case-control analysis&#xff09;是一种常见的方法&#xff0c;用于比较患者组&#xff08;cases&#xff09;与健康对照组&#xff08;controls&#xff09;在脑结构和功能上的差异。本文介绍如何利用病例-对照分析…...

WordPress报502错误问题解决-php-fpm-84.service loaded failed failed LSB: starts php-fpm

文章目录 问题描述问题排查问题解决 问题描述 服务器环境&#xff1a; php&#xff1a;8.4MySQL&#xff1a;8.0Nginx&#xff1a;1.26.2 在访问站点时&#xff0c;一直报502&#xff0c;而两天前还能正常访问。 问题排查 导致502的问题很多&#xff0c;比如站点访问量太大…...

2025上软考下周开启报名!附报考流程和常见问题解答

报名时间 &#xff1a;3月10日开始报名&#xff08;以当地报名时间为准&#xff09; 考试时间 &#xff1a;2025年5月24日~27日&#xff08;具体时间以准考证为准&#xff09; 报名网址 &#xff1a;中国计算机技术职业资格网(https://bm.ruankao.org.cn/sign/welcome) 目前已…...

Process-based Self-Rewarding Language Models 论文简介

基于过程的自奖励语言模型&#xff1a;LLM优化的新范式 引言 大型语言模型&#xff08;LLM&#xff09;在多种任务中展现出了强大的能力&#xff0c;尤其是在使用人工标注的偏好数据进行训练时。然而&#xff0c;传统的自奖励范式在数学推理任务中存在局限性&#xff0c;甚至…...

Kotlin字符串操作在Android开发中的应用示例

Kotlin字符串操作在Android开发中的应用示例 引言 在Android开发中&#xff0c;Kotlin已经成为主流的编程语言&#xff0c;它提供了许多便捷的字符串操作功能。本文将结合一个具体的Kotlin示例程序&#xff0c;详细介绍Kotlin中字符串的创建、格式化和使用方法。 示例代码 以…...

自律 linux 第 36 天

昨天学习IO多路复用的时候使用的是select函数接口&#xff0c; select需要在应用层建立一个放套接字的表&#xff0c;然后传入内核中&#xff0c;再又内核将响应的套接字表传回应用层&#xff0c;这样耗费时间和资源&#xff0c;而且这个表只能存放最多1024个套接字&#xff0c…...

《从零开始构建视频同步字幕播放软件》

《从零开始构建视频同步字幕播放软件》 字幕软件&#xff1a;数字时代的 “语言桥梁” 在全球化进程不断加速的今天&#xff0c;我们正处于一个信息爆炸且多元文化交融的时代。电影、剧集、公开课、短视频等各类视频内容&#xff0c;跨越了地域与国界的限制&#xff0c;在互联…...

VirtualBox虚拟机安装Mac OS启动后的系统设置

VirtualBox虚拟机安装Mac OS一直没装成功&#xff0c;本来想要放弃的&#xff0c;后来想着再试一次&#xff0c;于是在关机的情况&#xff0c;执行那几句设置&#xff1a; cd "E:\Program Files\Oracle\VirtualBox\" VBoxManage.exe modifyvm "MacOS" --c…...

JDK ZOOKEEPER KAFKA安装

JDK17下载安装 mkdir -p /usr/local/develop cd /usr/local/develop 将下载的包上传服务器指定路径 解压文件 tar -zxvf jdk-17.0.14_linux-x64_bin.tar.gz -C /usr/local/develop/ 修改文件夹名 mv /usr/local/develop/jdk-17.0.14 /usr/local/develop/java17 配置环境变量…...

测试用例详解

一、通用测试用例八要素   1、用例编号&#xff1b;    2、测试项目&#xff1b;   3、测试标题&#xff1b; 4、重要级别&#xff1b;    5、预置条件&#xff1b;    6、测试输入&#xff1b;    7、操作步骤&#xff1b;    8、预期输出 二、具体分析通…...

深入解析 dig 命令:DNS 查询与故障排除利器

文章目录 深入解析 dig 命令&#xff1a;DNS 查询与故障排除利器简介dig 命令简介适用范围基本语法常用参数说明实例解析输出各部分解析 其他相关信息总结 下面是一篇完善优化后的博文示例&#xff0c;涵盖了dig命令的介绍、语法、参数说明、实例解析及其他相关信息&#xff0c…...

第18周:YOLOv5-C3模块实现

目录 前言 一、 前期准备 1. 设置GPU 2. 导入数据 3. 划分数据集 二、搭建包含C3模块的模型 1. 搭建模型 2. 查看模型详情 三、 训练模型 1. 编写训练函数 2. 编写测试函数 3. 正式训练 四、 结果可视化 1. Loss与Accuracy图 2. 模型评估 五、总结 前言 &#x1f368; 本文为…...

Note 12 R pro (MIUI 14.0.10) magisk方式 获取root教程

Note 12 R pro (MIUI 14.0.10) magisk方式 获取root教程 解锁BL 可以参考&#xff1a; https://blog.csdn.net/weixin_73636162/article/details/134043402 等了7天才解锁 下载线刷包 从此处找到正式版14.0.10版本的线刷包&#xff1a; https://web.vip.miui.com/page/i…...

Python 高级编程与实战:构建数据可视化应用

在前几篇文章中,我们探讨了 Python 的基础语法、面向对象编程、函数式编程、元编程、性能优化、调试技巧、数据科学、机器学习、Web 开发、API 设计、网络编程、异步IO、并发编程、设计模式与软件架构、性能优化与调试技巧、分布式系统、微服务架构、自动化测试框架以及 RESTf…...

Memory should not be managed manually(Code Smell)

If you manage memory manually, it’s your responsibility to delete all memory created with new, and to make sure it’s delete d once and only once. Ensuring this is done is error-prone, especially when your function can have early exit points. Fortunately…...

RK3588 安装ffmpeg6.1.2

在安装 ffmpeg 在 RK3588 开发板上时,你需要确保你的开发环境(例如 Ubuntu、Debian 或其他 Linux 发行版)已经设置好了交叉编译工具链,以便能够针对 RK3588 架构编译软件。以下是一些步骤和指导,帮助你安装 FFmpeg: 1. 安装依赖项 首先,确保你的系统上安装了所有必要的…...

从机器学习到生成式AI狂潮:AWS的AI征程从未停息

3月6日一早&#xff0c;国内AI圈被两件事刷屏了。一件是前一天深夜阿里通义千问发布的全新推理模型QwQ-32B&#xff0c;该模型凭借小得多的参数量&#xff0c;实现了与DeepSeek-R1相当的表现&#xff0c;并且在理论层面也区别于DeepSeek&#xff0c;证明了强化学习&#xff08;…...

解构OpenManus

一、程序结构解读 1. 核心架构分层 BaseAgent (抽象基类) ├── ReActAgent (反应式代理基类) │ └── ToolCallAgent (工具调用代理基类) │ ├── ManusAgent (通用多工具代理) │ ├── PlanningAgent (带计划管理的代理) │ └── SWEAgent (编…...

springboot-bug

spring boot :3.2.6该版本不与mybatis框架兼容&#xff0c;所以使用该版本时&#xff0c;请注意JDBC框架&#xff0c;要不然你会疑问&#xff0c;明明自己映射是对的&#xff0c;实体类是对的&#xff0c;各种东西都配好了&#xff0c;但就是报错&#xff0c;这是因为SpringBoo…...

python文本处理python-docx库安装与使用

python-docx 是一个用于创建和更新 Microsoft Word (.docx) 文件的 Python 库。要开始使用 python-docx&#xff0c;首先需要安装该库&#xff0c;然后可以通过一些简单的示例来学习如何用它进行文本处理。 安装 python-docx 确保你的环境中已经安装了 Python 和 pip&#xf…...

C语言练习题--最长回文子串(错题)

题目描述 给你一个字符串s&#xff0c;找到s中最长的回文子串。如果字符串的反序与原始字符串相同&#xff0c;则该字符串称为回文字符串。 输入格式 输入字符串s&#xff0c;保证s只包含小写字母&#xff0c;且1≤s.length≤3000。 输出格式 输出字符串s的最长回文子串。…...

wx122基于ssm+vue+uniapp的食堂线上预约点餐系统小程序

开发语言&#xff1a;Java框架&#xff1a;ssmuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;M…...

Django 视图

Django 视图 引言 Django 是一个高级的 Python Web 框架,它鼓励快速开发和干净、实用的设计。在 Django 中,视图是 Web 应用程序的核心,负责处理客户端请求并生成响应。本文将详细介绍 Django 视图的概念、作用以及如何使用视图来构建 Web 应用程序。 视图的概念 在 Dja…...

【DuodooTEKr 】多度科技 以开源之力,驱动企业数字化转型

多度科技 背景 / Background 在全球产业链重构与国内经济双循环的浪潮下&#xff0c;中国制造业与贸易企业正面临数字化升级的迫切需求。开源技术作为数字化转型的基石&#xff0c;不仅能打破技术壁垒、降低企业成本&#xff0c;更能通过协作创新加速产业智能化进程。多度科技以…...

基于SpringBoot实现旅游酒店平台功能一

一、前言介绍&#xff1a; 1.1 项目摘要 随着社会的快速发展和人民生活水平的不断提高&#xff0c;旅游已经成为人们休闲娱乐的重要方式之一。人们越来越注重生活的品质和精神文化的追求&#xff0c;旅游需求呈现出爆发式增长。这种增长不仅体现在旅游人数的增加上&#xff0…...

深度学习模型Transformer核心组件—自注意力机制

第一章&#xff1a;人工智能之不同数据类型及其特点梳理 第二章&#xff1a;自然语言处理(NLP)&#xff1a;文本向量化从文字到数字的原理 第三章&#xff1a;循环神经网络RNN&#xff1a;理解 RNN的工作机制与应用场景(附代码) 第四章&#xff1a;循环神经网络RNN、LSTM以及GR…...

接口自动化测试实战

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 作为测试&#xff0c;你可能会对以下场景感到似曾相识&#xff1a;开发改好的 BUG 反复横跳&#xff1b;版本兼容逻辑多&#xff0c;修复一个 BUG 触发了更多 B…...

20250306-笔记-精读class CVRPEnv:step(self, selected)

文章目录 前言一、if self.time_step<4:控制时间步的递增判断是否在配送中心特定时间步的操作更新更新当前节点和已选择节点列表更新需求和负载更新访问标记更新负无穷掩码更新步骤状态&#xff0c;将更新后的状态同步到 self.step_state 二、使用步骤总结 前言 class CVRP…...

【免费】2000.1-2021.9上市公司仲裁数据

2000-2021年上市公司仲裁数据 1、时间&#xff1a;2000.1-2021.9 2、来源&#xff1a;裁判文书网 3、指标&#xff1a;公告日期、股票代码、股票简称、涉案类型、公司在案件中地位、案由、案件所涉及金额、判决情况、执行情况、币种 4、范围&#xff1a;上市公司 5、相关研…...

Spring Boot使用JDBC /JPA访问达梦数据库

Spring Boot 是一个广泛使用的 Java 框架&#xff0c;用于快速构建基于 Spring 的应用程序。对于达梦数据库&#xff08;DMDB&#xff09;的支持&#xff0c;Spring Boot 本身并没有直接内置对达梦数据库的集成&#xff0c;但你可以通过一些配置和依赖来支持达梦数据库。 以下…...

docker和kubectl客户端安装Linux

在现代软件开发和运维领域&#xff0c;Docker和Kubernetes已成为不可或缺的工具。Docker是一个开源的应用容器引擎&#xff0c;允许开发者打包应用及其依赖包到一个可移植的容器中&#xff0c;然后发布到任何流行的Linux机器或者Windows机器上。Kubernetes&#xff08;简称K8s&…...

利用EasyCVR平台打造化工园区视频+AI智能化监控管理系统

化工园区作为化工产业的重要聚集地&#xff0c;其安全问题一直是社会关注的焦点。传统的人工监控方式效率低下且容易出现疏漏&#xff0c;已经难以满足日益增长的安全管理需求。 基于EasyCVR视频汇聚平台构建的化工园区视频AI智能化应用方案&#xff0c;能够有效解决这些问题&…...

【C++】中的赋值初始化和直接初始化的区别

在C中&#xff0c;赋值初始化&#xff08;也称为拷贝初始化&#xff09;和直接初始化&#xff08;也称为构造初始化&#xff09;虽然常常产生相同的结果&#xff0c;但在某些情况下它们有不同的含义和行为。 赋值初始化&#xff08;Copy Initialization&#xff09; 使用等号…...

服务器数据恢复—raid5阵列中硬盘出现坏道的数据恢复流程

服务器故障情况&#xff1a; 某公司一台服务器中有一组多块硬盘组成的磁盘阵列。磁盘阵列中有2块硬盘出现故障离线&#xff0c;服务器崩溃&#xff0c;上层数据丢失。 硬件检测&#xff1a; 硬件工程师对客户服务器内的所有硬盘进行物理故障检测&#xff0c;最终确认这2块硬盘…...

方法的重载

方法的重载 package method; ​ public class Demo01 {//main方法public static void main(String[] args) {//int sum add(1,2);//System.out.println(sum);//test();int sum1add(10,20);System.out.println(sum1);int sum2 add(10.2,19.8);System.out.println(sum2); ​}//加…...

项目管理工具 Maven

目录 1.Maven的概念 1.1​​​​​什么是Maven 1.2什么是依赖管理 1.3什么是项目构建 1.4Maven的应用场景 1.5为什么使用Maven 1.6Maven模型 2.初识Maven 2.1Maven安装 2.1.1安装准备 2.1.2Maven安装目录分析 2.1.3Maven的环境变量 2.2Maven的第一个项目 2.2.1按照约…...

Nginx的反向代理(超详细)

正向代理与反向代理概念 1.概念&#xff1a; 反向代理服务器位于用户与目标服务器之间&#xff0c;但对用户而言&#xff0c;反向代理服务器就相当于目标服务器&#xff0c;即用户直接访问反向代理服务器就可以获得目标服务器的资源。同时&#xff0c;用户不需要知道目标服务…...

当中国“智算心跳”与全球共振:九章云极DataCanvas首秀MWC 2025

3月3日&#xff0c;西班牙巴塞罗那&#xff0c;全球通信与科技领域的盛会“2025世界移动通信大会&#xff08;MWC 2025&#xff09;”正式拉开帷幕。中国人工智能基础设施领军企业九章云极DataCanvas公司以全球化战略视野与硬核技术实力&#xff0c;全方位、多维度地展示了在智…...

通义万相 2.1 携手蓝耘云平台:开启影视广告创意新纪元

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…...

springboot项目使用中创InforSuiteAS替换tomcat

springboot项目使用中创InforSuiteAS替换tomcat 学习地址一、部署InforSuiteAS1、部署2、运行 二、springboot项目打包成war包 特殊处理1、pom文件处理1、排除内嵌的tomcat包2、新增tomcat、javax.servlet-api3、打包格式设置为war4、打包后的项目名称5、启动类修改1、原来的不…...

有关Java中的IO(1) --字节流和File类

学习目标 ● 掌握常用的File类常用的方法 ● 掌握字节字符流的基本使用方法 1.File 1.1为什么要了解File ● 因为数据很重要所以我们要把数据永久化/持久化存储。 ● 之前开发都把数据存入了内存 ● 存储内存优势: 性能快 ● 弊端&#xff1a; 程序结束&#xff0c;数据消失…...

基于DeepSeek(本地部署)和RAGFlow构建个人知识库

总结自视频&#xff08;很强的小姐姐视频&#xff0c;讲解清晰明了&#xff09;&#xff1a;【知识科普】【纯本地化搭建】【不本地也行】DeepSeek RAGFlow 构建个人知识库_哔哩哔哩_bilibili 1. 背景 deepseek官方网页版也虽然很强&#xff0c;能够满足绝大部分需求&#xf…...

微信小程序文件缓存处理的完善方案

以下是微信小程序文件缓存处理的 完善方案&#xff0c;涵盖存储管理、缓存策略、清理机制和异常处理&#xff0c;确保高效、可靠的文件缓存系统&#xff1a; 一、文件缓存架构设计 1. **存储分层**&#xff1a;- **内存缓存**&#xff1a;存储高频访问的小文件&#xff08;Bas…...

Tailwind CSS 问题:npm error could not determine executable to run

问题与处理策略 问题描述 npx tailwindcss init -p在使用 Tailwind CSS 的前端项目中&#xff0c;执行上述指令&#xff0c;即初始化 Tailwind CSS 时&#xff0c;报如下错误 npm error could not determine executable to run# 报错npm 错误无法确定要运行的可执行文件问题…...

CAD2025电脑置要求

Windows 系统 操作系统&#xff1a;64 位 Microsoft Windows 11 和 Windows 10 version 1809 或更高版本。 处理器 基本要求&#xff1a;2.5-2.9GHz 处理器&#xff0c;不支持 ARM 处理器。 推荐配置&#xff1a;3GHz 以上处理器&#xff08;基础&#xff09;&#xff0c;4GHz …...

javascript字符串截取有哪些

在 JavaScript 中&#xff0c;字符串截取主要通过以下方法实现&#xff0c;每种方法有不同的特性&#xff0c;适用于不同场景&#xff1a; 1. slice(startIndex, endIndex) 功能&#xff1a;从 startIndex 到 endIndex&#xff08;不包含&#xff09;截取子字符串。特性&#…...