利用Cloudflare Worker实现WebSocket安全反代
1. 为什么你需要一个WebSocket安全反代如果你正在开发一个需要实时通信的应用比如在线聊天室、实时数据仪表盘、多人在线游戏或者物联网设备控制面板那你肯定对WebSocket不陌生。它就像一个在客户端和服务器之间架设的“专用电话线”一旦接通双方就可以随时、高效地互发消息比传统的HTTP请求-响应模式好比不停地发短信确认要流畅得多。但问题来了。当你把应用部署上线尤其是服务器在国内而你的用户遍布全球时这条“电话线”可能会遇到各种麻烦。比如某些网络环境对WebSocket连接不太友好或者你的服务器域名没有备案导致连接直接被阻断。更常见的是你的后端服务可能只暴露了普通的ws://协议非加密的WebSocket这在公网传输敏感数据简直是“裸奔”既不安全也容易被中间人攻击或运营商干扰。这时候一个位于中间的安全代理就显得至关重要。它就像一位专业的“接线员”和“保镖”。客户端与这位“保镖”建立安全的、加密的连接使用wss://然后由“保镖”负责与后端的原始服务器进行通信。这个“保镖”不仅能解决协议转换、域名屏蔽的问题还能利用其全球网络优化连接速度。而Cloudflare Worker就是这个“保镖”角色的绝佳人选。它运行在Cloudflare遍布全球的边缘网络上无需你管理服务器写几十行JavaScript代码就能搭建一个高性能、高可用的WebSocket安全反代成本极低甚至免费。我自己的一个物联网项目就遇到过这个坑。设备上报数据用的ws://在测试环境一切正常一到生产环境部分地区的用户就死活连不上。排查了半天不是防火墙就是运营商策略问题。后来就是用Worker做了一个反代让客户端统一走Cloudflare的wss入口问题迎刃而解而且全球访问延迟都明显降低了。接下来我就手把手带你把这个“保镖”请回家。2. 理解核心从WSS到WS的协议转换在动手写代码之前我们得先搞清楚我们要做的事情到底是怎么一回事。这能帮你避开很多后续的坑。首先明确几个关键概念WS (WebSocket) 基于TCP的通信协议标识符是ws://。它本身不提供加密。WSS (WebSocket Secure) 基于TLS/SSL加密的WebSocket协议标识符是wss://。你可以把它理解为HTTPS版的WebSocket。反代 (Reverse Proxy) 代理服务器的一种。它代表后端服务器接收客户端的请求然后将请求转发给后端并将后端的响应返回给客户端。对客户端来说它就像在直接访问后端服务器。我们的目标场景是你的真实后端服务器在ws://your-backend.com提供服务。但出于安全或可访问性考虑你希望客户端通过wss://your-proxy.example.com来连接。那么Cloudflare Worker在这里扮演什么角色呢接收安全连接 Worker 部署在your-proxy.example.com下并且由Cloudflare自动提供SSL证书所以你不用操心证书问题。客户端向它发起一个wss://请求。协议剥离与转发 Worker 收到这个安全的wss请求后在内部将其“降级”为一个普通的ws请求。同时它会将请求的目标地址host从自己的域名your-proxy.example.com替换成你真实的后端服务器地址your-backend.com。建立代理通道 Worker 以客户端的身份向ws://your-backend.com发起一个新的WebSocket连接。双向数据透传 一旦两边的连接都建立成功Worker 就变成一个透明的“管道”。客户端发来的消息Worker 原样转发给后端后端返回的消息Worker 也原样传回给客户端。这个过程听起来复杂但得益于Worker对WebSocket协议的完整支持实现起来非常简洁。关键在于Worker的fetch()API 不仅可以处理HTTP请求也能直接处理WebSocket连接的升级Upgrade请求这让我们用很少的代码就能完成这个代理逻辑。注意这里有一个非常重要的点也是很多新手会困惑的。Worker 脚本本身并不“运行”一个持续的WebSocket服务器。它只是在连接建立的那一刻即HTTP升级为WebSocket的握手阶段进行拦截和转发。握手成功后数据的流动就直接在Cloudflare的边缘网络和你的后端服务器之间进行了Worker脚本不再参与每个数据帧的处理这保证了代理的高性能。3. 手把手实战编写你的第一个反代Worker理论说再多不如动手试一次。我们直接来看代码我会逐行解释并告诉你哪些地方可以按需修改。首先你需要一个Cloudflare账户。如果还没有去官网注册一个免费的套餐就足够我们测试和很多小型项目使用了。登录后进入Workers Pages面板点击创建应用程序-创建Worker。你会看到一个在线代码编辑器。我们把默认的代码全部删掉替换成下面这个增强版的脚本// 监听所有发送到该Worker的请求 addEventListener(fetch, event { event.respondWith(handleRequest(event.request)); }); async function handleRequest(request) { // 1. 获取请求的URL对象方便操作 let url new URL(request.url); // 2. 关键步骤协议转换 (WSS - WS) // 如果客户端使用的是 wss://我们将其改为 ws:// 以便连接后端 if (url.protocol wss:) { url.protocol ws:; } // 注意如果客户端直接用了 ws:// 连Worker这里可以不做修改 // 但强烈建议生产环境强制使用WSS。 // 3. 修改目标主机地址 // 将host替换为你真实的后端WebSocket服务器地址和端口 url.host your-real-backend.com:8080; // 请替换成你的地址 // 如果你的后端是标准WebSocket端口80/443可以省略端口 // 4. 可选但推荐设置WebSocket协议头 // 确保请求头中包含正确的Upgrade信息 let requestHeaders new Headers(request.headers); requestHeaders.set(Upgrade, websocket); requestHeaders.set(Connection, Upgrade); // 如果你的后端需要特定的Origin或Sec-WebSocket-Protocol可以在这里添加或修改 // requestHeaders.set(Sec-WebSocket-Protocol, your-protocol); // 5. 构建新的转发请求 // 注意这里使用原始的 request.method 和 body但替换了URL和Headers let newRequest new Request(url, { headers: requestHeaders, method: request.method, body: request.body }); // 6. 发起转发请求并获取响应 try { let response await fetch(newRequest); return response; } catch (error) { // 错误处理如果连接后端失败返回一个友好的错误信息 return new Response(WebSocket proxy failed to connect to backend: ${error.message}, { status: 502, statusText: Bad Gateway }); } }代码详解与个性化配置点第10-13行协议转换 这是核心逻辑。url.protocol属性可能是https:或wss:我们统一将其改为ws:。这样无论客户端怎么连过来我们转发出去的都是非加密的WebSocket请求前提是你的后端是ws服务。第17行修改目标Host这是你必须修改的地方把your-real-backend.com:8080替换成你实际的后端服务器IP或域名。如果后端服务运行在非标准端口比如8080、3000一定要带上端口号。第21-27行设置请求头 虽然很多情况下直接用原始的request构造新请求也能工作但显式地设置Upgrade和Connection头是更规范的做法能避免一些兼容性问题。如果你的客户端和服务器约定了子协议Sec-WebSocket-Protocol可以在这里进行设置或转发。第36-42行错误处理 这是一个非常重要的补充。原始代码没有错误处理如果后端服务器宕机或网络不通客户端只会得到一个晦涩的连接失败提示。加上try...catch后我们能捕获到fetch失败的错误并返回一个标准的502 Bad Gateway响应这在前端调试时会清晰很多。写好代码后点击右上角的“保存并部署”按钮。系统会为你分配一个*.workers.dev的子域名比如your-awesome-proxy.workers.dev。你的第一个WebSocket反代就上线了如何测试你可以使用任何WebSocket客户端工具进行测试。例如在浏览器控制台或者使用wscat这样的命令行工具。原始连接可能失败或不安全ws://your-real-backend.com:8080通过Worker代理的安全连接wss://your-awesome-proxy.workers.dev用第二个地址去连接如果一切顺利你应该能成功建立连接并进行通信而所有数据都经过了Cloudflare网络的加密中转。4. 进阶配置与优化技巧基础的代理跑通了但想用在生产环境我们还得考虑更多。下面这些技巧能让你的反代更健壮、更安全。4.1 路径转发与URL重写你的后端WebSocket服务可能不是挂在根路径/上比如可能是ws://backend.com/api/ws或ws://backend.com/socket.io/?EIO4transportwebsocket。我们的Worker需要能处理这种情况。修改第17行的url.host替换逻辑转而使用url.hostname和url.port并保留原始路径和查询参数// 替换目标主机和端口但保留路径和查询字符串 url.hostname your-real-backend.com; url.port 8080; // 如果端口是80或443可以省略这行 // url.pathname 和 url.search 会自动保留无需修改这样当客户端连接wss://your-proxy.workers.dev/api/ws时请求会被准确地转发到ws://your-real-backend.com:8080/api/ws。4.2 添加访问控制与安全头完全开放的代理是危险的可能被他人滥用。我们可以添加简单的认证比如通过URL密钥Token或验证请求来源Origin。示例通过查询参数验证async function handleRequest(request) { let url new URL(request.url); // 检查URL中是否包含预期的密钥 const expectedToken your-secret-token-here; if (url.searchParams.get(token) ! expectedToken) { return new Response(Forbidden: Invalid token, { status: 403 }); } // 验证通过后移除token参数避免泄露给后端 url.searchParams.delete(token); // ... 剩下的协议转换和转发逻辑 ... }这样只有使用wss://your-proxy.workers.dev/?tokenyour-secret-token-here的连接才会被代理。示例验证Origin针对浏览器客户端async function handleRequest(request) { const allowedOrigin https://your-frontend-app.com; const requestOrigin request.headers.get(Origin); // 如果请求来自浏览器且Origin不匹配则拒绝 if (requestOrigin requestOrigin ! allowedOrigin) { return new Response(Forbidden: Origin not allowed, { status: 403 }); } // ... 剩下的转发逻辑 ... }4.3 处理WebSocket子协议和自定义头有些WebSocket库或服务会使用子协议Sec-WebSocket-Protocol进行特性协商。为了确保兼容性我们应该将其从客户端请求中透传给后端。async function handleRequest(request) { // ... 前面的URL处理逻辑 ... let requestHeaders new Headers(request.headers); requestHeaders.set(Upgrade, websocket); requestHeaders.set(Connection, Upgrade); // 获取客户端请求的子协议并原样设置到转发请求中 const clientProtocol request.headers.get(Sec-WebSocket-Protocol); if (clientProtocol) { requestHeaders.set(Sec-WebSocket-Protocol, clientProtocol); } // 同样可以处理其他需要透传的头部如 Sec-WebSocket-Extensions const clientExtensions request.headers.get(Sec-WebSocket-Extensions); if (clientExtensions) { requestHeaders.set(Sec-WebSocket-Extensions, clientExtensions); } // ... 构建新请求并转发 ... }4.4 性能与超时设置Cloudflare Worker默认有请求超时限制免费版约30秒。对于长连接的WebSocket这个限制指的是建立连接握手过程的超时而非连接建立后的持续时间。一旦握手成功连接会持久化不受此超时影响。但为了更稳定你可以在fetch请求中传递一些初始化参数虽然对WebSocket握手阶段影响有限但养成好习惯。let response await fetch(newRequest, { // 以下设置主要影响初始的TCP/TLS连接阶段 cf: { // 可以设置一些Cloudflare特有的选项例如缓存行为对WS无效 } // fetch的timeout属性在Worker环境中不直接支持超时主要由平台控制 });更重要的“性能优化”在于合理使用Worker的全球边缘网络。你的用户会连接到离他最近的Cloudflare数据中心然后由Cloudflare的网络优化到你的后端服务器的路由。如果你的后端服务器也在Cloudflare上例如Pages或托管服务那延迟会更低。5. 常见问题排查与调试心得即使代码看起来正确在实际部署中你还是可能会遇到一些奇怪的问题。这里分享几个我踩过的坑和解决办法。问题一连接始终返回400 Bad Request或426 Upgrade Required。可能原因 请求头没有正确设置。确保在构造新Request时Upgrade: websocket和Connection: Upgrade这两个头部已经设置。有些后端服务器对这两个头检查非常严格。排查方法 在Worker代码中在fetch之前将newRequest.headers打印到控制台使用console.log([...newRequest.headers])部署后查看Worker的日志输出确认头部是否正确。问题二连接可以建立但几秒钟后自动断开。可能原因 这可能是由于后端服务器或客户端发送了ping/pong帧但Worker在握手成功后不再处理数据帧导致保活机制失效。不过根据Cloudflare的文档一旦WebSocket连接在Worker中建立后续的数据帧包括ping/pong会在边缘网络自动透传通常不会因此断开。更可能的原因 你的后端服务器有自己空闲连接超时设置而客户端没有发送足够的数据。这时代理是无辜的。你需要调整后端服务器的超时配置或在客户端实现定期发送心跳包ping的逻辑。问题三如何查看Worker的日志进行调试这是最重要的调试手段。在Worker编辑器的右下角点击“日志”标签。然后触发你的WebSocket连接请求这里会实时显示console.log的输出、未捕获的异常以及每个请求的概要信息。善用日志输出变量状态是定位问题最快的方式。问题四我想代理非WebSocket的HTTP流量可以吗当然可以。我们这个脚本的核心是一个条件判断。你可以在函数开头添加逻辑根据请求的路径或头部决定是进行WebSocket代理还是普通的HTTP反向代理甚至是返回一个静态页面。一个Worker可以同时处理多种类型的请求非常灵活。问题五免费套餐够用吗对于个人项目或中小型应用Cloudflare Worker的免费套餐每日10万次请求完全够用。需要关注的是出口流量免费套餐每月有10GB的限制。WebSocket连接建立后的数据传输会计入出口流量。如果你的应用是高频、大流量的实时数据推送需要估算一下用量。对于绝大多数场景免费额度都是绰绰有余的。最后我个人的经验是将这套反代方案用于生产环境前一定要在不同的网络环境下特别是移动网络进行充分测试。因为经过代理后问题的表象可能会变化扎实的测试是稳定的保证。这套方案我已经稳定运行了超过一年服务了几个不同的项目它极大地简化了我在实时通信服务上的运维复杂度让我能更专注于业务逻辑本身。