长轮询需绕过Revel生命周期,直接注册到http.ServeMux;前端用递归fetch+isMounted防泄漏;手动解析Cookie获取Session;仅在WebSocket不可用时选用。

长轮询必须绕过 Revel 的响应生命周期
Revel 默认会在 Filter 链末尾调用 FinishRequest(),强制关闭 http.ResponseWriter 的写入通道。如果你在 controller 里写个 time.Sleep(30 * time.Second) 再返回数据,大概率触发 http: multiple response.WriteHeader calls 或 i/o timeout 错误——因为框架已经帮你把连接关了。
正确做法是:不走 Revel 的 Controller 流程,直接注册到原生 http.ServeMux 上。在 app/init.go 末尾加:
http.HandleFunc("/lpoll", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
<pre class="brush:php;toolbar:false;">// 设置超时,避免客户端挂太久
ctx, cancel := context.WithTimeout(r.Context(), 45*time.Second)
defer cancel()
// 模拟等待新事件(比如从 channel 或 Redis pubsub 接收)
select {
case data := <-eventChan:
json.NewEncoder(w).Encode(map[string]interface{}{"data": data})
case <-ctx.Done():
json.NewEncoder(w).Encode(map[string]interface{}{"status": "timeout"})
}})
- 确保
conf/routes中没有GET /lpoll这类路由定义,否则 Revel 会先抢走请求 - 如果启用了
StaticFilter,确认它没对/lpoll路径做通配拦截(默认只匹配/static/*) - 不要在 handler 里调用
c.Render()或任何 Revel 封装方法,它们依赖 controller 生命周期
前端如何安全发起长轮询并避免内存泄漏
长轮询不是“发一次等一次”,而是“收到响应立刻再发”。前端若用 setInterval 固定间隔拉取,会和后端的阻塞逻辑错位,导致请求堆积或漏消息。
推荐用递归 fetch + 组件生命周期管理(以 React 为例):
function useLongPoll(url) {
useEffect(() => {
let isMounted = true;
<pre class="brush:php;toolbar:false;">const poll = async () => {
if (!isMounted) return;
try {
const res = await fetch(url, { signal: AbortSignal.timeout(50000) });
const data = await res.json();
if (isMounted) {
// 处理业务逻辑,比如更新状态
handleUpdate(data);
}
} catch (err) {
if (isMounted && err.name !== 'AbortError') {
console.warn('长轮询失败,2s后重试', err);
setTimeout(poll, 2000);
}
} finally {
if (isMounted) {
// 立即发起下一轮,保持连接链路
setTimeout(poll, 0);
}
}
};
poll();
return () => { isMounted = false };}, [url]); }
- 必须用
isMounted判断组件是否还存活,否则卸载后仍执行setState会报 warning - 别用
AbortSignal.timeout()设得比后端超时还短,否则前端总在超时前主动断开 - 服务端返回
408 Request Timeout或204 No Content时,前端应立即重试,而非等固定 delay
如何让长轮询感知用户登录态和 Session
Revel 的 c.Session 只在 controller 生命周期内有效,而长轮询 handler 是独立挂载的,拿不到 c 实例。但原始 *http.Request 是完整的,Cookie 和 Header 都还在。
你可以手动解析 Cookie 获取 session ID,再查 Revel 的 session store(假设你用的是默认 cookie store):
func parseSessionID(r *http.Request) string {
for _, cookie := range r.Cookies() {
if cookie.Name == revel.DEFAULT_COOKIE_NAME {
return cookie.Value
}
}
return ""
}
<p>// 然后用 revel.SessionStore.Get() 拿 session 数据(需提前导出或复用 Revel 内部实例)
- Revel 的 session 默认用加密 cookie 存储,value 是 base64 编码的 JSON,可直接解密读取(但不推荐——应走标准解析路径)
- 更稳妥的方式是:在
app.Init()里把revel.SessionStore赋值给全局变量,长轮询 handler 中复用它 - 注意并发安全:Revel 的
SessionStore是线程安全的,但你自己封装的读取逻辑别在多个 goroutine 里共用未加锁的 map
别把长轮询当成 WebSocket 的廉价替代品
长轮询本质仍是请求-响应模型,每次连接建立都有 TCP 握手、TLS 开销、HTTP header 解析成本。在高并发场景下,它比 WebSocket 更吃服务器资源,也更容易被 CDN 或反向代理(如 Nginx)主动断连。
真正该考虑长轮询的场景只有两个:
- 客户端运行环境受限(比如老旧企业内网浏览器不支持 WebSocket)
- 服务端部署受限(比如共享主机不允许长连接、无法开 WebSocket 端口)
其他情况,优先上 gorilla/websocket —— 它和长轮询代码量差不多,但连接复用、双向通信、低延迟才是实时消息的合理基线。最容易被忽略的一点是:长轮询的“事件到达”时间 = 后端处理时间 + 网络延迟 + 前端重试间隔,而 WebSocket 是毫秒级推送。这个延迟差,在订单状态变更、客服消息这类业务里,就是体验鸿沟。

















