接收结束与响应完成是两件事
一句话总结
对方发送完毕,并不意味着我不能再回复。必须把接收 EOF 与发送队列耗尽作为两种不同的状态来管理。
为什么需要它
前面实验中按行处理的服务器,遇到 EOF 就标记 closed 并回收套接字。在客户端收发完请求与响应之后关闭连接的范围内,这是一个容易理解的起点。但是,如果还要支持这样的客户端:发完请求后只关闭自己的发送方向,然后等待结果,就必须扩展这一策略。出问题的不是多路复用本身,而是只用一个布尔值来表示终止的模型的局限。
这次要做一个玩具般的“大写印刷所”。客户端发送稿件字节,然后关闭发送方向。印刷所把 EOF 当作稿件的结尾,发回把 ASCII 小写字母换成大写字母的答复。如果因为交出稿件的客户端闭上了嘴,就认为它连耳朵也关上了,那就会把答复丢掉。实验用的协议是每个连接只有一次请求,它不能替代 HTTP、TLS 或持久连接协议。
工作原理
shutdown(2) 会区分要终止读还是写的方向。客户端的 shutdown(socket.SHUT_WR) 表示不再发送,而仍然可以继续通过同一个套接字读取响应。它与回收本地文件描述符的 close 作用不同。尝试用已经 close 的套接字再 recv 的示例,测试的并不是半关闭。
recv(2) 在流遇到 EOF 时的返回值是 0。在 Python 中,是通过以正数大小请求的 recv 返回 b"" 来观察到的。不能把这条规则推广到 UDP 的长度为 0 的数据报,或 recv(0) 上。尤其是,如果因为输入缓冲区已经装满就调用 recv(0),就可能在没有稿件已结束的证据的情况下,判定为已结束。本实验会多留出一个字节的观察空间,超出最大允许量,以此区分是 EOF 还是超限。
| 状态 | 关注的事件 | 下一步行动 |
|---|---|---|
| 正在接收稿件 | READ | 一次最多读取 4096 字节 |
| EOF,答复尚有剩余 | WRITE | 发送一次,只清除实际发出的长度 |
| 答复已全部离开队列 | 无 | 取消注册后关闭套接字 |
| 大小超限、错误、截止时间 | 无 | 保留原因,并回收缓冲区和套接字 |
EOF 不是需要继续去读的事件。如果因为答复还有剩余,就一直保持注册 READ,就会反复观察到 EOF,循环会空转。反过来,如果在 EOF 时把 WRITE 也一并去掉,那该发送的答复就永远无法推进。关注掩码不是由“连接是否还活着”这一个条件计算出来的,而是根据接收状态和剩余输出来计算。
send(2) 的成功,并不是对方应用已经读取并处理了答复的确认。实验中的 complete 仅限于表示本地输出队列已经清空。对于交易完成这类需要确认到对方处理结果的系统,需要单独的响应或应用层确认消息。这就是为什么不能只看正确答案是否留下了 complete,还要核对真实客户端收到的字节。
内存上限与截止时间是两种不同的防御
即使把稿件上限设为 4096 字节,一个字节一个字节缓慢发送的客户端,仍然可以长时间占着连接。如果只有每次有活动就延长的 idle timeout,它甚至可以一直活下去。这里是在开始时用 time.monotonic() + 수명(占位符为生存时长)确定绝对截止时间,即使收到字节或看到 EOF 也不延长。在 EOF 之后,对方不读取答复而导致发送被阻塞时,也适用同一个截止时间。在生产环境中,可以分别设定请求接收、处理和响应发送的截止时间,但这次的模型用一个总生存时长来结束这一责任。
超出上限时,不会返回巨大的错误响应,而是直接关闭。这是针对小型转换服务器的教学策略,在生产协议中还必须另行设计错误帧以及对未接收输入的处理。即使单个连接的输入和输出各自有限,只要连接数不受限制,总内存就不是有限的。因此仅凭这个实验,不能说整个服务的资源保护已经完成。
所有权在一处结束
Peer.finish 只修改终止原因和缓冲区状态,不会直接关闭套接字。按照 Python selectors 所指引的顺序,由循环一侧先 unregister 再 close。如果先关闭,已经失效的描述符就会留在注册列表中,使异常或泄漏分析变得困难。最后的 finally 也是必要的,因为它不仅在正常完成时、也要在中途异常时回收套接字。
在现场相遇的样子
2026-09-13 确认的 Cloudflare Spectrum 招聘启事 要求具备 Linux 套接字、TCP 连接状态、连接生命周期问题的调试,以及生产故障分析的能力。这个模块练习其中的终止方向、资源回收和边界条件验证。它并不是保证企业官方培训或录用备考的课程,Go、Rust 以及大规模分布式系统方面的要求,也不能仅靠这一个实验来满足。
下一项实验要做什么
分 8 个步骤构建 Peer 状态和多路复用循环。最后的检查会打开四个真实的 TCP 连接。第一个客户端保持沉默,下一个客户端把稿件分几次发出后只调用 SHUT_WR,其余的则构造空请求和超出上限的请求。要确认:正常的客户端无需等待沉默客户端的终止,就能收到完整的答复;所有套接字都被回收。部分 send 和 EAGAIN 通过另外的确定性测试来强制触发。不能仅凭小的响应恰好传输顺利这一观察,就说部分发送分支已经得到验证。