用 Gin 做 SSE 推送,c.Stream 和 c.SSEvent 到底怎么配合?什么时候事件才会真正发给客户端?客户端断开后会怎样?这篇用真实的 HTTP 流量记下每个事件的到达时间,并验证 c.Stream 最容易踩的“step 不阻塞”问题。
context.go 的 Stream(Gin v1.12.0):
1322 | func (c *Context) Stream(step func(w io.Writer) bool) bool {
1323 | w := c.Writer
1324 | clientGone := w.CloseNotify()
1325 | for {
1326 | select {
1327 | case <-clientGone:
1328 | return true
1329 | default:
1330 | keepOpen := step(w)
1331 | w.Flush()
1332 | if !keepOpen {
1333 | return false
1334 | }
1335 | }
1336 | }
1337 | }
context.go 的 SSEvent(Gin v1.12.0):
1313 | func (c *Context) SSEvent(name string, message any) {
1314 | c.Render(-1, sse.Event{
1315 | Event: name,
1316 | Data: message,
1317 | })
1318 | }SSEvent 只是把事件写进 ResponseWriter,并不保证已经发给客户端。Stream 在每次 step 之后调用 Flush,所以事件即时到达。它的循环本身不等待:节奏完全由 step 决定。
状态 200;Content-Type="text/event-stream;charset=utf-8";Cache-Control="no-cache";Connection="";X-Accel-Buffering=""
原始正文:
|event:message
|data:hello
|
|event:num
|data:42
|
|event:obj
|data:{"a":1,"b":"x\u003cy"}
|
|event:multi
|data:line1
|data:line2
|data:line3
|
|data:没有事件名
|
|event:bytes
|data:raw
|
|event:nilmsg
|data:<nil>
|
|响应头 Content-Type: text/event-stream;charset=utf-8 与 Cache-Control: no-cache 由 SSEvent 设置;反向代理常用的 X-Accel-Buffering: no 与 Connection 需要自己加。多行文本会拆成多个 data: 行,结构体序列化成 JSON,传 nil 会输出字面量 <nil>,别把它当作“空数据”。
/stream 响应头到达 203 ms;事件到达:[1@203ms 2@404ms 3@604ms 4@805ms];TransferEncoding=[chunked]
/noflush 响应头到达 805 ms;事件到达:[1@806ms 2@806ms 3@806ms 4@806ms];TransferEncoding=[]
/flush 响应头到达 200 ms;事件到达:[1@200ms 2@402ms 3@603ms 4@805ms];TransferEncoding=[chunked]小于缓冲区(约 4 KB)的响应,不 Flush 就要等 handler 结束才发出——实测 4 个事件在 806 ms 一起到达。自己写循环时,每个事件之后都要 c.Writer.Flush()(与 c.Stream 的效果相同)。经过带缓冲的代理(nginx 默认)还需要关闭代理缓冲,本文没有验证。
客户端在约 330 ms 时断开,此时 step 已调用 6 次
断开后 31 ms:Stream 返回 clientGone=true;step 共被调用 7 次(断开之后又多调用 1 次)Stream 用 CloseNotify(已被标准库标记为废弃,但仍然可用)发现断开,检查发生在下一轮循环,所以断开之后 step 还会多执行一次。自己写循环时应该改用 c.Request.Context().Done() 判断。
/spin step 被调用 1911526 次
/wait step 被调用 3 次Stream 不会替你等待:step 如果立即返回 true,就会变成一个空转的死循环,100 ms 内调用了近 200 万次。把 step 写成“从 channel 取一个事件、写出、返回 true”(或者等 ticker),这样既有节奏也不占 CPU。