在Reactor模式网络库中,Connection类是当之无愧的"核心枢纽"------它封装了已建立的TCP连接,串联起事件监听、数据缓冲、状态管理、回调分发等所有核心操作,是连接底层I/O与上层业务逻辑的关键桥梁。
不同于其他单一职责的组件(如Channel封装fd、Poller封装epoll),Connection类需要处理复杂的多场景交互:既要响应底层I/O事件,又要管理连接生命周期,还要保证线程安全、处理异常情况,其设计复杂度远超网络库中的其他组件。
今天,我们就来逐行拆解Connection类的设计与实现,从状态机、双层回调、线程安全到智能指针管理,搞懂它的核心逻辑与设计巧思。
目录
- Connection类概述:核心定位与架构位置
- 连接状态机:四状态管控生命周期
- 双层回调机制:解耦I/O与业务逻辑
- 线程安全设计:RunInLoop模式的妙用
- 智能指针与生命周期管理:避免悬空指针的关键
- 非活跃连接销毁:防止资源泄漏
- 数据收发流程:从缓冲区到网络传输
- 协议升级机制:适配多协议通信
- 完整代码注释:手把手看懂实现
- 总结:Connection类的设计精髓
1. Connection类概述:核心定位与架构位置
Connection类的核心职责的是"封装TCP连接的全生命周期",具体来说,它要处理以下5件事:
- 事件处理:通过Channel监听conn_fd的读、写、关闭、错误四种事件;
- 数据缓冲:管理输入(接收数据)、输出(发送数据)缓冲区,解决TCP粘包/拆包问题;
- 状态管理:维护连接从建立到关闭的全状态,避免非法操作;
- 回调分发:将底层I/O事件转换为上层业务回调,让用户无需关心底层细节;
- 超时管理:支持非活跃连接自动销毁,释放系统资源。
1.1 核心成员变量
先看核心成员变量(精简版,保留关键),理解它的组成结构:
cpp
class Connection : public std::enable_shared_from_this<Connection> {
private:
// 基础属性
uint64_t _conn_id; // 连接唯一ID(也用作定时器ID)
int _sockfd; // 连接对应的文件描述符
ConnStatu _statu; // 连接状态(四状态)
EventLoop *_loop; // 所属的EventLoop(线程绑定)
// 功能组件
Socket _socket; // 套接字操作封装(bind/listen/accept等)
Channel _channel; // 事件监控管理(绑定conn_fd和回调)
Buffer _in_buffer; // 输入缓冲区(接收数据)
Buffer _out_buffer; // 输出缓冲区(发送数据)
Any _context; // 用户上下文(存储协议解析状态等)
// 超时管理
bool _enable_inactive_release; // 是否启用非活跃连接销毁
// 回调函数(双层回调)
ConnectedCallback _connected_callback; // 连接建立回调(用户)
MessageCallback _message_callback; // 消息到达回调(用户)
ClosedCallback _closed_callback; // 连接关闭回调(用户)
ClosedCallback _server_closed_callback; // 内部回调(TcpServer移除连接)
};
1.2 架构位置
Connection在Reactor架构中的位置十分关键,它上承TcpServer,下接EventLoop和操作系统TCP连接,内部整合多个功能组件:
TcpServer负责创建和管理所有Connection;Connection内部通过Channel注册到EventLoop,由EventLoop驱动其事件处理;Socket封装底层套接字操作,缓冲区负责数据暂存,最终与操作系统的TCP连接完成数据交互。
简单来说:Connection是TCP连接的"软件封装",是Reactor模式中"连接管理"的唯一入口。
2. 连接状态机:四状态管控生命周期
连接的生命周期存在多种状态,不同状态下的操作权限不同(比如断开状态不能发送数据),因此Connection类设计了四状态有限状态机,严格管控状态转换,避免非法操作。
2.1 四种核心状态
cpp
typedef enum {
DISCONNECTED, // 连接已关闭(初始/最终状态)
CONNECTING, // 连接刚建立,待初始化(半连接状态)
CONNECTED, // 连接就绪,可以正常通信(核心工作状态)
DISCONNECTING // 正在关闭中(还有数据待发送,半关闭状态)
} ConnStatu;
2.2 状态转换逻辑
状态转换有严格的规则,不能跨状态跳转,核心转换流程如下:
- 初始状态:accept()成功后,创建Connection,状态设为CONNECTING;
- CONNECTING → CONNECTED:调用Established(),启动读事件监控,触发连接建立回调;
- CONNECTED → DISCONNECTING:调用Shutdown(),或读取出错,进入半关闭状态;
- DISCONNECTING → DISCONNECTED:所有待发送数据发送完毕,调用Release(),关闭连接、移除事件监控;
- 任意状态 → DISCONNECTED:发生错误或主动调用Release(),直接关闭连接。
2.3 核心状态转换代码
cpp
// CONNECTING → CONNECTED(连接就绪)
void EstablishedInLoop() {
assert(_statu == CONNECTING); // 断言,防止非法状态转换
_statu = CONNECTED;
_channel.EnableRead(); // 启动读事件监控,准备接收数据
if (_connected_callback)
_connected_callback(shared_from_this()); // 触发用户回调
}
// CONNECTED → DISCONNECTING(半关闭)
void ShutdownInLoop() {
_statu = DISCONNECTING;
// 若还有待发送数据,启动写监控,发送完成后再关闭
if (_out_buffer.ReadAbleSize() == 0) {
Release();
}
}
// 任意状态 → DISCONNECTED(最终关闭)
void ReleaseInLoop() {
_statu = DISCONNECTED;
_channel.Remove(); // 从EventLoop中移除Channel
_socket.Close(); // 关闭套接字
// 触发关闭回调,通知TcpServer移除连接
if (_closed_callback) _closed_callback(shared_from_this());
if (_server_closed_callback) _server_closed_callback(shared_from_this());
}
3. 双层回调机制:解耦I/O与业务逻辑
双层回调是Connection类最核心的设计之一,其核心思想是"分离底层I/O处理和上层业务逻辑",让Connection负责I/O细节,用户只关心业务实现。
3.1 两层回调的结构
回调分为两层,职责清晰、设置者不同:
| 层次 | 职责 | 设置者 | 核心回调函数 |
|---|---|---|---|
| 第一层(Channel回调) | 处理底层I/O事件(读/写/关闭/错误) | Connection自身 | HandleRead/HandleWrite等 |
| 第二层(业务回调) | 处理上层业务逻辑(消息解析、业务处理) | 用户(通过TcpServer) | OnMessage/OnConnection等 |
3.2 回调设置时机
两层回调的设置时机不同,确保初始化顺序正确:
cpp
// 1. 第一层回调:Connection构造函数中设置(自身管理I/O回调)
Connection(EventLoop *loop, uint64_t conn_id, int sockfd)
: _conn_id(conn_id), _sockfd(sockfd), _loop(loop),
_statu(CONNECTING), _socket(_sockfd), _channel(loop, _sockfd)
{
// 将Channel的回调绑定到Connection的成员函数
_channel.SetReadCallback(std::bind(&Connection::HandleRead, this));
_channel.SetWriteCallback(std::bind(&Connection::HandleWrite, this));
_channel.SetCloseCallback(std::bind(&Connection::HandleClose, this));
_channel.SetErrorCallback(std::bind(&Connection::HandleError, this));
}
// 2. 第二层回调:TcpServer创建Connection后设置(用户业务回调)
// TcpServer::OnNewConnection() 中
conn->SetConnectedCallback(_connected_callback); // 用户的连接建立回调
conn->SetMessageCallback(_message_callback); // 用户的消息处理回调
conn->SetClosedCallback(_closed_callback); // 用户的连接关闭回调
3.3 读事件完整触发链(核心流程)
最能体现双层回调的是读事件触发流程,从客户端发数据到用户处理业务,完整链路如下:
- 客户端发送数据 → conn_fd变为可读;
- epoll_wait()返回 → EventLoop遍历活跃Channel;
- Channel::HandleEvent() → 检测到EPOLLIN,触发第一层回调(Channel::_read_callback);
- Connection::HandleRead() → 从socket读取数据,存入输入缓冲区;
- 触发第二层回调(_message_callback) → 调用用户的OnMessage();
- 用户处理业务逻辑 → 解析协议、处理请求,调用conn->Send()发送响应。
3.4 HandleRead代码详解(核心I/O处理)
cpp
void HandleRead() {
// 第一步:从socket读取数据(非阻塞读)
char buf[65536]; // 临时缓冲区,单次最多读65535字节
ssize_t ret = _socket.NonBlockRecv(buf, 65535);
if (ret < 0) {
// 读取出错,触发关闭流程
return ShutdownInLoop();
}
// 注意:ret == 0 是"本次没读到数据",不是连接断开;连接断开会触发EPOLLHUP
// 第二步:将读取到的数据存入输入缓冲区
_in_buffer.WriteAndPush(buf, ret);
// 第三步:调用用户的消息回调(数据准备就绪)
if (_in_buffer.ReadAbleSize() > 0) {
// shared_from_this():获取自身的shared_ptr,避免悬空指针
_message_callback(shared_from_this(), &_in_buffer);
}
}
3.5 为什么需要两层回调?
核心好处是"职责分离、降低耦合":
- 解耦I/O与业务:Connection负责底层I/O(读数据、存缓冲区、处理错误),用户只关心业务逻辑;
- 统一缓冲管理:Connection在调用用户回调前,已完成数据读取和缓冲,用户无需处理I/O细节;
- 统一错误处理:Connection可以统一捕获I/O错误,触发关闭流程,用户无需关心底层错误;
- 状态管控:Connection可以在回调前后维护连接状态,避免非法操作(比如断开状态调用业务回调)。
4. 线程安全设计:RunInLoop模式的妙用
Reactor模式的线程安全核心原则是:所有对Connection的操作,必须在其所属的EventLoop线程中执行。因为Connection中的Channel、缓冲区等组件都不是线程安全的,跨线程操作会导致数据竞争。
4.1 公有接口与私有实现分离
Connection采用"公有接口+私有实现"的设计,公有接口可能被任意线程调用,私有实现只在EventLoop线程执行:
cpp
class Connection {
public:
// 公有接口:可能被任意线程调用(对外暴露)
void Send(const char *data, size_t len); // 发送数据
void Shutdown(); // 半关闭连接
void Release(); // 释放连接
private:
// 私有实现:只在EventLoop线程执行(内部调用)
void SendInLoop(Buffer &buf);
void ShutdownInLoop();
void ReleaseInLoop();
};
4.2 RunInLoop模式:跨线程操作的解决方案
公有接口的核心作用是"将操作投递到EventLoop线程",通过EventLoop的RunInLoop/QueueInLoop实现线程安全,以Send()为例:
cpp
// 公有接口:可能被业务线程调用
void Send(const char *data, size_t len) {
// 关键:先将数据拷贝到Buffer中(避免原始数据是临时变量,跨线程访问出错)
Buffer buf;
buf.WriteAndPush(data, len);
// 将SendInLoop投递到EventLoop线程执行
_loop->RunInLoop(std::bind(&Connection::SendInLoop, this, std::move(buf)));
}
// 私有实现:保证在EventLoop线程执行
void SendInLoop(Buffer &buf) {
if (_statu == DISCONNECTED) return; // 连接已关闭,直接返回
_out_buffer.WriteBufferAndPush(buf); // 将数据追加到输出缓冲区
if (_channel.WriteAble() == false) {
_channel.EnableWrite(); // 启动写事件监控,准备发送数据
}
}
4.3 两个关键问题
(1)为什么Send要拷贝数据?
如果直接将用户传入的data指针投递到EventLoop线程,可能出现"业务线程释放data,EventLoop线程还在访问"的情况,导致野指针。拷贝数据后,Buffer会接管数据生命周期,确保线程安全。
(2)Release为什么用QueueInLoop,而不是RunInLoop?
RunInLoop:如果当前线程是EventLoop线程,会立即执行;QueueInLoop:无论当前线程是谁,都投递到任务队列,下一轮EventLoop再执行。
Release()可能在事件处理过程中被调用(比如HandleWrite中),如果用RunInLoop,会立即销毁Connection,导致后续事件处理代码访问已销毁对象;用QueueInLoop可以确保事件处理完成后,再销毁Connection,避免悬空指针。
5. 智能指针与生命周期管理:避免悬空指针的关键
Connection的生命周期十分复杂:TcpServer持有它、用户回调中可能保存它、定时器任务中可能引用它,一旦管理不当,就会出现悬空指针或内存泄漏。解决方案是:shared_ptr + enable_shared_from_this。
5.1 为什么用shared_ptr?
shared_ptr通过引用计数自动管理对象生命周期,只要有一个shared_ptr持有Connection,它就不会被销毁;当所有shared_ptr都释放时,自动调用析构函数,避免内存泄漏和悬空指针。
5.2 enable_shared_from_this的作用
Connection继承自std::enable_shared_from_this,其核心作用是"获取指向自身的shared_ptr",避免手动创建shared_ptr导致的双重释放:
cpp
// 错误做法:手动创建shared_ptr,与原有shared_ptr不共享引用计数,会双重释放
_message_callback(std::shared_ptr<Connection>(this), &_in_buffer);
// 正确做法:shared_from_this(),与原有shared_ptr共享引用计数
_message_callback(shared_from_this(), &_in_buffer);
比如在HandleRead中,调用用户的_message_callback时,用shared_from_this()将自身传递给用户,用户可以安全地保存这个shared_ptr,无需担心Connection被提前销毁。
5.3 生命周期保证逻辑
- TcpServer创建Connection后,用shared_ptr保存(_conns[conn_id] = shared_ptr),引用计数=1;
- 用户回调中调用shared_from_this(),引用计数临时+1;若用户保存这个shared_ptr,引用计数持续+1;
- 定时器任务中引用Connection时,用this(裸指针),而非shared_from_this()(避免定时器持有shared_ptr,导致Connection无法销毁);
- Release()被调用后,TcpServer会从_conns中删除Connection,引用计数-1;若没有其他shared_ptr持有,Connection自动析构。
6. 非活跃连接销毁:防止资源泄漏
在高并发场景中,会存在大量长时间没有数据交互的连接(比如客户端断开后未通知服务器),这些连接会占用fd、内存等资源,因此Connection需要支持"非活跃连接自动销毁"机制。
6.1 机制原理
启用非活跃销毁后,Connection会注册一个定时器(比如30秒),如果在定时器到期前有数据交互(读/写/任意事件),则刷新定时器(重新计时);若定时器到期仍无活动,则自动调用Release()销毁连接。
6.2 核心实现代码
cpp
// 启用非活跃销毁(公有接口)
void EnableInactiveRelease(int sec) {
_loop->RunInLoop(std::bind(&Connection::EnableInactiveReleaseInLoop, this, sec));
}
// 内部实现:注册/刷新定时器
void EnableInactiveReleaseInLoop(int sec) {
_enable_inactive_release = true;
if (_loop->HasTimer(_conn_id)) {
return _loop->TimerRefresh(_conn_id); // 已有定时器,刷新即可
}
// 注册定时任务:sec秒后调用Release()
_loop->TimerAdd(_conn_id, sec, std::bind(&Connection::Release, this));
}
// 任意事件触发时,刷新定时器(保持活跃)
void HandleEvent() {
if (_enable_inactive_release == true) {
_loop->TimerRefresh(_conn_id); // 有活动,重新计时
}
if (_event_callback) {
_event_callback(shared_from_this());
}
}
// 释放连接时,取消定时器(避免定时器到期访问已销毁对象)
void ReleaseInLoop() {
_statu = DISCONNECTED;
_channel.Remove();
_socket.Close();
// 关键:取消定时器
if (_loop->HasTimer(_conn_id)) {
CancelInactiveReleaseInLoop();
}
// 触发关闭回调...
}
7. 数据收发流程:从缓冲区到网络传输
Connection的数据收发依赖输入/输出缓冲区,核心是"非阻塞I/O + 缓冲区缓冲",解决TCP粘包/拆包、发送阻塞等问题。
7.1 接收数据流程
- 客户端发送数据 → 内核缓冲区接收数据 → conn_fd变为可读;
- EventLoop触发Channel读事件 → Connection::HandleRead();
- 从socket非阻塞读取数据 → 存入输入缓冲区(_in_buffer);
- 调用用户的_message_callback → 用户从缓冲区读取数据、解析协议、处理业务。
7.2 发送数据流程
- 用户调用conn->Send() → 数据拷贝到临时Buffer,投递到EventLoop线程;
- SendInLoop()将数据追加到输出缓冲区(_out_buffer);
- 启动写事件监控 → conn_fd变为可写时,触发HandleWrite();
- 从输出缓冲区读取数据,非阻塞发送到客户端;
- 数据发送完成后,关闭写事件监控;若处于半关闭状态,发送完成后释放连接。
7.3 发送流程关键代码(HandleWrite)
cpp
void HandleWrite() {
// 从输出缓冲区读取数据,非阻塞发送
ssize_t ret = _socket.NonBlockSend(
_out_buffer.ReadPosition(),
_out_buffer.ReadAbleSize()
);
if (ret < 0) {
// 发送出错,处理剩余数据后关闭
if (_in_buffer.ReadAbleSize() > 0) {
_message_callback(shared_from_this(), &_in_buffer);
}
return Release();
}
_out_buffer.MoveReadOffset(ret); // 移动读指针,标记已发送数据
if (_out_buffer.ReadAbleSize() == 0) {
_channel.DisableWrite(); // 数据发完,关闭写监控
if (_statu == DISCONNECTING) {
return Release(); // 半关闭状态,数据发完即关闭
}
}
}
8. 协议升级机制:适配多协议通信
在实际开发中,经常需要在同一个连接上切换通信协议(比如HTTP升级为WebSocket、HTTP/1.1升级为HTTP/2),Connection类通过Upgrade接口实现协议升级。
8.1 核心接口
cpp
void Upgrade(const Any &context,
const ConnectedCallback &conn,
const MessageCallback &msg,
const ClosedCallback &closed,
const AnyEventCallback &event) {
_loop->AssertInLoop(); // 断言:必须在EventLoop线程调用
_loop->RunInLoop(std::bind(&Connection::UpgradeInLoop, this,
context, conn, msg, closed, event));
}
// 内部实现:重置上下文和回调
void UpgradeInLoop(const Any &context,
const ConnectedCallback &conn,
const MessageCallback &msg,
const ClosedCallback &closed,
const AnyEventCallback &event) {
_context = context; // 重置协议上下文(比如WebSocket的握手状态)
_connected_callback = conn; // 重置连接回调
_message_callback = msg; // 重置消息回调(切换为新协议的消息处理)
_closed_callback = closed; // 重置关闭回调
_event_callback = event; // 重置事件回调
}
8.2 为什么要AssertInLoop?
协议升级必须在EventLoop线程立即执行,否则会出现"协议切换不及时"的问题:比如收到WebSocket握手请求后,Upgrade被投递到任务队列,下一个数据包(WebSocket帧)到来时,还是用旧的HTTP回调处理,导致解析错误。
AssertInLoop()用于断言当前线程是EventLoop线程,强制用户在正确的线程调用Upgrade,避免上述问题。
9. 总结:Connection类的设计精髓
Connection类是Reactor网络库中最复杂但最核心的组件,其设计围绕"安全、高效、解耦"三个核心目标,总结下来有6个关键设计点:
- 状态管理:用四状态有限状态机,严格管控连接生命周期,避免非法操作;
- 双层回调:分离底层I/O处理和上层业务逻辑,降低耦合,提升可维护性;
- 线程安全:采用"公有接口+私有实现"+RunInLoop模式,确保所有操作在EventLoop线程执行;
- 生命周期:shared_ptr + enable_shared_from_this,自动管理对象生命周期,避免悬空指针;
- 超时管理:定时器+活跃度刷新,实现非活跃连接自动销毁,防止资源泄漏;
- 协议升级:提供Upgrade接口,支持同一连接切换通信协议,提升灵活性。
理解Connection类的设计,不仅能掌握Reactor模式的核心实现,更能学会高并发网络编程中"资源管理、线程安全、解耦设计"的常用技巧------这些技巧在实际开发中,无论是自己实现网络库,还是使用开源网络库(如muduo),都能发挥重要作用。