必须用gorilla/websocket而非net/http原生升级,因其自动处理Ping/Pong、支持毫秒级WriteDeadline、错误堆栈可精准定位conn实例;roomid和clientid须原样透传,禁止strings.TrimSpace()等处理,否则导致注册失败或幽灵客户端;转发时须过滤自身消息并验证candidate字段,避免状态机崩溃。

为什么不能直接用 net/http 处理信令连接
WebRTC 信令本身不规定协议,但实际部署中几乎都依赖长连接(WebSocket 或 HTTP/2 Server-Sent Events),因为 net/http 的默认 handler 是无状态、短连接的——每次请求结束连接就关闭,无法维持客户端与服务端之间持续的会话绑定。你发一个 offer,服务端收得到,但没法立刻推给另一个在线 peer,除非你手动维护连接池 + 映射关系,这已经是在重复造轮子。
真正需要的是能双向实时通信的通道,所以得换底层传输机制:
-
gorilla/websocket是当前 Go 生态最稳定、文档最全的 WebSocket 实现,支持 ping/pong 心跳、连接超时控制、消息缓冲区配置 - 避免用
gobwas/ws:虽轻量,但对并发连接数突增的场景处理不够健壮,容易在高负载下漏消息 - 别自己基于
http.ResponseWriter拼Upgrade头:HTTP 协议升级逻辑复杂,gorilla/websocket已封装好边界检查和错误响应
如何用 gorilla/websocket 建立可路由的信令通道
关键不是“连上 WebSocket”,而是让每个连接能被识别、可寻址、可广播。你需要一个中心化的连接管理器,而不是把 conn 存在局部变量里。
典型结构是:map[string]*websocket.Conn + sync.RWMutex,key 通常来自 URL query 参数或 handshake 阶段的 token:
func handleSignaling(w http.ResponseWriter, r *http.Request) {
// 从 query 获取唯一标识,比如 ?id=alice
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "missing id", http.StatusBadRequest)
return
}
<pre class="brush:php;toolbar:false;">conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("upgrade error:", err)
return
}
defer conn.Close()
// 注册连接
clientsMu.Lock()
clients[id] = conn
clientsMu.Unlock()
defer func() {
clientsMu.Lock()
delete(clients, id)
clientsMu.Unlock()
}()
// 启动读写协程
go readPump(conn, id)
writePump(conn, id)}
注意点:
- 必须在
defer conn.Close()之前完成Upgrade,否则会 panic -
upgrader.CheckOrigin默认拒绝非同源请求,开发时可临时设为func(r *http.Request) bool { return true },上线前务必改回严格校验 - 不要在
readPump中直接调用conn.WriteMessage:WebSocket 不支持并发写,需用 channel + 单独writePump序列化输出
信令消息怎么设计才不容易丢、不串流
信令不是裸传 SDP 字符串,必须带上下文。常见错误是客户端只发 offer,服务端收到后随机选一个连接转发,结果发给了刚断线的 peer。
推荐最小可行消息格式(JSON):
{
"type": "offer",
"from": "alice",
"to": "bob",
"sdp": "v=0\r\no=- ..."
}
服务端据此做三件事:
- 验证
from是否已连接(查clientsmap) - 验证
to是否在线(同样查 map),否则返回{"type":"error","reason":"user-offline"} - 只向
to对应的*websocket.Conn发送,绝不广播
额外建议:
- 所有消息加
id字段(UUID),方便前端调试时追踪路径 - 服务端收到消息后立即回一个
{"type":"ack","id":"xxx"},客户端可据此判断是否需重发 - SDP 字符串不做任何解析或修改,原样透传——WebRTC 的兼容性问题大多出在人为篡改 SDP 上
为什么本地测试通了,一上公网就频繁断连
不是代码问题,大概率是中间网络设备(NAT、防火墙、CDN、反向代理)主动 kill 了空闲 WebSocket 连接。浏览器默认 ping 间隔是 30 秒,但很多企业网关只保持 15 秒连接。
解决方案必须双管齐下:
- 服务端启用
upgrader.KeepAlive = 10 * time.Second,强制 TCP 层保活 - 应用层加心跳:客户端每 25 秒发
{"type":"ping"},服务端收到即回{"type":"pong"},并更新该连接的最后活跃时间戳 - Nginx 反向代理必须显式配置:
proxy_read_timeout 60;、proxy_set_header Upgrade $http_upgrade;、proxy_set_header Connection "upgrade";
最容易被忽略的一点:客户端 WebSocket 构造函数里的 URL 必须带 wss://(而非 ws://)才能过 HTTPS 站点的混合内容拦截——哪怕你在本地用 http://localhost 测试没问题,部署到 HTTPS 页面后,浏览器会直接拒绝 ws:// 连接。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











