仿 muduo 高并发服务器项目:实现 Connection 连接管理模块,并整合 Any 上下文

前言

今天继续推进仿 muduo 的 C++ 高并发服务器项目。

前面已经实现了 SocketBufferChannelPollerEventLoopTimerWheelAny,但还缺少一个对象把它们组织成真正的客户端连接。

今天实现的 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() 切换上下文和协议处理回调

十、测试思路与预期现象

目前没有完整服务器运行输出,因此这里只记录测试方向和预期现象。

  1. 连接建立 :调用 Established() 后,预期状态变为 CONNECTED,开启读监控并触发一次建立回调。
  2. 消息接收 :通过 socketpair() 写入数据,预期内容进入输入 Buffer,并由消息回调正确消费。
  3. 部分发送:提交较大数据并限制对端读取,预期剩余数据保留在输出 Buffer,发完后关闭写监控。
  4. 优雅关闭 :有待发送数据时调用 Shutdown(),预期先进入 DISCONNECTING,发送完成后再释放。
  5. 异常释放:制造挂断或错误事件,预期先处理剩余输入,再依次执行用户关闭回调和服务器内部回调。
  6. 空闲超时:开启短超时,预期有事件时任务持续刷新,无事件时到期释放连接。
  7. Any 上下文 :保存自定义 Session 后按相同类型取出,预期字段保持一致。
  8. 协议升级 :在 Loop 线程调用 Upgrade(),预期后续数据只进入新的消息回调。

十一、当前版本的思考与优化方向

1. 明确 recv() 返回语义

原生 recv() 返回 0 表示对端关闭,EAGAINEINTR 则通过负返回值与 errno 表达。当前 Socket::Recv()EAGAIN/EINTR 转换为 0,把关闭及其他失败转换为 -1,所以 Connection 中的判断描述的是封装后语义。后续可以返回更明确的状态,分别处理暂时无数据、信号中断、正常关闭和真正错误。

2. 补齐回调判空

HandleRead()HandleWrite()HandleClose() 中存在直接调用 _message_callback 的位置。当前代码依赖服务器在连接建立前完成设置,后续最好统一判空,提高组件健壮性。

3. 防止重复释放

读写错误、挂断、超时和主动关闭可能在接近的时间触发多个释放任务。ReleaseInLoop() 可以先判断是否已经处于 DISCONNECTED,保证 Channel 移除、关闭回调和服务器删除操作只执行一次。

4. 处理裸 this 的生命周期风险

Channel、任务队列和定时器回调都通过 std::bind 保存裸 thisshared_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() 解耦业务状态与协议。

下一步准备实现 AcceptorTcpServer:接收新连接、分配连接 ID、使用连接表管理 PtrConnection,并验证建立、收发、关闭和超时销毁的完整服务器流程。

相关推荐
Sylvia33.1 小时前
足球数据接口开发实战:如何用火星数据API盘活赛事应用
java·服务器·开发语言·数据库·python
风123456789~1 小时前
【Linux专栏】history常用命令
linux·运维·服务器
熬夜苦读学习1 小时前
基于websocket的多用户五子棋网页游戏
linux·服务器·网络·websocket·网络协议·游戏·五子棋
爱学习的程序媛1 小时前
SIP抓包调试、故障排查与项目上线实战手册
运维·wireshark·呼叫中心·联络中心·sip协议·sip信令
fpcc2 小时前
Linux 7.1介绍
linux·运维
蝶恋舞者2 小时前
Wireshark 抓包工具
网络·测试工具·wireshark
Sirius Wu2 小时前
OpenClaw Observability 并发安全(Per-Request Hook)完整详解
服务器·网络·人工智能·安全·ai·架构·aigc
三8442 小时前
磁盘管理/fdisk
linux·运维·服务器
2601_954706492 小时前
云虚拟化移动端架构深度解析:API 自动化集群管控实战与商用选型方案
运维·架构·自动化