Revel框架不提供消息推送功能,需拆解为服务端投递和客户端接收;正确做法是用带缓冲channel解耦WS连接与RabbitMQ发布,并确保连接管理、序列化兼容及前端真实接收链路完整。

Revel 框架本身不提供消息推送功能,所谓“推送”必须拆解为「服务端投递」和「客户端接收」两个环节;直接在 Controller 里调用 channel.Publish 或 conn.Publish 是常见错误起点——它既不能实时触达浏览器,也不具备重连、回退、离线保序能力。
WebSocket 连接中发消息到 RabbitMQ,别在 WSAction 里同步调用 channel.Publish
Revel 的 WebSocket 路由(如 WS /chat Application.Chat)拿到的是 *websocket.Conn,这个连接生命周期长、goroutine 安全性差。如果在 Chat 方法里直接调用 RabbitMQ 的 channel.Publish,一旦 broker 延迟或断连,整个 WS 连接会被阻塞,客户端表现为“卡死”或“自动断开”。
- 正确做法:把消息体序列化后,立刻写入一个带缓冲的
chan []byte(例如make(chan []byte, 100)) - 启动独立 goroutine 消费该 channel,并持有自己的
amqp.Channel,调用ch.Publish时必须包裹context.WithTimeout(ctx, 3*time.Second) - 关闭时机:在
defer conn.Close()前,向该 channel 发送nil作为哨兵值,通知 producer goroutine 退出 - 禁止复用全局
amqp.Connection或跨 goroutine 共享amqp.Channel——Channel 不是线程安全的
HTTP 控制器里投递消息,用 revel.Interceptor 管理生命周期
比如用户下单成功后要发库存扣减消息,不能在 OrderController.Create 里直接 dial + publish。因为 Revel 的拦截器链是同步执行的,而消息发送失败时你既不能 rollback 数据库事务(Revel 不管 DB),也不能简单返回 500(业务已成功)。
- 推荐方案一:写入本地
outbox表(字段含payload TEXT,status VARCHAR(20),created_at TIMESTAMP),再用后台 goroutine 定期扫描并批量投递 - 推荐方案二:在
revel.BEFORE拦截器中注册 cleanup 函数,在请求结束前 flush 待发消息(适合轻量、低频场景) - 无论哪种方式,
amqp.Publishing必须设DeliveryMode: amqp.Persistent,且ContentType显式设为"application/json" - 别依赖
revel.App全局变量存连接——它可能被并发访问,应改用连接池或 per-request 初始化
RabbitMQ 连接初始化必须放在 app.Init,且要处理重连
把 amqp.Dial 放 controller 里会导致每次请求新建连接,快速耗尽文件描述符;放错位置还会让连接无法被 revel.Server.Shutdown 正确关闭。
- 在
app.Init阶段调用amqp.DialConfig,显式设置Heartbeat: 10和ConnectionTimeout: 5 * time.Second - 必须监听
conn.NotifyClose(make(chan *amqp.Error, 1)),收到通知后触发重建逻辑(不要用死循环重试,应加指数退避) - vhost 需从 URL 显式解析,例如
amqp://user:pass@localhost:5672/my_vhost,不能依赖默认/ - 消费者启动必须在
app.OnAppStart中,且 handler 函数签名应为func(d amqp.Delivery) bool,返回true表示 auto-ack
前端如何真正“收到”推送?别只靠 alert 模拟
后端发进队列 ≠ 前端弹窗。真实推送链路是:RabbitMQ → 消费者服务 → HTTP API / WebSocket 推送端点 → 浏览器 EventSource 或 WebSocket 实例。
- 不要在模板里用
{{if .flash.success}}<script>alert(...)</script>{{end}}冒充推送——Flash 是单次跳转携带,不是实时通道 - 若用 WebSocket,前端需维护心跳、重连逻辑(如用
reconnecting-websocket库),服务端需在conn.WriteMessage前检查conn.IsClosed() - 若用 Server-Sent Events(SSE),后端需返回
text/event-stream,并保持连接不关闭;Revel 中需手动设置c.Response.Out.Header().Set("Content-Type", "text/event-stream")并禁用缓冲 - 大消息(>16KB)别走 WebSocket 文本帧,应改用 binary frame 或分片传输,否则触发
websocket: write deadline exceeded
最容易被忽略的是:消息体序列化必须跨语言兼容。Go 的 json.Marshal 默认不导出小写字段,而 Python/Node.js 消费者很可能按驼峰或下划线约定取值——要么统一用 json.RawMessage 透传,要么在结构体 tag 里显式声明 json:"event_type"。


















