ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

WebRTC 通话怎么才能一直活下去:Cloudflare TURN 凭证刷新、ICE 重启与排障完整实战

WebRTC 通话怎么才能一直活下去:Cloudflare TURN 凭证刷新、ICE 重启与排障完整实战 WebRTC 通话怎么才能一直活下去Cloudflare TURN 凭证刷新、ICE 重启与排障完整实战【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills这次长通话又断了——第 48 小时零 3 分钟正好卡在凭证过期那个点对方画面卡住、雪花、彻底消失。这种掉线基本不是运气差而是 WebRTC 接入没按生产标准做。在 GitHub 推荐项目精选 skills4/skillsCodex 的 Skills 目录的 Cloudflare 部署模块里有一套现成的 TURN 实践模式覆盖凭证管理、ICE 重启和连接排障。下面按翻车现场 → 最小可用 → 做对 → 活久 → 自愈的顺序讲照着落地就行。三连断先认识这三个典型翻车现场现场一48 小时长通话死在到期瞬间。凭证一过期中继直接拒绝流量通话戛然而止。根因是只签发了凭证没做续期。现场二企业网络里 UDP 被墙。客户连了 47 分钟全是 TCP/TLS 回退在扛。如果端口顺序不对浏览器可能一直撞 UDP握手拖到超时。现场三地铁里切一次网通话就没了。蜂窝切 Wi-Fianycast 路由变了旧候选对失效状态机走进failed然后就没有然后了——因为代码里没人负责恢复。三个现场对应三件事续期、端口策略、ICE 重启。这也是后面正文的三条主线。写代码前先分清 STUN 和 TURN 的分工一句话STUN 负责告诉你门牌号TURN 负责你俩见不着面时帮你捎话。┌────────────── ① STUN探测公网候选 ──────────────┐ │ stun:stun.cloudflare.com:3478 │ A 客户端 ────────── ICE 协商 ────────── B 客户端 │ │ 直连失败NAT / 防火墙拦截时 │ └── 流量经 TURN 中继转发 ── turn.cloudflare.com │ anycast全球 310 城市就近接入 │两个地址一起塞进iceServers数组交给RTCPeerConnectionICE 协商会自动择优能直连就走直连直连不通才落到 TURN 中继。所以 TURN 是保命角色不是默认路径——这也是后面省钱和调策略的基础。先跑通最精简的 iceServers 配置浏览器端做的事其实很少向后端要一份临时凭证拼进iceServers。async function getTURNConfig(): PromiseRTCIceServer[] { const data await (await fetch(/api/turn-credentials)).json(); return [ // STUN发现公网候选免费且无凭证 { urls: stun:stun.cloudflare.com:3478 }, // TURN按延迟优先、TLS 兜底排序好的地址 临时凭证 { urls: [ turn:turn.cloudflare.com:3478?transportudp, turn:turn.cloudflare.com:3478?transporttcp, turns:turn.cloudflare.com:5349?transporttcp, turns:turn.cloudflare.com:443?transporttcp ], username: data.username, credential: data.credential, credentialType: password } ]; } const pc new RTCPeerConnection({ iceServers: await getTURNConfig() });坑先说凭证必须由服务端签发浏览器只拿结果。流程是——调 APIPOST /accounts/{account_id}/calls/turn_keys创建 TURN Key。响应里的key密钥本体只在创建那一次返回当场存好丢了只能重建密钥放进 Worker非敏感的TURN_KEY_ID可以写进varsTURN_KEY_SECRET用wrangler secret put单独注入生产再绑一个CREDENTIALS_CACHEKV 做凭证缓存Worker 收到浏览器请求后调凭证生成端点换一份临时凭证过滤端口后下发。完整 Worker 写法见 turn/configuration.md模式代码见 turn/patterns.md。端口取舍53 是坑顺序是命浏览器客户端推荐的尝试顺序按延迟从低到高、防火墙友好度从高到低排3478/udp—— 首选延迟最低3478/tcp—— UDP 被禁用的网络用它回退5349/tls—— 企业防火墙场景最稳443/tls—— 备用 TLS 端口最不像 WebRTC防火墙几乎不拦。⚠️53 端口一定要剔除。Chrome 和 Firefox 直接拦截该端口流量而且失败是静默的——不报错就是连不上排查起来极耗时间。为什么这事归服务端管凭证生成接口的响应里本来就带着turn:turn.cloudflare.com:53?transportudp和:80?transporttcp这类地址非浏览器客户端能用如果你原样转发等于把坑平移到每个浏览器里。过滤逻辑应该落在写缓存的那一次做掉就完了function filterICEServersForBrowser(urls: string[]): string[] { return urls .filter(url !url.includes(:53)) // 53 端口浏览器必死直接删 .sort((a, b) { // 优先级UDP 明文 TCP TLS if (a.includes(transportudp)) return -1; if (b.includes(transportudp)) return 1; if (a.includes(transporttcp) !a.startsWith(turns:)) return -1; if (b.includes(transporttcp) !b.startsWith(turns:)) return 1; return 0; }); }让连接活得久签发、缓存、提前续期先记住一个硬约束临时凭证 TTL 上限 48 小时172800 秒超过 API 直接拒绝。默认值因 API 而异仓库示例里常用 3600 秒。凭证一到期连接就断所以长通话必须同时有续期和缓存两条链路。续期setConfiguration()只换凭证不触发 ICE 重启。这一点很多人栽过跟头——它只更新配置已经失败的连接不会因此复活得配合下一节的restartIce()。刷新时机以 TTL 为基准ttl * 1000 - 60000提前 1 分钟动手别卡在到期线上。async function refreshTURNCredentials(pc: RTCPeerConnection) { const newCreds await fetch(/turn-credentials).then(r r.json()); const config pc.getConfiguration(); config.iceServers newCreds.iceServers; pc.setConfiguration(config); // 注意setConfiguration 不会触发 ICE 重启 // 连接已失败时见下文 restartIce() } const refreshInterval 3600 * 1000 - 60000; // TTL 1 小时 → 每 50 分钟刷一次 setInterval(() refreshTURNCredentials(pc), refreshInterval);缓存别让每个客户端都去敲生成端点。在服务端做一层TURNCredentialsManager未过期的凭证内存里直接吐过期了才带着密钥去换。注意两处细节缓存有效期同样提前 1 分钟- 60000留刷新窗口53 端口过滤在写入缓存时一次性完成ttl 172800做防御性校验和 API 侧约束保持一致class TURNCredentialsManager { private creds: { username: string; credential: string; urls: string[]; expiresAt: number } | null null; async getCredentials(keyId: string, keySecret: string): PromiseRTCIceServer[] { const now Date.now(); if (this.creds this.creds.expiresAt now) { return this.buildIceServers(this.creds); } const ttl 3600; if (ttl 172800) throw new Error(TTL 上限 48 小时); const res await fetch( https://rtc.live.cloudflare.com/v1/turn/keys/${keyId}/credentials/generate, { method: POST, headers: { Authorization: Bearer ${keySecret}, Content-Type: application/json }, body: JSON.stringify({ ttl }) } ); const data await res.json(); this.creds { username: data.iceServers.username, credential: data.iceServers.credential, urls: data.iceServers.urls.filter((u: string) !u.includes(:53)), expiresAt: now ttl * 1000 - 60000 // 提前 1 分钟失效 }; return this.buildIceServers(this.creds); } }另外留个后手发现某会话被攻陷可以调POST .../credentials/revoke吊销body 传对应username返回 204——计费立即停止活跃连接数秒内断开。断了怎么办ICE 状态机与自动恢复网络切换、TURN 维护、凭证过期最终都会把iceconnectionstatechange推进failed。生产级实现的恢复动作是固定四步刷新凭证 →restartIce()→ 带iceRestart: true重新 createOffer → 经信令通道发给对端。少任何一步恢复都是半成品pc.addEventListener(iceconnectionstatechange, async () { // failed 和 disconnected 都纳入恢复条件 // 防止移动网络切换时掉线 if (pc.iceConnectionState failed || pc.iceConnectionState disconnected) { await refreshTURNCredentials(pc); // ① 先换新鲜凭证 pc.restartIce(); // ② 触发 ICE 重启RFC 8445 §2.4 const offer await pc.createOffer({ iceRestart: true }); // ③ 重新发起 await pc.setLocalDescription(offer); // ④ 把 offer 通过信令通道发给对端 } });需要主动触发重启的场景有四类TURN 服务器维护Cloudflare 网络上偶发、anycast 路由调整、超过 1 小时的长会话里刷新凭证、以及连接进入failed。只判断failed不够稳——disconnected也捞进来地铁场景才真正兜住。按场景选策略iceTransportPolicy 与 SFU 自动中继同样是 TURN不同业务该用不同姿势// 视频会议先直连中继兜底 { iceServers: await getTURNConfig(), iceTransportPolicy: all } // IoT / 追求可预测连通性强制全走中继 { iceServers: await getTURNConfig(), iceTransportPolicy: relay } // 屏幕共享多路媒体聚合到一条传输通道降开销 new RTCPeerConnection({ iceServers: await getTURNConfig(), bundlePolicy: max-bundle });all默认思维P2P 优先省流量省钱relay全部流量强制经 TURN连通性可预期适合必须连上的设备场景max-bundle屏幕共享这类多流场景降开销。如果你用的是 Cloudflare Calls SFU这层编排可以整个省掉——callsClient.createSession({ appId, sessionId })之后TURN 会在需要时自动启用const session await callsClient.createSession({ appId: your-app-id, sessionId: meeting-123 });顺带说清账搭配 Cloudflare Calls SFU 使用时 TURN免费单独用则按$0.05/GB 出站流量计费。所以all默认直连优先不只是性能策略也是省钱策略。排障手册慢、断、被拦三个现场三个观察点先挂上后面所有排查都靠它们pc.addEventListener(icecandidate, e { if (e.candidate) { // type: host / srflx / relayrelay 出现说明走了 TURN console.log(候选:, e.candidate.type, e.candidate.protocol); } }); pc.addEventListener(iceconnectionstatechange, () { // 正常流转checking → connected → completed掉进 failed 才要救 console.log(ICE 状态:, pc.iceConnectionState); }); // getStats 里 selectedtrue 的 candidate-pair 才是真正在用的那条路 const stats await pc.getStats(); stats.forEach(r { if (r.type candidate-pair r.selected) console.log(选中:, r); });连接建立慢先看候选收齐了没有有没有relay候选再看客户端到 Cloudflare 边缘的延迟最后核对防火墙是否放行 3478 / 5349 / 443。企业网络别跟 UDP 硬刚直接用 443 上的 TURN over TLS。周期性掉线 / 高丢包先确认是不是凭证到期没续对一下expiresAt和时间线还不是的话查是否撞了单分配限额——下面这张表是按用户分配而非账户级超了就是丢包维度限额后果新唯一 IP 增速每秒 5 个丢包包速率入/出 5–10k pps丢包数据速率入/出 50–100 Mbps丢包被防火墙拦死严格环境可以对turn.cloudflare.com做 IP 白名单——IPv4141.101.90.1/32、162.159.207.1/32IPv62a06:98c1:3200::1/128、2606:4700:48::1/128。但注意这些 IP 提前 14 天通知即可变更务必用dig turn.cloudflare.com A/dig turn.cloudflare.com AAAA挂自动监控在 14 天窗口内更新白名单。日常接入则一律用域名turn.cloudflare.com不要硬编码 IP。上线前的硬性检查安全、限额、计费安全侧必须全过凭证只在服务端生成浏览器永远拿不到TURN_KEY_SECRET——它放 wrangler secrets不进vars签发前先做客户端认证凭证端点加限流为被攻陷的会话预留吊销 APITTL 贴着预期会话时长设且 ≤ 48 小时别为了省事开 7 天——ttl: 604800会被 API 直接拒浏览器客户端已过滤 53 端口且过滤在服务端完成。部署侧边界客户端到 TURN 支持 IPv4/IPv6但中继地址只分配 IPv4无 RFC 6156TCP 中继RFC 6062也不支持——IPv6 客户端能接入中继流量仍走 IPv4。TLS 1.1/1.2/1.3 均可用TLS 1.3 推荐AEAD-AES128-GCM-SHA256等 AEAD 套件TLS 1.2 推荐ECDHE-ECDSA-AES128-GCM-SHA256这类 ECDHE 套件细节见 turn/configuration.md。计费侧能用 Calls SFU 就走 SFUTURN 免费TTL 按需设、别过量预配默认all优先直连relay只在必要时开。继续深入仓库里的三篇参考文档turn/patterns.md —— 本文全部模式的完整实现浏览器配置、端口过滤、凭证缓存、ICE 重启turn/gotchas.md —— 常见错误对照、单分配限额、安全检查清单与逐项排障turn/api.md —— TURN Key 管理与凭证生成/吊销 API 的完整契约和 TypeScript 类型。【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表