【Linux网络】构建类似XShell功能的TCP服务器

📢博客主页:https://blog.csdn.net/2301_779549673

📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson

📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!

📢本文由 JohnKi 原创,首发于 CSDN🙉

📢未来很长,值得我们全力奔赴更美好的生活✨

文章目录


在上一篇文章中,笔者带大家实现了TCP服务器的 4种 客户端与服务端通信的模式 ,分别是单执行流模式、多进程模式、多线程模式、以及线程池模式

这一篇,我将带大家进一步理解TCP,就从用TCP实现类XShell功能服务器 ,采用的是多线程版本

🏳️‍🌈一、TcpServer.hpp

其他部分保持不变,我们添加一个处理回调函数,用来判断和执行相应的 xshell 命令

1.1 基本结构

复制代码
using handler_t = std::function<std::string(int sockfd, InetAddr addr)>;

class TcpServer{
    public:
        // 构造函数
        TcpServer(handler_t handler,uint16_t port = gport)
            :_handler(handler), _port(port), _isrunning(false){}
        // 初始化
        void InitServer(){}
        // server - 2 多线程版本
        void Loop(){}
        // 析构函数
        ~TcpServer(){}

    private:
        int _listensockfd; // 监听socket
        uint16_t _port;
        bool _isrunning;
        handler_t _handler;  
};

1.2 多线程处理函数

我们之前使用 Server 进行回显处理,这里将 Execute 函数的处理方法从 Server 改成 _handler就行了

复制代码
// 线程函数
        static void* Execute(void* args){
            ThreadDate* td = static_cast<ThreadDate*>(args);
            // 子线程结束后由系统自动回收资源,无需主线程调用 pthread_join
            pthread_detach(pthread_self()); // 分离新线程,无需主线程回收
            td->_self->_handler(td->_sockfd, td->_addr);
            delete td;
            return nullptr;
        }

🏳️‍🌈二、Common.hpp

2.1 基本结构

Command 类实现类似于XShell的功能,但是需要注意要让所有命令都可以执行,因为可能会导致删库等相关的问题。成员变量可以使用set容器存储允许执行命令的前缀!

复制代码
class Command{
    public:
        Command(){}
        bool SafeCheck(const std::string& cmdstr){}
        std::string Excute(const std::string& cmdstr){}
        void HandlerCommand(int sockfd, InetAddr addr){}
        ~Command(){}
    private:
        std::set<std::string> _safe_command;  // 允许执行的命令
};

2.2 构造、析构函数

根据自己的需要,在构造函数种将允许使用的命令插入到容器析构函数无需处理!

复制代码
Command() {
    // 白名单
    _safe_command.insert("ls");
    _safe_command.insert("pwd");
    _safe_command.insert("touch");
    _safe_command.insert("whoami");
    _safe_command.insert("which");
}
~Command(){}

2.3 SafeCheck()

检查当前命令是否在白名单中

复制代码
bool SafeCheck(const std::string& cmdstr){
            for(auto& cmd : _safe_command){
                // 值比较命令开头
                if(strncmp(cmd.c_str(), cmdstr.c_str(), cmd.size()) == 0){
                    return true;
                }
            }
            return false;
        }

2.4 Excute()

写这部分代码时,我们需要用到一个函数 - popen

popen 的作用是 ​创建子进程执行系统命令,并通过管道(Pipe)与其通信,他会启动子进程,调用 /bin/sh(或其他默认 shell)解析并执行指定的命令(如 ls -l、grep "error" 等)。

复制代码
std::string Excute(const std::string& cmdstr) {
    // 检查是否安全,不安全返回
    if (!SafeCheck(cmdstr)) {
        return "unsafe";
    }
    std::string result;
    // popen 创建子进程执行系统命令,并通过管道(Pipe)与其通信
    // popen(const char* command, const char* type)
    // command: 命令字符串
    // type: 管道类型,"r"表示读,"w"表示写,"r+"表示读写
    // 返回文件指针,失败返回NULL
    FILE* fp = popen(cmdstr.c_str(), "r");
    if (fp) {
        // 读取子进程的输出
        // 一行读取
        char line[1024];
        while (fgets(line, sizeof(line), fp)) {
            result += line;
        }
        return result.empty() ? "success" : result; // 有些命令创建无返回值
    }
}

2.5 HandlerCommand()

命令处理函数是一个长服务(死循环)先接收客户端的信息,如果接收成功则处理收到的消息(命令),并将处理的结果发送给客户端,如果读到文件结尾或者接收失败则退出循环!

复制代码
void HandlerCommand(int sockfd, InetAddr addr) {
    // 我们把它当作一个长服务
    while (true) {
        char commandbuffer[1024]; // 接收命令的缓冲区
        // 1. 接收消息
        // recv(int sockfd, void* buf, size_t len, int flags)
        // sockfd: 套接字描述符
        // buf: 接收缓冲区
        // len: 接收缓冲区大小
        // flags: 接收标志  0表示阻塞,非0表示非阻塞
        ssize_t n = ::recv(sockfd, commandbuffer, sizeof(commandbuffer) - 1, 0);
        if (n > 0) {
            commandbuffer[n] = 0;
            LOG(LogLevel::INFO) << "get command from client" << addr.AddrStr()
                                << ":" << commandbuffer;
            std::string result = Excute(commandbuffer);

            // 2. 发送消息
            // send(int sockfd, const void* buf, size_t len, int flags)
            // sockfd: 套接字描述符
            // buf: 发送缓冲区
            // len: 发送缓冲区大小
            // flags: 发送标志  0表示不阻塞,非0表示阻塞
            ::send(sockfd, result.c_str(), result.size(), 0);
        }
        // 读到文件结尾
        else if (n == 0) {
            LOG(LogLevel::INFO) << "client " << addr.AddrStr() << " quit";
            break;
        } else {
            LOG(LogLevel::ERROR) << "read error from client " << addr.AddrStr();
            break;
        }
    }
}

🏳️‍🌈三、TcpServer.cpp

服务端主函数使用智能指针构造Server对象(参数需要加执行方法)然后调用初始化与执行函数,调用主函数使用该可执行程序 + 端口号!

需要注意的是,Command::HandlerCommand 是 ​非静态成员函数,调用时必须通过 Command 类的实例(如 cmdservice)来访问。

复制代码
#include "TcpServer.hpp"
#include "Command.hpp"

int main(int argc, char* argv[]){
    if(argc != 2){
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        Die(1);
    }
    uint16_t port = std::stoi(argv[1]);

    Command cmdservice;

    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
        // &Command::HandlerCommand:成员函数指针。
        // &cmdservice:Command 对象的实例指针(this 指针)
        // _1 和 _2:占位符,表示回调函数接受两个参数(int sockfd 和 InetAddr addr)
        std::bind(&Command::HandlerCommand, &cmdservice, std::placeholders::_1, std::placeholders::_2),
        port);

    tsvr->InitServer();
    tsvr->Loop();

    return 0;
}

🏳️‍🌈四、测试

🏳️‍🌈五、整体代码

5.1 TcpServer.hpp

复制代码
#pragma once

#include <iostream>
#include <cstring>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include <functional>

#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"

#define BACKLOG 8

using namespace LogModule;
using namespace ThreadPoolModule;

static const uint16_t gport = 8080;


using handler_t = std::function<void(int sockfd, InetAddr addr)>;



class TcpServer{
    public:
        // 构造函数
        TcpServer(handler_t handler,uint16_t port = gport)
            :_handler(handler), _port(port), _isrunning(false){}

        // 初始化
        void InitServer(){
            // 1. 创建 socket
            _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if(_listensockfd < 0){
                LOG(LogLevel::ERROR) << "create socket error: " << strerror(errno);
                Die(2);
            }
            LOG(LogLevel::INFO) << "create sockfd success: " << _listensockfd;

            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(_port);
            local.sin_addr.s_addr = htonl(INADDR_ANY);


            // 2. 绑定 socket
            if(::bind(_listensockfd, CONV(&local), sizeof(local)) < 0){
                LOG(LogLevel::ERROR) << "bind socket error: " << strerror(errno);
                Die(3);
            }
            LOG(LogLevel::INFO) << "bind sockfd success: " << _listensockfd;



            // 3. 因为 tcp 是面向连接的,tcp需要未来不断地获取连接
                // listen 就是监听连接的意思,所以需要设置一个队列,来保存等待连接的客户端
                // 队列的长度为 8,表示最多可以有 8 个客户端等待连接
                // listen(int sockfd, int backlog)
                // sockfd 就是之前创建的 socket 句柄
                // backlog 就是队列的长度
                // 返回值:成功返回 0,失败返回 -1
            if(::listen(_listensockfd, BACKLOG) < 0){
                LOG(LogLevel::ERROR) << "listen socket error: " << strerror(errno);
                Die(4);
            }
            LOG(LogLevel::INFO) << "listen sockfd success: " << _listensockfd;
        }





        // server - 2 多线程版本
        // 为每个新连接分配独立的线程处理业务逻辑
        // (Loop 函数)通过 accept 循环监听新连接,为每个新连接创建线程(pthread_create)传递连接信息给子线程(通过 ThreadDate 结构体)
        // (Execute 函数)​​调用 pthread_detach 分离自身(避免主线程调用 pthread_join)执行 Server 函数处理具体业务逻辑线程结束后自动释放资源(通过 delete td)
        // (Server 函数)​循环读取客户端数据(read),处理业务逻辑(示例中的回显服务),发送响应(write)关闭连接(close(sockfd))
        void Loop(){
            _isrunning = true;
            while(_isrunning){
                struct sockaddr_in client;
                socklen_t len = sizeof(client);

                // 1. 获取新连接
                int sockfd = ::accept(_listensockfd, CONV(&client), &len);
                if(sockfd < 0){
                    LOG(LogLevel::ERROR) << "accept socket error: " << strerror(errno);
                    continue;
                }
                InetAddr cli(client);
                LOG(LogLevel::INFO) << "accept new connection from " << cli.AddrStr() << " sockfd: " << sockfd;

                // 获取成功
                pthread_t tid;
                ThreadDate* td = new ThreadDate(sockfd, this, cli);
                // pthread_create 第一个参数是线程id,第二个参数是线程属性,第三个参数是线程函数,第四个参数是线程函数参数
                pthread_create(&tid, nullptr, Execute, td);
            }
            _isrunning = false;
        }
        // 线程函数参数对象
        class ThreadDate{
            public:
                int _sockfd;
                TcpServer* _self;
                InetAddr _addr;
            public:
                ThreadDate(int sockfd, TcpServer* self, const InetAddr& addr)
                    : _sockfd(sockfd), _self(self), _addr(addr)
                {}
        };
        // 线程函数
        static void* Execute(void* args){
            ThreadDate* td = static_cast<ThreadDate*>(args);
            // 子线程结束后由系统自动回收资源,无需主线程调用 pthread_join
            pthread_detach(pthread_self()); // 分离新线程,无需主线程回收
            td->_self->_handler(td->_sockfd, td->_addr);
            delete td;
            return nullptr;
        }



        // 析构函数
        ~TcpServer(){}

    private:
        int _listensockfd; // 监听socket
        uint16_t _port;
        bool _isrunning;

        handler_t _handler;  
};

5.2 TcpServer.cpp

复制代码
#include "TcpServer.hpp"
#include "Command.hpp"

int main(int argc, char* argv[]){
    if(argc != 2){
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        Die(1);
    }
    uint16_t port = std::stoi(argv[1]);

    Command cmdservice;

    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
        // &Command::HandlerCommand:成员函数指针。
        // &cmdservice:Command 对象的实例指针(this 指针)
        // _1 和 _2:占位符,表示回调函数接受两个参数(int sockfd 和 InetAddr addr)
        std::bind(&Command::HandlerCommand, &cmdservice, std::placeholders::_1, std::placeholders::_2),
        port);

    tsvr->InitServer();
    tsvr->Loop();

    return 0;
}

5.3 Command.hpp

复制代码
#pragma once


#include <iostream>
#include <set>
#include <cstring>
#include <cstdio>

#include "InetAddr.hpp"
#include "Log.hpp"

using namespace LogModule;

class Command{
    public:
        Command(){
            // 白名单
            _safe_command.insert("ls");
            _safe_command.insert("pwd");
            _safe_command.insert("touch");
            _safe_command.insert("whoami");
            _safe_command.insert("which");
        }
        bool SafeCheck(const std::string& cmdstr){
            for(auto& cmd : _safe_command){
                // 值比较命令开头
                if(strncmp(cmd.c_str(), cmdstr.c_str(), cmd.size()) == 0){
                    return true;
                }
            }
            return false;
        }
        std::string Excute(const std::string& cmdstr){
            // 检查是否安全,不安全返回
            if(!SafeCheck(cmdstr)){
                return "unsafe";
            }
            std::string result;
            FILE* fp = popen(cmdstr.c_str(), "r");
            if(fp){
                char line[1024];
                while(fgets(line, sizeof(line), fp)){
                    result += line;
                }
                return result.empty() ? "success" : result; // 有些命令创建无返回值
            }
            return "Execute error";
        }
        std::string Excute(std::string& cmdstr){
            // 检查是否安全,不安全返回
            if(!SafeCheck(cmdstr)){
                return "unsafe";
            }
            std::string result;
            // popen 创建子进程执行系统命令,并通过管道(Pipe)与其通信
            // popen(const char* command, const char* type)
            // command: 命令字符串
            // type: 管道类型,"r"表示读,"w"表示写,"r+"表示读写
            // 返回文件指针,失败返回NULL
            FILE* fp = popen(cmdstr.c_str(), "r");
            if(fp){
                // 读取子进程的输出
                // 一行读取
                char line[1024];
                while(fgets(line, sizeof(line), fp)){
                    result += line;
                }
                return result.empty() ? "success" : result; // 有些命令创建无返回值
            }
        }
        void HandlerCommand(int sockfd, InetAddr addr){
            // 我们把它当作一个长服务
            while(true){
                char commandbuffer[1024];   // 接收命令的缓冲区
                // 1. 接收消息
                // recv(int sockfd, void* buf, size_t len, int flags)
                // sockfd: 套接字描述符
                // buf: 接收缓冲区
                // len: 接收缓冲区大小
                // flags: 接收标志  0表示阻塞,非0表示非阻塞
                ssize_t n = ::recv(sockfd, commandbuffer, sizeof(commandbuffer) - 1, 0);    
                if(n > 0){
                    commandbuffer[n] = 0;
                    LOG(LogLevel::INFO) << "get command from client" << addr.AddrStr() << ":" << commandbuffer;
                    std::string result = Excute(commandbuffer);

                    // 2. 发送消息
                    // send(int sockfd, const void* buf, size_t len, int flags)
                    // sockfd: 套接字描述符
                    // buf: 发送缓冲区
                    // len: 发送缓冲区大小
                    // flags: 发送标志  0表示不阻塞,非0表示阻塞
                    ::send(sockfd, result.c_str(), result.size(), 0);
                }
                // 读到文件结尾
                else if(n == 0){
                    LOG(LogLevel::INFO) << "client " << addr.AddrStr() << " quit";
                    break;
                }
                else{
                    LOG(LogLevel::ERROR) << "read error from client " << addr.AddrStr();
                    break;
                }
            }
        }
        ~Command(){}
    private:
        std::set<std::string> _safe_command;  // 允许执行的命令
};

👥总结

本篇博文对 【Linux网络】构建类似XShell功能的TCP服务器 做了一个较为详细的介绍,不知道对你有没有帮助呢

觉得博主写得还不错的三连支持下吧!会继续努力的~

相关推荐
亚力山大抵几秒前
实验2 python的TCP群聊系统实现
服务器·python·tcp/ip
李菠菜4 分钟前
Linux系统分区最佳实践
linux
互联网搬砖老肖12 分钟前
运维打铁:网络基础知识
运维·网络·智能路由器
学术小八14 分钟前
穿越链路的旅程:深入理解计算机网络中的数据链路层
linux·服务器·网络
三体世界21 分钟前
Linux 管道理解
linux·c语言·开发语言·c++·git·vscode·visual studio
茉莉玫瑰花茶1 小时前
socket编程基础
linux·服务器·网络
LouisCanBe1 小时前
Python 环境管理工具选择与安装实践:Conda 与 uv
linux·python
IT瘾君1 小时前
Java基础:网络编程UDP&TCP详解
java·网络·udp·tcp
桦01 小时前
Linux[指令与权限]
linux·运维·服务器
李匠20241 小时前
C++学习之游戏服务器开发十五QT登录器实现
服务器·c++·学习·游戏