Linux高级IO(六)基于ET模式、单reactor反应堆的epoll版本的TCP计算服务器

目录

Epoller.hpp

头文件说明

类私有成员解析

构造函数与析构函数

对外public接口

总结:

Reactor.hpp

头文件说明

​编辑

类私有内容解析

构造函数与析构函数

公有public成员函数

总结:

Connection.hpp

头文件说明

类私有内容解析

公有public成员函数

相关问题

总结

Listener.hpp

头文件说明

类私有成员解析

构造函数与析构函数

共有public成员函数

相关问题

总结:

IOManager.hpp

头文件说明

类私有成员解析

构造函数与析构函数

共有public成员函数

相关问题

总结

Protocol.hpp

头文件说明

[Protocol 协议类](#Protocol 协议类)

相关问题

总结

Calculator.hpp

Main.cc

头文件说明

[1. 数据计算业务模块](#1. 数据计算业务模块)

[2. Protocol 协议层模块](#2. Protocol 协议层模块)

[3. 实例化 Listener 监听连接对象](#3. 实例化 Listener 监听连接对象)

[4. 创建 reactor 反应堆](#4. 创建 reactor 反应堆)

模型总结

运行结果:

最终运行结果:

完整代码:

Epoller.hpp

Reactor.hpp

Connection.hpp

Listener.hpp

IOManager.hpp

Protocol.hpp

Calculator.hpp

Main.cc

Comm.hpp

InetAddr.hpp

Logger.hpp

Mutex.hpp

Socket.hpp

Makefile


本文我们将编写一个单 Reactor、ET 边缘触发、非阻塞 IO、自定义 JSON 分包协议的 TCP 计算器服务 ,其中包含了我们上一篇文章中讲的关于 epoll ET 模式、TCP 粘包拆包私有协议、同时兼顾读写事件、Reactor 事件分发模型。最终的代码我们能划分为系统底层、网络底层、事件调度层、连接管理层、协议解析层、业务层六层结构,自上而下解耦,实现工程化的分层设计。整套服务从 epoll 底层系统调用到最终业务运算都做了完整封装,我们会把不同职责的逻辑拆分、封装为独立类并存放至对应的头文件中,模块之间依靠对外接口交互,不会出现跨层耦合的问题。

为了搭建起完整的事件驱动网络服务,我们需要依次实现 epoll 内核操作类、Reactor 事件调度器类、连接抽象基类、监听套接字实现类、客户端数据连接实现类、自定义通信协议类、计算器业务类,最后提供程序入口文件串联全部组件,对应拆分为 Epoller.hpp、Reactor.hpp、Connection.hpp、Listener.hpp、IOManager.hpp、Protocol.hpp、Calculator.hpp、Main.cc 这几份核心代码文件。其中 Epoller.hpp 负责封装所有原生 epoll 系统调用,作为整个框架最底层的事件内核支撑;Reactor.hpp 依托 Epoller 实现全局事件循环,统一管理所有连接并分发读写、异常 IO 事件;Connection.hpp 定义所有套接字连接的通用抽象规范,提供读写异常的虚接口;Listener.hpp 继承连接基类,专门处理监听 fd 的读事件,接收新客户端并创建对应数据连接;IOManager.hpp 同样继承连接基类,是 ET 模式核心实现载体,完成非阻塞循环读写、收发缓冲区管理,调用协议模块处理字节流数据;Protocol.hpp 实现 JSON 报文的封包与拆包逻辑,解决 TCP 流式传输带来的粘包、半包问题;Calculator.hpp 是纯业务模块,仅负责四则运算,和网络事件逻辑完全隔离;最后的 Main.cc 作为程序入口,实例化上述所有类、绑定业务回调、注册监听连接并启动 Reactor 事件循环,完整打通服务从启动、接收连接、处理网络数据、执行业务运算到回写响应的全流程。接下来我们就按照底层支撑上层、前置文件不依赖后置文件的顺序,逐个文件拆解代码实现。


  • Epoller.hpp 仅封装原生 epoll 系统调用,无任何其他自定义类依赖,是整个事件框架最底层基础,所有上层调度逻辑都建立在它之上。
  • Reactor.hpp 内部只依赖 Epoller,用来实现事件循环、事件分发、连接容器管理,不依赖连接的具体实现类,只依赖连接基类的通用接口定义。
  • Connection.hpp 连接抽象基类,只定义读写异常虚接口、收发缓冲区、基础成员变量,不依赖 Reactor 以外的文件,Listener、IOManager 都是它的子类,必须先讲基类。
  • Listener.hpp 继承 Connection 基类,依赖 Reactor 完成新连接注册,逻辑仅负责监听套接字 accept,不会用到客户端数据读写、协议相关逻辑,无后置文件依赖。
  • IOManager.hpp 同样继承 Connection 基类,依赖 Reactor,会调用 Protocol 做数据解析封包,所以必须把 Protocol 放在它之后。这里存放 ET 模式循环读写核心代码,是网络数据处理的载体。
  • Protocol.hpp 提供粘包拆包、报文序列化反序列化工具,IOManager 读取到字节流后调用它解析,仅提供工具方法,不依赖任何业务类。
  • Calculator.hpp 纯粹业务计算模块,协议解析出请求数据后交给它运算,仅被上层入口调用,和网络、事件框架完全无关。
  • Main.cc 程序总入口,会实例化前面全部类,串联所有模块启动服务,是最后讲解的文件。

Epoller.hpp

Epoller.hpp 是整个事件驱动框架最底层的基石模块,所有上层 Reactor 的事件就绪、fd 监听事件全部依赖这个类封装的 epoll 系统调用实现。我们先从代码本身的结构、依赖头文件入手,再逐层拆解类的私有成员、构造析构、对外接口与内部私有方法。

头文件说明

头文件采用标准头文件保护宏,能够避免头文件被重复包含引发的编译报错,是 C++ 工程头文件的通用规范。文件引入的头文件分为系统标准头文件项目内部工具头文件两类。系统头文件中,<iostream> 用于基础控制台输出,<cstdlib> 提供程序退出相关函数,<sys/epoll.h> 是 Linux 系统 epoll 模型的核心头文件,epoll_create、epoll_ctl、epoll_wait、epoll_event 等所有 epoll 原生类型与系统调用都定义在此处,是这个文件功能实现的根本;项目内部头文件包含 Comm.hpp 和Logger.hpp,Comm.hpp 定义了程序各类错误退出码枚举,用于 epoll 创建失败时的异常退出,Logger.hpp 是我们自研的日志工具,整个类中所有运行信息、致命错误都会通过日志模块打印,方便线上调试与问题排查。

Epoller.hpp 文件定义唯一的 Epoller 类,该类的核心设计目标是封装 Linux 原生 epoll 的全套系统调用,屏蔽底层系统调用繁杂的参数、结构体填充、返回值校验逻辑,对外仅提供简单的业务接口。上层代码无需手动创建 epoll_event 结构体、不用记忆 epoll_ctl 的三种操作宏,只需要调用 AddEvents、ModEvents、DelEvents 就能完成 fd 监听事件的增、改、删,调用 WaitEvents 即可阻塞等待就绪 IO 事件,把底层内核交互逻辑全部收敛在这一个类内部,降低上层 Reactor 模块的代码复杂度,同时统一管控 epoll 实例的生命周期。

类私有成员解析

我们先看类的私有部分,分为私有成员变量与私有内部工具函数两块。

私有成员变量仅有一个 int _epfd,它用来保存 epoll 实例的文件描述符,也就是 epoll_create 调用后内核返回的 fd。整个程序全程只会存在一个 Epoller 实例,也就只会持有一个_epfd,代表我们只创建了一个 epoll 模型实例,所有客户端 fd、监听套接字 fd 都会注册到这同一个 epoll 实例中。

私有工具函数 bool EpollCtl(int sockfd, int oper, uint32_t events) 是整个类的内部核心逻辑,所有对外"增删改"事件的接口最终都会调用这个函数,它统一封装了 epoll_ctl 系统调用的执行逻辑。这里我们区分两种 oper 操作分支处理:当参数 op 的操作类型为 EPOLL_CTL_DEL 时,也就是要从 epoll 内核中移除某个 fd 时,epoll_ctl 的第四个参数允许直接传入 nullptr,因此这个分支不会初始化 epoll_event 结构体;其余两种操作 EPOLL_CTL_ADD 新增、EPOLL_CTL_MOD 修改监听事件都需要填充 epoll_event 结构体,实例化结构体 ev 对象,把传入的监听事件 events 赋值给 ev.events,把待操作的文件描述符 sockfd 存入 ev.data.fd,完成结构体初始化后再调用 epoll_ctl。函数内部接收系统调用返回值 m,通过 (void) m 做显式忽略,这里是一种编码习惯,代表我们暂时不需要捕获本次调用的返回状态,统一返回 true 给到上层接口。这个内部函数的价值在于消除重复代码,三种 epoll_ctl 操作共用同一套内核调用逻辑,不需要在 Add、Mod、Del 三个接口里重复编写结构体填充代码。

构造函数与析构函数

构造函数 Epoller ():

Epoller 的无参构造函数负责创建 epoll 内核实例,也就是 epoll 生命周期的起点。函数内部调用epoll_create(256),传入参数 size 为 256 ,该参数仅为参考值。 调用完成后会校验返回的_epfd 值:如果_epfd 小于 0,代表 epoll 实例创建失败,属于程序致命错误,此时会通过日志模块打印错误信息,再调用 exit 函数,传入 Comm.hpp 中定义的 EPOLAYER_ERROR 错误码,直接终止整个程序;如果创建成功,则打印 INFO 级别日志,输出当前 epoll 实例的文件描述符,方便我们调试时确认实例正常初始化。 只要上层创建一个 Epoller 对象,就会自动完成 epoll 实例创建,不需要使用者手动调用系统调用,封装了资源初始化逻辑。

析构函数~Epoller ():

析构函数管控 epoll 内核资源的释放,是 epoll 生命周期的终点。当 Epoller 对象被销毁时,析构函数会判断私有成员 _epfd 是否大于等于 0,确认存在有效的 epoll 句柄,若满足条件则调用 close (_epfd) 关闭 epoll 文件描述符,释放内核中对应的 epoll 资源,避免程序运行结束后出现内核资源泄漏。依靠 C++ 对象的生命周期机制,我们无需手动执行 close 释放 epoll 句柄,对象销毁时自动回收资源,实现了资源管理的 RAII 特性。

对外public接口

公有接口分为事件等待接口、fd 事件增改删接口两类,全部提供简洁的调用入参,屏蔽底层细节,供给上层 Reactor 类调用。

WaitEvents():

这个函数封装了 epoll_wait 系统调用,职责是阻塞等待内核返回就绪 IO 事件,也就是内核向用户输出就绪 fd 及就绪事件。参数 revs 是上层传入的 epoll_event 数组,用来存储内核返回的就绪事件;event_num 代表数组最大容量,限制单次最多获取多少个就绪 fd;timeout 设置阻塞超时时间,默认 1000 毫秒,单位为毫秒。函数直接返回 epoll_wait 的原生返回值,返回值含义和系统调用完全一致:大于 0 代表本次捕获到多少个就绪事件,等于 0 代表超时无事件,小于 0 代表系统调用出错,上层 Reactor 拿到返回值后再做对应逻辑处理。

​​​​​​AddEvents():

这个函数是新增监听事件的接口,对应 epoll_ctl 的 EPOLL_CTL_ADD 操作。参数 sockfd 是需要注册进 epoll 的文件描述符,events 是需要监听的事件集合,例如本项目会传入 EPOLLIN | EPOLLET 代表ET边缘触发读事件。函数内部直接调用上面讲过的私有 EpollCtl 工具函数,传入 EPOLL_CTL_ADD 操作码作为参数 op,完成 fd 的内核注册,返回内部函数的布尔结果。

ModEvents():

这个函数是修改 fd 监听事件的接口,对应 epoll_ctl 的 EPOLL_CTL_MOD 操作。当我们需要动态调整某个 fd 的监听事件时调用,比如客户端发送缓冲区存在残留数据,我们需要新增 EPOLLOUT 写事件监听,数据全部发送完毕后又取消写事件,就调用这个接口更新内核中该 fd 的事件,底层同样复用上面讲的 EpollCtl 函数。

DelEvents():

这个函数是删除 fd 监听的接口,对应 epoll_ctl 的 EPOLL_CTL_DEL 操作。当客户端连接断开、需要回收该连接资源时,上层会调用这个接口,将目标 sockfd 从 epoll 内核实例中移除,内核不再监控这个 fd 的 IO 事件,底层同样复用上面讲过的 EpollCtl 接口,第二个操作

参数固定传入 0,因为删除操作不需要事件。

总结:

整体看完 Epoller 类全部代码,我们能清晰感知这个底层封装模块的设计意义。从代码复用层面,它把三份 epoll_ctl 操作、epoll 实例创建与销毁、事件等待全部收敛在单一类中,上层代码不会出现零散的 epoll 系统调用;从开发友好层面,屏蔽了 epoll_event 结构体填充、系统调用返回值校验等重复机械的代码,对外暴露语义清晰的 Add/Mod/Del/Wait 接口,降低使用门槛;从资源安全层面,依靠构造、析构函数实现 epoll 内核资源自动创建与释放,杜绝句柄泄漏;同时结合日志模块,所有关键生命周期节点、错误场景都有日志留存,便于后期调试。 这个类是整套 Reactor 网络框架的底层支撑,下一节我们就基于这个 Epoller 类,讲解依赖它的 Reactor.hpp 事件调度核心。

Reactor.hpp

上面我们已经把底层事件内核封装类 Epoller.hpp 讲了,Epoller 只负责单纯封装 epoll 的系统调用,只提供增删改等待事件的基础能力,但它不会管理连接、不会分发就绪 IO 事件、也没有循环运行的事件调度逻辑。而 Reactor.hpp 就是整套单 Reactor 网络框架的中枢调度核心,它依托 Epoller 完成内核事件监听,统一管理程序里所有套接字连接,循环等待 IO 事件、分发读写异常任务,同时管控连接的注册、销毁、空闲超时回收,是串联底层 epoll 和上层连接业务代码的中间层。下面我们依旧从文件头、类私有成员、构造析构、全部公有成员函数逐层拆解,通俗讲解每一段代码的作用。

头文件说明

文件开头使用标准头文件保护宏,防止头文件重复包含引发编译错误。 我们分两类梳理引入的头文件:

  1. 系统标准头文件:<iostream> 用于基础打印;<unordered_map> 是哈希容器,用来存放所有套接字连接;<memory> 提供 std::unique_ptr、std::shared_ptr 智能指针,自动管理堆内存,避免内存泄漏。
  2. 项目内部依赖头文件
    • Logger.hpp:全局日志工具,程序事件、报错、调试信息全部依靠它输出;
    • Epoller.hpp:我们上面讲解的 epoll 底层封装类,Reactor 内部依靠它和 Linux 内核 epoll 交互;
    • Connection.hpp:连接抽象基类,所有监听套接字、客户端数据套接字都继承这个类,Reactor 容器里存储的全部是该基类的智能指针,依靠多态统一分发事件;
    • Socket.hpp:套接字基础封装,创建 TCP 套接字时会用到,这里仅做前置依赖。

文件内定义全局常量 grevs_num = 64,代表单次调用 epoll_wait 最多一次性获取 64 个就绪 IO 事件,控制就绪事件数组的长度。 整个文件只定义唯一的 Reactor 类,对应我们项目单 Reactor 架构设计,全局只需要实例化一个 Reactor 对象,就可以管理监听套接字 fd、全部客户端 fd 的 IO 事件,完成事件循环调度、连接生命周期管理两大核心工作。

类私有内容解析

私有部分分为私有成员变量私有工具函数两块,是 Reactor 内部运转的基础支撑。

私有成员变量:

  1. _epoller:管理的 Epoller 实例的智能指针,也就是我们上面讲的 epoll 底层封装类,Reactor 所有增删改 fd 事件、等待就绪事件的操作,全部转发给这个 _epoller 对象完成,相当于 Reactor 的底层内核操作代理。
  2. revsgrevs_num:长度为 64 的 epoll_event 数组,用来接收 epoll_wait 从内核返回的就绪事件(输出型参数),每次调用 WaitEvents 都会把内核就绪数据填充进这个数组,后续分发事件时遍历这个数组。
  3. _connections:哈希 map,key 是文件描述符 sockfd,value 是连接基类 Connection 的共享智能指针。它的作用是缓存程序内所有存活的套接字连接,不管是监听套接字 Listener,还是客户端数据套接字 IOManager,全部存在这个容器里。当内核返回某个 fd 就绪时,Reactor 可以通过 fd 快速从 map 中取出对应的连接对象,调用它的读写处理函数;需要删除连接时,也通过 fd 找到对应对象销毁。

私有工具函数 IsLegalConnection():

这个函数是内部校验工具,传入一个文件描述符 sockfd,去_connections 哈希 map 里查找这个 fd 是否存在。

  • 如果 map 内能找到该 fd,返回 true,代表这是一个合法且存在的连接;
  • 如果找不到,返回 false,代表这个 fd 已经被销毁、不存在,后续就不会对这个 fd 执行读写事件分发。 它主要用于事件分发 Dispatcher 函数内做安全校验,防止内核返回已经销毁的 fd,访问空指针引发程序崩溃。

构造函数与析构函数

构造函数 Reactor():

构造函数使用初始化列表,直接创建 Epoller 智能指针对象,程序创建 Reactor 实例的瞬间,底层内核 epoll 模型实例就同步创建完成,无需使用者手动初始化 Epoller,依靠 RAII 机制自动完成底层资源初始化。构造函数函数体为空,所有底层资源初始化交给 Epoller 自身构造函数处理。

析构函数~Reactor ()

析构函数会随着 Reactor 对象生命周期结束自动调用,_epoller 作为 unique_ptr,对象销毁时会自动释放 Epoller 资源,关闭 epoll 句柄;同时_connections 哈希 map 内所有 Connection 共享智能指针会自动析构,所有套接字 fd 会被关闭,自动回收全部网络资源,全程不需要手动释放句柄、内存,杜绝资源泄漏。

公有public成员函数

1. 新增连接函数AddConnection()

这个函数是把新创建的连接(监听连接 / 客户端连接)注册进 Reactor,完成内核 epoll 注册、本地容器缓存、双向指针绑定三步操作。

  1. 调用 _epoller->AddEvents() 把连接的 fd 和需要监听的事件 (带 EPOLLET 边缘触发) 注册到 epoll 内核,内核开始监控这个 fd 的 IO 状态;
  2. 将这个连接智能指针存入 _connections 哈希 map,以 sockfd 作为 key 缓存,后续事件分发可以快速查找;
  3. 给连接内部的 Connection 指针赋值 conn->R = this,让 Connection 连接对象反向持有 Reactor 的指针。这么设计的原因是:客户端连接 IOManager 在读写缓冲区时,需要动态修改监听的读写事件,它自身没有 Epoller 的权限,只能通过这个反向指针调用 Reactor 的EnableReadWrite方法修改内核监听事件。

2. 删除连接函数DelConnection()

这个函数的作用是销毁一个套接字连接 : 释放内核 epoll 监控、关闭套接字、本地容器移除记录。执行分为三步:

  1. 调用 _epoller->DelEvents(sockfd) 从 epoll 内核中移除这个 fd,内核不再监控该 fd 的任何 IO 事件;
  2. 通过 fd 从 _connections 取出对应的 Connection 对象,调用对象的 Close()方法,关闭底层 socket 文件描述符,释放系统套接字资源;
  3. 在_connections 哈希 map 中 erase 这个 fd 对应的键值对,本地容器不再缓存这条连接,智能指针引用计数减一,内存自动回收。
  4. 客户端正常断开、连接空闲超时、读写异常时,都会调用这个函数回收连接。

3. 动态修改 fd 监听事件EnableReadWrite()

这个函数是适配 ET 边缘触发、按需监听写事件的核心函数,可以动态修改某个 fd 在内核 epoll 中监听的事件掩码。

  1. 先用 IsLegalConnection 校验 fd 是否合法存活,非法 fd 直接跳过;
  2. 根据传入的 readable、writeable 布尔值拼接事件,强制带上 EPOLLET 开启边缘触发:可读则追加 EPOLLIN,可写则追加 EPOLLOUT;
  3. 更新对应 Connection 对象内部存储的事件集合;
  4. 调用_epoller->ModEvents(),把新的事件同步更新到 epoll 内核。
  5. 使用场景:客户端发送缓冲区有残留数据时 IOManager 会调用这个函数开启 EPOLLOUT 写事件监听;当缓冲区数据全部发送完毕,再次调用函数关闭 EPOLLOUT,减少 epoll 内核无效通知,提升 ET 模式性能。

4. 就绪事件分发函数Dispatcher()

这个函数的参数 n 代表本次 epoll_wait 捕获到的就绪事件总数,核心职责是遍历就绪事件数组,根据 fd 的就绪事件类型,调用对应连接对象的读写处理虚函数,依靠多态区分监听 fd 和客户端 fd。

  1. 循环遍历本次所有就绪事件,取出就绪的 fd 和就绪事件 revents;
  2. 统一异常处理:如果检测到 EPOLLERR、EPOLLHUP (套接字错误、对端关闭),自动给事件追加 EPOLLIN、EPOLLOUT,强制触发读写处理函数,在读写逻辑里统一处理连接异常关闭;
  3. 校验 fd 合法性:调用 IsLegalConnection,确认这个 fd 还存在于连接容器;
  4. 事件分支处理:
    • 如果是读事件 EPOLLIN 就绪:取出 map 里的 Connection 对象,调用 ->Recver() 虚函数。若是 Listener 监听连接,会执行 accept 接收新客户端;若是 IOManager 客户端连接,会执行 ET 模式循环读数据逻辑;
    • 如果是写事件 EPOLLOUT 就绪:取出 Connection 对象,调用 ->Sender() 虚函数,执行 ET 模式循环发送缓冲区残留数据。 整个分发逻辑不需要区分 fd 是监听套接字还是数据套接字,依靠 Connection 基类多态自动匹配子类实现,代码解耦性极强。

5. LoopOnce 单次事件循环函数

这个函数完成一轮完整的事件检测、事件分发流程,是整个事件循环的最小执行单元。

  1. 调用 _epoller->WaitEvents() 阻塞等待内核就绪事件,把就绪数据填充进 revs 数组,返回就绪事件数量 n;
  2. 用 switch 分支处理三种返回情况:
    • case 0:返回 0 代表 epoll_wait 超时,没有任何 IO 事件,打印日志后直接结束本次循环;
    • case -1:返回 - 1 代表 epoll_wait 系统调用出现致命错误,打印 FATAL 级日志,直接退出整个程序;
    • default:n>0,捕获到就绪 IO 事件,打印日志,调用 Dispatcher(n) 分发所有就绪事件,完成本轮事件处理。

6. 空闲连接超时检测函数CheckExpiredLink()

遍历_connections容器内所有存活连接,读取每条连接记录的最后活跃时间,和当前系统时间做差值。如果连接超过 100000 毫秒没有收发数据,判定为空闲超时,自动调用DelConnection 销毁这条闲置连接,释放系统套接字资源,防止大量空闲连接占用文件描述符。同时函数会返回距离超时最近的连接剩余时间,用来调整下一轮 epoll_wait 的阻塞超时时间,优化事件循环效率。

7. Loop 永久事件循环主函数

这是整个网络服务的主循环,服务启动后会永久执行这个循环,不会退出。 内部是无限 while (true) 循环,每一轮循环做两件核心工作:

  1. 调用 LoopOnce(timeout) 执行单次事件检测、事件分发,处理当前所有就绪 IO 事件;
  2. 调用 CheckExpiredLink() 遍历所有连接,回收空闲超时的闲置套接字。 只要服务不主动关闭,这个循环会持续运行,持续监听、处理所有客户端 IO 事件,是整套服务的运行入口。

8. DebugPrint 调试打印函数

遍历_connections容器里所有存活连接,打印每个连接的 fd 和客户端 IP 端口地址,仅用于开发调试,查看当前服务有多少活跃客户端连接,线上环境可以注释掉调用。

总结:

Reactor 类是整套单 Reactor ET 网络框架的调度大脑,承接底层 Epoller 的 epoll 内核能力,向上统一管理所有类型套接字连接,串联起事件等待、事件分发、连接生命周期管理、空闲连接回收、动态读写事件调整全套能力。

  1. 分层解耦:底层 epoll 系统调用全部交给 Epoller 处理,Reactor 只做事件调度和连接管理,不用关心 epoll_event 结构体、系统调用细节;
  2. 多态适配两种连接:依靠 Connection 基类多态,同一套 Dispatcher 分发逻辑,自动适配 Listener 监听套接字、IOManager 客户端数据套接字,新增其他类型套接字也无需修改 Reactor 核心代码;
  3. 贴合 ET 模式优化:提供 EnableReadWrite 动态修改读写事件接口,实现写事件按需注册,减少内核无效通知,最大化 ET 模式性能优势;
  4. 完整连接生命周期管控:提供新增、删除连接接口,搭配空闲超时自动回收,杜绝僵尸连接占用系统资源;
  5. 封装完整事件循环:Loop永久主循环、LoopOnce单次循环拆分,结构清晰,代码可读性强,对外只需要实例化 Reactor、添加监听连接、调用 Loop 即可启动完整网络服务。

讲完底层 Epoller 和调度中枢 Reactor 之后,下一节我们就进入连接抽象层,解析 Connection.hpp连接基类,理清所有套接字连接的统一规范。

Connection.hpp

前面我们已经完成底层事件封装 Epoller、调度中枢 Reactor 的拆解,Epoller 负责和 epoll 内核交互,Reactor 负责事件循环、连接容器管理、事件分发,但它本身并不清楚如何处理读数据、发送数据、处理连接异常。而 Connection.hpp 就是整套框架里所有套接字连接的**统一抽象基类,**Listener 监听套接字、IOManager 客户端数据套接字全部继承这个类,依靠 C++ 多态实现一套分发逻辑适配两种连接。

Connection 基类统一规定了所有连接必须具备的读事件、写事件、异常事件三大行为,同时统一管理所有连接共用的文件描述符、事件掩码、收发缓冲区、客户端地址、活跃时间等公共资源。上层 Reactor 完全依靠基类多态实现统一事件分发,不用区分 fd 类型。我们依旧沿用之前的讲解框架:文件整体与依赖头文件、私有成员变量、构造析构、所有公有成员函数。

头文件说明

文件开头使用标准头文件保护宏防止当前头文件被工程多个文件重复包含,避免编译阶段出现类重定义报错,是整套项目统一的头文件规范写法。我们同样分为系统标准头文件与项目内部头文件两类梳理。

  • 系统标准头文件中,<iostream> 用于基础日志辅助输出,<ctime> 用于获取系统时间戳,专门用来做连接空闲超时判断;<cstdint> 提供标准定长整数类型,专门用于存储 epoll 事件,保证跨平台一致性;<string> 作为我们自定义收发缓冲区的容器,存储原始 TCP 字节流,处理粘包半包数据;<sys/epoll.h> 是 Linux 内核 epoll 核心头文件,提供 EPOLLIN、EPOLLOUT、EPOLLET 等事件的宏定义。
  • 项目内部依赖头文件引入 InetAddr.hpp,该类封装 IP 与端口信息,当前 Connection 基类统一用来保存对端客户端地址,用于日志打印与连接信息记录。
    接下来是整个代码中关键的一处代码,这行代码是第 11 行 Reactor 类前置声明 class Reactor;,这里单独进行说明,是整个头文件解耦的核心。因为 Connection 类内部持有 Reactor* R 反向指针,需要识别 Reactor 类型,但如果直接 include Reactor.hpp,会和 Reactor 内部 include Connection.hpp 形成双向循环包含,直接编译报错。前置声明只告诉编译器 Reactor 是合法类名,不展开完整定义,足够支撑指针定义。

整个文件只定义唯一的抽象基类 Connection,不能直接实例化。内部包含三个纯虚函数,强制所有子类必须实现读写异常逻辑,所有套接字通用数据全部放在 protected 保护域,子类可直接复用,对外提供一整套规范化接口,供上层 Reactor 统一调用。

类私有内容解析

整个 Connection 类的成员分为 protected 保护成员变量和公有成员指针变量两大部分:

  1. protected 内部首先定义 _sockfd,用于保存当前连接绑定的套接字文件描述符,包括监听连接保存监听 fd 和客户端连接保存通信 fd,Reactor 所有增删查找连接全部依靠该 fd 作为唯一标识。
  2. _events 用于保存当前 fd 在 epoll 内核中注册的事件,包含可读、可写、边缘触发等组合标识,Reactor 动态修改事件时会同步更新该变量,保证内存与内核事件一致。
  3. _active_time 保存连接最后一次成功收发数据的时间戳,每次正常通信都会刷新该时间,Reactor 依靠该字段判断连接是否空闲超时,用于自动回收僵尸连接。
  4. _inbuffer 是当前连接的专属接收缓冲区,内核读取到的所有原始 TCP 字节流都会存入该缓冲区,后续交给协议层统一拆包解析,解决 TCP 无边界流式粘包问题。
  5. _outbuffer 是当前连接的专属发送缓冲区,业务处理完成、协议封包完毕的应答数据,全部先存入该缓冲区,等待可写事件触发后统一发送,实现按需写、异步写。
  6. _clinetaddr 保存当前连接对应的对端地址信息,存储客户端 IP 和端口,全程跟随连接生命周期,用于日志输出、连接信息追踪。
  7. 公有成员中定义 Reactor *R 反向指针,这是所有连接能够回调调度器的核心。每一个连接创建后都会绑定归属的 Reactor,让连接自身可以主动调用 Reactor 的接口,动态修改读写事件、主动删除自身连接,实现连接自主管控,极大增强框架灵活性。

构造函数与析构函数

构造函数

Connection 类提供两个构造函数,分别适配默认构造与真实连接初始化场景。

无参构造函数负责初始化默认安全值,将文件描述符初始化为 -1 表示非法 fd,默认监听事件初始化为读事件,同时初始化当前系统时间作为初始活跃时间,保证空对象状态合法、可安全使用。

带参构造函数接收 sockfd 与初始 events 两个参数,sockfd 为当前需要管理的套接字文件描述符,events 为该 fd 需要注册的初始 epoll 事件集合,构造过程中完成 fd、事件、活跃时间均初始化,所有真实的监听连接、客户端连接创建时都会调用该构造函数,完成连接落地初始化。

析构函数

析构函数为隐式虚析构,由于类中存在纯虚函数,编译器自动识别为虚析构,保证 Reactor 通过基类指针释放子类对象时,能够正确调用子类析构,杜绝子类内存泄漏。析构时 string 缓冲区、地址对象自动释放资源,无需手动回收,保证资源安全。

公有public成员函数

1. 获取最后活跃时间lastActiveTime()

该函数无参数,直接返回当前连接保存的活跃时间戳,专门供给 Reactor 的超时检测逻辑调用,用于计算连接空闲时长,判断是否达到超时释放条件,是连接超时回收机制的数据来源接口。

2. 刷新活跃时间Active()

该函数无参数,每次连接成功读数据、成功写数据后都会调用,函数内部刷新系统最新时间戳到 _active_time,保证正常通信的连接不会被误判为空闲连接,避免正常连接被错误回收。

3. 纯虚写/读/异常事件处理函数

  1. Sender() 无参数,返回 int 状态码,作为纯虚函数强制子类必须重写,负责处理 EPOLLOUT 可写事件。监听套接字无需写事件,子类可以不用实现;客户端连接子类实现 ET 模式循环发送逻辑,不断写出 outbuffer 数据直到发送完毕或阻塞,发送完成后主动关闭写事件监听。
  2. 纯虚读事件处理函数 Recver() 无参数,返回 int 状态码,纯虚函数强制子类重写,负责处理 EPOLLIN 可读事件。监听套接字子类实现循环 accept 获取新连接,客户端套接字子类实现 ET 模式循环读数据,一次性读空内核缓冲区数据,存入应用层缓冲区,是整套 ET 事件驱动的核心入口。
  3. 纯虚异常处理函数 Excepter()无参数,返回 int 状态码,纯虚函数强制子类重写,统一处理所有套接字异常,包含对端关闭、套接字错误、连接异常断开等场景,内部完成 fd 关闭、连接销毁、资源回收全套异常收尾逻辑。

4. 获取当前文件描述符Sockfd()

无参数,返回当前连接绑定的 sockfd,Reactor 新增连接、删除连接、查找连接、匹配事件全部依赖该接口获取 fd,是上层调度器与下层连接交互的关键接口。

5. 关闭套接字资源Close()

无参数,内部判断 fd 合法性,合法则调用系统 close 关闭套接字,并将 fd 置为 -1 标记失效,统一回收系统套接字资源,所有连接销毁流程最终都会调用该函数释放句柄。

6. 获取当前监听事件Events()

无参数,返回当前内存中保存的 epoll 事件,Reactor 添加事件、修改事件时读取该连接的原有事件集合,用于内核事件同步更新。

7. 设置新事件SetEvents()

接收一个 uint32_t 类型事件参数,用于更新当前连接内存中存储的事件集合,Reactor 动态修改读写事件时调用该接口,保证内存记录与内核监听事件完全一致。

8. 初始化客户端地址InitAddr()

接收一个 const 引用的 InetAddr 对象,用于给当前连接设置对端地址信息,新客户端接入后由 Listener 统一初始化,记录客户端 IP 端口。

9. 获取客户端地址Addr()

无参数,返回当前保存的客户端地址对象,用于日志打印、连接信息展示、调试追踪。

相关问题

1. 是不是每一个 Connection 对象,就对应三次握手完成后的一条 TCP 连接?Connection 只是应用层对连接的描述、封装,真正的连接管理工作交给 Reactor 来做,这个理解是否正确?

这个理解是正确的,三次握手在内核 TCP 协议栈完成,内核会生成一个套接字文件描述符 fd,这个 fd 就是操作系统内核层面代表 TCP 连接的标识。而我们的 Connection 类是运行在用户态应用程序里的 C++ 封装对象,属于应用层的代码逻辑,每一条成功建立的 TCP 连接,我们都会在程序内创建唯一的一个 Connection 子类对象(IOManager),对象内部存储这条连接对应的 fd,用这个对象完整描述这条连接的所有配套资源:收发缓冲区、客户端地址、监听事件、活跃时间等等。 Connection 只负责描述单条连接的自有数据、处理单条连接的读写业务逻辑,没有能力批量管理所有连接;而全局唯一的 Reactor 实例,是整个程序里所有连接的统一管理者,所有 Connection 对象创建之后都会交给 Reactor 保管,由 Reactor 统一和 epoll 内核交互、统一分发所有 fd 的 IO 事件。所以总结一下 : Connection 做单连接描述封装,Reactor 做全部连接的统一管理。

2. 每个 Connection 内部的 _inbuffer、_outbuffer 是不是专属当前 fd 的应用层收发缓冲区?是不是内核缓冲区的数据读到这里,发送时也是从这块缓冲区拿数据发给内核?

_inbuffer、_outbuffer 是应用层缓冲区,和内核 socket 缓冲区是完全隔离的,这两块字符串缓冲区确实是当前 fd、当前这条连接独有的应用层内存缓冲区,和操作系统内核维护的 socket 接收缓冲区、发送缓冲区是两块完全独立的内存区域,二者分工完全不同。 当读事件触发,我们在 IOManager 的 Recver 函数里循环调用 recv 系统调用,这个操作的本质是把内核 socket 接收缓冲区里已经收到的 TCP 数据,拷贝到用户态的_inbuffer 当中,拷贝完成之后,内核缓冲区对应的数据就会被清空,后续业务拆包、解析 JSON 报文,全部操作的都是 _inbuffer 里这份应用层数据,和内核不再产生关联。 发送数据的流程刚好反过来,业务处理完成之后,我们把封包后的应答字节流写入 _outbuffer,此时数据只存在于应用层内存,还没有发送给操作系统内核;等到 EPOLLOUT 写事件就绪,我们调用 send 系统调用,把_outbuffer 里的数据拷贝到内核 socket 发送缓冲区,内核再异步把数据通过网卡发给客户端,send 成功发送出去的字节,会从_outbuffer 头部删除。 简单总结:内核缓冲区是操作系统层面临时存放网络数据的地方,我们无法直接操作;_inbuffer 和_outbuffer 是我们代码自己开辟的用户态内存,用来缓存整份完整报文、处理 TCP 粘包半包,是业务和内核之间的数据中转层,每条连接独立拥有,互不干扰。

3. 所有 Connection 对象都会持有一个 Reactor* R 指针,每个连接都拿着指针去调用 Reactor 的接口,看起来各个连接都是各自调用各自的 R,那所有连接又是怎么统一关联到同一个 Reactor 实例上的?

所有连接的 R 指针,全部指向同一个全局 Reactor 实例,不存在 "各干各的",整个代码只会创建唯一的一个 Reactor 对象,程序启动时在 main 函数完成实例化。当 Listener 监听到新客户端、创建 IOManager 连接对象时,会把这个全局 Reactor 对象的 this 指针赋值给新连接内部的 R 成员,也就是说,程序里成千上百个 Connection 对象,它们内部存储的 R 指针,内存地址完全相同,全部指向那同一个 Reactor 实例。 从代码层面看,各个连接只是借助自身保存的指针,去调用同一个 Reactor 对象内部的成员函数,看似是每个连接自己调用 R,实际上全部操作都作用在同一个 Reactor 身上,所有操作最终都会修改 Reactor 内部统一的_connections 哈希容器、统一调用 Reactor 持有的_epoller 底层对象操作 epoll 内核,并不会出现多个独立管理对象、连接各自为政的情况。

4. epoll 内核内部红黑树会存储全部客户端 fd 及事件,既然每个连接各自调用自身 R 指针的方法,内核又是怎么把这么多 fd 统一管控、事件统一推送给 Reactor 的?

epoll 内核红黑树统一管理全部 fd,内核只和 Reactor 做事件交互,和 Connection 无直接接触,操作系统内核中的 epoll 实例,内部依靠红黑树存储所有注册进来的 fd,不管是监听 fd,还是数百上千个客户端 fd,全部都登记在这一颗红黑树里,内核会持续监控所有 fd 的 IO 状态。 但是内核不会感知、也不会存在任何 Connection 对象,内核唯一的交互对象是我们用户态程序里的 Reactor:Reactor 通过内部的 Epoller 封装类,调用 epoll_ctl 把所有 fd 注册进内核红黑树,同时定义每个 fd 需要监听的事件;Reactor 调用 epoll_wait 阻塞等待内核返回就绪事件,内核只会把当前产生 IO 事件的 fd 号批量拷贝给 Reactor 的 revs 数组。 后续的分发逻辑完全是用户态代码的行为:Reactor 拿到就绪 fd 之后,通过自身的_connections 哈希 map,找到对应 fd 绑定的 Connection 对象,再调用连接的读写处理函数。整个链路中,内核只认识 fd,只和 Reactor 通信;Connection 完全是用户态业务封装,不会直接和内核产生任何交互。 各个连接通过 R 指针调用 Reactor 接口,本质是通知统一的调度中枢去修改内核里对应 fd 的监听规则,所有修改指令都会汇总到同一个 Reactor、同一个 Epoller,最终由同一套 epoll 系统调用同步给内核红黑树,内核始终只有一份完整的 fd 清单,不会出现数据割裂、多个管理通道的冲突问题。

总结

Connection 基类是整个单 Reactor、ET 模式网络框架所有连接的规范基石,完成了数据与行为的统一抽象。依靠前置声明巧妙解决了 Reactor 与 Connection 的头文件循环依赖问题,让分层架构彻底解耦;统一收纳所有连接通用的 fd、事件、缓冲区、地址、活跃时间资源;通过三大纯虚函数强制规范读写异常行为,依托多态让 Reactor 同一套分发逻辑可以无缝适配监听连接、客户端连接两种完全不同的业务行为;全套封装资源初始化、资源关闭、时间刷新、参数获取接口,让所有连接行为标准化、规范化、可管控。

整个基类不负责具体业务,只负责定规范、存数据、留接口,支撑上层 Reactor 调度、下层 Listener 与 IOManager 业务实现,是整套框架分层设计最关键的抽象层。讲完连接抽象基类 Connection 之后,下一节我们就开始讲解它的第一个实现子类 Listener.hpp,专门负责监听套接字、接收新连接的完整逻辑。

Listener.hpp

上面我们已经讲了连接抽象基类 Connection.hpp,Connection 规定了所有套接字连接统一的读写、异常接口与通用资源存储,但是却不会区分监听套接字和客户端数据套接字的业务逻辑。而 Listener.hpp 是 Connection 基类的第一个实现子类,专门用来封装服务端的监听套接字,基于 ET 边缘触发方式实现,核心职责就是持续接收客户端发起的 TCP 连接握手请求,每捕获一条新连接就自动封装为 IOManager 客户端连接对象,注册进全局 Reactor 调度器统一管理。Listener 仅处理读事件,不需要处理写事件,我们依旧沿用之前讲解 Epoller、Reactor、Connection 完全一致的讲解结构,从文件头依赖、类私有成员、构造析构、全部重写的公有成员函数逐层完整拆解,全程段落式行文,细致讲解每个函数入参、返回值和代码业务逻辑。

头文件说明

文件开头使用标准头文件保护宏来防止当前头文件被工程多个代码文件重复引入,规避编译器报出类重复定义的编译错误,和项目内其他头文件保持统一规范。我们将文件引入的头文件分为系统标准头文件和项目内部自定义头文件,系统标准头文件包含用于内存智能指针管理的 <memory>,提供 epoll 事件宏定义的 <sys/epoll.h>,基础 IO 输出的<iostream>;项目内部头文件首先引入 Comm.hpp,里面封装了设置套接字非阻塞的工具函数、程序退出错误码,随后引入 Connection.hpp 作为本类继承的父类,Socket.hpp 提供 TCP 套接字基础封装,IOManager.hpp 是处理客户端数据的连接子类,Listener 接收新连接之后会实例化该类对象,最后 InetAddr.hpp 用来封装客户端 IP 与端口地址信息。
文件内定义全局静态常量 gdefaultport,默认值为 8080,作为服务端默认监听端口,外部创建 Listener 对象时可以自定义传入端口号,不传入参数就会默认监听 8080 端口。整个文件只定义唯一的 Listener 类,采用公有继承的方式继承 Connection 抽象基类,必须重写父类内 Sender、Recver、Excepter 三个纯虚函数才能完成实例化,该类仅用来管理 listen 监听 fd,只会向 epoll 内核注册 EPOLLIN | EPOLLET 读事件,不会注册任何写事件,只负责处理新连接接入的业务,不处理任何客户端数据收发逻辑。

类私有成员解析

Listener 类的私有成员变量都是监听套接字独有的资源,父类 Connection 的 fd、事件、活跃时间、收发缓冲区等通用资源全部继承复用,不需要在子类内部重复定义。

  1. 第一个私有成员变量是 uint16_t 类型的 _port,用来存储当前监听套接字绑定的端口号,构造函数接收外部传入的端口参数之后赋值给该变量,后续创建 TCP 监听套接字时读取该端口完成 bind 绑定操作。
  2. 第二个私有成员变量是 std::unique_ptr<Socket>类型的 _listensock,智能指针管理 TcpSocket 套接字对象,所有 socket 创建、绑定端口、开启监听、accept 获取新 fd 的底层操作全部交给这个套接字对象完成,智能指针自动管理套接字内存,对象生命周期结束时自动释放资源,避免内存泄漏。
  3. 第三个私有成员变量是 on_message_t 类型的_on_message_helper,这是一个函数回调类型变量,存储上层传入的报文处理回调函数,Listener 接收新客户端连接、创建 IOManager 对象时,会把这个回调函数传递给 IOManager,当客户端完成报文读取之后,IOManager 会调用这个回调函数完成报文解析与业务计算,实现监听模块、客户端数据模块、业务模块三层解耦。

构造函数与析构函数

构造函数

Listener 类只提供一个带参构造函数,构造函数接收两个参数,第一个参数是 on_message_t 类型的_on_message_helper,也就是上层传入的业务报文处理回调,第二个参数是 uint16_t 类型的 port,端口参数拥有默认值 gdefaultport,外部调用时可以选择只传入回调函数、省略端口号,自动使用 8080 默认端口。构造函数初始化列表会先把传入的回调、端口分别赋值给私有成员变量,再创建 TcpSocket 智能指针实例,进入函数体内部之后调用_listensock 的 BuildTcpSocketMethod 方法,传入端口完成套接字创建、端口复用设置、bind 绑定、listen 开启监听全套 TCP 服务端初始化流程,随后从套接字对象中提取生成好的 listen fd 赋值给父类继承而来的 _sockfd,设置当前 fd 注册到 epoll 的事件为 EPOLLIN | EPOLLET,开启 ET 边缘触发模式,最后调用父类继承的 Active 函数刷新连接活跃时间戳,完成监听连接对象的全部初始化操作,实例化完成之后只需要交给 Reactor 注册进 epoll 内核,就能自动监听客户端连接请求。

析构函数

Listener 的析构函数~Listener () 函数体为空,依靠智能指针的自动回收机制,私有成员_listensock 的 unique_ptr 销毁时会自动释放 TcpSocket 对象,关闭底层 listen 监听 fd,父类 Connection 拥有隐式虚析构,子类对象销毁时会完整执行子类析构逻辑,不会出现资源泄漏问题,不需要我们手动编写关闭套接字、释放内存的代码。

共有public成员函数

1. 重写写事件处理函数Sende()

该函数无任何传入参数,返回 int 类型的执行状态码 0,作为父类纯虚函数必须重写实现,监听套接字只用来接收客户端连接,不会注册 EPOLLOUT 写事件,内核不会对监听套接字 fd 触发写就绪事件,因此函数内部不需要任何业务逻辑,直接返回 0 作为执行成功标识即可,仅完成语法层面的纯虚函数重写要求。

2. 写读事件处理函数Recver()

该函数无传入参数,返回 int 类型的执行状态码,是 Listener 子类的核心业务函数,当 epoll 内核检测到监听套接字 fd 触发 EPOLLIN 读就绪事件,即有新连接待接入时,Reactor 的 Dispatcher 分发函数会调用这个函数,Recver() 函数内部遵循 ET 边缘触发方式,使用 while 循环持续调用 accept,一次性取完内核等待队列里所有待握手完成的客户端连接,避免 ET 模式下单次只读取一条新连接、剩余连接不再触发事件的问题。

循环内部首先定义 InetAddr 客户端地址对象、int 类型错误码 errcode,调用 _listensock 的 Accepter 方法执行 accept 系统调用,传入客户端地址对象与错误码变量,函数会返回新客户端的套接字 fd,打印 DEBUG 级别日志记录本次触发监听读事件,随后对返回的 sockfd 做分支判断,如果返回 fd 小于 0 代表 accept 调用出现异常,进入异常判断分支,当 errcode 等于 EWOULDBLOCK 或者 EAGAIN 时,代表内核连接等待队列已经没有剩余新连接,打印 INFO 日志告知全部客户端连接读取完毕,执行 break 跳出 while 循环结束本次读事件处理;如果 errcode 等于 EINTR,代表 accept 系统调用被系统信号中断,不会产生业务错误,执行 continue 重新进入循环再次尝试调用 accept;其余错误码代表 accept 出现致命异常,打印 WARNING 级别日志记录 accept 错误,直接 return -1 终止函数。

如果 accept 成功获取合法的客户端 sockfd,就会执行新连接封装注册逻辑,首先创建 IOManager 客户端连接的共享智能指针,传入新客户端 fd、EPOLLIN | EPOLLET 边缘触发读事件、构造函数保存的业务回调 _on_message_helper,调用新连接对象的 InitAddr 方法,把本次 accept 拿到的客户端地址对象赋值给连接内部存储的客户端地址,随后通过父类继承而来的 Reactor 反向指针 R,调用 Reactor 的 AddConnection 接口,把新生成的 IOManager 连接注册进全局 Reactor,Reactor 内部会自动完成 fd 添加进 epoll 内核、存入连接哈希容器、双向指针绑定全套操作,打印 INFO 日志输出新连接的 fd 与客户端 IP 端口地址,完成单条新连接的全流程处理,随后回到 while 循环头部继续尝试 accept 读取剩余新连接,直到内核无新连接跳出循环,函数最终 return 0 代表本次监听读事件处理全部成功。

3. 重写异常处理函数Excepter()

该函数无传入参数,返回 int 类型状态码 0,同样是父类纯虚函数必须重写实现,监听套接字生命周期和整个服务进程绑定,正常运行阶段不会出现套接字错误、对端关闭等异常场景,仅作为语法层面重写纯虚函数使用,函数内部无任何业务逻辑,直接返回 0 即可。

相关问题

1. Listener 的 Recver 函数拿到合法客户端 fd 之后创建 IOManager 对象,这个对象采用共享智能指针存储,这里是子类 IOManager 的智能指针赋值给 Connection 基类类型的智能指针,怎么理解?

IOManager 公有继承自 Connection 抽象基类,在 C++ 的面向对象语法里天然支持向上转型,也就是派生类向基类的转换,这种转换是编译器隐式支持、安全且不需要强制类型转换的。我们代码里用 std::shared_ptr<IOManager> 创建新客户端连接对象,而 Reactor 内部存储所有连接的哈希容器 _connections,它的 value 类型是 std::shared_ptr<Connection>,编译器会自动完成智能指针的向上转换,把 IOManager 类型的共享智能指针直接存入容器,整个转换过程不会产生对象拷贝,仅仅修改智能指针内部的引用计数,底层依旧指向堆上同一个 IOManager 实例,不存在内存拷贝带来的性能损耗,这也是整套框架依靠多态实现统一事件分发的基础,如果没有这种向上转型的语法支持,Reactor 容器就无法同时存放 Listener 和 IOManager 两种不同的连接子类对象,也就不能只用一套 Dispatcher 分发逻辑处理两种套接字。

2. 那这里我们就顺便再讲一下 C++ 中子类指针直接赋值给基类指针的底层原理

子类指针赋值给基类指针的底层逻辑,这里要区分裸指针和智能指针,二者向上转型的底层逻辑是相通的。从内存布局来看,IOManager 对象的内存空间最开头的内存段,是父类 Connection 的所有成员变量,编译器识别到子类对象首地址和基类子对象地址完全一致,因此可以直接把子类的指针、智能指针隐式转换为基类类型,转换完成之后,通过基类指针只能访问 Connection 基类里定义的成员变量与成员函数,子类 IOManager 独有的私有回调变量 _on_message 会被隐藏起来;但当我们通过基类指针调用 Sender、Recver、Excepter 这三个虚函数时,程序会借助虚函数表找到 IOManager 子类内部重写的函数实现,不会调用基类空的纯虚函数,这就是多态的核心实现原理,也解释了为什么 Reactor 只持有 Connection 基类智能指针,却能正确执行 IOManager 的客户端读写逻辑。

3. IOManager 对象调用 InitAddr 这个接口的具体作用、调用时机和设计意义。

这里我们要清楚 : accept 的第二个参数属于输出型参数。系统调用 accept 的函数原型里第二个参数是 sockaddr* 类型,第三个参数是 socklen_t*,内核会在 accept 成功获取新连接时,主动把客户端的 IP、端口数据填充进这块 sockaddr 内存空间,数据是函数执行结束之后才写入的,调用这个函数之前我们只提前开辟好了存储地址的内存,里面不存在有效的客户端信息,所有有效信息都靠函数内部向外输出,这就是输出型参数的定义。

放到我们 Listener 的 Recver 函数逻辑里,代码中我们定义了 InetAddr 局部对象,再把这个对象内部封装的 sockaddr 结构体地址传给 accept,函数执行完成之后,原本空白的 InetAddr 对象内部就完整保存了客户端地址,之后我们调用 IOManager 对象的 InitAddr 方法把这份地址存入连接对象,全程依靠 accept 输出型参数拿到客户端地址。

同时区分清楚输入参数和输出参数的差别,输入型参数是我们提前准备好数据传给函数使用,而输出型参数是函数用来向外返回数据的载体,accept 的返回值只用来输出新客户端 fd,客户端地址没有办法通过返回值传递,所以操作系统 API 设计时选择借助第二个参数作为输出载体,第三个参数作为输入输出复合参数,调用前我们写入 sockaddr 结构体长度,函数执行后内核会修改它,写入实际填充的地址字节长度。

总结:

Listener 子类是整套单 Reactor ET 网络框架里服务端接入层的实现,专门负责 TCP 服务端监听端口、接收客户端新连接,依托 Connection 基类的多态能力,和处理客户端数据的 IOManager 子类共用同一套 Reactor 事件分发逻辑,不用修改 Reactor 任何核心代码。

并且代码中遵循 ET 边缘触发的方式,使用 while 循环持续 accept 读取全部待接入客户端,规避 LT 模式监听 fd 丢连接的经典问题;整体分层解耦设计清晰,监听套接字的创建、绑定、listen 底层操作全部交给 Socket 封装类,新连接生成后的客户端数据处理逻辑交给 IOManager,业务计算逻辑通过外部传入的回调函数解耦,Listener 自身只专注处理新连接接入这单一职责,符合单一职责的面向对象设计思想。

依托父类 Connection 统一封装的反向 Reactor 指针,Listener 拿到新客户端 fd 之后可以直接调用 Reactor 新增连接接口,自动完成 epoll 内核注册、连接容器缓存,不需要感知 epoll 底层的系统调用细节;同时复用父类全部通用资源、标准化接口,极大减少子类内部重复冗余代码,整套监听逻辑简洁清晰,和底层事件调度、上层业务处理完全解耦,方便后续修改监听端口、替换底层套接字实现、更换业务处理逻辑。

讲完监听套接字子类 Listener.hpp 之后,下一节我们就开始讲解处理客户端数据的核心子类 IOManager.hpp,完整实现了 ET 边缘触发循环读写、应用层缓冲区粘包处理、动态按需开关写事件等全部核心网络逻辑。

IOManager.hpp

监听套接字子类 Listener.hpp 中 Listener 的职责仅仅是接收新客户端 TCP 连接,拿到客户端 fd 之后,会将 fd 封装为 IOManager 对象交给 Reactor 管理。IOManager.hpp 是 Connection 基类最核心的业务子类,代码中 ET 边缘触发、非阻塞循环读写、应用层缓冲区处理 TCP 粘包、按需动态开关写事件的核心逻辑都已实现,专门用来处理客户端与服务端之间的双向数据收发。IOManager 复用 Connection 基类的通用资源,重写 Sender、Recver、Excepter 三个纯虚接口,依靠回调函数和上层协议、业务逻辑解耦。

头文件说明

文件最开头放置标准头文件保护宏防止当前头文件被工程多个代码文件重复引入,避免编译器报出类重复定义的错误,和项目内其余所有头文件保持一致的编码规范。文件引入的头文件分为系统标准头文件与项目自定义内部头文件,系统标准头文件包含 <functional>,这个头文件提供 std::function 可调用函数模板,用来定义报文处理回调类型 on_message_t;项目内部头文件首先引入 Comm.hpp,该文件内部封装了设置套接字非阻塞的工具函数、程序各类错误退出码,IOManager 读写异常时会依赖里面的错误枚举打印日志;其次引入 Connection.hpp,作为当前 IOManager 类公有继承的抽象基类,继承父类全部 fd、事件掩码、收发缓冲区、客户端地址、Reactor 反向指针等通用资源与标准化接口。
文件内部定义全局静态常量 buffersize,固定数值为 1024,代表单次 recv 系统调用从内核缓冲区拉取数据的临时内存大小,每次读事件触发时,我们会开辟一块长度 1024 的字符数组作为临时读取容器,批量拷贝内核 TCP 数据到应用层。同时文件顶部使用 using 关键字重命名函数类型 on_message_t,该类型接收一个 std::string 引用参数 (也就是当前连接完整的接收缓冲区数据),返回 std::string 类型字符串 (经过协议解析、业务计算、封包之后的应答报文),这个回调函数作为 IOManager 和上层 Protocol 协议层、Calculator 业务层的桥梁,实现网络 IO 逻辑与业务计算逻辑完全解耦。

整个文件只定义唯一的 IOManager 类,公有继承自 Connection 抽象基类,必须完整重写父类 Sender、Recver、Excepter 三个纯虚函数才能够实例化,该类专门对应每一条三次握手完成后的客户端 TCP 连接,每一个客户端 fd 都会单独生成一个 IOManager 对象,对象内部依靠父类自带的_inbuffer、_outbuffer 完成粘包处理、应答数据缓存,遵循 ET 边缘触发方式,读、写逻辑全部采用 while 循环持续调用系统调用,直到内核缓冲区无数据可读、无数据可写,同时依靠 Reactor 反向指针动态调整当前 fd 监听的读写事件,实现写事件按需注册的高性能优化方案。

类私有成员解析

IOManager 类仅拥有唯一的私有成员变量 _on_message,类型为我们提前 using 定义的 on_message_t 函数对象,用来存储外部传入的报文处理回调函数。当 Recver 读事件函数循环读取完内核全部的 TCP 数据、追加存入 _inbuffer 接收缓冲区之后,程序会调用这个回调函数,把当前完整的接收缓冲区字符串传入回调,回调内部会执行 Protocol 协议类的拆包逻辑、Calculator 计算器业务运算、应答报文封包逻辑,最终返回组装完成的应答字符串,IOManager 会将这份应答字符串追加写入自身的 _outbuffer 发送缓冲区,等待写就绪事件触发后发送给客户端。

这份回调变量的设计实现了极强的分层解耦,IOManager 只专注完成网络层面的数据读写、缓冲区管理、事件监听调整,不用关心上层 JSON 协议解析、数据计算逻辑,后续如果需要替换通信协议、更换业务处理逻辑,只需要修改外部传入的回调函数,不需要改动 IOManager 内部的读写代码,极大提升了整套网络框架的拓展性。除此之外,套接字文件描述符_sockfd、事件_events、收发缓冲区_inbuffer 与_outbuffer、客户端地址_clinetaddr、反向 Reactor 指针 R、连接活跃时间_active_time 全部继承自父类 Connection,不需要在子类内部重复定义,复用通用代码。

构造函数与析构函数

构造函数

IOManager 类仅提供一个带参构造函数,构造函数一共接收三个参数,第一个参数是 int 类型的 sockfd,代表当前客户端 TCP 连接对应的套接字文件描述符,也就是 Listener 调用 accept 获取到的新连接 fd;第二个参数是 uint32_t 类型的 events,代表该 fd 初始注册到 epoll 内核的事件,默认传入 EPOLLIN | EPOLLET,全程开启 ET 边缘触发读事件;第三个参数是 on_message_t 类型的_on_message,也就是上层传入的报文业务处理回调函数。构造函数初始化列表首先调用父类 Connection 的带参构造,把 sockfd 和 events 传递给父类完成基础资源初始化,随后将传入的回调函数赋值给私有成员_on_message,进入函数体内部之后调用父类继承而来的 Active 函数,刷新当前连接的活跃时间戳,标记连接此刻处于活跃通信状态,完整完成一条客户端连接对象的初始化工作,Listener 拿到新客户端 fd 之后,直接调用该构造函数生成 IOManager 共享智能指针,再交由 Reactor 完成内核注册与容器缓存。

析构函数

IOManager 的析构函数~IOManager 函数体为空,依靠 C++ 智能指针与父类虚析构机制自动回收资源,IOManager 对象被销毁时,父类 Connection 的虚析构会自动执行,父类内部 string 类型的_inbuffer、_outbuffer 缓冲区、InetAddr 地址对象会自动释放占用的堆内存,客户端套接字 fd 的关闭逻辑统一封装在父类 Close 函数,由 Excepter 异常处理函数调用执行,析构函数无需手动编写资源回收代码,不会出现内存泄漏、句柄泄漏的问题。

共有public成员函数

1. 重写读事件处理函数Recver()

该函数无任何传入参数,返回 int 类型的执行状态码,是 IOManager 内部处理客户端读就绪事件的核心函数,当 epoll 内核检测到客户端 fd 的内核接收缓冲区存在新数据、触发 EPOLLIN 读就绪事件时,Reactor 的 Dispatcher 分发函数会调用这个函数,函数遵循 ET 边缘触发的方式,使用 while 循环持续调用 recv 系统调用,一次性读取完内核接收缓冲区内部全部 TCP 数据,规避了 ET 模式单次读取残留数据、不再重复触发读事件导致数据丢失的问题。

函数进入后首先调用父类 Active 函数刷新连接活跃时间戳,保证本次正常读数据操作更新活跃标记,避免连接被空闲超时回收,同时打印 DEBUG 级日志标记当前事件分发到 IOManager 读处理逻辑。随后开辟一块长度为 buffersize 的临时字符数组 buffer,开启 while (true) 循环执行 recv 读取操作,调用 recv 系统调用时传入当前连接 _sockfd、临时 buffer 数组、buffersize -1的读取长度,末尾传入 0 代表无特殊读取标记,系统调用返回值存入 ssize_t 类型变量 n,随后分多分支处理返回值 n 的不同场景。

如果 n 大于 0,代表本次 recv 成功从内核缓冲区读取到字节数据,我们在 buffer 读取到的末尾位置手动补字符串结束符,再将 buffer 内部全部读取到的字节数据追加写入父类继承而来的_inbuffer 接收缓冲区,随后直接回到 while 循环开头,继续尝试读取内核剩余数据;如果 n 等于 0,代表客户端主动正常关闭 TCP 连接,此时打印 INFO 级日志输出当前断开的客户端地址,调用自身 Excepter 异常处理函数执行连接销毁回收逻辑,同时 return -1 终止本次读事件处理;如果 n 小于 0,代表 recv 系统调用出现错误,此时读取全局 errno 错误码做细分判断,当 errno 等于 EWOULDBLOCK 或者 EAGAIN 时,代表内核接收缓冲区已经不存在任何可读数据,本次循环读取完毕,打印日志之后执行 break 跳出 while 循环,结束本轮数据读取;当 errno 等于 EINTR 时,代表 recv 系统调用被操作系统信号中断,本次读取没有业务层面错误,执行 continue 回到循环开头重新尝试读取数据;其余 errno 数值代表出现致命读取错误,打印 INFO 级日志输出错误客户端地址与套接字 fd,调用 Excepter 函数销毁连接,return -2 终止函数执行。

完成 while 循环读取全部内核数据之后,打印 DEBUG 级日志输出当前套接字 fd 与读取完毕后的完整_inbuffer 缓冲区内容,此时所有本次内核读取到的 TCP 字节流全部存入应用层的 _inbuffer 缓冲区,缓冲区内部可能存在半包、多条完整报文的混合数据,所以我们还需要将这个缓冲区字符串传入私有成员存储的 _on_message 报文回调函数,回调内部会执行 Protocol 协议层循环拆包、JSON 反序列化、Calculator 业务计算、应答报文封包全套逻辑,回调函数执行完成后会返回组装完毕的应答字符串,我们将返回的应答字符串直接追加写入父类的_outbuffer 发送缓冲区,等待后续写就绪事件触发发送给客户端。

写入应答数据到 _outbuffer 之后,程序会判断 _outbuffer 是否为空,如果缓冲区内部存在待发送的应答数据,代表当前存在数据需要发送给客户端,此时通过父类继承的 Reactor 反向指针 R 调用 Reactor 的 EnableReadWrite 接口,参数传入 true 开启读事件、true 开启写事件,动态修改当前 fd 在内核 epoll 的监听事件,新增 EPOLLOUT 写事件监听,内核一旦检测到套接字发送缓冲区有空闲空间,就会触发写就绪事件,调用 Sender 函数发送 _outbuffer 内部缓存的应答数据;函数最后返回 _inbuffer 的字符串长度,标识本次读操作总共读取到的字节数量。

2. 重写写事件处理函数Sender()

Sender()函数无传入参数,返回 int 类型执行状态码,负责处理 EPOLLOUT 写就绪事件,当 epoll 内核检测到客户端 fd 的内核发送缓冲区存在空闲位置、可以写入新数据时,Reactor 派发函数会调用该函数,同样遵循 ET 边缘触发规范,使用 while 循环持续调用 send 系统调用,尽可能一次性发送完_outbuffer 内部全部缓存的应答数据,减少 epoll 内核反复触发写事件带来的性能损耗。

函数进入首先调用 Active 函数刷新连接活跃时间戳,打印 DEBUG 级日志标记事件分发到 IOManager 写处理逻辑,开启 while (true) 无限循环执行 send 发送操作,调用 send 系统调用传入_sockfd、_outbuffer 的 c_str 底层字符指针、_outbuffer 当前整体长度,末尾 0 为无特殊发送标记,send 返回值存入 ssize_t 变量 n,分场景处理返回值。

如果 n 大于 0,代表本次 send 成功将 n 个字节的应答数据拷贝到内核发送缓冲区,我们调用_outbuffer 的 erase 方法删除缓冲区头部已经成功发送的 n 个字节,随后判断当前_outbuffer 是否已经清空,如果缓冲区不存在剩余待发送数据,直接 break 跳出 while 循环结束本次写操作;如果 n 小于 0,读取全局 errno 错误码进行分支判断,errno 等于 EWOULDBLOCK 或者 EAGAIN 代表内核发送缓冲区已经填满,没有多余空间接收新数据,执行 break 跳出循环暂停发送;errno 等于 EINTR 代表 send 调用被系统信号中断,执行 continue 回到循环头部重新尝试发送;其余错误码代表发送出现致命异常,打印 INFO 错误日志、调用 Excepter 销毁连接,return -1 终止函数。

循环发送逻辑结束之后,再次判断_outbuffer 缓冲区是否还有剩余未发送的应答数据,如果缓冲区不为空,代表本次没有把全部数据发送完毕,内核发送缓冲区已经占满,我们依旧通过 Reactor 指针 R 调用 EnableReadWrite 接口,保持读事件开启、写事件开启,持续监听 EPOLLOUT 写就绪事件,等待内核缓冲区腾出空间后再次触发 Sender 函数发送剩余数据,同时打印 INFO 日志告知当前缓冲区存在残留数据,持续托管写事件给 epoll 内核;如果_outbuffer 缓冲区已经全部清空,代表所有应答数据完整发送给内核,此时调用 EnableReadWrite 接口,第一个参数 true 保持读事件监听,第二个参数 false 关闭 EPOLLOUT 写事件,不再让内核触发写就绪事件,减少无效 epoll 通知,提升 ET 模式运行性能,同时打印 INFO 日志标记数据发送完毕、关闭写事件监听,函数最终 return 0 代表本次写事件处理全部成功。

3. 重写异常处理函数Excepter()

该函数无传入参数,返回 int 类型状态码,用来统一处理当前客户端 fd 产生的全部异常场景,包含客户端主动断开连接、recv/send 系统调用抛出致命错误、套接字内核错误 EPOLLERR、对端关闭 EPOLLHUP、连接空闲超时回收等所有会导致连接无法继续通信的场景。函数进入后首先打印 WARNING 级日志,输出当前全局 errno 错误码与对应的错误文字描述,方便开发调试定位异常原因,随后通过 Reactor 反向指针 R 调用 Reactor 的 DelConnection 接口,传入当前连接的_sockfd,Reactor 内部会完整执行从 epoll 内核移除 fd、调用 Connection 基类 Close 函数关闭套接字 fd、从_connections 哈希容器擦除该连接记录全套销毁逻辑,自动回收这条客户端 TCP 连接的全部资源,函数最后 return 0 标识异常处理流程执行完毕。

相关问题

1. 首先我们先确认一下 Recver 读取数据的流转逻辑,客户端发送的数据会先存入内核 TCP 接收缓冲区,Recver 内部调用 recv 系统调用拷贝内核里未经修改的原始网络字节流到应用层 _inbuffer 缓冲区,这个数据流转逻辑是否正确?

是的,整个数据流转的逻辑是正确的,客户端经由网络发送的二进制字节流抵达服务端网卡后,操作系统内核 TCP 协议栈会先将这份数据缓存到当前客户端 fd 专属的内核接收缓冲区,这块内存由操作系统全权管控,应用程序没有直接读写的权限,只能依靠 recv 系统调用完成数据从内核态到用户态的拷贝。IOManager 的 Recver 函数内部 while 循环持续执行 recv 调用,每一次调用都会把内核接收缓冲区现存的全部原始字节数据拷贝到函数内临时开辟的字符数组 buffer,这份字节流不会经过协议解析、字符转换等任何加工,纯粹是网络传输过来的原生二进制数据,数据拷贝完成之后内核缓冲区对应数据会被系统清理,随后程序把临时 buffer 里的数据追加写入继承自 Connection 基类的应用层字符串_inbuffer,内核缓冲区和 _inbuffer 属于两块完全隔离的内存,内核缓冲区负责操作系统层面临时缓存网络数据,_inbuffer 负责在应用层拼接分段报文,以此解决 TCP 无边界字节流带来的粘包、半包问题,整套读取逻辑全部封装在 Recver 的 while 循环之内,每次 EPOLLIN 读事件触发就会完整执行这套读取流程,直至内核接收缓冲区不存在可读数据才退出循环。

2. 在 Recver 函数写入应答数据到_outbuffer 之后,代码判断缓冲区非空就通过 R 指针调用 EnableReadWrite 开启写事件,但这处代码看不到 send 发送逻辑,同时 Sender 函数末尾判断_outbuffer 仍有残留数据时同样调用 EnableReadWrite,这里也没有 send 函数调用,也就是说我们不清楚 send 发送代码到底在什么位置执行。

这里核心要区分事件注册阶段和事件处理阶段两个独立的执行阶段,调用 EnableReadWrite 修改 epoll 监听掩码仅仅是事件注册操作,全程不会执行任何 send 数据发送逻辑,真正的 send 循环发送代码全部封装在 Sender 重写函数中,只有内核触发 EPOLLOUT 可写就绪事件时,Sender 函数才会被调度执行。我们再梳理一下逻辑 : Recver 读取完内核全部网络数据后调用_on_message 回调函数,回调返回组装完成的应答报文,程序将报文追加写入_outbuffer 应用层发送缓冲区,此时应答数据仅仅存放在用户态字符串内存中,完全没有拷贝至操作系统内核,更不会发送到网络链路中,随后代码判断 _outbuffer 不为空,借助反向 R 指针调用 EnableReadWrite 接口,传入参数同时开启读事件与写事件,这一步操作本质是调用 epoll_ctl 修改内核 epoll 实例内该 fd 的事件监听,只是通知内核后续需要监控这个 fd 的可写状态,整个操作不存在 send 系统调用,自然看不到数据发送相关代码。执行完 EnableReadWrite 之后当前 Recver 函数执行完毕并返回,本轮读事件处理流程结束,程序回到 Reactor 的 LoopOnce 函数继续阻塞在 epoll_wait 处等待下一轮 IO 事件,如果此时客户端 fd 对应的内核发送缓冲区存在空闲存储空间,内核就会生成 EPOLLOUT 就绪事件并返回给 epoll_wait,Reactor 拿到就绪事件后进入 Dispatcher 事件分发逻辑,识别到当前 fd 产生可写事件,就会调用 IOManager 重写的 Sender 函数,只有进入 Sender 函数内部,才会启动 while 循环反复调用 send 系统调用,循环将_outbuffer 内缓存的应答数据拷贝到内核发送缓冲区。

3. Sender 函数末尾再次调用 EnableReadWrite,这个操作的目的是不是二次确认数据有没有发送完毕?

不是,这个接口调用不是用来确认数据是否发送完成,缓冲区数据是否残留是靠上层 if 判断_outbuffer.empty () 完成的,EnableReadWrite 只负责修改 epoll 内核里 fd 的监听事件,它没有任何读取、检查缓冲区数据的逻辑,整个操作的核心目的是根据剩余数据状态,更新内核监听规则。如果_outbuffer 还有未发送完毕的数据,说明内核发送缓冲区空间不足、无法一次性写完所有应答报文,程序需要持续监听 EPOLLOUT 可写事件,等待内核缓冲区腾出空位后再次触发 Sender 发送剩余内容,因此调用接口同时保留读和写事件;如果_outbuffer 已经全部清空,不存在待发送数据,写事件就失去了监听价值,调用接口关闭 EPOLLOUT,仅保留业务永久需要的 EPOLLIN 读事件,全程只是调整内核监控规则,不存在任何数据读取校验的逻辑,确认数据是否发完的动作完全由外层的字符串判空语句完成,二者职责完全拆分,不能混淆。

4. Recver 函数写入应答数据到_outbuffer 后,调用 EnableReadWrite 同时开启读事件和写事件,为什么不能只保留读事件、单独新增写事件,而是同时开启两类事件呢?

我们首先要明确客户端连接的基础业务需求,只要 TCP 连接没有断开,程序就必须永久监听 EPOLLIN 读事件,客户端随时会下发新的请求报文,读事件是连接的常驻监听事件,任何修改事件掩码的操作都绝对不能关闭读事件。而 EnableReadWrite 函数的设计入参是两个布尔值,分别控制读事件是否开启、写事件是否开启,接口内部会根据两个参数组合生成全新的 epoll 事件掩码,再调用 epoll_ctl 同步更新到内核,这个函数没有 "仅追加某一类事件、保留原有事件不变" 的增量修改逻辑,它是全量覆盖式更新 fd 的监听事件。在 Recver 场景下,我们的诉求很清晰:读事件维持开启状态不变,额外新增开启写事件,放到这个全量更新的接口里,就必须显式传入 readable=true 来保证读事件不会被关闭,同时传入 writeable=true 开启写事件,最终生成同时包含 EPOLLIN 和 EPOLLOUT 的事件同步给内核,看起来是同时修改两类事件,本质只是固定保留常驻的读事件、按需打开临时写事件。我们可以反向推演,如果此处只传入 writeable=true、readable=false,接口会生成只包含 EPOLLOUT 的事件同步内核,内核会直接取消这条 fd 的读事件监听,客户端后续再发送新请求报文时,epoll 不会触发任何读就绪事件,程序完全接收不到新数据,直接导致整条连接彻底丧失接收请求的能力,引发严重业务 bug,所以必须显式把读事件的开关参数置为 true,确保常驻读事件持续生效。同时补充对比 Sender 函数末尾的调用逻辑,当 _outbuffer 数据全部发送干净时,我们传入 readable=true、writeable=false,依旧维持读事件开启,仅关闭临时写事件,整套接口调用逻辑始终坚守 "读事件永久开启、写事件按需启停" 的核心规则,只是因为接口是全量更新掩码的设计,每次调整写事件时都需要手动声明读事件保持开启,才会出现两个参数都传入 true 或 false 的写法,并不是程序需要主动重新开启读事件,而是接口设计约束下的必要传参操作。

5. 如果当下这个客户端 fd 对应的应用层 _outbuffer 还剩数据没发完,原因是内核发送缓冲区已满,此时代码要持续监听 EPOLLOUT 写就绪事件,这里为什么 epoll 监听的是我们本地 fd 的写事件,数据应该是发给对端客户端,不应当是对方主动从我们的发送缓冲区读取数据,不应该是对端监视写事件吗? (重要)

我们首先要纠正一个核心认知偏差,TCP 的内核发送缓冲区、内核接收缓冲区是本地操作系统 为当前 fd 分配的两块内存,完全归属服务端进程所在的机器,和对端客户端机器没有直接读写关系,不存在客户端主动读取我们本机内核发送缓冲区的行为。我们调用 send 系统调用的行为,本质是把应用层 _outbuffer 里的字节拷贝到本机内核发送缓冲区,操作系统内核 TCP 协议栈会自主把内核发送缓冲区的数据封装 TCP 报文,通过网卡发送到对端客户端;反过来客户端收到报文后,数据存入客户端机器的内核接收缓冲区,客户端程序调用 recv 读取数据,整个收发的内存操作完全在两台机器各自的内核中独立完成。

接着解释 EPOLLOUT 本地 fd 可写事件的真实定义,这个事件和对端客户端没有任何关联,它代表的含义是:当前本机 fd 对应的内核发送缓冲区还有空闲内存空间,应用程序现在调用 send 系统调用,能够成功把一部分数据拷贝进内核发送缓冲区。当我们的应用层_outbuffer 存在大量待发送应答数据,多次 send 之后本机内核发送缓冲区被填满,此时再调用 send 就会返回 EAGAIN/EWOULDBLOCK 错误,代表内核缓冲区没有剩余空间接纳新数据,程序不能继续循环调用 send,只能暂停发送逻辑,等待内核发送缓冲区腾出空位。那内核发送缓冲区什么时候会出现空闲?内核会持续把缓冲区里已经缓存的 TCP 报文发送给对端客户端,随着报文不断被网卡发出,内核发送缓冲区内部就会逐步释放出空闲内存,一旦出现空闲位置,操作系统就会给 epoll 触发当前 fd 的 EPOLLOUT 就绪事件,通知我们应用程序现在可以再次调用 send,继续拷贝_outbuffer 里残留的数据到内核缓冲区,这就是我们需要持续监听本地 fd 写事件的根本原因。

再结合上面场景里的代码,应用层_outbuffer 存有未发送数据、内核发送缓冲区写满后,此时我们调用 EnableReadWrite 同时开启读、写事件,持续监听本地 fd 的 EPOLLOUT 事件,程序退出 Sender 函数回到 Reactor 事件循环阻塞等待 epoll_wait。后续内核不断向外发送报文,内核发送缓冲区出现空闲空间,epoll 检测到这个状态变化,返回 EPOLLOUT 就绪事件,Reactor 的 Dispatcher 分发函数再次调用 Sender 函数,函数内部重新开启 while 循环调用 send,把_outbuffer 剩余数据拷贝到内核发送缓冲区,重复这个过程直到_outbuffer 全部清空,之后再调用 EnableReadWrite 关闭写事件,不再监听 EPOLLOUT。

最后区分清楚两个容易混淆的概念,EPOLLIN 读事件代表本机 fd 的内核接收缓冲区有对端发来的新数据,我们可以调用 recv 读取;EPOLLOUT 写事件代表本机 fd 的内核发送缓冲区有空位,我们可以调用 send 写入待发送数据。两个事件都是描述本机操作系统内核缓冲区的状态,全部绑定在本地打开的客户端 fd 上,和对端客户端的读写行为无关,并不是监听对方的读写操作,仅仅监控本地内核缓冲区的空闲 / 有数据状态,这也就解释了当内核发送缓冲区占满、还有数据待发送时,我们需要持续监听本地 fd 自身 EPOLLOUT 写事件的设计逻辑。


6. 确认 epoll 等待检测的读事件和写事件,判断依据是否为本机当前 fd 对应的内核接收缓冲区状态吗?

是的,epoll 的 EPOLLIN 可读事件只监测本机操作系统为该套接字 fd 分配的内核接收缓冲区,和对端客户端设备及对方的套接字缓冲区没有任何关联。当客户端通过网络发送 TCP 报文抵达本机网卡,内核 TCP 协议栈会把报文数据存入这条 fd 专属的内核接收缓冲区,只要这块缓冲区里存在未被应用程序 recv 读取的字节数据,内核就会标记该 fd 产生可读状态,epoll_wait 就能捕获到 EPOLLIN 事件,通知我们应用程序调用 recv,把内核缓冲区的数据拷贝到应用层的_inbuffer。反过来,如果内核接收缓冲区已经被应用程序读空,没有任何残留字节,即便客户端后续还会发数据,当下也不会触发读事件。同时补充区分配套的 EPOLLOUT 可写事件,它监测的是同一块 fd 对应的本机内核发送缓冲区是否存在空闲空间,二者全部基于本机内核缓冲区状态判定,epoll 不会跨机器感知对端程序的读写动作,仅监控本地进程所持 fd 的内核缓存状态,这也是整套 IOManager 读写逻辑的底层依据。

7. 所以 epoll_wait 的底层逻辑也是以阻塞等待的形式检测内核中注册的各个 fd 中的读写事件吗,直到有事件就绪后才返回?

是的,这个理解是正确的,epoll_wait 默认就是阻塞调用,它的底层执行逻辑完整分为两个阶段,第一阶段是进入阻塞休眠状态,当我们调用 epoll_wait 并传入合法 epollfd,同时没有设置超时、也没有当前已经就绪的 IO 事件时,当前用户态进程会被操作系统内核挂起,让出 CPU 执行权,进程停止在 epoll_wait 这一行代码,不会向下执行后续事件分发逻辑;内核则会持续监控 epoll 实例红黑树内所有注册的 fd,不断检查每一个 fd 对应的本机内核收发缓冲区状态,判断是否产生 EPOLLIN、EPOLLOUT、异常等就绪事件。第二阶段是唤醒进程并返回就绪事件列表,一旦任意一个注册 fd 出现 IO 就绪事件,内核会立刻唤醒被阻塞的进程,将所有当前就绪 fd 的事件信息拷贝到用户态传入的 epoll_event 数组内,epoll_wait 函数随之返回一个整数,代表本次捕获到的就绪事件数量,程序继续向下执行,由 Reactor 的 Dispatcher 函数遍历就绪事件、调用对应 Connection 的读写处理函数。同时补充超时参数的补充说明,如果调用 epoll_wait 时传入大于 0 的超时毫秒数值,函数只会阻塞对应时长,超时后哪怕没有任何就绪事件也会返回 0;如果传入 - 1 则是永久阻塞,也就是我们框架 Reactor 主循环默认使用的方式,全程依靠 epoll_wait 阻塞等待 IO 事件,避免程序空转消耗 CPU 资源,这也是 IO 多路复用核心的性能优势。

8. 那也就是程序里所有 IO 阻塞等待的耗时都全部集中在 epoll_wait 函数了吗?

是的,整个单 Reactor 框架里,进程主动让出 CPU、陷入阻塞等待 IO 的全部时间确实都消耗在 epoll_wait 调用上,框架其余所有代码逻辑全是非阻塞运行,不会产生任何的阻塞休眠。我们这套框架所有套接字全部设置为非阻塞模式,recv、send、accept 这些系统调用即便内核缓冲区无数据、无空间,也会立刻返回 EAGAIN 错误码直接退出函数,不会阻塞当前线程;Reactor 里循环遍历就绪事件、调用 IOManager 或 Listener 的读写函数、处理业务回调、管理连接哈希表、检查空闲超时连接的全部逻辑,都是拿到就绪事件之后同步执行的计算逻辑,执行完成后程序会再次回到 epoll_wait 函数发起新一轮阻塞等待。整段程序的运行模型就是 epoll_wait 长时间阻塞休眠,一旦有事件就绪就短暂唤醒执行一轮业务处理,处理完毕立刻重回阻塞,因此进程所有闲置等待的时间全部由 epoll_wait 承担,不会分散在其他函数中。

9. epoll_wait 同一轮调用可以一次性返回多个就绪 fd 事件,那为什么一检查到有事件就绪就返回? 整个过程看起来像是原子性的,为什么还会返回多个就绪的事件个数?

epoll_wait 的设计本身就支持单次调用返回一批多个就绪事件,内核会把当前已经触发就绪、存放在就绪链表内的全部 fd 事件,一次性拷贝到用户态的事件数组中返回,返回值代表本轮就绪事件总数量,理论上并发量越高,单次返回多个事件的情况就会越频繁。我们产生多数情况只拿到单个事件的感受,来自普通测试环境的低并发场景,本地自测时一般只会开启一个客户端连接持续收发请求,同一时间最多只有这一条连接产生读或者写事件,自然每次 epoll_wait 只会返回一条就绪记录;但放到线上高并发环境,成百上千客户端同时发送请求、多条连接的内核发送缓冲区同时腾出空闲,就会出现一轮 epoll_wait 返回数十条就绪事件的情况,Reactor 的代码本身也做好了批量处理的设计,会循环遍历 epoll_wait 输出的事件数组,逐个分发处理每一条就绪 fd,不会限制单次只能处理一个事件,只是低并发场景掩盖了批量返回的特性。

总结

IOManager 类是整套单 Reactor ET 边缘触发网络框架的核心业务载体,实现了 ET 模式,同时解决了 TCP 流式传输天然存在的粘包、半包问题,依靠分层回调实现网络 IO 逻辑与上层协议、业务计算完全解耦,兼具高性能、高拓展性、高可读性三大优势。

从 ET 规范落地层面来看,读、写两套逻辑全部使用 while 循环持续调用 recv、send 系统调用,直到内核缓冲区无数据可读、无空间可写,从代码层面杜绝 ET 模式下数据残留、事件丢失的经典 bug;同时采用按需注册写事件的优化方案,仅当_outbuffer 存在待发送数据时才开启 EPOLLOUT 监听,数据全部发送完毕立刻关闭写事件,大幅减少 epoll 内核无效事件通知,充分发挥 ET 边缘触发相比 LT 水平触发的性能优势。

从分层解耦设计层面来看,IOManager 内部只负责套接字底层读写、应用层缓冲区管理、epoll 事件动态调整三类纯粹网络相关逻辑,上层 JSON 协议拆包、数据计算逻辑全部通过外部传入的 on_message_t 回调函数实现,网络层不用感知业务细节,后续更换通信协议、新增业务功能只需要修改回调内部逻辑,无需改动 IOManager 核心读写代码,拓展性极强;同时完整复用 Connection 基类所有通用资源、标准化接口,依托 C++ 多态机制和 Listener 监听套接字共用同一套 Reactor 事件分发逻辑,Reactor 不需要区分监听 fd 与客户端 fd,极大简化了调度中枢的代码复杂度。

从资源管理与健壮性层面来看,完整封装套接字读写的全部异常分支,系统调用被信号中断、内核缓冲区满、客户端正常断开、致命读写错误等全部场景都做了细分处理,异常场景统一调用 Excepter 函数完成连接自动销毁,不会出现僵尸连接长期占用系统文件描述符;同时每次成功读写数据都会刷新连接活跃时间戳,配合 Reactor 的空闲超时检测逻辑,自动回收长时间无通信的闲置客户端连接,避免大量空闲连接耗尽系统 fd 资源,线上运行稳定性极强。

讲完客户端数据核心处理子类 IOManager.hpp 之后,下一节我们就开始讲解自定义通信协议文件 Protocol.hpp,这份文件专门实现长度头 + JSON 的私有分包协议,完整解决 TCP 粘包半包、JSON 报文序列化与反序列化的全部逻辑。

Protocol.hpp

前面我们已经讲解完完网络 IO 层的所有代码,下面讲一下 Protocol.hpp 文件, Protocol.hpp 是整套框架的业务协议层,专门用来解决 TCP 粘包半包问题、完成 JSON 报文序列化与反序列化,同时隔离网络读写逻辑和上层业务计算,因为 Protocol.hpp 文件我们之前在Socket 套接字编程的文章中已经讲过了,这里我们只是简单的再回顾一下。

头文件说明

文件开头采用 #pragma once 作为头文件保护,引入了标准字符串、jsoncpp 库、函数模板与日志头文件,整体分成 Request 请求报文类、Response 应答报文类、Protocol 协议处理类三大部分。Request 类封装客户端发来运算请求的三个核心数据,两个运算数字和一个操作符,内部提供 Serialize 序列化方法把对象转成 JSON 字符串,Deserialize 反序列化方法把收到的 JSON 字符串还原成 Request 对象;Response 类则封装服务端运算结果和业务状态码,同样配套序列化、反序列化两套方法,用来把运算结果打包成 JSON 发给客户端,两个报文类把业务数据和 JSON 转换逻辑收拢在一起,不用在业务代码里零散处理 JSON 转换。
文件里定义了全局分隔符 gsep 也就是\r\n,用来作为报文头尾标记,还声明了两种函数回调类型,分别是处理请求报文的业务回调、处理应答报文的回调,作用是把实际的加减乘除运算业务逻辑从协议类里剥离出去,协议层只负责报文解析打包,业务逻辑由外部传入的回调实现,实现分层解耦。

Protocol 协议类

核心的 Protocol 协议类维护版本号和两个业务回调作为私有成员,对外提供 Packet 打包、Unpack 解包、ParseRequest 解析请求缓冲区、ParseResponse 解析应答缓冲区几个核心接口。Packet 打包函数接收纯 JSON 字符串,在报文前后拼接长度头和分隔符,生成符合私有协议的完整网络报文,用来发送数据;Unpack 解包函数接收 IOManager 传过来的应用层 _inbuffer 字符串,依靠分隔符识别完整报文,提取报文长度校验数据完整性,切分出独立 JSON 字符串,同时截断缓冲区里已经解析完毕的报文数据,处理半包时直接留存剩余数据等待下一轮读事件补齐,以此彻底解决 TCP 流式传输的粘包半包问题。

ParseRequest 函数是服务端的解析入口,循环调用 Unpack 解包函数从接收缓冲区提取完整 JSON 报文,再把 JSON 字符串反序列化为 Request 请求对象,调用外部传入的业务回调执行数学运算,运算完成后生成 Response 应答对象,序列化应答 JSON 并调用 Packet 打包成网络报文返回给 IOManager 写入发送缓冲区;ParseResponse 则是客户端侧的解析逻辑,流程和服务端解析请求大体一致,提取 JSON 后转成 Response 对象,调用应答回调处理服务端返回的运算结果。

整体来看这份协议文件承担两层核心工作,第一层是自定义长度头 + 分隔符的私有分包协议,处理 TCP 天生的粘包半包缺陷,保证每次都能取出完整业务报文;第二层是封装 JSON 序列化反序列化、业务回调调度,把网络 IO、报文解析、业务计算三层逻辑彻底分开,IOManager 只负责读写字节流,Protocol 专注报文处理,实际运算逻辑交给外部回调,整套分层设计降低了代码耦合,后续修改协议格式、更换业务计算逻辑时,只需要改动协议类内部打包解包规则或者替换外部回调,不用修改底层网络读写代码。

相关问题

Protocol 协议属于应用层协议,是我们在应用层自定义的协议、下面我们对比它和 HTTP 应用层协议的异同、以及TCP 层到我们应用协议层的字节数据流、序列化反序列化、封包解包的关系。

  1. 首先明确 Protocol.hpp 里实现的代码逻辑,完完全全属于我们自行设计的应用层协议,是专门适配当前计算器业务场景定制的私有应用层协议,和 HTTP 协议一样都工作在 TCP 传输层之上,二者属于同一层级的协议,核心定位没有区别,都是基于 TCP 字节流、用来规范客户端与服务端之间业务数据传输规则的应用层标准。二者的区别在于业务场景与内置能力,HTTP 是通用公开标准协议,设计目标是承载网页资源的请求与响应,浏览器、各类 web 服务都统一遵循这套规则,HTTP 协议内部原生自带固定的请求行、请求头、请求体格式,协议本身已经内置了完整的报文边界识别、数据分段、内容序列化相关的规范,开发人员使用 HTTP 时不需要手动实现长度头、分隔符拆包,也不用自己封装 JSON 转换逻辑,HTTP 协议本身的头部信息就会标记请求体长度,天然解决 TCP 粘包问题;而我们这份计算器私有协议是业务专用的轻量化协议,没有通用标准可以复用,所以 TCP 流的分包规则、JSON 报文的序列化与反序列化、完整报文的提取拆包逻辑,全部都需要我们手动在 Protocol 类里编码实现,本质工作和 HTTP 协议底层自带的处理逻辑是对等的,只是 HTTP 由标准协议实现,我们是手写代码实现。

  2. 再梳理整个网络分层的数据流转流程,数据从 TCP 传输层抵达我们的应用程序之后,中间不存在其他现成应用层协议做中转,TCP 层直接对接我们手写的这套自定义应用层协议,整条链路就是网卡内核 TCP 缓冲区、recv 拷贝到 IOManager 应用层_inbuffer、再交给 Protocol 协议类处理,不存在额外协议层介入。我们调用 recv 系统调用从内核 TCP 接收缓冲区拷贝出来的数据,纯粹是未经任何加工的原始二进制字节流,TCP 协议本身只负责可靠传输字节流,完全不识别业务层面的请求、应答结构,也不会区分哪一段字节属于一条完整计算器请求,这就带来两个必须由我们自定义应用层协议解决的核心问题,分别是字节流的边界拆分、业务数据的格式转换。

  3. 第一个问题是 TCP 无边界字节流导致的粘包、半包现象,TCP 仅仅是连续的字节数据流协议,操作系统不会帮我们划分一条完整业务报文的起止位置,一次 recv 读取到的字节可能包含多条完整请求、也可能只读到半条请求,所以我们必须手动实现封包与解包逻辑,也就是 Protocol 里的 Packet 打包函数和 Unpack 解包函数,通过长度头加 \r\n 分隔符的私有规则标记每条报文的起止,读取缓冲区的时候循环拆分完整报文,把残缺的半包数据留在缓冲区等待下一轮数据补齐,这一步等同于 HTTP 依靠请求头 Content-Length 标记请求体长度的工作,只是我们自行编码实现。

  4. 第二个问题是原始二进制字节流无法直接转换成程序内部的 Request、Response 业务对象,内核缓冲区读出的字节只是字符串形式的二进制数据,程序无法直接识别哪一段是左操作数、哪一段是运算符,因此需要序列化与反序列化逻辑,序列化就是服务端把运算结果 Response 对象转换成 JSON 字符串、再交给 Packet 封包发送,反序列化就是客户端发来的 JSON 字节流拆包完成后,转换成代码里的 Request 请求对象,让程序能够读取里面的数字和运算符;对应到 HTTP 场景,HTTP 传输 JSON 数据时也需要业务代码完成 JSON 序列化与反序列化,HTTP 协议本身只会负责传输字符串,不会自动完成对象和字符串的转换,这一块工作无论使用 HTTP 还是我们私有协议,都需要业务代码手动实现,区别只在于粘包拆包的边界处理逻辑,HTTP 原生内置,我们的私有协议需要自行编码。

总结

我们手写的这套计算器协议属于 TCP 之上的自定义应用层协议,层级和 HTTP 对等,仅适配计算器运算业务;TCP 层给到应用程序的只有原始无边界二进制字节流,TCP 本身不处理报文边界、不识别业务数据结构,因此我们的应用层协议必须完成两套核心工作,第一套是封包、解包用来解决 TCP 粘包半包,划分完整业务报文边界,对应 HTTP 头部长度字段的作用,第二套是序列化、反序列化用来在业务对象和网络传输字符串之间互相转换,让程序能够解析、生成业务数据,这两块逻辑 HTTP 协议一部分内置、一部分需要业务代码补充,而我们轻量化私有协议全部由自己手动编码实现,TCP 传输层和我们自定义应用层协议之间没有其他中间应用层协议,数据经过 recv 读取后直接送入 Protocol 协议类处理。

Calculator.hpp

Calculator.hpp 文件是整套框架最顶层的纯业务计算模块,仅依赖 Protocol 里定义的 Request 请求类和 Response 应答类,不涉及任何网络读写、报文解析相关逻辑,实现了网络层和业务计算层的彻底解耦。

整个文件只包含一个 Calculator 类,对外仅暴露 Execute 这一个核心成员函数,函数接收一个只读的 Request 引用作为入参,这个 Request 对象已经经过协议层反序列化,内部存好了两个运算数字和运算符。函数内部通过 switch 分支匹配运算符,分别实现加、减、乘、除、取模五类运算,同时对除零、取模零、非法运算符这三类异常场景设置对应的错误状态码,正常运算则算出结果存入 Response 对象,最终封装好的 Response 应答对象直接返回给上层协议回调。

整体来看这个模块职责高度单一,只专注数学运算与运算异常判定,不用关心数据是怎么通过网络接收、怎么拆包解析出来的,上层 Protocol 协议类只需要把解析完成的请求对象传给 Execute,拿到应答结果后再完成序列化封包发送,后续如果要新增运算类型、修改异常判定规则,只需要改动这个文件内部的 switch 逻辑,底层网络、协议代码完全不用调整。

Main.cc

Main.cc 是整个计算器网络服务程序的程序入口,所有我们前面拆解过的 Calculator 业务模块、Protocol 协议模块、Listener 监听连接、IOManager 客户端连接、Reactor 事件调度、Epoller 底层 epoll 封装全部在这里完成实例化与关联绑定,同时完成两层核心回调函数的注册,最终启动事件循环持续处理网络 IO,我们顺着 main 函数代码从上到下逐段梳理,同步关联每一处代码对应的前置模块,完整还原程序从启动初始化、资源绑定、注册监听、持续运行的全部流程。

头文件说明

程序最先执行的是命令行参数校验逻辑,代码读取程序运行时传入的参数列表,强制要求运行程序时必须携带一个端口数字作为启动参数,如果参数数量不等于 2,程序会打印使用提示并直接返回 1 终止进程,这一步是服务启动的前置校验,用来确定当前服务要绑定监听的端口号,校验通过后将传入的字符串端口转换为 uint16_t 数值,供给后续 Listener 监听套接字使用。

紧接着执行 ENABLE_CONSOLE_LOG_STRATEGY 全局日志初始化函数,激活项目内统一的日志打印工具,我们前面所有模块里用到的 LOG 日志打印接口,都会依托这个初始化配置把调试、信息、警告日志输出到控制台,整个项目的日志系统统一在这里完成启动,后续 Listener、IOManager、Protocol、Calculator 里所有日志打印都可以正常生效。

1. 数据计算业务模块

下来进入第一层业务模块实例化,代码创建 Calculator 计算器业务对象的 unique 智能指针,Calculator 是整套框架最上层的业务计算层,内部只提供 Execute 运算函数,负责接收请求对象完成加减乘除取模运算、处理各类运算异常并返回带状态码的应答对象,这个对象会作为业务逻辑载体,传递给下层 Protocol 协议模块,成为协议解析完成后的运算回调执行体。

2. Protocol 协议层模块

随后创建 Protocol 协议层对象,实例化 Protocol 的时候直接传入一个 lambda 匿名回调函数,这是第一层回调注册,对应 Protocol 类定义的 HandlerRequest_t 请求处理回调类型,这个 lambda 内部捕获外部的 Calculator 智能指针,当 Protocol 解析出一条完整 Request 请求对象时,会自动执行这个回调,调用 cal->Execute 传入请求对象,拿到运算完成后的 Response 应答对象,协议层完全不用关心底层数学运算如何实现,仅依靠注册好的回调完成业务对接,完美实现协议层与业务计算层解耦,这一段代码把 Calculator 业务实例和 Protocol 协议实例牢牢绑定,完成业务回调的注册工作。

3. 实例化 Listener 监听连接对象

完成协议对象创建后,程序开始实例化 Listener 监听连接对象,Listener 的构造函数需要传入两个参数,第一个参数是另一层 lambda 回调函数,第二个参数是前面解析出来的监听端口,这个 lambda 回调对应 Listener 构造函数要求的报文处理回调类型,lambda 内部捕获已经创建好的 Protocol 协议智能指针,当 Listener 的 Recver 函数通过 accept 拿到新客户端 fd、创建 IOManager 连接对象时,会把这个 lambda 回调传递给 IOManager;后续 IOManager 的 Recver 函数读完内核全部网络字节流、存入_inbuffer 之后,就会执行这份回调,调用 protocol->ParseRequest 传入应用层接收缓冲区,完成 TCP 粘包半包解包、JSON 反序列化、业务回调调度、应答报文序列化封包全套协议逻辑,这一步完成了协议模块与网络 IO 模块之间的回调注册,打通了客户端网络数据读取到协议解析的完整链路,Listener 实例会被存储为 Connection 基类的 shared 智能指针,依托 C++ 向上转型,和后续生成的 IOManager 客户端连接共用同一套 Reactor 多态分发逻辑。

4. 创建 reactor 反应堆

之后创建全局唯一的 Reactor 调度器智能指针,Reactor 内部会自动实例化 Epoller 对象,底层调用 epoll_create 生成操作系统内核 epoll 实例,也就是我们之前反复讲解的 IO 多路复用核心组件,Reactor 作为整套程序唯一的 EventLoop 事件循环,统一持有 epoll 实例、存储所有存活连接的哈希容器、封装事件分发、空闲连接超时检测等全部调度逻辑,所有网络事件的等待、分发都会由这个 Reactor 实例全权处理,它是连接底层 epoll 内核和上层 Listener、IOManager 连接对象的中间调度核心。

5. 把连接添加到TcpServer中

执行 R->AddConnection (listener) 语句,将刚刚创建好的 Listener 监听连接对象注册进 Reactor,这个函数内部会执行三件事,第一件调用 Epoller 的 AddEvents 底层 epoll_ctl 接口,把 listen fd 连同 EPOLLIN | EPOLLET 读事件注册到内核 epoll 红黑树中,让内核持续监控端口的新连接请求;第二件将 Listener 的 shared 智能指针存入 Reactor 内部_connections 哈希容器,以 listen fd 作为键值保存,方便后续 epoll_wait 返回就绪 fd 时快速匹配对应的连接对象;第三件完成 Listener 内部 Reactor 反向指针 R 的赋值,让监听连接后续拿到新客户端 fd 时,可以直接调用 Reactor 接口新增 IOManager 连接,至此监听套接字正式交给 epoll 内核监控,服务具备接收客户端 TCP 握手连接的基础能力。

6. 开启 Loop 循环

最后调用 R->Loop () 启动 Reactor 永久事件循环,这是程序正式进入运行状态的核心步骤,Loop 函数内部开启无限 while 循环,循环内部首先执行 LoopOnce 单次事件处理逻辑,LoopOnce 里会调用 Epoller 的 WaitEvents 也就是 epoll_wait 阻塞等待内核返回就绪 fd 事件列表,进程绝大多数闲置时间都会阻塞在 epoll_wait 调用上,没有 IO 事件时不会空转消耗 CPU;一旦有就绪事件,epoll_wait 返回就绪事件数量,Reactor 的 Dispatcher 分发函数会遍历事件数组,通过 fd 从_connections 哈希容器取出对应的 Connection 子类对象,依靠多态自动调用 Listener 的 Recver 或者 IOManager 的 Recver、Sender、Excepter 重写函数。

我们完整走一遍事件触发后的联动流程来串联所有模块:如果是 listen fd 读事件就绪,Dispatcher 调用 Listener::Recver,函数内循环 accept 获取新客户端 fd,构造 IOManager 智能指针、绑定客户端地址、传递预先注册好的协议解析回调,调用 R->AddConnection 把 IOManager 注册进 Reactor 与 epoll 内核;如果是客户端 fd 读事件就绪,Dispatcher 调用 IOManager::Recver,while 循环调用 recv 读取内核全部原始字节流存入_inbuffer,随后执行构造时注册的 Protocol 解析回调,调用 protocol->ParseRequest 循环解包缓冲区里的 TCP 字节流,拆分出完整 JSON 报文,反序列化为 Request 对象,触发 Protocol 构造时注册的 Calculator 业务回调,执行数学运算拿到 Response 应答对象,序列化应答 JSON、调用 Packet 函数添加长度头与分隔符生成完整网络报文,写入 IOManager 的_outbuffer 发送缓冲区,判断缓冲区非空后调用 R->EnableReadWrite 开启当前 fd 的 EPOLLOUT 写事件监听,回到 Reactor 事件循环等待下一次 IO 事件;当内核发送缓冲区出现空闲空间,触发客户端 fd 写就绪事件,Dispatcher 调用 IOManager::Sender,while 循环调用 send 把_outbuffer 内应答数据拷贝至内核发送缓冲区,缓冲区清空则关闭写事件监听;如果出现套接字读写错误、客户端断开连接,会调用 Excepter 函数,执行 R->DelConnection 销毁整条客户端连接,从 epoll 内核、Reactor 连接容器中移除资源。

整个程序在 R->Loop () 内部持续循环运行,不会向下执行 return 0,只有进程收到终止信号、主动关闭服务时才会跳出循环执行 return 0 结束进程,整套运行流程严格分层,业务层 Calculator 只做运算、协议层 Protocol 只处理报文序列化与粘包解包、网络连接层 Listener 与 IOManager 只负责底层 recv/send/accept IO 读写、调度层 Reactor 统一管理 epoll 与事件分发,两层 lambda 回调分别完成业务层与协议层、协议层与网络 IO 层的解耦绑定,所有模块的实例化、回调注册、内核 fd 注册、事件循环启动全部集中在 Main.cc 入口文件,完整串联起我们之前逐份拆解的所有代码文件。

模型总结

最底层是操作系统 OS,它为整个服务提供内核级的 TCP/IP 协议栈、套接字文件描述符、epoll 实例、内核收发缓冲区等基础能力,所有网络数据的收发、事件就绪通知都由操作系统内核负责完成,是整个服务运行的底层支撑。Logger、Mutex、Socket 这些通用工具模块直接依赖操作系统提供的系统调用,Logger 负责控制台日志输出,Mutex 提供线程同步能力(这里单线程场景更多是预留扩展),Socket 封装了套接字创建、绑定、监听、accept 等基础操作,是 Listener 和 IOManager 使用的底层工具类。

往上一层是 Reactor 调度中枢,它是整套模型的核心事件循环,内部持有 Epoller 实例 (epoll 封装),通过 epoll_wait 阻塞等待操作系统返回就绪事件,同时用哈希容器统一管理所有 Connection 子类对象,包括 Listener 监听连接和 IOManager 客户端连接。Reactor 负责事件分发、连接生命周期管理、空闲超时回收,它不处理具体的读写和业务逻辑,只做调度和分发,把操作系统通知的就绪事件,转发给对应的连接对象去处理。

再往上是 Connection 抽象基类和它的两个子类:Listener 和 IOManager。Connection 是所有连接的统一抽象,定义了 Recver、Sender、Excepter 三个纯虚接口,以及收发缓冲区、客户端地址、活跃时间等通用资源,让 Reactor 可以用同一套逻辑调度不同类型的连接。Listener 是监听套接字的实现,只处理 EPOLLIN 读事件,收到新连接后会创建 IOManager 对象并交给 Reactor 管理,它的 Recver 函数里循环调用 accept 获取客户端 fd,完成三次握手后的新连接封装;IOManager 是客户端数据连接的实现,负责处理客户端的读写事件,Recver 里用 while 循环 recv 读取内核数据存入_inbuffer,Sender 里用 while 循环 send 把_outbuffer 数据拷贝到内核发送缓冲区,同时通过 Reactor 的 EnableReadWrite 按需开关写事件,Excepter 负责异常时销毁连接。

再往上是 Protocol 协议层,它和 IOManager 的 Recver 函数通过回调绑定在一起。IOManager 读完内核数据存入_inbuffer 后,会调用 Protocol 的 ParseRequest 方法,依靠自定义的长度头 + 分隔符协议完成 TCP 粘包半包的解包,拆分出完整的 JSON 报文,再反序列化为 Request 请求对象;处理完业务后,再把 Response 应答对象序列化为 JSON 字符串,通过 Packet 方法加上长度头和分隔符,封包后写入 IOManager 的_outbuffer,协议层只负责报文的序列化、反序列化、解包、封包,不关心业务逻辑本身。

最顶层是 Calculator 业务层,它通过回调和 Protocol 绑定在一起。Protocol 解析出 Request 对象后,会调用 Calculator 的 Execute 方法,完成加减乘除取模运算,处理除零、非法运算符等异常,返回带状态码的 Response 应答对象,业务层只专注数学运算,完全不感知网络和协议细节。

整个模型的分层是严格解耦的:OS 提供内核能力,Reactor 做事件调度,Listener/IOManager 做网络 IO 读写,Protocol 做报文处理,Calculator 做业务计算,每一层只依赖下一层提供的接口,不关心下一层的实现细节,后续修改任何一层的逻辑,都不会影响其他层的代码。

运行结果:

各个阶段的运行结果:

每隔2秒timeout一次
talnet的一瞬间监听套接字就收到事件就绪了

因为listen套接字一旦就绪了我就处理了,所以就只通知一次,那为什么IOManager就一直死循环打印,因为当我们发了nihao之后底层缓冲区有数据了,可是我们当前并没有处理这个数据,数据一直在缓冲区里存在,所以就死循环打印,这就是LT模式。
LT改为ET:

可是此时我们并没有处理事件,因为ET模式只通知一次,不管你处没处理,就像故事中的李四,快递送到了只通知一次,不管你取没取,不取我也不管了。

每个文件描述符都有自己的缓冲区 我们的服务器已经有能力把网络中收到的数据保存在该文件描述符匹配的缓冲区中 只不过现在我们还没处理

最终运行结果:

这组运行日志完整展示了整套计算器网络服务从客户端连接、报文解析到运算返回的全链路流程,是服务端和客户端严格遵循自定义协议交互的成功案例。服务端启动后,Reactor 先通过 epoll_create 初始化内核 epoll 实例,接着创建 Listener 监听套接字并注册到 epoll,持续监听 8080 端口的连接请求。客户端运行专用 netcal_client 程序,连接 127.0.0.1:8080 后,服务端的 Listener 收到新连接事件,通过 accept 获取客户端 fd 并创建 IOManager 对象,同时将客户端地址信息绑定到连接对象中,注册到 Reactor 的事件循环中。

客户端依次输入运算参数时,会将请求数据按自定义协议封装为带长度头和 \r\n 分隔符的 JSON 报文发送给服务端。服务端的 IOManager 收到读事件后,通过 recv 循环读取内核缓冲区的原始字节流存入_inbuffer,再调用 Protocol 的 ParseRequest 函数,依靠长度头和分隔符完成粘包半包解包,提取出完整的 JSON 报文,反序列化为 Request 请求对象。随后,服务端调用 Calculator 的 Execute 方法,根据请求中的运算符执行加减乘除运算,并处理除零、非法运算符等异常,生成带状态码的 Response 应答对象。应答对象再被序列化为 JSON 字符串,通过 Packet 函数加上长度头和分隔符封装成完整报文,写入 IOManager 的_outbuffer 发送缓冲区,服务端通过 Reactor 动态开启写事件监听,等待内核发送缓冲区有空闲空间时,调用 send 将应答数据发送回客户端,发送完毕后自动关闭写事件监听。

客户端收到应答报文后,解析 JSON 并打印运算结果,整个过程中服务端的日志清晰记录了 epoll 事件就绪、Listener 接收新连接、IOManager 读写事件分发、Protocol 解包封包、Calculator 业务运算的每一步流程,验证了从网络 IO、协议解析到业务计算的分层逻辑均正常工作,同时专用客户端严格遵循协议格式,避免了 telnet 裸数据发送导致的解析异常和进程崩溃,完整实现了基于自定义应用层协议的计算器网络服务功能。

完整代码:

Epoller.hpp

cpp 复制代码
#ifndef __EPOLLER_HPP
#define __EPOLLER_HPP

#include <iostream>
#include <cstdlib>
#include <sys/epoll.h>

#include "Comm.hpp"
#include "Logger.hpp"

using namespace NS_LOG_MODULE;


// #define READ_EVENT EPOLLIN

class Epoller
{
private:
    // User -> kernel
    bool EpollCtl(int sockfd, int oper, uint32_t events)
    {
        if (oper == EPOLL_CTL_DEL)
        {
            int n = epoll_ctl(_epfd, oper, sockfd, nullptr);
            (void)n;
            (void)events;
        }
        else
        {
            struct epoll_event ev;
            ev.events = events;
            ev.data.fd = sockfd;
            int m = epoll_ctl(_epfd, oper, sockfd, &ev);
            (void)m;
        }
        return true;
    }

public:
    Epoller()
    {
        _epfd = epoll_create(256);
        if (_epfd < 0)
        {
            LOG(LogLevel::FATAL) << "epoll_create error";
            exit(EXIT_CODE::EPOLLER_ERROR);
        }
        LOG(LogLevel::INFO) << "epoll_create success, epfd: " << _epfd;
    }
    // Kernel -> User
    int WaitEvents(struct epoll_event revs[], int event_num, int timeout = 1000)
    {
        return epoll_wait(_epfd, revs, event_num, timeout);
    }
    bool AddEvents(int sockfd, uint32_t events)
    {
        return EpollCtl(sockfd, EPOLL_CTL_ADD, events);
    }
    bool ModEvents(int sockfd, uint32_t events)
    {
        return EpollCtl(sockfd, EPOLL_CTL_MOD, events);
    }
    bool DelEvents(int sockfd)
    {
        return EpollCtl(sockfd, EPOLL_CTL_DEL, 0);
    }

    ~Epoller()
    {
        if (_epfd > 0)
            close(_epfd);
    }

private:
    int _epfd;
};

#endif

// class BasePoller
// {
// public:
//     virtual ~BasePoller() = default;
//     virtual void Wait() = 0;
//     virtual void Ctl() = 0;
// };

// class Epoller : public BasePoller
// {

// };

// class Selecter: public BasePoller
// {

// };

// class Poller : public BasePoller
// {

// };

Reactor.hpp

cpp 复制代码
#ifndef __TCPSERVER_HPP
#define __TCPSERVER_HPP

// 未来要做拆分

#include <iostream>
#include <memory>
#include <unordered_map>

#include "Logger.hpp"
#include "Epoller.hpp"
#include "Connection.hpp"

using namespace NS_LOG_MODULE;

static const int grevs_num = 64;

// 一个conn的容器
class Reactor
{
private:
    bool IsLegalConnection(int sockfd)
    {
        return _connections.find(sockfd) != _connections.end();
    }

public:
    Reactor() : _epoller(std::make_unique<Epoller>())
    {
    }

    void Dispatcher(int n)
    {
        for (int i = 0; i < n; i++)
        {
            int sockfd = revs[i].data.fd;
            uint32_t revents = revs[i].events;
            // 统一异常处理
            if (revents & EPOLLERR)
                revents |= (EPOLLIN | EPOLLOUT);
            if (revents & EPOLLHUP)
                revents |= (EPOLLIN | EPOLLOUT);

            if ((revents & EPOLLIN) && IsLegalConnection(sockfd))
                _connections[sockfd]->Recver();
            if ((revents & EPOLLOUT) && IsLegalConnection(sockfd))
                _connections[sockfd]->Sender();
        }
    }
    void LoopOnce(int timeout = 1000)
    {
        int n = _epoller->WaitEvents(revs, grevs_num, timeout);
        switch (n)
        {
        case 0:
            LOG(LogLevel::INFO) << "Time out...";
            break;
        case -1:
            LOG(LogLevel::FATAL) << "Epoll error";
            exit(EXIT_CODE::EPOLLER_WAIT_FATAL);
            break;
        default:
            LOG(LogLevel::INFO) << "event ready...";
            Dispatcher(n);
            break;
        }
    }
    // 一个链接如果超过100s没有活动,这个链接我就把他过期,关掉
    int CheckExpiredLink()
    {
        // 如何证明链接是活跃的??
        int timeout = 100000;
        time_t currenttime = time(nullptr);
        for(auto & conn : _connections)
        {
            time_t last_active = conn.second->lastActiveTime();
            time_t timediff = currenttime - last_active;
            if(timediff > 100000)
            {
                DelConnection(conn.second->Sockfd()); // 可以设置策略
            }
            time_t expiredtime = 100000 - timediff;
            if(timeout > expiredtime)
            {
                timeout = expiredtime; //保存超时最小值
            }
        }
        return timeout;
    }

    // 事件循环
    void Loop()
    {
        int timeout = 2000;
        while (true)
        {
            // // 打印合法fd
            // DebugPrint();
            // 1. 事件派发
            LoopOnce(timeout);

            // 2. 链接管理
            // timeout = CheckExpiredLink();
        }
    }
    void DebugPrint()
    {
        LOG(LogLevel::DEBUG) << "user in reactor: ";
        for (auto &conn : _connections)
        {
            LOG(LogLevel::DEBUG) << conn.second->Sockfd() << ", " << conn.second->Addr().ToString();
        }
        LOG(LogLevel::DEBUG) << "---------------------------------------";
    }
    ~Reactor()
    {
    }

public:
    void AddConnection(std::shared_ptr<Connection> conn)
    {
        // 1. 新连接写透到内核中
        _epoller->AddEvents(conn->Sockfd(), conn->Events());
        // 2. 新连接托管到_connections
        _connections[conn->Sockfd()] = conn;
        // 3. 让conn指向Reactor对象
        conn->R = this;
    }
    void EnableReadWrite(int sockfd, bool readable, bool writeable)
    {
        if (IsLegalConnection(sockfd))
        {
            uint32_t events = (EPOLLET | (readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0));
            _connections[sockfd]->SetEvents(events);
            // 写透到内核
            _epoller->ModEvents(_connections[sockfd]->Sockfd(), _connections[sockfd]->Events());
        }
    }
    void DelConnection(int sockfd)
    {
        if(IsLegalConnection(sockfd))
        {
            // 1. epoll中移除,穿透内核
            _epoller->DelEvents(sockfd);

            // 2. 关闭sockfd
            _connections[sockfd]->Close();

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

private:
    std::unique_ptr<Epoller> _epoller;
    struct epoll_event revs[grevs_num];
    std::unordered_map<int, std::shared_ptr<Connection>> _connections;
    // 一个对象IOManager,只能属于一个对象吗?? 最大堆结构, 定义定时器结构
    // std::list<std::shared_ptr<Connection>> _connection_list;
};

#endif

Connection.hpp

cpp 复制代码
#ifndef __CONNECTION_HPP
#define __CONNECTION_HPP

#include <iostream>
#include <ctime>
#include <cstdint>
#include <string>
#include <sys/epoll.h>
#include "InetAddr.hpp"

class Reactor;

class Connection
{
public:
    Connection()
        : _sockfd(-1),
          _events(EPOLLIN),
          _active_time(time(nullptr))
    {
    }
    Connection(int sockfd, uint32_t events)
        : _sockfd(sockfd),
          _events(events),
          _active_time(time(nullptr))
    {
    }
    void Active()
    {
        _active_time = time(nullptr); // 更新时间戳
    }
    time_t lastActiveTime()
    {
        return _active_time;
    }
    virtual int Sender() = 0;
    virtual int Recver() = 0;
    virtual int Excepter() = 0;

    int Sockfd()
    {
        return _sockfd;
    }
    void Close()
    {
        if (_sockfd >= 0)
        {
            close(_sockfd);
            _sockfd = -1;
        }
    }
    uint32_t Events()
    {
        return _events;
    }
    void SetEvents(uint32_t events)
    {
        _events = events;
    }
    void InitAddr(const InetAddr &addr)
    {
        _clinetaddr = addr;
    }
    InetAddr Addr()
    {
        return _clinetaddr;
    }
    ~Connection() {}

protected:
    int _sockfd;
    uint32_t _events;
    time_t _active_time; // 活跃的时间, 做问题说明
    // int heartbeat_cnt;

    std::string _inbuffer;  //  接收缓冲区, std::vector<char>
    std::string _outbuffer; // 发送缓冲区

    InetAddr _clinetaddr; // 客户端地址

public:
    Reactor *R; // 回指指针
};

#endif

Listener.hpp

cpp 复制代码
#ifndef __LISTENER_HPP
#define __LISTENER_HPP

// 链接管理器
#include <iostream>
#include <memory>
#include <sys/epoll.h>

#include "Comm.hpp"
#include "Connection.hpp"
#include "Socket.hpp"
#include "IOManager.hpp"

using namespace NS_SOCKET_MODULE;

static const int gdefaultport = 8080;

class Listener : public Connection
{
public:
    Listener(on_message_t on_message_helper, uint16_t port = gdefaultport) 
    :  _on_message_helper(on_message_helper),
      _port(port), 
      _listensock(std::make_unique<TcpSocket>())
    {
        _listensock->BuildTcpSocketMethod(_port);
        _sockfd = _listensock->Sockfd();
        _events = EPOLLIN | EPOLLET;
        Active();
    }
    int Recver() override
    {
        LOG(LogLevel::DEBUG) << "事件派发到了Listener";
        while (true)
        {
            // 获取新链接
            InetAddr clientaddr;
            int errcode = 0;
            int sockfd = _listensock->Accepter(clientaddr, &errcode); // 只会来一个链接吗??
            if (sockfd < 0)
            {
                if (errcode == EWOULDBLOCK || errcode == EAGAIN)
                {
                    LOG(LogLevel::INFO) << "accepter all client done";
                    break;
                }
                else if(errcode == EINTR)
                    continue;
                else
                {
                    LOG(LogLevel::WARNING) << "accepter Error";
                }
            }
            // 1. 获取了新链接,然后我们该怎么办??包装成为一个connection
            std::shared_ptr<Connection> conn = std::make_unique<IOManager>(sockfd, EPOLLIN | EPOLLET, _on_message_helper);
            conn->InitAddr(clientaddr);
            // 2. IOManager 要设置到Reactor中!!!
            R->AddConnection(conn);
            LOG(LogLevel::INFO) << "获取一个新链接, 新链接sockfd: " << sockfd << " client addr: " << clientaddr.ToString();
        }
        return 0;
    }
    int Excepter() override
    {
        return 0;
    }
    int Sender() override
    {
        return 0;
    }
    ~Listener() {}

private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensock;
    on_message_t _on_message_helper;
};

#endif

IOManager.hpp

cpp 复制代码
#ifndef __IOMANAGER_HPP
#define __IOMANAGER_HPP

#include "Comm.hpp"
#include "Connection.hpp"

#include <functional>

using on_message_t = std::function<std::string(std::string &)>;

static const int buffersize = 1024;

class IOManager : public Connection
{
public:
    IOManager(int sockfd, uint32_t events, on_message_t on_message) : Connection(sockfd, events), _on_message(on_message)
    {
        Active();
    }
    ~IOManager() {}
    int Recver() override
    {
        Active();
        LOG(LogLevel::DEBUG) << "事件派发到了IOManager,Recver()";
        // 实现非阻塞循环读写
        // 1. 读
        char buffer[buffersize];
        while (true)
        {
            ssize_t n = recv(_sockfd, buffer, buffersize - 1, 0); // 非阻塞读取了
            if (n > 0)
            {
                buffer[n] = 0;
                _inbuffer += buffer;
            }
            else if (n == 0)
            {
                LOG(LogLevel::INFO) << "client: " << _clinetaddr.ToString() << " quit";
                Excepter();
                return -1;
            }
            else
            {
                if (errno == EWOULDBLOCK || errno == EAGAIN)
                {
                    break;
                }
                if (errno == EINTR)
                {
                    continue;
                }
                else
                {
                    LOG(LogLevel::INFO) << "recv client: " << _clinetaddr.ToString() << " error, sockfd: " << _sockfd;
                    Excepter();
                    return -2;
                }
            }
        }

        // 2. 尝试回调处理
        // 只有一种情况
        LOG(LogLevel::DEBUG) << _sockfd << ", 本轮数据读完, inbuffer: " << _inbuffer;
        // 本轮数据我已经读取完毕了 == 一定有一个完整的请求报文呢?,有没有完整报文是谁决定的???应用层协议决定!!
        // inbuffer尝试交给协议处理1. 怎么交给上层协议?? 2. 上层协议是谁??
        if (_on_message)
        {
            _outbuffer += _on_message(_inbuffer);
        }
        // 对于新的文件fd,读事件默认不就绪,但是写事件默认是就绪的.
        // 写事件不能常设置,按需设置
        // 读不能直接读,写能直接写吗?能直接写!!
        // 对于写事件的处理,直接写,直到写失败,开启写事件关心
        // if (!_outbuffer.empty())
        // {
        //     Sender();
        // }

        if(!_outbuffer.empty())
        {
            R->EnableReadWrite(_sockfd, true, true); // 使能写事件关心
        }

        return _inbuffer.size();
    }
    int Excepter() override
    {
        // 归一化异常处理
        LOG(LogLevel::WARNING) << "errno: " << errno << " message: " << strerror(errno);
        R->DelConnection(_sockfd);
        return 0;
    }
    int Sender() override
    {
        Active();
        LOG(LogLevel::DEBUG) << "事件派发到了IOManager,Sender()";

        while (true)
        {
            ssize_t n = send(_sockfd, _outbuffer.c_str(), _outbuffer.size(), 0);
            if (n > 0)
            {
                _outbuffer.erase(0, n);
                if (_outbuffer.empty())
                    break; // 第1个
            }
            else
            {
                if (errno == EWOULDBLOCK || errno == EAGAIN)
                    break; // 第2个
                else if (errno == EINTR)
                    continue;
                else
                {
                    LOG(LogLevel::INFO) << "send client: " << _clinetaddr.ToString() << " error, sockfd: " << _sockfd;
                    Excepter();
                    return -1;
                }
            }
        }

        if (!_outbuffer.empty())
        {
            // _outbuffer没有发送完成 && 缓冲区写满了
            // 写条件不满足 -> 开启sockfd对写事件的关心 -> 按需设置
            R->EnableReadWrite(_sockfd, true, true);
            LOG(LogLevel::INFO) << "发送条件不满足,write event 托管给epoll: " << _sockfd;   
        }
        else
        {
            // _outbuffer发送完了
            R->EnableReadWrite(_sockfd, true, false);
            LOG(LogLevel::INFO) << "发送完毕,关闭写事件关系: "  << _sockfd;   
        }
        return 0;
    }

private:
    on_message_t _on_message;
};

#endif

Protocol.hpp

cpp 复制代码
#pragma once

// 自定义协议部分
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
#include <functional>
#include "Logger.hpp"

using namespace NS_LOG_MODULE;

// 1. 自己做 - 不建议的!
// 2. 用别人的 - json protobuf xml

// 请求报文
class Request
{
public:
    Request() : _data_x(0), _data_y(0), _oper(0)
    {
    }
    Request(int x, int y, char oper) : _data_x(x), _data_y(y), _oper(oper)
    {
    }
    bool Serialize(std::string *out)
    {
        // 结构化 -> "_data_x _oper _data_y"
        Json::Value root;
        root["left"] = _data_x;
        root["right"] = _data_y;
        root["oper"] = _oper;

        Json::FastWriter writer;
        *out = writer.write(root);
        return true;
    }
    bool Deserialize(std::string &in) // "_data_x _oper _data_y"
    {
        // "_data_x _oper _data_y" -> 结构化
        Json::Value root;
        Json::Reader reader;
        bool parsesuccess = reader.parse(in, root);
        if (!parsesuccess)
            return false;

        _data_x = root["left"].asInt();
        _data_y = root["right"].asInt();
        _oper = root["oper"].asInt();
        return true;
    }
    ~Request()
    {
    }
    // get set

public:
    // 10 20 '-' -> 10 - 20 = ?
    // _data_x _oper _data_y
    int _data_x;
    int _data_y;
    char _oper; // '+' '-' '/' '*' '%'
};

// 应答报文
class Response
{
public:
    Response() : _result(0), _code(0)
    {
    }
    Response(int result, int code) : _result(result), _code(code)
    {
    }
    bool Serialize(std::string *out)
    {
        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;

        Json::FastWriter writer;
        *out = writer.write(root);
        return true;
    }
    bool Deserialize(std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        bool parsesuccess = reader.parse(in, root);
        if (!parsesuccess)
            return false;

        _result = root["result"].asInt();
        _code = root["code"].asInt();
        return true;
    }
    ~Response()
    {
    }

public:
    int _result; // 结果
    int _code;   // 状态码
};

const std::string gsep = "\r\n";

using HandlerRequest_t = std::function<Response(Request &)>;
using HandlerResponse_t = std::function<void (Response &)>;

class Protocol
{
public:
    Protocol(HandlerRequest_t handler) : _version("1.0"), _handler_request(handler)
    {
    }
    Protocol(HandlerResponse_t handler_response):_version("1.0"), _handler_response(handler_response)
    {
    }
    // {"left": 10, "right": 20, oper: '+'}
    // len\r\n{"left": 10, "right": 20, oper: '+'}\r\n
    std::string Packet(const std::string &json_string)
    {
        return std::to_string(json_string.size()) + gsep + json_string + gsep;
    }
    // len\r\n{"left": 10, "right": 20, oper: '+'}\r\n
    // len\r\n{"left": 10, "right": 20, oper: '+'}\r\nlen\r\n{"left": 10, "right": 20, oper: '+'}\r\n
    // len\r\n{"left": 10, "right": 20, oper: '+'}\r\nlen\r\n{"left": 10,
    // len\r\n{"left": 10, "right": 20, o
    // le
    // ret > 0: no error, json_string != NULL
    // ret == 0: no error, json_string == NULL
    // ret < 0 : error.
    int Unpack(std::string &packet, std::string *json_string)
    {
        if (packet.empty())
            return 0;
        if (json_string == nullptr)
            return -1;

        // 分析报文
        auto pos = packet.find(gsep);
        if (pos == std::string::npos)
            return 0;
        std::string lenstr = packet.substr(0, pos);

        // lenstr 合法性判断,lenstr -> 123 345

        int len = std::stoi(lenstr);
        int total = lenstr.size() + len + 2 * gsep.size();
        if (packet.size() < total)
            return 0;
        // 提取报文
        *json_string = packet.substr(pos + gsep.size(), len);
        packet.erase(0, total);
        return 1;
    }
    // 如果读到半个报文,什么都不做
    // 如果读到一个报文+,循环处理,把所有合法的报文都进行统一处理
    std::string ParseRequest(std::string &inbuffer)
    {
        std::string result;
        while (true)
        {
            std::string json_string;
            // 1. 解包
            int n = Unpack(inbuffer, &json_string);
            if (n < 0)
            {
                LOG(LogLevel::DEBUG) << "no way !!";
                return std::string();
            }
            if (n == 0)
            {
                LOG(LogLevel::INFO) << inbuffer << " parse done";
                return result;
            }
            LOG(LogLevel::DEBUG) << "json_string:\n" << json_string;
            LOG(LogLevel::DEBUG) << "unpack done, inbuffer:\n" << inbuffer;
            // 2. 反序列化
            // 得到一个完整的报文jsonstring
            Request req;
            if (!req.Deserialize(json_string))
                return std::string();

            // 3. 业务计算
            Response resp;
            if (_handler_request)
                resp = _handler_request(req);

            // 4. 应答序列化
            std::string resp_json_string;
            resp.Serialize(&resp_json_string);

            // 5. 添加报头
            result += Packet(resp_json_string);
        }
    }
    std::string ParseResponse(std::string &inbuffer)
    {
        while (true)
        {
            std::string json_string;
            // 1. 解包
            int n = Unpack(inbuffer, &json_string);
            if (n < 0)
            {
                LOG(LogLevel::DEBUG) << "no way !!";
                return std::string();
            }
            if (n == 0)
            {
                LOG(LogLevel::INFO) << inbuffer << " parse done";
                return std::string();
            }
            // 2. 反序列化
            // 得到一个完整的报文jsonstring
            Response resp;
            if (!resp.Deserialize(json_string))
                return std::string();
            
            // 3. 回调处理
            if (_handler_response)
                _handler_response(resp);
        }
    }

    ~Protocol()
    {
    }

private:
    std::string _version;
    HandlerRequest_t _handler_request;
    HandlerResponse_t _handler_response;
};

Calculator.hpp

cpp 复制代码
#pragma once

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

class Calculator
{
public:
    Response Execute(const Request &req)
    {
        Response resp;
        switch (req._oper)
        {
        case '+':
            resp._result = req._data_x + req._data_y;
            break;
        case '-':
            resp._result = req._data_x - req._data_y;
            break;
        case '*':
            resp._result = req._data_x * req._data_y;
            break;
        case '/':
        {
            if (req._data_y == 0)
                resp._code = 1; // div error
            else
                resp._result = req._data_x / req._data_y;
        }
        break;
        case '%':
        {
            if (req._data_y == 0)
                resp._code = 2; // mod error
            else
                resp._result = req._data_x % req._data_y;
        }
        break;
        default:
            resp._code = 3; // 非法操作
            break;
        }
        return resp;
    }
};

Main.cc

cpp 复制代码
#include "Calculator.hpp"
#include "Protocol.hpp"
#include "Reactor.hpp"
#include "Listener.hpp"

#include <iostream>
#include <memory>


int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cout << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }

    ENABLE_CONSOLE_LOG_STRATEGY();

    uint16_t port = std::stoi(argv[1]);
    // 1. 创建网络计算器
    std::unique_ptr<Calculator> cal = std::make_unique<Calculator>();

    // 2. 定义协议对象
    std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>(
        [&cal](Request & req)->Response{
            return cal->Execute(req);
        }
    );

    // 3. 创建listener对象 -> base connection
    std::shared_ptr<Connection> listener = std::make_shared<Listener>([&protocol](std::string &inbuffer)->std::string{
        return protocol->ParseRequest(inbuffer);
    }, port);


    // 4. 创建一个Reactor
    std::unique_ptr<Reactor> R = std::make_unique<Reactor>();

    // 5. 把链接添加到TcpServer中
    R->AddConnection(listener);

    // 6. 事件派发逻辑
    R->Loop();


    return 0;
}

Comm.hpp

cpp 复制代码
#ifndef __COMMON_HPP
#define __COMMON_HPP

#include <iostream>
#include <fcntl.h>
#include "Logger.hpp"

using namespace NS_LOG_MODULE;

enum EXIT_CODE
{
    SUCCESS,
    EPOLLER_ERROR,
    EPOLLER_WAIT_FATAL
};

void SetNonBlock(int sockfd)
{
    int fl = fcntl(sockfd, F_GETFL);
    if(fl < 0)
    {
        return;
    }
    fcntl(sockfd, F_SETFL, fl | O_NONBLOCK);
}

#endif

InetAddr.hpp

cpp 复制代码
#pragma once

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

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

// 对客户端进行先描述
class InetAddr
{
public:
    InetAddr()
    {
    }
    InetAddr(const struct sockaddr_in &address) : _address(address), _len(sizeof(address))
    {
        // _ip = inet_ntoa(_address.sin_addr);
        char ipstr[32];
        inet_ntop(AF_INET, &(_address.sin_addr), ipstr, sizeof(ipstr));
        _ip = ipstr;
        _port = ntohs(_address.sin_port);
    }
    InetAddr(uint16_t port, const std::string &ip = "0.0.0.0") : _ip(ip), _port(port)
    {
        bzero(&_address, sizeof(_address));
        _address.sin_family = AF_INET;
        _address.sin_port = htons(_port); // h->n
        //_address.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 字符串ip->4字节IP 2. hton
        inet_pton(AF_INET, ip.c_str(), &(_address.sin_addr));
        _len = sizeof(_address);
    }
    bool operator==(const InetAddr &addr)
    {
        return (this->_ip == addr._ip) && (this->_port == addr._port);
    }
    void operator=(struct sockaddr_in &addr)
    {
        _address = addr;
        char ipstr[32];
        inet_ntop(AF_INET, &(_address.sin_addr), ipstr, sizeof(ipstr));
        _ip = ipstr;
        _port = ntohs(_address.sin_port);
    }
    std::string ToString()
    {
        return "[" + _ip + ":" + std::to_string(_port) + "]";
    }

    struct sockaddr *NetAddress()
    {
        return CONV(&_address);
    }
    socklen_t Len()
    {
        return _len;
    }
    ~InetAddr()
    {
    }

private:
    // net address
    struct sockaddr_in _address;
    socklen_t _len;
    // host address
    std::string _ip;
    uint16_t _port;
};

Logger.hpp

cpp 复制代码
#ifndef __LOGGER_HPP
#define __LOGGER_HPP

#include <iostream>
#include <cstdio>
#include <string>
#include <memory>
#include <sstream>
#include <ctime>
#include <sys/time.h>
#include <unistd.h>
#include <filesystem> // C++17
#include <fstream>
#include "Mutex.hpp"

namespace NS_LOG_MODULE
{
    enum class LogLevel
    {
        INFO,
        WARNING,
        ERROR,
        FATAL,
        DEBUG
    };
    std::string LogLevel2Message(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        case LogLevel::DEBUG:
            return "DEBUG";
        default:
            return "UNKNOWN";
        }
    }

    // 1. 时间戳 2. 日期+时间
    std::string GetCurrentTime()
    {
        struct timeval current_time;
        int n = gettimeofday(&current_time, nullptr);
        (void)n;

        // current_time.tv_sec; current_time.tv_usec;
        struct tm struct_time;
        localtime_r(&(current_time.tv_sec), &struct_time); // r: 可重入函数
        char timestr[128];
        snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
                 struct_time.tm_year + 1900,
                 struct_time.tm_mon + 1,
                 struct_time.tm_mday,
                 struct_time.tm_hour,
                 struct_time.tm_min,
                 struct_time.tm_sec,
                 current_time.tv_usec);
        return timestr;
    }

    // 输出角度 -- 刷新策略
    // 1. 显示器打印
    // 2. 文件写入
    // 策略模式,策略接口
    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;
        virtual void SyncLog(const std::string &message) = 0;
    };
    // 控制台日志刷新策略, 日志将来要向显示器打印
    class ConsoleStrategy : public LogStrategy
    {
    public:
        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);
            std::cerr << message << std::endl; // ??
        }
        ~ConsoleStrategy()
        {
        }

    private:
        Mutex _mutex;
    };

    const std::string defaultpath = "./log";
    const std::string defaultfilename = "log.txt";


    // 文件策略
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &path = defaultpath, const std::string &name = defaultfilename)
            : _logpath(path),
              _logfilename(name)
        {
            LockGuard lockguard(_mutex);
            if (std::filesystem::exists(_logpath))
                return;
            try
            {
                std::filesystem::create_directories(_logpath);
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << '\n';
            }
        }

        void SyncLog(const std::string &message) override
        {
            {
                LockGuard lockguard(_mutex);
                if (!_logpath.empty() && _logpath.back() != '/')
                {
                    _logpath += "/";
                }
                std::string targetlog = _logpath + _logfilename; // "./log/log.txt"
                std::ofstream out(targetlog, std::ios::app);     // 追加方式写入
                if (!out.is_open())
                {
                    std::cerr << "open " << targetlog << "failed" << std::endl;
                    return;
                }
                out << message << "\n";
                out.close();
            }
        }

        ~FileLogStrategy()
        {
        }

    private:
        std::string _logpath;
        std::string _logfilename;
        Mutex _mutex;
    };

    // 交给大家
    // const std::string defaultfilename = "log.info";
    // const std::string defaultfilename = "log.warning";
    // const std::string defaultfilename = "log.fatal";
    // const std::string defaultfilename = "log.error";
    // const std::string defaultfilename = "log.debug";
     // 文件策略&&分日志等级来进行保存
    // class FileLogLevelStrategy : public LogStrategy
    // {
    // public:
    // private:
    // };


    // 日志类:
    // 1. 日志的生成
    // 2. 根据不同的策略,进行刷新
    class Logger
    {
        // 日志的生成:
        // 构建日志字符串
    public:
        Logger()
        {
            UseConsoleStrategy();
        }
        void UseConsoleStrategy()
        {
            _strategy = std::make_unique<ConsoleStrategy>();
        }
        void UseFileStrategy()
        {
            _strategy = std::make_unique<FileLogStrategy>();
        }
        // 内部类, 标识一条完整的日志信息
        //  一条完整的日志信息 = 做半部分固定部分 + 右半部分不固定部分
        //  LogMessage RAII风格的方式,进行刷新
        class LogMessage
        {
        public:
            LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
                : _level(level),
                  _curr_time(GetCurrentTime()),
                  _pid(getpid()),
                  _filename(filename),
                  _line(line),
                  _logger(logger)
            {
                // 先构建出来左半部分
                std::stringstream ss;
                ss << "[" << _curr_time << "] "
                   << "[" << LogLevel2Message(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _filename << "] "
                   << "[" << _line << "] "
                   << " - ";

                _loginfo = ss.str();
            }
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return *this; // 返回当前LogMessage对象,方便下次继续进行<<
            }

            ~LogMessage()
            {
                if (_logger._strategy)
                {
                    _logger._strategy->SyncLog(_loginfo);
                }
            }

        private:
            LogLevel _level;
            std::string _curr_time;
            pid_t _pid;
            std::string _filename;
            int _line;
            std::string _loginfo; // 一条完整的日志信息

            // 一个引用,引用外部的Logger类对象
            Logger &_logger; // 方便我们后续进行策略式刷新
        };

        // 这里已经不是内部类了
        // 故意采用拷贝LogMessage
        LogMessage operator()(LogLevel level, std::string filename, int line)
        {
            return LogMessage(level, filename, line, *this);
        }

        ~Logger()
        {
        }

    private:
        std::unique_ptr<LogStrategy> _strategy; // 刷新策略
    };

    // 日志对象,全局使用
    Logger logger;

#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();

#define LOG(level) logger(level, __FILE__, __LINE__)

}

#endif

Mutex.hpp

cpp 复制代码
#pragma once

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

class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&_lock, nullptr);
    }
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    pthread_mutex_t *Ptr()
    {
        return &_lock;
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }
private:
    pthread_mutex_t _lock;
};

class LockGuard // RAII风格代码
{
public:
    LockGuard(Mutex &lock):_lockref(lock)
    {
        _lockref.Lock();
    }
    ~LockGuard()
    {
        _lockref.Unlock();
    }
private:
    Mutex &_lockref;
};

Socket.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include "Comm.hpp"
#include "Logger.hpp"
#include "InetAddr.hpp"

namespace NS_SOCKET_MODULE
{
    static const int gbacklog = 16;

    enum
    {
        OK = 0,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR
    };

    // 模版方法模式!
    class Socket
    {
    public:
        ~Socket()
        {
        }

    protected:
        virtual void CreateSocketOrDie() = 0;
        virtual void BindSocketOrDie(uint16_t port) = 0;
        virtual void ListenSocketOrDie() = 0;
        // virtual ssize_t Recv() = 0;
        // virtual void Send() = 0;
    public:
        // virtual std::shared_ptr<Socket> Accepter(InetAddr &addr) = 0;
        virtual int Accepter(InetAddr &addr, int *errcode) = 0;
        virtual int Sockfd() = 0;
        virtual int Recv(std::string *out) = 0;
        virtual int Send(const std::string &in) = 0;
        virtual void Close() = 0;
        virtual bool Connect(InetAddr &addr) = 0;
    public:
        void BuildTcpSocketMethod(uint16_t port) // 模版方法
        {
            CreateSocketOrDie();
            BindSocketOrDie(port);
            ListenSocketOrDie();
        }
        void BuildTcpClientSockMethod()
        {
            CreateSocketOrDie();
        }
        // void BuildUdpSocketMethod()
        // {
        //     CreateSocketOrDie();
        //     BindSocketOrDie();
        // }
    };

    class TcpSocket : public Socket
    {
    public:
        TcpSocket() : _sockfd(0), _reuseport(false)
        {
        }
        TcpSocket(int sockfd): _sockfd(sockfd), _reuseport(false)
        {}
        void SetReusePort(bool reuseport)
        {
            _reuseport = reuseport;
        }
        void CreateSocketOrDie() override
        {
            _sockfd = socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(LogLevel::FATAL) << "create socket error";
                exit(SOCKET_ERR);
            }
            int opt = 1;
            setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
            if (_reuseport)
            {
                setsockopt(_sockfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
                LOG(LogLevel::INFO) << "SO_REUSEPORT enabled";
            }
            SetNonBlock(_sockfd);
        }
        void BindSocketOrDie(uint16_t port) override
        {
            InetAddr addr(port);
            if (bind(_sockfd, addr.NetAddress(), addr.Len()) != 0)
            {
                LOG(LogLevel::FATAL) << "bind socket error";
                exit(BIND_ERR);
            }
        }
        void ListenSocketOrDie() override
        {
            if (listen(_sockfd, gbacklog) != 0)
            {
                LOG(LogLevel::FATAL) << "listen socket error";
                exit(LISTEN_ERR);
            }
        }
        // std::shared_ptr<Socket> Accepter(InetAddr &clientaddr) override
        int Accepter(InetAddr &clientaddr, int *errcode) override
        {
            struct sockaddr_in addr;
            socklen_t len = sizeof(addr);
            int sockfd = accept(_sockfd, CONV(&addr), &len);
            *errcode = errno;
            if(sockfd < 0)
            {
                // LOG(LogLevel::WARNING) << "accept error";
                // return nullptr;
                return -1;
            }
            SetNonBlock(sockfd);
            clientaddr = addr;
            // return std::make_shared<TcpSocket>(sockfd);
            return sockfd;
        }
        int Sockfd() override
        {
            return _sockfd;
        }

        int Recv(std::string *out) override
        {
            char inbuffer[1024];
            ssize_t n = recv(_sockfd, inbuffer, sizeof(inbuffer)-1, 0);
            if(n > 0)
            {
                inbuffer[n] = 0;
                *out += inbuffer; // 追加写入的
            }
            return n;
        }
        
        int Send(const std::string &in) override
        {
            return send(_sockfd, in.c_str(), in.size(), 0);
        }
        void Close() override
        {
            if(_sockfd>=0)
            {
                close(_sockfd);
                _sockfd = -1;
            }
        }
        bool Connect(InetAddr &addr) override
        {
            int n = connect(_sockfd, addr.NetAddress(), addr.Len());
            if(n < 0)
                return false;
            else
                return true;
        }

        ~TcpSocket() {}

    private:
        int _sockfd;
    };

    // class UdpSocket: public Socket
    // {

    // };

} // namespace name

Makefile

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

谢谢大家的观看!

相关推荐
越甲八千1 小时前
本地验证http服务器拉取文件
服务器·网络协议·http
程序大视界1 小时前
【C++ 从基础到项目实战】C++(九):友元与设计模式初探——打破封装的艺术
开发语言·c++·cpp
cpp_25011 小时前
P10377 [GESP202403 六级] 好斗的牛
数据结构·c++·算法·题解·洛谷·gesp六级
邪修king1 小时前
C++ 红黑树自平衡核心:旋转变色、规则详解与 STL 选型逻辑
数据结构·c++·b树·算法
jcbut2 小时前
在Linux上安装Kingbase 9
linux·kingbase·人大金仓·电科金仓
小此方3 小时前
Re:Linux系统篇(二十六)进程篇·十一:从底层原理到 exec* 家族:彻底搞懂 Linux 进程程序替换
linux·运维·服务器
wgc2k3 小时前
Node.js游戏服务器项目移植 3-手撸简单的内存泄露监控
服务器·游戏·node.js
cany10009 小时前
C++ -- 可变参数模板
c++
不会C语言的男孩10 小时前
C++ Primer 第2章:变量和基本类型
开发语言·c++