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 秒实验
- 把包大小从 200 改成 8000,猜 UDP 那一栏会怎么变——每包一次的固定开销被摊薄,UDP 会明显追上来。
- 把
SO_SNDBUF调大再跑 UDP,猜有没有用——没用,UDP 没有用户态缓冲,调大只是内核队列变长。 - 在 TCP 那一侧故意让服务端
sleep再 accept,猜首包前会怎样——会等到 accept 完成才返回,这就是握手的真实代价。
常见误区
- 「UDP 比 TCP 快」——只是省了握手与等待,吞吐不一定更高。
- 「WebRTC 就是 UDP 版的 RTMP」——它连传输层都自己实现了。
- 「本机测的延迟能代表公网」——环回上没有 RTT,测不出协议差异。
自测三问(点开对答案)
1. RTCPeerConnection 一个人管哪三件事? —— **ICE 找路、DTLS 加密、SRTP 送媒体**,三层叠加。 2. TCP 对直播最致命的一点是什么? —— **队头阻塞**——前面丢一个包,后面的包收到了也得等。 3. 为什么 WebRTC 的服务器带宽接近 0? —— **媒体走浏览器之间的直连**,服务器只传信令,不搬流。- ☐ (本节完,下一节 13.2.1 NAT 穿透)