网络地址与 Socket 封装

第 9 课:网络地址与 Socket 封装 对应源文件: trantor/net/InetAddress.h / InetAddress.cc — IPv4/IPv6 地址封装 trantor/net/inner/Socket.h / Socket.cc — 跨平台 Socket RAII 封装 一、两个类在架构中的位置 1 2 3 4 5 6 7 TcpServer / TcpClient │ ▼ Acceptor / Connector │ ├─ InetAddress ← 描述"连谁/绑哪里" └─ Socket ← 持有实际的系统 fd,负责创建/配置/关闭 这两个类是"最底层的 C++ 包装": InetAddress:把 struct sockaddr_in/in6 包成一个类型安全的 C++ 对象 Socket:RAII 管理 socket fd,把 setsockopt/bind/listen/accept 包成成员函数 二、InetAddress — 双协议地址封装 2.1 核心存储 1 2 3 4 5 6 7 // InetAddress.h(精简) union { struct sockaddr_in addr_; // IPv4:16 字节 struct sockaddr_in6 addr6_; // IPv6:28 字节 }; bool isIpV6_; // 区分当前存的是哪种 bool isUnspecified_; // 是否是"未指定地址"(0.0.0.0 / ::) 为什么用 union? ...

March 20, 2025 · 8 min · 1617 words

定时器系统(Timer + TimerQueue + TimingWheel)

第 8 课:定时器系统(Timer + TimerQueue + TimingWheel) 对应源文件: trantor/net/inner/Timer.h/.cc — 单个定时器对象 trantor/net/inner/TimerQueue.h/.cc — 定时器优先队列(最小堆) trantor/utils/TimingWheel.h/.cc — 分级时间轮(高并发连接超时) 一、三个类的分工 1 2 3 4 5 6 Timer — 单个定时器的数据:到期时间、回调、是否重复 │ TimerQueue — 管理所有定时器,最小堆,驱动到期回调 │ TimingWheel — 用于大量连接的超时检测,O(1) 插入/删除 基于 TimerQueue 的 runEvery 驱动 两套定时器,用途不同: TimerQueue:精确定时,适合少量定时任务(heartbeat 广播、延迟关闭等) TimingWheel:粗粒度超时,适合万级连接的空闲超时检测 二、Timer:单个定时器 2.1 数据成员 1 2 3 4 5 6 TimerCallback callback_; // 到期执行的函数 TimePoint when_; // 到期时间(steady_clock,单调时钟) TimeInterval interval_; // 重复间隔(microseconds,为 0 表示一次性) bool repeat_; // = (interval_.count() > 0) TimerId id_; // 全局唯一 ID(原子递增) static std::atomic<TimerId> timersCreated_; // 全局计数器 2.2 ID 生成 1 2 3 4 5 // Timer.cc 第 21 行 std::atomic<TimerId> Timer::timersCreated_ = ATOMIC_VAR_INIT(InvalidTimerId); // 初始值 0 // 构造时(Timer.cc 第 29 行) id_(++timersCreated_) // 原子前自增,从 1 开始 每个 Timer 对象构造时自动分配唯一 ID,多线程安全,不需要锁。InvalidTimerId = 0 作为哨兵值。 ...

March 18, 2025 · 10 min · 2008 words

Poller — I/O 多路复用

第 7 课:Poller — I/O 多路复用 对应源文件: trantor/net/inner/Poller.h / Poller.cc — 抽象基类 + 工厂函数 trantor/net/inner/poller/EpollPoller.h/.cc — Linux/Windows 实现 trantor/net/inner/poller/KQueue.h/.cc — macOS/BSD 实现 trantor/net/inner/poller/PollPoller.h/.cc — 其他 Unix 兜底实现 一、Poller 在架构中的位置 1 2 3 4 5 6 7 EventLoop │ poll(timeoutMs, &activeChannels) ▼ Poller(抽象基类) ├── EpollPoller ← Linux / Windows(wepoll) ├── KQueue ← macOS / FreeBSD / OpenBSD └── PollPoller ← 其他 Unix(兜底) Poller 是桥接模式的经典应用:上层 EventLoop 只依赖抽象基类 Poller,底层平台差异完全被屏蔽。EventLoop 的代码里看不到任何 epoll_wait 或 kevent。 ...

March 15, 2025 · 10 min · 2076 words

Channel — 事件通道

第 6 课:Channel — 事件通道 对应源文件: trantor/net/Channel.h — 公共接口 trantor/net/Channel.cc — 实现(仅 112 行) 一、Channel 是什么? Channel 是 trantor Reactor 模式中的中间层,它把一个文件描述符(fd)的事件管理和回调分发封装在一起。 1 2 fd ──── Channel ──── Poller(注册/更新感兴趣的事件) └────── EventLoop(事件就绪后,回调分发) 三个关键概念: events_:当前感兴趣的事件(告诉 Poller 我想监听什么) revents_:Poller 返回的实际发生的事件(poll/epoll 填写) 回调函数:事件发生后调用哪个函数 Channel 不拥有 fd——fd 的生命周期由 Socket 对象管理,Channel 只是"贴在" fd 上的事件管理标签。 二、事件标志位 1 2 3 4 // Channel.cc 第 31-34 行 const int Channel::kNoneEvent = 0; const int Channel::kReadEvent = POLLIN | POLLPRI; // 可读 + 紧急数据 const int Channel::kWriteEvent = POLLOUT; // 可写 为什么用 POLLIN/POLLOUT 而不是 EPOLLIN/EPOLLOUT? ...

March 12, 2025 · 8 min · 1684 words

EventLoop — 事件循环

第 5 课:EventLoop — 事件循环 对应源文件: trantor/net/EventLoop.h — 公共接口 trantor/net/EventLoop.cc — 实现 trantor/utils/LockFreeQueue.h — 无锁任务队列(MpscQueue) 一、EventLoop 是什么? EventLoop 是 trantor 整个框架的心脏,也是 Reactor 模式的核心。 一句话定义:一个线程,一个循环,监听所有 I/O 事件和定时器,串行处理所有回调。 1 2 3 4 5 6 7 8 9 ┌─────────────────────────────────────────┐ │ EventLoop::loop() │ │ │ │ while (!quit_) { │ │ ① poller_->poll(timeout) ←阻塞等事件│ │ ② 处理所有就绪的 Channel 回调 │ │ ③ doRunInLoopFuncs() ← 执行投递任务 │ │ } │ └─────────────────────────────────────────┘ 三个核心原则: One loop per thread:一个 EventLoop 只属于一个线程,且一个线程最多一个 EventLoop 所有 I/O 操作在 Loop 线程执行:不跨线程操作 socket 跨线程操作必须通过 runInLoop/queueInLoop:把任务投递进去,由 Loop 线程执行 二、核心主循环 loop() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 // EventLoop.cc 第 204-266 行(精简版) void EventLoop::loop() { assert(!looping_); assertInLoopThread(); // 必须在 Loop 线程调用 looping_.store(true, std::memory_order_release); auto loopFlagCleaner = makeScopeExit( // RAII:确保退出时清 looping_ [this]() { looping_.store(false, ...); }); while (!quit_.load(std::memory_order_acquire)) { activeChannels_.clear(); // ① 阻塞等待 I/O 事件(最长 10 秒) #ifdef __linux__ poller_->poll(kPollTimeMs, &activeChannels_); // Linux: epoll #else poller_->poll(timerQueue_->getTimeout(), &activeChannels_); timerQueue_->processTimers(); // 非 Linux: 手动处理定时器 #endif // ② 处理所有就绪 Channel 的回调 eventHandling_ = true; for (auto *channel : activeChannels_) { currentActiveChannel_ = channel; channel->handleEvent(); // 分发读/写/错误回调 } currentActiveChannel_ = nullptr; eventHandling_ = false; // ③ 执行跨线程投递进来的任务 doRunInLoopFuncs(); } // 退出后执行 runOnQuit 注册的清理函数 Func f; while (funcsOnQuit_.dequeue(f)) f(); } 主循环的三个阶段 1 2 3 4 5 6 7 8 9 10 11 12 Phase 1: poller_->poll() └─ 调用 epoll_wait(最多等 10 秒) └─ 返回:活跃的 Channel 列表(有读/写事件的 fd) Phase 2: handleEvent() └─ 遍历活跃 Channel,各自回调 └─ 例如:socket 可读 → RecvMessageCallback socket 可写 → WriteCompleteCallback Phase 3: doRunInLoopFuncs() └─ 消费 funcs_ 队列(MpscQueue),执行所有投递进来的任务 └─ 例如:其他线程调用了 queueInLoop(f) 三、wakeupFd:打破阻塞的关键 问题:epoll_wait 在等待时是阻塞的,如果其他线程此时投递了一个任务(queueInLoop),Loop 线程要等最多 10 秒才能执行。 ...

March 10, 2025 · 9 min · 1848 words

回调类型定义(callbacks.h)

第 4 课:回调类型定义(callbacks.h) 对应源文件: trantor/net/callbacks.h — 所有网络回调 typedef trantor/net/TcpConnection.h — 回调的注册接口与存储位置(辅助理解) 一、为什么要先读这个文件? callbacks.h 只有 46 行,却是后续所有网络模块的词汇表。 从第 5 课开始,你会在 EventLoop、TcpServer、TcpClient 里频繁看到这些类型名。如果不先建立印象,每次碰到 RecvMessageCallback 都要回头查,打断阅读节奏。 先把这张表背熟,后面读代码会顺畅得多。 二、完整回调类型表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // callbacks.h 全文(去掉注释) using TimerCallback = std::function<void()>; using TcpConnectionPtr = std::shared_ptr<TcpConnection>; using RecvMessageCallback = std::function<void(const TcpConnectionPtr &, MsgBuffer *)>; using ConnectionErrorCallback = std::function<void()>; using ConnectionCallback = std::function<void(const TcpConnectionPtr &)>; using CloseCallback = std::function<void(const TcpConnectionPtr &)>; using WriteCompleteCallback = std::function<void(const TcpConnectionPtr &)>; using HighWaterMarkCallback = std::function<void(const TcpConnectionPtr &, const size_t)>; using SSLErrorCallback = std::function<void(SSLError)>; using SockOptCallback = std::function<void(int)>; 三、逐一解析 3.1 TcpConnectionPtr — 连接的生命线 1 using TcpConnectionPtr = std::shared_ptr<TcpConnection>; 这不是回调,是基础类型,但几乎所有回调都把它作为第一个参数。 ...

March 9, 2025 · 7 min · 1402 words

日期时间 & 工具函数

第 3 课:日期时间 & 工具函数 对应源文件: trantor/utils/Date.h / Date.cc — 微秒精度时间点 trantor/utils/Funcs.h — 通用辅助函数(字节序 + 字符串分割) trantor/utils/NonCopyable.h — 禁拷贝基类 一、Date:微秒精度时间点 1.1 设计思路 Date 本质上只是一个 int64_t 的包装: 1 2 3 4 5 6 class Date { private: int64_t microSecondsSinceEpoch_{0}; // 从 1970-01-01 00:00:00 UTC 至今的微秒数 public: static constexpr long MICRO_SECONDS_PER_SEC = 1000000LL; }; 为什么用微秒而不是毫秒/秒? 定时器精度要求微秒级(定时器误差 < 1ms) 日志时间戳需要显示到微秒(高频事件排查) int64_t 表示微秒可以撑到 292471年,溢出不是问题 1.2 获取当前时间:Date::now() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // Date.cc 第 52-65 行 const Date Date::date() { #ifndef _WIN32 struct timeval tv; gettimeofday(&tv, NULL); // Linux: 系统调用,精度 ~1 微秒 int64_t seconds = tv.tv_sec; return Date(seconds * MICRO_SECONDS_PER_SEC + tv.tv_usec); #else // Windows: GetLocalTime → mktime → 精度只到毫秒(wMilliseconds * 1000) timeval tv; gettimeofday(&tv, NULL); // Windows 版本在同文件里实现 ... #endif } Linux 路径:gettimeofday 是 VDSO 调用(Virtual Dynamic Shared Object),在用户空间直接读取内核映射的时钟,不陷入内核,速度极快(几十纳秒)。 ...

March 8, 2025 · 8 min · 1603 words

消息缓冲区 MsgBuffer

第 2 课:消息缓冲区 MsgBuffer 对应源文件: trantor/utils/MsgBuffer.h / MsgBuffer.cc — 核心读写缓冲区 trantor/net/inner/BufferNode.h — 发送队列多态节点 一、为什么需要 MsgBuffer? TCP 是流式协议,recv() 一次调用不一定能读完一个完整消息,也可能读到多个消息粘在一起(粘包)。 需要一个弹性缓冲区来: 接收数据时:把内核 socket 缓冲区的数据读进来,等凑够一个完整包再交给上层 发送数据时:把待发送数据先攒在缓冲区,等 socket 可写时分批发出 支持在头部预留空间(prepend),方便后填消息长度字段 二、内存布局 2.1 物理结构 1 2 3 4 5 6 7 8 9 buffer_ (std::vector<char>,初始大小 = 2048 + 8) 索引: 0 8 tail_ buffer_.size() │ │ │ │ ▼ ▼ ▼ ▼ [prepend区][─── 可读数据 ───][──── 可写空间 ────] └── 8字节 ┘ ↑ ↑ head_ tail_ prepend 区 [0, head_):保留 8 字节,用于在数据头部插入字段(不需要移动数据) 可读区 [head_, tail_):已接收但未被消费的数据,大小 = tail_ - head_ 可写区 [tail_, buffer_.size()):空闲空间,大小 = buffer_.size() - tail_ 2.2 关键常量与初始状态 1 2 3 4 5 6 7 8 9 // MsgBuffer.cc 第 31 行 static constexpr size_t kBufferOffset{8}; // prepend 预留大小 // 构造函数(MsgBuffer.cc 第 34-37 行) MsgBuffer::MsgBuffer(size_t len) : head_(kBufferOffset), // head_ 从 8 开始 initCap_(len), // 记录初始容量,用于 retrieveAll 的缩容 buffer_(len + head_), // 总容量 = 用户要求 + 8字节 prepend tail_(head_) // tail_ 也从 8 开始,初始可读字节为 0 初始状态(len = 2048): ...

March 5, 2025 · 9 min · 1791 words

trantor 日志系统

第 1 课:trantor 日志系统 对应源文件: trantor/utils/LogStream.h — 底层缓冲区 + 流写入 trantor/utils/Logger.h — 核心日志类 + 所有宏定义 trantor/utils/AsyncFileLogger.h — 异步文件写入 一、整体架构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 用户代码: LOG_INFO << "玩家登录" << playerId; │ ▼ [Logger 对象 (临时变量)] 构造时:记录时间/文件名/行号/级别 .stream() 返回 LogStream& │ ▼ [LogStream 对象] operator<< 链式写入 数据落入 FixedBuffer (4000字节栈内存) │ ▼ [Logger 析构时] 调用 outputFunc_(index)(buf, len) 默认 → fwrite(stdout) 生产环境 → AsyncFileLogger::output() 核心思想:Logger 是一个临时对象,构造写头,析构输出,<< 运算符把数据攒进缓冲区。整个过程利用 C++ 的 RAII 机制自动完成,用户只需一行代码。 ...

March 4, 2025 · 7 min · 1444 words