网络系统的封装与调用——从散装组件到可调用系统

组件拆出来了,但还不能被直接调用——它们只是散装零件。要先封装成可调用的系统,再决定怎么被调用。封装有两种形态:框架化(有状态,入口启动)接口化(无状态,上层调用接口)。如果系统只有组件、没有封装,就要先封装成框架或接口。这一篇讲封装与调用。注意:编解码(解析)和消息路由属于入口系统,不在网络系统内部封装。


一、内部封装结构: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); // 注册业务 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 框架化组装」那种「谁创建组件」的歧义说法。 封装形态决定怎么被使用,这才是关键。