Bootstrap

计算机网络socket编程(6)_TCP实网络编程现 Command_server

个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创

计算机网络socket编程(6)_TCP实网络编程现 Command_server

收录于专栏【计算机网络
本专栏旨在分享学习计算机网络的一点学习笔记,欢迎大家在评论区交流讨论💌 
  

目录

功能介绍 

1. InetAddr.hpp

2. LockGuard.hpp

3. Log.hpp

4. Command.hpp

5. TcpServer.hpp

6. TcpServerMain.cc

7. TcpClientMain.cc


功能介绍 

和上回 TCP 网络编程一样, 实现简单的 Command_server

还有就是网络编程代码真的是又多又杂, 有的时候我自己都烦, 没办法网络部分就是这样的, 我最近会尽快更完这个 socket 编程, 提早进入概念部分, 一直编程感觉少了什么~ 还得跟概念结合起来看, 感兴趣的宝子们不要忘记了点赞关注哦! 我现在在网络部分真的待不了一点, 希望我能尽快挣脱网络, 更新数据库 MySQL 的东西吧!  

1. InetAddr.hpp

 老演员了, InetAddr 类封装了 IP 地址和端口号,提供了转换网络字节序和主机字节序的方法,支持比较、获取 IP 地址、端口号、地址字符串等功能。这个类的设计是典型的用于网络编程,特别是在处理 IPv4 地址时非常有用。

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.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_p to n
        // p: process
        // n: net
        // inet_pton(int af, const char *src, void *dst);
        // inet_pton(AF_INET, ip.c_str(), &addr.sin_addr.s_addr);
        ::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);
    }
    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;
    }
    std::string AddrStr()
    {
        return _ip + ":" + std::to_string(_port);
    }
    ~InetAddr()
    {
    }

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

2. LockGuard.hpp

老演员 +1, LockGuard 类是一个封装了互斥锁的RAII对象,提供了简化的锁管理功能。它确保在对象生命周期结束时自动释放锁,从而避免了忘记解锁和潜在的死锁问题。它特别适用于多线程程序中对共享资源的保护,能显著提高代码的可读性和可靠性。 

#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;
};

3. Log.hpp

 老演员+1, 这段代码是一个 C++ 日志系统的实现,它提供了日志记录功能,可以将日志输出到屏幕或写入文件。系统使用了互斥锁(pthread_mutex_t)来保证日志输出的线程安全,并且提供了多种日志级别(DEBUG, INFO, WARNING, ERROR, FATAL)。

#pragma once

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <ctime>
#include <cstdarg>
#include <fstream>
#include <cstring>
#include <pthread.h>
#include "LockGuard.hpp"

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 "UNKNOWN";
        }
    }

    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;

    // log.logMessage("", 12, INFO, "this is a %d message ,%f, %s hellwrodl", x, , , );
    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)
        {
            // 加过滤逻辑 --- TODO

            LockGuard lockguard(&glock);
            switch (_type)
            {
            case SCREEN_TYPE:
                FlushLogToScreen(lg);
                break;
            case FILE_TYPE:
                FlushLogToFile(lg);
                break;
            }
        }
        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)
};

4. Command.hpp

这个代码实现了一个简单的命令执行器,允许客户端通过网络发送命令并执行,同时对命令进行安全检查,确保执行的是在白名单中的安全命令。 

#pragma once

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

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() {}
    // ls;rm -rf /
    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 -a -l -> os --> ls -
            ssize_t n = ::recv(sockfd, commandbuf, sizeof(commandbuf) - 1, 0); // TODO
            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;
};

Command 构造函数

构造函数初始化时,构建了一个白名单 safe_command,该白名单包括允许执行的命令。只有这些命令可以在后续的调用中被安全地执行。

"ls":列出目录内容。

"touch":创建空文件(命令格式如 touch filename)。

"pwd":显示当前工作目录。

"whoami":显示当前用户。

"which":显示命令的完整路径。

这些命令一般是无害的,可以允许远程执行。其他的命令(例如 rm -rf /)则会被拒绝。

SafeCheck 函数

这个函数用于检查命令是否安全。它遍历白名单中的每个命令,使用 strncmp 判断是否有命令前缀匹配。如果有匹配的命令前缀(例如 ls -l),则认为这个命令是安全的,返回 true,否则返回 false。

strncmp 的作用:strncmp 比较两个字符串的前 n 个字符,cmd.size() 是匹配的字符长度,因此如果 cmdstr 以白名单命令的某个前缀开始,它将被认为是一个安全命令

Excute 函数

这个函数用于执行实际的命令。首先,它会调用 SafeCheck 函数来确保命令是安全的。如果命令不在白名单中,返回 "unsafe"。

如果命令安全,它通过 popen 调用操作系统的命令行执行命令,并读取命令输出:

popen(cmdstr.c_str(), "r") 会执行命令,并返回一个文件指针,可以通过它读取命令的标准输出。

使用 fgets 一行一行地读取命令输出,直到没有输出为止。

最后将结果返回。如果命令有输出,则返回输出内容;如果命令没有输出,则返回 "success"。

如果执行命令时发生错误(例如,popen 失败),返回 "execute error"。

HandlerCommand 函数

这个函数处理与客户端的交互。它是一个长连接服务,持续接收和处理客户端发送的命令。

工作流程:

使用 recv 函数接收来自客户端的命令,存储在 commandbuf 中。recv 会返回接收到的字节数,如果接收到有效数据(n > 0),继续处理。

通过 LOG(INFO, ...) 记录从客户端接收到的命令。

使用 Excute 函数执行命令并获取执行结果。

使用 send 将命令的执行结果返回给客户端。

如果接收到的数据为 0(即客户端关闭连接),则记录日志并结束循环。

如果发生读取错误(n < 0),记录错误日志并结束循环。

这个方法基本上是一个长连接的服务器循环,不断接收和执行命令,直到客户端断开连接或发生错误。

5. TcpServer.hpp

这段代码实现了一个基本的 TCP 服务器 类 (TcpServer),通过多线程处理客户端的连接。 

#pragma once
#include <iostream>
#include <cstring>
#include <functional>
#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 = 1,
    BIND_ERROR,
    LISTEN_ERR
};

const static int gport = 8888;
const static int gsock = -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)
        : _port(port),
          _listensockfd(gsock),
          _isrunning(false),
          _service(service)
    {
    }
    void InitServer()
    {
        // 1. 创建socket
        _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(FATAL, "socket create error\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 error\n");
            exit(BIND_ERROR);
        }
        LOG(INFO, "bind success\n");

        // 3. 因为tcp是面向连接的,tcp需要未来不断地能够做到获取连接
        if (::listen(_listensockfd, gblcklog) < 0)
        {
            LOG(FATAL, "listen error\n");
            exit(LISTEN_ERR);
        }
        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()
    {
        // signal(SIGCHLD, SIG_IGN);
        _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 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;
    //         }
    //     }
    // }

    ~TcpServer() {}

private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning;

    command_service_t _service;
};

常量定义

定义了一些常量,用于表示不同阶段可能发生的错误类型(如 SOCKET_ERROR, BIND_ERROR, LISTEN_ERR

gport:默认的服务器端口号 8888。

gsock:默认的套接字文件描述符值,表示未初始化的套接字。

gblcklog:listen() 系统调用的 backlog(最大连接数),即服务器在开始接收连接之前,内核的连接队列最大容量。

command_service_t 类型定义

定义了一个类型别名 command_service_t,它是一个 std::function 类型,表示一个接受两个参数(sockfd 和 addr)并返回 void 的函数。这个函数在实际的应用中将用于处理每个客户端的连接请求。

TcpServer 类构造函数

command_service_t service:构造函数接收一个处理客户端请求的回调函数 service,即每当一个客户端连接建立后,服务器就调用该函数来处理连接。

uint16_t port:默认端口为 8888,服务器将监听该端口。

初始化服务器 (InitServer 方法)

创建 Socket:使用 ::socket() 创建一个 TCP 套接字 (SOCK_STREAM)。

如果创建失败(socket 返回值小于 0),日志记录错误并退出程序。

绑定 Socket 地址:通过 ::bind() 将创建的套接字与指定的端口和 IP 地址绑定。

服务器绑定到本机所有可用 IP 地址 (INADDR_ANY),并监听指定的端口。

监听连接:通过 ::listen() 开始监听传入的连接,gblcklog 参数指定了等待连接的最大队列长度。

ThreadData 类

ThreadData:用于保存每个客户端连接的信息,包含:

_sockfd:客户端的套接字文件描述符。

_self:指向当前 TcpServer 实例的指针。

_addr:客户端的地址信息(封装在 InetAddr 对象中)。

Loop 方法

Loop:这是服务器的主循环,主要功能是:

使用 ::accept() 接受新的客户端连接。

每当接收到一个新连接时,使用 pthread_create 创建一个新线程,线程通过 Execute 函数处理客户端的请求。

每个连接的信息(套接字和客户端地址)封装在 ThreadData 中传递给线程。

pthread_create:创建一个新线程来处理客户端的请求。线程通过 Execute 方法执行指定的服务函数。

Execute 方法

Execute:线程的入口函数,执行以下操作:

使用 pthread_detach(pthread_self()) 将线程设为分离状态,确保线程结束后自动回收资源。

调用传入的回调函数 td->_self->_service(td->_sockfd, td->_addr),即执行传递给服务器的具体业务逻辑。

关闭客户端连接的套接字。

删除 ThreadData 对象。

6. 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-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;
}

7. TcpClientMain.cc

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

// ./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?If the connection or binding succeeds
    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 socket 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;
}

 效果展示 : 

虽然命令有些少, 但是整体还是没有问题的~ 

;