Muduo TcpServer 中 setConnectionCallback 与 setMessageCallback 详解
setConnectionCallback 和 setMessageCallback 是 Muduo 中 TcpServer 最核心的两个回调注册接口,分别用于处理TCP 连接状态变化 和数据读写事件,是 Muduo "事件驱动" 设计的核心体现 ------ 用户无需关注底层 epoll、非阻塞 I/O 细节,只需注册业务回调即可实现网络逻辑。
一、核心定位
| 函数 | 核心作用 | 触发场景 |
|---|---|---|
setConnectionCallback |
注册 "连接回调函数",处理 TCP 连接的建立 / 断开 | 三次握手完成(连接建立)、连接关闭 / 出错 |
setMessageCallback |
注册 "消息回调函数",处理 TCP 连接上收到的数据 | 底层检测到可读事件(EPOLLIN),且读取数据到 Buffer 后 |
两者的本质是:
TcpServer将用户注册的回调传递给底层的TcpConnection,当对应事件发生时,由TcpConnection触发回调执行。
二、函数原型与回调类型
1. 先明确回调类型(Muduo 预定义)
Muduo 用 std::function 封装回调类型,定义在 muduo/net/TcpServer.h/TcpConnection.h 中:
cpp
// 连接回调类型:参数是 TcpConnection 智能指针
using ConnectionCallback = std::function<void (const TcpConnectionPtr&)>;
// 消息回调类型:参数是 连接指针 + 数据缓冲区 + 数据接收时间戳
using MessageCallback = std::function<void (const TcpConnectionPtr&, Buffer*, Timestamp)>;
2. setConnectionCallback 原型
cpp
// TcpServer 类的成员函数
void TcpServer::setConnectionCallback(const ConnectionCallback& cb) {
connectionCallback_ = cb; // 保存用户回调到 TcpServer 成员变量
}
- 入参:
ConnectionCallback类型的函数对象(可传入普通函数、lambda、bind 绑定的成员函数); - 作用:将用户定义的 "连接处理逻辑" 保存到
TcpServer,后续传递给新创建的TcpConnection。
3. setMessageCallback 原型
cpp
// TcpServer 类的成员函数
void TcpServer::setMessageCallback(const MessageCallback& cb) {
messageCallback_ = cb; // 保存用户回调到 TcpServer 成员变量
}
- 入参:
MessageCallback类型的函数对象; - 作用:将用户定义的 "数据处理逻辑" 保存到
TcpServer,后续传递给TcpConnection。
三、回调触发时机与参数解析
1. setConnectionCallback:连接事件回调
触发时机
-
连接建立 :Acceptor 调用
accept()成功(三次握手完成),创建TcpConnection后,触发connected()状态,执行回调; -
连接断开:
-
主动关闭:调用
TcpConnection::shutdown()完成四次挥手; -
被动关闭:对端关闭连接 / 网络异常 / 超时,底层检测到 EPOLLRDHUP/EPOLLERR 事件;
-
触发
disconnected()状态,执行回调。
-
核心参数:TcpConnectionPtr
TcpConnectionPtr 是 std::shared_ptr<TcpConnection> 的别名,封装了连接的所有核心信息和操作,常用接口:
| 接口 | 作用 |
|---|---|
conn->connected() |
判断当前连接是否处于 "已建立" 状态(核心) |
conn->peerAddress() |
获取对端(客户端)的 IP:Port(InetAddress 类型,toIpPort() 转字符串) |
conn->localAddress() |
获取本地(服务器)的 IP:Port |
conn->fd() |
获取连接对应的文件描述符(fd)(一般无需直接操作) |
conn->shutdown() |
主动关闭连接(半关闭写端,触发后续断开回调) |
conn->setContext()/getContext() |
绑定自定义数据(如用户 ID、会话信息)到连接,实现连接级数据存储 |
2. setMessageCallback:数据读写回调
触发时机
-
底层
Channel检测到连接的 fd 有EPOLLIN事件(数据可读); -
TcpConnection调用handleRead(),以非阻塞方式读取数据到Buffer; -
读取完成后,触发用户注册的消息回调,传入
TcpConnectionPtr、Buffer*、Timestamp。
核心参数解析
| 参数 | 作用 |
|---|---|
const TcpConnectionPtr& conn |
当前收到数据的连接(可通过它回写数据、关闭连接等) |
Buffer* buf |
存储接收到的数据的缓冲区(Muduo 封装的 Buffer,解决粘包 / 拆包) |
Timestamp time |
数据接收完成的时间戳(Timestamp 类型,toString() 转字符串) |
Buffer 核心操作(解决 TCP 粘包 / 拆包)
Muduo Buffer 是处理流式数据的核心,常用接口:
| 接口 | 作用 |
|---|---|
buf->retrieveAllAsString() |
读取缓冲区所有数据并清空(适合 "整包处理",如回声服务器) |
buf->retrieve(n) |
读取 n 字节数据并移动读指针(适合 "定长包",如协议头固定 4 字节) |
buf->peek() |
查看缓冲区数据但不移动读指针(适合 "解析半包",如先读协议头再读包体) |
buf->readableBytes() |
获取缓冲区中可读数据的长度(字节数) |
四、底层原理:回调如何传递与触发
1. 回调传递流程
TcpServer::setXxxCallback() → 保存到 TcpServer 成员变量
↓
TcpServer::newConnection()(Acceptor 接收新连接时调用)
↓
创建 TcpConnection 对象 → 将 TcpServer 保存的回调传递给 TcpConnection
↓
TcpConnection 将回调绑定到 Channel 的事件处理(读事件/连接事件)
2. 回调执行线程模型
Muduo 是 "单 Reactor 多线程" 模型,回调执行线程有明确规则:
-
ConnectionCallback:在 主线程(Acceptor 所在的 EventLoop) 执行(因为新连接的创建由主线程的 Acceptor 处理); -
MessageCallback:在 工作线程(EventLoopThreadPool 中的子线程) 执行(TcpConnection 的 IO 事件被分配到工作线程的 EventLoop)。
⚠️ 注意:若回调中操作共享资源(如全局计数器、数据库连接池),需加锁保证线程安全;若需跨线程操作(如工作线程回调中更新主线程的日志),需用 EventLoop::runInLoop() 提交任务。
五、关键注意事项
1. 回调函数不能阻塞
ConnectionCallback阻塞会导致主线程(Reactor)无法处理新连接 / 其他事件;MessageCallback阻塞会导致工作线程无法处理其他连接的消息;
2. 线程安全
TcpConnection的成员函数并非全部线程安全:- 安全的:
connected()、peerAddress()、getLoop()、send()(内部会通过runInLoop切换到 IO 线程); - 不安全的:直接操作
Channel、Buffer等底层成员;
- 安全的:
- 若在非 IO 线程(如业务线程)操作
TcpConnection,必须通过conn->getLoop()->runInLoop()提交任务到 IO 线程执行。
3. 避免空回调
若未注册 MessageCallback,当有数据可读时,Muduo 会直接丢弃数据;若未注册 ConnectionCallback,连接事件会被忽略(无业务影响,但建议注册以监控连接状态)。
4. 数据发送的注意事项
conn->send()是异步的:若数据无法一次性写入 fd,Muduo 会将数据存入写缓冲区,等待EPOLLOUT事件触发后继续发送;- 若需确认数据发送完成,可注册
WriteCompleteCallback(TcpServer::setWriteCompleteCallback)。
5.业务线程
Muduo 规定:一个 TcpConnection 的所有 IO 操作(send/shutdown/ 修改 Channel 等),必须在其所属的 IO 线程执行。
- 业务线程的职责是处理 "耗时逻辑",不允许直接操作 IO 组件(如
conn->send()); - 若业务线程直接调用
conn->send(),即使 Muduo 内部做了线程安全封装(最终还是会切到 IO 线程),但显式通过runInLoop提交更高效、更符合规范,也能避免批量操作时的跨线程调度开销。