SSE 是 HTTP 里最简单的“服务器推送”:一个不结束的响应,一行行文本。它没有 WebSocket 的双向和二进制,但自带断线重连。这篇用本机服务器逐个验证它的格式、重连规则,以及和 WebSocket、反复请求相比每条消息的开销。
响应头:HTTP/1.1 200 OK | Content-Type: text/event-stream | Cache-Control: no-cache | Transfer-Encoding: chunked 事件流文本(117 字节):b'retry: 500\n\nid: 1\nevent: tick\ndata: message 1\n\nid: 2\nevent: tick\ndata: message 2\n\nid: 3\nevent: tick\ndata: message 3\n\n' 每个数据块到达的时刻(从发出请求起,ms):0, 205, 410, 616 连接在发完 3 个事件后被服务器掐断(没有结尾的 0 长度块):正常结束 = False
一个不设 Content-Length 的 chunked 响应,Content-Type: text/event-stream,服务器想发就发。
服务器发出的原始文本:
': 这是注释,用于保活'
''
'data: 第一行'
'data: 第二行'
''
'event: update'
'id: 7'
'data: {"x":1}'
''
'data:no-space'
''
'unknown: field'
'data: after-unknown'
''
'data: 没有结尾空行的事件'
派发的事件:类型 'message',数据 '第一行\n第二行',id None
派发的事件:类型 'update',数据 '{"x":1}',id '7'
派发的事件:类型 'message',数据 'no-space',id None
派发的事件:类型 'message',数据 'after-unknown',id None
注释行(: 开头)被忽略;两个 data 行变成一个事件、值用换行连接;"data:no-space" 冒号后没有空格也行;未知字段 unknown 被忽略;最后一个事件没有结尾空行,不会被派发(解析器只在遇到空行时派发)字段名: 值,空行结束一个事件;多行 data 用换行连接;注释行用于保活;没有结尾空行的事件不会派发。
三轮收到的事件 id:[['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9']](没有重复、没有缺失:每次从 Last-Event-ID 的下一个继续) 服务器记录到的三次请求带的 Last-Event-ID:[None, '3', '6'] 断线(我们读到连接被掐断)到下一次请求到达服务器的间隔:[504, 502] ms(客户端按服务器给的 retry: 500 睡了 500 ms,再加上建立连接的时间)
服务器用 retry 指定重连等待时间,客户端用 Last-Event-ID 请求头告知断点,实现不丢不重。
SSE:服务器发出 34110 字节(响应头 105 字节,之后每条 34.0 字节:"data: " 6 + 20 字节载荷 + "\n\n" 2 = 28 字节事件文本,再加分块框架 "1c\r\n" 和 "\r\n" 共 6 字节 = 34 字节) WebSocket:每条帧头 2 字节 + 20 字节载荷 = 22 字节(服务器→客户端不掩码,<126 字节的载荷),1000 条 22000 字节(不含一次性的升级握手) 反复请求(同一条 keep-alive 连接上 1000 次 GET):服务器→客户端 85000 字节(每次响应头约 65 字节 + 20 字节载荷);另外客户端→服务器每次还要发 51 字节的请求 按单条消息:SSE 34 字节、WebSocket 22 字节、反复请求 85 字节;反复请求还多一个往返的延迟
每条 20 字节消息:WebSocket 22 字节,SSE 34 字节,反复请求 85 字节(还多一个往返)。