前言
今天继续推进仿 muduo 的 C++ 高并发服务器项目。
前面已经实现了 Socket、Buffer、Channel、Poller、EventLoop、TimerWheel 和 Any,但还缺少一个对象把它们组织成真正的客户端连接。
今天实现的 Connection 负责管理连接的 fd、状态、缓冲区、回调、空闲超时和业务上下文,让分散的 Reactor 组件形成完整链路。
一、Connection 的职责与核心结构
socket fd 只是一个整数,不包含数据处理和生命周期信息。Connection 对"一条完整 TCP 连接"进行封装:
cpp
typedef enum {
DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING
} ConnStatu;
using PtrConnection = std::shared_ptr<Connection>;
class Connection : public std::enable_shared_from_this<Connection> {
uint64_t _conn_id;
int _sockfd;
bool _enable_inactive_release;
EventLoop *_loop;
ConnStatu _statu;
Socket _socket;
Channel _channel;
Buffer _in_buffer;
Buffer _out_buffer;
Any _context;
};
其中 _socket 负责非阻塞收发,_channel 负责事件监控,两个 Buffer 保存输入和待发送数据,_loop 管理线程边界,_context 保存业务状态。
连接的正常状态变化为:
CONNECTING -> CONNECTED -> DISCONNECTING -> DISCONNECTED
构造后连接处于 CONNECTING,就绪后进入 CONNECTED;需要等待剩余数据发完时进入 DISCONNECTING,清理结束后变为 DISCONNECTED。错误、挂断或超时也会进入释放流程。
二、智能指针与两组回调
Connection 通过 shared_from_this() 向业务回调传递自身的 shared_ptr,保证回调执行期间对象仍然存在。
其中 Channel 回调接收底层事件,业务回调通知组件使用者。
cpp
using ConnectedCallback = std::function<void(const PtrConnection&)>;
using MessageCallback = std::function<void(const PtrConnection&, Buffer *)>;
using ClosedCallback = std::function<void(const PtrConnection&)>;
using AnyEventCallback = std::function<void(const PtrConnection&)>;
Connection(EventLoop *loop, uint64_t id, int fd)
: _conn_id(id), _sockfd(fd), _enable_inactive_release(false),
_loop(loop), _statu(CONNECTING), _socket(fd), _channel(loop, fd) {
_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));
_channel.SetEventCallback(std::bind(&Connection::HandleEvent, this));
}
_connected_callback、_message_callback、_closed_callback 和 _event_callback 面向组件使用者;_server_closed_callback 则由服务器内部设置,用来在连接关闭时从连接表中移除对应记录。
需要注意,Connection 必须由 shared_ptr 管理后才能调用 shared_from_this(),不能把它当作普通栈对象使用。
三、连接建立与数据接收
连接创建后不能立刻监听读事件,因为业务回调可能还没有设置完成。Established() 通过 RunInLoop() 回到所属线程,再完成状态切换、开启读监控并通知业务层。
cpp
void EstablishedInLoop() {
assert(_statu == CONNECTING);
_statu = CONNECTED;
_channel.EnableRead();
if (_connected_callback)
_connected_callback(shared_from_this());
}
void HandleRead() {
char buf[65536];
ssize_t ret = _socket.NonBlockRecv(buf, 65535);
if (ret < 0) return ShutdownInLoop();
_in_buffer.WriteAndPush(buf, ret);
if (_in_buffer.ReadAbleSize() > 0)
_message_callback(shared_from_this(), &_in_buffer);
}
当 epoll 返回可读事件后,Channel 调用 HandleRead()。Connection 使用临时数组接收数据,再写入 _in_buffer,最后把连接和缓冲区交给业务消息回调。
Connection 不负责解析 HTTP 或其他协议。它只负责把字节流安全地读入 Buffer;半包、粘包以及一轮读取包含多个请求的情况,都由业务回调结合 Buffer 的读偏移继续处理。
四、非阻塞发送与写事件管理
业务层调用 Send() 时,外部 data 可能来自临时字符串,而跨线程任务不一定立即执行。因此代码先把数据复制到 Buffer,再将任务提交给 EventLoop,避免后续访问悬空指针。
cpp
void Send(const char *data, size_t len) {
Buffer buf;
buf.WriteAndPush(data, len);
_loop->RunInLoop(
std::bind(&Connection::SendInLoop, this, std::move(buf)));
}
void SendInLoop(Buffer &buf) {
if (_statu == DISCONNECTED) return;
_out_buffer.WriteBufferAndPush(buf);
if (!_channel.WriteAble()) _channel.EnableWrite();
}
void HandleWrite() {
ssize_t ret = _socket.NonBlockSend(
_out_buffer.ReadPosition(), _out_buffer.ReadAbleSize());
if (ret < 0) return Release();
_out_buffer.MoveReadOffset(ret);
if (_out_buffer.ReadAbleSize() == 0) {
_channel.DisableWrite();
if (_statu == DISCONNECTING) Release();
}
}
非阻塞 send() 一次不一定能发送完全部数据,所以必须按照实际返回值移动 Buffer 的读偏移,剩余数据继续等待下一次可写事件。
写事件也不能一直开启。大多数 socket 通常都可写,如果始终监听 EPOLLOUT,epoll 会频繁返回无意义事件。当前设计只在输出缓冲区有数据时开启写监控,全部发送完成后立即关闭。
五、优雅关闭与最终释放
Shutdown() 并不等于立即关闭 fd。它先把连接设为 DISCONNECTING:如果还有输入数据就交给业务层处理;如果还有输出数据就继续监听可写事件;只有输出缓冲区为空时才进入最终释放。
cpp
void ShutdownInLoop() {
_statu = DISCONNECTING;
if (_in_buffer.ReadAbleSize() > 0 && _message_callback)
_message_callback(shared_from_this(), &_in_buffer);
if (_out_buffer.ReadAbleSize() > 0 && !_channel.WriteAble())
_channel.EnableWrite();
if (_out_buffer.ReadAbleSize() == 0) Release();
}
void Release() {
_loop->QueueInLoop(std::bind(&Connection::ReleaseInLoop, this));
}
void ReleaseInLoop() {
_statu = DISCONNECTED;
_channel.Remove();
_socket.Close();
if (_loop->HasTimer(_conn_id)) CancelInactiveReleaseInLoop();
if (_closed_callback) _closed_callback(shared_from_this());
if (_server_closed_callback)
_server_closed_callback(shared_from_this());
}
Release() 特意使用 QueueInLoop(),让真正清理发生在当前事件回调结束之后,避免对象在自己的事件处理过程中被销毁。
释放顺序同样重要:先从 Poller 移除 Channel,再关闭 fd、取消定时任务、调用用户关闭回调,最后由服务器内部回调移除连接记录。服务器连接表中的 shared_ptr 可能是对象最后的所有者,因此内部移除操作必须放到最后。
六、非活跃连接超时释放
长期没有通信的连接会持续占用 fd 和内存。Connection 使用连接 ID 作为定时任务 ID,将 TimerWheel 接入连接生命周期。
cpp
void EnableInactiveReleaseInLoop(int sec) {
_enable_inactive_release = true;
if (_loop->HasTimer(_conn_id))
return _loop->TimerRefresh(_conn_id);
_loop->TimerAdd(_conn_id, sec,
std::bind(&Connection::Release, this));
}
void HandleEvent() {
if (_enable_inactive_release)
_loop->TimerRefresh(_conn_id);
if (_event_callback)
_event_callback(shared_from_this());
}
启用后,只要连接触发任意事件,就刷新对应定时任务;如果指定时间内一直没有事件,定时任务到期并调用 Release()。连接正常关闭时,还会取消尚未执行的超时任务。
七、Any 上下文与协议升级
上一篇实现的 Any 在这里成为 Connection 的业务上下文。网络层不需要知道具体类型,只负责保存和转交。
cpp
void SetContext(const Any &context) { _context = context; }
Any *GetContext() { return &_context; }
void UpgradeInLoop(const Any &context,
const ConnectedCallback &conn,
const MessageCallback &msg,
const ClosedCallback &closed,
const AnyEventCallback &event) {
_context = context;
_connected_callback = conn;
_message_callback = msg;
_closed_callback = closed;
_event_callback = event;
}
业务层可以在上下文中保存登录会话、HTTP 解析状态或其他自定义对象。取值时必须使用保存时的真实类型,例如:
Session *session = conn->GetContext()->get<Session>();
Upgrade() 会同时替换上下文和阶段性回调,可用于 WebSocket 等协议升级:连接最初使用 HTTP 上下文和解析回调,握手成功后立即切换到 WebSocket 上下文和帧处理回调。
该接口先调用 _loop->AssertInLoop(),要求升级发生在所属 EventLoop 线程中,防止新数据已经到达却仍被旧协议回调处理。当前实现只保存新的连接回调,不会再次主动调用它。
八、线程边界与完整处理流程
Established()、Send()、Shutdown() 和定时器接口都通过 RunInLoop() 执行:同线程调用时立即运行,其他线程调用时加入任务队列,并由 eventfd 唤醒 EventLoop。这样 Channel、Buffer、连接状态和定时器主要由同一线程串行修改,减少了到处加锁带来的复杂性。
整合后的主流程如下:
text
epoll_wait 返回事件
|
v
Poller 找到 Channel -> Channel 分发事件
|
v
Connection 读写 Buffer、维护状态和超时
|
+-- 收到数据 -> MessageCallback -> Any 业务上下文
+-- 发送数据 -> 输出 Buffer -> EPOLLOUT
+-- 任意事件 -> 刷新 TimerWheel
+-- 关闭/错误 -> QueueInLoop -> ReleaseInLoop
|
v
服务器移除连接记录
到这里,Poller -> Channel -> EventLoop -> Connection -> 业务回调 的 Reactor 连接链路已经形成。
九、公开接口梳理
| 接口 | 作用 |
|---|---|
Fd()、Id()、Connected() |
查询 fd、连接 ID 和状态 |
SetContext()、GetContext() |
设置或读取 Any 上下文 |
Set*Callback() |
设置建立、消息、关闭及任意事件回调 |
Established() |
完成连接就绪并开启读监控 |
Send() |
线程安全地提交发送数据 |
Shutdown() |
等待待发送数据完成后优雅关闭 |
Release() |
延后执行最终资源清理 |
EnableInactiveRelease() |
启用非活跃连接超时销毁 |
CancelInactiveRelease() |
取消非活跃销毁任务 |
Upgrade() |
切换上下文和协议处理回调 |
十、测试思路与预期现象
目前没有完整服务器运行输出,因此这里只记录测试方向和预期现象。
- 连接建立 :调用
Established()后,预期状态变为CONNECTED,开启读监控并触发一次建立回调。 - 消息接收 :通过
socketpair()写入数据,预期内容进入输入 Buffer,并由消息回调正确消费。 - 部分发送:提交较大数据并限制对端读取,预期剩余数据保留在输出 Buffer,发完后关闭写监控。
- 优雅关闭 :有待发送数据时调用
Shutdown(),预期先进入DISCONNECTING,发送完成后再释放。 - 异常释放:制造挂断或错误事件,预期先处理剩余输入,再依次执行用户关闭回调和服务器内部回调。
- 空闲超时:开启短超时,预期有事件时任务持续刷新,无事件时到期释放连接。
- Any 上下文 :保存自定义
Session后按相同类型取出,预期字段保持一致。 - 协议升级 :在 Loop 线程调用
Upgrade(),预期后续数据只进入新的消息回调。
十一、当前版本的思考与优化方向
1. 明确 recv() 返回语义
原生 recv() 返回 0 表示对端关闭,EAGAIN 和 EINTR 则通过负返回值与 errno 表达。当前 Socket::Recv() 把 EAGAIN/EINTR 转换为 0,把关闭及其他失败转换为 -1,所以 Connection 中的判断描述的是封装后语义。后续可以返回更明确的状态,分别处理暂时无数据、信号中断、正常关闭和真正错误。
2. 补齐回调判空
HandleRead()、HandleWrite() 和 HandleClose() 中存在直接调用 _message_callback 的位置。当前代码依赖服务器在连接建立前完成设置,后续最好统一判空,提高组件健壮性。
3. 防止重复释放
读写错误、挂断、超时和主动关闭可能在接近的时间触发多个释放任务。ReleaseInLoop() 可以先判断是否已经处于 DISCONNECTED,保证 Channel 移除、关闭回调和服务器删除操作只执行一次。
4. 处理裸 this 的生命周期风险
Channel、任务队列和定时器回调都通过 std::bind 保存裸 this。shared_from_this() 只能保证业务回调执行期间对象存活,并不会自动保护尚未执行的任务。后续需要结合服务器连接表明确生命周期约束,或在异步任务中捕获 shared_ptr/weak_ptr。
5. 保护空 Any 取值
如果还没有设置上下文就调用 get<T>(),_content 为空。后续应增加空值断言、has_value() 或安全返回接口,再进行类型检查。
6. 修正状态类型命名
当前代码使用 ConnStatu,更自然的名称是 ConnStatus。本文为如实记录当前版本保留原名,后续可统一修改。
总结与下一步计划
Connection 把 Socket、Channel、Buffer、EventLoop、TimerWheel 和 Any 组合成完整连接,负责非阻塞收发、状态转换、关闭、超时和业务上下文。
核心设计包括:用 Buffer 处理 TCP 字节流和部分发送;按需监听写事件;用 EventLoop 串行修改状态;通过 QueueInLoop() 延后释放;使用 Any 和 Upgrade() 解耦业务状态与协议。
下一步准备实现 Acceptor 和 TcpServer:接收新连接、分配连接 ID、使用连接表管理 PtrConnection,并验证建立、收发、关闭和超时销毁的完整服务器流程。