WebRTC 01:初步认识 WebRTC 与信令服务器

WebRTC 01:初步认识 WebRTC 与信令服务器

1. 是什么

  • WebRTC 协议:两端安全实时通信的一组规则集合,涵盖协商、连通、加密与媒体/数据传输。
  • WebRTC API:浏览器提供的 JavaScript 接口,驱动上述协议在终端侧运行。

核心对象与名词

  • RTCPeerConnection:端侧连接对象。
  • SDP:会话描述,文本格式,承载编解码与网络参数。
  • Offer/Answer:协商模型,一端发起 Offer,对端返回 Answer。
  • ICE:交互式连通建立,收集候选并选择最佳路径。
  • STUN/TURN:获取公网可达地址与中继转发。
  • DTLS/SRTP/SCTP:加密握手、媒体加密与数据通道承载。
  • DataChannel:可靠/不可靠、有序/无序的数据传输。

2. 为什么

  • 开放标准,多实现互通。
  • 强制加密,默认安全。
  • NAT 穿透,面向真实网络。
  • 拥塞控制与自适应,追求亚秒级延迟。

3. 四步法总览

  • 信令 Signaling:交换 SDP 与候选所需的元数据。发生在 WebRTC 之外。
  • 连接 Connecting:通过 ICE 完成候选收敛与连通性检测。
  • 加密 Securing:通过 DTLS 建立密钥,派生 SRTP/SCTP。
  • 通信 Communicating:使用 SRTP 传媒体,SCTP 传数据。

示意图:信令与建链时序(Mermaid)

sequenceDiagram
  participant A as Peer A
  participant S as Signaling Server
  participant B as Peer B
  participant T as STUN/TURN
  A->>S: join(room)
  B->>S: join(room)
  A->>S: offer (SDP)
  S-->>B: offer
  B->>S: answer (SDP)
  S-->>A: answer
  A->>S: ice-candidate
  S-->>B: ice-candidate
  B->>S: ice-candidate
  S-->>A: ice-candidate
  A->>T: STUN binding request
  T-->>A: srflx candidate
  B->>T: STUN binding request
  T-->>B: srflx candidate
  A-->>B: ICE connectivity checks
  A-->>B: DTLS handshake
  A-->>B: SRTP/SCTP secured traffic
Signaling sequence

4. 信令:不内置,需你自定义

定义

  • 信令用于引导呼叫。它不是 WebRTC 自带能力,需用自选通道传递文本消息(如 WebSocket、HTTP)。

SDP 与 Offer/Answer

  • SDP 是 RFC 8866 定义的文本格式,常见键:v/o/s/t/cm(媒体描述)、a(属性)。
  • JSEP 指定了浏览器使用的 SDP 子集与流程。
  • Offer/Answer 通过交换 SDP 在能力与参数上达成一致。

消息类型是开发者自定义

  • join/leave/offer/answer/ice-candidate/renegotiate/bye 都是应用层自定义的消息名,用于携带 SDP 或候选及会话控制。

最小消息模型

{ "type": "join", "roomId": "r1", "from": "uA" }
{ "type": "offer", "roomId": "r1", "from": "uA", "to": "uB", "sdp": "..." }
{ "type": "answer", "roomId": "r1", "from": "uB", "to": "uA", "sdp": "..." }
{ "type": "ice-candidate", "roomId": "r1", "from": "uA", "to": "uB", "candidate": { "candidate": "candidate:...", "sdpMid": "0", "sdpMLineIndex": 0 } }
{ "type": "renegotiate", "roomId": "r1", "from": "uA", "to": "uB" }
{ "type": "bye", "roomId": "r1", "from": "uA", "to": "uB" }
{ "type": "leave", "roomId": "r1", "from": "uA" }

示意图:组件关系(Mermaid)

graph TD
  A[Peer A] -- signaling --> S[Signaling]
  B[Peer B] -- signaling --> S
  A -- STUN/TURN --> T[(STUN/TURN)]
  B -- STUN/TURN --> T
  A == media/data ==> B
Signaling Components

5. 连接:ICE 的关键点

  • 候选类型:hostsrflx(STUN 反射)、relay(TURN 中继)。
  • Trickle ICE:边收集边发送,缩短首帧时间。
  • 角色:controlling/controlled,涉及最终提名。
  • 续连:ICE restart 在网络变化后重新收敛。

6. 加密与传输

  • DTLS:在已建立的通道上握手并验证证书指纹(来自 SDP)。
  • SRTP:从 DTLS 派生密钥,用于音视频加密传输。
  • SCTP over DTLS:承载 DataChannel。

7. 最小浏览器侧示例

const ws = new WebSocket("wss://example.com/signal");
const pc = new RTCPeerConnection({
  iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});
function send(m) {
  if (ws.readyState === 1) ws.send(JSON.stringify(m));
}
ws.onopen = () => send({ type: "join", roomId: "r1", from: "uA" });
ws.onmessage = async (e) => {
  const m = JSON.parse(e.data);
  if (m.type === "answer" && m.to === "uA")
    await pc.setRemoteDescription({ type: "answer", sdp: m.sdp });
  if (m.type === "ice-candidate" && m.to === "uA")
    await pc.addIceCandidate(m.candidate);
};
pc.onicecandidate = (e) => {
  if (e.candidate)
    send({
      type: "ice-candidate",
      roomId: "r1",
      from: "uA",
      to: "uB",
      candidate: e.candidate,
    });
};
(async () => {
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true,
  });
  stream.getTracks().forEach((t) => pc.addTrack(t, stream));
  const offer = await pc.createOffer();
  await pc.setLocalDescription(offer);
  send({ type: "offer", roomId: "r1", from: "uA", to: "uB", sdp: offer.sdp });
})();

8. 最小信令服务(Go + WebSocket)

package main
import (
    "encoding/json"
    "net/http"
    "sync"
    "github.com/gorilla/websocket"
)
type Message struct{ Type, RoomID, From, To, SDP string; Candidate json.RawMessage }
type Client struct{ id string; conn *websocket.Conn }
var rooms = struct{ sync.RWMutex; m map[string]map[*Client]struct{} }{ m: make(map[string]map[*Client]struct{}) }
func join(c *Client, room string){ rooms.Lock(); if rooms.m[room]==nil{ rooms.m[room]=make(map[*Client]struct{}) }; rooms.m[room][c]=struct{}{}; rooms.Unlock() }
func leave(c *Client, room string){ rooms.Lock(); if set:=rooms.m[room]; set!=nil{ delete(set,c); if len(set)==0{ delete(rooms.m,room) } }; rooms.Unlock() }
func route(room string, from *Client, raw []byte, to string){ rooms.RLock(); for cli := range rooms.m[room]{ if cli!=from && (to=="" || cli.id==to){ cli.conn.WriteMessage(websocket.TextMessage, raw) } }; rooms.RUnlock() }
var up = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true } }
func ws(w http.ResponseWriter, r *http.Request){ id := r.URL.Query().Get("uid"); c, _ := up.Upgrade(w,r,nil); cli := &Client{id:id, conn:c}; defer c.Close()
    for { _, raw, err := c.ReadMessage(); if err!=nil{ break }; var m Message; if json.Unmarshal(raw,&m)!=nil{ continue }
        switch m.Type{ case "join": join(cli,m.RoomID); case "leave": leave(cli,m.RoomID); default: route(m.RoomID, cli, raw, m.To) }
    }
}
func main(){ http.HandleFunc("/signal", ws); http.ListenAndServe(":8080", nil) }

9. 开发者操作清单

  • 选择信令通道与消息模型,明确字段与路由。
  • 配置 iceServers,优先 STUN,生产补 TURN(支持 TCP/TLS)。
  • 启用 Trickle ICE,缩短建链。
  • 使用 chrome://webrtc-internalspc.getStats() 观测端到端指标。

10. 常见排障

  • 只有 host 候选:检查 STUN 可达性与代理设置。
  • ICE failed:补 TURN,确认 3478/5349 策略,必要时 iceTransportPolicy: 'relay' 验证。
  • DTLS failed:校验证书指纹与系统时间。
  • 无声画:设备权限、轨添加与渲染绑定。

术语索引

  • SDP:RFC 8866 文本会话描述。
  • JSEP:RFC 8829,浏览器如何使用 SDP。
  • ICE:候选收集与连通性检测。
  • STUN/TURN:地址反射与中继。
  • DTLS/SRTP/SCTP:加密与承载层。

This article was updated on November 15, 2025