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

WebRTC 是浏览器里唯一能「点对点实时」的技术。 它把找路、加密、送媒体三件事打包成三个对象。

📝 本节和前面几节的关系

这一章从浏览器这一侧讲起,服务器侧的 SRS 已经在 11.5 讲过。 13.2 开始讲它为什么需要「打洞」。

一、三个 API,撑起全部功能

WebRTC 在浏览器里只暴露三个对象,所有网页直播都建在它们上面:

API 干什么 底下跑什么


getUserMedia 拿摄像头与麦克风 设备权限 RTCPeerConnection 建连接、收发媒体 ICE、DTLS、SRTP RTCDataChannel 顺便传点任意数据 SCTP over DTLS

中间那个是主体,它一个人管三件事: ICE 找路、DTLS 加密、SRTP 送媒体。 这三层是叠加的:先找到路,再在路上建加密通道,最后在通道里送音视频。

这个分层顺序不能颠倒,也不能省: 没有路就无所谓加密,没有加密浏览器根本不许发媒体。 所以你会看到连接过程总是「先 connecting、再 connected」—— 前一个状态是 ICE 在试路,后一个状态才是 DTLS 握手完成。

二、为什么非要用 UDP

最直接的一条:TCP 要先握手,UDP 不用。 而握手那一个往返,就是直播要省的第一个几十毫秒。

但更关键的是第二条,叫队头阻塞: TCP 保证有序,所以前面一个包丢了,后面收到的包也得在缓冲区里等着。 对文件下载这是优点,对直播是灾难—— 迟到的画面本来就没有价值,等它只会让整条流一起变慢。

这两条加起来,就是 WebRTC 非用 UDP 不可的原因。 代价也全在这里:UDP 不保证不丢、不保证有序、不保证到达。 所以 WebRTC 必须自己在 UDP 上面补一层—— 补序号、补时间戳、补重传(rtx)、补拥塞控制,这一层就是 SRTP 加 RTP。

换句话说,它把 TCP 做过的事重新做了一遍, 只是这次可以「按需做」——该丢的丢,该等的等,自己说了算。

三、代价:路要自己找

TCP 有个隐藏的好处:给个域名,内核帮你连到对端。 UDP 没有连接这个概念,两边都必须先知道对方「公网地址 + 端口」。

而这个地址自己往往不知道,因为它在家用路由器后面。 所以要先把能想到的地址全列出来,一个个试:

本机候选 对端候选 能不能直接用


127.0.0.1 10.0.8.7 只有本机能用 127.0.0.1 203.0.113.9 只有本机能用 127.0.0.1 198.51.100.20 只有本机能用 192.168.3.152 10.0.8.7 私网地址,跨网不通 192.168.3.152 203.0.113.9 公网地址,可以直接试 192.168.3.152 198.51.100.20 公网地址,可以直接试

本机 2 个候选、对端 3 个,配起来就是 6 对。 每一对都要真发一个包去试,谁先通就用谁。 这就是 ICE 干的事:它不聪明,就是把可能性全试一遍。 候选越多,试得越久——所以候选不是越多越好,也要控制数量。

四、它和 RTMP、HLS 的根本差别

三种协议摆在一起,差别不在技术细节,在「谁在搬数据」:

对比项 RTMP HLS WebRTC


传输层 TCP TCP UDP 路径 推流到服务器 服务器当文件发 浏览器直连 典型延迟 1 到 3 秒 5 到 30 秒 100 到 500 毫秒 服务器带宽 收一份发 N 份 收一份发 N 份 接近 0 能不能穿 NAT 能,因为是连服务器 能,同上 要打洞,打不通要中继

注意第四行:WebRTC 的服务器带宽接近 0,这不是优化得好,是架构不同。 RTMP 与 HLS 的流量必须过服务器,WebRTC 的流量能绕过服务器。

这也解释了为什么大厂直播还是用 RTMP 加 HLS 为主: WebRTC 省带宽但不可控——观众越多,越可能有人打不通洞, 那时候还是得回落中继,成本一下又上去了。 所以常见做法是「能用 WebRTC 就用,打不通自动降级」。

五、动手 Demo:WebRTC 简介——UDP 与 TCP 差在哪

各发 1000 个 200 字节的包,记建连耗时与发送耗时。 目的不是比吞吐,是看「首包之前要等多久」。

def bench(kind, n=1000, sz=200):
    """跑一轮,返回 (首包前微秒, 发送毫秒, 收到字节)。"""
    ls = socket.socket(socket.AF_INET, kind)
    ls.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    ls.bind(("127.0.0.1", 0))
    port = ls.getsockname()[1]
    got = {"b": 0}

    def srv():
        if kind == socket.SOCK_STREAM:
            ls.listen(1)
            conn, _ = ls.accept()
            while True:
                d = conn.recv(65536)
                if not d:
                    break
                got["b"] += len(d)
        else:
            while True:
                d, _a = ls.recvfrom(65536)
                got["b"] += len(d)

    threading.Thread(target=srv, daemon=True).start()
    t0 = time.perf_counter()
    if kind == socket.SOCK_STREAM:
        c = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        c.settimeout(10)
        c.connect(("127.0.0.1", port))
    else:
        c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        c.connect(("127.0.0.1", port))
    t1 = time.perf_counter()

    payload = b"x" * sz
    for _i in range(n):
        if kind == socket.SOCK_STREAM:
            c.sendall(payload)
        else:
            c.send(payload)
    t2 = time.perf_counter()
    c.close()
    return (t1 - t0) * 1e6, (t2 - t1) * 1000.0, got["b"]

_tcp_bench(n=2)                 # 先热身一次,把一次性开销挡在外面
for name, kind in (("TCP", socket.SOCK_STREAM),
                   ("UDP", socket.SOCK_DGRAM)):
    us, ms, nb = bench(kind)
    print(name, "首包前 %.0f 微秒," % us,
          "发 1000 包 %.2f 毫秒,收到 %d 字节" % (ms, nb))

实跑输出:

--------------------------------------------------------------------
WebRTC 简介——UDP 与 TCP 差在哪
--------------------------------------------------------------------
**① 三个 API 撑起全部功能**

  **WebRTC 在浏览器里只暴露三个对象,所有网页直播都建在它们上面:**

  API                   干什么               底下跑什么
  --------------------------------------------------------------
  getUserMedia          拿摄像头与麦克风     设备权限
  RTCPeerConnection     建连接、收发媒体     ICE、DTLS、SRTP
  RTCDataChannel        顺便传点任意数据     SCTP over DTLS

  **中间那个是主体,它一个人管三件事:**
  **ICE 找路、DTLS 加密、SRTP 送媒体。**
  **这三层是叠加的:先找到路,再在路上建加密通道,最后在通道里送音视频。**

**② 真测:同样一千个包,UDP 与 TCP 差多少**

  **各发 1000 个 200 字节的包,记建连耗时与发送耗时。**

  项目           TCP            UDP
  ---------------------------------------------
  首包前要等     21336 微秒     478 微秒
  发 1000 包     23.15 毫秒     141.13 毫秒
  收到字节       200000         200000

  **两者的字节数一模一样:200000 对 200000。**
  **差别不在吞吐,在「首包之前要等多久」:**
  **TCP 要先握手,UDP 直接发。**
  **环回上这点差距只有几十微秒,跨公网就是一个 RTT,几十毫秒。**

  **而这几十毫秒对直播来说正是要省的那一段。**
  **更关键的是 TCP 的「队头阻塞」:**
  **前面一个包丢了,后面的包即使收到了也得等着。**
  **对直播来说,迟到的画面没有价值,不如丢掉。**
  **这两条加起来,就是 WebRTC 非用 UDP 不可的原因。**

**③ 代价:UDP 要自己找路**

  **TCP 有个好处:给个域名,内核帮你连。**
  **UDP 没有连接概念,两边都得知道对方「公网地址加端口」。**
  **而这个地址,自己往往是不知道的**——**它在 NAT 后面。**

  本机候选          对端候选          能不能直接用
  ------------------------------------------------------------
  127.0.0.1         10.0.8.7          只有本机能用
  127.0.0.1         203.0.113.9       只有本机能用
  127.0.0.1         198.51.100.20     只有本机能用
  192.168.3.152     10.0.8.7          私网地址,跨网不通
  192.168.3.152     203.0.113.9       公网地址,可以直接试
  192.168.3.152     198.51.100.20     公网地址,可以直接试

  **本机 2 个候选、对端 3 个,配起来就是 6 对。**
  **每一对都要真发一个包去试,谁先通就用谁。**
  **这就是 ICE 干的事,它不聪明,就是把可能性全试一遍。**
  **候选越多,试得越久**——**所以候选不是越多越好,也要控制数量。**

两者的字节数一模一样:200000 对 200000,一个都没丢。 差别全在「首包之前要等多久」:TCP 要先握手,UDP 直接发。 但这台机器测出来的 TCP 建连是二十多毫秒,比环回的正常值高得多。 原因不在网络,在线程:accept 那一侧要被唤醒,中间隔着进程调度。 所以这一栏只能说明「TCP 要先等一个往返」,具体数值不能当真。 发送那一栏更反直觉:UDP 反而慢了六倍。 这也不是协议差异——TCP 有发送缓冲,一千次小写会被合并出去; UDP 每包一次系统调用,一千次就是一千次。 真机上包要走网卡,这点差距会被带宽掩盖掉。

30 秒实验

  1. 把包大小从 200 改成 8000,猜 UDP 那一栏会怎么变——每包一次的固定开销被摊薄,UDP 会明显追上来。
  2. 把 SO_SNDBUF 调大再跑 UDP,猜有没有用——没用,UDP 没有用户态缓冲,调大只是内核队列变长。
  3. 在 TCP 那一侧故意让服务端 sleep 再 accept,猜首包前会怎样——会等到 accept 完成才返回,这就是握手的真实代价。

常见误区

  1. 「UDP 比 TCP 快」——只是省了握手与等待,吞吐不一定更高。
  2. 「WebRTC 就是 UDP 版的 RTMP」——它连传输层都自己实现了。
  3. 「本机测的延迟能代表公网」——环回上没有 RTT,测不出协议差异。
自测三问(点开对答案) 1. RTCPeerConnection 一个人管哪三件事? —— **ICE 找路、DTLS 加密、SRTP 送媒体**,三层叠加。 2. TCP 对直播最致命的一点是什么? —— **队头阻塞**——前面丢一个包,后面的包收到了也得等。 3. 为什么 WebRTC 的服务器带宽接近 0? —— **媒体走浏览器之间的直连**,服务器只传信令,不搬流。

相关笔记