
一次搞懂 Cloudflare TURN:WebRTC 通话从拨号到断线自救的完整指南【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skillsWebRTC 通话连不上、或者打到一半声音中断,多半是 NAT 或防火墙在作怪。Cloudflare TURN 是跑在覆盖 310 城市的 anycast 网络上(不含中国网络)的托管中继服务,当两端直连失败时由它转发流量,保证通话不断。这篇文章按一次通话的生命周期带你走完整套 WebRTC TURN 接入:拨号前怎么建 Key、怎么让后端代签凭证,通话中怎么选端口、怎么在凭证到期前完成刷新,断线时怎么一键恢复,最后教你确认流量到底走的直连还是中继。先诊断:NAT 和防火墙为什么掐断 P2P 直连WebRTC 的理想路径是双方各自发现公网候选(host 与 srflx),然后点对点直连。但现实里,对称型 NAT 会为不同目标地址建立不同映射,导致 STUN 发现的候选对端根本够不到;企业防火墙经常把 UDP 3478 整个拦掉;移动网络上的运营商级 NAT 更不可预测。STUN 在这种局面下只能报告而不能打通,所以还需要 TURN:双方把流量都发给同一个中继,由中继代转。SFU(选择性转发单元)则是另一层的事,它解决多方媒体的分发,TURN 只解决传输可达。想清楚这条分界,后面的决策就不难:视频会议这类成本敏感场景,让直连优先、TURN 兜底;IoT 这类要求可预测的场景,则可以直接强制全走中继。拨号之前:创建 TURN Key,再让后端代签短期凭证一次 POST 创建 TURN Key先创建一把长期 Key。所有管理端点都要求具备 Calls Write 权限的 Cloudflare API Token,Base URL 是https://api.cloudflare.com/client/v4。创建动作很简单:POST /accounts/{account_id}/calls/turn_keys Content-Type: application/json { name: my-turn-key }响应包含uid、key、name、created、modified五个字段,其中key就是密钥本体,只在创建时返回一次——务必当场保存,之后再查只有标识符。列表、改名、删除分别是对turn_keys和turn_keys/{key_id}的 GET/PUT/DELETE,细节可查 api.md。用 Worker 代签短期凭证接着部署一个 Worker,由它替浏览器换短期凭证。服务端需要四个环境变量:CLOUDFLARE_ACCOUNT_ID、CLOUDFLARE_API_TOKEN、TURN_KEY_ID、TURN_KEY_SECRET。其中不敏感的TURN_KEY_ID可以放进 wrangler 的vars,TURN_KEY_SECRET必须用wrangler secret put单独注入;生产环境还能再绑一个CREDENTIALS_CACHE的 KV 命名空间做缓存,完整配置参考 configuration.md。Worker 本体就干三件事:验登录、拿密钥换凭证、返回:async function handleTurn(request: Request, env: Env): PromiseResponse { if (!request.headers.get(Authorization)) return new Response(Unauthorized, { status: 401 }); const res await fetch( https://rtc.live.cloudflare.com/v1/turn/keys/${env.TURN_KEY_ID}/credentials/generate, { method: POST, headers: { Authorization: Bearer ${env.TURN_KEY_SECRET}, Content-Type: application/json }, body: JSON.stringify({ ttl: 3600 }) }); const data await res.json(); return Response.json(data.iceServers); }先校验客户端身份,是为了不让任何人免费消耗中继容量;ttl: 3600适配 1 小时会议,更长会期可以调大,上限规则见下文。浏览器端组装 iceServers最后在浏览器端组装iceServers:从自己的后端拉凭证,再固定叠一路公开 STUN。STUN 负责发现公网候选,TURN 负责直连失败时兜底,两者放进同一个数组,由 ICE 协商自动择优:async function buildIceServers(): PromiseRTCIceServer[] { const creds await fetch(/turn-credentials).then((r) r.json()); return [ { urls: stun:stun.cloudflare.com:3478 }, { urls: creds.urls, username: creds.username, credential: creds.credential, credentialType: password } ]; } const pc new RTCPeerConnection({ iceServers: await buildIceServers() });端口选择:先过滤掉浏览器禁用的 53 端口,再按偏好排序凭证生成端点的响应会返回一整串地址,其中turn:turn.cloudflare.com:53?transportudp和turn:turn.cloudflare.com:80?transporttcp对桌面端等非浏览器客户端是合法可用的,但 Chrome 和 Firefox 在浏览器层拦截 53 端口,这些地址在页面上会静默失败。所以过滤要放在服务端下发之前做,而不是指望客户端:function pickBrowserUrls(raw: string[]): string[] { return raw .filter((u) !u.includes(:53)) .sort((a, b) { 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; }); }排序体现的偏好是:3478/udp打头(延迟最低),3478/tcp兜住封 UDP 的网络,5349/tls在企业防火墙里最稳,443/tls是另一条防火墙友好的路。把排好序的列表交给浏览器后,ICE 会按这个顺序尝试,任何一个通了其余的就不起作用了。会话维护:到期前 1 分钟刷新凭证,服务端顺手做缓存短期凭证的硬上限是172800 秒(48 小时),API 见到超值的请求直接拒绝。凭证一旦过期,中继会话随即失效,任何长于 TTL 的通话必然中断。刷新时点取ttl * 1000 - 60000,即到期前 1 分钟。有个容易踩的点:setConfiguration()能热替换 iceServers,但它不会触发 ICE 重启,所以如果连接此时已经失败,还得配合下一节的恢复流程:async function renewTurn(pc: RTCPeerConnection): Promisevoid { const fresh await fetch(/turn-credentials).then((r) r.json()); const config pc.getConfiguration(); config.iceServers fresh.iceServers; pc.setConfiguration(config); } // ttl(秒)即生成时传入的值,例如 3600 时约每 50 分钟刷新一次 setInterval(() renewTurn(pc), ttl * 1000 - 60000);服务端建议再加一层缓存,免得每个浏览器请求都打到生成端点;缓存未过期就直接发,顺便在本地提前做 TTL 校验:let cached: { username: string; credential: string; urls: string[]; validUntil: number } | null null; async function issueTurn(keyId: string, keySecret: string, ttl 3600): PromiseRTCIceServer[] { if (cached cached.validUntil Date.now()) return toIceServers(cached); 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} }, body: JSON.stringify({ ttl }) }); const data await res.json(); cached { username: data.iceServers.username, credential: data.iceServers.credential, urls: data.iceServers.urls.filter((u: string) !u.includes(:53)), validUntil: Date.now() ttl * 1000 - 60000 }; return toIceServers(cached); }toIceServers就是把凭证按上一节的方式打包成 STUNTURN 两条数组。注意三个细节:缓存有效期比 TTL 少 1 分钟,留刷新窗口;53 端口在写入缓存时一次性过滤;ttl 172800的防御性校验与 API 侧约束对齐。若需要立刻掐掉某个会话(比如凭据被泄露),调用同前缀的credentials/revoke端点,body 传username,成功返回 204,计费即刻停止,活跃连接会在数秒内断开。断连怎么救:走一遍 ICE 重启的完整流程 有四类情况会把连接推入failed:TURN 服务器维护窗口(anycast 网络上偶发)、网络拓扑变化(路由调整)、超过 1 小时的长会话里的凭证刷新、以及连接本身已经失败。恢复动作是固定的:先刷新凭证,再重启 ICE,然后生成带iceRestart: true的新 offer,最后经信令通道发给对端:pc.addEventListener(iceconnectionstatechange, async () { if (pc.iceConnectionState ! failed pc.iceConnectionState ! disconnected) return; await renewTurn(pc); // 先换新鲜凭证 pc.restartIce(); // 触发 ICE 重启 const offer await pc.createOffer({ iceRestart: true }); await pc.setLocalDescription(offer); sendToPeer(offer); // 经信令通道发给对方 });把disconnected也纳入恢复条件,是因为移动网络切换时连接往往先进disconnected、随后才变failed,只盯failed经常慢一拍。怎么看清:用候选对确认流量到底走了直连还是中继 三个 API 足以还原整个连接过程:两个事件负责实时观察,一个方法负责按需拉取:pc.addEventListener(icecandidate, (e) { if (e.candidate) console.log(候选:, e.candidate.type, e.candidate.protocol); }); pc.addEventListener(iceconnectionstatechange, () { console.log(状态:, pc.iceConnectionState); }); const stats await pc.getStats(); stats.forEach((r) { if (r.type candidate-pair r.selected) console.log(实际选中:, r); });读法如下:icecandidate的type只有 host、srflx 而没有 relay,说明中继地址没配对或被拦;iceconnectionstatechange正常应走checking → connected → completed,停在failed就回到上一节的恢复流程;getStats()里candidate-pair中selected为 true 的条目,就是当前真正在用的候选对——是直连还是 TURN 中继,一眼可辨。如果建连缓慢,按这个顺序排查:候选收集是否完整、到 Cloudflare 边缘的延迟、防火墙是否放行 3478/5349/443,企业网络是否该改走 443 上的 TURN over TLS。生产加固:限额、安全、成本与 IPv6/TLS 边界先说限额。注意这些数值是按用户分配计的,不是账户全局:单一分配上每秒出现超过5 个新源 IP、包速率超过5k–10k pps(入出方向)、或数据速率超过50–100 Mbps(入出方向),后果都是丢包。监控里看到持续丢包,先对照这三条线。再说安全:密钥只在服务端交换,绝不进浏览器;TURN_KEY_SECRET放 wrangler secrets 而不是vars;TTL 贴合预期会期且不超过 48 小时;凭证端点加限流与客户端认证;不要硬编码 IP——官方给的企业白名单地址是 IPv4141.101.90.1/32与162.159.207.1/32、IPv62a06:98c1:3200::1/128与2606:4700:48::1/128。若防火墙必须白名单,请配上 DNS 监控:dig turn.cloudflare.com A dig turn.cloudflare.com AAAA官方可能提前 14 天通知变更这些 IP,白名单必须在该窗口内更新。成本与协议边界可以概括成两条:搭配 Cloudflare Calls SFU 使用时 TURN 免费,独立使用则按$0.05/GB出站计费,所以优先用iceTransportPolicy: all让直连省下中继费用,relay只留给强可预测场景。另外,IPv6 客户端可以接入 TURN,但中继地址只分配 IPv4(不支持 RFC 6156),TCP 中继(RFC 6062)也不支持——IPv6 客户端的中继流量最终仍走 IPv4。TLS 1.1/1.2/1.3 均支持,推荐套件见 configuration.md 中的清单。收尾:把关键参数再过一遍端口偏好3478/udp优先,5349/443 的 TLS 兜底,53 端口在服务端过滤;TTL 上限48 小时,按ttl*1000-60000提前刷新,且setConfiguration本身不重启 ICE;failed与disconnected都要恢复:刷新凭证 restartIce()iceRestart: true的新 offer;用getStats()的candidate-pair.selected核对真实走向;限额、白名单与吊销 API 的完整清单见 gotchas.md。延伸阅读:api.md 收录凭证生成/吊销与 Key 管理端点,configuration.md 讲 Worker、wrangler 与白名单,patterns.md 给出完整示例,README.md 列有全部服务地址与端口;SKILL.md 的网络连通性决策树里,实时通信场景对应的正是turn/与realtime-sfu/模块。【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考