文章

【笔记】HTTP 与 SSE 的线协议模型

【笔记】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 规定”之前要先确认三个问题:

  1. 具体是哪一个 RFC;
  2. 它的 status/category 是什么;
  3. 是否已被后续 RFC 更新或废弃。

RFC 编号不会被回收或原地改写。后续修订通常发布新 RFC,并通过 UpdatesObsoletes 关系连接旧文档。

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 不双向”等问题时,依次检查:

  1. 连接层:实际连到哪个 IP 和端口,TLS SNI 是什么;
  2. HTTP 层:method、authority/Host、status、Content-Type 是否正确;
  3. SSE framing:每行字段和事件空行边界是否正确;
  4. 消息层:JSON-RPC ID、method、result/error 如何关联;
  5. 框架层:具体版本如何把对象序列化为 wire bytes。

上层对象看起来正确,不代表下层字节正确;反过来,wire format 正确也不代表应用层的消息关联和重试语义正确。

7. 复习索引

  • RFC 是文档系列,不是“RFC = 强制标准”。
  • Header 冒号后允许 OWS,冒号前不允许空白。
  • SSE 是服务器到客户端的持续 HTTP 响应,不是双向协议。
  • 多行数据在线上必须使用多条 data: 字段。
  • HTTP + SSE 可组成逻辑双向通道,但顺序、关联和恢复属于上层协议。
  • Spring SseEmitter 的多行字符串行为有版本差异,最终以 wire bytes 为准。

8. 核验入口

本文由作者按照 CC BY 4.0 进行授权