DNS 解析 — Resolver、NormalResolver、AresResolver

第 16 课:DNS 解析 — Resolver、NormalResolver、AresResolver 对应源文件: trantor/net/Resolver.h — 异步 DNS 解析抽象接口 trantor/net/inner/NormalResolver.h / NormalResolver.cc — 基于 getaddrinfo 的线程池实现 trantor/net/inner/AresResolver.h — 基于 c-ares 的真异步实现 一、为什么 DNS 解析需要异步? getaddrinfo() 是系统标准 DNS 解析函数,但它是阻塞的——在 DNS 服务器响应之前,调用线程会一直挂起。 在 EventLoop 单线程模型中,如果直接调用 getaddrinfo(): EventLoop 线程被阻塞 该线程上所有其他连接的 I/O 事件、定时器全部停止响应 哪怕只是 100ms 的 DNS 查询,对游戏服务器来说都是灾难性的抖动 trantor 提供两种解决方案: 1 2 3 4 5 6 7 8 9 10 11 12 DNS 解析请求 │ ├── NormalResolver(默认) │ → 投递到 ConcurrentTaskQueue(阻塞线程池) │ → getaddrinfo() 在工作线程里阻塞 │ → 完成后回调(在工作线程里直接调用) │ └── AresResolver(c-ares 可用时) → 在 EventLoop 线程里全程非阻塞 → c-ares 管理 DNS socket,注册到 epoll → EventLoop 处理 DNS socket 的读写事件 → 完成后在 EventLoop 线程里调用回调 二、Resolver — 统一抽象接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Resolver { public: using Callback = std::function<void(const trantor::InetAddress&)>; using ResolverResultsCallback = std::function<void(const std::vector<trantor::InetAddress>&)>; // 工厂函数:根据编译配置选择实现 static std::shared_ptr<Resolver> newResolver( EventLoop *loop = nullptr, size_t timeout = 60); // timeout:DNS 缓存有效期(秒) // 解析单个地址 virtual void resolve(const std::string& hostname, const Callback& callback) = 0; // 解析所有地址(A/AAAA 记录,可能有多个 IP) virtual void resolve(const std::string& hostname, const ResolverResultsCallback& callback) = 0; static bool isCAresUsed(); // 当前是否在用 c-ares }; 两个回调的区别: ...

April 5, 2025 · 10 min · 1921 words

TLS 安全通信

第 15 课:TLS 安全通信 对应源文件: trantor/net/TLSPolicy.h — TLS 策略配置(证书、验证规则、ALPN 等) trantor/net/Certificate.h — 证书抽象接口 trantor/net/inner/TLSProvider.h — TLS 提供者抽象基类(策略模式) 具体实现(未深入):OpenSSLProvider.cc(OpenSSL 后端)、BotanTLSProvider.cc(Botan 后端) 一、TLS 在 trantor 中的架构 trantor 的 TLS 是完全透明的——插入到 TcpConnectionImpl 和用户代码之间,用户几乎感知不到加密的存在: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 用户代码(send/recvMsgCallback) │ ▲ │ 明文数据 │ 解密后的明文 ▼ │ ┌───────────────────────────┐ │ TLSProvider │ ← 透明加密/解密层 │ startEncryption() │ │ sendData(明文) → 密文 │ │ recvData(密文) → 明文 │ └───────────────────────────┘ │ ▲ │ TLS 密文 │ 从 socket 读到的密文 ▼ │ TcpConnectionImpl(writeRaw / readBuffer_) │ ▼ TCP socket(内核) 策略模式(Strategy Pattern) TLSProvider 是一个纯虚接口,具体的 TLS 实现(OpenSSL、Botan)是策略类: ...

April 1, 2025 · 9 min · 1902 words

任务队列 — TaskQueue、SerialTaskQueue、ConcurrentTaskQueue

第 14 课:任务队列 — TaskQueue、SerialTaskQueue、ConcurrentTaskQueue 对应源文件: trantor/utils/TaskQueue.h — 抽象基类 trantor/utils/SerialTaskQueue.h / SerialTaskQueue.cc — 串行执行队列 trantor/utils/ConcurrentTaskQueue.h / ConcurrentTaskQueue.cc — 并发线程池队列 一、为什么需要 TaskQueue? EventLoop 是单线程的,其中不能执行任何阻塞操作(数据库查询、文件 I/O、耗时计算),否则整条链路的 I/O 响应都会被拖慢。 TaskQueue 提供了一个"卸载阻塞任务"的机制: 1 2 3 4 5 6 [EventLoop 线程] [TaskQueue 线程] 收到玩家请求 │ → 投递到 TaskQueue ────────────►│ 执行 DB 查询(可阻塞) → 立即返回,处理下一个事件 │ 查询完成 │ → 回调投递回 EventLoop ◄── 收到结果,发送响应 ────────────┘ 这是异步编程的基本模式:不阻塞事件循环,把耗时操作委托给专用线程。 二、TaskQueue — 抽象基类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class TaskQueue : public NonCopyable { public: // 纯虚:子类实现具体的投递方式 virtual void runTaskInQueue(const std::function<void()> &task) = 0; virtual void runTaskInQueue(std::function<void()> &&task) = 0; virtual std::string getName() const { return ""; } // 同步执行:投递任务并阻塞等待完成(基类实现,子类免费获得) void syncTaskInQueue(const std::function<void()> &task) { std::promise<int> prom; std::future<int> fut = prom.get_future(); runTaskInQueue([&]() { task(); prom.set_value(1); // 任务完成,解锁调用方 }); fut.get(); // 阻塞等待 } }; syncTaskInQueue 的精妙之处: ...

March 29, 2025 · 8 min · 1504 words

多线程 EventLoop — EventLoopThread & EventLoopThreadPool

第 13 课:多线程 EventLoop — EventLoopThread & EventLoopThreadPool 对应源文件: trantor/net/EventLoopThread.h / EventLoopThread.cc — 在独立线程运行一个 EventLoop trantor/net/EventLoopThreadPool.h / EventLoopThreadPool.cc — EventLoop 线程池 一、为什么需要这两个类? 在第 12 课里,我们看到 TcpServer::setIoLoopNum(4) 内部创建了一个 EventLoopThreadPool。它们解决的核心问题是: 如何安全地在新线程里创建 EventLoop,并确保 EventLoop 真正开始运行后再返回给调用者? 这看起来简单,实际上有一个微妙的同步问题: EventLoop 对象必须在它将要运行的线程里创建(t_loopInThisThread 线程局部变量) 调用者拿到 EventLoop * 之前,要保证该指针有效(对象已创建) run() 返回之前,要保证 EventLoop 确实进入了 loop() 主循环(否则第一个 runInLoop 可能无法立刻执行) trantor 用三个 std::promise 精确解决了这个三阶段同步问题。 二、EventLoopThread 的三阶段启动协议 2.1 成员变量一览 1 2 3 4 5 6 7 8 std::shared_ptr<EventLoop> loop_; // EventLoop 对象(新线程里创建) std::mutex loopMutex_; // 保护 loop_ 的读写(析构时用) std::string loopThreadName_; // 线程名(prctl 设置) std::promise<std::shared_ptr<EventLoop>> promiseForLoopPointer_; // ① EventLoop 指针就绪 std::promise<int> promiseForRun_; // ② "请开始循环"信号 std::promise<int> promiseForLoop_; // ③ "循环已在运行"确认 std::once_flag once_; // 保证 run() 只执行一次 std::thread thread_; // 实际的 OS 线程 2.2 三阶段时序图 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 主线程 新线程(loopFuncs) │ │ │ EventLoopThread(name) │ │ → thread_ = std::thread(loopFuncs) │ 线程启动 │ → f = promiseForLoopPointer_.get_future() │ │ ↓ 阻塞等待 ① │ prctl(PR_SET_NAME) │ │ loop = make_shared<EventLoop>() │ 【①】│ promiseForLoopPointer_.set_value(loop) │ ← f.get() 返回 loop 指针 │ │ → this->loop_ = loop │ promiseForLoop_: queueInLoop 注册回调 │ │ f2 = promiseForRun_.get_future() │ (构造完成,loop_ 有效,但循环未开始) │ ↓ 阻塞等待 ② │ │ │ run() │ │ → std::call_once { │ │ f3 = promiseForLoop_.get_future() │ │ 【②】promiseForRun_.set_value(1) │ │ ↓ 阻塞等待 ③ │ ← f2.get() 返回 │ │ loop->loop() 开始 │ │ → 第一次 poll │ │ → doRunInLoopFuncs() │ 【③】│ → promiseForLoop_.set_value(1) │ ← f3.get() 返回 │ (循环持续运行...) │ } │ (run() 返回,EventLoop 确保在运行中) 三个 promise 的职责: ...

March 28, 2025 · 9 min · 1757 words

TcpServer & TcpClient — 网络通信的两端

第 12 课:TcpServer & TcpClient — 网络通信的两端 对应源文件: trantor/net/TcpServer.h / TcpServer.cc — TCP 服务器 trantor/net/TcpClient.h / TcpClient.cc — TCP 客户端 一、两个类的定位 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ┌──────────────────────────────────────────┐ │ TcpServer │ │ loop_(Accept 线程) │ │ Acceptor(监听 socket) │ │ connSet_(所有连接的生命周期管理) │ │ ioLoops_(I/O 线程池) │ │ timingWheelMap_(每个 I/O 线程一个时间轮)│ └──────────────────────────────────────────┘ │ newConnection() ▼ TcpConnectionImpl(每个连接一个) 运行在 ioLoops_ 中的某个 EventLoop ┌──────────────────────────────────────────┐ │ TcpClient │ │ loop_(单一 EventLoop) │ │ connector_(发起连接) │ │ connection_(当前连接,mutex_ 保护) │ └──────────────────────────────────────────┘ TcpServer 是一对多:管理一个监听端口和大量并发连接。 TcpClient 是一对一:管理一条到服务器的连接(可断线重连)。 ...

March 25, 2025 · 11 min · 2242 words

TcpConnection — 连接生命周期

第 11 课:TcpConnection — 连接生命周期 对应源文件: trantor/net/TcpConnection.h — 公共抽象接口(用户使用) trantor/net/inner/TcpConnectionImpl.h / TcpConnectionImpl.cc — 内部实现 一、设计:接口与实现分离 1 2 3 4 5 6 7 8 9 10 11 12 TcpConnection(纯虚基类) │ 定义公共 API:send/sendFile/shutdown/forceClose/setContext... │ 存储回调:recvMsgCallback_/connectionCallback_/closeCallback_... │ └── TcpConnectionImpl(具体实现) 继承 TcpConnection + NonCopyable + enable_shared_from_this │ ├── Channel(ioChannelPtr_)— fd 事件分发 ├── Socket(socketPtr_) — RAII fd 管理 ├── MsgBuffer readBuffer_ — 接收缓冲区 ├── list<BufferNodePtr> writeBufferList_ — 发送队列 └── TLSProvider(可选) — 透明 TLS 加密层 为什么分离接口和实现? ...

March 22, 2025 · 10 min · 1966 words

Acceptor & Connector — 连接的两端

第 10 课:Acceptor & Connector — 连接的两端 对应源文件: trantor/net/inner/Acceptor.h / Acceptor.cc — 服务端:监听并接受连接 trantor/net/inner/Connector.h / Connector.cc — 客户端:主动发起连接(含重连) 一、两个类在架构中的角色 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ┌──────────────────────────┐ │ TcpServer │ │ ┌─────────────────────┐ │ │ │ Acceptor │ │ │ │ Socket(listenFd) │ │ │ │ Channel │ │ │ └─────────────────────┘ │ └──────────────────────────┘ ↑ listen 客户端发起 connect ↓ accept → 回调 newConnectionCallback_ ┌──────────────────────────┐ │ TcpClient │ │ ┌─────────────────────┐ │ │ │ Connector │ │ │ │ (非阻塞 connect) │ │ │ │ 指数退避重连 │ │ │ └─────────────────────┘ │ └──────────────────────────┘ Acceptor 和 Connector 都不拥有连接——它们的职责是把一个已就绪的 fd 交给上层(TcpServer/TcpClient),由上层创建 TcpConnection 对象来管理该 fd 的后续生命周期。 ...

March 21, 2025 · 11 min · 2276 words

网络地址与 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