Linux-> TCP 编程2

目录

本文说明

一:command程序的几个问题

二:代码

1:TcpServer.hpp

2:CommandExcute.hpp

3:Main.cc

4:MainClient.cc

5:safe.txt

6:InetAddr.hpp

7:Log.hpp

三:效果


本文说明

本文实现了一个TCP编程下的command程序,客户端发送命令,会在服务端去执行该命令,然后客户端会显示在服务端执行该命令的结果,并且是在多线程版本下实现的command程序

而TCP相关的接口已经在上篇博客中见过了,整体的代码逻辑是类似的,不再赘述,如下:

https://blog.csdn.net/shylyly_/article/details/152226621

一:command程序的几个问题

Q1:我们的指令从何而来?

**A1:**和UDP的字典程序一样,我们创建一个文件safe.txt,文件中存储的就是我们允许客户端输入的指令,到时候,再把该文件的内容导入到set容器中即可

Q2:是否需要手动的fork() + pipe() + exec() + dup2() ?

A2:

1: 不需要也不能这么做,会使整个服务器进程崩溃!因为我们目前是在多线程的情况下去实现的command程序,所以当某个线程调用 fork() 时,会复制整个进程 的所有线程状态,但只复制了调用 fork() 的这一个线程,其他线程都"消失"了。然后 exec()替换整个进程的地址空间 ,导致:①所有其他线程突然死亡,② 进程状态不一致,③资源泄漏,④整个服务崩溃!而如果只有单线程则可以使用 fork() + pipe() + exec() + dup2()这一套逻辑,在之前的自定义shell中就实现过

自定义shell博客: https://blog.csdn.net/shylyly_/article/details/149660081

2: 系统给我们提供了popen系列的接口,popen内部也是使用 fork()+exec(),但关键区别在于:popen() 在库函数层面处理了多线程安全问题 ,现代系统的 popen() 实现会:①:在fork前锁定必要的资源,②:处理多线程环境下的特殊状况,③:提供更安全的进程创建,所以我们只需直接使用系统已经提供好的接口即可!

**3:**所以其实popen系列的接口和我们之前的 fork() + pipe() + exec() + dup2() 是类似的,只不过他在执行这套逻辑的时候,内部安全地处理了 fork 和 exec 的流程。

Q3:指令有选项怎么提取主命令?

A3: 我们只需设定分隔符SEP为空格,然后查找到第一个空格,那么这个空格之前的内容就是主命令,比如"ls -l"则会截取到 ls 主命令,而如果没找到分隔符,则证明该命令没有带选项

Q4:指令的安全性怎么确保?

A4: 获取到客户端输入的指令之后,我们应该在白名单safe.txt中查找该指令,找到了证明其是安全的指令,才回去执行该指令,反之不安全,驳回

Q5:松耦合的实现逻辑

A5:

1: 和之前几篇的socket编程的逻辑一样,我们的服务类依旧是松耦合的,也就是我们的服务类是回调一个函数,该函数位于其他类中的,所以当我们服务类接收到一条来自客户端的指令的时候我,服务类会是把该指令作为参数传递到回调函数中的,回调函数会调用另一个类中的成员函数

**2:**在之前的UDP编程博客中,我们常常不需要考虑多线程,因为多线程往往是因为TCP的监听连接后的死循环服务而存在的,因为死循环,所以循环不结束,则无法进行下一次的监听连接!而UDP没有监听连接,所以只需循环地从fd套接字中接收信息和发送信息即可!而发送信息和接收信息等功能都在Server函数中,如果此时需要松耦合,则在Server中再调用回调函数即可

**3:**而作为多线程,我们的松耦合会显得复杂一点,因为线程必定需要一个线程执行的函数,所以我们在线程的执行函数中,先调用Server函数,该函数内部再去调用回调函数!仅仅多绕了一步而已

最后介绍一下popen和pclose函数:

popen 是标准 C 库函数,用于创建管道并执行 shell 命令。它封装了 fork()pipe()exec() 等系统调用的复杂细节。

函数原型:

cpp 复制代码
#include <stdio.h>

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

int pclose(FILE *stream);

参数说明:

  • command: 要执行的 shell 命令字符串

  • type:

    • "r": 读取模式 - 读取命令的输出

    • "w": 写入模式 - 向命令输入数据

返回值:

FILE*类型,直接使用文件的读写函数进行读取popen执行命令之后的返回信息即可

pclose() 参数:

  • stream : popen() 返回的文件指针

二:代码

1:TcpServer.hpp

TcpServer.hpp就是服务端的代码

cpp 复制代码
#include <iostream>
#include <string>
#include <functional>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <pthread.h>
#include <sys/types.h>
#include <sys/wait.h>

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

// 枚举错误类型
enum
{
    SOCKET_ERROR = 1, // 创建套接字错误
    BIND_ERROR,       // 绑定错误
    LISTEN_ERROR,     // 监听错误
    USAGE_ERROR       // 运行程序时输入的main的参数错误
};

const static int defaultsockfd = -1; // 设定的创建套接字返回的fd的初始值
const static int gbacklog = 16;      // 默认的listen接口的第二个参数的初始值

class TcpServer; // 声明一下 方便ThreadData类使用

// 线程数据类
class ThreadData
{
public:
    // 构造函数
    ThreadData(int fd, InetAddr addr, TcpServer *s) : sockfd(fd), clientaddr(addr), self(s)
    {
    }

public:
    int sockfd;          // 创建连接之后返回的fd
    InetAddr clientaddr; // 客户端的网络属性结构体
    TcpServer *self;     // 服务类指针
};

using func_t = std::function<std::string(const std::string &)>; // 服务器类回调的函数 该函数就是command程序的核心

// 服务类
class TcpServer
{
public:
    // 构造函数                          端口号        socket的返回值              是否运行          回调函数
    TcpServer(int port, func_t func) : _port(port), _listensock(defaultsockfd), _isrunning(false), _func(func)
    {
    }

    // 初始化服务端
    void InitServer()
    {
        // 1:创建tcp socket 套接字
        _listensock = ::socket(AF_INET, SOCK_STREAM, 0);

        // 创建套接字失败 打印语句提醒
        if (_listensock < 0)
        {
            LOG(FATAL, "socket error");
            exit(SOCKET_ERROR);
        }

        // 创建套接字成功 打印语句提醒
        LOG(DEBUG, "socket create success, sockfd is : %d\n", _listensock);

        // 2 填充sockaddr_in结构
        struct sockaddr_in local;           // 网络通信 所以定义struct sockaddr_in类型的变量
        memset(&local, 0, sizeof(local));   // 先把结构体清空 好习惯
        local.sin_family = AF_INET;         // 填写第一个字段 (地址类型)
        local.sin_port = htons(_port);      // 填写第二个字段PORT (需先转化为网络字节序)
        local.sin_addr.s_addr = INADDR_ANY; // 填写第三个字段IP (直接填写0即可,INADDR_ANY就是为0的宏)

        // 3 bind绑定
        // 我们填充好的local和我们创建的套接字进行绑定(绑定我们接收信息发送信息的端口)
        int n = ::bind(_listensock, (struct sockaddr *)&local, sizeof(local));

        // 绑定失败 打印语句提醒
        if (n < 0)
        {
            LOG(FATAL, "bind error");
            exit(BIND_ERROR);
        }
        // 绑定成功 打印语句提醒
        LOG(DEBUG, "bind success, sockfd is : %d\n", _listensock);

        // 4 监听连接
        // tcp是面向连接的,所以通信之前,必须先建立连接,而在连接之前 又需要先监听

        // 监听(第二个参数默认为16 在上文已被设置)
        n = ::listen(_listensock, gbacklog);

        // 监听失败 打印语句提醒
        if (n < 0)
        {
            LOG(FATAL, "listen error");
            exit(LISTEN_ERROR);
        }

        // 监听成功 打印语句提醒
        LOG(DEBUG, "listen success, sockfd is : %d\n", _listensock);
    }

    // Service(服务函数)
    // 负责监听连接成功之后的数据的发送与接收
    void Service(int sockfd, InetAddr client)
    {
        // 来到这里代表监听连接已经成功 打印语句提醒链接的客户端的IP和PORT 以及连接accept返回的fd
        LOG(DEBUG, "get a new link, info %s:%d, fd : %d\n", client.Ip().c_str(), client.Port(), sockfd);

        // 创建用户的前缀标识符 这样在服务端可以知道是哪个客户端发送的command指令了
        std::string clientaddr = "[" + client.Ip() + ":" + std::to_string(client.Port()) + "]# ";

        // 开始循环的接收客户端的发来的指令 并且调用回调函数把结果返回去
        while (true)
        {
            char inbuffer[1024]; // 对方端发来的信息 存储在inbuffer中

            ssize_t n = recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0); // 读取客户端发来的信息 放进inbuffer中

            // 读取成功
            if (n > 0)
            {
                inbuffer[n] = 0;                                  // 先把inbuffer的n下标置为0 使得读取数组内容时及时停止
                std::cout << clientaddr << inbuffer << std::endl; // 打印 前缀+指令  就知道是哪个客户端发的哪个指令

                std::string result = _func(inbuffer); // 调用回调函数_func  得到执行指令后的返回值result

                send(sockfd, result.c_str(), result.size(), 0); // 将返回值result返回给客户端
            }

            // 读取失败1: 客户端退出并且关闭了连接
            else if (n == 0)
            {
                // client 退出&&关闭连接了
                LOG(INFO, "%s quit\n", clientaddr.c_str());
                break; // 则跳出while循环 其清理干净连接产生的fd
            }

            // 读取失败2: 单纯的读取失败
            else
            {
                LOG(ERROR, "read error\n", clientaddr.c_str());
                break; // 则跳出while循环 其清理干净连接产生的fd
            }
        }
        // 客户端断开连接后的清理工作
        std::cout << "客户端断开连接,开始清理" << std::endl;
        ::close(sockfd); // 重要:关闭套接字
        std::cout << "连接已关闭" << std::endl;
    }

    // 线程执行的函数
    static void *HandlerSock(void *args) // IO 和 业务进行解耦
    {
        pthread_detach(pthread_self());                   // 线程分离 避免主线程等待新线程 导致无法并行
        ThreadData *td = static_cast<ThreadData *>(args); // 把参数恢复成ThreadData *类型的变量 用于调用Server函数
        td->self->Service(td->sockfd, td->clientaddr);    // 调用server函数 进行在Server函数中回调_func函数
        delete td;                                        // 回收资源
        return nullptr;
    }

    // Loop函数
    // 用于连接
    void Loop()
    {
        _isrunning = true;
        // 进行连接
        while (_isrunning)
        {
            struct sockaddr_in peer; // 用于接收客户端的网络属性结构体
            socklen_t len = sizeof(peer);

            // 连接函数 返回的值是server函数的参数 因为这个fd才是进行数据发送和接收的fd
            int sockfd = ::accept(_listensock, (struct sockaddr *)&peer, &len);

            // 连接失败
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error\n"); // 打印语句提醒
                continue;
            }
            // 采用多线程
            pthread_t t;

            // 创建一个ThreadData类类型的指针 作为HandlerSock函数的参数 方便调用服务器类的成员变量和成员函数
            ThreadData *td = new ThreadData(sockfd, InetAddr(peer), this);
            pthread_create(&t, nullptr, HandlerSock, td); // 每个线程都会去执行HandlerSock函数
        }
        _isrunning = false;
    }

    // 析构函数
    ~TcpServer()
    {
        if (_listensock > defaultsockfd)
            ::close(_listensock); // 服务类的析构函数才会真正的关闭监听套接字 上面的close都是关闭每次连接产生的套接字
    }

private:
    uint16_t _port;  // 端口号
    int _listensock; // 监听套接字
    bool _isrunning; // 是否运行

    func_t _func; // 回调函数
};

解释:

**①:**handler函数在类中的使用,首先handler由于在类中,所以必须是static修饰的,这样才可以符合handler的类型(避免this的干扰),其次我们上次采取的是,handler的参数是this指针,但是今天不行,因为我们的server函数的参数是fd和结构体,这些不是服务器类的成员,而是类中的临时变量,这就意味着,我们handler的参数虽然只有一个,但是该参数需要包含三个东西,所以我们把this,fd,结构体 都封装进一个类ThreadData中即可!在吧ThreadData*作为参数传递给handler!

**②:**使用线程也会有等待线程从而造成阻塞的问题,所以在每个线程的HandlerSock函数中先进行线程分离即可!

**③:**多线程之间是共享一份文件描述符表的!这意味着千万不能关闭其他线程的fd,比如主线程的监听套接字,或者其他新线程的acceot的fd!

**④:**然后线程执行函数中调用了Server函数,该Server函数内部会调用回调函数,参数就是客户端传过来的指令字符串!

2:CommandExcute.hpp

CommandExcute.hpp就是command程序的核心,回调函数就位于此类中,这也是松耦合的体现

cpp 复制代码
#pragma once

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

const std::string sep = " "; // 声明分隔符

// Command类
// 指令类(内部的成员函数Excute是Command程序的核心)
class Command
{
private:
    // LoadConf函数
    // 负责加载指定路径下的白名单文件中的指令
    void LoadConf(const std::string &conf)
    {
        // 打开文件
        std::ifstream in(conf);

        // 打开失败
        if (!in.is_open())
        {
            LOG(FATAL, "open %s error\n", conf.c_str()); // 打印语句 提醒
            return;
        }

        // 来到这 代表打开已经成功
        std::string line; // 读取到的每一行 放进line中

        // 则开始读取每一行
        while (std::getline(in, line))
        {
            // 把读取到的每一行都打印出来 在服务端显现出来 方便查看
            LOG(DEBUG, "load command [%s] success\n", line.c_str());
            // 把每次读取到的line都插入到set容器_safe_cmd中
            _safe_cmd.insert(line);
        }
        in.close(); // 关闭文件
    }

public:
    // 构造函数 (参数为白名单的路径)
    Command(const std::string &cond_path) : _cond_path(cond_path)
    {
        // 调用LoadConf函数 把指定路径下的白名单全部读取加载仅set容器_safe_cmd中
        LoadConf(_cond_path);
    }

    // PrefixCommand函数
    // 用于提取主命令名 避免选项的干扰
    std::string PrefixCommand(const std::string &cmd)
    {
        if (cmd.empty()) // 如果命令为空,返回空字符串
            return std::string();
        auto pos = cmd.find(sep);     // 查找分隔符sep
        if (pos == std::string::npos) // 如果找不到分隔符,则代表该命名无选项 则返回整个命令
            return cmd;
        else // 找到分隔符,返回分隔符之前的部分
            return cmd.substr(0, pos);

        // sep是空格
        // PrefixCommand("ls -a -l")     // 返回 "ls"
        // PrefixCommand("touch a.txt")  // 返回 "touch"
        // PrefixCommand("pwd")          // 返回 "pwd"(没有参数)
        // PrefixCommand("")             // 返回 ""
    }

    // SafeCheck函数
    // 检查命令是否在白名单中 杜绝执行危险指令
    bool SafeCheck(const std::string &cmd)
    {
        // 调用 PrefixCommand 获取主命令名
        std::string prefix = PrefixCommand(cmd); // ls -a -l , touch a.txt

        // 在 set容器_safe_cmd 中查找该主命令名
        auto iter = _safe_cmd.find(prefix);

        // 没找到 则代表是非白名单指令 是不安全的 则返回false
        if (iter == _safe_cmd.end())
            return false;
        return true; // 找到了则代表是白名单命令 是安全的  返回true
    }

    // Excute函数
    // 执行指令函数
    std::string Excute(const std::string &cmd) // ls -a -l
    {
        // result用于存储执行命令之后的返回值idea
        std::string result;

        // 调用SafeCheck函数 (其内部会完成提取主命令名+判断主命令名是否安全)
        if (SafeCheck(cmd))
        {
            // 来到这里代表主命令名是安全的

            // popen:创建管道和子进程,子进程执行命令,然后返回值为file*方便读取执行命令后的结果
            // 则打开管道执行命令 "r"代表以读取模式打开,获取命令输出
            FILE *fp = popen(cmd.c_str(), "r");

            // 如果打开失败,返回 "failed"
            if (fp == nullptr)
            {
                return "failed";
            }

            // 读取命令输出
            char buffer[1024];                                // buffer用于存储命令输出 每次存储一部分 不断的+=到result中
            while (fgets(buffer, sizeof(buffer), fp) != NULL) // fgets():逐行读取命令输出到buffer中
            {
                result += buffer; // 将buffer的内容+=到result字符串中
            }
            pclose(fp); // 关闭管道,释放资源
        }
        // 命令不安全
        else
        {
            result = "坏人\n"; // 打印坏人
        }
        return result; // 把result返回去 服务类中的回调函数就完成了 然后result会被send函数发送到客户端!
    }

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

private:
    std::set<std::string> _safe_cmd; // 用于存储安全指令的set容器(自动排序、去重)
    std::string _cond_path;          // 白名单文件的路径
};

解释:此类是整个command程序的核心

**①:**LoadConf函数用于把白名单的指令全部加载到Command类的成员变量set类型的_safe_cmd中,方便以后在set中查找某个指令,以判断是否位于_safe_cmd中。此函数会被构造函数所调用

**②:**PrefixCommand函数用于提取接收到的指令中的主命令,对于空指令和仅仅只有主命令的指令或者带有选项的指令,都能够做出正确的判断

**③:**SafeCheck函数用于检查命令是否在白名单中,杜绝执行危险指令的作用。所以第一步肯定是调用PrefixCommand先获取主命令,然后再在已经通过LoadConf函数填充好的_safe_cmd中去查找判断客户端发送的指令是否为安全指令

**④:**Excute函数就是服务类中回调函数所调用的函数,也是服务类中的构造函数的参数,所以Excute必定必定包含了SafeCheck函数,而SafeCheck内部又包含了PrefixCommand函数,所以Excute是一个综合性函数,其不但会提取主命令,还会确保指令的安全,然后才会去执行该命令

**⑤:**而执行命令我们上文说过,不再采取fork() + pipe() + exec() + dup2()的操作,而是使用popen接口和pclose去完成,我们一切都交给了popen去做,我们只需从popen的返回值FILE*类型的值fp中去读取popen执行完指令之后返回的信息即可!

**⑥:**而popen执行完命令返回的信息可能很多,所以我们不推荐不断地往result字符串去+=,而是一部分一部分的读取到1024大小的buffer中,当buffer满了,我们才+=到result中

3:Main.cc

Main.cc就是调用服务器类的代码文件!

cpp 复制代码
#include "TcpServer.hpp"
#include "CommandExcute.hpp"
#include <memory>

//
void Usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " local_port\n"
              << std::endl;
}

// ./tcpserver port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }
    EnableScreen();                     // 日志打印在屏幕上
    uint16_t port = std::stoi(argv[1]); // 从main的参数列表中获取端口号
    Command cmd("./safe.txt");          // 创建一个Command对象 其会自动加载"./safe.txt"下的白名单指令

    // 创建一个服务类的回调函数cmdExec  其作为服务类的构造函数的参数
    // 而cmdExec就是bind的Command类中的成员函数Excute
    func_t cmdExec = std::bind(&Command::Excute, &cmd, std::placeholders::_1);

    // 创建服务类对象 把回调函数cmdExec作为构造函数的参数传进去即可
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, cmdExec);

    tsvr->InitServer(); // 创建套接字--->bind绑定--->监听
    tsvr->Loop();       // 连接--->线程会执行线程函数--->线程函数调用Server函数--->Server函数内部调用回调函数--->回调函数就是cmdExec
    return 0;
}

解释:

唯一需要解释的就是我们的bind,之前我们的bind都是在创建服务器对象的时候,在填写构造函数参数的时候去进行bind的,现在只不过是先把回调函数去进行绑定bind了,再把bind之后的cmdExec函数填写到服务器类对象的构造函数的参数中罢了,都是一个道理

bind的用法博客: https://blog.csdn.net/shylyly_/article/details/151109228

4:MainClient.cc

MainClient.cc是客户端的代码

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

// 启动客户端 需要用户输入服务端的IP+PORT
void Usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
              << std::endl;
}

// ./tcp_client serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string serverip = argv[1];           // 从main的参数列表中获取到服务端IP
    uint16_t serverport = std::stoi(argv[2]); // 从main的参数列表中获取到服务端PORT

    // 1. 创建socket
    int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }
    // 2. 无需显式的bind OS会自己做

    // 构建目标主机 也就是服务端的网络属性结构体 方便后续的读取和发送数据.
    struct sockaddr_in server;

    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    // 客户端向服务端 发起连接请求 接口为connect
    int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));

    // 连接失败
    if (n < 0)
    {
        std::cerr << "connect error" << std::endl; // 打印语句提醒
        exit(3);
    }

    // 来到这代表连接已经成功 则开始发送和接收信息
    while (true)
    {
        std::cout << "Please Enter# "; // 打印请输入提示符
        std::string outstring;
        std::getline(std::cin, outstring); // 获取用户在客户端输入的消息 存放进outstring

        ssize_t s = send(sockfd, outstring.c_str(), outstring.size(), 0); // 向服务器发送信息

        // send 发送成功
        if (s > 0)
        {
            char inbuffer[1024];
            ssize_t m = recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0); // 接收服务器返回的信息

            // recv接收成功
            if (m > 0)
            {
                inbuffer[m] = 0;                    // 在字符串后面置0
                std::cout << inbuffer << std::endl; // 打印服务器返回的信息 也就是执行命令之后的执行结果
            }

            // recv接收失败
            else
            {
                break;
            }
        }
        // send发送失败
        else
        {
            break;
        }
    }

    ::close(sockfd); // 关闭客户端套接字
    return 0;
}

**解释:**客户端的逻辑完全和TCP博客1类似,不再赘述

5:safe.txt

safe.txt就是一个白名单文件,里面你也可以自由添加一些安全指令

cpp 复制代码
ls
pwd
tree
whoami
who
uname
cat
touch

接下来的就是一些老生常谈的文件了,日志文件和网络属性结构体文件,在之前的博客中都有提到不再赘述了~~

6:InetAddr.hpp

InetAddr类很简单,就是接收一个网络属性结构体,然后可以通过成员函数打印出单独的IP或PORT,让一个网络属性结构体更细粒度得被使用

cpp 复制代码
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
 
// 网络属性类 (该类可以返回某个用户对应的网络属性中的 IP 或P ORT 或直接返回网络属性结构体)
class InetAddr
{
private:
    // 私有方法(获取用户的IP 和 PORT)
    void GetAddress(std::string *ip, uint16_t *port)
    {
        // 通通需要转网络字节序
        *port = ntohs(_addr.sin_port);   // 存储进了成员变量_ip中
        *ip = inet_ntoa(_addr.sin_addr); // 存储进了成员变量_port中
    }
 
public:
    // 构造函数
    InetAddr(const struct sockaddr_in &addr) : _addr(addr)
    {
        GetAddress(&_ip, &_port); // 内部直接调用私有方法 GetAddress(获取用户的IP 和 PORT) 方便之后可以直接获取属性
    }
 
    // 获取用户的IP
    std::string Ip()
    {
        return _ip;
    }
 
    // 重载InetAddr类的==符号
    // 用于判断用户是否在存储用户的vector中
    bool operator==(const InetAddr &addr)
    {
        //比较ip和port  同时相等 才认为存在!
        if (_ip == addr._ip && _port == addr._port)
        {
            return true;
        }
        return false;
    }
 
    // 获取用户的网络属性结构体
    struct sockaddr_in Addr()
    {
        return _addr;
    }
 
    // 获取用户的PORT
    uint16_t Port()
    {
        return _port;
    }
 
    // 析构函数
    ~InetAddr()
    {
    }
 
private:
    struct sockaddr_in _addr; // 成员变量_addr 用于接收一个网络属性结构体
    std::string _ip;          // 成员变量_ip 用来存储用户的IP
    uint16_t _port;           // 成员变量_port 用来存储用户的PORT
};

7:Log.hpp

日志博客:https://blog.csdn.net/shylyly_/article/details/151263351

cpp 复制代码
#pragma once
 
#include <iostream>    //C++必备头文件
#include <cstdio>      //snprintf
#include <string>      //std::string
#include <ctime>       //time
#include <cstdarg>     //va_接口
#include <sys/types.h> //getpid
#include <unistd.h>    //getpid
#include <thread>      //锁
#include <mutex>       //锁
#include <fstream>     //C++的文件操作
 
std::mutex g_mutex;                    // 定义全局互斥锁
bool gIsSave = false;                  // 定义一个bool类型 用来判断打印到屏幕还是保存到文件
const std::string logname = "log.txt"; // 保存日志信息的文件名字
 
// 日志等级
enum Level
{
    DEBUG = 0,
    INFO,
    WARNING,
    ERROR,
    FATAL
};
 
// 将日志写进文件的函数
void SaveFile(const std::string &filename, const std::string &message)
{
    std::ofstream out(filename, std::ios::app);
    if (!out.is_open())
    {
        return;
    }
    out << message << std::endl;
    out.close();
}
 
// 日志等级转字符串--->字符串才能表示等级的意义 0123意义不清晰
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 GetTimeString()
{
    // 获取当前时间的时间戳(从1970-01-01 00:00:00开始的秒数)
    time_t curr_time = time(nullptr);
 
    // 将时间戳转换为本地时间的tm结构体
    // tm结构体包含年、月、日、时、分、秒等字段
    struct tm *format_time = localtime(&curr_time);
 
    // 检查时间转换是否成功
    if (format_time == nullptr)
        return "None";
 
    // 缓冲区用于存储格式化后的时间字符串
    char time_buffer[1024];
 
    // 格式化时间字符串:年-月-日 时:分:秒
    snprintf(time_buffer, sizeof(time_buffer), "%d-%02d-%02d %02d:%02d:%02d",
             format_time->tm_year + 1900, // tm_year: 从1900年开始的年数,需要加1900
             format_time->tm_mon + 1,     // tm_mon: 月份范围0-11,需要加1得到实际月份
             format_time->tm_mday,        // tm_mday: 月中的日期(1-31)
             format_time->tm_hour,        // tm_hour: 小时(0-23)
             format_time->tm_min,         // tm_min: 分钟(0-59)
             format_time->tm_sec);        // tm_sec: 秒(0-60,60表示闰秒)
 
    return time_buffer; // 返回格式化后的时间字符串
}
 
// 日志函数-->打印出日志
// 格式:时间 + 等级 + PID + 文件名 + 代码行数 + 可变参数
void LogMessage(int level, std::string filename, int line, bool issave, const char *format, ...)
{
 
    std::string levelstr = LevelToString(level); // 得到等级字符串
    std::string timestr = GetTimeString();       // 得到时间字符串
    pid_t selfid = getpid();                     // 得到PID
 
    // 使用va_接口+vsnprintf得到用户想要的可变参数的字符串 存储与buffer中
    char buffer[1024];
    va_list arg;
    va_start(arg, format);
    vsnprintf(buffer, sizeof(buffer), format, arg);
    va_end(arg);
 
    std::lock_guard<std::mutex> lock(g_mutex); // 引入C++的RAII的锁 保护打印功能
 
    // 保存格式为时间 + 等级 + PID + 文件名 + 代码行数 + 可变参数 的日志信息 到message中
    std::string message = "[" + timestr + "]" + "[" + levelstr + "]" +
                          "[" + std::to_string(selfid) + "]" +
                          "[" + filename + "]" + "[" + std::to_string(line) + "] " + buffer;
 
    // 打印到屏幕
    if (!issave)
    {
        std::cout << message << std::endl;
    }
 
    // 保存进文件
    else
    {
        SaveFile(logname, message);
    }
}
 
// 宏定义 省略掉__FILE__  和 __LINE__
#define LOG(level, format, ...)                                                \
    do                                                                         \
    {                                                                          \
        LogMessage(level, __FILE__, __LINE__, gIsSave, format, ##__VA_ARGS__); \
    } while (0)
 
// 用户调用则意味着保存到文件
#define EnableScreen() (gIsSave = false)
 
// 用户调用则意味着打印到屏幕
#define EnableFile() (gIsSave = true)

三:效果

解释:

我们客户端和服务器连接之后,客户端输入的指令,会在服务器上运行,返回的也是服务器的shell的信息,pwd,就显示的是服务端的路径......

**注意:**删除需要ctrl+回车即可

相关推荐
迎風吹頭髮2 小时前
UNIX下C语言编程与实践33-UNIX 僵死进程预防:wait 法、托管法、信号忽略与捕获
c语言·网络·unix
upgrador3 小时前
操作系统命令:Linux与Shell(Operating System & Command Line, OS/CLI)目录导航、文件操作与日志查看命令实践
linux·ubuntu·centos
夜月yeyue3 小时前
多级流水线与指令预测
linux·网络·stm32·单片机·嵌入式硬件
xxtzaaa3 小时前
抖音私密账号显示IP属地吗?能更改IP么?
网络·网络协议·tcp/ip
IvanCodes4 小时前
十五、深入理解 SELinux
linux·运维·服务器
云博客-资源宝4 小时前
【防火墙源码】WordPress防火墙插件1.0测试版
linux·服务器·数据库
qq_479875434 小时前
systemd-resolved.service实验实战2
linux·服务器·网络
牧码岛4 小时前
服务端之NestJS接口响应message编写规范详解、写给前后端都舒服的接口、API提示信息标准化
服务器·后端·node.js·nestjs
CS Beginner6 小时前
【Linux】安装配置mysql中出现的问题2
linux·mysql·adb