Kratos本身不内置长轮询支持,但可在HTTP handler中用net/http实现:需禁用超时、正确使用context和事件channel;http.TimeoutHandler会强制返回504使长轮询退化为短轮询,且默认Read/WriteTimeout必须设为0,否则连接被中间件切断。

Kratos 本身不内置长轮询支持,但完全可以用标准 net/http 方式在 Kratos 的 HTTP handler 中实现,关键在于禁用超时、正确使用 context 和事件 channel。
为什么不能直接用 Kratos 的 http.TimeoutHandler
因为 http.TimeoutHandler 在超时后会强制关闭连接并返回 504,这会让长轮询退化成短轮询——客户端刚收到空响应,立刻发下一次请求,完全失去“有数据立刻返回”的意义。Kratos 默认的 HTTP server 配置(如 ReadTimeout / WriteTimeout)也必须设为 0,否则 Nginx 或 Go 自身会在中间切断连接。
常见错误现象:
- 前端看到大量 504 或连接被 reset,日志里反复出现
context deadline exceeded - 明明服务端已推送数据,客户端却卡住直到超时才收到响应
- 并发一高,goroutine 数暴涨且不回收,内存持续增长
在 Kratos 的 HTTP handler 里怎么写长轮询逻辑
Kratos 的 HTTP handler 本质还是 http.HandlerFunc,你可以在 internal/server/http.go 或自定义路由中直接注册一个长轮询 endpoint,比如 /notify/longpoll。核心是用 select 监听三类信号:
-
req.Context().Done():客户端断开或主动取消(必须检查) - 自定义事件 channel(如从消息总线、
sync.Map或业务层推送来的chan interface{}) -
time.After(timeout):防止单次等待无限期挂起(建议 30–60 秒)
示例片段(非完整服务,仅展示关键控制流):
func longPollHandler(w http.ResponseWriter, r *http.Request) {
// 禁用 gzip,避免流式响应被缓冲
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
<pre class="brush:php;toolbar:false;">ctx := r.Context()
done := ctx.Done()
// 每次请求新建一个接收 channel,避免复用和阻塞
ch := make(chan string, 1)
defer close(ch)
// 注册到全局事件分发器(比如用 sync.Map 存 remoteAddr → ch)
registerListener(r.RemoteAddr, ch)
// 主等待循环
select {
case data := <-ch:
json.NewEncoder(w).Encode(map[string]string{"data": data})
case <-done:
// 客户端断开,清理
unregisterListener(r.RemoteAddr)
return
case <-time.After(45 * time.Second):
// 超时,返回空响应,让前端重连
json.NewEncoder(w).Encode(map[string]string{"data": ""})
}
// 每次响应后务必清理
unregisterListener(r.RemoteAddr)}
如何避免 goroutine 泄漏和 write on closed connection
长轮询本质是“一个连接一个 goroutine”,一旦客户端异常断连而服务端没感知,goroutine 就永远卡在 select 里。Kratos 的 http.Request.Context() 是唯一可靠信号源,必须在所有可能阻塞的地方检查它:
- 每次向
http.ResponseWriter写数据前,加if ctx.Err() != nil { return } - 不要把
*http.ResponseWriter保存到结构体或全局变量中——它不是线程安全的,且生命周期只限当前 handler - 事件推送侧用
select { case ch 非阻塞发送,防止因接收方未 ready 卡住生产者 - 用
sync.Map管理活跃连接对应的 channel,key 建议用r.RemoteAddr + timestamp避免冲突
真正难的不是写通逻辑,而是压测时暴露的连接泄漏、channel 积压、Nginx 代理超时配置遗漏——这些细节往往在开发阶段完全看不出来,只有真实用户量上来才会爆发。


















