订单已经发货,页面为什么还停在“已支付”?
订单 O204 已创建。用户希望页面及时显示 paid、shipped、delivered 三个状态,偶尔还会提交操作。这里有两种方向:浏览器发出业务请求,服务器把稍后发生的变化送回页面。
每秒查询一次能工作,但没有变化时也会反复收到相同结果。保留一条连接也不自动解决通知:HTTP/1.1 Keep-Alive 只允许复用通道,HTTP/2 多路复用只允许多个交换并发,它们没有替应用定义“订阅订单变化”。
先选消息方向,再选传输方式,最后补上断线后的业务恢复。 一条可靠连接无法把关闭期间发生的事件自动送到下一条连接。
四种方式,等待放在哪里?
| 方式 | 谁先发起、何时返回 | 服务器到浏览器 | 浏览器到服务器 | 主要代价 |
|---|---|---|---|---|
| 普通轮询 | 客户端定时查询,每次尽快返回 | 到下一次查询才看到变化 | 普通 HTTP 请求 | 空查询与间隔延迟 |
| 长轮询 | 客户端查询,服务器等变化或截止时间后返回 | 待处理请求可较快返回变化 | 普通 HTTP 请求 | 保持待处理请求,返回后再发下一次 |
| SSE | 客户端建立 HTTP 请求,响应持续传送事件 | 同一响应不断提供事件 | 另发普通 HTTP 请求 | 流式转发、重连与事件历史 |
| WebSocket | 先建立协议通道,再交换消息 | 可主动发送消息 | 可主动发送消息 | 连接状态、心跳、恢复与队列管理 |
长轮询不是服务器无条件一直挂住请求。服务端通常设截止时间;没有变化也结束一次请求,客户端再订阅。若返回和下一次请求之间出现变化,服务端需要按游标查询历史或返回当前快照,不能只监听“下一刻的新变化”。长轮询的资源、超时与中间代理问题可参考 RFC 6202 §2。
普通轮询适合更新不频繁、允许几秒延迟的页面。建议对 O204 这种以服务器通知为主的场景先考虑 SSE,操作继续使用 HTTP;如果两个方向都要频繁交换低延迟消息,例如交互式协作,再考虑 WebSocket。方向不是唯一因素,还要看现有网关、客户端支持、消息量与恢复需求。
SSE:一个 HTTP 响应里有很多事件
服务器发送 Content-Type: text/event-stream,正文使用 UTF-8,按行组织字段。下面是一份教学事件块,最后还有一个空行:
id: 41
event: order
data: {"orderId":"O204","status":"paid"}data 是事件数据;多行 data 会以换行连接。event 指定事件类型,这里应监听 order,而不是只设置 onmessage;省略 event 时才使用默认 message。id 更新事件源保存的最后事件 ID,retry: 300 可给出以毫秒计的重连等待参数。以冒号开头的行是注释,不产生业务消息。
空行结束一个事件块,网络分块不等于事件边界。 如果最后一块没有空行就遇到流结束,浏览器不会把这份未完成数据当成完整事件派发。HTML Standard:事件流解析与解释
浏览器原生 EventSource 会按规则尝试重连,并在保存的 ID 非空时携带 Last-Event-ID。服务端收到 41 后能否提供 42、43,取决于它是否保存历史和如何解释游标。客户端 close 可主动结束订阅;服务端返回 204 可以要求停止重连,并不是所有错误都应无限重试。ID 在规范中是字符串,不保证递增,也不自动去重;这里把编号顺序作为订单服务自己的契约。HTML Standard:Last-Event-ID
原生 EventSource 构造器只接受 URL 和 withCredentials 配置,不提供任意方法、请求体或自定义 Authorization 首部选项。跨源凭据需要相应的 CORS 和 Cookie 条件;需要 POST 或自定义首部时,可以选择 fetch 读取流,但解析、重连等能力需要另外实现,不能把它当作原生 EventSource 的同一调用路径。
真实实验:半个事件不会变成一次业务通知
仓库公开入口用 Python 3.10+ 标准库启动本地 HTTP 服务,再让浏览器自己的 EventSource 解析。命令从仓库根目录运行,也可使用绝对路径:
python3 examples/network-interview/user_code/sse_resume.py打开终端打印的 http://127.0.0.1:随机端口/。服务仅监听回环地址,默认 60 秒后停止,也可 Ctrl-C;端口冲突时不需要猜一个新数字,默认由系统分配。先运行入口,再按 examples/network-interview/core/README.md 阅读 core/sse.py 的服务端与页面映射。
服务端第一次发送完整的 41,再发送缺少末尾空行的 42,然后结束响应。重连请求携带 41 时,服务端补发完整的 42,故意重复一份 42,再发 43。页面同时记录原始事件与显式去重后的应用编号,收到 43 后调用 close。
查看 Mermaid 源码
sequenceDiagram
participant B as 浏览器 EventSource
participant S as 本地订单事件服务
B->>S: GET /events,无 Last-Event-ID
S->>B: 完整事件 41,paid
S->>B: 未完成事件 42,缺空行
Note over B,S: 响应结束,42 不派发
B->>S: 自动重连,Last-Event-ID: 41
S->>B: 完整事件 42,shipped
S->>B: 重复事件 42,shipped
S->>B: 完整事件 43,delivered
Note right of B: 原始 ID:41、42、42、43<br/>显式应用 ID:41、42、43
Note right of B: close 停止订阅2026-10-09 实测 Python 3.13.3、Chrome 151.0.7922.34:原始 ID 为 41,42,42,43,应用编号为 41,42,43;服务端实际收到两次请求,分别没有 Last-Event-ID、携带 41。注释未派发,未完成的 42 未派发,完整重复的 42 仍派发两次。
实验中的 Set 只是页面内存去重。刷新页面会清空它,服务端历史也是写死的教学数据;实验没有验证多实例、持久日志、代理、认证或可靠业务提交。它证明的是浏览器事件边界、重连字段和重复派发行为。
动手对照派发游标与应用记录
同样的完整事件,可以被应用一次、两次,或者处理失败。沿着实验轨迹推进,观察两份记录何时分开。
固定订单O204:第一次响应在半个42后结束,重连补发42、重复42和43。逐步对照事件源与应用。
已派发ID:尚无
事件源游标:空
已应用ID:尚无
应用次数:0
最后已应用:空
用下一步观察空行、重连和重复事件分别改变哪一份记录。
固定轨迹重放,不建立EventSource或提交业务。失败分支假定应用捕获错误并暂停后续处理;原生事件源仍继续解析。编号顺序、历史和暂停策略均为本例契约。跨刷新恢复需要一致保存业务状态与已应用位置;真实浏览器实验在上方公开入口。
WebSocket:握手后双向交换消息
经典 HTTP/1.1 建连方式先发送 Upgrade 请求,服务端同意后返回 101。下面使用 RFC 的示例 nonce;这是握手格式,不是订单登录凭据:
GET /orders/O204/socket HTTP/1.1
Host: shop.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://shop.example.comHTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Accept 由 Key 字符串与规定 GUID 拼接,计算 SHA-1 后 Base64 得到,用于确认握手协议;它不是证书验证、密码登录或消息加密。之后双方使用 WebSocket 帧,不再为每条消息重复 HTTP 请求/响应首部。RFC 6455 §4
WebSocket 有文本、二进制与控制帧;一条消息可以拆成多个帧,浏览器 message 事件面对的是协议组装后的消息,不能将帧等同于 TCP 段。客户端到服务端的帧需要掩码,掩码不是加密;wss 才使用 TLS 保护相应的连接。RFC 6455 §5/§10.3
“所有 WebSocket 都靠 101 升级”也不准确。HTTP/2 可用扩展 CONNECT 建立 WebSocket,HTTP/3 有对应的建立方式;这些路径要双方支持,不能把上面的 Connection/Upgrade 报文原样套进去。RFC 8441:HTTP/2 WebSocket、RFC 9220:HTTP/3 WebSocket
浏览器 WebSocket 不按 fetch 的 CORS 响应共享规则来保护服务器,服务端仍需检查允许的 Origin、认证订阅者,并授权其访问 O204。收到 Origin 不等于通过身份认证,非浏览器客户端还可自行构造该字段。RFC 6455 §10.2:Origin 与安全
心跳证明了什么?
| 机制 | 能观察的信号 | 不能直接推出的结论 |
|---|---|---|
| TCP Keepalive | 按系统配置探测空闲传输连接 | 订单处理、消息落库成功 |
| WebSocket Ping/Pong | 对端协议处理响应,可帮助探测连接 | 某个业务消费者已经处理订单 |
| SSE 注释 | 响应有字节流动,可帮助避免空闲断开 | 浏览器已经应用事件、客户端回了确认 |
| 应用层确认 | 由应用契约定义“收到”或“处理完成” | 未明确契约时不能宣称恰好一次 |
浏览器 JavaScript 的标准 WebSocket API 不暴露协议 Ping/Pong 方法;socket.send("ping") 发的是应用数据,需要服务端按自定义规则响应,不能假装它是控制帧。WebSockets Standard:Ping/Pong
SSE 注释可以帮助维持空闲响应,但间隔要匹配链路中网关/NAT 的实际策略,还要让字节及时经过缓冲层。规范中的注释建议不是所有部署的统一超时。页面后台调度、网络短暂停顿、设备休眠也会影响应用心跳,超时意味着当前通道不可信,需要恢复策略,不自动等于服务器宕机。
标准 WebSocket API 没有自动重连功能。应用应区分主动结束、认证失败与暂时断线,对可重试错误采用退避和随机延迟;不要每个客户端同时无间隔重连。send 将数据放入发送流程,bufferedAmount 可观察未发送队列,不能当作服务器业务确认。WebSockets Standard:接口与发送
重新连上,如何证明没有漏消息?
假设页面应用到 41 后断线,服务端期间产生了 42、43。重建通道只恢复未来通信,恢复历史需要应用定义:
- 事件身份与范围。 编号属于哪个订单或订阅范围,服务端是否保留稳定顺序;验证用户是否有权读取该范围。
- 可重放历史。 按最后已应用游标提供后续事件。游标超过保留期时,明确返回需要重新获取快照,不能默默跳过缺口。
- 重复与应用确认。 消费端按事件身份/业务版本处理重复;如果要跨刷新或崩溃恢复,保存状态与已应用游标要有一致性契约。
- 快照与增量衔接。 获取最新订单快照后,从对应版本接续增量,避免“查完快照、订阅之前”的变化再次漏掉。
这是应用设计建议,SSE 和 WebSocket 都不会替你实现。尤其 Last-Event-ID 是浏览器事件源的解析状态,不是数据库提交成功的 ACK;事件处理函数仍可能失败,新的 EventSource 对象也不会自动继承旧对象的 ID。
对只显示当前状态的 O204 页面,重新查询一个带版本的最新快照可能已经足够。若需要显示完整操作记录,则必须保留可重放历史。若消息触发扣款等副作用,还要结合第 14 章的幂等与提交契约,不能只用页面 Set 保证资金正确。
服务器发了,为什么页面很久才看到?
若服务器有 flush 记录,客户端却整批收到,先检查代理是否缓冲响应、压缩层是否积攒输出,以及缓存配置;禁用缓存并不自动禁用转发缓冲。若连接周期性断开,则核对各跳空闲超时与最大连接时长,而不只看应用自己的定时器。
若页面越来越占内存,检查慢消费者和无界消息队列。WebSocket 双向并不等于无限吞吐,SSE 自动重连也不限制业务累积;建议按场景丢弃可替换状态、限制队列或提供快照恢复。订单通知不能因为其他订阅者卡住就无限保存每个客户端的副本。
30—60 秒参考回答
普通轮询定期查询,长轮询把一次请求留到有变化或超时后返回。SSE 在 HTTP 响应中持续发送 UTF-8 事件,适合服务器通知,浏览器 EventSource 支持按规则重连和 Last-Event-ID;客户端操作仍可走普通 HTTP。WebSocket 建立通道后能双向发消息,适合频繁交互,但标准浏览器 API 不自动重连。选型还要看代理、消息量和恢复需求。心跳只说明对应层的响应,TCP 可靠也不覆盖断线期间的消息;需要事件 ID、历史、去重、游标或快照来完成业务恢复。
四个追问及解析
1. SSE 是单向,所以页面不能提交操作吗?
单向说的是这条事件响应。页面仍可用 HTTP POST 提交操作,再从 SSE 接收变化;双向业务不必强行放进一条双向通道。若两个方向都频繁连续发消息,再比较 WebSocket 的收益。
2. Last-Event-ID 为 42,服务器就可以删除 42 吗?
不能据此认定业务已应用。它反映事件源保存的 ID,不是持久处理确认;服务器还要考虑其他消费者、历史保留期与应用 ACK 契约。浏览器内存状态不能代替业务数据库状态。
3. TCP 会重传,为什么还要重放事件?
TCP 在一条连接内恢复字节。连接已经结束后,新连接没有自动继承旧连接和业务消费位置;发送成功或入队也不能证明业务处理成功。重放解决跨连接、应用层的历史恢复。
4. WebSocket 比 SSE 更“实时”吗?
两者都可以及时转发服务器消息。延迟取决于生成、排队、转发缓冲、网络和消费速度;协议名字不提供统一毫秒保证。双向能力是选择因素,不能拿它替代实际延迟证据。
两个容易说错的结论
“SSE 自动重连,所以天然不丢不重。” 实验已经显示同 ID 的完整事件会重复派发;服务端没有历史时也无法补漏。自动建立通道和可靠应用是两件需要配合的工作。
“收到 Pong 就表示订单完成。” 协议处理层可能响应 Pong,而订单工作线程仍阻塞。只有应用契约明确的业务结果或确认,才能说明那笔订单的状态。
消息通道已经选好,字节仍可能先经过 CDN、反向代理和负载均衡。事件在哪里被缓冲、连接由谁终止、重连又落到哪台服务器,会直接影响页面看到的结果。
资料核验日期:2026-10-09。依据 WHATWG HTML/WebSockets 与 IETF RFC;SSE 实验的环境和输出已单独列明,WebSocket 报文为规范教学样例。