网络系统的构成——从一坨代码到清晰的边界

通信程序拆成入口系统、网络系统、业务系统三个系统。这一篇单独拆「网络系统」——它内部到底有哪些东西,各负责什么,边界怎么划。先明确两点:① 网络系统是分组件、不是分层,解析层和分发层属于入口系统;② 网络系统单独不构成软件,最多做成网络库,要配合入口系统和业务系统才能形成完整的网络通信软件。


一、从一个 socket 调用说起

最简单的网络程序:

1
2
3
4
5
int sock = socket(AF_INET, SOCK_STREAM, 0);
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
send(sock, "hello", 5, 0);
recv(sock, buf, sizeof(buf), 0);
close(sock);

所有东西都在一个函数里——连接、发送、接收、关闭。没有边界。这和「所有东西写在 main 里」是同一个阶段。

当程序需要同时处理多个连接时,这坨代码就不够用了:

1
2
3
4
5
6
7
8
9
// 伪代码:同时处理 100 个连接
while (1) {
for (int i = 0; i < 100; i++) {
if (数据可读[i]) {
recv(sock[i], buf, sizeof(buf), 0);
处理(buf); // 业务逻辑混在一起
}
}
}

连接管理、数据收发、业务处理全混在一起。 这就是「需要拆分」的信号。


二、网络系统里的东西分三类,不是五个组件

网络系统里不是「五个组件」,而是三类性质不同的东西

1
2
3
4
5
6
网络系统
├── EventLoop ← 有状态组件(有生命周期、独立线程)
├── Connection ← 有状态组件(有状态、有生命周期)
├── Session ← 有状态组件(有状态、有生命周期)
├── Buffer ← 无状态工具(数据结构 + 算法,被读写时操作)
└── Protocol ← 数据结构(协议格式定义,不是组件)

为什么这样分?关键看「组件」的定义:组件 = 数据结构 + 算法 + 接口,它有行为、能被调用。

1
2
3
4
5
6
7
8
9
10
11
有状态组件(EventLoop / Connection / Session):
有数据、有算法、有接口,还有状态和生命周期
→ 是组件,而且有状态

无状态工具(Buffer):
有数据、有算法、有接口,但没有状态、没有生命周期
→ 是组件,但是无状态工具

数据结构(Protocol):
只有数据的定义(struct Message),没有算法、没有接口、没有生命周期
→ 不是组件,是一份「定义」

注意这里的边界:

1
2
✅ 网络系统只关心:字节怎么收发、连接怎么管理、会话怎么维护、协议格式是什么
❌ 不关心:消息怎么解析(字节↔消息)、消息交给谁处理

解析(parse)和调度(dispatch)不属于网络系统——它们是入口系统(通信程序)的组件。网络系统处理的是「字节」,入口系统处理的是「消息」。


三、协议层 vs 解析层:协议是数据结构,解析是转换

这是最容易混的地方,必须分清楚:

1
2
3
4
5
6
7
8
9
10
协议(Protocol,属于网络系统):
→ 本质是「消息长什么样」的定义 = 一份数据结构
→ protocol.cpp = 协议格式定义(struct Message)
→ 只定义,不做转换,不需要运行、不需要生命周期
→ 所以它不是一个「组件」

解析层(Parser,属于入口系统):
→ 完成「字节流 ↔ 消息结构」的转换
→ parse.cpp = 解析层
→ 只管转换,不定义格式
1
2
3
4
5
6
7
8
9
10
11
protocol.cpp(协议定义,网络系统):
struct Message {
uint32_t length;
uint32_t command;
uint8_t payload[];
};
// 只是一份数据结构定义,不是组件

parse.cpp(解析层,入口系统):
Message parse_request(const uint8_t* bytes, size_t len);
// 做字节流 → 消息结构的转换,属于入口系统

两者是不同的东西

1
2
协议回答「消息长什么样」(定义,一份数据结构)
解析回答「怎么从字节变出消息」(转换,一段可运行的逻辑)

那「协议状态」(握手/心跳/关闭)在哪? 它不属于 Protocol——那是 Session 的状态。Session 里有一个 state 字段记录「当前在握手、还是正常、还是关闭中」。协议格式是死的定义,协议状态是活的、属于某个会话的。


四、三类东西的边界与异常

EventLoop(有状态组件,有生命周期)

问题:100 个连接同时有数据,程序怎么知道该处理哪个?

1
2
3
EventLoop:调用 epoll_wait/kqueue 等待事件
事件分发:把事件(可读/可写/新连接/断开)分发给对应的处理器
定时器:超时检测、心跳保活

边界:只管「什么时候有事发生」,不管事情是什么。

异常:事件循环被中断、fd 耗尽、定时器精度不够。

Connection(有状态组件)

问题:一条连接从建立到关闭,生命周期谁来管?

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

边界:只管「一条连接的生死」,不管字节是什么意思。

异常:连接被拒绝、被重置、超时断开。

Session(有状态组件)

问题:每个客户端连接需要维护自己的状态,状态放哪?

1
2
3
4
5
6
7
struct Session {
int fd;
Buffer recv_buf;
Buffer send_buf;
ProtocolState state; // 协议状态(握手/正常/关闭)—— 状态属于 Session
time_t last_active;
};

边界:EventLoop 只管事件分发,Session 管状态。

异常:会话过期、协议版本不匹配(握手失败)、协议状态机异常。

Buffer(无状态工具)

问题:收发的数据先存哪?

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

Buffer 是无状态工具——有数据(字节数组)+ 算法(读写指针、扩容)+ 接口(read/write),但它不持有跨调用的状态、不需要初始化,被读写时才操作。

边界:只管「数据暂存」,不管内容。

异常:缓冲区满。

Protocol(数据结构,不是组件)

问题:收发的字节长什么样?

1
协议格式定义:消息格式(头+体)、字段含义、版本

边界:只是「消息长什么样」的定义(struct Message),不做字节↔消息的转换(那是入口系统解析层的事)。

异常:协议本身没有异常——它是死的定义。版本不匹配是 Session 的异常,格式错误是解析层的异常。


五、组件之间的数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
字节从网卡进来
↓ EventLoop 触发「可读」事件
↓ Buffer 暂存
↓ Connection 取出数据
↓ ────────────── 网络系统边界 ──────────────
↓ Parser(入口系统):字节流 → 消息对象
↓ Dispatcher(入口系统):消息类型 → Handler
↓ Handler(业务系统)处理业务
↓ ────────────── 网络系统边界 ──────────────
↓ Protocol 数据结构定义格式,Connection/Buffer 组装响应字节
↓ EventLoop 触发「可写」事件
↓ Connection 发送
字节到网卡出去

网络系统只负责字节进、字节出;中间的消息解析和分发是入口系统的事。


六、有状态 vs 无状态:封装形态由性质决定

这些东西怎么封装成可调用的形态?由它们的性质决定:

1
2
3
4
5
6
7
8
9
10
有状态组件(有状态、有生命周期)→ 类封装:
EventLoop ← 自己的事件循环线程,构造创建、run 启动、stop 停止
Connection ← 有状态,管理连接生命周期
Session ← 有状态,管理会话生命周期

无状态工具(无状态、无生命周期)→ 函数导出:
Buffer ← 被读写时才操作,无需初始化

数据结构(不是组件)→ 不需要封装:
Protocol ← 一份 struct Message 定义,被直接引用

一个术语澄清:这里的「类封装 / 函数导出」就是接口形态演化线里说的「框架化 / 接口化」——它对应的不是「谁创建组件」,而是两种使用方式

1
2
接口形态(本线,11-模块与构建单元/01):框架化 = 有状态 → 类封装;接口化 = 无状态 → 函数导出
使用方式(下一篇,封装与调用):框架化 → 入口启动;接口化 → 上层调用接口

系统封装成哪种形态,就决定它怎么被使用——框架化由入口启动,接口化由上层调用。下一篇展开。


七、每个组件的异常处理差异

每个组件遇到的异常完全不同,处理方式也完全不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
EventLoop 异常:
→ epoll_wait 被信号中断(EINTR)
→ 文件描述符耗尽(EMFILE)
→ 处理:重试事件循环、记录警告

Connection 异常:
→ 连接被拒绝(ECONNREFUSED)
→ 连接被重置(ECONNRESET)
→ 处理:重试、记录日志、关闭连接

Session 异常:
→ 会话过期
→ 协议版本不匹配(握手失败)
→ 处理:清理会话、通知上层

Buffer 异常:
→ 缓冲区满
→ 处理:丢弃数据、记录日志

消息格式错误是解析层(入口系统)的异常,不属于网络系统。 协议本身(数据结构)没有异常。

每个组件的异常是独立的——EventLoop 不应该处理连接断开(那是 Connection 的事),Session 不应该处理缓冲区满(那是 Buffer 的事),解析失败(那是入口系统解析层的事)。


八、网络系统 vs 入口系统的边界总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
网络系统(3 有状态组件 + 1 无状态工具 + 1 数据结构):
EventLoop / Connection / Session(有状态组件)
Buffer(无状态工具)
Protocol(数据结构:协议格式定义)
处理对象:字节
职责:收发、连接、会话、协议格式

入口系统(通信程序):
解析层(Parser):字节 ↔ 消息
分发层(Dispatcher):消息 → Handler
处理对象:消息
职责:解析、路由、分发

业务系统:
处理对象:业务数据
职责:业务逻辑

三个系统各管一段:网络系统管字节,入口系统管消息,业务系统管业务。

网络系统单独只是库

网络系统不是软件,它只是完整软件里的一个系统:

1
2
只有网络系统     → 最多做成网络库(如 libevent / libuv / asio),提供网络能力
网络系统 + 入口系统 + 业务系统 → 才是完整的网络通信软件

一个网络库可以独立存在、被任何程序链接;但要「形成软件」,必须有入口系统把网络事件解析成消息、路由给业务 Handler,有业务系统真正处理业务。


收束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
网络系统里分三类东西,不是五个组件:
有状态组件 EventLoop / Connection / Session(类封装,有生命周期)
无状态工具 Buffer(函数导出,被读写时操作)
数据结构 Protocol(struct Message 定义,不是组件)

protocol.cpp 是协议定义(数据结构),parse 是解析层(字节↔消息)
→ 协议属于网络系统
→ 解析层属于入口系统

协议状态(握手/心跳/关闭)属于 Session,不属于 Protocol

三个系统各管一段:
网络系统管字节,入口系统管消息,业务系统管业务

网络系统单独只是库,配合入口和业务才能形成软件

网络系统是分组件、不是分层;协议是数据结构、不是组件;网络系统单独是库、不是软件。 抓住这三条,就不会再把 Protocol 当组件、把「框架化/接口化」的接口形态和组装方式混在一起。

本篇讲网络系统「内部有哪些东西」(构成视角);04-网络系统设计从想法到实现 讲「拿到通信需求后按什么步骤设计」(设计视角,含组件设计的原子步骤清单)。