
本文详解如何在基于 antage/eventsource 的 SSE 服务中可靠检测客户端断开连接,避免 goroutine 泄漏;核心方案是利用 http.Request.Context().Done() 通道监听请求生命周期终结事件。
本文详解如何在基于 `antage/eventsource` 的 sse 服务中可靠检测客户端断开连接,避免 goroutine 泄漏;核心方案是利用 `http.request.context().done()` 通道监听请求生命周期终结事件。
在构建基于 Server-Sent Events(SSE)的实时聊天系统(如按 Twitter 标签聚合的聊天室)时,一个常见却关键的问题是:服务端无法感知客户端何时关闭连接,导致后台推送 goroutine 持续运行、资源泄漏甚至引发 panic。你提供的代码中,go func(){ ... } 启动了一个无限循环向客户端发送消息的协程,但缺少对连接状态的监听机制——一旦用户刷新页面、关闭标签页或网络中断,该 goroutine 仍会持续尝试写入已关闭的 HTTP 连接,最终可能触发 write: broken pipe 错误或堆积大量无用 goroutine。
✅ 正确做法:监听 Request.Context().Done()
自 Go 1.8 起,http.CloseNotifier 已被弃用,官方推荐且唯一健壮的方式是使用 *http.Request 自带的上下文(req.Context())。该上下文会在以下任一情况发生时被取消,并关闭其 Done() 通道:
- 客户端主动断开连接(如关闭浏览器标签页);
- 请求超时(由 http.Server.ReadTimeout 或反向代理配置决定);
- 服务端主动调用 context.CancelFunc(较少见)。
因此,我们应在启动推送 goroutine 前,预先启动一个监听协程,等待 req.Context().Done() 触发,从而安全地终止消息推送逻辑。
? 示例:修复后的完整 ServeHTTP 实现
func (sh StreamHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
es := eventsource.New(
&eventsource.Settings{
Timeout: 2 * time.Second,
CloseOnTimeout: true,
IdleTimeout: 2 * time.Second,
Gzip: true,
},
func(req *http.Request) [][]byte {
return [][]byte{
[]byte("X-Accel-Buffering: no"),
[]byte("Access-Control-Allow-Origin: *"),
}
},
)
// 启动 SSE 服务(非阻塞)
es.ServeHTTP(resp, req)
// ✅ 关键:监听请求上下文取消信号
done := make(chan struct{})
go func() {
<-req.Context().Done()
close(done)
}()
// ✅ 推送协程需主动检查连接是否有效
go func() {
var id int
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
id++
// SendEventMessage 在连接断开时会失败(返回 error),应检查
if err := es.SendEventMessage("blabla", "message", strconv.Itoa(id)); err != nil {
// 日志记录 + 提前退出
log.Printf("SSE send failed for client %s: %v", req.RemoteAddr, err)
return
}
case <-done:
// 客户端已断开,优雅退出
log.Printf("Client %s disconnected, stopping SSE feed", req.RemoteAddr)
return
}
}
}()
}⚠️ 注意事项与最佳实践
- 不要忽略 SendEventMessage 的返回值:该方法在底层 http.ResponseWriter.Write() 失败时会返回非 nil error(例如 write: broken pipe),必须显式检查并退出循环,否则可能掩盖断连信号。
- 避免使用 time.Sleep 配合 select:原始代码中的 time.Sleep(1 * time.Second) 是阻塞式延时,无法响应 done 通道。务必改用 time.Ticker + select 实现非阻塞定时与多路复用。
- 日志与可观测性:建议记录客户端断连事件(含 req.RemoteAddr 和时间戳),便于排查连接稳定性问题。
- 资源清理:若推送逻辑中持有额外资源(如数据库连接、订阅句柄等),应在 case <-done: 分支中执行清理操作。
- Context 超时设置:确保你的 http.Server 配置了合理的 ReadTimeout 和 WriteTimeout,以配合 req.Context() 的生命周期管理。
✅ 总结
检测 SSE 客户端断连的本质,不是“轮询连接状态”,而是信任 Go HTTP 标准库对请求生命周期的管理能力。通过监听 req.Context().Done() 并结合非阻塞的 select 控制流,即可实现零泄漏、高可靠的消息推送服务。这是现代 Go Web 开发中处理长连接的标准范式,适用于所有基于 net/http 的流式协议(SSE、WebSocket 封装层、gRPC-Web 等)。










