C++ 网络编程 Muduo 库使用

大纲

前言

本文将介绍 Muduo 网络库的使用,在使用 Muduo 之前,建议先熟悉并掌握以下技术内容:

  • 熟悉 C++ 11 语法
  • 掌握事件驱动模型(Reactor 模式)
  • 熟悉 C/C++ 多线程并发和线程安全设计
  • 熟悉 Linux 网络编程(如使用 epollsocket

学习资源

版本说明

本文使用的各软件版本如下:

软件版本说明
C++11
Muduo2.0.2
LinuxDebian 12Muduo 只支持 Linux 平台,不支持 Windows 平台

Muduo 的介绍

网络 I/O 模型

主流的网络 I/O 模型有以下几种,Muduo 采用的是第四种(reactors in threads - one loop per thread)。

  • (1) accept + read/write

    • 不适用于并发服务器
  • (2) accept + fork - process-pre-connection

    • 适合并发连接数不大,计算任务工作量大于 Fork 的开销。
  • (3) accept + thread - thread-pre-connection

    • 比第二种网络 I/O 模型的开销小了一点,但是并发造成的线程堆积过多。
  • (4) reactors in threads - one loop per thread

    • 这是 Muduo 库的网络设计方案,底层实质上是基于 Linux 的 epoll + pthread 线程池实现,且依赖了 Boost 库,适用于并发连接数较大的场景。
    • 有一个 Main Reactor 负载 Accept 连接,然后将连接分发给某个 SubReactor(采用轮询的方式来选择 SubReactor),该连接的所用操作都在那个 SubReactor 所处的线程中完成。多个连接可能被分派到多个线程中被处理,以充分利用 CPU。
    • 有一个 Base I/O Thread 负责 Accept 新的连接,接收到新的连接以后,使用轮询的方式在 Reactor Pool 中找到合适的 SubReactor 将这个连接挂载上去,这个连接上的所有任务都在这个 SubReactor 所处的线程中完成。
    • Reactor Poll 的大小是固定的,根据 CPU 的核心数量来确定。如果有过多的耗费 CPU 资源的计算任务,可以提交到 ThreadPool 线程池中专门处理耗时的计算任务。
  • (5) reactors in process - one loop pre process

    • 这是 Nginx 服务器的网络设计方案,基于进程设计,采用多个 Reactors 充当 I/O 进程和工作进程,通过一个 accept 锁,完美解决多个 Reactors 之间的 “惊群现象”。

Muduo 的简介

Muduo 是一个用 C++ 编写的高性能、基于事件驱动的网络库,专门设计用于构建 Linux 下高并发、低延迟的网络服务,特别适合开发分布式系统、微服务、消息中间件、网络游戏服务器等后端程序。

  • 核心特性

    • 基于事件驱动模型:使用 Reactor 模式,即单线程 I/O + 多线程计算。
    • 高性能:使用 epoll I/O 多路复用技术、非阻塞 I/O、零内存拷贝技术。
    • 线程安全:网络部分是线程安全的,使用线程池和回调。
    • C++ 11 标准:需要使用支持 C++ 11 的编译器。
    • 仅支持 Linux 平台:利用 Linux 特性优化性能,不支持跨平台。
    • 可组合性强:解耦的模块设计,便于扩展和组合。
  • 核心模块

    • base(基础模块)
      • 非网络相关的通用工具
      • 如线程池、时间戳、日志系统、原子操作等
    • net(网络模块)
      • TCP 服务器 / 客户端模型
      • Reactor 事件分发器
      • Buffer、Channel、EventLoop、TcpConnection 等核心组件
  • 核心组件

    • EventLoop
      • 事件循环,是每个线程的核心对象
      • 封装了 epoll 库,处理文件描述符的读写事件
    • Channel
      • 表示一个 fd(文件描述符)及其感兴趣的事件(如读写)
      • 是 EventLoop 与具体 I/O 事件之间的桥梁
    • Poller
      • 封装 epollpoll 的接口(Muduo 默认用 epoll
    • TcpServer / TcpClient
      • 高层封装,简化服务端和客户端的使用
      • 支持多线程连接处理
    • Callback 机制
      • 所有 I/O 事件都通过用户注册的回调函数处理(高扩展性)
  • 性能优势

    • 完全采用非阻塞、异步 I/O 模型
    • 使用智能指针管理资源(如 std::shared_ptr<TcpConnection>
    • 零内存拷贝的数据缓冲机制(Buffer)
    • 合理利用多线程资源(EventLoopThreadPool)
  • 适用场景

    • 高并发 TCP 服务器(如 Redis、MQTT、游戏网关)
    • 微服务通信框架(可自定义通信协议)
    • 高性能 HTTP 服务(支持 HTTP 1.0/1.1)
    • 自研 RPC 系统

平台兼容性

  • Muduo 库只支持 Linux 平台,不兼容 Windows 平台,因为其底层使用了 Linux 平台的 pthreadepoll

Muduo 的 Reactor 模型

The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs.
The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.

从上面的描述,可以看出以下关键点:

  • 事件驱动处理
  • 可以处理一个或多个输入源
  • 通过 Service Handler 同步地将输入事件(Event)采用多路复用分发给相应的 RequestHandler(多个)来处理

学习建议

建议阅读 Muduo 的源码,从 TcpServerstart() 方法开始,阅读一下 Muduo 的底层源码实现,理解 MainReactor 和 SubReactor 的工作原理,这样有利于应付 Muduo 相关的面试问题,也能更深入的去表达 Muduo 相关的内容。

Muduo 的安装

安装 Boost 库

1
2
3
4
5
# 安装 Boost 的所有组件和头文件
sudo apt-get install -y libboost-all-dev

# 查看 Bootst 版本
sudo dpkg -s libboost-all-dev | grep Version

提示

由于 Muduo 使用了 Boost 库(如 boost::any),因此需要安装 Boost 库。

安装 Muduo 库

  • 编译安装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Git 克隆代码
git clone https://github.com/chenshuo/muduo.git

# 进入代码目录
cd muduo

# 创建构建目录
mkdir -p build

# 进入构建目录
cd build

# 生成构建文件
cmake ..

# 编译代码
make -j$(nproc)

# 执行安装
sudo make install

# 更新系统的共享库缓存
sudo ldconfig /usr/local/lib/
  • 验证安装
1
2
3
4
5
# 查看 Muduo 库的头文件
ls -al /usr/local/include/muduo

# 查看 Muduo 库的静态库
ls -al /usr/local/lib | grep muduo

提示

  • Muduo 的编译依赖 CMake 和 Boost 库,默认编译生成的是静态库(.a),如果需要编译生成共享库(.so),可以自行修改 CMakeLists.txt 中的配置。
  • Muduo 支持 C++ 11,仅支持 Linux 平台,不支持 Windows 平台,建议使用 7.x 及以后版本的 g++ 编译器。

Muduo 的使用

使用 Muduo 的日志系统

在开发软件产品过程中,日志的输出非常重要,可以记录很多软件运行过程中的信息,方便定位调试问题和跟踪统计信息等等。

  • Muduo 提供的日志级别
1
2
3
4
5
6
7
8
#define LOG_TRACE if (muduo::Logger::logLevel() <= muduo::Logger::TRACE) muduo::Logger(__FILE__, __LINE__, muduo::Logger::TRACE, __func__).stream()
#define LOG_DEBUG if (muduo::Logger::logLevel() <= muduo::Logger::DEBUG) muduo::Logger(__FILE__, __LINE__, muduo::Logger::DEBUG, __func__).stream()
#define LOG_INFO if (muduo::Logger::logLevel() <= muduo::Logger::INFO) muduo::Logger(__FILE__, __LINE__).stream()
#define LOG_WARN muduo::Logger(__FILE__, __LINE__, muduo::Logger::WARN).stream()
#define LOG_ERROR muduo::Logger(__FILE__, __LINE__, muduo::Logger::ERROR).stream()
#define LOG_FATAL muduo::Logger(__FILE__, __LINE__, muduo::Logger::FATAL).stream()
#define LOG_SYSERR muduo::Logger(__FILE__, __LINE__, false).stream()
#define LOG_SYSFATAL muduo::Logger(__FILE__, __LINE__, true).stream()
  • Muduo 日志级别的使用
1
2
3
4
5
6
7
#include <muduo/base/Logging.h>

int main() {
LOG_INFO << "这是一个 info 日志";
LOG_WARN << "这是一个 warn 日志";
LOG_ERROR << "这是一个 error 日志";
}

程序运行输出的结果:

1
2
3
20250522 22:34:32.067870Z 22233 INFO  这是一个 info 日志 - main.cpp:45
20250522 22:34:32.067995Z 22233 WARN 这是一个 warn 日志 - main.cpp:46
20250522 22:34:32.068365Z 22233 ERROR 这是一个 error 日志 - main.cpp:47

实现 TCP 客户端和服务端

实现基于 Muduo 网络库的 TCP 客户端和服务端程序,只需要简单地组合使用 TcpServerTcpClient 就可以。

TCP 服务端代码

  • muduo_server.h 源文件
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <muduo/net/EventLoop.h>
#include <muduo/net/TcpServer.h>

// 基于Muduo开发服务端程序
class ChatServer {
public:
// 构造函数
ChatServer(muduo::net::EventLoop *loop, const muduo::net::InetAddress &listenAddr, const std::string &nameArg)
: _server(loop, listenAddr, nameArg), _loop(loop) {
// 设置服务端注册用户连接的创建和断开回调
_server.setConnectionCallback(std::bind(&ChatServer::onConnection, this, std::placeholders::_1));

// 设置服务端注册用户读写事件的回调
_server.setMessageCallback(std::bind(&ChatServer::onMessage, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3));

// 设置EventLoop的线程数量(比如:1个I/O线程,3个Worker线程)
_server.setThreadNum(4);
}

// 析构函数
~ChatServer() {
}

// 启动服务端
void start() {
// 开启事件循环处理
_server.start();
}

private:
// 处理用户的连接创建和断开
void onConnection(const muduo::net::TcpConnectionPtr &conn) {
// 连接创建
if (conn->connected()) {
std::cout << "==> server " << conn->peerAddress().toIpPort() << " -> " << conn->localAddress().toIpPort()
<< " state: online" << std::endl;
}
// 连接断开
else {
std::cout << "==> server " << conn->peerAddress().toIpPort() << " -> " << conn->localAddress().toIpPort()
<< " state: offline" << std::endl;
// 断开连接(释放资源)
conn->shutdown();
}
}

// 处理用户读写事件(比如接收客户端发送的数据)
void onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buffer, muduo::Timestamp time) {
std::string message = buffer->retrieveAllAsString();
std::cout << "==> server receive message: " << message << ", time: " << time.toFormattedString(false)
<< std::endl;
}

muduo::net::TcpServer _server;
muduo::net::EventLoop *_loop;
};
  • 当需要测试 TCP 服务端代码时,可以执行 telnet 127.0.0.1 6000 命令启动一个 TCP 客户端,然后输入要发送的内容,最后按下回车键即可发送数据给 TCP 服务端。
  • 当需要断开 Telnet 连接(TCP 客户端连接)时,可以使用快捷键 ctrl + ] 切换到一个特殊的 Telnet 命令行界面;然后在这个界面输入 quit 命令,就可以退出 Telnet 命令行界面。
  • 或者,推荐直接使用 Linux 的 nc 127.0.0.1 6000 命令来启动一个 TCP 客户端,然后再发送数据给 TCP 服务端,断开 TCP 客户端连接只需要使用快捷键 ctrl + c 关闭 nc 命令的进程即可。

TCP 客户端代码

  • muduo_client.h 源文件
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#pragma once

#include <iostream>
#include <string>
#include <muduo/net/EventLoop.h>
#include <muduo/net/TcpClient.h>

class ChatClient {
public:
// 构造函数
ChatClient(muduo::net::EventLoop* loop, const muduo::net::InetAddress& serverAddr, const std::string& nameArg)
: _client(loop, serverAddr, nameArg), _loop(loop) {
// 设置客户端TCP连接的回调
_client.setConnectionCallback(std::bind(&ChatClient::onConnection, this, std::placeholders::_1));

// 设置客户端接收数据的回调
_client.setMessageCallback(std::bind(&ChatClient::onMessage, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3));
}

// 析构函数
~ChatClient() {
// 发起断开连接
_client.disconnect();
// 停止内部 Connector 的重连机制,避免异步行为
_client.stop();
}

// 连接服务端
void connect() {
_client.connect();
}

private:
// 客户端绑定连接回调函数,当连接或者断开服务端时调用
void onConnection(const muduo::net::TcpConnectionPtr& conn) {
// 连接创建
if (conn->connected()) {
std::cout << "==> client " << conn->localAddress().toIpPort() << " -> " << conn->peerAddress().toIpPort()
<< " state: connected" << std::endl;
}
// 连接断开
else {
std::cout << "==> client " << conn->localAddress().toIpPort() << " -> " << conn->peerAddress().toIpPort()
<< " state: disconnected" << std::endl;
// 断开连接(释放资源)
conn->shutdown();
}
}

// 客户端绑定消息回调函数,当有数据接收时调用
void onMessage(const muduo::net::TcpConnectionPtr& conn, muduo::net::Buffer* buf, muduo::Timestamp time) {
std::string message = buf->retrieveAllAsString();
std::cout << "==> client receive message: " << message << ", time: " << time.toFormattedString(false)
<< std::endl;
}

muduo::net::TcpClient _client;
muduo::net::EventLoop* _loop;
};
  • 当需要测试 TCP 客户端代码时,可以执行 Linux 的 nc -l 127.0.0.1 6000 命令启动一个 TCP 服务端,然后输入要发送的内容,最后按下回车键即可发送数据给 TCP 客户端。
  • 当需要关闭 TCP 服务端(断开 TCP 客户端连接)时,只需要使用快捷键 ctrl + c 关闭 nc 命令的进程即可。

TCP 连接测试代码

  • test.cpp 源文件
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <string>
#include <thread>
#include <muduo/net/EventLoop.h>
#include <muduo/net/TcpServer.h>
#include "muduo_server.h"
#include "muduo_client.h"

// 初始化服务端
void initServer() {
// 创建服务端
muduo::net::EventLoop loop;
muduo::net::InetAddress addr("127.0.0.1", 6000);
ChatServer server(&loop, addr, "ChatServer");

// 启动服务端
server.start();

// 以阻塞方式等待新客户端的连接、已连接客户端的读写事件等
loop.loop();
}

// 初始化客户端
void initClient() {
// 创建客户端
muduo::net::EventLoop loop;
muduo::net::InetAddress addr("127.0.0.1", 6000);
ChatClient client(&loop, addr, "ChatClient");

// 连接服务端
client.connect();

// 以阻塞方式等待服务端发送的数据
loop.loop();
}

int main() {
// 启动服务端线程
std::thread serverThread(initServer);
serverThread.detach();

// 等待一会,让服务端线程先启动
std::this_thread::sleep_for(std::chrono::seconds(5));

// 启动客户端线程
std::thread clientThread(initClient);
clientThread.detach();

// 阻塞等待用户按下任意键,然后结束程序运行
char c = getchar();

return 0;
}

程序运行后输出的结果:

1
2
==> client 127.0.0.1:45636 -> 127.0.0.1:6000 state: connected
==> server 127.0.0.1:45636 -> 127.0.0.1:6000 state: online

使用 Muduo 的线程池执行任务

Muduo 线程池的介绍

采用 Muduo 进行服务器编程时,如果遇到需要开辟多个线程来单独处理复杂的计算任务或者其它阻塞任务,那么就不需要手动调用 pthread_create() 来创建线程了;因为 Muduo 提供的 ThreadPool 线程池管理类已经将 Linux 的线程创建完全封装起来了。如果想研究 Muduo 线程池的底层源码,可以剖析 Muduo 项目中的 ThreadPool.ccThread.cc 源文件。ThreadPool 的使用示例如下:

1
2
3
4
5
// 线程池
muduo::ThreadPool _pool;

// 在单独的线程中接收客户端的连接,并执行相应的操作
void userClient(const muduo::net::TcpConnectionPtr &con);
1
2
3
4
5
6
7
8
9
10
11
// 客户端与服务器连接成功
if (con->connected()) {
LOG_INFO << "connection opened!";

// 让线程池分配线程专门处理客户端连接
_pool.run(bind(&ChatClient::userClient, this, conn));
}
// 客户端与服务器断开连接
else {
LOG_INFO << "connection closed!";
}

Muduo 线程池的使用

这里将介绍如何使用 Muduo 的线程池来执行多个计算任务(并行计算)。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <atomic>
#include <muduo/base/CountDownLatch.h>
#include <muduo/base/ThreadPool.h>

using ULong = unsigned long long;

int main() {
const int threadNumber = 8;
const ULong kRange = 100000000;

// 创建并启动 Muduo 线程池
muduo::ThreadPool pool;
pool.setMaxQueueSize(100);
pool.start(threadNumber);

// 用于汇总所有线程的计算结果
std::atomic<ULong> totalSum(0);

// 用于等待所有任务执行完成
muduo::CountDownLatch latch(threadNumber);

// 将 [1, kRange] 平均分成 N 段,每段提交一个任务
ULong blockSize = kRange / threadNumber;
for (int i = 0; i < threadNumber; ++i) {
ULong start = i * blockSize + 1;
ULong end = (i == threadNumber - 1) ? kRange : (i + 1) * blockSize;

// 添加任务到线程池
pool.run([start, end, &totalSum, &latch]() {
// 执行累加计算
ULong localSum = 0;
for (ULong x = start; x <= end; ++x) {
localSum += x;
}
// 原子地累加到总和
totalSum.fetch_add(localSum, std::memory_order_relaxed);
// 标记此任务完成
latch.countDown();
});
}

// 阻塞等待所有线程完成计算
latch.wait();

// 输出计算结果
std::cout << "1 + 2 + ... + " << kRange << " = " << totalSum.load() << std::endl;

// 关闭线程池
pool.stop();

return 0;
}

程序运行后输出的结果:

1
1 + 2 + ... + 100000000 = 5000000050000000

Muduo 的原理

Muduo 是一个高性能的 Reactor 模式网络库,采用 one loop per thread + thread pool 的架构实现,其代码简洁、逻辑清晰,是学习高性能网络编程的优秀范例。

整体类图

下面这张图是陈硕提供的 Muduo 网络库的类图(图中灰色的类是内部类,白色的是外部类)。

整体架构

Muduo 的代码结构主要分为两个模块:basenet,其中 base 模块实现了一些通用的基础功能,而 net 模块基于 base 提供的工具类,构建了更高层次的网络编程抽象。

base 模块

base 模块实现了一些通用的基础功能,例如日志系统(log)、线程(thread)、线程池(threadpool)、互斥锁(mutex)、队列(queue)等。这些组件不仅在网络模块中被广泛复用,也为构建高性能程序提供了良好的基础。base 模块的设计强调高内聚、低耦合,模块之间依赖关系清晰,职责划分明确,代码风格一致,逻辑简单直接。因此,即使是初学者,在阅读 base 源码时也能较容易理解其实现思路和设计意图。由于 base 模块本身并不涉及复杂的网络细节,这里不做深入探讨,重点将放在更具挑战性的 net 模块上,分析其在事件驱动、连接管理及多线程调度等方面的实现细节。

net 模块

net 模块基于 base 提供的工具类,构建了更高层次的网络编程抽象。网络编程的本质在于对底层 socket 接口以及 I/O 多路复用机制(如 pollepoll)的封装,目的在于提升易用性、可维护性,同时屏蔽掉底层 API 中存在的一些坑。在实现了基本的网络 I/O 能力后,系统性能与并发处理能力便成为网络库设计的关键。Muduo 使用基于 poll / epoll 的 I/O 多路复用模型构建事件驱动系统,并采用 one loop per thread 的架构设计,即每个 I/O 线程拥有一个独立的事件循环 EventLoop。这种设计结合线程池机制,使得多个线程可以并行处理不同连接或任务,从而充分发挥多核硬件的性能优势,实现高性能、高并发的网络处理能力。net 模块的封装较为彻底,向上层应用提供了简洁易用的接口,隐藏了复杂的内部实现逻辑。得益于这种设计,开发者在使用 Muduo 进行网络编程时,能够专注于业务逻辑本身,而无需深入底层处理细节。

整体设计

EventLoop

EventLoop 事件循环(即 Reactor 反应器),负责 I/O 事件和定时器事件的分派,每个线程只能有一个 EventLoop 实体。

  • 使用 TimerQueue 作为计时器管理。
  • 使用 Poller 作为 I/O 多路复用机制。
TimerQueue

TimerQueue 底层使用 timerfd_* 系列函数将定时器转换为 fd,并添加到事件循环中。

  • 当时间到达后,这些 fd 会自动触发事件。
  • 内部使用 std::set 管理所有注册的 Timer
  • 由于 set 有自动排序功能,所以事件循环中总是优先处理最早到期的 Timer。
Poller 抽象类

Poller 是 I/O 多路复用的抽象类。

  • 其具体实现由子类 PollPoller(封装了 poll)和 EpollPoller(封装了 epoll)完成。
  • Pooler 是 Muduo 中唯一使用面向对象(虚函数)实现的模块,提供了回调功能。
  • Poller 中的 updateChannel() 函数用于注册和更新 fd 的事件,所有的 fd 都必须通过它添加到事件循环中。
任务队列

除了管理定时器和 I/O 事件外,EventLoop 还包含一个任务队列。

  • 任务队列用于执行一些计算任务,可由其他线程添加到任务进队列中。
  • EventLoop 在处理完 I/O 事件后,会依次执行队列中的任务。
  • 多线程处理共享资源时,可以通过将操作提交到某个 EventLoop 的任务队列中,让该线程负责资源管理,其他线程无需加锁,只需加锁队列即可,从而简化并发模型,减少锁的使用。

异步唤醒线程:

  • 如果 EventLoop 阻塞在 epoll_wait 中,则会无法及时处理队列中的任务。
  • 因此需要一种通信机制来唤醒 EventLoop 所在线程,被唤醒后就取出队列中的任务进行执行。
  • Muduo 使用 eventfd(2) 来实现异步唤醒。

Channel 事件包装器

在 Muduo 中,通过 Channelfd 进行封装。更准确地说,是对 fd 的事件相关方法的封装。

  • 它负责注册 fd 的可读或者可写事件到 EventLoop
  • 以及事件产生时,定义事件如何响应。
  • 每个 fd 对应一个 Channel(聚合关系)。
  • Channel 析构时不会 close 掉这个 fd
  • 提供 handleEvent() 函数,当 fd 有事件产生时,EventLoop 会调用它进行处理。
  • handleEvent() 的内部会根据可读或者可写事件分别调用已注册的回调函数。

通常 Channel 被作为其他类的成员来使用,例如:

  • EventLoop 内部通过 std::vector<Channel*> 管理多个注册的 fd
  • EventLoop 与多个 Channel 形成一对多的关系。

Socket 封装类

Socket 也是对 fd 的封装,但不同于 Channel

  • Socket 仅封装由 ::socket 系统调用产生的 fd
  • 提供获取 / 设置网络连接属性的方法(如地址复用、关闭 Nagle 算法等)。
  • fd 是组合关系,Socket 析构时会自动 close 这个 fd

尽管有 SocketChannel 的封装,某些系统调用仍然需要直接传递原始 fd,因此实际代码中常见同时持有 fdSocketChannel 的情况。

TcpConnection 抽象类

TcpConnection 是对一个 TCP 连接的抽象。

  • 一个 TcpConnection 包含一个 Socket 和一个 Channel
  • 在构造时就为 Channel 注册好了事件回调:
    • handleRead():处理可读事件
    • handleWrite():处理可写事件

当有事件触发后:

  • handleRead() 会先处理接收数据。
  • 然后转交给上层逻辑(调用上层注册的回调函数)。
  • 回调函数由用户通过 TcpConnection 注册,内部会传递给 Channel
  • 因为 Channel 是个内部类,所以回调函数的注册只能由 TcpConnection 负责,上层无需接触 Channel,只需将回调函数注册到 TcpConnection 即可。

为什么需要 TcpConnection?

  • 为了解决 TCP 协议下收发数据的阻塞问题
  • 比如调用 ::write 时,若内核缓冲区已满,则写操作会阻塞。
  • 一个优秀的网络库应允许上层调用一次 sendMessage() 就能完成数据发送,而非频繁判断是否可写。
  • TcpConnection 内部维护了 inputBufferoutputBuffer
    • 支持数据缓存和分批发送。
    • 保证在连接断开前,数据能够全部发送完成。

Acceptor 连接接收器

Acceptor 用来接受新连接。

  • 内部维护一个 listenfd 和对应的 Channel
  • 初始化时执行:
    • ::bind
    • ::listen
    • 并将 listenfd 注册到事件循环。

当有新连接到来:

  • 触发 ChannelhandleRead(),调用 accept() 接收连接。
  • 再调用上层注册的 newConnectionCallback
  • AcceptorTcpServer 的内部类,回调由 TcpServer 注册。
  • TcpServer 会为每个新连接创建一个 TcpConnection,并设置相应的回调函数。
  • 所有连接通过一个 mapTcpServer 管理。

并发连接的处理:

  • TcpServer 使用线程池处理并发请求。
  • 线程池包含多个 I/O 线程(即多个 EventLoop)。
  • 每个连接会自动分配给某个线程的 EventLoop,在其上创建 TcpConnection
  • 大幅提高了服务器的并发处理能力。

Connector 和 TcpClient

  • ConnectorTcpClient 的关系类似于 AcceptorTcpServer
  • 区别在于 Connector 用于主动发起连接。
  • 负责连接重试、超时等连接建立相关逻辑。

参考资料