beego中websocket需禁用自动渲染并调用c.stoprun()避免劫持连接后写http头错误,推荐单controller分流路径支持页面与ws,为send channel设缓冲防阻塞。

Beego 做实时日志推送控制台完全可行,但直接用默认配置会卡在连接劫持和模板渲染上——核心问题不是 WebSocket 本身,而是 Beego 的 HTTP 生命周期与 http.Hijacker 冲突。
WebSocket 连接总报 “response.WriteHeader on hijacked connection”
这是最典型的错误,出现在调用 upgrader.Upgrade 后 Beego 仍试图执行 Render() 或写入 HTTP 响应头。根本原因是 Beego 默认在 Controller 方法返回后自动调用 c.Render(),而 WebSocket 升级已劫持底层 TCP 连接,此时再写 HTTP 头就会 panic。
- 必须禁用当前控制器的自动渲染:在
Get()方法开头加c.Layout = ""和c.TplName = "",再调用c.StopRun()阻止后续流程 - 不能全局关
autorender = false(除非整个项目都不用模板),否则所有 HTTP 接口都会丢失自动渲染能力 - 升级前确保 ResponseWriter 尚未写入任何内容,比如避免提前调用
c.Data["json"] = ...或c.ServeJSON()
如何让一个 Controller 同时支持 HTTP 页面和 WebSocket 升级
常见需求是 /logs 页面展示 UI,/logs/ws 提供 WebSocket 接口。别用两个路由绑两个 Controller,用一个 Controller 分流更干净。
- 在
Get()中判断 URL 路径:if strings.HasSuffix(c.Ctx.Request.URL.Path, "/ws") { ... } - 路径匹配后立即做 WebSocket 升级,并调用
c.StopRun();否则走正常页面渲染逻辑 - 注意:Beego 的
c.Ctx.Input.Param(":splat")在 WebSocket 路由下不可靠,建议用原始 URL 解析 - 升级时传入的
*http.Request必须是原始请求对象,不要用 Beego 封装过的c.Ctx.Request副本(某些旧版 Beego 有浅拷贝问题)
日志行推送时 goroutine 泄漏和 channel 阻塞
日志源(如 tail -f 或文件监听)往 channel 发数据,WebSocket 客户端读 channel —— 一旦某个 client 的 send channel 满(比如网络卡住),整个日志广播就会被堵死。
- 给每个 client 的 send channel 设合理缓冲,比如
make(chan []byte, 64),而非无缓冲 - writePump 必须带超时写入:
select { case c.send - 注册 client 时把
deliverId或sessionid记进 map,便于按需关闭特定连接,而不是全量广播 - 别在主线程里循环
for range logChan向所有 client 发送——改用 fan-out 模式:一个 goroutine 读日志,多个 goroutine 各自负责一个 client 的发送
前端连不上 ws://localhost:8080/logs/ws?检查这几个点
浏览器控制台显示 WebSocket connection to 'ws://...' failed,大概率不是代码问题,而是 Beego 的中间件或配置拦截了 upgrade 请求。
- 确认路由注册用了
beego.Router("/logs/ws", &controllers.LogsController{}),不是beego.Get—— 后者只允许 GET,不支持 Upgrade header - 检查
conf/app.conf是否启用了EnableGzip = true,Gzip 中间件会干扰 WebSocket 升级,开发期建议关掉 - 如果用了 Nginx 反向代理,必须显式透传 Upgrade 和 Connection 头:
proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection "upgrade"; - Beego 默认开启 XSRF,但 WebSocket 不走 cookie 校验,所以不用关 XSRF,但要确保前端没误带
X-Requested-With等触发校验的 header
真正难处理的不是握手或推送,而是连接断开后如何清理 goroutine 和 channel。Beego 没有内置连接生命周期钩子,readPump 的 conn.ReadMessage 返回 error 时,必须手动从全局 client map 中删掉该实例并 close send channel——漏掉这步,内存和 goroutine 就会缓慢增长。











