Unity WebGL 全平台网络兼容:用 WebSocket 突破浏览器 Socket 限制实战

Unity WebGL 全平台网络兼容:用 WebSocket 突破浏览器 Socket 限制实战 Unity WebSocket全平台极限兼容浏览器禁掉原生 Socket 后WebGL 到底怎么连服务器做过多平台联机项目的朋友应该都碰到过这个坎同一个 C# 网络层在 PC 上用System.Net.Sockets跑得好好的一发布到 WebGL 平台就全线崩盘TcpClient 直接报平台不支持。原因其实很本质——浏览器的沙箱模型从设计上就禁止网页拿到操作系统级别的 TCP/UDP Socket 能力所以 WebGL 环境里根本没有原生套接字这条路。这时候 WebSocket 就成了唯一通行的网络方案但它的接入和管理方式跟传统 Socket 完全是两套心智模型。这篇文章我就拿一个实际项目做底子聊聊 Unity WebGL 下 WebSocket 到底怎么落地从协议差异、踩坑记录到最终的全平台兼容方案一条线讲透。这套兼容方案的核心就一句话网络层抽象隔离 运行时分发。逻辑上先把网络层拆成接口按平台分别实现底层连接方式PC/编辑器走原生 Socket 或标准 WebSocket 库WebGL 走浏览器自带的 WebSocket 对象通过 JSLib 或 NativeWebSocket 插件桥接业务层写一套逻辑通吃所有平台。这个思路不新鲜但细节坑非常多。最典型的就是 WebGL 发布后你发现在编辑器里跑得好好的协程、异步回调到浏览器里全变了个脾气。原因无外乎两个一个是浏览器主线程限制一个是音频和网络事件回调会受渲染帧率影响。再加上浏览器对 WebSocket 的消息格式、二进制帧、连接状态管理都有自己的一套约束不好好处理联机功能大概率白写。1. 为什么 WebGL 不能直接连服务器1.1 浏览器安全模型下的网络封锁先说一个很多新手容易懵的点Unity WebGL 发布出来本质上是 HTML5 JavaScript WebAssembly 的组合体。浏览器为了一套页面能安全运行从内核层面砍掉了常规应用的网络能力。一个网页运行环境中不存在直接创建 TCP Socket 的接口给你的是一个基于 HTTP 的 WebSocket/WebTransport 这类更上层的抽象。这不是 Unity 不想让你直连是整个运行环境就不允许。这带来几个直接后果System.Net.Sockets.TcpClient、UdpClient在 WebGL 平台下编译没问题但运行时会抛出PlatformNotSupportedException。普通 HTTP 请求需要做跨域处理CORS服务器不配置对应响应头请求会被浏览器拦截。WebSocket 虽然在 WebGL 里可以用但服务端地址必须满足浏览器的安全策略ws://指向局域网或公网明文地址通常可以用但如果是https页面混用ws://大概率被浏览器直接当混合内容拦掉。连接不受你 C# 代码完全控制重连、超时、心跳机制也得基于浏览器的事件模型去设计而不是传统的 Socket 超时那一套。很多团队第一次把项目发到 WebGL 上网络模块爆红第一反应是找 Unity 的 Bug。其实这是运行平台的规则问题不是引擎的问题。理解了这一层才知道为什么不管你原本用 LiteNetLib、Mirror 还是 Photon到了 WebGL 都得单独给一层 WebSocket 适配绕不开。1.2 WebSocket 在 WebGL 里到底扮演什么角色WebSocket 在 WebGL 里的角色其实就是浏览器提供的“准Socket”。它跟我们平时在 PC 上用的 Socket 有几个关键区别搞清楚这些差异后续实现才不会有幻觉。传统 TCP Socket 是长连接底层按字节流收发没有消息边界。需要自己约定封包格式比如长度头 消息体代码层级偏底层。WebSocket 是建立在 TCP 之上的应用层协议自带帧格式天然有消息边界文本消息和二进制消息分得很清。这在做游戏协议时其实是好事情省掉了大量粘包拆包的工作。但 WebSocket 也带来自己的限制头部开销比裸 TCP 大毕竟每一帧都要走协议封装。帧大小越小头的开销占比越明显。主动断开连接、检测对方存活需要靠 Close 帧、Ping/Pong 帧配合应用层心跳来做。跨域策略是硬性的。WebGL 所在页面的域和 WebSocket 服务器的域不一致时服务器必须返回允许的 Origin 才能建立连接。连接状态不稳定尤其在移动端浏览器页面切后台或锁屏WebSocket 连接会被浏览器回收需要自带重连兜底。把 WebSocket 理解成“浏览器版本的 Socket”能解决 60% 的理解问题。剩下 40% 是浏览器环境特有的生命周期问题和跨域细节这个后面实操部分会细讲。2. 全平台兼容方案架构设计2.1 网络层抽象接口先行多个平台跑不同网络后端业务层肯定不能写两套逻辑。我的做法是先把网络层定义成接口接口的长相尽量贴近项目的实际业务需求。项目通常需要的是连接管理、发送数据、接收数据、断开回调、错误回调。这段跟具体底层无关。接口大致这样设计public interface INetworkClient { event Action OnConnected; event Actionbyte[] OnReceiveData; event Actionstring OnDisconnected; event Actionstring OnError; void Connect(string url, int timeoutSeconds); void Send(byte[] data); void Close(); }接口事件回调里我把收包结果直接给到byte[]主要是因为业务层通常需要做反序列化。但注意一个点如果是 WebSocket 后端消息可能是文本消息也可能是二进制消息。后端用的什么格式C# 层要按对应类型去触发。我习惯在底层统一把文本消息按 UTF-8 转成byte[]再抛出去业务层不关心文本还是二进制只关心字节流这样跟 Socket 的逻辑统一。Connect 里的超时也跟原生 Socket 不太一样原生 Socket 有同步连接超时WebSocket 是异步连接没有默认超时。我需要在底层自己维护一个超时计时器超过了指定时间还没走通 OnConnected 回调就要主动放弃连接。这个在 PC 后端和 WebGL 后端都要做确保业务层语义一致。2.2 平台分发与后端选择接口定好之后分发逻辑相对直接。根据RuntimePlatform或预处理宏决定给业务层挂哪个后端实现。两个典型后端PC/Server/Android非 WebGL 常规平台用原生 C# Socket 或者基于它的成熟网络库。如果你原本就接了 Mirror/LiteNetLib这层不用动。WebGL强制走 WebSocket。Unity 官方没有直接提供完整的 WebSocket 支持引擎内没有内置托管实现你需要二选一跑 JSLib 桥接浏览器原生 WebSocket或跑完全跨平台的 C# WebSocket 客户端库。我个人在选择时更倾向双轨策略编辑器内特别是开发阶段直接用 C# WebSocket 客户端库连接真实服务器。本地调试不需要被浏览器环境干扰C# 层断点方便。WebGL 发布后走 JSLib 方案底层的连接和收发完全由浏览器原生 WebSocket 处理性能最好也没有第三方库引入的兼容性麻烦。代码里通过宏区分就行public static INetworkClient Create() { #if UNITY_WEBGL !UNITY_EDITOR return new WebSocketClient(); #else return new NativeSocketClient(); #endif }这里必须注意UNITY_EDITOR的判断顺序。在编辑器里以 WebGL 为目标运行 Play Mode 时UNITY_WEBGL有时候也是定义的但如果不写!UNITY_EDITOR这个分支编辑器里会走进 WebSocket 分支反而没法用 C# 层调试。2.3 为什么不用统一方案可能有人问既然 WebGL 必须用 WebSocket那所有平台干脆都用 WebSocket 不就行了吗这样逻辑就一套了。理论上可以实际部分项目也会这么做。但我个人的观点是PC 端保留原生 Socket 后端价值更大原生 Socket 可用的协议面更广TCP/UDP/KCP 都能玩WebSocket 只是 TCP 上的应用层封装玩法受限。自建服务器如果走自定义 TCP 协议WebSocket 反而是多余封装。直接用 Socket 层更灵活性能也更好。对于已经在跑的存量服务比如你用 KCP 或 UDP 做帧同步不可能因为一个 WebGL 端就把服务端全改成 WebSocket。所以务实的路线是保留多个后端接口统一。服务端能同时监听 TCP 和 WebSocket 就最好了这样一个联机游戏既能服务于原生应用也能服务于网页用户。业务层代码因为是同一套接口额外维护成本并不高。3. JSLib 方案实战拆解3.1 桥接代码设计与库文件结构如果选 JSLib 方案核心工作就是把 C# 层的方法绑定到 JavaScript 实现上。Unity 的官方文档把这套交互标记叫__Internal或__Internal.jslib通过.jslib文件引入。一个最小可用的.jslib文件通常长这样mergeInto(LibraryManager.library, { WS_Connect: function(callbackId, urlPtr, urlLen) { // ... }, WS_Send: function(connId, dataPtr, dataLen) { // ... }, WS_Close: function(connId) { // ... } });C# 层封装的时候要注意字符串传递。C# 的字符串不能直接传引用给 JavaScript需要先转成 UTF-8 字节数组把指针和长度传过去JavaScript 侧再通过UTF8ToString解码。二进制发送同理从 C# 调用 WebGL 层传二进制时应该拿Uint8Array的里面就放 C# 传入的字节地址然后再创建 Blob 或者 ArrayBuffer 给 WebSocket 的send()。交互设计上我的习惯是C# 侧发起连接时给一个整数连接 ID一个页面可能同时存在多个连接并注册对应的回调函数。JavaScript 侧的onopen、onmessage、onclose、onerror触发时通过 Unity 的全局方法挂在游戏对象上的把对应事件回传给 C# 的接口实现。回调线程注意WebGL 是单线程的。JavaScript 回调最终还是要寻址到 Unity 的 main thread 去执行。好在 Unity WebGL 帮你把事件调度放到了主线程不需要像原生多线程那样担心锁。一个比较成熟的实践是后端事件不过度埋点。连接握手成功、收到完整消息、连接关闭、错误这四类事件统一走四个 C# 事件对应即可业务层自己决定怎么处理。过度设计只会让 JSLib 代码变成一团乱麻。3.2 C# 封装层的坑点与正确姿势C# 侧的 WebSocketClient 要处理几个容易被忽略的细节连接状态机。C# 里维护自己的状态因为浏览器 WebSocket 虽然也有 readyState但 C# 层不一定每次都能及时拿到。我使用一个枚举public enum ConnState { Idle, Connecting, Connected, Closing, Closed }所有公共方法都要判断当前状态是不是允许执行该操作。尤其是 Send如果状态不是 Connected 就直接返回失败不要让底层真的去调用send()否则会抛 JavaScript 异常。回调注册与反注册。JSLib 里把消息回传给 C# 的方法通常是一个挂在 GameObject 上的 MonoBehaviour通过SendMessage或直接调用静态方法。如果场景里没有对应物体或者物体被销毁回调就会丢。我的做法是创建一个常驻的 NetworkBridge GameObject并且设置DontDestroyOnLoad避免切场景的时候把回调载体干掉。伪代码大致长这样public class NetworkBridge : MonoBehaviour { internal WebSocketClient client; public void OnConnect(string msg) { client?.HandleOpen(msg); } public void OnMessage(byte[] data) { client?.HandleData(data); } public void OnClose(string reason) { client?.HandleClose(reason); } public void OnError(string err) { client?.HandleError(err); } }消息缓冲问题。浏览器里的 WebSocket 大消息不一定一次到达可能会拆成多帧但 C# 层完全没有必要关心这个问题。WebSocket 协议本来就跟 Socket 不同浏览器已经帮你做了重装。你只会在 OnMessage 回调里收到一条完整消息。这一点一定要想清楚——如果你还在 JSLib 里自己做消息边界处理那就是画蛇添足。3.3 二进制消息与文本消息处理做游戏联机多数项目用二进制做协议封装比如 MessagePack、Protobuf 或者自研的位流协议。WebSocket 原生支持文本帧和二进制帧两种格式。浏览器侧的 send 区分很简单ws.send(string)发 Text 帧ws.send(arraybuffer)或ws.send(blob)发 Binary 帧C# 侧通过 JSLib 发二进制时理想方式是把 C# 里的 byte[] 地址直接传给 JS做一个零拷贝的 Uint8Array。Unity 给的HeapU8其实是对 wasm 内存的视图如果用HEAPU8.subarray(ptr, ptr len)直接取子数组然后传给 WebSocket要小心内存生命周期。send()是异步的但浏览器在调用 send 时会立即复制数据。这个比较特殊我看了几个项目的实测表现send 里同步复制基本能认为安全调用时数据会自动拷入浏览器内部缓冲区。不过保守起见如果要多次复用发送缓冲区建议每次发送前用HEAPU8.slice拷贝一份独立数据WS_Send: function(connId, dataPtr, dataLen) { let socket sockets[connId]; if (!socket || socket.readyState ! 1) return; let bytes new Uint8Array(HEAPU8.buffer, dataPtr, dataLen); let copy bytes.slice(); // 复制一份 socket.send(copy.buffer); }不要小看这一步。如果你用的是共享缓冲区发送间隙缓冲区被其他数据覆盖收到的就是脏包而且还极难排查因为错误行为不稳定时好时坏。4. 编辑器与真机环境的调试验证4.1 本地测试服务器的快速搭建做 WebGL 联机开发本地测试服务器最好支持 WebSocket 协议不要用静态服务器去测不然你永远验证不了双向通信。我用的最多的是 Node.js 的ws库因为它轻而且配起来最快。一个最小可行的 echo 服务器也就几十行const WebSocket require(ws); const server new WebSocket.Server({ port: 3658 }); server.on(connection, (ws) { console.log(client connected); ws.on(message, (data) { // 回显 ws.send(data); }); ws.on(close, () console.log(client closed)); }); console.log(listening on 3658);启动后先用纯 Web 页面验证一下这个服务能不能连然后编辑器里用 C# 客户端连一次最后发布 WebGL 到本地 HTTP 服务器再连一次。三关都过说明链路是通的。ws://127.0.0.1:3658在开发时很方便但如果发布出去要给别人测试地址就不能写死成 127.0.0.1 了需要服务器支持远程 WebSocket。注意 WebGL 页面运行时如果页面是通过 http 访问的WebSocket 地址也要跟你服务器的跨域策略匹配。4.2 跨域问题到底怎么解决WebGL 页面所在域与 WebSocket 服务器所在域不是同一个域时服务器需要显式允许跨域连接。很多人习惯性地只加 HTTP 的Access-Control-Allow-Origin响应头但对 WebSocket 握手来说这就够。握手时服务器会校验请求里的 Origin 字段如果需要并在响应头给出允许的标识。WebSocket 协议不是靠传统的 CORS 机制来拦截的浏览器对 WebSocket 跨域行为实际上与 CORS 不太一样但它同样会阻止不符合服务器响应预期的连接。实践证明Node.js 的 ws 库默认允许全部跨域连接但在生产环境里建议显式校验 Origin 来源const server new WebSocket.Server({ port: 3658, verifyClient: (info, done) { const origin info.origin; if (!origin || origin.includes(localhost) || origin.includes(mysite.com)) { done(true); } else { done(false, 403, Forbidden); } } });如果部署在云服务器还要注意安全组和防火墙是否放行了 WebSocket 端口。这个看似跟 Unity 没关系但实际排查时常在这里卡半天。4.3 编辑器 Play Mode 下的调试技巧在编辑器中跑网络逻辑最大的优势是可以断点。我建议把逻辑尽量挪到非 Coroutine 的地方用 Unirx UniTask 之类的异步模型驱动网络主流程否则在 Coroutine 里断点很容易因为调度不及时而看不清楚。我实际开发中惯用的检查清单确定编辑器里走的是哪个INetworkClient实现用日志打出来。确认 WebSocket 连接的 URL 带上了正确协议头ws://或wss://别拿http://直接接。确认连接的服务器端口没有被防火墙拦截。在编辑器本地连127.0.0.1时几乎不会遇到防火墙问题一旦改成局域网 IP就要排查。定义好协议的消息分隔或者在业务层设置消息超时防止服务器不回包时整个逻辑挂死。WebGL 不支持多线程尤其不要在网络收到消息后的回调里直接开Thread或Task.Run。这在编辑器里跑没问题一发布到 WebGL 就很有可能触发未支持的 API 调用。跨平台逻辑抽象好后大部分模块可以用一套代码跑通剩下的问题集中在平台差异的边缘。所谓极限兼容就是把这一层差异彻底封装好业务层一个if都不用写。5. 常见问题与排查技巧实录5.1 连接失败、连不上服务器问题表现WebGL 发布后打开页面控制台发现WebSocket connection to ws://... failed连不上。排查方向检查 URL 是否可访问。先用浏览器直接访问该 WebSocket 地址比如ws://123.45.67.89:8080地址栏不支持直接输这个一般要借助在线 WebSocket 测试工具来试。检查是否跨域。页面是https://a.com服务器是ws://b.com:8080跨域握手被服务器拒绝换wss并且服务器配置允许 b.com 或者*源就可以了。检查混合内容。页面本身是https但你连的是ws://明文地址。浏览器默认是拦混合内容的会直接拒绝连接。解决方案页面和 WebSocket 服务器统一走 TLS 加密使用wss://。检查服务器监听地址是否为 127.0.0.1 还是 0.0.0.0。绑定到 127.0.0.1 的话外部设备访问不到。需要监听0.0.0.0或具体内网 IP。5.2 网页长时间挂机后自动断连问题表现用户把页面开着不动过几分钟再看连接已经断了或者功能异常。这是很典型的 WebSocket 空闲回收问题。浏览器为了省资源和缓解服务器压力长时间没有数据传输的连接会被自动断开。服务器也可能主动断开空闲连接。应对方案应用层心跳。客户端每 15 到 30 秒发一个 Ping 包可以是 WebSocket 的 Ping 帧也可以是自己业务协议里的心跳消息。服务端收到心跳后回一个 Pong。客户端如果在超时时间比如 45 秒内没有收到任何消息主动重连。注意重连要有退避策略避免服务器恢复瞬间几千个客户端同时撞上来造成雪崩。常见做法是第一次退避 1 秒然后 2 秒、4 秒最多几十秒封顶。有的项目倾向于纯依赖 WebSocket 协议层的 Ping/Pong但浏览器端的 Ping/Pong 帧不一定完全可控且某些代理可能会吃掉这些帧。我建议用业务层的心跳包虽然多了几字节数据但行为完全可控日志里也能直接看到。5.3 发送大数据帧时性能表现差问题表现当单次传输超过几百 KB 甚至上 MB 时明显感觉卡顿或者帧率下降。原因不复杂。WebGL 本身跑在浏览器主线程上如果onmessage处理大帧时做大量解析、游戏对象创建、UI 刷新等同步操作主线程必然卡顿。优化方向大帧数据和普通数据区分优先级。重要的大帧如场景同步、资源配置走独立的队列避免阻塞高频的小数据消息。在消息处理侧避免大量GameObject.Find、Instantiate等开销高的调用把对象的引用提前缓存好。如果确实有高频大批量数据考虑二进制压缩GZip/Deflate或者调整消息结构。把不必要的字段从消息体里拿掉能省下大量带宽和 CPU。WebGL 的Uint8Array转 C#byte[]是有内存拷贝的大数据帧会触发OnMessage里的字节数组分配GC 压力升高。减少分配次数是核心。如果数据量大可以考虑对象池复用接收缓冲区但要小心消息生命周期确保消息内容在处理完之前不被覆盖。5.4 浏览器升级后突然不支持 WebGL 2问题表现用户反馈打不开项目或者浏览器页面提示类似your browser does not support graphics api webgl2的错误换了浏览器或升级版本后正常。这不是网络问题但联调的时候也特别常出现。本质是运行设备的显卡驱动或浏览器设置把 WebGL 2 给禁了。Unity WebGL 发布默认按 WebGL 2 构建但使用旧显卡或者特定安全策略时可能要自动回退到 WebGL 1。解决方案如果项目 3D 特性用得不多且 UI 表现足够可以尝试用 WebGL 1 的 Player Settings 重新发布。注意部分 Shader 和图形 API 在 WebGL 1 下不支持。对用户侧做提示要求用户开启浏览器的硬件加速。Chrome 在设置-系统里可以打开硬件加速部分企业的安全策略或 Mac 上的某些浏览器版本会默认关闭 WebGL。实测发现 MacBook 上的 Chrome/Edge 如果系统节能模式开得太激进或内存占用过高WebGL 也会被停用。让用户先关闭多余标签页更新到最新版浏览器大概率能解决。6. 我用过的第三方 WebSocket 方案对比6.1 自研 JSLib vs 成熟 C# 库做 WebGL 网络模块前先考虑要不要重复造轮子。如果你用的是一款成熟的 C# WebSocket 库它可能有很好的编辑器开发体验API 顺手还支持多平台。但它的内部很可能依赖原生 Socket需要看它有没有针对 WebGL 做特殊适配。已知部分库确实有 WebGL backend但大多数因为原生 Socket 的隔离限制上不了 WebGL。自研 JSLib 的好处是可控性强、连接管理和事件机制透明、发布产物干净。代价是需要熟悉 JavaScript 侧编码处理回调传输出了问题只能自己调试。做一个项目如果只涉及简单的收发、连断自研成本不高。涉及复杂的安全机制、token 握手、协商模式可能需要封装的东西多一些。从长期维护角度如果公司同时有多个 WebGL 联机项目我建议沉淀一套公共组件一次性把 JSLib 的封装、心跳、重连、日志输出等能力全部做进公共包里实现一劳永逸。单个项目直接引第三方库可能快些但二次开发和个性化定制空间也相对受限。6.2 面向延迟敏感场景的取舍如果你的项目是帧同步或者需要精确延迟控制的联机射击游戏建议尽量绕开 WebGL。WebGL 的 WebSocket 方案在延迟上普遍比原生 PC TCP 高 10 到 30 毫秒而且是浏览器调度不可控因素更多。如果业务不可避免要上一个 WebGL 版本可以考虑以下思路协议层精简到最小消息体不塞冗余字段。使用二进制消息不走文本 JSON。文本解析开销在 WebGL 上会被放大。保证服务端逻辑在低延迟地区部署让网络 RTT 本身尽可能是最小因子。服务端和客户端都做好频率控制避免大量无关广播数据灌进网页端。WebGL 的联机开发很多场景不追求竞技性实时帧同步而是回合制、棋牌或轻量多人交互。这些场景用 WebSocket 完全够用稳定性和开发效率都能接受。6.3 生产级别接入还得看什么真正上生产不要只看 demo 能通。还有几个被忽略的细节需要补上。连接保活与重连逻辑要写在客户端基础层不要让每个业务现场自己写。否则每个界面挂一次、回收到后台再回来就断连用户体感会非常差。断线重连时服务端的会话状态需要能恢复。纯消息型游戏服务端直接为客户端重发关键数据即可。状态型游戏客户端要能从重连包重新同步世界状态。别以为 WebSocket 重连了就能自动恢复之前的数据它是纯传输层概念状态恢复需要应用层去配合。日志采集要跨层贯通。WebGL 项目不同于原生 App出问题后拿不到设备日志。前端要埋足够的埋点在云端把错误回调等关键节点记录下来。对连接失败率、重连率、平均消息延迟这些指标做监控是最基础的要求。安全层面。WebGL 页面是完全暴露给用户的代码里不要硬编码服务器连接密钥或者敏感的反作弊逻辑。所有的鉴权信息都要通过连接参数或特定握手消息从服务端获取和挑战验证客户端里塞什么都会被人从浏览器 DevTools 里翻出来。7. 一套可用工程的落地建议7.1 项目文件结构与放置策略网络层代码分开目录放至少是这个结构Assets/Scripts/Network/Interface—— 公共接口和事件定义Assets/Scripts/Network/Implementation—— PC/Editor 的原生后端Assets/Scripts/Network/WebGL—— WebGL 的 C# 封装和 JSLib 文件Assets/Scripts/Network/Utility—— 队列、缓冲池、心跳工具等这种结构的好处是后续要加 WebTransport、加 Steam 后端或者接其他第三方库都只是在这个目录下新增实现类不需要改业务调用。7.2 编译宏与编辑器约束除了前面提到的UNITY_WEBGL !UNITY_EDITOR这个判断还有一个容易踩的坑UNITY_WEBGL的宏在浏览器预览里和编辑器 Play Mode 里并不完全等价。如果只是想在编辑器中模拟 WebGL 环境需要用 Play Mode 设置里的 Simulator 方式但那个模拟只能模拟部分系统 API网络行为差异依然很大。我的建议开发期跑 C# 后端不要硬在编辑器中模拟 WebGL 的运行。反正最终发布产物才是检验的标准。遇到问题先把环境切换到 WebGL 发布后在浏览器 Console 里看错误这样定位最快。7.3 如何验证兼容性做完整个方案之后需要有一份验证清单PC 编辑器连本机 WebSocket Server功能全部跑通。Mac/Windows 桌面打包后走原生 Socket 后端通信正常。WebGL 发布到本地静态服务器Chrome 上测通信正常。WebGL 发布后Firefox 和 Edge 各测一轮。不同浏览器对 WebSocket 的事件回调细节有不同的调度表现个别浏览器对 Blob 和 ArrayBuffer 的处理也不一样。JSLib 里发送时最好显式传 ArrayBuffer不要传 Blob避免部分浏览器兼容问题。内网传输大文件几百 KB 以上、频繁断连、快速重连、服务器重启等多重场景都走一遍。7.4 从系统层面看兼容如果项目还涉及移动设备上的浏览器兼容性要更进一步考虑Android Chrome、iOS Safari、平板自带浏览器等。iOS Safari 在低功耗模式下会冻结后台页面的 JS 执行WebSocket 连接容易因而被强制断开。所以除了 WebSocket 本身的重连还要监听页面的visibilitychange事件在页面回到前台时主动检测连接可用性必要时主动发起重连。这个细节你要是提前不加用户从后台切回来后的第一波操作可能直接石沉大海。如果真的要在多个系统浏览器上做极限适配注意把音频播放、UI 渲染、异常上报等与网络相关的交互同步考虑进去避免因系统级策略导致联机功能整体不可用。8. 总结之外的一点真实经验我做 WebGL 联机项目踩过最深的坑不是 WebSocket 本身不会写而是整个调试链路太弱。浏览器报错信息对 Unity 开发者来说往往不够直观稍不留神就把问题定位到 C# 逻辑其实底层早就在 JavaScript 侧挂了。后来学乖了在 JSLib 里把每个关键步骤的 console.log 都打上客户端状态机每跳转一步也打日志线上加一个 remote log 通道瞬间清爽很多。最后提醒一句不要因为 WebGL 的网络能力受限就绕过 WebSocket 去找别的偏门方案。浏览器技术栈的大方向就是服务化 WebSocket/WebTransport尽早把网络抽象层做好后面不管 WebGL 怎么进化业务都能平稳接住。这套全平台极限兼容的做法最终目的不是给某一款产品护航而是让团队面对跨平台时永远多一条逃生通道。