🎬 第十三章 WebRTC网页直播
📄 一句话概括

信令服务器从头到尾看不见一个音视频包。 它传的是几张几百字节的纸片,业务逻辑只有「按房间转发」。

📝 本节和前面几节的关系

13.3.1 装好了 STUN 与 TURN,这一节装交换 SDP 的那个角色。 13.3.3 装发页面的 Web 服务器。

一、它不碰媒体,只传纸片

WebRTC 把两件事分得很干净: 媒体走 UDP 点对点,信令走另一条路——通常是 WebSocket。 信令服务器传的是下面这几张小纸片:

消息 方向 典型字节 作用


join 浏览器 → 服务器 42 报上自己在哪个房间 offer 浏览器 → 服务器 1462 我把我的 SDP 交上去 answer 浏览器 → 服务器 981 我把回应交上去 candidate 浏览器 → 服务器 118 我又试出一条路 bye 浏览器 → 服务器 36 我走了

五条加起来两千六百多字节,还不到 3 KB。 而一路 2 Mbps 的流跑一分钟就是 15 MB。

所以信令服务器再忙也不会是瓶颈,它的价值在「撮合」,不在「转发」。 真正要盯的指标是「同时在线多少个房间、多少条连接」,不是每秒多少字节。

二、握手就一步算术

SignalMaster 用的是 socket.io,底层就是 WebSocket。 握手只有一步算术:客户端给一个随机 key,服务器把它拼上一串固定 GUID, 算 SHA-1 再 base64,回给客户端。这个值对不上下游直接断。

RFC 6455 自己给了一组样例:key 是 dGhlIHNhbXBsZSBub25jZQ==, 应答应该是 s3pPLMBiTxaQ9kYGzzhZRbK+xOo=。 把它算一遍能对上,就说明 GUID 那一串没抄错。

两个固定长度值得记住: key 永远是 16 字节随机数编码成 24 个字符,accept 永远是 28 个字符。 如果算出来的长度不对,说明编码方式错了,不是 GUID 错了。

这个握手的好处是「无状态」:服务器不用记任何东西, 同样的 key 永远算出同样的 accept,所以负载均衡随便打。 代价是它只证明「你能算 SHA-1」,不证明你是谁——认证要在应用层另做。

三、按房间转发,不给自己

SignalMaster 的全部业务逻辑就一句话:同一个房间里,谁说话就转给其他人。 服务器按房间号分桶,桶内互转,不回声、不缓存。

「不回声」和「不缓存」这两条会带来两个可见现象: ① 你发给别人的消息,自己收不到,界面上的状态要自己维护; ② 后进房间的人,看不到前面的人说过的话。

第二条看起来像 bug,其实是设计: 信令是「当下」的交换,不是为了回放。 后进来的人如果也需要 offer,得让房间里的人重新发一份—— 这就是为什么多人会议里,新加入者会触发一串重新协商。

第三条规律是「转发会放大」: 一条消息出门按「同房间人数减一」复制。 两个人一间是 1 倍,十个人一间是 9 倍。 这是信令服务器唯一会放大的地方,也是它唯一的容量风险。

四、它的短板:房间表在内存里

SignalMaster 把房间存在内存里,进程一重启,所有人的房间关系全丢。 而且它是单进程的,两个实例之间不共享房间表,所以扩不了。

所以它只适合做实验。要上线,有两条路: ① 把房间表搬到 Redis,让多个实例共享; ② 干脆不用它,用现成媒体服务器自带的信令。

第二条路在 11.5.3 那节已经见过:SRS 把信令做进了 HTTP 接口, 浏览器一个 POST 就把 offer 换回 answer,根本不需要单独的 WebSocket 服务。 这也是为什么现在自建 WebRTC 越来越少单独部署信令服务。

但理解它仍然有价值: 知道信令要传什么、为什么不能缓存、为什么会放大, 换成任何一套现成方案,这些判断都用得上。

五、动手 Demo:搭建信令服务器 SignalMaster——它到底转发什么

真算一次 RFC 6455 的握手应答,再起一个按房间转发的服务端。 让四个客户端分别进两个房间,看谁收到什么。

GUID = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

def ws_accept(key):
    """RFC 6455 的应答值:key 接上固定 GUID,SHA-1,再 base64。"""
    return base64.b64encode(
        hashlib.sha1(key + GUID).digest()).decode("ascii")

sample = b"dGhlIHNhbXBsZSBub25jZQ=="
print("RFC 样例算出", ws_accept(sample))
print("标准期望  s3pPLMBiTxaQ9kYGzzhZRbK+xOo=")

rand = base64.b64encode(os.urandom(16))
print("随机 key", len(rand), "字符,accept",
      len(ws_accept(rand)), "字符")

# 按房间转发:谁说话就转给同房间的其他人
def dispatch(rooms, room, sender, text):
    """做一个服务端→客户端的文本帧(服务端不打掩码)。"""
    raw = text.encode("utf-8")
    n = len(raw)
    hdr = struct.pack("!BB", 0x81, n) if n < 126 \
        else struct.pack("!BBH", 0x81, 126, n)
    data = hdr + raw
    sent = 0
    for peer in rooms.get(room, []):
        if peer is sender:
            continue          # 不回声
        peer.sendall(data)
        sent += len(data)
    return sent, len(rooms.get(room, [])) - 1

rooms = {"room1": [sock_a, sock_b, sock_d], "room2": [sock_c]}
for who in ("A", "B", "D"):
    print(who, "进 room1,进完房间人数", 3)

实跑输出:

--------------------------------------------------------------------
搭建信令服务器 SignalMaster——它到底转发什么
--------------------------------------------------------------------
**① 信令服务器不碰媒体,它只传「纸片」**

  **WebRTC 把两件事分得很干净:**
  **媒体走 UDP 点对点,信令走另一条路——通常是 WebSocket。**
  **信令服务器从头到尾看不见一个音视频包,它传的是下面这几张小纸片:**

  消息          方向                典型字节     作用
  ----------------------------------------------------------------------
  join          浏览器 → 服务器     42           报上自己在哪个房间
  offer         浏览器 → 服务器     1462         我把我的 SDP 交上去
  answer        浏览器 → 服务器     981          我把回应交上去
  candidate     浏览器 → 服务器     118          我又试出一条路
  bye           浏览器 → 服务器     36           我走了

  **五条加起来 2639 字节,还不到 3 KB。**
  **而一路 2 Mbps 的流跑一分钟就是 15 MB。**
  **所以信令服务器再忙也不会是瓶颈,它的价值在「撮合」,不在「转发」。**

**② 真算一次 WebSocket 握手**

  **SignalMaster 用的是 socket.io,底层就是 WebSocket。**
  **握手只有一步算术:客户端给一个随机 key,服务器把它拼上一串固定 GUID,**
  **算 SHA-1 再 base64,回给客户端。这个值对不上下游直接断。**

  **RFC 6455 自己给的样例:**

    key     dGhlIHNhbXBsZSBub25jZQ==
    accept  s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

  **对一下标准里的期望值 `s3pPLMBiTxaQ9kYGzzhZRbK+xOo=`,一字不差。**

  **换一个真的随机 key 再看一遍:**

    key     ZTeHSm2uvBYDOoN5nmEA1Q==  (24 字符)
    accept  Xi3vdPHfNdvXEWX9XJpOPtuwlAk=  (28 字符)

  **key 永远是 16 字节随机数编码成 24 个字符,accept 永远是 28 个字符。**
  **GUID 那一串是写死的常量,抄错一个字母,所有浏览器都连不上。**

**③ 真跑一个按房间转发的信令服务**

  **SignalMaster 的全部业务逻辑就一句话:同一个房间里,谁说话就转给其他人。**
  **这里起一个真的 WebSocket 服务端,让四个客户端分别进两个房间试试。**

  **四个客户端的收发情况:**

  客户端     房间      收到几条     收到的都是什么
  --------------------------------------------------------------
  A          room1     1            answer
  B          room1     2            offer、candidate
  D          room1     3            offer、answer、candidate
  C          room2     0            一条也没有

  **A、B、D 三个人在 room1,C 一个人待在 room2。**
  **结果:C 一条都没收到,D 收到三条,A 只收到 B 的 answer。**
  **A 收得少是因为他自己发的那两条不会再回给他——服务器不回声。**
  **D 收得最多是因为他一句话没说,别人说的全落他身上。**
  **这三行摆在一起,就把「按房间分桶、桶内互转、不给自己」讲完了。**

  **收发的字节数:服务端共读进 3284 字节,转发出去 5242 字节。**
  **读进来的只算「每条消息一次」,转发出去的按「同房间人数减一」算。**
  **room1 三个人,于是一条消息出门就变两份,字节数立刻顶上去。**
  **十个人一间时,一份 offer 变九份流量。**
  **这是信令服务器唯一会「放大」的地方,也是它唯一需要担心的容量问题。**

**④ 信令与媒体,量级差多远**

  项目              字节         相当于
  ----------------------------------------------------
  整场信令          3284         一条 3 KB 的图片
  25 帧视频         10503        3.2 倍的整场信令
  1 分钟 2 Mbps     15000000     4568 倍的整场信令

  **信令只在一头一尾出现,媒体是全程奔流。**
  **所以优化信令服务器没有意义——它本来就不占带宽,只占并发连接数。**
  **SignalMaster 这类工具真正要盯的指标是「同时在线多少个房间、多少条连接」,**
  **不是「每秒多少字节」。**

**⑤ 但它有一个绕不过去的短板**

  **SignalMaster 把房间存在内存里,进程一重启,所有人的房间关系全丢。**
  **而且它是单进程的,只能水平扩不了——两个实例之间不共享房间表。**
  **所以它只适合做实验。要上线,信令层得换成带 Redis 的房间服务,**
  **或者干脆用现成的媒体服务器(SRS、ZLMediaKit)自带的信令。**
  **这一点在 11.5.3 那节已经见过:SRS 把信令做进了 HTTP 接口,**
  **浏览器一个 POST 就把 offer 换回 answer,根本不需要单独的 WebSocket 服务。**

RFC 那组样例算出来一字不差,说明 GUID 没抄错。 随机 key 是 24 个字符,accept 是 28 个字符,长度固定。 服务端共读进 3284 字节,转发出去 5242 字节——转发比读进多六成。 因为 room1 有三个人,一条消息出门就复制两份。 四个人里 A 收一条、B 收两条、D 收三条、C 一条也没有。 A 收得少是因为自己发的不回声,D 收得多是因为他一句没说。 C 一条没有,是因为他一个人在 room2,房间隔离生效了。

30 秒实验

  1. 把 GUID 最后一个字母从 1 改成 2,猜会怎样——算出来的 accept 完全不同,所有浏览器都返回 400。
  2. 让 C 也进 room1,猜转发字节怎么变——每条消息复制三份,字节数再涨五成。
  3. 让 A 先发 offer 再让 D 进房间,猜 D 能不能收到——收不到,服务器不缓存历史,必须重新协商。

常见误区

  1. 「信令服务器带宽压力大」——它只传几 KB 的纸片。
  2. 「后进的人能看到之前的 offer」——不缓存,要重发。
  3. 「SignalMaster 可以直接上生产」——房间表在内存里,扩不了。
自测三问(点开对答案) 1. WebSocket 握手用哪个哈希算应答值? —— **SHA-1**,把 key 拼上固定 GUID 后 base64。 2. 信令服务器为什么「不回声」? —— **它只按房间转发给其他人**,发送方自己收不到。 3. 十个人一间时,一条消息会变成几份流量? —— **九份**,按同房间人数减一复制。

相关笔记