可行但需自建连接池与精准路由:必须维护clientid→conn映射、心跳检测、定向转发offer/answer/candidate,杜绝广播式发送,严格校验消息字段与在线状态。

直接用 Gin 搭 WebRTC 信令服务是可行的,但关键不在“能不能跑起来”,而在于 gorilla/websocket 连接生命周期管理、消息路由逻辑是否健壮——绝大多数线上故障都出在这两块,不是 SDP 交换失败,而是连接断了没感知、候选发丢了没重传、用户离线后还在往空连接发消息。
为什么不用 WebSocket 库自带的 broadcast 而要自己管连接池
WebRTC 信令是点对点强状态的:A 发 Offer 给 B,B 必须在连接存活时收到并响应 Answer;如果 B 的 WebSocket 连接已断开(比如页面刷新、网络抖动),Gin 后端若还把消息往这个连接写,会触发 write tcp: broken pipe 或静默失败。gorilla/websocket 的 hub 示例只适合聊天室这种弱状态场景,不适用于信令。
- 必须为每个用户维护唯一
clientID→*websocket.Conn映射,且带心跳检测(建议用conn.SetPingHandler+ 定时conn.WriteMessage(websocket.PingMessage, nil)) - 连接断开时,立刻从 map 中删除,并广播
{"type":"offline","from":"userA"}给房间内其他用户 - 发送消息前,先查 map 是否存在该 clientID,不存在就丢弃或返回错误(不要 panic,也不要 log 后继续写)
Offer/Answer/Candidate 消息怎么路由才不出错
信令消息不是广播,是定向转发。前端发来的 {"type":"offer","to":"userB","sdp":"v=0..."},后端不能简单存进全局 channel 然后随机推给所有客户端——必须精确匹配 to 字段,找到 userB 的连接,再 write。
-
type字段必须校验:只允许"offer"、"answer"、"candidate"、"join"、"leave",其余直接 reject -
to和from都要非空且合法(比如正则^[a-zA-Z0-9_]{3,20}$),防止注入或误路由 -
candidate消息必须带mid和candidate字段,否则前端pc.addIceCandidate()会静默失败,无报错 - 转发前检查接收方是否在线,不在线就返回
{"type":"error","reason":"user_offline","to":"userB"},前端据此提示“对方不在线”
ICE 候选为什么经常漏发或乱序
RTCPeerConnection 触发的 onicecandidate 是异步、多次、无序的,一个 Offer 可能伴随 5~20 条 candidate 消息。Gin 后端若用同步 write,或未加锁操作连接对象,极易出现部分 candidate 丢失或发到错误连接。
- 每个
RTCPeerConnection实例应绑定唯一roomID或callID,candidate 消息里必须带上该 ID,用于后端做上下文关联 - 不要在
onicecandidate回调里直接调用send(),先 push 到带缓冲的 channel(如chan *CandidateMsg),由单独 goroutine 顺序消费并转发 - candidate 消息体必须包含完整字段:
{"type":"candidate","to":"userB","candidate":"...","sdpMid":"0","sdpMLineIndex":0,"room":"abc123"},缺一不可 - 前端收到 candidate 后,必须立即调用
pc.addIceCandidate(),哪怕pc.remoteDescription还没 set —— WebRTC 允许先收 candidate 后 set remote,但顺序不能颠倒
Gin 路由和 WebSocket 升级要注意什么
gin.Engine 默认不处理 WebSocket 升级,必须显式拦截并交由 gorilla/websocket 处理,且不能走中间件(比如 JWT 验证必须在 upgrade 前完成,否则 header 已被消耗)。
- WebSocket 路径如
/ws必须注册为GET,且 handler 内第一件事是检查r.Header.Get("Upgrade") == "websocket",否则可能被反向代理(如 Nginx)截断 - JWT token 应放在 query 参数(
/ws?token=xxx)或Sec-WebSocket-Protocolheader,不要用 Cookie——部分浏览器在跨域 WebSocket 下不发送 Cookie - 升级后立即读取一次初始消息(如
{"type":"join","room":"xyz"}),验证用户身份和房间权限,失败则 close 连接,避免空连接占用资源 - 设置
upgrader.CheckOrigin = func(r *http.Request) bool { return true }仅用于开发,生产必须严格校验 Referer 或 Origin
真正难的不是把 Offer 发出去,而是确保每条 Candidate 都按序抵达、每个断连都被及时清理、每个 pc.setRemoteDescription() 前已有完整 SDP + 所有 candidate 缓存到位——这些细节没有标准库帮你兜底,全靠你在 Gin 里写清楚状态机和错误分支。











