拓冰建站拓冰建站
首页 / 资讯中心 / 正文

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属性有时它并不可靠。我通常会自己封装一层状态管理包含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 Listbyte _messageBuffer new Listbyte(); 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 安全的事件派发机制实现实现一个线程安全的主线程调度器是标配。它的核心是一个并发队列如ConcurrentQueueAction网络回调将需要主线程执行的操作以Action或自定义委托形式入队。在Unity主线程的Update()循环中从这个队列里取出并依次执行这些操作。using System.Collections.Concurrent; using UnityEngine; public class MainThreadDispatcher : MonoBehaviour { private static readonly ConcurrentQueueAction _executionQueue new ConcurrentQueueAction(); private static MainThreadDispatcher _instance; public static MainThreadDispatcher Instance { get { if (_instance null) { var go new GameObject(MainThreadDispatcher); _instance go.AddComponentMainThreadDispatcher(); 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.DeserializeMyMessage(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[]或Listbyte缓冲区在连接关闭后应置为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功能极其强大不仅WebSocketHTTP/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 Actionstring OnDisconnected; public event Actionbyte[] OnDataReceived; // 内部组件 private IWebSocketClient _wsClient; // 底层库接口 private CancellationTokenSource _heartbeatCts; private CancellationTokenSource _connectionCts; private readonly ConcurrentQueueAction _mainThreadQueue new ConcurrentQueueAction(); 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补充完整它就能成为一个可靠的网络通信基石。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门