1. 项目概述:为什么Unity开发者需要关注NativeWebSocket?
如果你是一名Unity开发者,并且你的项目涉及到任何形式的实时数据交换——无论是多人在线游戏、实时数据可视化大屏、远程协作工具,还是物联网设备的控制面板——那么你一定对网络通信的稳定性和跨平台兼容性感到头疼。传统的Unity网络方案,比如Unity自带的UNet(已弃用)、第三方插件如Photon(功能强大但成本不菲),或是基于HTTP的长轮询,在面对高实时性、低延迟的需求时,总显得有些力不从心。这时,WebSocket协议以其全双工、低开销的特性,成为了一个极具吸引力的选择。
然而,Unity官方并未提供原生的WebSocket支持。过去,我们往往需要依赖一些C#的第三方库,比如WebSocketSharp、Fleck,或者通过Unity的WWW或UnityWebRequest进行封装。但这些方案在跨平台,尤其是移动端(iOS/Android)和WebGL平台上,常常会遇到各种“坑”:证书问题、线程安全、内存泄漏,或者在WebGL下根本无法使用纯C#的Socket实现。于是,一个名为NativeWebSocket的开源解决方案进入了我们的视野。它不是一个简单的C#库,而是一个巧妙地桥接了各平台原生WebSocket能力的“胶水层”,旨在为Unity提供一个真正全平台、高性能、易用的WebSocket客户端。
简单来说,NativeWebSocket解决的核心痛点是:让Unity开发者用一套几乎相同的C# API,在编辑器、PC、Mac、iOS、Android、WebGL等所有Unity支持的平台上,稳定、高效地使用WebSocket进行通信。它底层在移动端调用iOS的URLSessionWebSocketTask和Android的java.net.WebSocket,在WebGL端使用浏览器的WebSocket对象,在Standalone平台则可能回退到性能优秀的C#实现,从而实现了“一次编写,处处运行”的理想状态。接下来,我将结合自己多次在项目中的实战经验,为你深度拆解这个方案。
2. NativeWebSocket核心架构与设计思路拆解
2.1 设计哲学:抽象与桥接
NativeWebSocket的设计非常清晰,遵循了良好的软件工程原则。它的核心是一个C#抽象层(通常是一个接口,如IWebSocket),定义了一套标准的连接、发送、接收、关闭等操作。这个抽象层是开发者直接交互的对象,保证了代码的跨平台一致性。
关键在于其下的具体实现层:
- 编辑器与独立平台:在Windows、Mac、Linux的编辑器模式和打包后的独立应用中,它可以使用一个纯C#的WebSocket客户端实现,例如基于
System.Net.WebSockets或更轻量、兼容性更好的第三方库。这保证了在开发阶段和桌面端的高效调试与运行。 - iOS/Android平台:通过Unity的插件机制(
.a静态库或.jar包),调用平台原生的WebSocket API。这是性能最优、最稳定的方式,因为直接使用了操作系统提供的网络栈,能更好地处理后台、锁屏、网络切换等复杂场景。 - WebGL平台:这是传统C#网络库的“禁区”。NativeWebSocket通过C#的
[DllImport]特性,调用由Emscripten编译生成的JavaScript代码,后者再直接调用浏览器环境的WebSocket对象。这种“C# -> JS Interop -> Browser API”的桥接,是能在WebGL中使用WebSocket的唯一可行路径。
这种架构的优势显而易见:将平台差异性封装在底层,向上提供统一接口。作为开发者,你不需要关心在iPhone上用的是URLSession,在安卓上是OkHttp,在浏览器里是window.WebSocket。你只需要new一个WebSocket对象,调用ConnectAsync(),然后监听OnMessage事件即可。
2.2 与同类方案的横向对比
在引入NativeWebSocket之前,我们通常会评估几个选项:
纯C# WebSocket库(如WebSocketSharp):
- 优点:代码纯C#,易于集成和调试。
- 缺点:在iOS/Android上可能因系统网络权限、后台策略导致连接不稳定;在WebGL上完全无法工作;某些库的SSL/TLS实现可能不完善。
- 适用场景:仅针对PC/Mac/Linux的桌面端项目。
Unity Transport Package (UTP) 或 Netcode for GameObjects:
- 优点:Unity官方维护,与Unity深度集成,为游戏优化,支持可靠/不可靠传输。
- 缺点:它更偏向于游戏网络层协议,而非通用的WebSocket。如果你想连接一个标准的WebSocket服务端(比如用Node.js、Spring Boot写的),需要额外的适配层,不够直接。
- 适用场景:纯Unity游戏项目,且服务端也采用UTP协议。
商业网络引擎(如Photon、Mirror):
- 优点:功能极其强大,提供完整的房间管理、匹配、RPC等游戏网络解决方案。
- 缺点:成本高,架构重,学习曲线陡峭。如果你只需要一个简单的双向数据通道,用它们就像“用大炮打蚊子”。
- 适用场景:中大型商业网游,需要完整的网络后端服务。
NativeWebSocket:
- 优点:轻量、专注、全平台。它只做一件事——提供标准的WebSocket客户端。集成简单,API直观,免费开源。
- 缺点:它只是一个客户端,不提供房间、匹配等高级游戏网络功能。需要你自己处理重连、心跳、消息序列化等应用层逻辑。
- 适用场景:需要与现有WebSocket服务端通信的跨平台Unity应用。无论是游戏中的实时排行榜、聊天室,还是工业领域的实时数据监控、教育领域的互动课件,都是其用武之地。
实操心得:选择网络方案时,一定要明确需求边界。如果你的项目是“Unity应用 + 标准WebSocket服务端”的架构,并且对全平台部署有硬性要求,那么NativeWebSocket几乎是目前最优雅、成本最低的解决方案。我曾在一个跨平台(Win/iOS/Android/Web)的实时数据看板项目中,用NativeWebSocket替换了原先混合使用Socket.IO(WebGL)和TcpClient(移动端)的混乱方案,代码量减少了60%,连接稳定性提升了不止一个档次。
3. 核心细节解析与集成实操要点
3.1 项目集成与基础配置
NativeWebSocket通常以Unity Package的形式提供,可以通过Unity的Package Manager从Git URL添加,或者直接下载源码放入项目的Plugins目录。这里以通过Git集成为例:
通过Package Manager添加:
- 打开Unity,进入
Window -> Package Manager。 - 点击左上角的“+”号,选择“Add package from git URL...”。
- 输入NativeWebSocket仓库的URL(例如:
https://github.com/endel/NativeWebSocket.git)。注意,需要确认该仓库提供了package.json文件。 - 点击“Add”。Unity会自动下载并编译该包。
- 打开Unity,进入
源码集成:
- 如果Git方式不奏效,或者你需要修改源码,可以直接克隆仓库,将其中的
Assets/Plugins/NativeWebSocket文件夹复制到你Unity项目的Assets目录下。 - 这种方式更直接,但需要注意管理更新。
- 如果Git方式不奏效,或者你需要修改源码,可以直接克隆仓库,将其中的
关键配置检查:
- iOS:确保
Player Settings -> Other Settings -> Configuration中的Internet Client和Allow downloads over HTTP权限已开启。如果使用WSS(WebSocket Secure),通常不需要额外处理证书,因为原生API会处理。 - Android:检查
Player Settings -> Player -> Android -> Publishing Settings中的Internet Access权限是否为Require。对于Android 9 (Pie)及以上,如果服务端使用HTTP,可能需要在Network Security Config中配置明文通信允许,但强烈建议生产环境使用WSS。 - WebGL:这是配置最省心的平台,因为一切都由浏览器环境管理。只需注意,如果服务端使用自签名证书或WSS,在浏览器中首次访问时需要用户手动信任。
3.2 API使用模式与核心事件
NativeWebSocket的API设计非常简洁,主要围绕几个核心方法和事件。下面是一个最基础的使用流程:
using NativeWebSocket; using System.Threading.Tasks; using UnityEngine; public class WebSocketManager : MonoBehaviour { WebSocket websocket; async void Start() { // 1. 创建WebSocket实例 websocket = new WebSocket("wss://yourserver.com:port/path"); // 2. 订阅关键事件 websocket.OnOpen += () => { Debug.Log("连接建立成功!"); // 连接成功后可以发送初始消息 SendMessage("Hello Server!"); }; websocket.OnError += (errorMsg) => { Debug.LogError($"WebSocket错误: {errorMsg}"); }; websocket.OnClose += (closeCode) => { Debug.Log($"连接关闭,代码: {closeCode}"); }; websocket.OnMessage += (bytes) => { // 接收到二进制消息 var message = System.Text.Encoding.UTF8.GetString(bytes); Debug.Log($"收到消息: {message}"); // 处理消息逻辑... }; // 3. 发起连接 try { await websocket.Connect(); } catch (System.Exception ex) { Debug.LogError($"连接失败: {ex.Message}"); } } void Update() { // 4. 必须定期调用DispatchMessageQueue来派发消息事件 // 这是为了将网络线程的回调调度到主线程(Unity主循环) #if !UNITY_WEBGL || UNITY_EDITOR websocket?.DispatchMessageQueue(); #endif } async void SendMessage(string message) { if (websocket.State == WebSocketState.Open) { var bytes = System.Text.Encoding.UTF8.GetBytes(message); await websocket.Send(bytes); } } async void OnDestroy() { // 5. 妥善关闭连接 if (websocket != null) { await websocket.Close(); } } }核心要点解析:
DispatchMessageQueue():这是非WebGL平台最关键的一步。WebSocket的网络事件(接收消息、连接关闭等)发生在后台线程,而Unity的GameObject和UI操作必须在主线程执行。DispatchMessageQueue()的作用就是将累积在队列中的事件回调(如OnMessage)安全地切换到主线程触发。忘记调用它,你将收不到任何消息回调!WebGL平台不需要此调用,因为浏览器的JavaScript回调本身就在主线程。- 异步方法:
Connect()和Send()方法都是async的,建议使用await调用以避免阻塞主线程。对于发送,也可以使用SendText()方法直接发送字符串。 - 状态管理:通过
websocket.State(WebSocketState枚举)可以随时检查连接状态(连接中、已打开、关闭中、已关闭),这是实现重连逻辑的基础。 - 资源清理:在
OnDestroy或OnApplicationQuit中主动关闭连接是良好习惯,可以发送一个正式的关闭帧(Close Frame)通知服务端。
3.3 消息协议与序列化方案
WebSocket底层支持二进制(ArraySegment<byte>)和文本(string)两种帧格式。NativeWebSocket的OnMessage事件同时提供了byte[]和string的重载。选择哪种格式取决于你的应用协议。
- 文本协议(JSON):最通用,易于调试。适合消息结构复杂但数据量不大的场景。
// 发送 var message = new { type = "move", x = 10, y = 20 }; string json = JsonUtility.ToJson(message); // 或 Newtonsoft.Json await websocket.SendText(json); // 接收 websocket.OnMessage += (string messageStr) => { var data = JsonUtility.FromJson<MoveData>(messageStr); // 处理data... }; - 二进制协议:效率高,节省带宽。适合高频、小数据包或对性能要求极高的场景(如游戏同步)。你需要自定义编解码规则。
// 假设协议:前4字节为int类型表示消息类型,后面为数据体 websocket.OnMessage += (byte[] bytes) => { using (var ms = new System.IO.MemoryStream(bytes)) using (var reader = new System.IO.BinaryReader(ms)) { int msgType = reader.ReadInt32(); switch (msgType) { case 1: float posX = reader.ReadSingle(); float posY = reader.ReadSingle(); break; // ... 其他类型 } } };
注意事项:对于复杂项目,强烈建议在应用层定义一套自己的消息头(包含消息ID、长度、序列号等),并封装一个统一的
MessageDispatcher来处理消息的路由和反序列化,而不是在OnMessage回调里写一堆if-else或switch。
4. 高级应用与稳定性实战
4.1 实现自动重连与心跳机制
一个健壮的实时通信模块,必须处理网络波动和中断。NativeWebSocket提供了连接状态和关闭事件,我们可以基于此构建重连逻辑。
public class RobustWebSocketClient : MonoBehaviour { private WebSocket ws; private string serverUrl = "wss://yourserver.com"; private bool shouldReconnect = true; private int reconnectDelay = 1; // 初始重连延迟(秒) private const int maxReconnectDelay = 32; // 最大延迟 private Coroutine reconnectCoroutine; private async void ConnectToServer() { if (ws != null) { ws.OnOpen -= OnConnected; ws.OnError -= OnError; ws.OnClose -= OnDisconnected; await ws.Close(); } ws = new WebSocket(serverUrl); ws.OnOpen += OnConnected; ws.OnError += OnError; ws.OnClose += OnDisconnected; try { await ws.Connect(); } catch { ScheduleReconnect(); } } private void OnConnected() { Debug.Log("连接成功"); reconnectDelay = 1; // 重置重连延迟 CancelInvoke(nameof(SendPing)); // 清除旧的心跳 InvokeRepeating(nameof(SendPing), 30f, 30f); // 连接成功后开始每30秒发送一次心跳 } private void OnDisconnected(WebSocketCloseCode code) { Debug.Log($"连接断开,代码: {code}"); CancelInvoke(nameof(SendPing)); // 停止心跳 if (shouldReconnect) { ScheduleReconnect(); } } private void ScheduleReconnect() { if (reconnectCoroutine != null) StopCoroutine(reconnectCoroutine); reconnectCoroutine = StartCoroutine(ReconnectAfterDelay()); } private System.Collections.IEnumerator ReconnectAfterDelay() { Debug.Log($"等待{reconnectDelay}秒后尝试重连..."); yield return new WaitForSeconds(reconnectDelay); ConnectToServer(); // 指数退避策略,避免频繁重连冲击服务器 reconnectDelay = Mathf.Min(reconnectDelay * 2, maxReconnectDelay); } private async void SendPing() { if (ws?.State == WebSocketState.Open) { try { // 发送一个特定的ping消息,或者使用WebSocket协议自带的Ping帧(如果库支持) await ws.SendText("{\"type\":\"ping\",\"timestamp\":" + DateTime.UtcNow.Ticks + "}"); } catch { // 发送失败,可能连接已失效,触发重连 OnDisconnected(WebSocketCloseCode.Abnormal); } } } void Update() { #if !UNITY_WEBGL || UNITY_EDITOR ws?.DispatchMessageQueue(); #endif } void OnDestroy() { shouldReconnect = false; if (reconnectCoroutine != null) StopCoroutine(reconnectCoroutine); CancelInvoke(nameof(SendPing)); ws?.Close(); } }关键设计:
- 指数退避重连:每次重连失败后,等待时间加倍(上限为maxReconnectDelay),避免在网络短暂故障时产生“重连风暴”。
- 心跳保活:定期向服务器发送轻量级消息(Ping),用于:a) 保持NAT映射,防止连接因超时被中间路由器断开;b) 探测连接是否存活,以便及时触发重连。注意,WebSocket协议本身有Ping/Pong帧,但并非所有客户端/服务端实现都暴露了该API。上述代码使用应用层心跳作为通用方案。
- 状态清理:在重连前,务必取消旧连接的所有事件订阅并关闭它,防止内存泄漏和事件重复触发。
4.2 多线程安全与主线程调度
如前所述,DispatchMessageQueue()是保证线程安全的核心。但还有一些细节需要注意:
- 发送消息:
Send方法是异步的,可以在任何线程调用。但如果你需要在发送前后操作Unity对象(如更新UI文本显示“发送中”),最好使用MainThreadDispatcher或确保在Update等主线程方法中调用。 - 复杂消息处理:如果
OnMessage回调中的处理逻辑非常耗时(比如解析一个巨大的JSON,或进行复杂的数学计算),这将会阻塞主线程,导致游戏卡顿。解决方案是:
你需要自己实现或找一个websocket.OnMessage += (byte[] bytes) => { // 将耗时的处理抛到线程池 Task.Run(() => { var processedData = ProcessMessageHeavy(bytes); // 将结果传回主线程更新Unity对象 UnityMainThreadDispatcher.Instance.Enqueue(() => { ApplyProcessedData(processedData); }); }); };UnityMainThreadDispatcher工具类,其核心是利用UnitySynchronizationContext或ExecuteOnMainThread。
4.3 性能优化与内存管理
- 消息池:对于高频消息(如游戏位置同步),频繁创建和GC
byte[]或字符串会引发性能问题。可以设计一个ArrayPool<byte>或自定义对象池来复用内存。 - 流量控制:不要无节制地发送消息。对于高频更新(如角色位置),可以设置发送频率上限(如每秒10-20次),或者使用差值压缩、状态同步等算法减少数据量。
- 连接数:一个客户端通常只维持一个WebSocket连接。避免创建多个连接,这会增加服务器压力和客户端资源消耗。
5. 全平台适配与疑难问题排查实录
5.1 各平台特性与适配要点
| 平台 | 特性与注意事项 | 常见问题与解决方案 |
|---|---|---|
| 编辑器/PC/Mac | 环境最宽松,可使用性能最好的C#实现。调试方便。 | 防火墙或杀毒软件可能拦截连接。确保在安全软件中添加例外。 |
| iOS | 使用NSURLSessionWebSocketTask,对后台活动限制严格。 | 后台断连:App进入后台后,Socket可能被系统挂起或关闭。需要配置Background Modes中的Voice over IP或使用PushKit(如果适用)来维持连接,或者实现快速重连。ATS:如果使用ws://(非安全),需要在Info.plist中配置NSAppTransportSecurity允许任意加载,但App Store审核可能不通过,强烈建议使用WSS。 |
| Android | 使用java.net.WebSocket或OkHttp等实现。版本碎片化严重。 | 网络权限:确保AndroidManifest.xml有INTERNET权限。明文通信:针对Android 9+,如果必须用ws://,需配置网络安全策略。同样,建议始终使用WSS。后台保活:相比iOS稍宽松,但仍需注意省电策略。可以使用Foreground Service或WorkManager来维持重要连接,但需向用户说明。 |
| WebGL | 完全依赖浏览器环境,受同源策略、CORS限制。 | CORS错误:如果WebSocket服务端与网页宿主不同源,服务端必须设置正确的CORS头(Access-Control-Allow-Origin)。WSS证书:必须使用受信任的CA签发的证书,自签名证书会导致连接失败。浏览器兼容性:现代浏览器均支持WebSocket,但注意个别老旧浏览器或特殊环境(如微信内置浏览器)的兼容性。 |
5.2 常见问题排查速查表
在实际开发中,你会遇到各种各样的问题。下面这个表格整理了我踩过的一些“坑”及其排查思路:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 连接失败,无错误信息 | 1. URL格式错误。 2. 网络不通。 3. 服务端未启动或端口错误。 4. (WebGL) CORS策略限制。 | 1. 检查URL,确保是ws://或wss://开头。2. 用 ping或telnet命令测试服务器IP和端口是否可达。3. 确认服务端进程正在运行并监听正确端口。 4. 打开浏览器开发者工具(F12)的“网络(Network)”标签,查看WebSocket连接请求是否被CORS策略阻止。需要服务端配置响应头。 |
| 连接成功但立刻断开 | 1. 服务端主动拒绝(如鉴权失败)。 2. 心跳机制缺失,被中间设备断开。 3. (移动端) 网络切换(WiFi到4G)。 | 1. 查看服务端日志,确认连接建立后是否有立即关闭的逻辑。 2. 实现应用层心跳,保持连接活跃。 3. 在Unity中监听 Application.internetReachability变化,在网络切换时主动重连。 |
| 能连接,但收不到消息 | 1.非WebGL平台忘记调用DispatchMessageQueue()。2. 消息格式与 OnMessage事件订阅类型不匹配(二进制 vs 文本)。3. 服务端发送的消息路由错误。 | 1.这是最高频的原因!确保在Update()中调用了websocket.DispatchMessageQueue()。2. 确认服务端发送的是文本帧还是二进制帧,并订阅对应的事件( OnMessage的string或byte[]重载)。3. 使用Wireshark或服务端调试,确认消息是否确实从服务端发出。 |
| 移动端(尤其iOS)在后台几分钟后断连 | 系统为省电/流量,暂停了后台应用的网络活动。 | 1. 对于需要持久连接的应用(如即时通讯),考虑使用iOS的VoIP或推送通知能力。 2. 实现“快速重连”机制。当应用从后台唤醒时( OnApplicationPause(false)),立即检查Socket状态并尝试重连。3. 向用户说明后台数据刷新可能受限。 |
| WebGL版本在编辑器正常,发布后失败 | 1. 发布后的域名/端口与服务端不匹配。 2. 使用了 ws://但页面是https://,浏览器会阻止混合内容。3. 服务器防火墙未开放WebSocket端口。 | 1. 使用构建后的实际访问地址来配置WebSocket连接URL。 2.必须保持协议一致:如果网页是 https://,WebSocket必须使用wss://。3. 检查服务器安全组/防火墙设置,确保WebSocket端口(通常是80/443或自定义端口)对外开放。 |
| 发送大量消息时卡顿或崩溃 | 1. 主线程被DispatchMessageQueue或消息处理逻辑阻塞。2. 内存暴涨,GC频繁。 | 1. 优化OnMessage处理逻辑,将耗时操作移到子线程。2. 实现消息池,复用 byte[]数组,减少GC压力。3. 对发送频率进行节流(Throttle)或防抖(Debounce)。 |
5.3 调试技巧与工具推荐
- 服务端模拟与测试:在开发初期,可以使用一些在线工具或本地工具快速搭建一个WebSocket回显服务器进行测试,比如
websocket.org提供的在线测试工具,或者使用Node.js的ws库几行代码写一个测试服务器。 - 网络抓包分析:对于复杂问题,网络抓包是终极武器。
- 桌面/移动端:使用Wireshark或Fiddler抓取TCP/WebSocket流量,可以清晰看到握手过程、数据帧和关闭帧,对于排查协议层面的问题无比有效。
- WebGL:直接使用浏览器自带的开发者工具(F12),在“网络(Network)”标签页中筛选
WS或WSS,可以查看每条WebSocket帧的内容。
- 日志分级:在Unity中实现一个详细的日志系统,将WebSocket的连接、发送、接收、错误、状态变更都记录下来,并区分Info、Warning、Error等级别。在测试包中开启详细日志,能帮你快速定位问题发生的时间点和上下文。
6. 项目实战:构建一个简单的跨平台聊天室示例
为了将上述所有知识点串联起来,我们构想一个简单的实战项目:一个支持在PC、手机和浏览器上运行的Unity实时聊天室。
1. 服务端(Node.js + ws库示例)
// server.js const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', (ws) => { console.log('新客户端连接'); // 广播消息给所有连接的客户端 ws.on('message', (message) => { console.log('收到消息: %s', message); wss.clients.forEach((client) => { if (client !== ws && client.readyState === WebSocket.OPEN) { client.send(message); } }); }); ws.on('close', () => { console.log('客户端断开连接'); }); });2. Unity客户端核心逻辑我们将创建一个ChatClient脚本,负责UI交互和网络通信。
using NativeWebSocket; using UnityEngine; using UnityEngine.UI; using System.Collections.Generic; public class ChatClient : MonoBehaviour { public InputField serverInput; public InputField usernameInput; public InputField messageInput; public Button connectBtn; public Button sendBtn; public Text chatLogText; public GameObject loginPanel; public GameObject chatPanel; private WebSocket ws; private string username; private void Start() { connectBtn.onClick.AddListener(OnConnectClicked); sendBtn.onClick.AddListener(OnSendClicked); // 加载保存的用户名 usernameInput.text = PlayerPrefs.GetString("ChatUsername", "Player_" + Random.Range(1000, 9999)); } private async void OnConnectClicked() { string serverUrl = serverInput.text.Trim(); username = usernameInput.text.Trim(); if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(username)) { AppendLog("服务器地址和用户名不能为空!"); return; } PlayerPrefs.SetString("ChatUsername", username); connectBtn.interactable = false; ws = new WebSocket(serverUrl); ws.OnOpen += () => { AppendLog($"已连接到服务器: {serverUrl}"); // 切换UI UnityMainThreadDispatcher.Instance.Enqueue(() => { loginPanel.SetActive(false); chatPanel.SetActive(true); messageInput.Select(); }); // 发送加入聊天室的通知 SendChatMessage($"{username} 进入了聊天室。"); }; ws.OnError += (err) => AppendLog($"错误: {err}"); ws.OnClose += (code) => { AppendLog("连接已关闭"); UnityMainThreadDispatcher.Instance.Enqueue(() => { chatPanel.SetActive(false); loginPanel.SetActive(true); connectBtn.interactable = true; }); }; ws.OnMessage += (bytes) => { string msg = System.Text.Encoding.UTF8.GetString(bytes); UnityMainThreadDispatcher.Instance.Enqueue(() => AppendLog(msg)); }; try { await ws.Connect(); } catch (System.Exception e) { AppendLog($"连接失败: {e.Message}"); connectBtn.interactable = true; } } private async void OnSendClicked() { string text = messageInput.text.Trim(); if (string.IsNullOrEmpty(text) || ws?.State != WebSocketState.Open) return; await SendChatMessage($"{username}: {text}"); messageInput.text = ""; messageInput.Select(); } private async Task SendChatMessage(string fullMessage) { if (ws.State == WebSocketState.Open) { await ws.SendText(fullMessage); } } private void AppendLog(string message) { chatLogText.text += $"\n[{System.DateTime.Now:HH:mm:ss}] {message}"; // 可选:自动滚动到底部 } void Update() { #if !UNITY_WEBGL || UNITY_EDITOR ws?.DispatchMessageQueue(); #endif // 处理回车键发送 if (chatPanel.activeSelf && Input.GetKeyDown(KeyCode.Return) && !string.IsNullOrEmpty(messageInput.text)) { OnSendClicked(); } } private async void OnApplicationQuit() { if (ws != null && ws.State == WebSocketState.Open) { await ws.Close(); } } }3. 项目构建与测试
- 将上述脚本挂载到Unity场景中的一个GameObject上,并配置好对应的UI组件引用。
- 运行Node.js服务端:
node server.js。 - 在Unity编辑器中运行,输入服务器地址(如
ws://localhost:8080)和用户名,点击连接。 - 发送消息,观察聊天日志。打开多个客户端实例(或构建到不同平台),可以看到消息广播。
- 分别构建Windows、Android、iOS(需配置证书和描述文件)、WebGL版本,在不同设备上测试连接和聊天功能。
通过这个完整的小项目,你可以亲身体验到NativeWebSocket如何以几乎零平台差异的代码,实现一套功能在多个终端上运行。这其中的关键,就在于它为我们妥善处理了底层的所有复杂性。