第 07 篇

RTC 入门 07:两端怎样找到能通话的网络路径

从 IP、端口和 NAT 讲起,沿一通电话的建立过程理解信令、SDP、ICE、STUN 与 TURN,用连接记录和本机浏览器实验判断通话卡在哪一步。

Hex4C59 16 分钟阅读 更新于 2026-10-17

软件工程·原创·概念与设计

小林的电脑和小周的手机都已登录同一个应用。小林按下呼叫,小周能看到来电,也能按下接听。两边的界面都显示“正在连接”,却一直没有声音。

既然呼叫消息能送到,声音为什么不能沿着同一条路过去?

呼叫消息通常经过应用服务器。这个服务器知道应该把“有人找你”通知给谁,却未必承担声音转发。两端还要交换通信参数、尝试网络路径、完成安全处理,再核对媒体是否真的开始传输。业务消息送达,只完成了建立通话过程中的一部分。

前六篇都从“已有一条媒体路径”出发,讨论声音的数据、时限、恢复和排队。这一篇把起点往前移:路径还没有时,两端怎样找到它。阅读不要求先学过网络编程;正文会先解释地址与端口。读完后,你应当能够对一段新的连接记录指出:哪些步骤已经有证据,下一步还该查什么。

账号找到了人,IP 和端口才开始找接收位置 #

假设小林的账号叫 A,小周的账号叫 B。应用服务器能查到 B 当前在线,将来电通知送给 B。这个账号解决的是“找谁”。

一份网络数据还需要知道“往哪里送”。先把三个东西分清:

  • IP(Internet Protocol,互联网协议)地址标识网络中的地址位置。路由设备据此决定数据下一步往哪里转发。
  • 端口是传输协议中的编号。接收设备借助端口等信息,把数据交给相应的通信端点。
  • 传输协议规定传输层怎样处理数据。这里先讨论 UDP(User Datagram Protocol,用户数据报协议):它把应用交付的数据作为数据报发送,不保证数据必达或按序到达。另一个常见协议 TCP(Transmission Control Protocol,传输控制协议)则提供可靠、有序的字节流。

例如 UDP 192.168.1.10:50000 表示使用 UDP,IP 是 192.168.1.10,端口是 50000。知道 IP 却不知道接收端口,还不足以给本例中的接收端发送数据。TCP 的同号端口也不能直接当成这个 UDP 端点。

小林和小周各在自己家里,两人的设备甚至可能都叫 192.168.1.10。这类私有地址可在不同内网中重复使用,不是可以让互联网用户按同一个地址直接找到的全球唯一位置。把自己的内网地址复制给对方,不能替代路径检查。

下面出现的 192.0.2.x、198.51.100.x、203.0.113.x 都是文档示例地址,端口也是教学设定;它们不是可以试连的公网服务。1

先商量这次怎样通信:信令、SDP 和 Offer/Answer #

双方除了找到地址,还要知道彼此准备传什么。

小林想发送一路音频,小周是否接收?双方有没有兼容的音频编码?这次连接要使用哪些传输和安全参数?这些信息都需要在发送声音前交给对方。

信令就是为建立、修改或结束会话而交换的控制信息。WebRTC 没有替应用规定统一的信令服务器和消息传输方式;应用可以通过自己的 HTTP、WebSocket 等机制交换信息。复制粘贴会话文本也在做信令,只是由人搬运消息。2

WebRTC 常用 SDP(Session Description Protocol,会话描述协议) 承载会话说明。它是一种文本格式,里面描述媒体、格式和传输等参数。SDP 不包含等着播放的一段音频。下面是读它时需要认识的内容,不是一份可以直接导入的完整 SDP:3

内容此时需要回答的问题
音频媒体段、方向这次有没有音频?本端准备发送、接收,还是双向通信?
编码格式及参数双方准备用哪些兼容格式处理声音?
ICE 参数双方用哪组会话凭据,核对收到的检查请求是否属于这轮连接?
证书指纹用证书内容计算出的短标识,核对后续安全握手提供的证书是否与描述一致
候选信息有哪些可以尝试的传输地址?候选也可能通过后续消息单独交付

Offer/Answer(提议/应答) 组织这次协商:一端提出描述,另一端根据它给出自己的应答。Answer 需要遵守协商规则,不能把不兼容的任意配置当成“我同意了”。4

以 A 发起为例,先忽略候选到达的具体时间:

  1. A 生成 Offer,并把它设置为自己的本地描述。
  2. 应用将 Offer 交给 B;B 将它设置为远端描述。
  3. B 生成 Answer,设置为自己的本地描述,再通过应用交给 A。
  4. A 将收到的 Answer 设置为远端描述。

“本地”与“远端”是相对于当前这一端说的。同一份 A 的 Offer,在 A 是本地描述,在 B 是远端描述。浏览器需要知道两边的描述,才能按协商结果继续处理。

这几个步骤完成,说明描述已经按规则交给了两端。它还没有回答“描述里的哪条网络路径真正可达”。

路由器改了地址,外部看到的会是什么 #

先沿一份 UDP 数据走出家门。

小林的电脑从 192.168.1.10:50000 向外发送数据。家庭路由器可能把这份数据的源地址和端口改成外侧的 198.51.100.10:62000,并保存一份转换关系。外部响应到达这个外侧地址时,路由器才能按相应状态把数据交回内网。

这种地址转换叫 NAT(Network Address Translation,网络地址转换)。本例只画一层;真实路径可能存在多层转换,也可能使用不需要这类转换的 IPv6 路径。

STUN(Session Traversal Utilities for NAT) 提供一组消息机制。这里先使用其中的 Binding 请求/响应:A 向 STUN 服务器发请求,服务器在响应中告诉 A,它看到的源地址是什么。A 由此知道了“从这个服务器看过来”的映射地址。5

观察位置本例看到的地址
A 自己使用的本地端点192.168.1.10:50000
STUN 服务器看到的来源198.51.100.10:62000
A 发请求的 STUN 服务入口192.0.2.10:3478

第三行是问路的服务地址;第二行才是本次观察得到的映射地址。不要把 STUN 服务器本身的地址填成 A 的媒体接收地址。

查到了映射,为什么仍可能收不到对方的数据 #

这里有两个容易混在一起的问题。

映射行为决定:A 向不同目的地发送时,外侧地址是否保持相同。某条实际路径可能让 A 发往 STUN 服务器时显示为 198.51.100.10:62000,发往 B 时却使用 198.51.100.10:62004。前一次查询不能保证后一条路径沿用原端口。

过滤行为决定:哪些外部来源的数据可以被放回来。即便映射保持不变,路由器或防火墙也可能允许 STUN 服务器的响应,却没有放行 B 发来的数据。6

因此,“收到了 STUN 响应”提供的是一次地址发现结果。它没有证明另一个用户能通过这个地址把数据发进来。只凭这一次结果,也不能完整判断整条网络路径的 NAT 行为。

ICE 把可尝试地址变成一条经过检查的路径 #

A 可能有本地地址、外侧映射地址,还可能申请到中继地址。B 也有自己的选择。接下来要解决的事情是:拿哪些地址互相试,怎样确认结果,最终使用哪条路。

ICE(Interactive Connectivity Establishment,交互式连接建立) 负责收集、交换候选,进行连通性检查,并通过提名等过程确定要使用的候选对。它不是另一台专门收发所有声音的服务器。7

候选是一项尝试机会 #

候选(candidate) 是可供尝试的传输地址及其相关属性。先认识三种来源:

候选类型怎样得到只看它存在,能否证明已经连通
host,本地候选来自本地网络接口对应的传输地址不能。另一端可能不在可达的网络中
srflx,服务器反射候选通过服务器观察获知外侧映射地址不能。还要检查对端路径
relay,中继候选向 TURN 服务申请分配,得到中继地址不能。中继分配和后续路径也要能使用

浏览器有时会用 .local 结尾的 mDNS(Multicast DNS,组播域名系统)名称隐藏本地数字 IP 的直接展示。候选里没有出现熟悉的 192.168.x.x,不等于没有收集到本地候选。更完整的 ICE 中还有检查过程中发现的 prflx,即对端反射候选;基础实验先读前三种。8

候选对需要真正检查 #

候选对(candidate pair) 把本端候选和远端候选放在一起,代表一条可检查的组合。协议、地址族等条件也要匹配,并不是把所有行不加区分地相乘就能使用。

ICE 使用带本轮凭据的 STUN 消息进行连通性检查。这时检查消息发向候选对所指向的对端路径,用来检查对方是否能按规则响应。它与前面“向 STUN 服务器询问映射地址”使用相关的消息机制,但目的不同。9

下面是一份简化的教学记录,没有展示重试计时、全部候选和内部状态字段:

已检查的候选对本次结果可以据此说什么
A 的 host ↔ B 的 host检查未成功这次尝试没有建立这条路径
A 的 srflx ↔ B 的 srflx检查超时有映射地址仍未使这次检查成功;仅凭超时不能断言原因
A 的 relay ↔ B 的 srflx检查成功,随后被提名并选中这次连接选用包含 A 中继的路径

检查成功说明候选对通过了相应检查。可能存在多个检查成功的组合。ICE 还需要按协议标明准备使用哪一组,这一步叫提名;被提名并选中才说明它进入本次实际使用的选择。ICE 会按角色、优先级和协议过程处理它们,不能把“最早成功”“看起来 RTT 最低”直接当成所有实现的固定选路规则。10

上表为了让结果易读而逐行排列。实际收集和检查可能交错进行,TURN 候选也可以在建立连接早期就准备好,不需要先等所有直连尝试宣告失败。

TURN 参与的是数据转发 #

如果找不到可用直连路径,可以通过中继尝试通信。

TURN(Traversal Using Relays around NAT) 让客户端申请一个中继地址,由服务器按分配及权限等状态转发数据。本例中 A 主动联系服务入口 192.0.2.30:3478,申请得到 192.0.2.30:55000。后一个地址属于这次中继分配,不是所有客户端共用的固定媒体端口。11

若前面那组 relay ↔ srflx 被选中,A 发出的数据沿“ A → TURN → B ”走;返回数据沿“ B → TURN → A ”走。双方不必都使用 relay 候选才算经过 TURN:候选对中一端的中继已经改变了这条路径。

对照信令与实际数据路径:直连和 TURN 中继 · 在独立窗口中查看

切换两种路径,观察中间的数据通路。上方的信令服务仍交换会话信息;切到 TURN 后,下方增加了实际承接数据的中继。STUN 地址发现没有画进持续数据通路,因为做过一次地址发现,不代表声音要不断经过那个 STUN 服务器。

使用 TURN 会消耗服务器转发带宽,也要求服务可达、凭据有效、分配及相关权限状态得到维护。它不能保证所有受限网络都能通。TURN 转发也不等于解码音频、混音或订阅多路媒体;处理这些任务的媒体服务器会在后续多人通信中讨论。

路通了,还要看安全处理和声音本身 #

选定路径之后,WebRTC 还需要完成安全处理。在媒体场景中,DTLS(Datagram Transport Layer Security,数据报传输层安全) 参与安全握手,并通过 DTLS-SRTP 机制导出媒体保护所需的密钥;SRTP(Secure Real-time Transport Protocol,安全实时传输协议) 保护 RTP 媒体的机密性和完整性等:加密媒体内容,并检查数据是否遭到改动。ICE 找到可用路径,不能代替这些处理。12

SDP 中交换的证书指纹与后续握手验证有关。应用仍要保护信令、确认通信对象和房间权限;不能仅凭某次 DTLS 握手成功,就宣称登录身份和全部业务授权都已验证。TURN 本身只转发数据,也不会因为位于路径上就自动获得解密 SRTP 的密钥。13

即使路径和握手都已成功,麦克风也可能没采到数据,发送端可能没有加入音频轨道,接收端可能没有开始播放。浏览器中的 MediaStreamTrack(媒体轨道) 表示一路媒体源或远端媒体对象;收到远端轨道事件,也不能单独证明声音已经播放。

排查时,证据要跟着处理位置往后走:

已有证据下一步检查
来电和接听消息成功双方是否应用了对应的描述,候选是否交付
候选收集结束连通性检查是否成功,实际选中了哪组候选
ICE 已连接安全握手是否成功,整体连接状态是否正常
连接已建立是否存在有效发送轨道,音频 RTP 的发送/接收计数是否增长
接收音频数据正在增长后续解码、输出设备、静音和播放策略是否允许声音出来

接收字节增长时要说明自己看的是什么计数。连接总字节里可能有检查和控制流量;音频 RTP 的接收增长比“网卡有流量”更接近问题,但也仍需核对实际输出。14

做一次本机实验:让两个连接传一条消息 #

先用一个不需要麦克风的小实验观察建立连接。打开配套页面,按“建立连接并发消息”:

打开本机 ICE 观察实验 · 保存实验 HTML

页面会在同一浏览器页面中创建 A、B 两个 RTCPeerConnection 对象。这个浏览器对象负责管理一次 WebRTC 连接。页面自动搬运 Offer 和 Answer,页面里的代码在承担信令交换,没有部署额外信令服务器。

实验使用 RTCDataChannel(WebRTC 数据通道) 发送文本 rtc-hello,收到后回传 ack:rtc-hello。它用于确认应用数据走通,本例没有音频轨道。数据通道用 SCTP(Stream Control Transmission Protocol,流控制传输协议)承载数据,外层由 DTLS 保护;前面媒体使用的 SRTP 不能直接套到这条文本消息上。15

运行条件与范围 #

实验不配置外部 STUN 或 TURN,不访问麦克风、摄像头,不把描述和候选写入浏览器存储,也不显示完整候选地址。浏览器会执行本机 ICE 网络检查;这不等于“完全没有网络活动”。页面没有外部脚本、统计上报和 CDN 依赖。

可以直接在博客上打开。也可以把 HTML 下载到一个空目录,在该目录运行:

python3 -m http.server 8000 --bind 127.0.0.1

再用现代桌面浏览器打开 http://127.0.0.1:8000/ice-lab.html。命令需要 Python 3,作用只是提供本地静态页面,结束后按 Ctrl+C 关闭。不要把整个个人目录作为服务目录。若系统策略禁止 WebRTC 或本地检查,页面会给出错误;这个失败不等于所有公网 WebRTC 路径都失败。

页面做了哪些操作 #

为了让实验顺序容易核对,页面采用等待本端候选收集结束,再交付完整描述的办法:

  1. 创建 A、B;A 创建数据通道,使协商中包含需要的数据传输。
  2. A 创建并设置 Offer,等待 A 本轮候选收集结束。
  3. 把 A 当前的本地描述交给 B,B 应用它。
  4. B 创建并设置 Answer,等待 B 本轮收集结束,再交给 A。
  5. 观察连接状态;数据通道打开后,A 发出文本,B 回传确认。
  6. 读取统计中的已选候选对,显示两端候选类型。完成观察后,按“关闭并清理”。

实际应用常用 Trickle ICE(增量交换 ICE 候选):候选一有结果就陆续交付,减少等待全部收集完成的时间。它改变候选交付时机,并没有省掉信令。候选还必须送到正确连接、正确的一轮协商中。16

怎样读结果 #

先看事件顺序,再看连接快照。成功记录应包含 A/B 应用描述、通道打开、B 收到 rtc-hello、A 收到 ack:rtc-hello。最后一项能确认本次测试消息及其应用回传已经通过。

如果统计接口提供 transport.selectedCandidatePairId,页面会沿这个 ID 找到本端和远端候选,再显示 host 等类型。浏览器若没有提供对应字段,页面就显示“未提供”,不会从若干成功候选对中猜一个选中结果。

同机实验中观察到本地候选连接,只覆盖当前浏览器和本机环境。它不能验证两户家庭、企业网络或移动网络的公网直连,更不能验证 TURN,因为这里根本没有配置 TURN。不同浏览器可能暴露不同候选和统计字段,不要求输出地址、端口或候选数量完全一致。

2026 年 10 月 15 日,在 macOS 的 Codex 内置浏览器中运行成功,观察到消息与确认回传、host ↔ host 的已选候选对;关闭后连接对象已清理。这个结果说明本例数据通道可用,尚不能证明麦克风采集、音频编解码或扬声器播放成功。下一篇才把真正的媒体轨道接进双端实验。

换一段记录,自己判断 #

下面是另一组教学记录。先写下理由,再展开答案。

练习一:候选收集结束后,还缺什么 #

A:Offer 已交给 B
B:Answer 已交给 A
A、B:iceGatheringState = complete
A:收集到 2 个 host、1 个 srflx
B:收集到 1 个 host、1 个 srflx
当前:没有任何候选对检查成功的记录

可以把界面改成“通话已建立”吗?能否据此说 A 的 NAT 一定属于某一种类型?

展开答案与判断依据

两项都不能。iceGatheringState = complete 表示当前收集阶段结束,候选类型和数量说明有这些可尝试项,没有提供候选对成功、选中、安全握手及媒体收发的证据。

srflx 的存在也没有完整描述映射与过滤行为。应继续查看候选是否正确交付、本轮凭据是否匹配、检查结果和实际网络路径。若把“收集结束”当成“连接结束”,回看“候选对需要真正检查”。

练习二:只有一端是 relay,数据经过哪里 #

A 的已选本地候选:relay,192.0.2.30:55000
A 的已选远端候选:srflx,203.0.113.20:41000
对应候选对:已检查成功并选中
安全处理:本例媒体握手成功

A 发向 B 的媒体会经过 TURN 吗?B 返回 A 的媒体呢?这能证明 TURN 已解码声音吗?

展开答案与路径

两个方向都经过 A 的中继。逻辑路径分别是 A → TURN → B 和 B → TURN → A。B 返回时发送到 A 的中继地址,TURN 再交付给 A;不要求 B 自己也申请中继。

这不能证明 TURN 解码了声音。普通 TURN 转发与媒体解码是不同任务,媒体保护的密钥也不会仅因转发而交给 TURN。若把 relay 当成“双方都必须用的模式”,回看图中的中继路径。

练习三:消息传通了,为什么仍不能证明能听见声音 #

ICE:已连接
DataChannel:open
B 收到:rtc-hello
A 收到:ack:rtc-hello
音频轨道:0 条

这些记录证明了什么?若要验证语音,还需补哪些证据?

展开答案与检查顺序

它们证明这次数据通道成功传输了测试消息,并完成应用回传。没有音频轨道,当然不能据此确认声音。

语音实验需要加入有效音频轨道,检查音频发送和接收计数、后续解码与实际播放,并记录设备和权限状态。即便先收到了远端轨道对象,也要继续验证内容是否传来、是否输出。若直接把 open 当成“能听见”,回看“路通了,还要看安全处理和声音本身”。

建立连接之后,前面学过的时间约束才有了承载它们的实际路径。接下来的双端实验会沿同一条顺序核对:交换描述、选中路径、安全处理、媒体收发,最后确认实际输出。

回看第六篇:怎样从收发记录看出延迟在增长 · 系列目录

参考资料 #

下列 W3C 规范于 2026 年 10 月 15 日核对;实验展示字段以运行时浏览器实际提供的结果为准。


  1. RFC 1918:私有地址;RFC 5737:文档示例 IPv4 地址。 ↩︎

  2. W3C WebRTC:Introduction 与 RTCPeerConnection 接口,包括描述、候选、状态与轨道处理。 ↩︎

  3. RFC 8866 §5:SDP 描述格式;RFC 8839:SDP 中的 ICE 使用。 ↩︎

  4. RFC 3264 §5—6:生成 Offer 与 Answer。 ↩︎

  5. RFC 8489 §7、§14.2:Binding 与映射地址,2020 年 2 月发布;其摘要也明确说明 STUN 本身不是完整的 NAT 穿越方案。 ↩︎

  6. RFC 4787 §4.1、§5:NAT 映射与过滤,2007 年 1 月发布。它给出行为分类和要求,不能据此假设所有实际设备都符合要求。 ↩︎

  7. RFC 8445 §2、§5—8:ICE 概览、收集、检查与结束处理,2018 年 7 月发布。 ↩︎

  8. RFC 8828:WebRTC IP 地址处理;W3C WebRTC:隐私与本地网络地址,说明候选暴露与隐私边界。mDNS 方案的背景见 Using Multicast DNS to protect privacy when exposing ICE candidates(2021 年 12 月 6 日草案,已过期,非 RFC)。prflx 的发现见 RFC 8445 §7。 ↩︎

  9. RFC 8445 §7:ICE 连通性检查。 ↩︎

  10. RFC 8445 §8.1.1:候选对提名。 ↩︎

  11. RFC 8656 §3:TURN 操作概览,2020 年 2 月发布,区分服务端地址、分配得到的中继地址与相关状态。 ↩︎

  12. RFC 5764 §4:DTLS-SRTP;RFC 3711 §3:SRTP。 ↩︎

  13. RFC 8827 §6:WebRTC 安全架构;RFC 8656 §21:TURN 安全考虑。 ↩︎

  14. W3C WebRTC Statistics:transport、候选对 与 入站 RTP。实验只按统计对象之间的 ID 引用读取已选路径。 ↩︎

  15. RFC 8831 §5:WebRTC 数据通道传输,2021 年 1 月发布。 ↩︎

  16. RFC 8838 §1、§9:Trickle ICE 与增量交付,2021 年 1 月发布。 ↩︎

一起讨论

选中正文中的文字,可以针对这段内容发起讨论。

正在读取讨论…

在 GitHub 查看与管理讨论 ↗

文章讨论