直接用net/http实现SSE会断连,因未显式设置Content-Type:text/event-stream、Cache-Control:no-cache和Connection:keep-alive,未调用http.Flusher.Flush()强制刷新缓冲区,且未禁用Read/WriteTimeout或对齐Nginx proxy_read_timeout。

为什么直接用 net/http 实现 SSE 会断连?
Go 标准库的 http.ResponseWriter 默认启用 HTTP/1.1 的连接复用(keep-alive),但 SSE 要求连接长期保持打开且持续写入数据。如果响应体未显式设置 Content-Type: text/event-stream、未禁用缓冲、未及时刷新,客户端会等超时或收到不完整 chunk 后关闭连接。
- 必须在写入前调用
w.Header().Set("Content-Type", "text/event-stream") - 必须调用
w.(http.Flusher).Flush()强制刷新,否则数据卡在 Go 的bufio.Writer缓冲区 - 需设置
Cache-Control: no-cache和Connection: keep-alive防止代理或浏览器缓存中断流 - 不要用
json.Marshal直接写结构体——SSE 协议要求每条消息以data:开头,换行分隔
如何安全地向多个客户端广播事件而不阻塞?
用全局 map + 互斥锁管理客户端连接容易因写锁争用导致广播延迟;而为每个连接启 goroutine 监听 channel 又可能失控。推荐用“发布-订阅”模式配合无锁通道 + 连接生命周期管理。
- 每个客户端连接对应一个独立的
chan string,写入事件时只发到该 channel,不涉及共享 map 写操作 - 用
sync.Map存储clientID → chan string映射,读多写少场景下比sync.RWMutex + map更轻量 - 在 handler 中启动 goroutine 持续从 channel 读取并写入 response,主 goroutine 仅负责注册/注销和接收广播信号
- 务必监听
http.Request.Context().Done(),客户端断开时及时 close channel 并从sync.Map中删除键值
如何避免 long-running handler 导致内存泄漏?
goroutine 泄漏是 SSE 微服务最常见的故障源:客户端异常断开后,若未检测到连接关闭,对应的广播 goroutine 会永远阻塞在 channel 读取上,堆积大量 goroutine。
- 每次向 client channel 发送前,先用
select+default判断是否已满(说明客户端消费慢或断开) - 在写入 loop 中加入
if !ok { break }检查 channel 是否已被 close - 使用
http.TimeoutHandler包裹 handler 无法解决根本问题——SSE 本就是长连接,应靠 context 和 channel 关闭机制清理 - 可加简单心跳:每 30 秒写入一个空
:注释行,既维持连接,又能让客户端感知存活状态
怎样让 SSE 服务支持 reconnect 和 event ID 恢复?
浏览器原生 SSE 自动重连,但默认从头开始。要实现断线续传,需服务端维护每个 client 的最后事件 ID,并在重连请求中通过 headers["Last-Event-ID"] 获取断点。
立即学习“go语言免费学习笔记(深入)”;
- 客户端首次连接不带
Last-Event-ID,服务端从当前游标开始推送;重连时带上该 header,服务端据此跳过已发送事件 - 事件格式必须包含
id:字段,例如id:12345\ndata:{"msg":"hello"}\n\n,否则浏览器不会更新 Last-Event-ID - 服务端需为每个 client 维护一个递增的
lastSentID,可通过原子计数器或数据库序列生成 - 注意:Go 的
http.Request.Header.Get("Last-Event-ID")返回的是字符串,需手动strconv.ParseInt转换,失败则按 0 处理
proxy_read_timeout 300;Kubernetes Service 的 connection timeout 也需同步调整。这些不是代码层能绕过的限制。


















