Socket网络编程(2)-command_server

目录

引言

代码展示

复用代码部分

网络地址封装类InetAddr.hpp

锁的封装类LockGuard.hpp

日志类Log.hpp

新增/修改代码部分

命令业务类-Command.hpp

Tcp服务端源文件-TcpServerMain.cc

Tcp服务器端头文件-TcpServer.hpp

Tcp客户端源文件-TcpClientMain.cc


引言

通过前面的简单回显用户数据的基础版本,我们已经能够知道tcp通信的基础原理及过程,接下来我们可以来带点业务了,本章我们就来讲讲我们的指令服务版本。

代码展示

复用代码部分

网络地址封装类InetAddr.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// 封装网络地址类
class InetAddr
{
private:
    void ToHost(const struct sockaddr_in &addr)
    {
        _port = ntohs(addr.sin_port);
        //_ip = inet_ntoa(addr.sin_addr);
        char ip_buf[32];
        ::inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf));
        _ip = ip_buf;
    }

public:
    InetAddr(const struct sockaddr_in &addr)
        : _addr(addr)
    {
        ToHost(addr); // 将addr进行转换
    }

    std::string AddrStr()
    {
        return _ip + ":" + std::to_string(_port);
    }
    InetAddr()
    {
    }

    bool operator==(const InetAddr &addr)
    {
        return (this->_ip == addr._ip && this->_port == addr._port);
    }

    std::string Ip()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }
    struct sockaddr_in Addr()
    {
        return _addr;
    }
    ~InetAddr()
    {
    }

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

锁的封装类LockGuard.hpp

cpp 复制代码
#pragma once

#include <pthread.h>

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex) : _mutex(mutex)
    {
        pthread_mutex_lock(_mutex);
    }
    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);
    }

private:
    pthread_mutex_t *_mutex;
};

日志类Log.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <ctime>
#include <stdarg.h>
#include <fstream>
#include <string.h>
#include <pthread.h>


namespace log_ns
{
    enum
    {
        DEBUG = 1,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    std::string LevelToString(int level)
    {
        switch (level)
        {
        case DEBUG:
            return "DEBUG";
        case INFO:
            return "INFO";
        case WARNING:
            return "WARNING";
        case ERROR:
            return "ERROR";
        case FATAL:
            return "FATAL";
        default:
            return "UNKNOW";
        }
    }

    std::string GetCurrTime()
    {
        time_t now = time(nullptr);
        struct tm *curr_time = localtime(&now);
        char buffer[128];
        snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
                curr_time->tm_year + 1900,
                curr_time->tm_mon + 1,
                curr_time->tm_mday,
                curr_time->tm_hour,
                curr_time->tm_min,
                curr_time->tm_sec);
        return buffer;
    }

    class logmessage
    {
    public:
        std::string _level;
        pid_t _id;
        std::string _filename;
        int _filenumber;
        std::string _curr_time;
        std::string _message_info;
    };

    #define SCREEN_TYPE 1
    #define FILE_TYPE 2

    const std::string glogfile = "./log.txt";
    pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;

    class Log
    {
    public:
        Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE)
        {
        }
        void Enable(int type)
        {
            _type = type;
        }

        void FlushLogToScreen(const logmessage &lg)
        {
            printf("[%s][%d][%s][%d][%s] %s",
                lg._level.c_str(),
                lg._id,
                lg._filename.c_str(),
                lg._filenumber,
                lg._curr_time.c_str(),
                lg._message_info.c_str());
        }

        void FlushLogToFile(const logmessage &lg)
        {
            std::ofstream out(_logfile, std::ios::app);
            if (!out.is_open())
                return;
            char logtxt[2048];
            snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",
                    lg._level.c_str(),
                    lg._id,
                    lg._filename.c_str(),
                    lg._filenumber,
                    lg._curr_time.c_str(),
                    lg._message_info.c_str());
            out.write(logtxt, strlen(logtxt));
            out.close();
        }

        void FlushLog(const logmessage &lg)
        {
            pthread_mutex_lock(&glock);
            switch (_type)
            {
            case SCREEN_TYPE:
                FlushLogToScreen(lg);
                break;
            case FILE_TYPE:
                FlushLogToFile(lg);
                break;
            }
            pthread_mutex_unlock(&glock);
        }

        void logMessage(std::string filename, int filenumber, int level, const char *format, ...)
        {
            logmessage lg;

            lg._level = LevelToString(level);
            lg._id = getpid();
            lg._filename = filename;
            lg._filenumber = filenumber;
            lg._curr_time = GetCurrTime();

            va_list ap;
            va_start(ap, format);
            char log_info[1024];
            vsnprintf(log_info, sizeof(log_info), format, ap);
            va_end(ap);
            lg._message_info = log_info;

            // 打印出日志
            FlushLog(lg);
        }
        ~Log()
        {
        }

    private:
        int _type;
        std::string _logfile;
    };

    Log lg;

    #define LOG(level, Format, ...) do {lg.logMessage(__FILE__, __LINE__, level, Format, ##__VA_ARGS__); }while (0)
    #define EnableScreen() do {lg.Enable(SCREEN_TYPE);}while(0)
    #define EnableFile() do {lg.Enable(FILE_TYPE);}while(0)
}

新增/修改代码部分

命令业务类-Command.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cstdio>
#include <set>
#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
    }
    ~Command()
    {
    }

    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 commandbuf[1024]; // 当作字符串,ls -l
            ssize_t n = ::recv(sockfd, commandbuf, sizeof(commandbuf) - 1, 0);
            if (n > 0)
            {
                commandbuf[n] = 0;
                LOG(INFO, "get command from client %s, command: %s\n", addr.AddrStr().c_str(), commandbuf);
                std::string result = Excute(commandbuf);
                ::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: %s\n", addr.AddrStr().c_str());
                break;
            }
        }
    }

private:
    std::set<std::string> _safe_command; // 我们只以较为安全的命令来测试
};

命令业务类,顾名思义就是专门处理我们客户输入的指令的类,具体是如何做到的呢?我们就一步一步来看。

我们先来分析我们的私有成员变量,我们使用了一个集合类型的变量,这是干什么用的呢?我们知到Linux的指令使用方式非常多样,比如除了基础的指令之外还有多种指令一起使用的情况,而有些情况不做特殊处理是会出现bug的,因此我们这里为了不发生这些我们本次不关注的问题的干扰,我们预先定义一些安全的指令集合来保证我们业务的正常运行。

我们的构造函数的作用就是预先设置一部分安全的指令。比如ls,touch,pwd,whoami,which这些基础的指令。

我们需要有一个安全检查功能的函数,用于检查用户输入的指令是否是安全的。

安全检查功能结束后我们就可以启动我们的服务了,我们这里使用长服务的方式,因为用户不止输入一次指令。

  • Excute 函数

    • 接收一个字符串类型的命令(cmdstr)作为参数。
    • 首先调用 SafeCheck 函数对命令进行安全检查,若检查不通过则返回 "unsafe"。
    • 若安全检查通过,通过 popen 函数执行该命令(以读模式打开,用于获取命令输出)。
    • 使用 fgets 循环读取命令执行过程中的输出内容,拼接至 result 字符串中。
    • 命令执行完成后,若输出结果为空(如创建文件这类无输出的命令),返回 "success";否则返回命令的实际输出结果。
    • popen 调用失败(如无法执行命令),返回 "execute error"。
  • HandlerCommand 函数

    • 接收一个套接字描述符(sockfd)和客户端地址(InetAddr 类型的 addr),作为处理客户端命令的核心逻辑。
    • 采用无限循环实现 "长服务" 模式,持续处理来自同一客户端的命令。
    • 通过 recv 函数从套接字接收客户端发送的命令(存储在 commandbuf 缓冲区,最多接收 1023 字节,预留一个字节给字符串结束符)。
    • 若接收成功(n > 0),为命令添加字符串结束符,通过日志记录客户端地址和接收的命令。
    • 调用 Excute 函数执行该命令,获取执行结果,并通过 send 函数将结果发送回客户端。
    • 若接收字节数为 0(n == 0),表示客户端主动关闭连接,记录日志并退出循环。
    • 若接收失败(n < 0),记录错误日志并退出循环。

Tcp服务端源文件-TcpServerMain.cc

cpp 复制代码
#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-port" << 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;
}

源文件部分跟我们上一篇文章几乎一模一样,就是多了个绑定业务可调用对象的模块,这回我们为了简洁,直接在参数部分将这个可调用对象绑定好传给服务端去处理。

Tcp服务器端头文件-TcpServer.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <sys/wait.h>
#include <pthread.h>
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace log_ns;

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    LISTEN_ERROR
};

const static int gport = 8888;
const static int gsock = -1;
const static int gbacklog = 8;

using command_server_t = std::function<void(int sockfd, InetAddr addr)>;
class TcpServer
{
public:
    TcpServer(command_server_t service, uint16_t port = gport)
        : _port(port), _listensockfd(gsock), _isrunning(false), _service(service)
    {
    }
    void InitServer()
    {
        // 1.创建socket
        _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(FATAL, "listensockfd create error\n");
            exit(SOCKET_ERROR);
        }
        LOG(INFO, "listensockfd create success, fd: %d\n", _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 = INADDR_ANY;

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

        // 3.因为tcp是面向连接的,tcp需要未来不断地能够做到获取连接
        if (::listen(_listensockfd, gbacklog) < 0)
        {
            LOG(FATAL, "listen error\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);
            // 4. 获取连接
            int sockfd = ::accept(_listensockfd, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error\n");
                continue;
            }
            InetAddr addr(client);
            LOG(INFO, "get a new link, client info: %s, sockfd is : %d\n", addr.AddrStr().c_str(), sockfd);

            // version 0 --- 不稳定版本
            // Service(sockfd, addr);

            // // version 1 ---多进程版本
            // pid_t id = fork();
            // if (id == 0)
            // {
            //     // child
            //     ::close(_listensockfd); // 建议

            //     if (fork() > 0)
            //         exit(0);

            //     Service(sockfd, addr);
            //     exit(0);
            // }
            // // father
            // ::close(sockfd);
            // int n = waitpid(id, nullptr, 0);
            // if (n > 0)
            // {
            //     LOG(INFO, "wait child success.\n");
            // }

            // version 2 --- 多线程版本 --- 不能关闭fd了,也不需要了
            pthread_t tid;
            ThreadData *td = new ThreadData(sockfd, this, addr);
            pthread_create(&tid, nullptr, Execute, td); // 新线程进行分离
        }
        _isrunning = false;
    }
    static void *Execute(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        td->_self->_service(td->_sockfd, td->_addr);
        ::close(td->_sockfd);
        delete td;
        return nullptr;
    }

    void Service(int sockfd, InetAddr addr)
    {
        // 长服务
        while (true)
        {
            char inbuffer[1024]; // 当作字符串
            ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1);
            if (n > 0)
            {
                inbuffer[n] = 0;
                LOG(INFO, "get message from client %s, message: %s\n", addr.AddrStr().c_str(), inbuffer);
                std::string echo_string = "[server echo]# ";
                echo_string += inbuffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                LOG(INFO, "client %s quit\n", addr.AddrStr().c_str());
                break;
            }
            else
            {
                LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str());
                break;
            }
        }
        ::close(sockfd);
    }

    ~TcpServer()
    {
    }

private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning;

    command_server_t _service;
};

服务端的头文件我们也只是在原基础上将简单的echo业务转换为我们的command指令执行业务。

私有成员变量这一块多了一个可调用对象类型的变量_service,也就是我们的业务。

多了这一个业务变量,我们自然就要在构造函数上面给我们的业务赋值,这个值就是我们源文件中传的对象。

构建服务器这块我们是一模一样的,我就不细说了。

cpp 复制代码
    void Loop()
    {
        _isrunning = true;
        while (_isrunning)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // 4. 获取连接
            int sockfd = ::accept(_listensockfd, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error\n");
                continue;
            }
            InetAddr addr(client);
            LOG(INFO, "get a new link, client info: %s, sockfd is : %d\n", addr.AddrStr().c_str(), sockfd);

            // version 0 --- 不稳定版本
            // Service(sockfd, addr);

            // // version 1 ---多进程版本
            // pid_t id = fork();
            // if (id == 0)
            // {
            //     // child
            //     ::close(_listensockfd); // 建议

            //     if (fork() > 0)
            //         exit(0);

            //     Service(sockfd, addr);
            //     exit(0);
            // }
            // // father
            // ::close(sockfd);
            // int n = waitpid(id, nullptr, 0);
            // if (n > 0)
            // {
            //     LOG(INFO, "wait child success.\n");
            // }

            // version 2 --- 多线程版本 --- 不能关闭fd了,也不需要了
            pthread_t tid;
            ThreadData *td = new ThreadData(sockfd, this, addr);
            pthread_create(&tid, nullptr, Execute, td); // 新线程进行分离
        }
        _isrunning = false;
    }

Loop循环里面我们也是没做什么改动,只不过这回我们不用线程池,我们就用多线程版本,也就是执行线程函数。

我们先分离线程,然后做指针的安全类型转换,然后调用可调用对象执行任务,执行完成之后我们关闭套接字,我们的任务就完成了。

Tcp客户端源文件-TcpClientMain.cc

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>

// ./tcpclient server-ip server-port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 1. 创建socket
    int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(1);
    }

    // 注意:不需要显示的bind,但是一定要有自己的IP和port,所以需要隐式的bind,os会自动bind sockfd,用自己的IP和随机端口号
    // 什么时候进行自动bind?connect
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    ::inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);

    int n = ::connect(sockfd, (struct sockaddr *)&server, sizeof(server));
    if (n < 0)
    {
        std::cerr << "connect sockfd error" << std::endl;
        exit(2);
    }

    while (true)
    {
        std::string message;
        std::cout << "Enter # ";
        std::getline(std::cin, message);

        write(sockfd, message.c_str(), message.size());

        char echo_buffer[1024];
        n = read(sockfd, echo_buffer, sizeof(echo_buffer));
        if (n > 0)
        {
            echo_buffer[n] = 0;
            std::cout << echo_buffer << std::endl;
        }
        else
        {
            break;
        }
    }
    ::close(sockfd);
    return 0;
}

这个文件我们是没有任何修改的,因为客户端跟上回的echo一样,我们的客户端只需要写和读,不需要执行业务,所以无需修改。

相关推荐
维尔切3 小时前
Docker 存储与数据共享
运维·docker·容器
温柔一只鬼.3 小时前
Docker快速入门——第四章Docker镜像
运维·docker·容器
温柔一只鬼.4 小时前
Docker快速入门——Windowns系统下Docker安装(2025最新理解与完整,附带WSL1如何升级为WSL2)
运维·docker·容器
何朴尧4 小时前
centos/cuos如何开启软件源
linux·运维·centos
派阿喵搞电子5 小时前
关于使用docker部署srs服务器的相关指令
服务器·docker·容器
qq_339191145 小时前
aws ec2防ssh爆破, aws服务器加固, 亚马逊服务器ssh安全,防止ip扫描ssh。 aws安装fail2ban, ec2配置fail2ban
服务器·ssh·aws
csdn_Hzx5 小时前
Linux添加一个系统服务
linux·运维·服务器
重生之我在20年代敲代码6 小时前
【Linux】初始线程
linux·运维·服务器
问道飞鱼6 小时前
【Linux知识】Linux磁盘开机挂载
linux·运维·网络·磁盘·自动挂载