UE5 C++原生WebSocket客户端开发:从连接到JSON处理实战指南

📅 2026/7/31 15:53:20
UE5 C++原生WebSocket客户端开发:从连接到JSON处理实战指南
1. 项目概述为什么UE5需要原生WebSocket客户端在UE5项目中集成实时数据流比如从游戏服务器接收动态更新的玩家状态、从后端服务拉取实时排行榜、或者实现一个聊天系统WebSocket几乎是目前最主流的选择。相比于传统的HTTP轮询WebSocket提供了全双工、低延迟的持久连接。很多开发者可能会第一时间想到用蓝图Blueprint去调用一些插件或者用HTTP节点变通实现但对于需要高性能、稳定连接和复杂消息处理尤其是带JSON解析的核心系统用C从头构建一个原生的WebSocket客户端是更可靠、更可控的方案。我经历过好几个项目初期为了快用蓝图搭了个简易的HTTP轮询当在线人数上去、消息频率高的时候延迟和服务器压力立刻就成了问题。后来重构为C WebSocket不仅连接稳定了消息处理的性能也提升了一个数量级。这个实战流程就是把我踩过的坑和总结的最佳实践串起来从环境配置、连接建立、消息收发到最棘手的JSON数据解析与处理给你一个完整的、可复现的解决方案。无论你是想对接一个SpringBoot后端还是处理TVBox那样的JSON配置接口这套流程都能直接套用。2. 核心依赖配置与项目设置在UE5中直接用C写WebSocket我们主要依赖两个东西UE5内置的IWebSocket模块以及一个处理JSON的库。UE5本身对WebSocket的支持已经比较完善不需要额外安装第三方插件这省了不少事。2.1 启用必要的UE5模块首先你需要修改项目的.Build.cs文件确保引入了WebSockets和Json模块。很多新手会忘记这一步导致编译时一堆“未识别符号”的错误。打开你的项目源代码目录找到YourProjectName.Build.cs文件比如我的项目叫WebSocketDemo那就是WebSocketDemo.Build.cs。在PublicDependencyModuleNames这个数组里添加WebSockets和Json。using UnrealBuildTool; public class WebSocketDemo : ModuleRules { public WebSocketDemo(ReadOnlyTargetRules Target) : base(Target) { PCHUsage PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange(new string[] { Core, CoreUObject, Engine, InputCore, // 添加以下两个核心模块 WebSockets, Json, JsonUtilities // 为了更方便的JSON转换也建议加上 }); // ... 其他私有依赖等 } }注意JsonUtilities模块不是必须的但它提供了像FJsonObjectConverter这样非常便捷的工具类能将JSON直接与USTRUCT结构体相互转换强烈建议一并添加。修改完保存后在Visual Studio里右键点击你的.uproject文件选择“Generate Visual Studio project files”重新生成一下解决方案让更改生效。2.2 理解UE5的WebSocket接口UE5的WebSocket API主要位于IWebSocket接口中。我们通常不直接实现它而是通过FWebSocketsModule模块的工厂方法来创建WebSocket实例。核心的类/接口有IWebSocket 定义了连接、发送、接收、关闭等所有核心操作。FWebSocketsModule 用于获取WebSocket工厂。FModuleManager 用于加载WebSockets模块。在代码中你的基本操作流程将是加载模块 - 创建Socket实例 - 绑定事件委托OnConnected, OnMessage, OnError等 - 连接 - 收发数据。2.3 开发环境准备VSCode/Visual Studio对于C开发Visual Studio 2022是UE5官方推荐且兼容性最好的IDE。确保安装了“使用C的游戏开发”工作负载。如果你偏爱VSCode的轻量也可以配置为辅助编辑器但编译和调试重度依赖Visual Studio。一个常见的坑是Microsoft Visual C Redistributable版本问题。确保你的开发机和目标测试机安装了匹配的VC运行库否则打包后的游戏可能无法运行。通常安装Visual Studio 2022时会自动搞定开发环境但分发时需要注意。3. WebSocket客户端连接管理与事件驱动架构建立连接不仅仅是调用一个Connect函数那么简单。一个健壮的客户端需要考虑连接状态管理、自动重连、错误处理和事件驱动的消息处理流程。3.1 创建并连接WebSocket我们通常会创建一个类来封装WebSocket客户端的生命周期。下面是一个简单的AWebSocketClientActor的示例在BeginPlay时建立连接。首先在头文件.h中声明#pragma once #include CoreMinimal.h #include GameFramework/Actor.h #include IWebSocket.h // 核心WebSocket接口 #include WebSocketClientActor.generated.h UCLASS() class WEBSOCKETDEMO_API AWebSocketClientActor : public AActor { GENERATED_BODY() public: AWebSocketClientActor(); protected: virtual void BeginPlay() override; virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; // WebSocket实例的共享指针用于管理生命周期 TSharedPtrIWebSocket WebSocket; // 服务器地址可在编辑器细节面板中配置 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category WebSocket Config) FString ServerURL TEXT(ws://localhost:8080/ws); // 连接超时时间秒 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category WebSocket Config) float ConnectionTimeout 5.0f; private: // 内部连接函数 void ConnectToServer(); };在源文件.cpp中实现连接逻辑#include WebSocketClientActor.h #include WebSocketsModule.h // 需要包含此模块头文件 #include Modules/ModuleManager.h AWebSocketClientActor::AWebSocketClientActor() { PrimaryActorTick.bCanEverTick true; // 如果需要每帧检查可以开启Tick } void AWebSocketClientActor::BeginPlay() { Super::BeginPlay(); ConnectToServer(); } void AWebSocketClientActor::ConnectToServer() { // 确保WebSocket模块已加载 FModuleManager::LoadModuleCheckedFWebSocketsModule(TEXT(WebSockets)); // 创建WebSocket实例。第二个参数可以是协议字符串如“protocol1, protocol2”通常留空。 WebSocket FWebSocketsModule::Get().CreateWebSocket(ServerURL); // 绑定事件委托 if (WebSocket.IsValid()) { WebSocket-OnConnected().AddLambda([this]() - void { UE_LOG(LogTemp, Log, TEXT(WebSocket Connected to %s), *ServerURL); // 这里可以广播一个自定义事件通知蓝图或其他C类连接已建立 }); WebSocket-OnConnectionError().AddLambda([this](const FString Error) - void { UE_LOG(LogTemp, Error, TEXT(WebSocket Connection Error: %s), *Error); // 处理连接错误例如触发重连逻辑 }); WebSocket-OnClosed().AddLambda([this](int32 StatusCode, const FString Reason, bool bWasClean) - void { UE_LOG(LogTemp, Warning, TEXT(WebSocket Closed: %d - %s (Clean: %d)), StatusCode, *Reason, bWasClean); // 处理连接关闭 }); // 最重要的消息接收委托 WebSocket-OnMessage().AddLambda([this](const FString MessageString) - void { UE_LOG(LogTemp, Log, TEXT(Received Message: %s), *MessageString); // 在这里处理收到的字符串消息通常是JSON ProcessWebSocketMessage(MessageString); }); WebSocket-OnRawMessage().AddLambda([this](const void* Data, SIZE_T Size, SIZE_T BytesRemaining) - void { // 处理原始二进制数据如果你接收的是二进制协议如Protobuf // 对于JSON我们通常用OnMessage处理字符串即可 }); WebSocket-OnMessageSent().AddLambda([this](const FString MessageString) - void { UE_LOG(LogTemp, VeryVerbose, TEXT(Message Sent: %s), *MessageString); }); // 开始连接 UE_LOG(LogTemp, Log, TEXT(Attempting to connect to %s), *ServerURL); WebSocket-Connect(); } } void AWebSocketClientActor::EndPlay(const EEndPlayReason::Type EndPlayReason) { // 确保在Actor销毁或关卡结束时关闭连接 if (WebSocket.IsValid() WebSocket-IsConnected()) { WebSocket-Close(); } Super::EndPlay(EndPlayReason); }3.2 实现自动重连与心跳机制生产环境中的网络是不稳定的。一个健壮的客户端必须能处理断线重连。我们可以利用一个定时器FTimerHandle来实现简单的重连逻辑。在头文件中添加private: FTimerHandle ReconnectTimerHandle; int32 CurrentReconnectAttempt; int32 MaxReconnectAttempts; void StartReconnectTimer(); void AttemptReconnect();在源文件中补充实现AWebSocketClientActor::AWebSocketClientActor() { // ... 其他初始化 CurrentReconnectAttempt 0; MaxReconnectAttempts 5; // 最大重试次数 } void AWebSocketClientActor::ConnectToServer() { // ... 之前的连接和委托绑定代码 WebSocket-OnConnectionError().AddLambda([this](const FString Error) - void { UE_LOG(LogTemp, Error, TEXT(WebSocket Connection Error: %s), *Error); StartReconnectTimer(); }); WebSocket-OnClosed().AddLambda([this](int32 StatusCode, const FString Reason, bool bWasClean) - void { UE_LOG(LogTemp, Warning, TEXT(WebSocket Closed: %d - %s (Clean: %d)), StatusCode, *Reason, bWasClean); if (!bWasClean) // 非正常关闭尝试重连 { StartReconnectTimer(); } }); // ... 开始连接 } void AWebSocketClientActor::StartReconnectTimer() { if (CurrentReconnectAttempt MaxReconnectAttempts) { UE_LOG(LogTemp, Error, TEXT(Max reconnect attempts (%d) reached. Giving up.), MaxReconnectAttempts); return; } CurrentReconnectAttempt; float DelaySeconds FMath::Pow(2.0f, CurrentReconnectAttempt); // 指数退避2秒4秒8秒... UE_LOG(LogTemp, Warning, TEXT(Will attempt to reconnect in %.1f seconds (Attempt %d/%d)), DelaySeconds, CurrentReconnectAttempt, MaxReconnectAttempts); GetWorld()-GetTimerManager().SetTimer(ReconnectTimerHandle, this, AWebSocketClientActor::AttemptReconnect, DelaySeconds, false); } void AWebSocketClientActor::AttemptReconnect() { UE_LOG(LogTemp, Log, TEXT(Attempting reconnect...)); if (WebSocket.IsValid()) { // 先清理旧的连接 WebSocket-Close(); // 可以稍等片刻再创建新连接这里简单处理 WebSocket.Reset(); } // 重新创建并连接 ConnectToServer(); } void AWebSocketClientActor::EndPlay(const EEndPlayReason::Type EndPlayReason) { GetWorld()-GetTimerManager().ClearTimer(ReconnectTimerHandle); // 清理重连定时器 // ... 关闭连接 }实操心得重连逻辑要小心不要陷入死循环。我一般会设置一个最大重试次数并且使用“指数退避”策略来增加重连间隔避免在服务器短暂故障时疯狂重连加重服务器负担。连接成功后别忘了将CurrentReconnectAttempt重置为0。对于长连接心跳机制Heartbeat/Ping-Pong也很有用用于保持连接活跃和检测死连接。WebSocket协议本身有Ping/Pong帧但UE5的IWebSocket接口没有直接暴露。一个常见的做法是定时比如每30秒向服务器发送一个特定的心跳消息例如{type:ping}并期待一个{type:pong}的回复。如果在规定时间内没收到回复可以主动断开并触发重连。4. JSON消息的解析与构造实战WebSocket传输的 payload 绝大部分是JSON字符串。在UE5的C中处理JSON主要使用FJsonObject和相关工具类。这个过程比蓝图中的Parse JSON节点要底层一些但灵活性和性能更高。4.1 解析收到的JSON消息当OnMessage委托触发时我们收到一个FString。第一步是将其解析为TSharedPtrFJsonObject。void AWebSocketClientActor::ProcessWebSocketMessage(const FString MessageString) { TSharedPtrFJsonObject JsonObject; TSharedRefTJsonReader JsonReader TJsonReaderFactory::Create(MessageString); if (FJsonSerializer::Deserialize(JsonReader, JsonObject) JsonObject.IsValid()) { // 解析成功开始提取数据 ParseJsonMessage(JsonObject); } else { UE_LOG(LogTemp, Error, TEXT(Failed to parse JSON message: %s), *MessageString); } } void AWebSocketClientActor::ParseJsonMessage(const TSharedPtrFJsonObject JsonObject) { // 1. 读取基本类型字段 FString MessageType; if (JsonObject-TryGetStringField(TEXT(type), MessageType)) { UE_LOG(LogTemp, Log, TEXT(Message type: %s), *MessageType); // 根据消息类型分发处理 if (MessageType.Equals(TEXT(playerState), ESearchCase::IgnoreCase)) { HandlePlayerStateMessage(JsonObject); } else if (MessageType.Equals(TEXT(chat), ESearchCase::IgnoreCase)) { HandleChatMessage(JsonObject); } // ... 其他消息类型 } // 2. 读取数字和布尔值 int32 PlayerScore 0; if (JsonObject-TryGetNumberField(TEXT(score), PlayerScore)) { // 使用PlayerScore } bool bIsReady false; if (JsonObject-TryGetBoolField(TEXT(isReady), bIsReady)) { // 使用bIsReady } // 3. 读取嵌套对象 const TSharedPtrFJsonObject* PlayerDataObject nullptr; if (JsonObject-TryGetObjectField(TEXT(playerData), PlayerDataObject)) { FString PlayerName; int32 PlayerLevel 0; (*PlayerDataObject)-TryGetStringField(TEXT(name), PlayerName); (*PlayerDataObject)-TryGetNumberField(TEXT(level), PlayerLevel); // 处理嵌套数据... } // 4. 读取数组 const TArrayTSharedPtrFJsonValue* ItemsArray nullptr; if (JsonObject-TryGetArrayField(TEXT(items), ItemsArray)) { for (const TSharedPtrFJsonValue ItemValue : *ItemsArray) { if (ItemValue-Type EJson::String) { FString ItemName ItemValue-AsString(); // 处理字符串数组项... } else if (ItemValue-Type EJson::Object) { TSharedPtrFJsonObject ItemObject ItemValue-AsObject(); // 处理对象数组项... } } } }4.2 使用FJsonObjectConverter处理复杂结构手动解析每个字段在数据结构复杂时会非常繁琐。UE5提供了FJsonObjectConverter工具它可以将JSON对象直接转换为USTRUCT结构体反之亦然。这大大简化了代码。首先定义一个与JSON对应的USTRUCT。注意必须使用GENERATED_BODY()宏并且属性标记为UPROPERTY()。// 在头文件中定义结构体 USTRUCT(BlueprintType) struct FPlayerStateData { GENERATED_BODY() UPROPERTY() FString PlayerId; UPROPERTY() FString PlayerName; UPROPERTY() int32 Score 0; UPROPERTY() TArrayFString EquippedItems; // 嵌套结构体 UPROPERTY() FVector2D Position FVector2D::ZeroVector; // 需要默认构造函数 FPlayerStateData() default; };然后在解析函数中使用转换器#include JsonObjectConverter.h // 需要包含此头文件 void AWebSocketClientActor::HandlePlayerStateMessage(const TSharedPtrFJsonObject JsonObject) { FPlayerStateData PlayerStateData; if (FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), PlayerStateData)) { // 转换成功PlayerStateData里的所有字段都已经被自动填充。 UE_LOG(LogTemp, Log, TEXT(Player %s has score %d), *PlayerStateData.PlayerName, PlayerStateData.Score); // 现在你可以安全地将这个结构体传递给游戏逻辑比如更新UI或角色状态。 } else { UE_LOG(LogTemp, Error, TEXT(Failed to convert JSON to PlayerStateData struct.)); } }注意事项FJsonObjectConverter::JsonObjectToUStruct要求JSON中的字段名与USTRUCT中的属性名完全匹配包括大小写。如果服务器返回的JSON使用snake_case如player_name而你的USTRUCT使用PascalCase如PlayerName转换会失败。你需要要么统一命名规范要么在接收后手动处理映射或者考虑使用更高级的序列化库。4.3 构造并发送JSON消息发送消息时我们需要构造一个TSharedPtrFJsonObject填充数据然后将其序列化为字符串。void AWebSocketClientActor::SendChatMessage(const FString Content, const FString TargetPlayerId) { if (!WebSocket.IsValid() || !WebSocket-IsConnected()) { UE_LOG(LogTemp, Warning, TEXT(WebSocket is not connected. Cannot send message.)); return; } // 1. 创建Json对象 TSharedPtrFJsonObject JsonObject MakeShareable(new FJsonObject); JsonObject-SetStringField(TEXT(type), TEXT(chat)); JsonObject-SetStringField(TEXT(content), Content); JsonObject-SetStringField(TEXT(target), TargetPlayerId); JsonObject-SetNumberField(TEXT(timestamp), FDateTime::UtcNow().ToUnixTimestamp()); // 2. 序列化为字符串 FString OutputString; TSharedRefTJsonWriter JsonWriter TJsonWriterFactory::Create(OutputString); if (FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter)) { // 3. 通过WebSocket发送 WebSocket-Send(OutputString); UE_LOG(LogTemp, Verbose, TEXT(Sent chat message: %s), *OutputString); } else { UE_LOG(LogTemp, Error, TEXT(Failed to serialize JSON for chat message.)); } }同样你也可以使用USTRUCT配合FJsonObjectConverter来构造JSON这对于复杂且重复的数据结构非常方便void AWebSocketClientActor::SendPlayerStateUpdate(const FPlayerStateData StateData) { TSharedPtrFJsonObject JsonObject FJsonObjectConverter::UStructToJsonObject(StateData); if (JsonObject.IsValid()) { JsonObject-SetStringField(TEXT(type), TEXT(playerStateUpdate)); // 可以额外添加字段 FString OutputString; TSharedRefTJsonWriter JsonWriter TJsonWriterFactory::Create(OutputString); if (FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter)) { WebSocket-Send(OutputString); } } }5. 与游戏逻辑集成及性能优化WebSocket客户端本身是一个通信工具它的价值在于将接收到的数据驱动游戏逻辑。我们需要安全地将数据从WebSocket线程或回调上下文传递到游戏线程GameThread因为UE4/5的大部分API如修改UObject、生成Actor、更新UI都必须在游戏线程上执行。5.1 线程安全与委托广播OnMessage等回调可能在非游戏线程中触发。直接在这些回调里调用游戏逻辑是危险的。标准的做法是使用AsyncTask或FFunctionGraphTask将任务派发到游戏线程。void AWebSocketClientActor::ProcessWebSocketMessage(const FString MessageString) { // ... 解析JSON得到JsonObject if (JsonObject.IsValid()) { // 派发到游戏线程进行处理 AsyncTask(ENamedThreads::GameThread, [this, JsonObject]() { ParseJsonMessage(JsonObject); // 现在这个函数在游戏线程中运行了 }); } }更优雅的方式是使用自定义的委托Delegate进行解耦。让WebSocket客户端只负责接收和解析原始数据然后通过委托将结构化数据广播出去让关心这些数据的其他系统如UI控制器、游戏状态机、其他Actor来订阅和处理。在头文件中声明多播委托DECLARE_MULTICAST_DELEGATE_OneParam(FOnPlayerStateReceived, const FPlayerStateData); DECLARE_MULTICAST_DELEGATE_TwoParams(FOnChatMessageReceived, const FString /*Sender*/, const FString /*Message*/); class WEBSOCKETDEMO_API AWebSocketClientActor : public AActor { // ... public: FOnPlayerStateReceived OnPlayerStateReceived; FOnChatMessageReceived OnChatMessageReceived; // ... };在解析函数中触发委托void AWebSocketClientActor::HandlePlayerStateMessage(const TSharedPtrFJsonObject JsonObject) { FPlayerStateData PlayerStateData; if (FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), PlayerStateData)) { // 确保在游戏线程触发委托 AsyncTask(ENamedThreads::GameThread, [this, PlayerStateData]() { OnPlayerStateReceived.Broadcast(PlayerStateData); }); } }这样一个UI Widget类就可以在BeginPlay时绑定到这个委托并在回调里更新血条、分数等显示实现了完美的逻辑分离。5.2 消息队列与流量控制在高频消息场景下比如实时位置同步如果每收到一条消息就立刻处理并广播可能会压垮游戏线程。一个常见的优化是引入一个消息队列TArray或TQueue。在WebSocket回调中不直接处理而是将解析好的结构化数据如FPlayerStateData推入一个线程安全的队列。然后在Actor的Tick函数中或一个自定义的定时器从队列中取出一定数量的消息比如每帧最多处理5条进行处理和广播。这可以平滑处理峰值流量避免游戏卡顿。// 头文件中 TQueueFPlayerStateData, EQueueMode::Mpsc PlayerStateMessageQueue; // 多生产者单消费者队列 // Tick函数中 void AWebSocketClientActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); FPlayerStateData StateData; int32 MessagesProcessedThisFrame 0; const int32 MaxMessagesPerFrame 5; while (MessagesProcessedThisFrame MaxMessagesPerFrame PlayerStateMessageQueue.Dequeue(StateData)) { // 处理或广播StateData OnPlayerStateReceived.Broadcast(StateData); MessagesProcessedThisFrame; } }5.3 资源管理与连接清理务必在Actor的EndPlay或析构函数中正确关闭WebSocket连接并清理资源。除了之前提到的关闭连接和清除定时器如果使用了消息队列也需要确保队列被清空避免持有无效的引用。AWebSocketClientActor::~AWebSocketClientActor() { // 确保连接关闭虽然EndPlay通常会被调用但这里是双重保险 if (WebSocket.IsValid() WebSocket-IsConnected()) { WebSocket-Close(); } WebSocket.Reset(); }6. 常见问题排查与调试技巧即使按照流程操作也难免会遇到问题。这里记录几个我调试时最常遇到的坑和解决方法。6.1 连接失败与“Insecure WebSocket Connection”问题在编辑器里运行连接ws://localhost:8080失败浏览器控制台或UE日志里可能有安全警告。排查检查服务器是否运行这是最常犯的错误。确保你的SpringBoot或其他WebSocket服务器已经启动并在指定端口监听。检查URL格式WebSocket URL以ws://非加密或wss://加密开头。确保没有拼写错误。跨域问题如果UE5客户端是打包后的程序或通过不同域访问服务器需要设置正确的CORS头部。对于开发阶段的本地服务器可以暂时配置为允许所有来源Access-Control-Allow-Origin: *但生产环境需要严格设置。防火墙/杀毒软件偶尔它们会阻止本地连接可以尝试临时禁用。6.2 JSON解析失败返回空对象或错误问题FJsonSerializer::Deserialize返回falseJsonObject是空的或无效。排查打印原始消息在ProcessWebSocketMessage函数里第一时间将收到的MessageString打印出来。很可能消息根本不是有效的JSON格式或者包含了不可见的控制字符如BOM头。验证JSON格式将打印出来的字符串复制到在线的JSON验证器如JSONLint中检查。常见的错误包括尾随逗号、字符串引号不匹配、缺少括号等。编码问题确保服务器发送的是UTF-8编码的纯文本JSON。某些服务器可能默认发送了其他编码。使用TryGetField在访问字段前始终使用TryGetStringField、TryGetNumberField等方法并检查返回值。不要直接使用GetStringField因为字段不存在时会报错。6.3 消息接收延迟或丢失问题发送消息很快但接收端反应慢或者偶尔丢消息。排查检查网络延迟和带宽使用网络工具测试基础网络状况。检查游戏线程是否阻塞如果游戏线程因为复杂的计算或同步加载而卡住那么即使WebSocket线程收到了消息派发到游戏线程的任务也会被积压。用stat unit命令查看游戏线程帧时间。实现应用层确认机制对于关键指令如“购买道具”可以让客户端在收到后回复一个“ACK”消息。服务器如果在超时时间内没收到ACK可以重发。这能解决因网络抖动导致的丢包问题。优化消息体积过大的JSON比如包含很长的Base64图片数据会增大传输延迟。考虑对消息进行压缩如GZIP或者将大资源通过其他途径如HTTP下载传输WebSocket只传控制指令和URL。6.4 打包后无法连接问题在编辑器里运行正常但打包后的独立游戏无法连接到服务器。排查URL地址编辑器里可能用localhost或127.0.0.1打包后需要连接到服务器的公网IP或域名。确保你的连接地址配置是正确的并且可以从目标机器访问。安全协议WSS生产环境务必使用wss://WebSocket Secure。服务器需要配置有效的SSL证书。打包后的游戏对非安全连接ws://的限制可能比编辑器更严格。平台权限在某些平台如Android、iOS需要在项目设置中显式声明网络访问权限。调试时充分利用UE_LOG在不同频道LogTemp,LogNet等和不同级别Verbose,Log,Warning,Error输出信息。在开发阶段可以多开Verbose日志追踪完整的消息流和生命周期事件发布时再关闭。