Unity WebSocket实战:连接管理、消息处理与多线程通信全解析

📅 2026/8/3 18:41:17 👁️ 阅读次数 📝 编程学习
Unity WebSocket实战:连接管理、消息处理与多线程通信全解析

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属性,有时它并不可靠。我通常会自己封装一层状态管理,包含ConnectingConnectedDisconnectingDisconnectedReconnecting等状态,并通过状态机来管理状态转换,确保业务逻辑在正确的状态下执行。

心跳机制(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解析失败。解决方案是定义明确的消息边界。常见的方法有:

  1. 长度前缀法:在每个消息前加上固定字节(如4字节的int)表示消息体的长度。接收方先读取长度,再读取指定字节数的内容作为一个完整消息。
  2. 分隔符法:用一个特殊的字符(如换行符\n)作为消息结束标记。这种方法对文本协议简单有效,但要确保消息内容本身不包含分隔符。
  3. 自描述协议:如JSON、Protobuf,消息本身有结束标记(如JSON的}),但需要流式解析器来正确处理。对于JSON,可以使用JsonTextReader等支持流式读取的解析器。

在Unity中,如果使用MemoryStreambyte[]接收数据,我强烈推荐使用长度前缀法。它的处理逻辑清晰,性能也好。下面是一个简单的处理示例:

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方法就完了,你需要一个清晰的流程。

主动关闭流程:

  1. 业务逻辑触发关闭(如用户退出游戏、切换场景)。
  2. 调用WebSocket的CloseAsync方法,发送关闭帧给服务器。务必使用带有超时参数的CloseAsync,避免网络不佳时无限等待。
  3. 等待CloseAsync完成,或等待OnClose回调被触发。
  4. OnClose回调中,执行资源清理:取消所有订阅的事件、停止心跳协程、释放缓冲区、将内部状态置为Disconnected
  5. 最后,如果WebSocket对象实现了IDisposable,调用Dispose()

被动关闭处理(服务器或网络断开):

  1. OnErrorOnClose回调会被触发。
  2. 同样执行上述第4步的资源清理工作。
  3. 根据错误码判断是否需要进行重连(例如,非正常的关闭码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,必须在连接关闭时用StopCoroutineDispose()正确停止。
  • 缓冲区:用于组包的大容量byte[]List<byte>缓冲区,在连接关闭后应置为null或调用Clear(),以便GC回收。
  • 静态引用:避免在静态类或单例中持有对游戏对象或场景特定对象的长期引用。如果必须引用,使用WeakReference

6. 平台特异性问题与优化

6.1 移动端(iOS/Android)的注意事项

移动端环境比PC复杂得多,需要特别关注。

  • 后台连接保持:当App切换到后台,默认情况下,iOS会很快挂起所有线程,包括网络线程,导致连接断开。Android上情况类似,但策略更宽松一些。如果需要后台保持连接(如即时通讯应用),需要配置相应的后台模式(iOS的VoIPBackground 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对象的方案。通常,第三方插件(如NativeWebSocketBestHTTP的WebGL版本)已经处理好了这层封装。但需要注意:

  • 线程模型:WebGL是单线程的,没有真正的多线程。所有网络回调都会在主线程执行,因此前面提到的多线程派发问题在WebGL上不存在,但也要注意不要让耗时的消息处理阻塞主线程。
  • 同步调用:避免在WebGL上使用同步的Receive调用,这会导致主线程阻塞,页面“卡死”。务必使用异步API。
  • 大小限制:浏览器对WebSocket消息大小可能有隐式限制。虽然协议支持分帧传输大消息,但为了兼容性,建议将单个应用层消息控制在合理大小(例如几十KB以内),对于更大的数据(如文件),应分片发送。

7. 调试、监控与性能分析

7.1 有效的日志与调试信息

“我的消息发出去为什么没反应?” 没有完善的日志,调试网络问题如同盲人摸象。你需要一个分级的日志系统,在开发阶段输出详尽信息,在生产环境关闭或仅记录错误。

关键日志点:

  1. 连接生命周期:开始连接、连接成功、连接失败(附带错误码和原因)、连接关闭(附带关闭码和原因)、重连开始。
  2. 消息流量:发送和接收的每条消息(至少记录消息类型或ID,生产环境可关闭内容日志)。可以记录消息大小,用于监控流量。
  3. 心跳:发送Ping和收到Pong的时间戳,用于计算延迟和诊断超时。
  4. 线程信息:在日志中输出当前线程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补充完整,它就能成为一个可靠的网络通信基石。