Webrtc信令交互

一、STUN和TURN

STUN(Session Traversal Utilities for NAT)
  • 做什么:客户端去问 STUN 服务器:"从外面看,我的公网 IP 和端口是什么?"服务器把 反射地址(reflexive candidate) 告诉你。
  • 结果:双方各自知道自己在公网上的"洞",很多情况下可以直接 P2P 收发媒体/数据,不经过第三方转发。
  • 特点:流量仍是 端到端,STUN 只参与 发现地址 和少量保活类报文;带宽成本主要在用户之间,STUN 服务器负载相对小。
TURN(Traversal Using Relays around NAT)
  • 做什么:当打洞失败(对称 NAT、严格防火墙、双方无法直连等)时,客户端和 TURN 服务器建立中继,媒体/数据经过 TURN 转发。
  • 结果:连通性最好,但不再算纯 P2P,服务器要扛双向流量。
  • 特点:要账号/密钥、占服务器带宽与机器资源,运维和成本都比 STUN 高;一般作为 最后手段和STUN一起配。
  • STUN:尽量让你知道公网地址,争取直连。
  • TURN:直连不行时,走中继保证能通。

实际产品里常见写法是:同一个 URL或不同条目里同时配公共STUN(如 stun:...)和 自建/租用的 TURN(turn:...,带用户名密码),由 ICE 自动选最优路径。


STUN 和 TURN 是两种用于解决 NAT(网络地址转换)穿透问题的关键协议,简单来说:STUN负责"指路",帮设备找到自己在公网上的"门牌号";TURN负责"修路",当直连不通时,充当一个"信使"来中转所有数据。

在实际应用中,STUN和TURN并非互斥,而是通过一个叫做 ICE(交互式连接建立) 的机制智能地协同工作。它的策略可以理解为"先试最优的,不行再降级":

  1. 尝试最优路径(P2P直连) :ICE会先收集所有可能的连接方式,其中最理想的是主机自己的内网地址,其次是STUN服务器发现的公网地址。然后,双方会尝试通过这些地址直接建立连接。

  2. 触发保底方案(TURN中继) :如果上述所有直接连接的尝试都失败了(例如,任何一方处于严格的对称型NAT之后),ICE就会自动切换到保底方案------使用TURN服务器进行中继

之所以这样设计,是为了在"连接质量"和"连接成功率"之间取得最佳平衡。

  • STUN成功时 :你的视频通话会走点对点直连,享受到最低的延迟和最高的传输效率。

  • TURN介入时 :虽然成本更高、延迟也略有增加,但它确保了连接能够100%建立成功,避免了"无法通话"的尴尬。


二、对端和本端通信流程

  1. 信令通道先建好

设备 ws->open("ws://.../server")。

onOpen:信令 WebSocket 已连上,后面 JSON 都走这条连接

  1. 对端发 request("我要和你建会话")

对端 → 设备(JSON):

{ "type": "request", "id": "<peer_id>" }

复制代码
//转化为XML格式
<?xml version="1.0" encoding="UTF-8"?>
<signaling>
  <id>MBF672lwa7</id>
  <type>request</type>
</signaling>

因为没有 SDP,sdp_chars=0

设备侧做了什么:

为该 id 建 PeerConnection,按需加 H264 视频轨、本端 DataChannel,onDataChannel;注册 onGatheringStateChange / onLocalCandidate / onStateChange;然后 setLocalDescription(),开始收集 ICE 并生成 本地 SDP(offer)。

  1. 设备发 candidate(本端 ICE)

设备 → 对端(可能多条):

{ "type": "candidate", "id": "<peer_id>", "candidate": "candidate:...", "mid": "..." }

复制代码
//转化为XML格式
<?xml version="1.0" encoding="UTF-8"?>
<signaling>
  <id>MBF672lwa7</id>
  <type>candidate</type>
  <candidate><![CDATA[candidate:1 1 UDP 2114977791 192.168.6.161 46924 typ host]]></candidate>
  <mid>cam-video</mid>
</signaling>

TX type=candidate(no sdp field) 是因为这条消息里没有 **sdp** 字段,只有 **candidate`。

  1. 设备发 offer(本端 SDP)

设备 → 对端(ICE gathering complete 后):

{ "type": "offer", "id": "<peer_id>", "sdp": "v=0\r\no=..." }

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<signaling>
  <id>MBF672lwa7</id>
  <type>offer</type>
  <sdp><![CDATA[v=0
o=rtc 3908217902 0 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE cam-video 0
a=group:LS cam-video
a=msid-semantic:WMS *
a=ice-options:ice2,trickle
a=fingerprint:sha-256 EE:10:86:14:35:16:EF:54:C4:BE:E1:62:16:CA:AC:88:09:36:A4:DA:12:7E:5C:E5:E1:97:F2:F1:5C:62:DE:A8
m=video 46924 UDP/TLS/RTP/SAVPF 102
c=IN IP4 192.168.6.161
a=mid:cam-video
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=sendonly
a=ssrc:1 cname:cam-video
a=ssrc:1 msid:stream0 cam-video
a=msid:stream0 cam-video
a=rtcp-mux
a=rtpmap:102 H264/90000
a=rtcp-fb:102 nack
a=rtcp-fb:102 nack pli
a=rtcp-fb:102 goog-remb
a=fmtp:102 profile-level-id=42e01f;packetization-mode=1;level-asymmetry-allowed=1
a=setup:actpass
a=ice-ufrag:15GH
a=ice-pwd:VED2rtk1uih9nzxYabBCKr
a=candidate:1 1 UDP 2114977791 192.168.6.161 46924 typ host
a=candidate:2 1 UDP 1678769919 183.238.154.133 6313 typ srflx raddr 0.0.0.0 rport 0
a=end-of-candidates
m=application 46924 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 192.168.6.161
a=mid:0
a=sendrecv
a=sctp-port:5000
a=max-message-size:262144
a=setup:actpass
a=ice-ufrag:15GH
a=ice-pwd:VED2rtk1uih9nzxYabBCKr
]]></sdp>
</signaling>

此时 PC state 多为 Connecting(ICE/DTLS 进行中)。

  1. 对端发 answer

对端 → 设备:

{ "type": "answer", "id": "<peer_id>", "sdp": "..." }

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<signaling>
  <id>MBF672lwa7</id>
  <type>answer</type>
  <sdp><![CDATA[v=0
o=- 454412499414640947 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE cam-video 0
a=msid-semantic: WMS
m=video 9 UDP/TLS/RTP/SAVPF 102
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=candidate:3758453149 1 udp 2113937151 e0448cdf-8171-455e-8757-f546c14dd4e1.local 63464 typ host generation 0 network-cost 999
a=ice-ufrag:iWXL
a=ice-pwd:bz+8Mw5lticb2quOdoAbg4yu
a=ice-options:trickle
a=fingerprint:sha-256 EB:ED:D9:F3:3E:86:3E:E7:16:F1:2D:F9:66:1E:61:03:27:49:74:28:EE:62:73:5A:70:54:EA:F8:22:FB:2D:56
a=setup:active
a=mid:cam-video
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=recvonly
a=rtcp-mux
a=rtpmap:102 H264/90000
a=rtcp-fb:102 goog-remb
a=rtcp-fb:102 nack
a=rtcp-fb:102 nack pli
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 0.0.0.0
a=ice-ufrag:iWXL
a=ice-pwd:bz+8Mw5lticb2quOdoAbg4yu
a=ice-options:trickle
a=fingerprint:sha-256 EB:ED:D9:F3:3E:86:3E:E7:16:F1:2D:F9:66:1E:61:03:27:49:74:28:EE:62:73:5A:70:54:EA:F8:22:FB:2D:56
a=setup:active
a=mid:0
a=sctp-port:5000
a=max-message-size:262144
]]></sdp>
</signaling>

设备侧:setRemoteDescription(answer);必要时 从 SDP 同步 RTP MID(日志里的 RTP MID header ext_id=4)。

复制代码
[17:14:55.049] [webrtc_ldc] answer sdp first line: v=0
[17:14:55.054] [webrtc_ldc] RTP MID header ext_id=4 mid=cam-video uri=urn:ietf:params:rtp-hdrext:sdes:mid
[17:14:55.068] [webrtc_ldc] setRemoteDescription(answer)

之后 ICE/DTLS 继续,直到 Connected。

  1. 对端发 candidate(可选、常与 answer 交错)

对端 → 设备:

{ "type": "candidate", "candidate": "...", "mid": "..." }

设备侧:addRemoteCandidate。

这段日志里,answer 之后没有再打 RX candidate,说明对端候选可能都编在 SDP 里,或 ICE 已够用。)

  1. 媒体与 DataChannel 就绪

video track open:Track 已可用,设备开始 sendFrame(并 IDR)。

PC state=2:一般为 已连接。

DataChannel open:本端创建的 app 等通道可用。

复制代码
对端(浏览器)                 信令服务器(WS)                      设备
    |                              |                              |
    |  WS 已连接(本端先连上)       |<======= WS open ============ |
    |                              |                              |
    |-------- JSON: request(id) --------------------------------->|
    |                              |         建 PeerConnection
    |                              |         addTrack(H264) / createDC("app")
    |                              |         setLocalDescription()
    |                              |                              |
    |                              |                              |
    |<------- JSON: candidate(id,mid=cam-video) ------------------|
    |<------- JSON: candidate(id,mid=cam-video) ------------------|
    |        (host → srflx)        |         onLocalCandidate
    |                              |                              |
    |<------- JSON: offer(id,sdp) --------------------------------|
    |                              |   gathering Complete → 发 SDP
    |                              |                              |
    |-------- JSON: answer(id,sdp) ------------------------------>|
    |                              |   setRemoteDescription(answer)
    |                              |   RTP MID 同步 / track open
    |                              |   PC state=2 / DC open(local,app)
    |                              |                              |
    |<======== DTLS-SRTP(视频) / SCTP(DataChannel),不经信令 WS ========>|
    |                              |         (ICE 选路;与 WS 无关)

一句话

对端用 request 触发;

设备用多条 candidate + 一条 offer 回应;

对端用 answer(及可选 candidate)收尾;之后在 UDP 上做 ICE,媒体走 SRTP,信令仍走原 WebSocket。

相关推荐
2301_780789661 小时前
多层级 CC 防护体系:前端验证与后端限流的协同配置实践
运维·服务器·前端·网络安全·智能路由器·状态模式
林瞅瞅1 小时前
Jenkins+Docker实现Nuxt2自动化部署
服务器·ci/cd
jsons11 小时前
linux 用户内存保障管理配置
linux·运维·服务器
北京智和信通2 小时前
智和信通助力某信息工程大学实现校园全域运维监控
运维·服务器·网络监控·网络管理软件·网管软件·网管运维·网络管理系统
Fanfanaas2 小时前
Linux 系统编程 文件篇 (一)
linux·运维·服务器·c++·学习
七牛云行业应用2 小时前
MCP 服务器本地部署实战【2026】:Python/Node.js 搭建 + Claude/Cursor/TRAE
服务器·python·node.js
j_xxx404_2 小时前
Linux信号机制:从键盘到内核、进阶实战硬核剖析
linux·运维·服务器·c++·人工智能·ai
Mr. zhihao2 小时前
从 `cat file.txt` 到屏幕:一次 Linux 文件读取的完整旅程
linux·运维·服务器
码完就睡2 小时前
Linux——进程间通信
linux·运维·服务器