
🎬 个人主页 :艾莉丝努力练剑
❄专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》
《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》
⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平
🎬 艾莉丝的简介:

文章目录
- [0 ~> 项目目录](#0 ~> 项目目录)
- [1 ~> 推陈出新:网络版本计算器(客户端)- OnlineCalClient.cc](#1 ~> 推陈出新:网络版本计算器(客户端)- OnlineCalClient.cc)
-
- [1.1 OnlineCalClient.cc](#1.1 OnlineCalClient.cc)
- [1.2 Calculator.hpp](#1.2 Calculator.hpp)
- [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(×tamp, &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要点
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა
