1. 项目概述与核心痛点
在Unity项目中集成实时网络通信,WebSocket几乎是绕不开的技术选型。无论是做多人在线游戏、实时数据看板,还是需要服务端主动推送消息的各类应用,WebSocket的双向、低延迟特性都让它成为首选。然而,从GitHub上找一个UnityWebSocket的插件或自己封装一个原生System.Net.WebSockets,到真正稳定、可靠地跑在项目里,这中间的路往往布满了“坑”。我自己在多个商业项目中趟过这些雷,从连接莫名断开、消息乱序,到移动端上的性能陷阱和内存泄漏,几乎把能踩的坑都踩了一遍。这篇文章,我就把这些年积累下来的、关于UnityWebSocket最常见问题的解决方案,掰开揉碎了讲给你听。这不是一份API文档,而是一份实战排雷手册,目标是让你在集成WebSocket时,能提前避开那些让项目延期、让头发减少的“暗礁”。
2. 连接建立与生命周期管理
2.1 连接失败与重连策略
连接都建立不起来,后面的一切都无从谈起。连接失败的原因五花八门,但最常见的无非几种:网络不可用、服务器地址/端口错误、SSL证书问题、或服务器未就绪。
首先,别在Start()或Awake()里直接进行连接操作。游戏启动时,资源加载、场景初始化可能占用大量资源,此时发起网络连接容易失败或阻塞主线程。我习惯在Start()方法中延迟几帧,或者监听一个明确的“游戏准备就绪”事件后再发起连接。对于连接地址,务必做好校验和容错。不要硬编码,而是通过配置表或服务器下发的地址来连接。对于WebSocket Secure (WSS),Unity的证书处理有时会比较棘手,尤其是在某些Android设备上。如果使用自签名证书进行测试,你可能需要实现一个自定义的证书验证回调来接受所有证书(仅限测试环境!),否则连接会因证书无效而失败。
重连策略是保障服务可用的核心。一个简单的指数退避重连算法是必备的。不要连接一失败就立刻无限重试,这会给服务器造成压力,也可能快速耗尽客户端电量。我的常用策略是:第一次失败后等待1秒重试,第二次失败后等待2秒,第三次4秒,以此类推,直到达到一个最大等待时间(比如30秒)。同时,需要设置一个最大重试次数,超过后应通知用户检查网络或服务器状态。重连逻辑必须放在独立的协程或异步任务中,并确保在连接成功后被正确取消,避免多个重连协程同时运行。
private async void ConnectWithRetryAsync() { int retryCount = 0; int maxRetry = 5; while (!_webSocket.IsConnected && retryCount < maxRetry) { try { await _webSocket.ConnectAsync(); // 连接成功,重置重试计数 retryCount = 0; OnConnected?.Invoke(); return; } catch (Exception ex) { retryCount++; Debug.LogWarning($"连接失败,第{retryCount}次重试。错误:{ex.Message}"); if (retryCount >= maxRetry) { Debug.LogError("达到最大重试次数,连接失败。"); OnConnectionFailed?.Invoke(); break; } // 指数退避等待 int delay = Mathf.Min(30, (int)Mathf.Pow(2, retryCount)); await Task.Delay(delay * 1000); } } }2.2 连接状态维护与心跳机制
WebSocket连接建立后,并非一劳永逸。网络波动、服务器重启、中间件超时都可能导致连接在客户端不知情的情况下变为“死连接”。客户端认为连接还在,但实际已经断开了,这时发送消息会失败。
因此,维护一个准确的内置连接状态至关重要。不要完全依赖第三方库提供的IsConnected属性,有时它并不可靠。我通常会自己封装一层状态管理,包含Connecting、Connected、Disconnecting、Disconnected和Reconnecting等状态,并通过状态机来管理状态转换,确保业务逻辑在正确的状态下执行。
心跳机制(Heartbeat/Ping-Pong)是检测死连接的最有效手段。原理很简单:客户端定期(比如每30秒)向服务器发送一个特定的Ping消息,服务器收到后立即回复一个Pong消息。如果客户端在预定时间内(比如60秒)没有收到任何Pong回复,就可以判定连接已失效,主动触发重连。
WebSocket协议本身有标准的Ping/Pong帧,但有些服务器实现可能不支持或不规范。更通用的做法是在应用层定义自己的心跳协议,比如发送一个{“cmd”: “ping”}的JSON消息,服务器回复{“cmd”: “pong”}。实现时,需要用一个独立的协程或定时器来发送心跳,并记录最后一次收到消息(无论是心跳回复还是业务消息)的时间。在Update循环或另一个定时器中检查,如果当前时间与最后一次收到消息的时间差超过阈值,则判定超时。
注意:心跳间隔不宜过短,否则会增加不必要的流量和服务器负担;也不宜过长,否则无法及时检测到连接断开。根据应用场景,30-60秒是一个常见的区间。移动网络下,可以考虑根据网络类型动态调整间隔。
3. 消息处理与数据序列化
3.1 消息粘包、拆包与完整性保障
WebSocket协议是基于帧(Frame)的,理论上一个消息可能被分成多个帧发送,也可能多个小消息被合并到一个帧里。虽然底层库通常会处理好帧的组装,但在处理高速消息流时,我们依然要面对“粘包”问题:即一次接收到的数据缓冲区里,可能包含了不止一个完整的应用层消息。
例如,服务器快速发送了两条JSON消息:{"id":1}和{"id":2}。客户端在一次OnMessage回调中,收到的数据可能是{"id":1}{"id":2},这会导致JSON解析失败。解决方案是定义明确的消息边界。常见的方法有:
- 长度前缀法:在每个消息前加上固定字节(如4字节的int)表示消息体的长度。接收方先读取长度,再读取指定字节数的内容作为一个完整消息。
- 分隔符法:用一个特殊的字符(如换行符
\n)作为消息结束标记。这种方法对文本协议简单有效,但要确保消息内容本身不包含分隔符。 - 自描述协议:如JSON、Protobuf,消息本身有结束标记(如JSON的
}),但需要流式解析器来正确处理。对于JSON,可以使用JsonTextReader等支持流式读取的解析器。
在Unity中,如果使用MemoryStream或byte[]接收数据,我强烈推荐使用长度前缀法。它的处理逻辑清晰,性能也好。下面是一个简单的处理示例:
private List<byte> _messageBuffer = new List<byte>(); private int _expectedMessageLength = -1; private void ProcessRawData(byte[] data) { _messageBuffer.AddRange(data); while (_messageBuffer.Count > 0) { // 如果还不知道消息长度,尝试读取长度头(假设为4字节int) if (_expectedMessageLength < 0 && _messageBuffer.Count >= 4) { _expectedMessageLength = BitConverter.ToInt32(_messageBuffer.ToArray(), 0); _messageBuffer.RemoveRange(0, 4); // 移除长度头 } // 如果已知长度,并且缓冲区数据足够 if (_expectedMessageLength > 0 && _messageBuffer.Count >= _expectedMessageLength) { byte[] completeMessage = _messageBuffer.GetRange(0, _expectedMessageLength).ToArray(); _messageBuffer.RemoveRange(0, _expectedMessageLength); _expectedMessageLength = -1; // 重置,准备读取下一条消息 // 处理完整的消息 completeMessage OnCompleteMessageReceived(completeMessage); } else { // 数据还不够,等待下次接收 break; } } }3.2 序列化方案选择与性能考量
消息的序列化(编码)与反序列化(解码)直接影响到网络传输效率和CPU开销。在Unity中,常见的选择有JSON、Protobuf、MessagePack和FlatBuffers。
- JSON (Newtonsoft.Json/Unity内置JsonUtility):人类可读,开发调试方便,与Web前端互通性好。但体积大,序列化/反序列化速度相对慢。
JsonUtility性能优于Newtonsoft.Json,但不支持复杂类型(如字典、多态)。对于小规模、非性能关键的实时消息,JSON是快速上手的选择。 - Protobuf (Google.Protobuf):二进制协议,体积小,序列化速度快,跨语言支持好。需要预先定义
.proto文件并生成C#代码,灵活性稍差。适合消息格式固定、对性能和流量敏感的项目。 - MessagePack-CSharp:类似于JSON的二进制序列化框架,号称比Protobuf更快,且通常无需预编译(虽然也支持AOT生成)。API友好,可以直接序列化现有的C#类。在Unity中性能表现优异,是我目前最常用的方案。
- FlatBuffers:最大的特点是反序列化时无需解析,直接访问内存中的偏移量即可读取数据,速度极快,内存效率高。但API较为复杂,数据结构需要严格定义。适合对性能有极致要求的场景,如高频更新的游戏状态同步。
选择建议:初期快速原型用JSON(配合JsonUtility)。进入性能优化阶段,如果消息结构复杂且变化不频繁,考虑Protobuf。如果追求开发效率和性能的平衡,MessagePack是绝佳选择。对于UI数据更新、聊天消息等,MessagePack的性能提升是立竿见影的。
实操心得:无论选择哪种序列化,一定要做压力测试。在编辑器和目标真机(尤其是低端安卓机)上,模拟每秒几十上百条消息的收发,用Profiler查看CPU的
GC Alloc(垃圾回收分配)。不合理的序列化会产生大量临时字节数组和字符串,引发频繁GC,导致游戏卡顿。MessagePack和Protobuf在这方面通常表现远好于JSON。
4. 多线程、Unity主线程与事件派发
4.1 网络线程与主线程的通信壁垒
这是Unity WebSocket开发中最容易引发诡异Bug的领域。绝大多数WebSocket库(如System.Net.WebSockets)的回调(OnMessage,OnError,OnClose)都是在后台线程触发的。而Unity的绝大多数API(如GameObject的创建销毁、Transform操作、UI更新、Debug.Log)都必须在主线程中执行。
如果你在后台线程的回调里直接去设置一个Text组件的文本,运气好时可能不报错,但绝大多数情况下会导致崩溃、UI无响应或难以调试的异常。解决方案是必须将网络层接收到的事件“派发”到Unity的主线程队列中执行。
4.2 安全的事件派发机制实现
实现一个线程安全的主线程调度器是标配。它的核心是一个并发队列(如ConcurrentQueue<Action>),网络回调将需要主线程执行的操作(以Action或自定义委托形式)入队。在Unity主线程的Update()循环中,从这个队列里取出并依次执行这些操作。
using System.Collections.Concurrent; using UnityEngine; public class MainThreadDispatcher : MonoBehaviour { private static readonly ConcurrentQueue<Action> _executionQueue = new ConcurrentQueue<Action>(); private static MainThreadDispatcher _instance; public static MainThreadDispatcher Instance { get { if (_instance == null) { var go = new GameObject("MainThreadDispatcher"); _instance = go.AddComponent<MainThreadDispatcher>(); DontDestroyOnLoad(go); } return _instance; } } public void Enqueue(Action action) { _executionQueue.Enqueue(action); } void Update() { while (_executionQueue.TryDequeue(out var action)) { action?.Invoke(); } } }在你的WebSocket管理类中,这样使用它:
private void OnWebSocketMessageReceived(byte[] data) { // 这个回调在后台线程 var message = MessagePackSerializer.Deserialize<MyMessage>(data); // 将UI更新操作派发到主线程 MainThreadDispatcher.Instance.Enqueue(() => { // 现在在主线程了,可以安全操作UI statusText.text = $"收到消息: {message.Content}"; // 也可以触发UnityEvent,其他MonoBehaviour能安全响应 OnMessageReceived?.Invoke(message); }); }重要提示:确保
MainThreadDispatcher的GameObject在场景中不会被意外销毁。通常将其放在一个启动场景并标记为DontDestroyOnLoad。此外,当游戏退出或场景切换时,注意清空队列,避免执行无效或已销毁对象上的操作。
5. 资源释放、内存管理与连接关闭
5.1 连接关闭的完整生命周期
不正确地关闭WebSocket连接是内存泄漏和资源悬挂的常见原因。关闭连接不是简单地调用一个Close方法就完了,你需要一个清晰的流程。
主动关闭流程:
- 业务逻辑触发关闭(如用户退出游戏、切换场景)。
- 调用WebSocket的
CloseAsync方法,发送关闭帧给服务器。务必使用带有超时参数的CloseAsync,避免网络不佳时无限等待。 - 等待
CloseAsync完成,或等待OnClose回调被触发。 - 在
OnClose回调中,执行资源清理:取消所有订阅的事件、停止心跳协程、释放缓冲区、将内部状态置为Disconnected。 - 最后,如果WebSocket对象实现了
IDisposable,调用Dispose()。
被动关闭处理(服务器或网络断开):
OnError或OnClose回调会被触发。- 同样执行上述第4步的资源清理工作。
- 根据错误码判断是否需要进行重连(例如,非正常的关闭码1006可能需要重连)。
public async Task CloseConnectionAsync() { if (_webSocket.State != WebSocketState.Open) { return; } _isManualClosing = true; // 标记是主动关闭,避免触发重连逻辑 try { // 发送关闭帧,并等待最多3秒 await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client closing", CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(3)); } catch (Exception ex) { Debug.LogWarning($"关闭连接时发生异常: {ex.Message}"); // 即使关闭失败,也强制清理本地资源 } finally { CleanupResources(); _webSocket.Dispose(); _webSocket = null; } } private void OnWebSocketClosed(WebSocketCloseStatus closeStatus, string reason) { // 无论主动被动关闭,都会进入这里 CleanupResources(); if (!_isManualClosing && closeStatus != WebSocketCloseStatus.NormalClosure) { // 非主动的正常关闭,触发重连 StartReconnection(); } }5.2 预防内存泄漏的要点
除了连接对象本身,还要注意以下几点:
- 事件订阅:如果你的WebSocket管理器使用了C#事件,确保在关闭时将所有从外部订阅的事件处理器(
+=)取消订阅(-=)。否则,持有该管理器引用的对象将无法被GC回收。 - 协程与定时器:启动的心跳协程、超时检测的
Timer,必须在连接关闭时用StopCoroutine和Dispose()正确停止。 - 缓冲区:用于组包的大容量
byte[]或List<byte>缓冲区,在连接关闭后应置为null或调用Clear(),以便GC回收。 - 静态引用:避免在静态类或单例中持有对游戏对象或场景特定对象的长期引用。如果必须引用,使用
WeakReference。
6. 平台特异性问题与优化
6.1 移动端(iOS/Android)的注意事项
移动端环境比PC复杂得多,需要特别关注。
- 后台连接保持:当App切换到后台,默认情况下,iOS会很快挂起所有线程,包括网络线程,导致连接断开。Android上情况类似,但策略更宽松一些。如果需要后台保持连接(如即时通讯应用),需要配置相应的后台模式(iOS的
VoIP、Background Fetch等能力,并在Info.plist中声明)和Android的Foreground Service。这是一项复杂的功能,需要仔细阅读平台文档并权衡电量消耗。 - 网络状态监听:移动网络(4G/5G/Wi-Fi切换)不稳定。必须监听系统的网络状态变化事件(Unity可使用
Application.internetReachability,但更推荐使用UnityEngine.NetworkReachability结合平台原生API)。当检测到网络从无到有,应主动尝试重连。 - 电量与性能:频繁的心跳、重试会消耗电量。在移动端,可以考虑适当延长心跳间隔(如60秒),并使用更高效的数据序列化格式(如MessagePack)来减少CPU工作量和数据流量。
- IL2CPP与代码裁剪:如果使用Protobuf等依赖反射的序列化库,在IL2CPP编译和代码裁剪(Code Stripping)时,可能会因为反射调用被裁剪而导致运行时错误。需要在
link.xml文件中添加必要的类型保留声明,或者使用预代码生成(AOT)版本的序列化库。
6.2 WebGL平台的限制与变通方案
Unity WebGL不支持标准的System.Net.WebSockets。你必须使用基于浏览器原生WebSocket对象的方案。通常,第三方插件(如NativeWebSocket、BestHTTP的WebGL版本)已经处理好了这层封装。但需要注意:
- 线程模型:WebGL是单线程的,没有真正的多线程。所有网络回调都会在主线程执行,因此前面提到的多线程派发问题在WebGL上不存在,但也要注意不要让耗时的消息处理阻塞主线程。
- 同步调用:避免在WebGL上使用同步的
Receive调用,这会导致主线程阻塞,页面“卡死”。务必使用异步API。 - 大小限制:浏览器对WebSocket消息大小可能有隐式限制。虽然协议支持分帧传输大消息,但为了兼容性,建议将单个应用层消息控制在合理大小(例如几十KB以内),对于更大的数据(如文件),应分片发送。
7. 调试、监控与性能分析
7.1 有效的日志与调试信息
“我的消息发出去为什么没反应?” 没有完善的日志,调试网络问题如同盲人摸象。你需要一个分级的日志系统,在开发阶段输出详尽信息,在生产环境关闭或仅记录错误。
关键日志点:
- 连接生命周期:开始连接、连接成功、连接失败(附带错误码和原因)、连接关闭(附带关闭码和原因)、重连开始。
- 消息流量:发送和接收的每条消息(至少记录消息类型或ID,生产环境可关闭内容日志)。可以记录消息大小,用于监控流量。
- 心跳:发送Ping和收到Pong的时间戳,用于计算延迟和诊断超时。
- 线程信息:在日志中输出当前线程ID或名称,帮助判断是否在主线程。
建议使用条件编译#if UNITY_EDITOR或自定义的日志级别来控制输出。
7.2 性能监控关键指标
在Profiler中,你需要重点关注:
- CPU开销:
Update中网络逻辑(如派发消息、心跳检查)的耗时。消息反序列化(尤其是JSON)可能产生峰值。 - GC Alloc:这是重中之重。每次消息收发、字符串创建、字节数组拼接都可能产生垃圾。用Profiler的Deep Profile模式,定位分配大户。优化方向包括:使用对象池复用
byte[]缓冲区、采用零分配或低分配的序列化库、避免在热路径中拼接字符串。 - 内存:监控WebSocket管理器及其缓冲区的内存占用是否平稳,有无持续增长(内存泄漏)。
可以编写一个简单的运行时监控UI,显示:连接状态、延迟(通过心跳计算)、每秒收发消息数、总流量、当前缓冲区大小等。这些信息对线上问题排查极具价值。
8. 第三方库选型与封装建议
8.1 常见库对比与选择
不建议从零开始用System.Net.WebSockets封装,除非有极致的定制需求。选择一个成熟稳定的第三方库是更高效的做法。以下是我对几个流行库的简要评价:
| 库名称 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| NativeWebSocket | 纯C#实现,API简洁,支持多平台(包括WebGL),活跃维护。 | 功能相对基础,高级特性需自己实现。 | 需要支持WebGL的轻量级项目,快速上手。 |
| BestHTTP/HTTPS | 功能极其强大,不仅WebSocket,HTTP/2、SignalR等都支持,稳定可靠。 | 商业收费(有免费版但功能受限),库体积较大,API稍复杂。 | 企业级项目,需要一站式网络解决方案,且预算充足。 |
| WebSocketSharp | 老牌库,功能全面。 | 在Unity中可能有一些兼容性问题,维护活跃度一般。 | 传统.NET项目迁移,或对其特性有特定需求。 |
| System.Net.WebSockets | 官方,无需额外依赖。 | 在Unity旧版本或某些平台支持不完善,需要自己处理多线程和生命周期。 | 对依赖数量有严格限制,且愿意投入时间封装和踩坑的项目。 |
我个人在大多数项目中首选NativeWebSocket(对于WebGL必须)或BestHTTP(对于功能复杂的PC/移动端项目)。
8.2 设计一个健壮的封装层
无论选择哪个库,都建议在其之上再封装一层应用层的网络管理器。这个管理器负责:
- 统一接口:对外提供
Connect,Send,Close,RegisterHandler等稳定接口,隐藏底层库的差异。 - 生命周期管理:集成连接、重连、心跳、超时控制。
- 线程安全派发:集成主线程派发器,让业务层无需关心线程问题。
- 消息路由:根据消息ID或类型,将消息自动分发给注册的处理函数。这比用一个大
switch语句清晰得多。 - 状态管理:提供清晰的连接状态枚举和事件(
OnConnected,OnDisconnected,OnMessage等)。 - 配置化:将服务器地址、重试策略、心跳间隔等参数做成可配置的。
这样的封装,使得业务代码(如UI控制器、游戏逻辑)能够以安全、简单的方式使用网络功能,底层库的更换也不会波及上层业务。这是架构上值得投入的前期工作。
9. 高级场景与疑难杂症
9.1 大规模消息处理与流量控制
当消息频率非常高时(如实时竞技游戏的帧同步),简单的“来一条处理一条”可能会压垮主线程。解决方案是消息队列与消费速率控制。
在网络管理器的内部,维护一个接收队列。后台线程将收到的完整消息对象(反序列化后的)放入队列。在主线程的Update中,不是一次性处理完所有消息,而是每帧只处理固定数量(例如10条)的消息,剩下的留到下一帧。这可以平滑CPU占用,避免帧率骤降。
对于发送端,如果业务逻辑可能在一帧内触发大量发送请求(如多个玩家同时开枪),也可以做一个发送队列和节流机制,避免短时间向网络堆栈注入过多数据包。
9.2 断线重连后的状态同步
这是实时应用的核心难题。连接断开又恢复后,客户端和服务器状态可能已经不一致。简单的重连后,服务器需要将关键状态(如玩家位置、血量、游戏阶段)重新同步给客户端。
常见的策略是“全量同步”和“增量同步+快照”:
- 全量同步:重连成功后,服务器将客户端所控角色及周围关键实体的完整状态数据打包发送。实现简单,但数据量可能较大。
- 增量同步+快照:服务器定期(如每秒)保存一份完整的游戏世界快照。客户端重连时,发送其最后确认收到的指令ID,服务器计算出从那个时间点到当前快照之间所有相关的状态变化,发送给客户端。更复杂,但数据量更优。
你需要和服务器端约定好重连协议。客户端在连接建立后,发送一个“重连认证”消息,携带上次会话的令牌或最后收到的消息ID。服务器据此进行状态同步。
9.3 错误码解析与应对
WebSocket关闭时会有一个关闭码(Close Status Code)。理解这些代码有助于快速定位问题。
- 1000 (Normal Closure):正常关闭。通常是客户端或服务器主动调用关闭方法。
- 1001 (Endpoint Going Away):端点“离开”,例如服务器进程崩溃或重启。
- 1006 (Connection Abnormally Closed):最常见的异常关闭码。通常表示底层TCP连接异常断开(如网络突然中断、防火墙杀连接),而WebSocket协议层没有收到正式的关闭帧。遇到此码,客户端应尝试重连。
- 1009 (Message Too Big):消息太大,超过了服务器或中间件设置的最大帧大小。需要检查发送的消息尺寸,或在服务器端调整配置(如Spring Boot的
setMaxTextMessageBufferSize)。 - 1011 (Internal Server Error):服务器内部错误。需要联系服务器端开发查看日志。
- 1015 (TLS Handshake Failure):SSL/TLS握手失败。检查证书有效性、域名是否匹配等。
在你的网络管理器中,应该将这些常见的错误码进行分类处理,例如将1006归为“网络异常需重连”,将1009归为“客户端数据错误需检查”,将1011归为“服务器错误需上报”。
10. 实战:构建一个简易但健壮的UnityWebSocket管理器
结合以上所有要点,我们来勾勒一个简易但健壮的管理器核心框架。这个框架不使用任何特定第三方库的API,只展示设计思路和关键代码片段。
using System; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using UnityEngine; public enum ConnectionState { Disconnected, Connecting, Connected, Reconnecting } public class RobustWebSocketManager : MonoBehaviour { // 配置 public string serverUrl = "ws://localhost:8080"; public int heartbeatInterval = 30; public ReconnectPolicy reconnectPolicy; // 状态与事件 public ConnectionState State { get; private set; } public event Action OnConnected; public event Action<string> OnDisconnected; public event Action<byte[]> OnDataReceived; // 内部组件 private IWebSocketClient _wsClient; // 底层库接口 private CancellationTokenSource _heartbeatCts; private CancellationTokenSource _connectionCts; private readonly ConcurrentQueue<Action> _mainThreadQueue = new ConcurrentQueue<Action>(); private DateTime _lastMessageTime; private bool _isManualClose; void Start() => DontDestroyOnLoad(gameObject); void Update() => DrainMainThreadQueue(); public async void Connect() { if (State != ConnectionState.Disconnected) return; SetState(ConnectionState.Connecting); _isManualClose = false; _connectionCts = new CancellationTokenSource(); await EstablishConnectionWithRetry(_connectionCts.Token); } private async Task EstablishConnectionWithRetry(CancellationToken ct) { int attempt = 0; while (!ct.IsCancellationRequested) { try { _wsClient = CreateWebSocketClient(); // 工厂方法创建具体实现 await _wsClient.ConnectAsync(serverUrl); OnSocketConnected(); return; } catch (Exception e) { attempt++; Debug.Log($"连接尝试{attempt}失败: {e.Message}"); if (attempt >= reconnectPolicy.maxAttempts) break; await Task.Delay(CalculateBackoffDelay(attempt), ct); } } SetState(ConnectionState.Disconnected); EnqueueToMainThread(() => OnDisconnected?.Invoke("Max retries exceeded")); } private void OnSocketConnected() { _lastMessageTime = DateTime.UtcNow; SetState(ConnectionState.Connected); StartHeartbeat(); EnqueueToMainThread(() => OnConnected?.Invoke()); _ = Task.Run(ListenForMessages); // 开始监听消息 } private async void ListenForMessages() { var buffer = new byte[4096]; while (State == ConnectionState.Connected && _wsClient?.IsConnected == true) { try { var result = await _wsClient.ReceiveAsync(buffer, CancellationToken.None); if (result.MessageType == WebSocketMessageType.Close) { HandleClose(result.CloseStatus, result.CloseDescription); break; } _lastMessageTime = DateTime.UtcNow; ProcessReceivedData(buffer, result.Count); } catch (Exception e) { Debug.LogError($"接收消息异常: {e}"); HandleClose(null, e.Message); break; } } } private void ProcessReceivedData(byte[] data, int length) { // 这里应调用你的消息组包逻辑(如2.1节所述) // 假设组包后得到完整消息 completeMessage byte[] completeMessage = YourMessageDeframer.Deframe(data, length); if (completeMessage != null) { EnqueueToMainThread(() => OnDataReceived?.Invoke(completeMessage)); } } private void StartHeartbeat() { _heartbeatCts = new CancellationTokenSource(); Task.Run(async () => { while (!_heartbeatCts.Token.IsCancellationRequested && State == ConnectionState.Connected) { await Task.Delay(heartbeatInterval * 1000, _heartbeatCts.Token); if ((DateTime.UtcNow - _lastMessageTime).TotalSeconds > heartbeatInterval * 2) { Debug.LogWarning("心跳超时,连接可能已死"); HandleClose(null, "Heartbeat timeout"); break; } await SendPingAsync(); } }, _heartbeatCts.Token); } private void HandleClose(WebSocketCloseStatus? code, string reason) { StopHeartbeat(); CleanupWebSocketClient(); SetState(ConnectionState.Disconnected); string disconnectReason = code?.ToString() ?? reason; EnqueueToMainThread(() => OnDisconnected?.Invoke(disconnectReason)); if (!_isManualClose && code != WebSocketCloseStatus.NormalClosure) { SetState(ConnectionState.Reconnecting); _ = Task.Run(() => EstablishConnectionWithRetry(CancellationToken.None)); } } public async void SendMessage(byte[] data) { if (State != ConnectionState.Connected) return; try { await _wsClient.SendAsync(data); } catch (Exception e) { Debug.LogError($"发送失败: {e}"); } } private void EnqueueToMainThread(Action action) => _mainThreadQueue.Enqueue(action); private void DrainMainThreadQueue() { while (_mainThreadQueue.TryDequeue(out var action)) action?.Invoke(); } private void SetState(ConnectionState newState) => State = newState; // ... 其他辅助方法:StopHeartbeat, CleanupWebSocketClient, SendPingAsync, CalculateBackoffDelay 等 } // 配置类 [System.Serializable] public class ReconnectPolicy { public int maxAttempts = 5; public int baseDelay = 1; // 秒 public int maxDelay = 30; // 秒 }这个管理器集成了状态管理、自动重连、心跳检测、主线程派发和简易的消息接收框架。你可以根据选择的底层库实现IWebSocketClient接口,并将消息组包逻辑YourMessageDeframer补充完整,它就能成为一个可靠的网络通信基石。