TT Lab
开始
学习 学习路径 课程

SSE — 服务端先说话的方式

照规范写一个解析器

在 TT Lab 中继续学习

目标

亲手实现 WHATWG HTML 标准的事件流解析规则,并在其上加上通过重连来续传的机制。

为什么重要

SSE 是“普通的 HTTP”,所以不用库也能使用。因此在实际工作中,一行 split("\n\n") 的解析器非常常见,而这种解析器遇到被拆成片段到达的输入时,马上就会崩溃。因为 TCP 并不保证按你发送的单位到达。

续传就更是如此。根据 id 缓冲区是否在每个事件之后被重置、是否派发被截断的最后一个事件,重连之后的界面会出现空缺或重叠。 规则在规范中都写得很清楚,本实验就是把这些规则转化为代码。

要创建的内容

在 /root/work/resume/client.py 中提供下面这些。

名称 契约
StreamParser feed(조각)(占位符为数据片段)→ 这次完成的事件列表。属性 last_event_id、reconnection_time
replay(events, last_event_id) 服务器一侧的回放窗口——返回该 id 之后的内容
resume_headers(parser) 重连时要携带的请求头字典

一个事件是一个字典:{"event": ..., "data": ..., "id": ...}。

步骤

  1. 片段与行结束符——feed 无论在哪里被截断,结果都相同
  2. 字段规则——注释、没有冒号的行、拼接 data
  3. id 缓冲区不会被重置
  4. retry 只在是 ASCII 数字时才生效
  5. 回放窗口——replay
  6. 重连请求头——resume_headers
  7. 亲自测量并写下——/root/work/resume/07-gap.txt
  8. 总结——/root/work/resume/08-notes.md

参考

片段与行结束符

在 /root/work/resume/client.py 中创建 StreamParser。feed(조각)(占位符为数据片段)返回用这个片段完成了的事件列表。无论输入在哪里被截断后到达,结果都必须相同,行结束符必须同时接受 CRLF、LF、CR 三种。最前面的一个 BOM 要丢弃。

mkdir -p /root/work/resume。把尚未处理的尾部保留在实例的缓冲区中,只处理到能找到行结束符为止。有一个陷阱——如果缓冲区以 \r 结尾,它可能是 \r\n 的前半部分,所以必须等待下一个片段。这一步只处理 data: 也可以。

字段规则

加入规范的字段规则。以冒号开头的行忽略,没有冒号的行整行是字段名,值为空字符串,data 把值拼接起来并每次多加一个换行,event 修改事件类型(默认是 message)。未知的字段忽略。

值前面的空格只去掉一个。在派发时,要在去掉换行之前检查 data 缓冲区是否为空字符串,如果不为空,只去掉最后的一个换行。所以只有一行 data 的块,会作为 data 为空字符串的事件派发出去。

id 缓冲区与最后的 id 字符串

处理 id 字段。如果值中含有 U+0000 NULL,就忽略这个字段,否则把内部的 id 缓冲区改成该值。用于重连的 last_event_id 会在派发事件的位置更新为该缓冲区的值,派发之后也不会被重置。所派发事件的 id 中放入该值。

规范在派发阶段写的是:①把最后的 id 字符串改成缓冲区的值,②如果 data 缓冲区为空,就从那里返回。顺序就是这样,所以只有 id: 而没有 data 的块,也会移动重连的位置,但如果空行没有到来,派发本身没有发生,就不会移动。而且 id 是字符串——不要把它转成整数。

retry 只在是 ASCII 数字时才生效

处理 retry 字段。只有值完全由 ASCII 数字组成时,才把 reconnection_time 改成该整数,否则忽略。初始值是 None。只有 retry: 而没有 data 的块不是事件。

Python 的 str.isdigit() 对全角数字也返回真。请像 value.isascii() and value.isdigit() 这样同时检查两个条件。空字符串、+100、3s、3.5 都属于要忽略的对象。

服务器一侧的回放窗口

添加 replay(events, last_event_id)。events 是带有 id 的字典列表。如果 last_event_id 为空或为 None,就返回全部;如果是窗口内的 id,就返回它之后的内容;如果是窗口中没有的 id,就返回 None。

None 是“无法续传,请从头重新接收”的信号。如果只发送还留着的内容,中间就会出现缺口,而且没有人知道这件事。id 的比较要按字符串原样进行——实际上确实会使用像 b7-1042 这样的 id。

重连请求头

添加 resume_headers(parser)。只有解析器的 last_event_id 不是空字符串时,才返回 {"Last-Event-ID": 값}(占位符为值),否则返回空字典。

规范只有在最后的 id 字符串不是空字符串时才携带请求头。如果带着空值发送,服务器就无法区分是“从 0 号之后开始”还是“从头开始”。请直接使用请求头名称的大小写和连字符。

亲自测量被截断的事件

把 /opt/fixtures/sse-resume/cut.txt 整个灌入你的解析器,并把得到的值以 이름=값(占位符依次为名称与值)的格式写成三行,保存到 /root/work/resume/07-gap.txt。events——派发的事件数。last_event_id——此时最后的 id。retry_ms——此时的 reconnection_time。

那个文件的最后一个事件在没有空行的地方被截断了。规范规定不要派发这样的事件,所以三个数字全都取决于这条规则。评分器会用你的解析器重新灌入同一个文件,并与写下的值核对,所以不要凭眼睛估算。

是什么堵住了缺口

在 /root/work/resume/08-notes.md 中写三行以上。没有 Last-Event-ID 时,重连之后会看到什么;派发被截断的事件会出现什么错位;对超出回放窗口的 id 只发送剩余内容,会产生什么。

正文中必须包含 Last-Event-ID、재접속(韩文,意为“重连”)、잘린(韩文,意为“被截断的”)(或 불완전(韩文,意为“不完整”))。这三点就是把 SSE 投入生产时,真正会把人叫起来的三件事。