【笔记】HTTP 与 SSE 的线协议模型
HTTP、SSE、JSON-RPC 经常一起出现,但它们不在同一层。HTTP 提供请求和响应,SSE 定义服务器如何在一个响应体里持续发送事件,JSON-RPC 则定义事件或请求承载的应用消息。
一句话心智模型是:先看线上实际传输的 method、header 和 body bytes,再讨论框架 API 或“像 stdin/stdout”这样的类比。
1. RFC 不是“所有文档都是标准”
RFC 是 Request for Comments 的缩写,名称来自互联网早期以公开备忘录讨论设计的传统。今天它是一套编号、永久发布的技术文档系列,但 RFC 的发布状态并不都相同。
一份 RFC 可能是:
- Standards Track 上的 Proposed Standard 或 Internet Standard;
- Best Current Practice(BCP);
- Informational;
- Experimental;
- Historic。
因此,“RFC 规定”之前要先确认三个问题:
- 具体是哪一个 RFC;
- 它的 status/category 是什么;
- 是否已被后续 RFC 更新或废弃。
RFC 编号不会被回收或原地改写。后续修订通常发布新 RFC,并通过 Updates、Obsoletes 关系连接旧文档。
2. HTTP Header 的宽松外观来自明确语法
下面两种写法都会被常见客户端接受:
1
2
curl -H 'Host:example.com' https://example.com
curl -H 'Host: example.com' https://example.com
在线语法可以简化为:
1
field-name ":" OWS field-value OWS
OWS 是 optional whitespace。冒号后可以没有空格,也可以有可选空白;冒号前不能插入空白。字段名大小写不敏感,但 HTTP/2 和 HTTP/3 在线上要求字段名使用小写。
所以 curl -H 'host: demo-service.example.com' 并不是 curl 随意猜测格式,而是它接受一个 header 字符串,并按 HTTP 语法发送。排查时可用:
1
curl -v -H 'Host: demo-service.example.com' https://origin.example.com/health
这里 URL 决定连接目标和 TLS 处理;Host 或 HTTP/2 的 :authority 表达应用层目标主机。两者可以不同,这正是反向代理、虚拟主机和 Ingress 调试常用的手段。
3. SSE 是一个持续的 HTTP 响应
服务器返回 Content-Type: text/event-stream 后,可以保持响应体打开并连续写入事件:
1
2
3
4
5
event: message
id: 42
data: first line
data: second line
空行结束一个事件。浏览器解析多个连续 data: 字段时,会用换行拼接它们,最后移除末尾换行。因此上面的应用数据是:
1
2
first line
second line
SSE 的常见字段还有:
event::事件类型;id::更新客户端的 last event ID;retry::建议重连等待时间;- 以
:开头的行:注释,可用于心跳; - 未识别字段:忽略。
SSE 只定义服务器到客户端的事件流。它没有为客户端到服务器增加一条反向通道。
4. HTTP + SSE 可以拼出应用层双向通信
一些协议会把两个方向拆成不同 HTTP 交互:
sequenceDiagram
participant C as Client
participant S as Server
C->>S: GET,建立 SSE 响应
S-->>C: SSE event,持续下行
C->>S: POST,提交 JSON-RPC 消息
S-->>C: HTTP response,确认 POST 结果
S-->>C: SSE event,发送异步 JSON-RPC 消息
把它类比为 stdio 时,可以说:
- POST 类似客户端写入 server stdin;
- SSE response 类似客户端持续读取 server stdout。
这个类比只解释方向,不代表协议等价:
- stdio 是同一进程关系中的字节流;HTTP 是独立 request/response;
- SSE 具有事件边界、字段语法、重连和 last-event-id 语义;stdout 没有;
- POST 成功只说明这次 HTTP 请求完成,不天然证明异步处理完成;
- 并发、顺序、关联 ID、断线恢复都必须由上层协议定义。
所以更准确的说法是:HTTP request 与 SSE response 被上层协议组合成逻辑双向通道,而不是“SSE 本身是双工协议”。
5. 多行 data 要区分规范与框架版本
在线格式不能直接写成:
1
2
3
data:first line
second line
第二行没有字段名,会被解析器忽略。正确格式是每一行都带 data::
1
2
3
data:first line
data:second line
5.1 Spring SseEmitter 的行为发生过变化
不能笼统地说 SseEmitter 始终会或始终不会处理换行:
- Spring Framework 5.3 和 6.0 的
SseEventBuilderImpl.data(...)直接把对象放在一个data:字段后,不主动拆分字符串换行; - Spring Framework 6.1、6.2 会把字符串中的
\n替换成\ndata:; - 当前主线进一步统一处理
\n、\r和\r\n,为每个逻辑行补上data:。
因此,升级或排查时应同时确认 Spring 版本、传入对象类型和最终选中的 HttpMessageConverter。只有 String 分支的源码行为,不能自动推导任意 JSON 对象序列化后的换行处理方式。
5.2 最可靠的验证方式
不要只看 Java 对象或框架方法名,直接观察 wire format:
1
curl -N -v https://example.com/events
测试至少覆盖:
- 单行字符串;
\n、\r\n和空行;- JSON 字符串中的转义换行;
- 多个连续事件的空行边界;
- 断线重连与
Last-Event-ID。
JSON 中的 "\\n" 是两个转义字符在 JSON 文本中的表示,和 SSE framing 使用的真实换行不是同一层。先完成 JSON 编解码,再判断得到的应用数据是否需要映射成多条 data: 字段。
6. 一套分层排障方法
遇到“消息丢行、header 不生效、SSE 不双向”等问题时,依次检查:
- 连接层:实际连到哪个 IP 和端口,TLS SNI 是什么;
- HTTP 层:method、authority/Host、status、Content-Type 是否正确;
- SSE framing:每行字段和事件空行边界是否正确;
- 消息层:JSON-RPC ID、method、result/error 如何关联;
- 框架层:具体版本如何把对象序列化为 wire bytes。
上层对象看起来正确,不代表下层字节正确;反过来,wire format 正确也不代表应用层的消息关联和重试语义正确。
7. 复习索引
- RFC 是文档系列,不是“RFC = 强制标准”。
- Header 冒号后允许 OWS,冒号前不允许空白。
- SSE 是服务器到客户端的持续 HTTP 响应,不是双向协议。
- 多行数据在线上必须使用多条
data:字段。 - HTTP + SSE 可组成逻辑双向通道,但顺序、关联和恢复属于上层协议。
- Spring
SseEmitter的多行字符串行为有版本差异,最终以 wire bytes 为准。