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

news2025/3/10 11:33:29

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;
            // 启动后就进行connect
            struct 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.coloradmin.cn/o/2312666.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

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

一、信创类型 &#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框…

Unity DOTS从入门到精通之EntityCommandBufferSystem

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

<论文>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…

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…

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英寸:折叠屏领域的新突破

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

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…

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 插入居…

电子学会—2024年月6青少年软件编程(图形化)四级等级考试真题——水仙花数

水仙花数 如果一个三位数等于它各个数位上的数字的立方和&#xff0c;那么这个数就是水仙花数&#xff0c;例如:153 111 555 333&#xff0c;153就是一个水仙花数。 1.准备工作 (1)保留默认角色小猫; (2)白色背景。 2.功能实现 (1)使用循环遍历所有三位数&#xff0c;把所…

JetBrains学生申请

目录 JetBrains学生免费授权申请 IDEA安装与使用 第一个JAVA代码 1.利用txt文件和cmd命令运行 2.使用IDEA新建项目 JetBrains学生免费授权申请 本教程采用学生校园邮箱申请&#xff0c;所以要先去自己的学校申请校园邮箱。 进入JetBrains官网 点击立即申请&#xff0c;然…

langchain系列(终)- LangGraph 多智能体详解

目录 一、导读 二、概念原理 1、智能体 2、多智能体 3、智能体弊端 4、多智能体优点 5、多智能体架构 6、交接&#xff08;Handoffs&#xff09; 7、架构说明 &#xff08;1&#xff09;网络 &#xff08;2&#xff09;监督者 &#xff08;3&#xff09;监督者&…