网络系统的封装与调用——从散装组件到可调用系统
组件拆出来了,但还不能被直接调用——它们只是散装零件。要先封装成可调用的系统,再决定怎么被调用。封装有两种形态:框架化(有状态,入口启动) 和 接口化(无状态,上层调用接口)。如果系统只有组件、没有封装,就要先封装成框架或接口。这一篇讲封装与调用。注意:编解码(解析)和消息路由属于入口系统,不在网络系统内部封装。
一、内部封装结构:EventLoop 是心脏
封装的第一步,是把散装组件按依赖关系拼起来。起点是 EventLoop——因为它是唯一拥有自己线程的有状态组件:
1 2 3 4
| EventLoop 是心脏: → 它在自己的线程里运行 → 它决定什么时候处理什么事件 → 其他组件被它驱动
|
内部的封装关系:
1 2 3 4 5
| EventLoop(有状态,心脏) ├── 驱动 Connection(连接事件 → 创建/销毁连接) ├── 驱动 Session(连接建立 → 创建会话;断开 → 销毁会话) ├── 引用 Protocol(消息格式定义) └── 操作 Buffer(数据可读 → 写入缓冲;数据可写 → 从缓冲发送)
|
EventLoop 是封装的起点,也是运行时的控制中心。 内部封装只到「字节」为止——字节怎么变成消息、消息交给谁,是入口系统在边界处挂上去的。
二、两种封装形态:框架化 vs 接口化
系统封装好之后,对外是什么形态,决定了它怎么被使用。只有两种形态:
1 2 3 4 5 6 7 8 9
| ① 框架化(有状态、有生命周期): → 用类封装,组件在类里组装好 → 使用方式:入口启动 入口创建对象 → start/run → 运行 → stop
② 接口化(无状态): → 用函数导出,即调即用 → 使用方式:上层调用接口 上层直接调 connect/send/close 等函数
|
框架化是入口启动,接口化是上层调用接口——系统封装成哪种形态,决定它被怎么使用。
本篇讲封装形态(框架化/接口化);04-网络系统设计从想法到实现 第 ⑥ 步把封装放在完整设计线的位置,含框架设计的原子步骤清单。
网络系统通常是框架化的,因为它有 EventLoop 这个有状态的核心(独立线程、有生命周期):
1 2 3 4 5 6
| NetworkServer server; server.on_message(handler); server.start();
server.stop();
|
而一个只有纯能力的网络工具(比如「把一段字节编码成消息头」),才是接口化的:
1 2
| int encode_header(uint8_t* buf, uint32_t len, uint32_t cmd);
|
三、只有组件时:先封装成框架或接口
不是所有系统都「已经封装好了」。拆解可能只产生一堆只有组件、没有封装的东西:
1 2 3
| 只有组件: ✅ 有:数据结构、算法、原子接口(组件 = 数据 + 算法 + 接口) ❌ 没有:统一生命周期、初始化顺序、资源所有权
|
这样的东西没法直接被调用——调用者不知道「先初始化什么、谁拥有什么、用完怎么释放」。必须先封装:
1 2
| 接口化 → 无状态、即调即用(落点由归属决定:业务组件→业务系统内部、库接口→基础设施层原子化接口、全局工具→utils) 框架化 → 有状态、有生命周期(落点由是否全局决定:全局工具→utils、非全局→main 启动)
|
1 2 3 4
| 同一堆日志组件,两种命运: 简单日志 → 接口化 → utils/log.h(log("hello") 即调即用) 复杂日志 → 框架化 → utils/log/LogSystem(start/stop,有队列线程) 都是全局工具,所以都在 utils——区别是生命周期,不是位置
|
这条详见《拆解与组织》主题的「只有组件的子系统如何被加载」。
四、和入口系统的衔接:在边界处对接
网络系统封装好后,和入口系统怎么接上?——在网络系统的「可读事件」处对接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 网络系统(EventLoop): Buffer 收到字节 Connection 取出数据 ↓ 边界:把原始字节交给入口系统 入口系统(Parser + Dispatcher): Parser 把字节解码成 Message Dispatcher 把 Message 路由给 Handler ↓ 边界:Handler 返回响应消息 入口系统(Serializer): 把响应 Message 编码成字节 ↓ 边界:交回网络系统 网络系统(Connection + Buffer + EventLoop): Buffer 暂存响应字节 EventLoop 触发可写 → Connection 发送
|
1 2 3 4 5 6
| server.on_data([&](const uint8_t* bytes, size_t len) { Message msg = parse(bytes, len); dispatch(msg, handlers); });
|
封装权在入口系统——它把网络系统的字节事件和业务 Handler 连起来。 网络系统只暴露「有数据了」这个事件,不关心之后发生什么。
五、框架化的启动顺序
框架化系统有启动顺序,接口化系统没有(即调即用):
1 2 3 4 5 6 7 8
| 框架化(入口启动)的启动顺序: 1. 入口创建框架对象(内部创建 EventLoop、Connection Manager...) 2. 注册 Handler 3. 入口调用 start/run(框架自动完成剩余初始化并启动) 4. 运行中(框架自己跑事件循环) 5. 入口调用 stop(框架收尾、释放资源)
接口化(上层调用):没有启动顺序,直接调用
|
启动顺序是启动流在网络系统中的体现。
六、运行时的调用链
网络系统运行时的调用链:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| EventLoop 线程: epoll_wait 检测到事件 → 新连接事件 → 创建 Connection → 创建 Session → 通知入口系统:on_connect → 数据可读事件 → Buffer 暂存字节 → Connection 取出 → 通知入口系统:on_data(bytes) ← 边界 → 入口系统:parse → dispatch → Handler → 入口系统:serialize 响应字节 → Connection 接收响应字节 → Buffer 暂存 → 可写事件 → Buffer 发送 → Connection 写出 → 断开事件 → 销毁 Session、Connection → 通知入口系统:on_disconnect
|
网络系统控制字节的收发,入口系统在 on_data 回调里插入消息处理。
七、异常处理
封装时需要决定:异常在哪一层被处理?
1 2 3 4 5 6 7 8 9 10 11 12
| 网络系统内部(本线负责): EventLoop 异常 → 重启事件循环 Connection 异常 → 重试或关闭连接 Session 异常 → 清理会话(含协议版本不匹配) Buffer 异常 → 丢弃数据、记录日志
入口系统(挂在 on_data 边界后): 解析异常 → 发送错误响应 路由异常 → 发送「未知命令」错误
业务系统: Handler 异常 → 业务层处理
|
边界原则:网络系统不处理「消息格式错误」(那是解析层的事),入口系统不处理「连接断开」(那是 Connection 的事)。
1 2 3 4 5 6 7
| EventLoop 异常 → 重启事件循环 Connection 异常 → 重试或关闭连接 Session 异常 → 清理会话(含协议版本不匹配) Buffer 异常 → 丢弃数据、记录日志 ─── 边界 ─── 解析/路由异常 → 入口系统处理(发送错误响应) 业务异常 → 业务系统处理(你的 Handler)
|
收束
1 2 3 4 5 6 7 8 9 10 11 12
| 组件拆出来了,不能直接被调用——要先封装成可调用的系统。
封装有两种形态: 框架化(有状态)→ 类封装 → 入口启动 接口化(无状态)→ 函数导出 → 上层调用接口
只有组件、没封装 → 先封装成框架或接口,再被调用
网络系统通常框架化(有 EventLoop 这个有状态核心): 入口创建对象 → start/run → stop
封装权在入口系统,它把网络系统的字节事件和业务 Handler 连起来
|
框架化是入口启动,接口化是上层调用接口——不是「手动组装 vs 框架化组装」那种「谁创建组件」的歧义说法。 封装形态决定怎么被使用,这才是关键。