gin仅负责转发信令消息(offer/answer/candidate),不参与sdp解析、dtls握手或ice候选交换;webrtc的p2p连接由浏览器rtcpeerconnection自主建立,gin只是“递纸条的人”。

直接用 Gin 搭 WebRTC 信令服务是可行的,但 Gin 本身不处理 ICE 候选交换、SDP 协商或 RTCPeerConnection 生命周期——它只负责把 WebSocket 消息转给前端、把前端发来的 offer/answer/ice-candidate 转发给对端。真正建立直传通道的是浏览器里的 RTCPeerConnection 和 RTCDataChannel,Gin 只是那个“递纸条的人”。
为什么 Gin 不能直接“建立”WebRTC 连接
Gin 是 HTTP 路由框架,而 WebRTC 的 P2P 连接发生在两个浏览器之间,不经过后端中转媒体或数据流。Gin 唯一能参与的环节是信令:接收 A 发来的 offer,再把它推给 B;B 回一个 answer,Gin 再转给 A;双方各自发现 ICE 候选(onicecandidate),Gin 把这些字符串逐条转发。整个过程里:
- Gin 不解析 SDP,不修改任何字段,不参与 DTLS 握手或 SRTP 加密
-
RTCDataChannel创建后,所有文件分片都走 UDP 直连,Gin 完全看不到数据内容 - 如果 NAT 穿透失败(比如双方都在对称型 NAT 后),Gin 也无法补救——得靠 TURN 服务器
用 gorilla/websocket + Gin 实现最小可行信令服务
别用 Gin 自带的 gin.Context.Upgrade 做 WebSocket,它不支持并发读写和连接管理。标准做法是用 gorilla/websocket 手动接管连接,Gin 只做路由入口:
func setupWebsocketRoutes(r *gin.Engine) {
r.GET("/ws/:room", func(c *gin.Context) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
defer conn.Close()
room := c.Param("room")
client := &Client{Conn: conn, Room: room}
clients[room] = append(clients[room], client)
// 启动读协程
go readPump(client, room)
// 启动写协程(可选,用于广播)
go writePump(client, room)
})
}
-
upgrader是websocket.Upgrader实例,需提前配置CheckOrigin - 每个
Client要绑定Room,否则无法区分谁该收哪条ice-candidate -
readPump里用conn.ReadMessage()解析 JSON,识别type: "offer"、"answer"、"candidate"字段,再按room广播给其他客户端 - 别在主线程里
conn.WriteMessage,必须用单独 goroutine + channel 防止阻塞
前端 RTCDataChannel 文件直传的关键控制点
浏览器端用 RTCDataChannel 传文件,不是把整个文件 send() 一次就完事。实际要处理分片、序号、重传、流控:
- 发送前检查
dataChannel.readyState === "open",否则会静默丢包 - 大文件必须分块(建议 16KB–64KB),每块带上
id和offset,服务端不参与拼接,全靠接收方 JS 组装 - 启用
ordered: false和maxRetransmits: 0可降低延迟,但需自己实现可靠传输逻辑(比如用Uint8Array标记已收块) - 监听
dataChannel.bufferedAmount,超过 1MB 就暂停发送,避免内存溢出或触发浏览器限速 - 关闭前调用
dataChannel.close(),否则远端可能卡在"connecting"状态
常见失败场景和对应检查项
90% 的“连不上”问题和 Gin 无关,而是信令逻辑或 WebRTC 配置错位:
- Chrome 控制台报
Failed to set remote offer sdp: Called in wrong state: stable→ 接收方没在收到offer后立刻调用setRemoteDescription(offer) - ICE 连接一直
"checking"→ STUN 服务器地址写错,或防火墙屏蔽了 UDP 5349/3478 端口;本地测试可用stun:stun.l.google.com:19302 - 文件传到一半卡住 → 没监听
dataChannel.onbufferedamountlow,或者发送方未等onopen就开始 send - 同一房间多人时消息乱串 → Gin 没按
room隔离广播,把 A 的 candidate 发给了 C
真正的难点不在 Gin 怎么写,而在如何让两个浏览器在不碰面的情况下,靠几段字符串就协商出一条 UDP 直连通道——这中间任何一步参数不匹配、时机不对、状态没同步,都会导致白屏或卡死。信令代码可以抄,但每个 setLocalDescription 调用前后该是什么状态,得自己对着 WebRTC 状态机理清楚。











