网络系统是怎么被拆成这几个组件的

主线《从函数到网络系统》第七步「组件化」只甩出一句话「网络系统拆成几个组件」。这一篇把这一步展开:网络系统从一坨职责混杂的代码,是怎么一步步被拆出 EventLoop、Connection、Session、Buffer、Protocol 的。每个组件都不是「设计出来的」,而是「扛不住什么」才被逼出来的。


一、起点:一坨混在一起的代码

最早的网络代码,所有职责混在一个 while 里:

1
2
3
4
5
6
7
8
9
10
11
12
13
int clients[100];
int count = 0;

while (1) {
clients[count++] = accept(server, ...); // 管连接
for (int i = 0; i < count; i++) {
if (有数据可读(clients[i])) {
recv(clients[i], buf, sizeof(buf), 0); // 管收发
处理(buf); // 管业务
send(clients[i], response, len, 0); // 管收发
}
}
}

这一个循环里,同时扛着四件事:

1
2
3
4
① 什么时候有事(遍历、等待事件)
② 一条连接的生老病死(accept、登记、关闭、清理)
③ 每个连接自己的状态(缓冲、进度)
④ 字节怎么收发、消息长什么样

拆分的起点不是「想拆」,而是「这些事混在一起,改一个要动全身」。 每一件事被逼出来,就多一个组件。


二、EventLoop 被逼出来:遍历太慢

问题:100 个连接里可能只有 2 个有数据,但要逐个检查。

信号for (i = 0; i < count; i++) 遍历全部连接,太慢。

拆分:操作系统提供高效通知(epoll/kqueue)——「只告诉谁有事,不用逐个查」。

1
2
3
4
while (1) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1); // 只返回有事的
for (int i = 0; i < n; i++) { ... }
}

拆出来的东西:EventLoop——事件循环,职责是「什么时候有事」。

驱动力:扩展(连接变多,遍历扛不住)。

1
2
拆分依据:把「等待/分发事件」从「处理单个连接」里拆出来
边界:EventLoop 只管「什么时候有事」,不管「事情是什么」

三、Connection 被逼出来:连接生死散落在回调里

问题:一条连接从 accept 到 close,中间要登记、要跟踪状态、要清理资源。这些动作散落在 EventLoop 的各个回调里。

信号:连接状态变复杂(半关闭、超时、被重置、异常断开),每个回调都要重复处理「这个连接现在什么状态」。

拆分:把「一条连接的生老病死」独立出来,交给 Connection:

1
2
3
4
Connection:
建立 accept 后登记
维护 收发数据、跟踪状态
关闭 正常关闭 / 异常断开,清理资源

拆出来的东西:Connection——连接管理,职责是「一条连接的生死」。

驱动力:职责(连接生命周期散落各处)。

1
2
拆分依据:把「连接的生命周期」从「事件分发」里拆出来
边界:Connection 只管「连接的生死」,不管「字节是什么意思」

四、Session 被逼出来:每个连接的状态散落

问题:每个客户端连接需要维护自己的状态——缓冲区、已收到的消息、协议状态(握手/正常/关闭)、最后活跃时间。这些状态不该散落在 EventLoop 回调里。

信号:每个连接的状态被塞进全局数组、或者靠 fd 索引到处查,改起来容易错。

拆分:把「每个连接的状态」集中到一个结构里:

1
2
3
4
5
6
7
struct Session {
int fd; // 连接 fd
Buffer recv_buf; // 接收缓冲
Buffer send_buf; // 发送缓冲
ProtocolState state; // 协议状态
time_t last_active; // 最后活跃时间
};

拆出来的东西:Session——会话管理,职责是「每个连接的状态」。

驱动力:职责(状态散落)。

1
2
拆分依据:把「状态」从「事件分发」里拆出来
边界:EventLoop 管事件分发,Session 管状态

注意 Session 和 Connection 的分工

1
2
Connection:一条连接的「生命周期」(建立/关闭/清理)
Session: 一个会话的「状态」(缓冲、协议状态、活跃时间)

一个 Connection 上挂一个 Session——连接管「死活」,会话管「此刻是什么状态」。


五、Buffer 被逼出来:TCP 是流,一次收不到整条消息

问题:TCP 是流式协议,一次 recv 可能收到半条消息,或者两条消息粘在一起

信号:处理消息的代码到处要判断「这条消息收全了没有」,逻辑重复且容易错。

拆分:把「字节先攒起来」这件事独立出来,交给 Buffer:

1
2
3
Buffer:
接收缓冲 字节先存进来,攒够一条消息再处理
发送缓冲 待发的数据排队发出

拆出来的东西:Buffer——缓冲区,职责是「数据暂存」。

驱动力:职责(TCP 流式 vs 消息,需要一个中间缓冲)。

1
2
拆分依据:把「暂存字节」从「收发逻辑」里拆出来
边界:Buffer 只管「数据暂存」,不管「内容」

六、Protocol 被逼出来:裸字节没法用

问题:收发的是原始字节,调用者得自己知道「这 4 字节是命令、接下来 N 字节是数据」——每个调用者都自己猜。

信号:每个处理方都要重复定义「消息长什么样」,版本一改全乱。

拆分:把「消息长什么样」的定义独立出来,交给 Protocol:

1
2
3
4
5
struct Message {
uint32_t length; // 消息长度
uint32_t command; // 命令类型
uint8_t payload[];// 消息体
};

拆出来的东西:Protocol——协议格式定义,职责是「消息长什么样」。

驱动力:职责(格式定义被重复写)。

1
2
拆分依据:把「格式定义」从「收发逻辑」里拆出来
边界:Protocol 只定义格式,不做字节↔消息的转换(那是入口系统解析层的事)

注意:Protocol 是数据结构,不是组件——它没有算法、没有接口、没有生命周期,就是一份 struct 定义。


七、拆分演化的总表

1
2
3
4
5
6
7
组成部分      扛不住什么              拆分依据             性质
──────────────────────────────────────────────────────────
EventLoop 遍历太慢 事件等待 vs 处理 有状态组件
Connection 连接生死散落 生命周期 vs 分发 有状态组件
Session 每连接状态散落 状态 vs 分发 有状态组件
Buffer TCP 流式、半条消息 暂存 vs 收发 无状态工具
Protocol 裸字节没法用 格式定义 vs 收发 数据结构

一条规律贯穿:每一步拆分的依据都是「职责」——一件事职责混杂了,就把它从另一件事里剥出来。

1
2
3
4
5
6
职责决定拆分(单一职责主题):
什么时候有事 → EventLoop
连接的生死 → Connection
连接的状态 → Session
字节暂存 → Buffer
消息格式 → Protocol

八、拆分完成后,边界在哪

拆完不代表结束,还要守住边界:

1
2
✅ 网络系统只关心:字节收发、连接、会话、协议格式
❌ 不关心:字节↔消息解析、消息路由(那是入口系统的解析层/分发层)

拆分的终点不是「组件越多越好」,而是「每个组件职责单一、边界清晰」。边界一旦混了,又要重新拆。


九、和其他线的关系

1
2
3
4
5
6
7
8
9
10
11
本线 ←→ 拆解与组织(主题)
本线是「沿职责边界拆解」在网络系统领域的一次完整应用

本线 ←→ 单一职责(主题)
每个组件的拆分依据都是「职责单一」

本线 ←→ 从指令到系统/02(主线)
主线第七步「组件化」是本线的压缩版,本线是它的展开

本线 ←→ 组件拆分.md
组件拆分讲拆完后的「静态分类」,本线讲「怎么拆出来」的过程

收束

1
2
3
4
5
6
7
8
9
10
网络系统拆成 5 个组成部分,每一步都是「扛不住」逼出来的:

遍历太慢 → EventLoop(什么时候有事)
生死散落 → Connection(连接的死活)
状态散落 → Session(连接的状态)
TCP 流式 → Buffer(字节暂存)
裸字节没用 → Protocol(消息格式)

拆分依据是「职责」:一件事职责混杂,就把它剥出来。
拆完后守住边界:网络系统管字节,解析/路由是入口系统的事。

组件不是设计出来的,是「扛不住」逼出来的。 每个组件下一篇单独展开,看它「有什么」。