news 2026/9/15 21:41:33

WebRTC 老是连不通?Cloudflare TURN 生产级落地完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WebRTC 老是连不通?Cloudflare TURN 生产级落地完整指南

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 是公共中转站:双方各自把数据交给它,它负责递到对方手里。

STUNTURN
干什么让你知道自己的公网地址替你转发流量
何时生效直连能通、NAT 不算凶直连不通、必须借道
你的配置里长什么样stun:stun.cloudflare.com:3478turn:/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"}

响应里有uidkeynamecreatedmodified。⚠️ 注意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 的varsTURN_KEY_SECRETwrangler 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延迟最低,首选
公司网封了 UDPturn:turn.cloudflare.com:3478?transport=tcpTCP 能穿透多数企业防火墙
严格企业防火墙turns:turn.cloudflare.com:5349?transport=tcpTLS 中继,企业场景最稳
只放行 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。建议把faileddisconnected都纳入恢复条件,移动端切网时才不会白丢一单。

排障矩阵

症状可能原因动作
建连慢候选收集不全;到 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-SHA256AEAD-AES256-GCM-SHA384AEAD-CHACHA20-POLY1305-SHA256;TLS 1.2 推荐ECDHE-ECDSA-AES128-GCM-SHA256ECDHE-RSA-AES128-GCM-SHA256等。

企业 IP 白名单:严格防火墙可对turn.cloudflare.com放行141.101.90.1/32162.159.207.1/322a06:98c1:3200::1/1282606: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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 21:41:24

Loop:三步配好 macOS 窗口管理

Loop:三步配好 macOS 窗口管理 【免费下载链接】Loop Window management made elegant. 项目地址: https://gitcode.com/GitHub_Trending/lo/Loop 下午三点,你又去拖某个窗口的右下角,想把它塞进屏幕左半边,边缘却总差着几…

作者头像 李华
网站建设 2026/9/15 21:40:35

AI Agent工程化开发:从LLM到RAG再到Agent的90天实战切片

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 21:40:09

Vue3+OpenLayers加载GeoTIFF:前端栅格可视化完整实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 21:39:59

Ubuntu 26.04 cuDNN安装避坑指南:动态库路径与内核模块加载机制详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 21:39:49

superpowers开发工作流:从Codex CLI到Cursor的AI编程协作者实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 21:39:01

Docker部署ClickHouse实战:从容器配置到OLAP查询优化

1. 先搞清楚:为什么我要用 Docker 来跑 ClickHouse做数据相关工作的朋友应该对 ClickHouse 不陌生,它是一个标准的列式 OLAP 数据库,单机就能扛住每秒百万行级别的写入,聚合查询比传统行式数据库快一到两个数量级。但很多人在第一…

作者头像 李华