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的位置

测试

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

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/32116.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

wireshark 如何关闭混杂模式 wireshark操作

Fiddler和Wireshark都是进行抓包的工具&#xff1a;所谓抓包就是将网络传输发送与接收的数据包进行截获、重发、编辑、转存等操作&#xff0c;也用来检查网络安全。抓包也经常被用来进行数据截取等。黑客常常会用抓包软件获取你非加密的上网数据&#xff0c;然后通过分析&#…

IDEA2024又一坑:连接Docker服务连不上,提示:Cannot run program “docker“: CreateProcess error=2

为新电脑安装了IDEA2024版&#xff0c;因为局域网中安装有Docker,所以这台电脑上没有安装&#xff0c;当运行时发现死活连不上Docker报&#xff1a;Cannot run program “docker“: CreateProcess error2 分析&#xff1a; Docker服务有问题 其它电脑都能连&#xff0c;排除 网…

文件包含漏洞第一关

一、什么是文件包含漏洞 1.文件包含漏洞概述 和SQL注入等攻击方式一样&#xff0c;文件包含漏洞也是一种注入型漏洞&#xff0c;其本质就是输入一段用户能够控制的脚本或者代码&#xff0c;并让服务端执行。 什么叫包含呢&#xff1f;以PHP为例&#xff0c;我们常常把可重复使…

网络安全事件响应--应急响应(windows)

应用系统日志 Windows主要有以下三类日志记录系统事件&#xff1a;应用程序日志、系统日志和安全日志。 系统和应用程序日志存储着故障排除信息&#xff0c;对于系统管理员更为有用。安全日志记录着事件审计信息&#xff0c;包括用户验证&#xff08;登录、远程访问等&#x…

C++蓝桥杯基础篇(九)

片头 嗨&#xff01;小伙伴们&#xff0c;大家好~ 今天我们将学习蓝桥杯基础篇&#xff08;十&#xff09;&#xff0c;学习函数相关知识&#xff0c;准备好了吗&#xff1f;咱们开始咯&#xff01; 一、函数基础 一个典型的函数定义包括以下部分&#xff1a;返回类型、函数名…

JVM内存结构笔记01-运行时数据区域

文章目录 前言运行时数据区域1.程序计数器定义特点总结 2.虚拟机栈2.1 定义局部变量表 ★操作数栈动态链接方法返回地址(方法出口) 2.2 栈内存溢出演示栈内存溢出 java.lang.StackOverflowError 2.3问题辨析1. 垃圾回收是否涉及栈内存&#xff1f;2. 栈内存分配越大越好吗&…

01-简单几步!在Windows上用llama.cpp运行DeepSeek-R1模型

1.llama.cpp介绍 Llama.cpp 是一个开源的、轻量级的项目&#xff0c;旨在实现 Meta 推出的开源大语言模型 Llama 的推理&#xff08;inference&#xff09;。Llama 是 Meta 在 2023 年开源的一个 70B 参数的高质量大语言模型&#xff0c;而 llama.cpp 是一个用 C 实现的轻量化…

对开源VLA sota π0的微调——如何基于各种开源数据集、以及你自己的私有数据集微调π0(含我司的微调实践)

前言 25年2.4日&#xff0c;几个月前推出π0的公司Physical Intelligence (π)宣布正式开源π0及π0-FAST&#xff0c;如之前所介绍的&#xff0c;他们对用超过 10,000 小时的机器人数据进行了预训练 该GitHub代码仓库「 π0及π0-FAST的GitHub地址&#xff1a;github.com/Ph…

Redis网络模型

redis为什么快 1.主要原因是因为redis是基于内存操作的&#xff0c;比起直接操作磁盘速度快好几倍 2.基于内存的数据库瓶颈主要是在网络io这一块&#xff0c;redis网络模型采用io多路复用技术能够高效的处理并发连接。 3.redis使用单线程执行命令&#xff0c;可以避免上下文…

PyTorch系列教程:Tensor.view() 方法详解

这篇简明扼要的文章是关于PyTorch中的tensor.view()方法的介绍与应用&#xff0c;与reshape()方法的区别&#xff0c;同时给出示例进行详细解释。 Tensor基础 Tensor(张量)的视图是一个新的Tensor&#xff0c;它与原始Tensor共享相同的底层数据&#xff0c;但具有不同的形状或…

Python数据分析之数据可视化

Python 数据分析重点知识点 本系列不同其他的知识点讲解&#xff0c;力求通过例子让新同学学习用法&#xff0c;帮助老同学快速回忆知识点 可视化系列&#xff1a; Python基础数据分析工具数据处理与分析数据可视化机器学习基础 四、数据可视化 图表类型与选择 根据数据特…

swift -(5) 汇编分析结构体、类的内存布局

一、结构体 在 Swift 标准库中&#xff0c;绝大多数的公开类型都是结构体&#xff0c;而枚举和类只占很小一部分 比如Bool、 Int、 Double、 String、 Array、 Dictionary等常见类型都是结构体 ① struct Date { ② var year: Int ③ var month: Int ④ …

推荐一个比较好的开源的工作流引擎

由于DeepSeek等AI大模型的出现&#xff0c;工作流模式再次流行起来&#xff0c;低代码甚至零代码就可以实现应用开发&#xff0c;而且有DeepSeek这样的超级AI作为大脑&#xff0c;人人都可以开发自动化工作流。 比如搭建邮件助手工作流&#xff0c;可以自动润色各种邮件内容。…

CarPlanner:用于自动驾驶大规模强化学习的一致性自回归轨迹规划

25年2月来自浙大和菜鸟网络的论文“CarPlanner: Consistent Auto-regressive Trajectory Planning for Large-scale Reinforcement Learning in Autonomous Driving”。 轨迹规划对于自动驾驶至关重要&#xff0c;可确保在复杂环境中安全高效地导航。虽然最近基于学习的方法&a…

Fedora41安装MySQL8.4.4

Fedora41安装MySQL8.4.4 Fedora41用yum仓库安装MySQL8.4.4 笔记250310下载安装启动mysqld服务查看生成的初始密码 , 用初始密码登录登录后,必须修改初始密码才能执行其它操作可选设置降低密码强度要求, 使用简单密码降低 validate_password 组件对密码强度的要求 用SET GLOBAL命…

信息安全意识之安全组织架构图

一、信息安全技术概论1.网络在当今社会中的重要作用2.信息安全的内涵 网络出现前&#xff1a;主要面向数据的安全&#xff0c;对信息的机密性、完整性和可用性的保护&#xff0c;即CIA三元组 网络出现后&#xff0c;还涵盖了面向用户的安全&#xff0c;即鉴别&#xff0c;授权&…

安卓Android与iOS设备管理对比:企业选择指南

目录 一、管理方式差异 Android Enterprise方案包含三种典型模式&#xff1a; Apple MDM方案主要提供两种模式&#xff1a; 二、安全防护能力 Android系统特点&#xff1a; 三、应用管理方案 四、设备选择建议 五、典型场景推荐 需求场景 推荐方案 六、决策建议要点…

linunx ubuntu24.04.02装libfuse2导致无法开机进不了桌面解决办法

osu.appimage运行需要libfuse2 然后我就下了fuse,打了两把第二天无法开机 这样是不能开机的 这样是可以开机的 解决办法一&#xff1a;玩星火商店的osu&#xff0c;好了问题解决 解决办法二&#xff1a; 在这个页面 ctrl alt f2进入tty6 sudo apt install ubuntu-desktop 进…

mysql-8.0.41-winx64 手动安装详细教程(2025版)

mysql-8.0.41-winx64 手动安装详细教程&#xff08;2025版&#xff09; 一、下载安装包二、配置环境变量三、安装配置四、启动 MySQL 服务&#xff0c;修改密码 一、下载安装包 安装地址如下&#xff1a; https://dev.mysql.com/downloads/mysql/使用7-zip或其他解压软件&…

wireguard搭配udp2raw部署内网

前言 上一篇写了使用 wireguard 可以非常轻松的进行组网部署&#xff0c;但是如果服务器厂商屏蔽了 udp 端口&#xff0c;那就没法了 针对 udp 被服务器厂商屏蔽的情况&#xff0c;需要使用一款 udp2raw 或 socat 类似的工具&#xff0c;来将 udp 打包成 tcp 进行通信 这里以…