资讯动态

Unity WebGL 通过 MQTT over WebSocket 实现实时通信的完整指南

发布时间:2026/10/15 21:29:52 来源:尧图企业网站定制
简介本资源面向Unity开发者与物联网应用实践者聚焦将Unity项目打包为WebGL格式并接入MQTT协议实现实时通信这一典型场景适合具备一定C#与Unity基础、希望打通网页端3D内容与远程设备数据交换的中级开发者。压缩包共约2000个文件、整体295.47MB以cs脚本、meta资源描述、png贴图、dll动态库、json配置、prefab预制体、mat材质、fbx模型及wasm等WebGL构建产物为主完整保留了Assets资源目录、ProjectSettings项目设置、Packages包管理与Library缓存等标准工程结构。资源围绕WebGL导出流程、MQTT客户端集成、主题订阅与消息收发等知识点展开读者可据此还原一套可在线运行的3D应用工程理解Unity工程各目录职责与构建配置方式并掌握在Web环境中与远程设备进行数据交互的实现思路。目前已有721人学习下载。1. Unity打包WebGL并使用MQTT浏览器里跑通实时通信的真实路径把 Unity 项目导出成 WebGL再让它通过 MQTT 跟后端或其他终端交换数据这件事听起来像是把两个不同世界硬拼在一起——一个是编译成 WebAssembly 在浏览器沙箱里跑的 3D 引擎一个是传统物联网场景里基于 TCP 长连接的轻量消息协议。我最初接到这个需求时也犯过嘀咕浏览器不支持原生 SocketMQTT 的常规实现又依赖 TCP这条路到底走不走得通答案是能走但必须换一条腿——用 WebSocket 承载 MQTT 报文也就是 MQTT over WebSocket。这个方案在数字孪生看板、工业组态监控、多人协同编辑、展厅互动大屏这些场景里已经相当成熟适合手里有 Unity 项目、需要让网页端实时收发设备数据或多人同步状态的开发者。下面把我从环境准备到上线排坑的完整路径拆开讲参数和代码都能直接抄。2. 先搞清楚 MQTT over WebSocket 在 Unity WebGL 里怎么落地2.1 为什么不能直接用 TCP 版 MQTT 库Unity 在编辑器模式和 Standalone 构建里C# 的System.Net.Sockets是可用的所以像 MQTTnet 这类基于 TCP 的库跑得很顺。但一旦切换到 WebGL 平台IL2CPP 会把代码编译成 WebAssembly浏览器沙箱不允许直接创建 TCP SocketTcpClient、Socket这些类型在 WebGL 构建里要么编译报错要么运行时直接抛异常。这是浏览器安全模型决定的不是 Unity 的 bug。所以 WebGL 平台下必须走浏览器提供的通信能力XMLHttpRequest、Fetch、WebSocket。MQTT 协议本身是定义在 TCP 之上的但 OASIS 标准里也定义了 WebSocket 作为传输层绑定报文格式完全一致只是把 TCP 帧换成 WebSocket 帧。服务端 Broker 需要同时监听两个端口1883 给 TCP 客户端8083 或 8084 给 WebSocket 客户端。这一点在选 Broker 的时候就要确认清楚。2.2 选型Unity 侧用哪个 MQTT 客户端WebGL 平台能用的 MQTT 客户端库不多我实际对比过三个方向方案原理WebGL 支持维护状态适用场景MQTTnet 自定义 WebSocket 传输改写 MQTTnet 的传输层需大量改造社区有尝试但不稳定不推荐纯 C# 手写 MQTT 报文 WebSocket自己拼报文可行自己维护学习用生产慎用基于 jslib 调用浏览器 WebSocketC# 调 JS原生支持需自己封装推荐我最终选的是第三条路在 Unity 的Plugins/WebGL目录下放一个.jslib文件用 JavaScript 的WebSocketAPI 建立连接C# 侧通过DllImport调用。这样连接管理、心跳、重连逻辑都在 C# 里写底层只借用浏览器的 WebSocket 能力。MQTT 报文的编解码自己在 C# 里实现CONNECT、SUBSCRIBE、PUBLISH、PINGREQ 这几个报文类型手写一遍并不复杂反而比引入一个不兼容的库更可控。2.3 Broker 端要开的配置以常见的 Mosquitto 为例配置文件里必须显式开启 WebSocket 监听# mosquitto.conf listener 1883 protocol mqtt listener 8083 protocol websockets allow_anonymous trueprotocol websockets这一行是关键没有它 8083 端口不会接受 WebSocket 升级请求。生产环境要把allow_anonymous关掉改用用户名密码或客户端证书。改完配置重启 Broker用mosquitto_sub -h 127.0.0.1 -p 1883 -t test/# -v先确认 TCP 侧通再用浏览器控制台new WebSocket(ws://127.0.0.1:8083)确认 WebSocket 侧能握手成功。两步都过了再动 Unity。2.4 Unity 项目的基础设置在Build Settings里切换到 WebGL 平台之前先到Player Settings做几项调整。Publishing Settings里Compression Format建议选Disabled或GzipBrotli在某些服务器上需要额外配置 Content-Encoding 头容易翻车。Configuration里Api Compatibility Level选.NET Standard 2.1避免用到 WebGL 不支持的 API。Managed Stripping Level如果选High反射相关的代码可能被裁掉MQTT 报文解析如果用到了反射要留意。另外WebGL 构建默认是单线程的Application.runInBackground在浏览器标签页失焦时行为不一致心跳定时器不能依赖InvokeRepeating的精确性后面排坑章节会细说。3. 手把手从 jslib 到 C# 的 MQTT 最小可跑通实现3.1 写 WebSocket 桥接的 jslib 文件在Assets/Plugins/WebGL下新建MqttWebSocket.jslib// MqttWebSocket.jslib mergeInto(LibraryManager.library, { // 创建 WebSocket 连接返回一个句柄 id MqttWS_Connect: function (urlPtr, id) { var url UTF8ToString(urlPtr); var ws new WebSocket(url); ws.binaryType arraybuffer; if (!Module.mqttSockets) Module.mqttSockets {}; Module.mqttSockets[id] ws; ws.onopen function () { SendMessage(MqttBridge, OnWsOpen, String(id)); }; ws.onmessage function (evt) { var bytes new Uint8Array(evt.data); // 把二进制数据转成 base64 传给 C#避免指针传递的复杂性 var binary ; for (var i 0; i bytes.length; i) { binary String.fromCharCode(bytes[i]); } var b64 btoa(binary); SendMessage(MqttBridge, OnWsMessage, b64); }; ws.onerror function () { SendMessage(MqttBridge, OnWsError, String(id)); }; ws.onclose function () { SendMessage(MqttBridge, OnWsClose, String(id)); }; }, // 发送二进制数据dataPtr 指向 C# 侧传过来的字节数组 MqttWS_Send: function (id, dataPtr, length) { var ws Module.mqttSockets[id]; if (!ws || ws.readyState ! 1) return; var bytes new Uint8Array(length); for (var i 0; i length; i) { bytes[i] HEAPU8[dataPtr i]; } ws.send(bytes.buffer); }, MqttWS_Close: function (id) { var ws Module.mqttSockets[id]; if (ws) ws.close(); } });这段代码做了三件事建立 WebSocket 连接、把收到的二进制数据用 base64 编码后通过SendMessage回调给 C#、提供发送和关闭的接口。用 base64 而不是直接传指针是因为 WebGL 的堆内存在 JS 回调时机上容易和 C# 的 GC 冲突base64 虽然多一次编解码开销但稳定性高得多。SendMessage的目标对象名MqttBridge要和 C# 侧挂载的 GameObject 名字一致。3.2 C# 侧封装 MQTT 报文新建MqttBridge.cs挂在一个名为MqttBridge的空 GameObject 上using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; using UnityEngine; public class MqttBridge : MonoBehaviour { // 声明 jslib 里的外部函数 [DllImport(__Internal)] private static extern void MqttWS_Connect(string url, int id); [DllImport(__Internal)] private static extern void MqttWS_Send(int id, byte[] data, int length); [DllImport(__Internal)] private static extern void MqttWS_Close(int id); private const int SOCKET_ID 1; private bool _connected false; private ushort _packetId 1; // 订阅回调topic - 处理函数 public Actionstring, byte[] OnMessageReceived; void Start() { // WebGL 平台才调用 jslib编辑器模式用别的实现 #if UNITY_WEBGL !UNITY_EDITOR MqttWS_Connect(ws://127.0.0.1:8083, SOCKET_ID); #endif } // 由 jslib 的 SendMessage 回调 public void OnWsOpen(string id) { _connected true; SendConnectPacket(); } public void OnWsMessage(string base64) { byte[] raw Convert.FromBase64String(base64); HandleMqttPacket(raw); } public void OnWsError(string id) { _connected false; } public void OnWsClose(string id) { _connected false; } // 构造 MQTT CONNECT 报文 private void SendConnectPacket() { var body new Listbyte(); // 协议名 MQTT body.AddRange(Encoding.UTF8.GetBytes(MQTT)); body.Add(4); // 协议级别 4 MQTT 3.1.1 body.Add(0x02); // 连接标志clean session body.AddRange(new byte[] { 0x00, 0x3C }); // keepalive 60 秒 // ClientId string clientId unity_ Guid.NewGuid().ToString(N).Substring(0, 8); body.Add((byte)(clientId.Length 8)); body.Add((byte)(clientId.Length 0xFF)); body.AddRange(Encoding.UTF8.GetBytes(clientId)); SendPacket(0x10, body.ToArray()); } // 构造 PUBLISH 报文 public void Publish(string topic, string payload) { if (!_connected) return; var body new Listbyte(); byte[] topicBytes Encoding.UTF8.GetBytes(topic); body.Add((byte)(topicBytes.Length 8)); body.Add((byte)(topicBytes.Length 0xFF)); body.AddRange(topicBytes); // QoS 0不带 packetId body.AddRange(Encoding.UTF8.GetBytes(payload)); SendPacket(0x30, body.ToArray()); } // 构造 SUBSCRIBE 报文 public void Subscribe(string topic) { if (!_connected) return; var body new Listbyte(); ushort pid _packetId; body.Add((byte)(pid 8)); body.Add((byte)(pid 0xFF)); byte[] topicBytes Encoding.UTF8.GetBytes(topic); body.Add((byte)(topicBytes.Length 8)); body.Add((byte)(topicBytes.Length 0xFF)); body.AddRange(topicBytes); body.Add(0x00); // QoS 0 SendPacket(0x82, body.ToArray()); } // 固定头 剩余长度编码 private void SendPacket(byte header, byte[] body) { var packet new Listbyte { header }; int len body.Length; do { byte digit (byte)(len % 128); len / 128; if (len 0) digit | 0x80; packet.Add(digit); } while (len 0); packet.AddRange(body); byte[] arr packet.ToArray(); #if UNITY_WEBGL !UNITY_EDITOR MqttWS_Send(SOCKET_ID, arr, arr.Length); #endif } // 解析收到的报文这里只处理 PUBLISH private void HandleMqttPacket(byte[] data) { if (data.Length 2) return; byte type (byte)(data[0] 4); if (type 3) // PUBLISH { int pos 1; // 跳过剩余长度字段 while ((data[pos] 0x80) ! 0) pos; pos; int topicLen (data[pos] 8) | data[pos 1]; pos 2; string topic Encoding.UTF8.GetString(data, pos, topicLen); pos topicLen; byte[] payload new byte[data.Length - pos]; Array.Copy(data, pos, payload, 0, payload.Length); OnMessageReceived?.Invoke(topic, payload); } } void OnDestroy() { #if UNITY_WEBGL !UNITY_EDITOR MqttWS_Close(SOCKET_ID); #endif } }这段代码的核心是SendPacket里的剩余长度编码——MQTT 用变长字节表示报文长度每字节低 7 位有效最高位是延续标志。Publish和Subscribe都走这个函数。HandleMqttPacket目前只解析 PUBLISH实际项目里还要处理 SUBACK、PINGRESP但最小可跑通版本这些够了。3.3 在场景里接上业务逻辑新建一个测试脚本挂到场景里using UnityEngine; using UnityEngine.UI; public class MqttTest : MonoBehaviour { public MqttBridge bridge; public Text statusText; void Start() { bridge.OnMessageReceived (topic, payload) { string msg System.Text.Encoding.UTF8.GetString(payload); statusText.text $收到 [{topic}]: {msg}; }; // 等连接建立后再订阅实际项目里用事件回调触发 Invoke(nameof(DoSubscribe), 2f); } void DoSubscribe() { bridge.Subscribe(unity/test); bridge.Publish(unity/test, hello from webgl); } }Invoke延迟 2 秒订阅是个偷懒写法生产环境应该在OnWsOpen之后触发订阅。测试时先用mosquitto_sub -h 127.0.0.1 -p 1883 -t unity/test -v在终端监听再打开 WebGL 页面终端应该能看到hello from webgl页面上的 Text 应该显示自己发出去的消息被 Broker 回传回来。3.4 构建和本地验证Build And Run之后 Unity 会起一个本地 HTTP 服务但默认的ws://127.0.0.1:8083在 HTTPS 页面下会被浏览器拦截。本地测试用http://localhost访问没问题一旦部署到 HTTPS 域名WebSocket 必须用wss://Broker 也要配 TLS 证书。这一步是上线前最容易忽略的后面排坑章节展开。验证顺序建议先用mosquitto_pub确认 Broker 的 WebSocket 端口活着再打开 Unity WebGL 页面看浏览器控制台有没有 WebSocket 握手日志最后看 C# 侧OnWsOpen有没有被调用。三层都过了链路就通了。4. 避坑与排查WebGL MQTT 最容易翻车的五个点4.1 现象编辑器里跑得好好的打包 WebGL 后连接直接失败原因编辑器模式下UNITY_WEBGL !UNITY_EDITOR这个宏不成立DllImport(__Internal)的函数根本没被调用实际走的是别的路径或者干脆没连。很多人只在编辑器里测忽略了 WebGL 构建的差异。解决在Start里加平台判断日志WebGL 构建后用浏览器 F12 看 Console 有没有MqttWS_Connect相关的输出。如果 jslib 文件没被正确打包Console 会报MqttWS_Connect is not defined这时候检查.jslib文件是否在Plugins/WebGL目录下以及文件名是否和DllImport里的名字对应。4.2 现象连接建立后几十秒就断反复重连原因MQTT 的 keepalive 机制要求客户端在约定时间内发送 PINGREQ。WebGL 页面在标签页失焦时浏览器的定时器会被降频甚至暂停InvokeRepeating的精度完全不可靠导致 PINGREQ 没按时发出去Broker 认为客户端失联就断开了。解决把心跳逻辑放到 jslib 的 JS 侧用setInterval发送或者利用 WebSocket 协议自带的 ping/pong 帧。更稳妥的做法是在 C# 侧记录上次发送时间在Update里判断如果超过 keepalive 的一半就补发 PINGREQUpdate在 WebGL 里即使失焦也会以较低频率执行。keepalive 值建议设 60 秒不要设太小。4.3 现象HTTPS 页面下 WebSocket 连接被浏览器直接拒绝原因混合内容策略。HTTPS 页面里不允许发起ws://的明文 WebSocket 连接浏览器会直接拦截Console 报Mixed Content错误。解决Broker 开启 TLSWebSocket 走wss://端口通常用 8084。证书可以是自签的但浏览器会提示不安全生产环境要用受信任的证书。Unity 侧把连接 URL 改成wss://your-domain:8084。如果 Broker 前面有 Nginx 反代Nginx 配置里要加proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection upgrade;否则 WebSocket 升级请求会被当成普通 HTTP 请求处理。4.4 现象收到的中文消息乱码原因MQTT PUBLISH 报文的 payload 是原始字节Encoding.UTF8.GetString本身没问题但如果 jslib 里 base64 编码时用了btoa处理非 Latin1 字符会出问题。btoa只接受 Latin1 范围的字符中文字符的 UTF-8 字节超过 255 时String.fromCharCode会产生错误结果。解决jslib 里不要用btoa改用Uint8Array直接通过SendMessage传数组或者用TextDecoder先转成字符串再传。我现在的做法是在 jslib 里把字节数组转成逗号分隔的数字字符串传给 C#C# 侧再还原成 byte[]虽然丑但绝对不出错。另一种做法是用Module.HEAPU8配合_malloc传指针但要处理好内存释放时机。4.5 现象发布频率一高就丢消息或页面卡顿原因WebGL 是单线程的SendMessage从 JS 回调到 C# 是同步的如果 PUBLISH 频率很高每次都要走 base64 编解码和SendMessage主线程会被占满帧率掉得厉害。解决在 C# 侧做一个发送队列Update里每帧最多发 N 条N 根据实测调整一般 10 到 20 条不会明显影响帧率。接收侧同理如果订阅的 topic 消息量很大在 jslib 里做一层缓冲攒够一批再SendMessage。另外 QoS 选 0WebGL 场景下 QoS 1 和 2 的确认重传机制带来的复杂度远大于收益。5. 进阶让这套方案在生产环境站得住最小可跑通之后还有几件事值得做。第一是断线重连在OnWsClose回调里启动一个指数退避的重连定时器间隔从 1 秒开始翻倍上限 30 秒重连成功后重新订阅之前的 topic 列表。第二是 topic 管理把订阅关系抽成一个字典重连时遍历重新 SUBSCRIBE避免手动维护。第三是消息序列化实际项目里 payload 通常是 JSON用JsonUtility在 C# 侧解析注意 WebGL 下JsonUtility对某些嵌套结构的支持有限复杂结构建议用轻量 JSON 库。验证方面我习惯在浏览器 DevTools 的 Network 面板里看 WebSocket 帧每一帧的二进制内容都能看到对照 MQTT 报文格式手动解析一下能快速定位是编码问题还是 Broker 问题。另外在 Broker 侧开日志Mosquitto 的log_type all能看到每个客户端的连接、订阅、断开事件和浏览器侧的时间戳对一下链路哪一段慢一目了然。最后说个血泪教训我早期图省事把连接 URL 和 topic 硬编码在 C# 里结果测试环境和生产环境切换时要重新打包浪费了大量时间。后来改成从Application.absoluteURL里解析参数或者用一个ScriptableObject存配置构建后改 JSON 文件就行不用重新出包。这个习惯帮我省下的时间比优化任何一处代码都多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑