【Linux网络】多路转接和反应堆模式:基于单Reactor实现的网络版本计算器

🎬 个人主页艾莉丝努力练剑
专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录
Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬 艾莉丝的简介:


文章目录

  • [0 ~> 项目目录](#0 ~> 项目目录)
  • [1 ~> 推陈出新:网络版本计算器(客户端)- OnlineCalClient.cc](#1 ~> 推陈出新:网络版本计算器(客户端)- OnlineCalClient.cc)
  • [2 ~> 协议模块 - Protocol.hpp](#2 ~> 协议模块 - Protocol.hpp)
  • [3 ~> 日志类](#3 ~> 日志类)
    • [3.1 日志类实现 - Logger.hpp](#3.1 日志类实现 - Logger.hpp)
    • [3.2 互斥锁 - Mutex.hpp](#3.2 互斥锁 - Mutex.hpp)
    • [3.3 套接字 - (模版方法模式,继承 + 多态)Socket.hpp](#3.3 套接字 - (模版方法模式,继承 + 多态)Socket.hpp)
  • [4 ~> 客户端地址:网络和本地socket转换的类 - InetAddr.hpp](#4 ~> 客户端地址:网络和本地socket转换的类 - InetAddr.hpp)
    • [4.1 InetAddr.hpp](#4.1 InetAddr.hpp)
  • [5 ~> 多路转接版块:单Reactor模式(epoll方式)](#5 ~> 多路转接版块:单Reactor模式(epoll方式))
    • [5.1 "先描述"](#5.1 “先描述”)
      • [5.1.1 基类 - Connection.hpp](#5.1.1 基类 - Connection.hpp)
      • [5.1.2 子类](#5.1.2 子类)
        • [5.1.2.1 子类:连接管理器(虽然这么叫,但是特别像当年的TcpServer.hpp)- Listener.hpp](#5.1.2.1 子类:连接管理器(虽然这么叫,但是特别像当年的TcpServer.hpp)- Listener.hpp)
        • [5.1.2.2 子类:IO处理器(只负责读取和发送)- IOHandler.hpp](#5.1.2.2 子类:IO处理器(只负责读取和发送)- IOHandler.hpp)
    • [5.2 "再组织"](#5.2 “再组织”)
      • [5.2.1 多路转接模块 - Poller.hpp(epoll实现的,帮我监听所有的fd是否就绪)](#5.2.1 多路转接模块 - Poller.hpp(epoll实现的,帮我监听所有的fd是否就绪))
      • [5.2.2 单Reacror模式 - Reactor.hpp(基于事件驱动(Reactor模式)的高并发网络服务器底层框架,用于监听网络连接、读写IO事件并分发处理业务逻辑)](#5.2.2 单Reacror模式 - Reactor.hpp(基于事件驱动(Reactor模式)的高并发网络服务器底层框架,用于监听网络连接、读写IO事件并分发处理业务逻辑))
  • [6 ~> 公共类 - Common.hpp](#6 ~> 公共类 - Common.hpp)
    • [6.1 Common.hpp](#6.1 Common.hpp)
  • [7 ~> 主函数 - Main.cc](#7 ~> 主函数 - Main.cc)
  • [8 ~> 编译、链接、管理依赖关系 - Makefile](#8 ~> 编译、链接、管理依赖关系 - Makefile)
    • [8.1 Makefile的作用](#8.1 Makefile的作用)
    • [8.2 Makefile](#8.2 Makefile)
    • [8.3 Makefile在"基于单Reactor实现的网络版本计算器"中的作用](#8.3 Makefile在“基于单Reactor实现的网络版本计算器”中的作用)
  • [9 ~> 运行演示](#9 ~> 运行演示)
    • [9.1 备注](#9.1 备注)
    • [9.2 演示](#9.2 演示)
  • 结尾


0 ~> 项目目录


1 ~> 推陈出新:网络版本计算器(客户端)- OnlineCalClient.cc

1.1 OnlineCalClient.cc

cpp 复制代码
#include "Protocol.hpp"
#include "Socket.hpp"
#include <iostream>
#include <memory>

// 测试:客户端直接拿网络版本计算器里面实现过的(OnlineCalClient.cc)

void Usage(std::string procname)
{
    std::cout << "Usage: " << procname << " Server_Ip Server_Port" << std::endl;
}

int main(int argc,char *argv[])
{    
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    std::unique_ptr<Socket> sockfd = std::make_unique<TcpSocket>();
    sockfd->BuildClientSocketMethod(serverip, serverport);

    std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>();

    std::string inbuffer;
    while (true)
    {
        int cnt = 3;
        std::string sendstring;
        while(cnt--)
        {
            // 0.构建请求
            Request req;
            std::cout << "Enter x# ";
            std::cin >> req._x;
            std::cout << "Enter y# ";
            std::cin >> req._y;
            std::cout << "Enter oper# ";
            std::cin >> req._oper;

            // 1.序列化
            std::string reqjsonstr;
            req.Serialize(&reqjsonstr);

            // 2.封装报头
            sendstring += protocol->Package(reqjsonstr);
        }
        std::cout << sendstring << std::endl;

        // 3.发送
        sockfd->Send(sendstring);

        // 4.接收应答
        sockfd->Recv(&inbuffer);    // 你能保证读到的是完整的报文吗?

        // 5.解包并且判断
        while(true)
        {
            std::string jsonrespstr;
            int n = protocol->UnPackage(inbuffer, &jsonrespstr);
            LOG(LogLevel::DEBUG) << "UnPackage result: " << n << ", jsonrespstr: " << jsonrespstr;
            if(n == 0)
            {
                LOG(LogLevel::DEBUG) << "解包完成!";
                break;
            }
            
            // 6.反序列化
            Response resp;
            resp.DeSerialize(jsonrespstr);

            // 7.打印结果
            std::cout << resp._result << "[" << resp._exitCode << "]" << std::endl;
        }
    }
}

1.2 Calculator.hpp

cpp 复制代码
#ifndef __CALCULATOR_HPP
#define __CALCULATOR_HPP

#include <iostream>
#include <string>
#include "Protocol.hpp"

class Request;
class Response;

class Calculator
{
public:
    Calculator() {}
    ~Calculator() {}

public:
    Response Exec(const Request &req)
    {
        LOG(LogLevel::DEBUG) << "Enter Calculator Exec";
        
        Response resp; // {0, 0}
        // Switch case语句
        switch (req.Oper())
        {
        case '+':
            resp.SetResult(req.X() + req.Y());
            break;
        case '-':
            resp.SetResult(req.X() - req.Y());
            break;
        case '*':
            resp.SetResult(req.X() * req.Y());
            break;
        case '/':
        {
            if (req.Y() == 0)
                resp.SetExitCode(1);
            else
                resp.SetResult(req.X() / req.Y());
        }
        break;
        case '%':
        {
            if (req.Y() == 0)
                resp.SetExitCode(2);
            else
                resp.SetResult(req.X() % req.Y());
        }
        break;
        default:
            resp.SetExitCode(3);
            break;
        }

        return resp;
    }
};

#endif

2 ~> 协议模块 - Protocol.hpp

cpp 复制代码
#ifndef __PROTOCOL_HPP
#define __PROTOCOL_HPP

#include <iostream>
#include <string>
#include <memory>
#include <sstream>
#include <functional>
#include <jsoncpp/json/json.h>
#include "Logger.hpp"

using namespace LogModule;

// 请求报文
class Request
{
public:
    Request() : _x(0), _y(0), _oper('+')
    {}
    Request(int x,int y,char oper) : _x(x),_y(y),_oper(oper)
    {}
    // 序列化和反序列化
    void Serialize(std::string *outstr)
    {
        Json::Value root;
        root["datax"] = _x;
        root["datay"] = _y;
        root["oper"] = _oper;

        Json::FastWriter writer;
        *outstr = writer.write(root);
    }
    void DeSerialize(std::string &instr)
    {
        // 反序列化 --> 字节流信息(一变多)
        Json::Value root;
        Json::Reader reader;
        if(reader.parse(instr,root))    // parse要转进来instr、root,第三个参数不管
        {
            _x = root["datax"].asInt();
            _y = root["datay"].asInt();
            _oper = root["oper"].asInt();   // oper是char,没有AsChar
        }   // 至此就完成了反序列化
        else
        {
            // 这里也不搞什么bool了,直接打印"bug"
            std::cout << "bug!!!" << std::endl;
        }
    }
    void Print()
    {
        std::cout << "_x: " << _x << std::endl;
        std::cout << "_y: " << _y << std::endl;
        std::cout << "_oper: " << _oper << std::endl;

        // Json::Value root;
        // root["datax"] = _x;
        // root["datay"] = _y;
        // root["oper"] = _oper;
        // Json::FastWriter writer;
        // std::cout << writer.write(root);
    }
    ~Request()
    {}

    int X() const { return _x; }
    int Y() const { return _y; }
    char Oper() const { return _oper; }

private:
    int _x;
    int _y;
    char _oper;
};

// 应答报文,server -> client
class Response
{
public:
    Response() : _result(0), _exitcode(0)
    {}

    void SetResult(int result) { _result = result; }
    void SetExitCode(int exitcode) { _exitcode = exitcode; }
    int Result() const { return _result; }
    int ExitCode() const { return _exitcode; }

    void Serialize(std::string *outstr)
    {
        Json::Value root;
        root["result"] = _result;
        root["exitcode"] = _exitcode;

        Json::FastWriter writer;
        *outstr = writer.write(root);
    }
    void DeSerialize(std::string &instr)
    {
        Json::Value root;
        Json::Reader reader;
        if(reader.parse(instr,root))
        {
            _result = root["result"].asInt();
            _exitcode = root["exitcode"].asInt();
        }
    }
    ~Response()
    {}
private:
    int _result;
    int _exitcode;
};

// 特殊的分隔符
const std::string gsep = "\r\n";

// Calculator --> 对于协议来说,需要一个回调函数
using callback_t = std::function<Response(const Request &)>;

class Protocol
{
public:
    Protocol() {}
    Protocol(callback_t cb) : _cb(cb)   // 回调函数加进来
    {
    }
    ~Protocol() {}
    // 封包 {"datax":10,"datay":20,"oper":43} -> "40"\r\n{"datax":10,"datay":20,"oper":43}\r\n
    std::string Package(const std::string jsonstr)
    {
        uint32_t len = jsonstr.size();
        return std::to_string(len) + gsep + jsonstr + gsep;
    }

    // --> 返回值也要设置一个 <--
    // int > 0: 提取了一个完整json报文
    // int == 0: 不完整:什么都不做
    // int < 0: UnPackage出错了
    // "LE
    // "LEN"
    // "LEN"\\r\\n
    // --> 下面至少是读取到了长度 <--
    // "LEN"\\r\\n
    // "LEN"\\r\\n{"datax"
    // "LEN"\\r\\n{"datax":10,"dat
    // "LEN"\\r\\n{"datax":10,"datay":20,"oper":
    // "LEN"\\r\\n{"datax":10,"datay":20,"oper":43}\\r\\n"LEN"\\r\\n{"datax":10,"datay":20,"oper":43}\\r\\n"LEN"\\r\\n{"datax":10,"datay":20,"oper":43}\\r\\n

    // "LEN"\\r\\n{"datax":10,"datay":20,"oper":43}\\r\\n"LEN"\\r\\n{"datax"

    // 解包
    int UnPackage(std::string &streamstr,std::string *jsonstr) // 参数就要传一个,前面的参数一定是引用的
    {
        // a. 完整: 提取,处理;b.不完整:什么都不做
        // 从左向右找分隔符
        auto pos = streamstr.find(gsep);
        // 报文不完整
        if(pos == std::string::npos)
            return 0;   // 报文不完整
        std::string packlenstr = streamstr.substr(0,pos);
        uint32_t packlenint = std::stoi(packlenstr);    // "40"->40

        // 如果一个报文是完整的,应该是多长
        uint32_t targetlen = packlenstr.size() + packlenint + 2 * gsep.size();

        if(streamstr.size() < targetlen)
            return 0;   // 报文不完整

        // 提取这个完整的jsonstr
        *jsonstr = streamstr.substr(pos + gsep.size(), packlenint);
        // 从0号位置开始移除目标长度的内容
        streamstr.erase(0,targetlen);   // 相当于报文出队列
        return targetlen;
    }

public:
    std::string HandlerRequest(std::string &streamstr,int *code)  // 这里要改一下名字
    {
        LOG(LogLevel::DEBUG) << "Enter HandlerRequest";
        std::string resp_package;
        // 1. 检测报文完整性
        while (true)
        {
            std::string jsonstring;
            int n = UnPackage(streamstr, &jsonstring);
            if (n == 0)
            {
                *code = 0;
                LOG(LogLevel::DEBUG) << "解析完毕";
                return resp_package;
            }
            else if (n == -1)
            {
                *code = -1;
                LOG(LogLevel::DEBUG) << "协议解析失败";
                // exit(1);    // 以前是多进程,解析失败会让进程退出!现在是单Reactor,这里改一下,去掉exit
                // 解析失败,返回一个空串
                return std::string();
            }
            // 我就可以保证,streamstr 一定至少有一个完整的报文!
            LOG(LogLevel::DEBUG) << "request : jsonstring: " << jsonstring;

            // 2. {"datax":10,"datay":20,"oper":43} -> 反序列化
            Request req;
            req.DeSerialize(jsonstring);

            // 3. Request->Response -> 计算过程 --- 属于业务逻辑
            Response resp = _cb(req);   // 业务处理和协议无关,加一个回调

            // 4. 序列化
            std::string respjsonstr;
            resp.Serialize(&respjsonstr);

            LOG(LogLevel::DEBUG) << "response : respjsonstr: " << respjsonstr;

            // 5. 封包
            resp_package += Package(respjsonstr);
            LOG(LogLevel::DEBUG) << "Package :\r\n" << resp_package;
        }
        return resp_package;
    }

private:
    // 类内
    callback_t _cb;
};

3 ~> 日志类

3.1 日志类实现 - Logger.hpp

cpp 复制代码
#ifndef __LOGGER_HPP__
#define __LOGGER_HPP__

#include <iostream>
#include <cstdio>
#include <string>
// #include <filename>
#include <fstream>
#include <ctime>    
#include <filesystem>   // C++17的封装:头文件是filesystem
#include <memory>   // 真正想要的日志类
#include <sstream>  // 设计一个内部类,stringstream的头文件
#include <unistd.h> // getpid要包一下头文件
#include "Mutex.hpp"

namespace LogModule
 {
    // 基础工作1:获取时间戳
    std::string GetTimeStamp()
    {
        time_t timestamp = time(nullptr);
        struct tm data_time;
        localtime_r(&timestamp, &data_time);    // _r:可重入

        char data_time_str[128];    // 时间戳字符串
        // 选择C封装的接口,格式化输出比较容易
        snprintf(data_time_str,sizeof(data_time_str),"%4d-%02d-%02d %02d:%02d:%02d",
            data_time.tm_year + 1900,
            data_time.tm_mon + 1,
            data_time.tm_mday,
            data_time.tm_hour,
            data_time.tm_min,
            data_time.tm_sec
        ); 
        return data_time_str;
    }

    // 基础工作2:日志等级

    // 日志等级v1:整型枚举:int类型
    // v1.1:枚举就用C++的方式,强制性地带上class(把将来的作用域带上)
    enum class LogLevel
    {
        // 日志等级由以下几种元素构成
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL   // 致命的,影响后面的代码跑了,必须debug了
    };

    // // v1.2:枚举类可以重载 << 运算符
    // std::ostream& operator<<(std::ostream& os, LogLevel level)
    // {
    //     os << static_cast<int>(level);
    //     return os;
    // }

    // // v1.3:枚举类可以重载 << 运算符
    // std::ostream& operator<<(std::ostream& os, LogLevel level)
    // {
    //     os << static_cast<int>(level);
    //     return os;
    // }

    // 日志等级v2:日志等级转换成字符串!
    // switch case语句:根据日志等级返回对应的字符串表示
    std::string LogLevelToString(LogLevel level)
    {
        switch(level)
        {
            case LogLevel::DEBUG:
                return "DEBUG";
            case LogLevel::INFO:
                return "INFO";
            case LogLevel::WARNING:
                return "WARNING";
            case LogLevel::ERROR:
                return "ERROR";
            case LogLevel::FATAL:
                return "FATAL";
            default:
                return "UNKNOWN";
        }
    }

    // 基础工作3:日志刷新-->策略模式
    
    // 基类:策略基类,设置刷新策略的
    // ===> 刷新日志 <===
    class LogStrategy
    {
    public:
        // 虚函数
        virtual ~LogStrategy() = default;
        virtual void SyncLog(const std::string &Logmessage) = 0;
    };

    // -----> 策略1:控制台打印 <-----
    class ConsoleLogStrategy : public LogStrategy // (多态)子类:继承纯虚接口类
    {
    public:
    // 构造函数、析构函数
        ConsoleLogStrategy() {}
        ~ConsoleLogStrategy() {}
        void SyncLog(const std::string &Logmessage) override
        {
            // 加锁,锁的保护
            // 消息会出现错乱、交叉(临界资源显示器没有被锁保护)
            LockGuard lockguard(&_mutex);
            std::cout << Logmessage << std::endl;
        }

    private:
        Mutex _mutex;
    };

    // -----> 策略2:文件内打印 <-----
    // 路径代表目录,知道在哪个目录底下
    // 所有的日志默认就在当前目录底下,有一个.log的文件
    // static const std::string glogdir = "./log/";    // 模仿glog
    // static const std::string glogfilename = "log.txt";
    
    // 在 C++ 中,普通的 static const std::string 不能在类体内直接初始化,除非使用 C++17 引入的 inline 关键字
    static inline const std::string glogdir = "./log/";
    static inline const std::string glogfilename = "log.txt";

    class FileLogStrategy : public LogStrategy  // (多态)子类:继承纯虚接口类
    {
    public:
        FileLogStrategy(const std::string &dir = glogdir,const std::string &filename = glogfilename)
        : _logdir(dir),_logfilename(filename)
        {
            // log / log.txt
            // 为了线程安全,往文件里写入也要带上锁
            LockGuard lockguard(&_mutex);   // 保证了原子性
            if(std::filesystem::exists(_logdir))    // exists:判断路径是否存在
            {
                return;
            }
            else
            {
                try
                {
                    // 目录存在返回不存在创建:底层是调的mkdir的系统调用,搞个抛异常,因为可能会出错
                    std::filesystem::create_directories(_logdir);   // 对应一个或多个 mkdir 系统调用
                }
                catch(const std::filesystem::filesystem_error &e)
                {
                    std::cerr << e.what() << "\n";
                }
            }
        }

        ~FileLogStrategy()
        {}

        // 打开一个文件
        void SyncLog(const std::string &Logmessage) override
        {
            // 加锁
            LockGuard lockguard(&_mutex);
            std::string target = _logdir + _logfilename;
            // 日志必须追加写入
            std::ofstream out(target,std::ios::app);   // 追加写入文件
            // 打开文件
            if(!out.is_open())
            {
                return;
            }
            // 在打开和关闭文件之间进行文件写入
            // out.write(Logmessage.c_str(), Logmessage.size());   // write写入
            out << Logmessage << "\n";  // C++流写入
            // 关闭文件
            out.close();
        }
    private:
    // 告诉我指定的文件工作目录是什么?日志文件名是什么?
    // 设置参数来规定
        std::string _logdir;
        std::string _logfilename;   // ./log/XXX.log
        Mutex _mutex;
    };

    // 我们真正想要的日志类:一个日志将来选择哪一种策略?
    class Logger
    {
    public:
        Logger()
        {
            UseConsoleLogStrategy();
        }
        ~Logger()
        {}
        // 智能指针,
        void UseConsoleLogStrategy()    // 显示器策略
        {
            // <RAII(资源获取即初始化)思想>!
            _strategy = std::make_unique<ConsoleLogStrategy>(); // 日志输出到 屏幕/终端
            // 1. 创建 ConsoleLogStrategy 对象
            // 2. 赋值给 _strategy
            // 3. 当 Logger 对象销毁时,_strategy 自动销毁,释放内存

            // std::make_unique<ConsoleLogStrategy>() 做了三件事:
            // 1. 创建对象 :在堆上 new 一个 ConsoleLogStrategy 对象
            // 2. 包装成智能指针 :用 std::unique_ptr 包装这个对象
            // 3. 自动管理内存 :对象生命周期结束时自动 delete,防止内存泄漏
        }
        void UseFileLogStrategy()   // 文件策略
        {
            _strategy = std::make_unique<FileLogStrategy>();    // 日志写入到(路径)./log/log.txt 文件
        } 

        // 设计一个内部类,访问外部属性会快一点
        // ==========> 左半部分 <==========
        // 目标是把一个类对象,变成一个string字符串
        class LogMessage
        {
        public:
        // 当前时间已经有函数可以帮我获取了:基础工作的重要性
            LogMessage(LogLevel level,std::string &filename,int line,Logger&self)
            :_current_time(GetTimeStamp()),
            _level(level),
            _pid(getpid()),
            _filename(filename),
             _line(line),
             _logger(self),  // 由内部类来引用
             _strategy(self._strategy.get())    // 保存当前策略指针
            {
                // stringstream:C++标准库中处理字符串流的类
                std::stringstream ss;
                // 字符串拼接
                ss << "[" << _current_time << "]"
                    << "[" << LogLevelToString(_level) << "]"
                    << "[" << _pid << "]" 
                    << "[" << _filename << "]"
                    << "[" << _line << "]"
                    << "- ";
                _loginfo = ss.str();
            }

            // 仿函数:这个设计非常巧妙,值得学习
            // 它实现了链式调用,让日志打印可以像 std::cout 一样连续使用 <<
            template <typename T>  // 将来怎么调用日志?
            LogMessage &operator<<(const T &info)
            {
                std::stringstream ss;
                ss << info; // // 将任意类型转为字符串
                _loginfo += ss.str();   // 拼接参数到日志内容
                return *this;   // 返回自身引用,支持链式调用
            }

            ~LogMessage()   // RALL风格的日志刷新!
            {
                // v2版本
                if(_strategy)
                {
                    _strategy->SyncLog(_loginfo);  // 使用保存的策略,而不是实时获取
                }

                //  // v1版本
                // if(_logger._strategy)
                // {
                //     // 类内可以通过.来访问类内属性
                //     _logger._strategy->SyncLog(_loginfo);   // 实时获取当前策略 
                //     // 直接可以刷新到显示器或者文件里去了
                // }
            }
        private:
            std::string _current_time;  // 当前时间
            LogLevel _level;    // 日志等级
            pid_t _pid;  // 进程pid
            std::string _filename;  // 输出日志对应的文件名
            int _line;  // 行号
            // std::strinh _logcontent;    // 日志内容
            std::string _loginfo;   // 一条完整的日志

            Logger &_logger; // 外部类的引用
            LogStrategy *_strategy; // 保存创建时的策略指针
        };

        // Logger对象打印日志的时候,故意返回一个临时的LogMessage对象
        // 为什么要返回一个临时的内部类对象?
        LogMessage operator()(LogLevel level,std::string filename,int line)
        {
            return LogMessage(level,filename,line,*this);
        }
    private:
        std::unique_ptr<LogStrategy> _strategy; // 刷新日志的策略
    };

    Logger logger;

// 使用宏,包装我们的日志打印过程,宏有一个特点,#define A B,B替换成为A
// 预定义宏,在编译的时候由预处理器自动替换成对应的信息
#define LOG(level) logger(level,__FILE__,__LINE__)

// ---> 动态调整日志策略 <---
// 全局变量使用控制台的策略
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleLogStrategy()
// 文件版本的策略
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileLogStrategy()

} // namespace LogModule

#endif

3.2 互斥锁 - Mutex.hpp

cpp 复制代码
#ifndef MUTEX_HPP
#define MUTEX_HPP

#include <iostream>
#include <pthread.h>

// 自己实现互斥锁的封装

class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&_lock,nullptr);
    }
    
    void Lock()
    {
        // 手动加锁
        pthread_mutex_lock(&_lock);
    }
    
    pthread_mutex_t *Origin()
    {
        // 返回mutex的原生指针
        return &_lock;
    }

    void Unlock()
    {
        // 解锁
        pthread_mutex_unlock(&_lock);
    }

    ~Mutex()
    {
        // 调用析构
        pthread_mutex_destroy(&_lock);
    }
private:
    pthread_mutex_t _lock;
};

// 锁的开关
class LockGuard // 守卫
{
public:
    LockGuard(Mutex *lockp) : _lockp(lockp)
    {
        _lockp->Lock();
    }
    ~LockGuard()
    {
        _lockp->Unlock();
    }
private:
    Mutex *_lockp;
};

#endif

3.3 套接字 - (模版方法模式,继承 + 多态)Socket.hpp

cpp 复制代码
#ifndef __SOCKRT_HPP
#define __SOCKRT_HPP

#include <iostream>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <memory>
#include "InetAddr.hpp"
#include "Logger.hpp"
// 退出码封装成Common.hpp(公共)类,包含一下
#include "Common.hpp"

static const int gbacklog = 16;

using namespace LogModule;

// 基类
// 把socket创建过层,模版化,方法化 -- 模版方法模式
class Socket
{
public:
    virtual ~Socket() {}
    virtual void CreateSocketOrDie() = 0;
    virtual void BindSocketOrDie(uint16_t port) = 0;
    virtual void ListenSocketOrDie() = 0;
    // virtual std::shared_ptr<Socket> Accepter(InetAddr *clientaddr) = 0;

    // 基类方法那里改成int
    virtual int Accepter(InetAddr *clientaddr, int *errcode) = 0;
    virtual void ConnectOrDie(const std::string &serverip, uint16_t serverport) = 0;
    virtual int Socketfd() = 0;
    virtual void Close() = 0;

    virtual int Recv(std::string *outstr) = 0;
    virtual int Send(const std::string &outstr) = 0;

public:
    void BuildSocketMethod(uint16_t port)
    {
        CreateSocketOrDie();
        BindSocketOrDie(port);
        ListenSocketOrDie();
    }
    void BuildClientSocketMethod(const std::string &serverip, uint16_t serverport)
    {
        CreateSocketOrDie();
        ConnectOrDie(serverip, serverport);
    }
};

class TcpSocket : public Socket
{
public:
    TcpSocket() : _sockfd(-1)
    {
    }
    TcpSocket(int sockfd) : _sockfd(sockfd)
    {
    }

    void CreateSocketOrDie() override
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_ERR);
        }
        // 创建套接字的时候就要把文件描述符设置为非阻塞
        SetNonBlock(_sockfd);
        int opt = 1;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
        LOG(LogLevel::INFO) << "create socket success";
    }
    void BindSocketOrDie(uint16_t port) override
    {
        InetAddr local(port);
        int n = bind(_sockfd, local.Addr(), local.AddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind socket success";
    }
    void ListenSocketOrDie() override
    {
        int n = listen(_sockfd, gbacklog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen socket error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel::INFO) << "listen socket success";
    }
    // std::shared_ptr<Socket> Accepter(InetAddr *clientaddr) override
    // {
    //     struct sockaddr_in peer;
    //     socklen_t len = sizeof(peer);
    //     int sockfd = accept(_sockfd, CONV(&peer), &len);
    //     if (sockfd < 0)
    //     {
    //         return nullptr;
    //     }
    //     *clientaddr = peer;
    //     return std::make_unique<TcpSocket>(sockfd);
    // }

    // 获取新连接改一下
    int Accepter(InetAddr *clientaddr,int *errcode) override
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // 变成非阻塞了
        int sockfd = accept(_sockfd, CONV(&peer), &len);
        // 做一下判断,accept失败,返回值错误码会表明原因
        *errcode = errno; // 把这个暴露出来
        if (sockfd < 0)
        {
            return -1;
        }
        *clientaddr = peer;
        return sockfd;
    }

    int Socketfd() override
    {
        return _sockfd;
    }
    
    void Close() override
    {
        if (_sockfd >= 0)
        {
            close(_sockfd);
            _sockfd = -1;
        }
    }
    int Recv(std::string *outstr) override // 读写的依旧是字符串
    {
        char buffer[1024];
        ssize_t n = recv(_sockfd, buffer, sizeof(buffer) - 1, 0); // bug
        if (n > 0)
        {
            buffer[n] = 0;
            *outstr += buffer; // +=的本质是拼接,入队列,outstr当做一个字节流队列!
            return n;
        }
        else if (n == 0)
        {
            return 0;
        }
        else
        {
            return -1;
        }
    }
    int Send(const std::string &outstr) override
    {
        return send(_sockfd, outstr.c_str(), outstr.size(), 0);
    }
    void ConnectOrDie(const std::string &serverip, uint16_t serverport) override
    {
        InetAddr serveraddr(serverport, serverip);
        int n = connect(_sockfd, serveraddr.Addr(), serveraddr.AddrLen());
        if (n != 0)
        {
            LOG(LogLevel::FATAL) << "connect " << serveraddr.StringAddress() << " failed";
            return;
        }
        LOG(LogLevel::INFO) << "connect " << serveraddr.StringAddress() << " success";
    }

private:
    int _sockfd;
};

//	// UDP
// class UdpSocket : public Socket
// {

// };

4 ~> 客户端地址:网络和本地socket转换的类 - InetAddr.hpp

4.1 InetAddr.hpp

cpp 复制代码
#pragma once

// 网络和本地socket转换的类

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

#define CONV(addr) ((struct sockaddr *)(addr))

class InetAddr
{
public:
    InetAddr()
    {
    }
    // n to h
    InetAddr(struct sockaddr_in &addr) : _net_addr(addr)
    {
        _port = ntohs(_net_addr.sin_port);
        _ip = inet_ntoa(_net_addr.sin_addr);
    }
    InetAddr(uint16_t port, std::string ip = "0.0.0.0")
        : _port(port), _ip(ip)
    {
        _net_addr.sin_family = AF_INET;
        _net_addr.sin_port = htons(_port);
        _net_addr.sin_addr.s_addr = inet_addr(_ip.c_str()); // 等价 INADDR_ANY
    }
    uint16_t Port() { return _port; }
    std::string Ip() { return _ip; }
    struct sockaddr *Addr()
    {
        return CONV(&_net_addr);
    }
    bool operator==(const InetAddr &addr)
    {
        return (_ip == addr._ip) && (_port == addr._port); // ?
    }
    void operator=(const struct sockaddr_in &addr)
    {
        _net_addr = addr;
        _port = ntohs(_net_addr.sin_port);
        _ip = inet_ntoa(_net_addr.sin_addr);
    }
    socklen_t AddrLen()
    {
        return sizeof(_net_addr);
    }
    std::string StringAddress()
    {
        return "[" + _ip + ":" + std::to_string(_port) + "]";
    }
    ~InetAddr()
    {
    }

private:
    // 本地地址
    uint16_t _port;
    std::string _ip;
    // 网络地址
    struct sockaddr_in _net_addr;
};

5 ~> 多路转接版块:单Reactor模式(epoll方式)

5.1 "先描述"

5.1.1 基类 - Connection.hpp

cpp 复制代码
#pragma once

// 每一个fd,后续都对应一个connection连接 -- 针对套接字的二次封装
#include <iostream>
#include <string>
#include "InetAddr.hpp"

// 前置声明
class Reactor;

// "先描述" - 基类
class Connection
{
public:
    Connection() : _events(0),_R(nullptr)
    {}

    // 有了Events和Sockfd,任何一个连接就可以获取对应的套接字和事件了
    uint32_t Events()
    {
        return _events;
    }
    // 设置标志位
    void SetEvents(uint32_t events) // 光这样还不够。新的连接要关心什么事件?
    {
        _events = events;
    }
    // 客户端地址
    void SetAddress(const InetAddr &addr)
    {
        _clientaddr = addr; 
    }
    virtual int Sockfd() = 0;
    virtual void Recver() = 0;
    virtual void Sender() = 0;
    virtual void Excepter() = 0;
    virtual void Close() = 0;
    // 更新最近活跃时间
    void Update()
    {
        _last_time_stamp = CurrentTimeStamp();
        // 并且在Recver和Sender开头都更新一下时间戳
    }

    ~Connection(){}
protected:
    InetAddr _clientaddr;   // client socket套接字对应客户端地址
    uint32_t _events;   // Connection关心什么事件

    // 怎么知道当前连接是活跃的还是不活跃的?不活跃的怎么处理呢?
    // 于是我们就可以在我们的Connection中去定义一个给所有连接维护的一个时间戳
    uint64_t _last_time_stamp;   // 设置最近活跃时间戳

    // TODO -- 除了这些,未来还需要什么再继续往下加
public:
    Reactor *_R;    // 回指指针
};

5.1.2 子类

5.1.2.1 子类:连接管理器(虽然这么叫,但是特别像当年的TcpServer.hpp)- Listener.hpp
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <memory>
#include <sys/epoll.h>
#include "Logger.hpp"
#include "Connection.hpp"
#include "Socket.hpp" // 作为一个Listener,必须包含Socket.hpp模块
#include "IOHandler.hpp"    // IOHandler.hpp模块

// ===========> 连接管理器(虽然这么叫,但是特别像当年的TcpServer.hpp)- 先描述 <============
// Listener模块是一个特殊的connection,也是connection的派生类

class Listener : public Connection
{
public:
    Listener(uint16_t port,OnMessage_t on_message)
    : _port(port),
      _on_message(on_message),
      _listensock(std::make_unique<TcpSocket>())
    {
        _listensock->BuildSocketMethod(_port);
        // 打一个日志,保证未来报文的完整性的判断处理
        LOG(LogLevel::INFO) << "Listen sockfd create success: " << _listensock->Socketfd();
    }

    int Sockfd() override
    {
        return _listensock->Socketfd();
    }

    // 关闭套接字的接口 - Listener.hpp实现基类中的纯虚函数
    void Close() override
    {
        _listensock->Close();
    }

    void Recver() override
    {
        // _listensock->Accepter(); // --> 未来,Listener的Recver就是底层调用一下Accepter,但是其它的套接字调用就是调用Recver
        // 连接到来,事件派发到Linstener模块的Recver里面
        while(true)
        {
            int errcode = 0;
            InetAddr clientaddr;
            int sockfd = _listensock->Accepter(&clientaddr,&errcode);    // 调用Accepter,不断获取新连接,带出错误码
            if(sockfd >= 0)
            {
                // success
                LOG(LogLevel::INFO) << "get a new sockfd success: " << sockfd << "client addr: " << clientaddr.StringAddress();
                // 获取连接成功了怎么办?可以直接recv吗?recv(sockfd)?
                // 肯定不可以!能的话我也不用做那么多的封装了,缓冲区放函数里面万一释放了怎么办?
                // a.sockfd包装成为Connection --> IOHandler
                SetNonBlock(sockfd);    // 1.设置非阻塞
                std::shared_ptr<Connection> conn = std::make_shared<IOHandler>(sockfd,_on_message);   // 2.sockfd包装成为Connection
                // 具体类型是IOHandler
                // 光这样还不够。新的连接要关心什么事件?
                // 1、对于普通套接字,刚开始的时候需要关心下面的事件
                conn->SetEvents(EPOLLIN | EPOLLET); // 3.设置sockfd关心的事件,整个代码都要以ET模式工作
                // 客户端地址传进来
                conn->SetAddress(clientaddr);
                // b.sockfd上面的事件就绪,我怎么知道的?epoll->Reactor!新的连接反向插入到Reactor中,托管给epoll
                _R->AddConnection(conn);
            }
            else
            {
                // 不一定是真的失败,也可能是对方底层没有新连接了!
                // 失败的时候我最关心的是错误码errno是多少
                if(errcode == EAGAIN || errcode == EWOULDBLOCK)
                {
                    LOG(LogLevel::INFO) << "accepter Finish!";
                    break;
                }
                else if(errcode == EINTR)
                {
                    // 可能会被信号阻塞
                    LOG(LogLevel::INFO) << "accepter interupt!";
                    continue;
                }
                else
                {
                    // 这一次才是真的出错了! 
                    LOG(LogLevel::INFO) << "accepter interupt!";
                    break;
                }
                // LOG(LogLevel::ERROR) << "accepter error,errno : " << errcode;
                // break;  // 因为是循环
            }
        }

        // LOG(LogLevel::DEBUG) << "event ready,Listener : Recver";
    }

    void Sender() override
    {
        // 忽略
    }

    void Excepter() override
    {
        // 忽略
    }

    // 析构这里不写了
private:
    uint16_t _port;
    // 只有在Listener里面获取连接的时候会构造IOHandler,Listener模块里也设置一个OnMesage
    OnMessage_t _on_message;    // OnMesage需要由IOHandler来构建,IOHandler需要由Listener来设置
    std::unique_ptr<Socket> _listensock;
};
5.1.2.2 子类:IO处理器(只负责读取和发送)- IOHandler.hpp
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include "Connection.hpp"
#include "Logger.hpp"

using namespace LogModule;

// 定义一个临时缓冲区buffer,大小自拟
static const int gbuffersize = 1024;

// 出错了就给我出错码,我需要一个函数 -- 需要传引用和code(当成输出型参数)
using OnMessage_t = std::function<std::string (std::string &,int *code)>;   // 定义了一个回调类型

// =========> IO处理器 <=========
// 先描述
// 只负责读取和发送
class IOHandler : public Connection
{
public:
    IOHandler(int sockfd,OnMessage_t on_message)
    : _sockfd(sockfd),
      _on_message(on_message)
    {}

    int Sockfd() override
    {
        return _sockfd;
    }

    // 关闭套接字的接口 - IOHandler.hpp实现基类中的纯虚函数
    void Close() override
    {
        close(_sockfd);
    }

    void Recver() override
    {
        // 更新一下时间戳
        Update();

        // sleep(100);  // 这里的sleep先去掉
        // 打一个日志,保证未来报文的完整性的判断处理
        LOG(LogLevel::INFO) << "event ready, sockfd is: " << _sockfd;
        // 读取 --> 你必须循环式的把本次数据全部读取完毕!
        char buffer[gbuffersize];
        while(true)
        {
            ssize_t n = recv(_sockfd,buffer,gbuffersize - 1,0);
            if(n > 0)
            {
                // 只要读我就加,报文完整不完整跟IOHandler没关系,IOHandler只负责读取
                buffer[n] = 0;
                _inbuffer += buffer;    // +=本质是在入队列
            }
            else if(n == 0)
            {
                LOG(LogLevel::INFO) << "client quit,address is : " << _clientaddr.StringAddress() << " sockfd: " << _sockfd;
                // 读取出错了,我不管 -- 不管是什么意思?统一交给异常处理函数来处理
                Excepter();
                return; // 注意!return,函数调用结束
            }
            else
            {
                // 读取出错了 -- 套接字已经被设置为非阻塞了,
                // IO处理器当中进行IO处理时,不一定是出错了,也有可能是recv告诉我当前底层没有数据了!
                // 不一定是出错了!
                if(errno == EAGAIN || errno == EWOULDBLOCK)
                {
                    // 证明底层没数据了,本轮读取完了
                    break;
                }
                else if(errno == EINTR)
                {
                    // 读取的时候没有读上来
                    continue;
                }
                else
                {
                    // 再else才算是真正的读取错误
                    LOG(LogLevel::ERROR) << "recv error,address is : " << _clientaddr.StringAddress() << " sockfd: " << _sockfd;
                    // 读取出错了,我不管 -- 不管是什么意思?统一交给异常处理函数来处理
                    Excepter();
                    return; // 注意!出错了都是return,函数调用结束
                }
            }
        }
        // 尝试检查 -> 处理,(收到的数据,是否有至少一个完整的报文??如果有,你要提取,然后交给上层进行反序列化处理!)
        //  这些工作,不应该让IOHandler来做,而应该结合协议来做!!!如果没有,什么都不做!!
        // 意思是这样的,如果数据读上来了:
        // 1. 接下来就要在recv里面尝试检查收到的数据,至少有一个完整的报文;
        // 2. 如果有,你要提取,然后交给上层进行反序列化处理!
        // 3. 如果没有,就什么都不做!
        
        // 网络版本计算器!
        
        // 如果出错了
        int code = 0;
        std::string result = _on_message(_inbuffer,&code);  // OnMessage_t 定义的第二个参数是 int* ,这里传的是 int
        if(code == 0)
        {
            _outbuffer += result;   // 如果解析没有出错,就拼接到outbuffer里面:result: string(),result: response
        }
        else
        {
            // code = -1,直接走异常处理
            Excepter();
            return;
        }
        // 目前可公开情报:连接关了,会异常处理

        // ?如果出异常就不会走到下面了,那么走到下面就证明读完了,但是不一定能够处理一个完整的请求

        // 发送思路
        // 未来如何正确发送?
        // 1. 直接发送(默认情况下就绪),发送完了就直接结束;
        // 2. 直接发送,发送条件不满足了(发送缓冲区满了),这个时候就要想办法把sockfd对应的EPOLLOUT事件设置到epoll中!

        // // Version1:直接发
        // if(!_outbuffer.empty())
        //     Sender();

        // Version2:使能写事件,发数据自动化
        if(!_outbuffer.empty())
            _R->EnableReadWrite(_sockfd,true,true); // 使能写事件关心!

        // // 走到下面就是读取到了
        // std::cout << "inbuffer:\n" << _inbuffer;    // 一个套接字一个缓冲区,数据都会临时保存在inbuffer里面

        // // 5号连接会被路由到哪里?
        // LOG(LogLevel::DEBUG) << "haha,Recver event,IOHandler,sockfd: " << _sockfd;
    }

    void Sender() override
    {
        // 更新一下时间戳
        Update();

        sleep(100);
        // 这里有一个困惑,之前我们事件就绪派发(Reactor.hpp)的时候不是调用了写的方法嘛,怎么这里又调用了
        // 原因:套接字本身是阻塞的,发送的方式是ET模式,我需要一直发送
        while(true)
        {
            // 返回值n(决定真实发送多少)代表实际发送多少个字节
            ssize_t n = send(_sockfd,_outbuffer.c_str(),_outbuffer.size(),0);   // send的本质是拷贝函数
            if(n >= 0)
            {
                // 把已经发出去的做移除,移除了之后可以下次再发
                _outbuffer.erase(0,n);
                // 这里严格一点,做一个判断
                if(_outbuffer.empty())
                {
                    break;
                }
            }
            else
            {
                // 再else就是发送报错,真的是这样吗?
                // 有没有可能是进行发送时,因为流量控制的原因,内核没有写入到网络里面,导致发送条件不满足了?
                // 因为是非阻塞,它会告诉我
                // 是真的报错了吗?不一定!
                if(errno == EAGAIN || errno == EWOULDBLOCK)
                {
                    // 本轮数据发送完毕了! -- 只能证明本轮发完了,outbuffer里面还有数据,此时就break
                    break;
                }
                else if(errno == EINTR)
                {
                    continue;
                }
                else
                {
                    // 这才是真正的发送失败,我直接return,return之前先Excepter -- 统一异常处理段,读写出错全部走到Excepter
                    // 发送失败
                    LOG(LogLevel::ERROR) << "send error.address is: " << _clientaddr.StringAddress() << " sockfd: " << _sockfd;
                    Excepter();
                    return;
                }
            }
        }

        // 走到下面就是一定没有错误
        // 1. 刚好发送完毕 
        // 2. 没发完 -- 发送条件不满足了
        // --- 一直关心读,读是常设的,设置为true,写事件我们就根据这里的不同来设置 ---
        // 发完没发完由outbuffer是否为空来判定
        if(_outbuffer.empty())
            _R->EnableReadWrite(_sockfd,true,false);
        else    // 如果缓冲区没有发完说明最后发送条件不满足了 -- 没发完?发送条件不满足了!对conn,修改sockfd关心的事件,epoll->EPOLLOUT
            _R->EnableReadWrite(_sockfd,true,true); // 基类有回调指针,直接使用
    }

    void Excepter() override
    {
        // 统一异常处理段,读写出错全部走到Excepter
        // 直接打一个日志
        LOG(LogLevel::ERROR) << "Excepter,address is : " << _clientaddr.StringAddress() << " sockfd: " << _sockfd;
        // 异常处理时其实什么都不用做,只需要这样
        _R->DelConnection(_sockfd);
    }

    ~IOHandler()
    {}

private:
    int _sockfd;
    OnMessage_t _on_message;    // 需要一个OnMessage_t
    std::string _inbuffer;  // 接收缓冲区
    std::string _outbuffer; // 发送缓冲区
};

5.2 "再组织"

5.2.1 多路转接模块 - Poller.hpp(epoll实现的,帮我监听所有的fd是否就绪)

cpp 复制代码
#pragma once

// 专门进行事件管理的epoll模型
// Poller.hpp将来帮助我们监听所有的fd是否就绪!

#include <iostream>
#include <string>
#include <sys/epoll.h>
// exit的头文件
#include <cstdlib>
// 退出码封装成Common.hpp(公共)类,包含一下
#include "Common.hpp"
#include "Logger.hpp"

// // 封装一下IN(读)事件、OUT(写)事件,对事件做一下二次包装
// // 这样添加事件的时候就不用管什么EPOLLIN、EPOLLOUT什么的了,今天不做这个了,我直接把EPOLLIN、EPOLLOUT暴露出去
// #define IN EPOLLIN
// #define IN EPOLLOUT

static const int gsize = 128;

using namespace LogModule;

class Poller
{
// 再添加一个函数
private:
    // ADD(EPOLL_CTL_ADD):添加一个新的文件描述符到 epoll 监听集中
    // MOD(EPOLL_CTL_MOD):更改已存在的文件描述符所关注的事件类型(例如从只读改为可读可写)
    // DEL(EPOLL_CTL_DEL):从 epoll 监听集中删除该文件描述符
    int EpollCtlHelper(int sockfd,uint32_t events,int oper)  // 这个oper就是EPOLL_CTL_ADD这种
    {
        // 刚才说的关闭情况下的修改也加上了
        if(oper == EPOLL_CTL_DEL)
        {
            return epoll_ctl(_epfd,oper,sockfd,nullptr);    // 关心事件设置为0或者nullptr即可
        }

        epoll_event ev;
        ev.events = events;
        ev.data.fd = sockfd;
        return epoll_ctl(_epfd,oper,sockfd,&ev);
    }

public:
    Poller()
    {
        _epfd = epoll_create(gsize);
        // epoll创建失败,不用玩了
        if(_epfd < 0)
        {
            LOG(LogLevel::FATAL) << "epoll_create error!";
            exit(EPOLL_ERR);
        }
        LOG(LogLevel::INFO) << "epoll_create success: " << _epfd;
    }

    // 实现delete事件
    void DelEvents(int sockfd)
    {
        EpollCtlHelper(sockfd,0,EPOLL_CTL_DEL);
    }

    // Poller也需要给提供一个接口:添加事件的接口
    void AddEvents(int sockfd,uint32_t events)
    {
        // 对sockfd添加关心的事件
        int n = EpollCtlHelper(sockfd, events,EPOLL_CTL_ADD);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "AddEvents error!";
        }
    }

    void ModEvents(int sockfd,uint32_t events)
    {
        // 更改sockfd关心的事件 -- 更改已存在的文件描述符所关注的事件类型
        int n = EpollCtlHelper(sockfd,events,EPOLL_CTL_ADD);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "ModEvents error!";
        }
    }

    // 就绪事件
    int WaitEvents(struct epoll_event revs[],int num,int timeout)   // num是数组缓冲区大小
    {
        int n = epoll_wait(_epfd,revs,num,timeout);
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "epoll_wait error!";
        }
        return n;
    }

    ~Poller()
    {

    }

private:
    int _epfd;
};

// // 这个工作做了的话,代码量就更夸张了,感兴趣让AI写一下
// class Poller
// {
// public: 
//     // 创建模型、删除模型、获取就绪事件、设置对应的就绪事件
//     virtual bool Create() = 0;
//     virtual bool Destroy() = 0;
//     virtual void GetEvents() = 0;
//     virtual void SetEvents() = 0;
// };

// // 三种多路转接方法都实现,想用哪种就用哪种
// class SelectPoller : Poller
// {

// };

// class PollPoller : Poller
// {

// };

// class EpollPoller : Poller
// {

// };

5.2.2 单Reacror模式 - Reactor.hpp(基于事件驱动(Reactor模式)的高并发网络服务器底层框架,用于监听网络连接、读写IO事件并分发处理业务逻辑)

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <memory>
#include <unordered_map>    // 要存在映射
#include "Logger.hpp"
#include "Connection.hpp"
#include "Poller.hpp"

// 已经就绪的事件清单
static const int gnum = 128;

class Reactor
{
private:
    bool IsConnectionExists(int sockfd) // 代码还是有问题,根据鲁棒性,需要一个bool类型的判断连接是否存在于容器中的接口,通过文件描述符 
    {
        return _connections.find(sockfd) != _connections.end();
    }

public:
    Reactor() : _epoller(std::make_unique<Poller>())  // 调用Poller,epoller就有了
    {}

    // AddConnection -- 新增Connection的接口!
    void AddConnection(std::shared_ptr<Connection> &conn)
    {
        // 1.把conn->sockfd && event(文件描述符和事件)写透到内核
        int sockfd = conn->Sockfd();
        uint32_t events = conn->Events();
        _epoller->AddEvents(sockfd,events); // 文件描述符和事件

        // 2.conn托管给_connections
        _connections[sockfd] = conn;

        // 3.conn回指Reactor
        conn->_R = this;    // 指向当前对象

        // 输出一下:新连接就被添加进去了
        LOG(LogLevel::INFO) << "insert: " << conn->Sockfd() << " into reactor!";
    }

    // void EnableReadWrite(int sockfd,bool isread,bool iswrite)
    // 使能读和写的接口设计,这里改一下参数名字
    void EnableReadWrite(int sockfd,bool enableread,bool enablewrite)
    {
        // 不存在就结束,存在就修改对事件的关心
        if(!IsConnectionExists(sockfd))
            return;
        // 首先,先检测连接是否存在,这里有鲁棒性问题
        // 对读事件关心使能或者对写事件关心使能?
        // 通过位运算,提取出来怎么关心读写事件
        uint32_t events = ((enableread ? EPOLLIN : 0) | (enablewrite ? EPOLLOUT : 0) | EPOLLET);    // 0跟任何数字按位与都是0本身
        _connections[sockfd]->SetEvents(events);
        // 写透到内核
        _epoller->ModEvents(sockfd,events);
    }

    // 新增一个生成连接的接口
    void DelConnection(int sockfd)
    {
        if(!IsConnectionExists(sockfd))
            return;

        // 新增函数就是这个,问题出在日志上 -- 当时没有处理std::endl,把std::endl去掉就行
        LOG(LogLevel::INFO) << "delete sockfd: " << sockfd;
        // LOG(LogLevel::INFO) << "delete sockfd: " << sockfd << std::endl;
        // 1.从epoll中移除
        _epoller->DelEvents(sockfd);

        // 2.关闭fd
        _connections[sockfd]->Close();
        // // 如果不想这样写,就在基类封装一个纯虚接口,然后在子类中分别实现,如上所示
        // --- 一种写法 ---
        // close(sockfd);

        // 3.从_connections中移除
        _connections.erase(sockfd);
    }

    void LoopOnce(int timeout)
    {
        // 事件派发的时候每次LopOnce都会设置一个timout
        int n = _epoller->WaitEvents(revs,gnum,timeout); // 捞取到就绪事件
        for(int i = 0;i < n;i++)
        {
            // 哪一个fd
            int sockfd = revs[i].data.fd;
            // 就绪事件是谁
            uint32_t revents = revs[i].events;
            // ============> 采用多态属性屏蔽了的底层差异 <===========
            // 变成这样的逻辑:只要是异常,不管是什么异常,直接:
            if((revents & EPOLLERR) || (revents & EPOLLHUP))
                revents = (EPOLLIN | EPOLLOUT); // 把错误转换成读写 --> 即便读写事件没就绪,也手动设置成就绪,将来IOHandler读的时候必然出错
            // 出错了我只需要盯着Excepter一个函数就可以了,这就叫做统一异常处理!
            if((revents & EPOLLIN) && IsConnectionExists(sockfd))
                _connections[sockfd]->Recver();
            if((revents & EPOLLOUT) && IsConnectionExists(sockfd))
                 _connections[sockfd]->Sender();
        }
    }

    // 这里的DebugPrint太碍眼了,放到后面去

    // 让TcpServer提供一个派发接口
    void Dispatcher()
    {
        // 我可不可以获取最近不活跃的连接的时间戳,或者干脆这样,简单一点:
        // 每隔一秒,事件就绪完;非阻塞轮询,可以做其它事情,比如连接保活机制!
        int timeout = 1000;
        while(true) // 死循环
        {
            // DebugPrint();   // DebugPrint一下,打一下文件描述符清单
            LoopOnce(timeout);  // 重写事件派发
            // 连接保活机制!
            ConnectionKeepAlive();
        }

        // // 设置为0,非阻塞轮询检测
        // // int timeout = 0;
        // // 改成-1,有任务来就处理,没有就在epoll阻塞等待
        // int timeout = -1;    // 为什么事件派发的时候要这么做(还有一个timeout)?

        // // 死循环
        // while(true)
        // {
        //     // DebugPrint一下,打一下文件描述符清单
        //     DebugPrint();
        //     // 重写事件派发
        //     LoopOnce(timeout);
        // }
    }

    // connections里面的文件描述符越来越多,我可以遍历一下connections
    void DebugPrint()
    {
        std::cout << "Reactor sockfd list: ";
        for(auto &conn : _connections)
        {
            std::cout << conn.second->Sockfd() << " ";
        }
        std::cout << std::endl;
    }

    ~Reactor()
    {

    }
private:
    // 1.必须得有一个epoller模型
    std::unique_ptr<Poller> _epoller;   // 我们从epoll这里拿到的之后,哪一个fd的哪些事件就绪!
    // 2.组织所有的connection!
    std::unordered_map<int,std::shared_ptr<Connection>> _connections;   // 服务器内所有的Connection
    
    // // 连接保活机制!-- ConnectionKeepAlive(); -- 不实现了,提供一下思路
    // // 遍历unordered_map,淘汰时间戳(递增数据)最小的,走delete逻辑。connection是一个个的指针,可以搞一个最小堆
    // Heap<std::shared_ptr<Connection>> Heap; // 这个代码我们具体就不写了,再写真成项目了
    // // 每一个连接既属于unordered_map又属于最小堆,要关闭连接直接从堆顶开始 -- 这样就可以做管理了

    // 3.已经就绪的事件清单
    struct epoll_event revs[gnum];
};

6 ~> 公共类 - Common.hpp

6.1 Common.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <sys/epoll.h>
#include <fcntl.h>  // 为了把一个连接设置成非阻塞
#include "Logger.hpp"   // 日志比Common.hpp更靠近上层,还是要包含头文件

// 获取最近活跃时间戳需要的头文件
// #include <fcntl.h>      // 提供 fcntl() 函数、F_GETFL、F_SETFL、O_NONBLOCK -- 上面已经包含了,这里就看看这个函数的功能
#include <time.h>       // 提供 time() 函数(C++ 中也可用 <ctime>)
#include <stdint.h>     // 提供 uint64_t 类型(C++ 中也可用 <cstdint>)
#include <unistd.h>     // (建议)提供文件描述符类型 fd 及相关宏,Linux 下 fcntl 常与之配套

using namespace LogModule;

// 错误码放这里,统一进行管理
enum
{
    SOCKET_ERR = 1,
    BIND_ERR,
    LISTEN_ERR,
    EPOLL_ERR
};

// 文件描述符设置为非阻塞的接口
void SetNonBlock(int fd)
{
    int flags = fcntl(fd,F_GETFL);
    if(flags < 0)
    {
        LOG(LogLevel::ERROR) << "fcntl error set: " << fd << " non block failed";
        return;
    }
    fcntl(fd,F_SETFL,flags | O_NONBLOCK);
}

// 当前就有了一个获取时间戳的函数
uint64_t CurrentTimeStamp()
{
    return (uint64_t)time(nullptr);
}

7 ~> 主函数 - Main.cc

7.1 Main.cc

cpp 复制代码
// 网络版本计算器
#include "Calculator.hpp"
#include "Protocol.hpp"

#include "Connection.hpp"
// 头文件包含顺序需要调整 --> 让编译器先看到Reactor,再看到Listener
#include "Reactor.hpp"
#include "Listener.hpp"
#include <memory>

const uint16_t gport = 8080;

int main(int argc, char *argv[])
{
    // 开启日志:选择控制台
    ENABLE_CONSOLE_LOG_STRATEGY();
    // 1.定义网络版本计算器对象
    std::unique_ptr<Calculator> cal = std::make_unique<Calculator>();

    // 2.定义一个带参的协议对象
    std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>(
        [&cal](const Request &req)->Response{
            // 网络版本计算器里面的方法Exec
            return cal->Exec(req);
        }
    );

    // 3.listener
    std::shared_ptr<Connection> conn = std::make_shared<Listener>(gport,
        [&protocol](std::string &inbuffer,int *code)->std::string{
            return protocol->HandlerRequest(inbuffer,code);
        }
    );
    // 标志位设置进来了,设置了ET模式
    conn->SetEvents(EPOLLIN | EPOLLET); // 设置关心的事件 -- ET模式

    // 4.Reactor -- 有了Listener,还要先描述再组织,管理起来
    std::unique_ptr<Reactor> R = std::make_unique<Reactor>();

    // 5.listener -> Reactor -- - 把Listener对象添加到Reactor 
    R->AddConnection(conn);

    // 6.开始事件派发 -- 怎么添加进去的?套接字、事件拿了,写透到内核了,未来容器里面就有了第一个成员,接下来,服务器就要开始事件派发了
    R->Dispatcher();

    return 0;
} 

8 ~> 编译、链接、管理依赖关系 - Makefile

8.1 Makefile的作用

编译 :将所有的 .cc 源文件编译成目标文件(.o)。

链接 :将目标文件链接成最终的可执行程序(很可能是两个目标:服务端 Main 和客户端 OnlineCalClient)。

管理依赖:处理头文件(.hpp)变更后需要重新编译的规则。

8.2 Makefile

bash 复制代码
Reactor_server:Main.cc
	g++ -o $@ $^ -std=c++17 -ljsoncpp
.PHONY:clean
clean:
	rm -f Reactor_server

8.3 Makefile在"基于单Reactor实现的网络版本计算器"中的作用

只编译 Main.cc 这一个源文件链接 jsoncpp 生成服务端可执行文件 Reactor_server并提供 make clean 删除它


9 ~> 运行演示

9.1 备注

由于艾莉丝没有完整实现最近活跃时间戳、更新时间戳和连接保活机制这三个逻辑,所以编译是报错的,其它模块都顺利编过了。

这个代码因为做了较为彻底的解耦合,好处是方便后续的各种改造,也不能说是坏处只能说很容易翻车,毕竟代码联动性很强。

一个Reactor到底要添加哪些文件描述符跟怎么设计有关系,所以必须要把这些东西全部解耦合。因此我就可以知道之前实现的Reactor解耦的这么彻底就是为了方便后续的这种改造。

9.2 演示

编译并没有完全通过哈,不过没关系,核心的都实现并且编译通过了。


结尾

uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!" "技术之路难免有困惑,但同行的人会让前进更有方向。" |

结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!

往期回顾

【Linux网络】多路转接epoll(五)Reactor模式:基于epoll的高性能网络服务器设计与实现(下)剩余细节补充 + 多进程多线程实现Reactor要点

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა