0


【Linux网络编程】第七弹---构建类似XShell功能的TCP服务器:从TcpServer类到主程序的完整实现

✨个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】

上一弹使用TCP协议实现客户端与服务端的通信,此弹实现一个类似于XShell的功能,客户端发出命令,服务端执行命令,并将执行结果返回给客户端!基本结构还是上一弹的结构,此处使用多线程版本即可,无需线程池的代码!

因为客户端的方法需要在TcpServer.hpp.hpp中声明,因此先讲解TcpServer.hpp!

1、TcpServer.hpp

TcpServer.hpp封装TcpServer类!

TcpServer类相较于上一弹只需要稍微修改即可,首先因为需要执行XShell的功能,必不可少的就是函数方法

函数方法声明:

// sockfd 用于接收消息和发送消息,addr 用于查看是谁发送的
using command_service_t = std::function<void(int sockfd,InetAddr addr)>;

1.1、TcpServer类基本结构

TcpServer类基本结构只需加一个方法成员变量,构造函数加该方法初始化即可

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

// 面向字节流
class TcpServer
{
public:
    TcpServer(command_service_t service,uint16_t port = gport)
        :_service(service), _port(port),_listensockfd(gsockfd),_isrunning(false)
    {}
    void InitServer();
    void Loop();
    ~TcpServer();
private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning;

    command_service_t _service;
};

1.2、 Execute()

Execute()执行回调函数!

static void *Execute(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    pthread_detach(pthread_self()); // 分离新线程,无需主线程回收
    td->_self->_service(td->_sockfd,td->_addr); // 执行回调
    ::close(td->_sockfd);
    delete td;
    return nullptr;
}

2、Command.hpp

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

2.1、Command类基本结构

成员变量使用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("touch"); // touch filename
    _safe_command.insert("pwd");
    _safe_command.insert("whoami");
    _safe_command.insert("which"); // which pwd
}

~Command()
{}

2.3、SafeCheck()

安全检查函数检查字符串的前缀,如果与set容器中的其中一个内容相同则返回true,不相同则返回false

此处用到C语言的字符串比较函数,比较前n个字节,相等则返回0!

strncmp()

#include <string.h>

int strncmp(const char *s1, const char *s2, size_t n);

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()函数直接执行C语言字符串的命令,如果执行成功(返回值不为空)以行读取的方式将结果拼接到result字符串中,但是有些命令没有执行结果,此时打印success,****执行失败(返回值为空)则返回Execute error。

注意:前提需要判断命令是否安全,不安全直接返回unsafe!

popen()

创建一个管道,并将该管道与一个命令(通过shell执行)的输入或输出连接起来。

#include <stdio.h>

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

int pclose(FILE *stream);

参数

  • command: 一个指向以null结尾的字符串的指针,该字符串包含了要执行的命令
  • type: 一个指向以null结尾的字符串的指针,该字符串决定了管道的方向。它可以是** "r"(表示读取命令的输出)**或 "w"(表示向命令写入输入)。

返回值

  • **成功时,popen返回一个指向FILE对象的指针(与fopen函数一样)**,该对象可用于freadfwritefprintffscanf等标准I/O函数。
  • 失败时,返回nullptr,并设置errno以指示错误。

Excute()

// 安全执行
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";
}

2.4、HandlerCommand()

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

此处换一批接收消息和发送的函数,与read和write还是基本一致的,有稍微差别!

recv()

与套接字(sockets)一起使用,用于从连接的对等端接收数据。

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数

  • sockfd: 套接字描述符,标识一个打开的套接字
  • buf:*指向一个缓冲区的指针,该缓冲区用于*存储接收到的数据
  • len: 指定缓冲区的长度(以字节为单位),即recv函数最多可以接收的数据量。
  • **flags:**通常设置为0,但也可以指定一些特殊的标志来修改recv的行为。例如,MSG_PEEK标志允许程序查看数据而不从套接字缓冲区中移除它。

返回值

  • 成功时,recv返回实际接收到的字节数。如果连接已经正常关闭,返回0
  • 失败时,返回-1,并设置errno以指示错误类型。

send()

与套接字(sockets)一起使用,用于向连接的对等端发送数据。

#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数

  • sockfd: 套接字描述符,标识一个打开的套接字
  • buf: 指向包含要发送数据的缓冲区的指针
  • len: 指定要发送的数据的字节数
  • flags: 通常设置为0,但也可以指定一些特殊的标志来修改send的行为。例如,MSG_DONTWAIT标志可以使send函数在非阻塞套接字上立即返回,如果无法立即发送数据则返回错误。

返回值

  • 成功时,send返回实际发送的字节数。这个值可能小于len,特别是当套接字是非阻塞的或发送缓冲区已满时。
  • 失败时,返回-1,并设置errno以指示错误类型。
void HandlerCommand(int sockfd, InetAddr addr)
{
    // 我们把他当做一个长服务
    while (true)
    {
        char commandbuffer[1024]; // 当做字符串
        // 1.接收消息(read)
        ssize_t n = ::recv(sockfd, commandbuffer, sizeof(commandbuffer) - 1,0); // TODO
        if (n > 0)
        {
            commandbuffer[n] = 0;
            LOG(INFO, "get command from client [%s],command: %s\n", addr.AddrStr().c_str(), commandbuffer);
            
            std::string result = Excute(commandbuffer);
            // 2.发送消息(write)
            ::send(sockfd, result.c_str(), result.size(),0);
        }
        // 读到文件结尾
        else if (n == 0)
        {
            LOG(INFO, "client %s quit\n", addr.AddrStr().c_str());
            break;
        }
        else
        {
            LOG(ERROR, "read error\n", addr.AddrStr().c_str());
            break;
        }
    }
}

3、TcpServerMain.cc

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

注意:声明的函数方法只有两个参数,而Command类的命令行处理函数有this指针,因此需要使用bind()绑定函数!

// ./tcpserver 8888
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " local-post" << std::endl;
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);

    Command cmdservice;

    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
        std::bind(&Command::HandlerCommand,
                  &cmdservice, std::placeholders::_1,
                  std::placeholders::_2),
        port); // 绑定函数

    tsvr->InitServer();
    tsvr->Loop();
    return 0;
}

运行结果

4、完整代码

4.1、TcpServer.hpp

#pragma once
#include <iostream>
#include <functional>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <pthread.h>
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace log_ns;

enum 
{
    SOCKET_ERROR,
    BIND_ERROR,
    LISTEN_ERROR
};

const static uint16_t gport = 8888;
const static int gsockfd = -1;
const static int gblcklog = 8;

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

// 面向字节流
class TcpServer
{
public:
    TcpServer(command_service_t service,uint16_t port = gport)
        :_service(service), _port(port),_listensockfd(gsockfd),_isrunning(false)
    {}

    void InitServer()
    {
        // 1.创建socket
        _listensockfd = ::socket(AF_INET,SOCK_STREAM,0);
        if(_listensockfd < 0)
        {
            LOG(FATAL,"socket create eror\n");
            exit(SOCKET_ERROR);
        }
        LOG(INFO,"socket create success,sockfd: %d\n",_listensockfd); // 3

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

        // 2.bind sockfd 和 socket addr
        if(::bind(_listensockfd,(struct sockaddr*)&local,sizeof(local)) < 0)
        {
            LOG(FATAL,"bind eror\n");
            exit(BIND_ERROR);
        }
        LOG(INFO,"bind success\n");

        // 3.因为tcp是面向连接的,tcp需要未来不短地获取连接
        // 老板模式,随时等待被连接
        if(::listen(_listensockfd,gblcklog) < 0)
        {
            LOG(FATAL,"listen eror\n");
            exit(LISTEN_ERROR);
        }
        LOG(INFO,"listen success\n");
    }
    // 内部类
    class ThreadData
    {
    public:
        int _sockfd;
        TcpServer* _self;
        InetAddr _addr;
    public:
        ThreadData(int sockfd,TcpServer* self,const InetAddr &addr)
            :_sockfd(sockfd),_self(self),_addr(addr)
        {}
    };
    void Loop()
    {
        _isrunning = true;
        while(_isrunning)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // 1.获取新连接
            int sockfd = ::accept(_listensockfd,(struct sockaddr*)&client,&len);
            // 获取失败继续获取
            if(sockfd < 0)
            {
                LOG(WARNING,"sccept reeor\n");
                continue;
            }
            InetAddr addr(client);
            LOG(INFO,"get a new link,client info: %s,sockfd:%d\n",addr.AddrStr().c_str(),sockfd); // 4
            
            // 获取成功
            // version 2 -- 多线程版 -- 不能关闭fd了,也不需要 
            pthread_t tid;
            ThreadData *td = new ThreadData(sockfd, this,addr);
            pthread_create(&tid,nullptr,Execute,td); // 新线程分离
        }
        _isrunning = false;
    }
    // 无法调用类内成员 无法看到sockfd
    static void *Execute(void *args)
    {
        ThreadData *td = static_cast<ThreadData *>(args);
        pthread_detach(pthread_self()); // 分离新线程,无需主线程回收
        td->_self->_service(td->_sockfd,td->_addr); // 执行回调
        ::close(td->_sockfd);
        delete td;
        return nullptr;
    }
    ~TcpServer()
    {}
private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning;

    command_service_t _service;
};

4.2、Command.hpp

#pragma once

#include <iostream>
#include <set>
#include <string>
#include <cstring>
#include <cstdio>
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace log_ns;

class Command
{
public:
    Command()
    {
        // 白名单
        _safe_command.insert("ls");
        _safe_command.insert("touch"); // touch filename
        _safe_command.insert("pwd");
        _safe_command.insert("whoami");
        _safe_command.insert("which"); // which pwd
    }
    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";
    }
    void HandlerCommand(int sockfd, InetAddr addr)
    {
        // 我们把他当做一个长服务
        while (true)
        {
            char commandbuffer[1024]; // 当做字符串
            // 1.接收消息(read)
            ssize_t n = ::recv(sockfd, commandbuffer, sizeof(commandbuffer) - 1,0); // TODO
            if (n > 0)
            {
                commandbuffer[n] = 0;
                LOG(INFO, "get command from client [%s],command: %s\n", addr.AddrStr().c_str(), commandbuffer);
                
                std::string result = Excute(commandbuffer);
                // 2.发送消息(write)
                ::send(sockfd, result.c_str(), result.size(),0);
            }
            // 读到文件结尾
            else if (n == 0)
            {
                LOG(INFO, "client %s quit\n", addr.AddrStr().c_str());
                break;
            }
            else
            {
                LOG(ERROR, "read error\n", addr.AddrStr().c_str());
                break;
            }
        }
    }
    ~Command()
    {
    }
private:
    std::set<std::string> _safe_command; // 只允许执行的命令
};

4.3、TcpServerMain.cc

#include "TcpServer.hpp"
#include "Command.hpp"
#include <memory>

// ./tcpserver 8888
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " local-post" << std::endl;
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);

    Command cmdservice;

    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
        std::bind(&Command::HandlerCommand,
                  &cmdservice, std::placeholders::_1,
                  std::placeholders::_2),
        port); // 绑定函数

    tsvr->InitServer();
    tsvr->Loop();
    return 0;
}
标签: 网络 服务器 运维

本文转载自: https://blog.csdn.net/2201_75584283/article/details/143098000
版权归原作者 小林熬夜学编程 所有, 如有侵权,请联系我们删除。

“【Linux网络编程】第七弹---构建类似XShell功能的TCP服务器:从TcpServer类到主程序的完整实现”的评论:

还没有评论