从 socket 到网络系统——网络通信是如何一步步长出来的

和「从指令到系统」同一种写法:从最简单的网络程序开始,每一步都是上一步「扛不住了」逼出来的。最终长出 EventLoop、协议、会话——这些概念不是某天被发明的,而是从一条 socket 调用演化出来的。


一、一条 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 里」是同一个阶段。

这个阶段有:socket、地址、字节流。没有连接管理,没有协议,没有并发。


二、第二个拐弯:循环处理

一条 socket 调用只能处理一次请求。要处理多次,需要循环:

1
2
3
4
5
6
7
8
while (1) {
int client = accept(server, ...); // 等待连接
char buf[1024];
recv(client, buf, sizeof(buf), 0); // 接收数据
// 处理...
send(client, response, len, 0); // 发送响应
close(client); // 关闭连接
}

和「顺序执行 → 循环」是同一步——程序从「做一次」变成「反复做」。

但这里有一个严重问题:处理一个连接时,其他连接在排队等待。 如果一个客户端发了大量数据,其他客户端全部阻塞。


三、第三个拐弯:多连接

要同时处理多个连接,需要管理多个 socket:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
// 处理...
send(clients[i], response, len, 0);
}
}
}

和「单文件 → 多文件」是同一步——职责从一个地方扩散到多个地方。

但遍历所有 socket 太慢了——100 个连接里可能只有 2 个有数据,但要逐个检查。


四、第四个拐弯:EventLoop

操作系统提供了高效的通知机制——只告诉「谁有事」,不用逐个检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// epoll(Linux)
int epfd = epoll_create(1);

while (1) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1); // 等待事件
for (int i = 0; i < n; i++) {
if (events[i].data.fd == server) {
// 新连接
int client = accept(server, ...);
epoll_ctl(epfd, EPOLL_CTL_ADD, client, &ev);
} else {
// 某个连接有数据
recv(events[i].data.fd, buf, sizeof(buf), 0);
// 处理...
}
}
}

这就是 EventLoop——事件循环。它的本质是:把「遍历所有连接」变成「只处理有事的连接」。

EventLoop 是网络系统的心脏——后续所有组件都围绕它运转。

EventLoop 是第一个有状态组件——它有自己的运行循环,自己决定什么时候处理什么事件。


五、第五个拐弯:协议出现

EventLoop 解决了并发问题,但收发的还是原始字节。调用者需要知道「这 4 个字节是命令,接下来 N 个字节是数据」——每个调用者都要自己解析。

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

需要一个统一的协议来定义消息格式:

1
2
3
4
5
6
// 协议:定义消息长什么样
struct Message {
uint32_t length; // 消息长度
uint32_t command; // 命令类型
uint8_t payload[];// 消息体
};

协议是独立的——它定义「消息长什么样」,不关心「字节怎么收发」。

协议的出现把「字节流」变成了「消息流」——调用者不再处理原始字节,而是处理结构化消息。


六、第六个拐弯:会话管理独立

每个客户端连接需要维护自己的状态——缓冲区、已收到的消息、协议状态(握手/正常/关闭)。这些状态不应该散落在 EventLoop 的回调里。需要独立的会话管理

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 是有状态的——每个客户端一个 Session,生命周期从连接建立到连接断开。

会话管理把「每个连接的状态」从 EventLoop 中抽离出来——EventLoop 只管事件分发,Session 管状态。


七、第七个拐弯:组件化

现在网络系统有了这些组成部分:

1
2
3
4
5
6
网络系统
├── EventLoop ← 有状态组件(事件循环,有自己的线程)
├── Connection ← 有状态组件(连接管理)
├── Session ← 有状态组件(会话管理)
├── Buffer ← 无状态工具(缓冲区)
└── Protocol ← 数据结构(协议定义,不是组件)

和「按功能分模块」是同一步——职责从一个 EventLoop 回调扩散到多个独立组件。

每个组件有明确的职责边界:EventLoop 管事件,Connection 管连接,Session 管状态,Protocol 管格式,Buffer 管缓冲。

注意:解析(字节 ↔ 消息转换)和调度(消息路由)不属于网络系统——它们属于入口系统(通信程序)。网络系统只负责收发字节和管理连接,不关心消息怎么解析、交给谁处理。


八、第八个拐弯:封装与调用

组件拆出来了,但还不能被直接调用——需要封装成可调用的系统。系统封装有两种:

1
2
3
4
5
6
① 内部封装好了框架或接口:
框架化 → 入口启动(入口创建对象、start/run、stop)
接口化 → 上层调用接口化后的接口(connect/send/close 等函数)

② 只有组件、没有封装:
→ 需要额外封装为框架(类)或接口(函数导出),才能被调用

框架化是入口启动,接口化是上层调用接口——系统封装成哪种形态,决定它被怎么使用。


九、网络系统 vs 业务系统

网络系统组装好后,和业务系统的关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
网络系统:
EventLoop + Connection + Session + Protocol + Buffer
职责:收发字节、管理连接、管理会话、定义协议格式
不关心消息怎么解析、交给谁处理

业务系统:
AuthService + SearchService + FileService ...
职责:处理具体的业务逻辑
不关心字节怎么收发、消息怎么解析

入口系统(CLI/服务器):
组装网络系统和业务系统
把入口系统的 Handler 指向业务系统的函数
1
2
3
入口系统
├── 网络系统(收发字节 + 管理连接)
└── 业务系统(具体业务逻辑)

网络系统和业务系统是独立演化的——网络系统不知道业务是什么,业务系统不知道网络怎么收发。它们通过 Handler 接口连接。


十、演化总结

1
2
3
4
5
6
7
8
9
一条 socket 调用      最简单的网络程序
循环处理 从一次到反复
多连接 同时处理多个客户端
EventLoop 高效事件通知(第一个有状态组件)
协议 字节流 → 消息流
会话管理 每个连接的状态独立
组件化 3 有状态组件 + 1 工具 + 1 数据结构(网络系统)
封装与调用 框架化(入口启动)/ 接口化(上层调用接口)
多系统 网络系统 + 业务系统 + 入口系统

网络系统的构成:有状态组件 EventLoop/Connection/Session + 无状态工具 Buffer + 协议数据结构 Protocol。
解析层和分发层属于入口系统(通信程序),不属于网络系统。

每一步都是上一步「扛不住了」逼出来的:

1
2
3
4
5
6
7
8
循环 → 因为一次不够
多连接 → 因为一个不够
EventLoop → 因为遍历太慢
协议 → 因为裸字节没法用
会话 → 因为状态散落各处
组件化 → 因为职责混在一起
封装与调用 → 因为组件需要被封装成可调用的系统
多系统 → 因为网络和业务需要独立演化

这就是网络系统从一条 socket 调用长成完整系统的全过程。和「从指令到系统」是同一种演化逻辑——只是领域不同。

演化史 vs 设计线:本篇讲网络系统「怎么一步步长出来」(演化视角);04-网络系统设计从想法到实现 讲拿到通信需求后「按什么步骤设计」(设计视角,含协议/组件/接口/框架的原子步骤清单)。两者互补。