本文详解如何利用 http 请求上下文(request.context)实时检测 sse 客户端断开连接,避免 goroutine 泄漏,并提供可落地的代码实现与关键注意事项。
本文详解如何利用 http 请求上下文(request.context)实时检测 sse 客户端断开连接,避免 goroutine 泄漏,并提供可落地的代码实现与关键注意事项。
在基于 Server-Sent Events(SSE)构建实时聊天室(如按 Twitter 标签聚合)时,一个常见却易被忽视的问题是:客户端静默断开后,服务端仍在持续向已失效连接发送事件,导致 goroutine 泄漏、资源耗尽甚至内存增长。使用 antage/eventsource 等第三方库时,其 SendEventMessage 方法不会主动返回错误,也无法直接感知底层 TCP 连接状态——因此,必须借助 Go 标准库提供的生命周期信号机制。
✅ 正确方案:监听 Request.Context().Done()
自 Go 1.8 起,http.Request.CloseNotifier() 已被弃用;现代 Go 应用应统一使用 req.Context().Done() 通道来响应客户端断连、超时或请求取消事件。该通道会在以下任一情况发生时被关闭:
- 客户端主动关闭连接(如页面刷新、标签页关闭、网络中断);
- 客户端请求超时(由 net/http.Server.ReadTimeout 或反向代理设置);
- 服务端主动调用 http.CloseNotifier(不推荐)或上下文被取消。
? 实现示例(修复原始代码)
以下是重构后的 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)
// 启动消息推送 goroutine,并监听断连信号
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 在连接断开后会立即失败(底层 write 返回 io.ErrClosedPipe 等)
// 但更可靠的做法是先检查 done 通道
if err := es.SendEventMessage("blabla", "message", strconv.Itoa(id)); err != nil {
// 日志记录 + 提前退出
log.Printf("failed to send SSE event: %v", err)
return
}
case <-done:
log.Println("client disconnected, stopping message loop")
return
}
}
}()
}⚠️ 关键注意事项
- 不要忽略 SendEventMessage 的返回值:虽然 eventsource 库未强制校验,但底层 http.ResponseWriter.Write 在连接关闭后会返回 io.ErrClosedPipe、io.EOF 或 net.ErrClosed。务必检查错误并及时退出 goroutine。
- 使用 select + done 通道而非单纯 <-req.Context().Done():避免因 SendEventMessage 阻塞而错过断连信号。通过 select 实现非阻塞协作式退出。
- 避免 goroutine 泄漏:每个连接对应一个推送 goroutine,若未监听 Context.Done() 或未正确退出,将长期驻留内存。建议配合 pprof 定期监控 goroutine 数量。
- 注意 IdleTimeout 设置:eventsource.Settings.IdleTimeout 控制空闲连接超时,但它依赖于客户端心跳(如发送 :ping 注释)。若客户端不支持,仍需依赖 Context 判断真实断连。
- 生产环境补充日志与指标:记录断连原因(req.Context().Err() 可返回 context.Canceled 或 context.DeadlineExceeded),并上报断连率至 Prometheus。
✅ 总结
检测 SSE 客户端断连的本质,是将 HTTP 请求生命周期与业务 goroutine 生命周期对齐。req.Context().Done() 是 Go 生态的标准契约,它轻量、可靠且无需额外依赖。结合 select 控制流与错误检查,即可构建高健壮性的实时推送服务——既保障用户体验,又守住服务稳定性底线。










