WebRTC 老是连不通?Cloudflare TURN 生产级落地完整指南
【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills
WebRTC 通话里,只要两端藏在 NAT 或公司防火墙后面,直连就容易被掐断。Cloudflare TURN 在全球 anycast 网络(覆盖 310+ 城市,不含中国网络)上提供中继站点:直连不通时把流量倒进中继站,通话就不会挂。这篇文章带你四步接上 TURN,再讲清 TURN 端口选择、TURN 凭证刷新与断线自动恢复。
直连失败的三个典型现场
NAT 挡路。双方都在内网,ICE 打洞方向被挡,候选收不齐,通话迟迟建不起来。这是最常见的"卡在建连"。
企业防火墙封端口。办公网通常只放行 HTTPS 出网,UDP 3478 基本全灭,浏览器能用的只剩 443 上的 TLS 中继。
移动网络切换 + 凭证到期。Wi-Fi 切 4G,或者 TURN 凭证过了 TTL,连接直接掉进failed,而且不会自己回来。
这三个现场缺的是同一套东西:一条可靠的中继路线,加一条会自愈的恢复链路。下面把两块短板一次补齐。
30 秒看懂:TURN 到底在干什么
把 NAT 想象成一面看不见的墙。STUN 是墙上的"问路窗口"——你敲一下,它告诉你自己的公网地址。ICE 则是"多路线并试探",哪条通走哪条。而 TURN 是公共中转站:双方各自把数据交给它,它负责递到对方手里。
| STUN | TURN | |
|---|---|---|
| 干什么 | 让你知道自己的公网地址 | 替你转发流量 |
| 何时生效 | 直连能通、NAT 不算凶 | 直连不通、必须借道 |
| 你的配置里长什么样 | stun:stun.cloudflare.com:3478 | turn:/turns:多端口地址 |
两个一起交给RTCPeerConnection就行,ICE 协商会自动择优,不用你手动调度。
🔌 四步接上 TURN
① 创建 TURN Key:一个请求的事
用带Calls Write权限的 API Token,打https://api.cloudflare.com/client/v4:
POST /accounts/{account_id}/calls/turn_keys,body 传{"name": "my-turn-key"}。
响应里有uid、key、name、created、modified。⚠️ 注意key(真正的密钥)只在创建这一次返回,必须当场存好。后续还能用同一路径做列表(GET)、查详情(GET)、改名字(PUT)、删除(DELETE),详见 references/turn/api.md。
② 一个签发凭证的 Worker
浏览器永远不该碰密钥,所以要有一个后端 Worker 代跑凭证生成:
POST https://rtc.live.cloudflare.com/v1/turn/keys/{key_id}/credentials/generate,请求头带Authorization: Bearer {key_secret},body 传{"ttl": 3600}。
Worker 的环境变量这样分工:非敏感的TURN_KEY_ID放进 wrangler.jsonc 的vars,TURN_KEY_SECRET用wrangler secret put TURN_KEY_SECRET单独注入;生产环境再绑一个CREDENTIALS_CACHEKV 命名空间做凭证缓存。响应里的username(形如1738035200:user123)和 Base64 编码的credential就是浏览器要用的那对"门票"。
③ 浏览器客户端:组装 iceServers
这段代码解决"客户端拿票、拼服务器列表"的问题:
const ticket = await (await fetch('/api/relay-ticket')).json(); const iceServers: RTCIceServer[] = [ { urls: 'stun:stun.cloudflare.com:3478' }, { urls: [ 'turn:turn.cloudflare.com:3478?transport=udp', 'turn:turn.cloudflare.com:3478?transport=tcp', 'turns:turn.cloudflare.com:5349?transport=tcp', 'turns:turn.cloudflare.com:443?transport=tcp' ], username: ticket.name, credential: ticket.pass, credentialType: 'password' } ];把iceServers传进new RTCPeerConnection({ iceServers })即完成接入:STUN 负责问路,四条 TURN 地址按优先级待命。
④ 验证中继生效:看选中的候选对
用三个观测点确认流量真的走了中继:icecandidate事件里看候选的type(host/srflx/relay)与protocol,确认出现过 relay 候选;iceconnectionstatechange应走完checking → connected → completed;最后调pc.getStats(),selected为 true 的candidate-pair记录就是实际在用的候选对——指向中继地址即代表 TURN 生效。
跑通之后,按业务调三档旋钮:视频会议用iceTransportPolicy: 'all',先试直连、失败再中继;IoT 这类要可预测性的场景用'relay',强制全流量走 TURN;屏幕共享加bundlePolicy: 'max-bundle',把多路流塞进一条通道省开销。接 Cloudflare Calls SFU 则更省事——TURN 需要时自动启用,createSession({ appId, sessionId })即可,无需手动编排。
端口与传输协议怎么选
一张矩阵表定方案:
| 你的网络 | 推荐端口/传输 | 理由 |
|---|---|---|
| 普通家庭/办公网,UDP 开放 | turn:turn.cloudflare.com:3478?transport=udp | 延迟最低,首选 |
| 公司网封了 UDP | turn:turn.cloudflare.com:3478?transport=tcp | TCP 能穿透多数企业防火墙 |
| 严格企业防火墙 | turns:turn.cloudflare.com:5349?transport=tcp | TLS 中继,企业场景最稳 |
| 只放行 443 出网 | turns:turn.cloudflare.com:443?transport=tcp | 复用 HTTPS 端口,必放行 |
为什么必须过滤 53 端口?因为 Chrome 和 Firefox 都会拦截浏览器往 53 端口的流量,而凭证生成接口的响应里恰好混着:53、:80这类地址——在浏览器里是静默失败,所以过滤要放在服务端、返回之前完成:
function shapeUrlsForBrowser(urls: string[]): string[] { return urls .filter(u => !u.includes(':53')) .sort((a, b) => { const rank = (s: string) => s.includes('transport=udp') ? 0 : s.includes('transport=tcp') && !s.startsWith('turns:') ? 1 : 2; return rank(a) - rank(b); }); }处理完,URL 列表就是"UDP 优先、TCP 次之、TLS 兜底"的顺序,客户端可以原样使用。
长通话不断线:凭证刷新与缓存
TURN 凭证有硬性寿命:TTL 上限 172800 秒(48 小时),超过会被 API 直接拒;常规会话用 3600 秒就够了。到期即断,所以长通话要维护两条链路。
刷新:按ttl * 1000 - 60000设定时任务,提前 1 分钟换新。注意setConfiguration()只换iceServers,不会重新触发 ICE 协商——连接还活着时足够,连接已坏时必须配合restartIce()(下一节展开)。
缓存:这段代码解决"每个客户端都打生成接口"的浪费问题,Worker 内只缓存未过期的票:
class RelayTicketStore { private cache: Ticket | null = null; async issue(keyId: string, secret: string) { if (this.cache && this.cache.dueAt > Date.now()) return this.cache; const ttl = 3600; // 硬上限 172800 const res = await fetch( `https://rtc.live.cloudflare.com/v1/turn/keys/${keyId}/credentials/generate`, { method: 'POST', headers: { Authorization: `Bearer ${secret}` }, body: JSON.stringify({ ttl }) }); const box = await res.json(); this.cache = { ...box.iceServers, urls: box.iceServers.urls.filter(u => !u.includes(':53')), dueAt: Date.now() + ttl * 1000 - 60000 }; return this.cache; } }两个细节:dueAt比真实到期早 1 分钟,留出刷新窗口;53 端口在写缓存时一次过滤掉。另外,发现会话被滥用时可调POST .../credentials/revoke(Bearer +username),返回 204,计费立刻停,活跃连接几秒内断开。
🩹 断线了怎么自动救回来
恢复链路就一条,别拆散:状态落到failed/disconnected→ 刷新凭证 →restartIce()→ 生成带iceRestart: true的 offer → 经信令通道发给对端。
pc.addEventListener('iceconnectionstatechange', async () => { if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'disconnected') { const fresh = await (await fetch('/api/relay-ticket')).json(); const cfg = pc.getConfiguration(); cfg.iceServers = fresh.iceServers; pc.setConfiguration(cfg); pc.restartIce(); const offer = await pc.createOffer({ iceRestart: true }); await pc.setLocalDescription(offer); signaling.send(offer); // 递给对端 } });四类事件必须能触发这套链路:TURN 服务器维护(anycast 网络上偶发)、anycast 路由调整、超过 1 小时的长会话凭证刷新、连接进入failed。建议把failed和disconnected都纳入恢复条件,移动端切网时才不会白丢一单。
排障矩阵
| 症状 | 可能原因 | 动作 |
|---|---|---|
| 建连慢 | 候选收集不全;到 Cloudflare 边缘延迟高;防火墙没放行 3478/5349/443 | 看候选是否收齐;企业网改用 443 上的 TURN over TLS |
| 没有 relay 候选 | 浏览器里保留了:53/:80地址 | 服务端过滤后再下发,别指望客户端兜底 |
| 约 48 小时后掉线 | 凭证 TTL 到期 | 按ttl*1000-60000提前刷新;坏了再重启 ICE |
| 请求凭证被拒 | ttl传了 7 天这类超 172800 的值 | 压回 86400 以内 |
| 随机丢包 | 超过按用户分配的限额(见下节) | 核对 pps、带宽、新 IP 频率 |
| 换网/维护后没恢复 | 只打日志,没重启 ICE | 接上上一节的恢复链 |
| 硬编码 IP 突然失效 | Cloudflare 变了 IP(提前 14 天通知) | 改用turn.cloudflare.com域名 + DNS 监控 |
| 密钥出现在前端代码里 | 客户端直接调了生成接口 | 凭证只在服务端生成,客户端只请求自己的 API |
生产细节:成本、限额、IPv6 与 TLS、IP 白名单
成本:搭配 Cloudflare Calls SFU 使用时 TURN 免费;单独按量则是 $0.05/GB 出站流量。TTL 别开满,够用就好。
限额(按用户分配,不是按账户):每秒 >5 个新 IP、5–10k pps 包速率、50–100 Mbps 数据速率,三项任一超限的后果都是丢包,没有报错可查。
IPv6 边界:客户端用 IPv4 或 IPv6 都能接入;但中继地址只分配 IPv4(不支持 RFC 6156),也没有 TCP 中继(RFC 6062)——IPv6 用户接得进来,中转流量仍走 IPv4。
TLS:支持 1.1 / 1.2 / 1.3。TLS 1.3 推荐AEAD-AES128-GCM-SHA256、AEAD-AES256-GCM-SHA384、AEAD-CHACHA20-POLY1305-SHA256;TLS 1.2 推荐ECDHE-ECDSA-AES128-GCM-SHA256、ECDHE-RSA-AES128-GCM-SHA256等。
企业 IP 白名单:严格防火墙可对turn.cloudflare.com放行141.101.90.1/32、162.159.207.1/32、2a06:98c1:3200::1/128、2606:4700:48::1/128。这些 IP 可能变更,但会提前 14 天通知——用dig turn.cloudflare.com A/dig turn.cloudflare.com AAAA定期核对,配上自动监控,14 天内把白名单换掉。
📋 上线前检查清单
- TURN 凭证只在服务端生成,Key Secret 绝不下发到浏览器
TURN_KEY_SECRET走 wrangler secrets,不进vars- TTL ≤ 预期会话时长,且 ≤ 48 小时
- 凭证生成端点加了限流
- 签发前先完成客户端认证
- 备好凭证吊销 API,应对被攻陷的会话
- 不硬编码 IP(或已建立 DNS 变更监控)
- 浏览器客户端的 URL 已过滤 53 端口
进一步阅读
- TURN 凭证生成/吊销与 Key 管理:references/turn/api.md
- Worker 搭建、环境变量与 IP 白名单:references/turn/configuration.md
- 常见坑、限额与排查:references/turn/gotchas.md
- 服务地址、端口清单与快速开始:references/turn/README.md
【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考