尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

UE4 WebSocket服务器插件开发:实现多端实时通信的完整指南

UE4 WebSocket服务器插件开发:实现多端实时通信的完整指南 1. 项目概述为什么要在UE4里折腾WebSocket服务器如果你是一个UE4开发者最近被“多端通信”的需求搞得焦头烂额比如想让手机App、网页后台、甚至另一个独立的游戏客户端都能和你的UE4游戏实例实时对话那么你很可能已经研究过TCP/UDP Socket、HTTP轮询这些方案并且发现它们各有各的“坑”。TCP长连接管理复杂HTTP轮询实时性差且浪费资源。这时候WebSocket协议就成了一个非常优雅的解决方案——它建立在TCP之上提供全双工通信一次握手长久连接特别适合游戏里的实时状态同步、聊天、指令下发等场景。但UE4官方并没有提供一个开箱即用的、功能完善的WebSocket服务器模块。市面上有一些第三方插件但要么年久失修要么功能不符合你的定制化需求比如你需要特定的二进制协议、复杂的房间管理逻辑或者与后端业务系统深度集成。于是自己动手开发一个UE4 WebSocket服务器插件就从“可选项”变成了“必选项”。这不仅仅是封装一个网络库那么简单它涉及到UE4插件体系的理解、网络线程与游戏线程的协同、蓝图与C的暴露以及如何设计一个健壮、易用的多端通信架构。这个过程能让你对UE4引擎的网络底层和模块化开发有更深的认识最终得到的不仅是一个工具更是一套可复用的技术资产。2. 核心架构设计与技术选型2.1 协议与库的选择为何是WebSocket开发WebSocket服务器的第一步是选择底层网络库。在C领域有几个热门选择libwebsockets、WebSocket和Boost.Beast。libwebsocketsC语言库非常轻量高效在嵌入式领域应用广泛。但其C语言的API在面向对象的UE4 C项目中集成起来略显繁琐错误处理和资源管理需要更多手动控制。Boost.Beast属于Boost库的一部分功能强大且现代支持HTTP和WebSocket。但它依赖整个Boost体系可能会增加项目的编译复杂性和体积。对于专注于WebSocket且希望保持轻量的插件来说可能有些“杀鸡用牛刀”。WebSocket这是一个用现代C11编写的头文件库专门为WebSocket协议设计。它的API清晰、面向对象与STL结合紧密并且不依赖Boost可选依赖。其基于事件的异步I/O模型依赖Asio与UE4自身的网络框架有相似之处集成起来思维模型更接近。综合考量我选择了WebSocket。理由如下轻量与专注纯头文件库集成简单只需包含头文件并链接Asio即可。它只做WebSocket一件事并且做得很好。现代C友好大量使用std::function、智能指针等与UE4的智能指针系统TSharedPtr可以较好地协同内存管理更安全。清晰的抽象提供了server、connection、message等清晰的类事件回调机制如on_open,on_message,on_close非常直观易于封装成UE4可用的对象。注意WebSocket底层依赖于Asio或独立的Boost.Asio。UE4本身已经包含了一个修改版的Asio在Runtime/Online相关模块中但为了减少与引擎版本的耦合和潜在的冲突我建议在插件内独立引入一个特定版本的Asio。这能确保插件行为的确定性。2.2 UE4插件结构设计一个规范的UE4插件是独立、可分发和可重用的单元。我们的WebSocket服务器插件结构应该如下规划YourProject/Plugins/ └── WebSocketServer/ ├── Source/ │ ├── WebSocketServer/ │ │ ├── Private/ │ │ │ ├── WebSocketServerCore.cpp // 核心服务器C实现 │ │ │ ├── WebSocketConnection.cpp // 连接管理类 │ │ │ └── ... │ │ ├── Public/ │ │ │ ├── WebSocketServerCore.h │ │ │ ├── IWebSocketServer.h // 模块接口 │ │ │ └── ... │ │ └── WebSocketServer.Build.cs │ ├── WebSocketServerEditor/ // 可选编辑器工具 │ └── WebSocketServerRuntime/ // 运行时模块 ├── Resources/ └── WebSocketServer.uplugin关键设计点模块划分至少需要一个运行时模块WebSocketServerRuntime来承载服务器功能。可以额外创建一个编辑器模块WebSocketServerEditor来添加编辑器工具栏按钮、配置面板等方便在编辑器中启动/停止服务器进行调试。线程模型这是核心挑战。WebSocket的服务器运行在它自己的I/O服务线程中。绝不能在这个网络线程中直接调用UE4的蓝图函数或修改UObject属性这会导致崩溃。必须通过线程安全的队列将收到的消息传递到UE4的游戏线程GameThread进行处理。蓝图暴露为了便于关卡设计师和蓝图程序员使用我们需要通过UCLASS和UFUNCTION将核心功能暴露给蓝图。例如创建一个UWebSocketServerSubsystem继承自UEngineSubsystem或UGameInstanceSubsystem作为全局访问点或者创建一个AWebSocketServerActor放置在关卡中。2.3 多端通信的会话管理一个服务器必然要面对多个客户端连接。我们需要设计一个会话Session或连接Connection管理器。每个连接到服务器的客户端无论是浏览器、手机App还是另一个UE4实例都应该被赋予一个唯一的连接标识并维护其上下文信息。基础会话管理应包括连接标识使用WebSocket提供的连接句柄connection_hdl作为底层标识但对外暴露一个更友好的ID如递增整数或GUID。状态维护记录连接状态连接中、已连接、断开中、远端地址、连接时间等。消息路由提供“向指定连接发送消息”、“广播给所有连接”、“广播给除某个连接外的所有连接”等方法。生命周期绑定将UE4端的UObject如代表一个玩家的APlayerController或自定义的数据对象与网络连接关联起来当连接断开时能清理或通知对应的游戏对象。3. 核心模块实现详解3.1 第三方库的引入与编译集成首先你需要获取WebSocket和Asio的源代码。建议使用Git子模块或直接下载Release包放入插件的ThirdParty目录。Plugins/WebSocketServer/Source/ThirdParty/ ├── websocketpp/ │ └── (所有头文件) └── asio/ └── asio/include/asio.hpp接下来修改插件的构建文件WebSocketServer.Build.cs将第三方库的头文件路径添加到编译系统中并添加必要的预处理器定义。// WebSocketServer.Build.cs using UnrealBuildTool; public class WebSocketServer : ModuleRules { public WebSocketServer(ReadOnlyTargetRules Target) : base(Target) { PCHUsage ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; PublicIncludePaths.AddRange( new string[] { // ... 其他公共路径 } ); PrivateIncludePaths.AddRange( new string[] { // 添加第三方库路径 Path.Combine(ModuleDirectory, ThirdParty, websocketpp), Path.Combine(ModuleDirectory, ThirdParty, asio, asio, include), } ); PublicDependencyModuleNames.AddRange( new string[] { Core, CoreUObject, Engine, // Sockets, // 可选如果你需要用到UE4底层Socket // Networking, // 可选 } ); PrivateDependencyModuleNames.AddRange( new string[] { // ... 私有依赖 } ); // 定义ASIO_STANDALONE告诉Asio我们不使用Boost PublicDefinitions.Add(ASIO_STANDALONE); // 如果你使用Boost.Asio则不需要上面这行但需要正确链接Boost库 } }实操心得在引入Asio时务必确保ASIO_STANDALONE宏的定义与你的Asio版本匹配。独立版的Asio头文件位置和部分API可能与Boost版有细微差别。统一采用独立版可以避免与引擎内可能存在的Boost版本冲突。3.2 WebSocket服务器核心类封装我们将创建一个FWebSocketServerCore类它管理WebSocket服务器的生命周期并封装其事件。这个类通常不直接继承自UObject而是一个普通的C类由某个UObject如Subsystem持有。头文件关键部分 (WebSocketServerCore.h):#pragma once #include CoreMinimal.h #include websocketpp/config/asio_no_tls.hpp #include websocketpp/server.hpp #include functional #include queue #include mutex typedef websocketpp::serverwebsocketpp::config::asio WSServer; typedef WSServer::message_ptr WSMessagePtr; typedef websocketpp::connection_hdl WSConnectionHdl; // 定义从网络线程传递到游戏线程的消息结构 struct FWebSocketMessage { WSConnectionHdl ConnectionHandle; FString Data; // 或 TArrayuint8 用于二进制数据 bool bIsBinary; }; class WEBSOCKETSERVER_API FWebSocketServerCore { public: FWebSocketServerCore(); ~FWebSocketServerCore(); bool StartServer(int32 Port); void StopServer(); void SendMessage(const WSConnectionHdl Hdl, const FString Message); void BroadcastMessage(const FString Message, const WSConnectionHdl ExcludeHdl nullptr); // 委托用于在游戏线程通知外部 DECLARE_MULTICAST_DELEGATE_OneParam(FOnClientConnected, const WSConnectionHdl); DECLARE_MULTICAST_DELEGATE_TwoParams(FOnMessageReceived, const WSConnectionHdl, const FString); DECLARE_MULTICAST_DELEGATE_OneParam(FOnClientDisconnected, const WSConnectionHdl); FOnClientConnected OnClientConnected; FOnMessageReceived OnMessageReceived; FOnClientDisconnected OnClientDisconnected; // 供游戏线程定期调用处理积压的消息 void ProcessPendingMessages(); private: void OnOpen(WSConnectionHdl Hdl); void OnMessage(WSConnectionHdl Hdl, WSMessagePtr Msg); void OnClose(WSConnectionHdl Hdl); TUniquePtrWSServer ServerInstance; std::thread ServerThread; std::atomicbool bIsRunning; // 线程安全的队列用于存放从网络线程收到的消息 std::queueFWebSocketMessage MessageQueue; std::mutex QueueMutex; // 连接映射表需要线程安全访问或仅在游戏线程访问通过传递的Hdl来间接操作 // TMapWSConnectionHdl, FClientInfo ActiveConnections; // 示例 };实现要点 (WebSocketServerCore.cpp):启动服务器在StartServer中初始化WSServer对象设置事件回调set_open_handler,set_message_handler,set_close_handler绑定到本类的成员函数。然后在ServerThread中调用server.run()。run()是阻塞调用所以必须在新线程中执行。事件回调与线程安全OnOpen,OnMessage,OnClose是在WebSocket的内部网络线程中被调用的。在这里我们绝不能直接触发UE4的委托或修改游戏状态。正确的做法是在OnMessage中将消息内容和连接句柄包装成FWebSocketMessage然后通过锁std::mutex推入MessageQueue。可以立即调用SendMessage回复因为WebSocket的send方法是线程安全的。游戏线程处理暴露一个ProcessPendingMessages方法需要在游戏线程中定期调用例如在某个Actor的Tick中或使用FTicker。在这个方法里锁住队列取出所有积压的消息然后在游戏线程安全地触发OnMessageReceived等多播委托。发送消息SendMessage和BroadcastMessage方法内部应检查服务器是否运行并调用server.send(hdl, payload, opcode)。这些方法可以被蓝图在游戏线程调用因为WebSocket的send内部做了线程安全处理。3.3 蓝图可访问的接口层为了让设计师使用我们创建UWebSocketServerSubsystem继承自UGameInstanceSubsystem。GameInstance子系统在游戏生命周期内一直存在非常适合管理这种全局网络服务。// WebSocketServerSubsystem.h UCLASS() class WEBSOCKETSERVER_API UWebSocketServerSubsystem : public UGameInstanceSubsystem { GENERATED_BODY() public: virtual void Initialize(FSubsystemCollectionBase Collection) override; virtual void Deinitialize() override; UFUNCTION(BlueprintCallable, Category WebSocket Server) bool StartWebSocketServer(int32 Port 9000); UFUNCTION(BlueprintCallable, Category WebSocket Server) void StopWebSocketServer(); UFUNCTION(BlueprintCallable, Category WebSocket Server) void SendToClient(const FString ClientId, const FString Message); // 需自己维护ClientId到Hdl的映射 UFUNCTION(BlueprintCallable, Category WebSocket Server) void BroadcastMessage(const FString Message); // 蓝图可绑定的动态多播委托 DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnWSClientConnected, const FString, ClientId); DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnWSMessageReceived, const FString, ClientId, const FString, Message); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnWSClientDisconnected, const FString, ClientId); UPROPERTY(BlueprintAssignable, Category WebSocket Server|Events) FOnWSClientConnected OnClientConnected; UPROPERTY(BlueprintAssignable, Category WebSocket Server|Events) FOnWSMessageReceived OnMessageReceived; UPROPERTY(BlueprintAssignable, Category WebSocket Server|Events) FOnWSClientDisconnected OnClientDisconnected; private: void OnCoreClientConnected(const websocketpp::connection_hdl Hdl); void OnCoreMessageReceived(const websocketpp::connection_hdl Hdl, const FString Message); void OnCoreClientDisconnected(const websocketpp::connection_hdl Hdl); void ProcessGameThreadMessages(); TSharedPtrFWebSocketServerCore ServerCore; FTSTicker::FDelegateHandle TickHandle; // 映射连接句柄 - 对外暴露的客户端ID TMapwebsocketpp::connection_hdl, FString, std::owner_lesswebsocketpp::connection_hdl ConnectionMap; TMapFString, websocketpp::connection_hdl ClientIdMap; std::atomicint32 NextClientId; };这个子系统的核心工作是封装与转换持有FWebSocketServerCore实例将核心层的C委托FOnClientConnected等绑定到自己的私有方法在这些方法中将不透明的connection_hdl转换为蓝图友好的FString类型客户端ID并维护两个映射表。游戏线程Tick在Initialize中注册一个Tick委托定期调用ServerCore-ProcessPendingMessages()进而触发蓝图事件。提供蓝图节点StartWebSocketServer、StopWebSocketServer、SendToClient等函数可以直接在蓝图中调用。暴露事件动态多播委托允许在蓝图中用“Event Dispatcher”节点进行绑定实现事件驱动逻辑。4. 多端通信实战与数据协议设计4.1 连接与消息流转全流程假设我们已经启动服务器在端口9000。一个Web端JavaScript连接并通信的流程如下客户端连接// 网页JavaScript const socket new WebSocket(ws://你的机器IP:9000); socket.onopen function(event) { console.log(Connected to UE4 Server!); socket.send(JSON.stringify({type: login, username: WebUser})); };UE4服务器端网络线程触发FWebSocketServerCore::OnOpen生成一个临时ID如Conn_1将hdl和ID存入队列。游戏线程下次ProcessPendingMessages时从队列取出连接事件UWebSocketServerSubsystem将其转换为蓝图委托OnClientConnected参数为Conn_1。蓝图接收到OnClientConnected事件可以打印日志或将这个ClientId与一个游戏内的角色绑定。消息处理网页发送JSON字符串。网络线程触发OnMessage将字符串和hdl放入队列。游戏线程处理队列触发OnMessageReceived(ClientId, MessageString)。蓝图解析收到的JSON字符串根据type字段执行不同逻辑如login处理登录move处理移动指令。UE4发送消息蓝图中调用SendToClient节点传入ClientId和消息字符串。子系统通过ClientIdMap找到对应的hdl调用ServerCore-SendMessage。WebSocket在网络线程中将消息发送给指定客户端。断开连接流程与连接类似最终触发蓝图的OnClientDisconnected事件进行清理。4.2 数据协议JSON vs. 二进制对于多端通信尤其是与网页、移动端交互JSON是首选的数据交换格式。它人类可读、跨语言支持极好、易于调试。在UE4中处理JSON// 发送消息时构造JSON TSharedPtrFJsonObject JsonObject MakeSharedFJsonObject(); JsonObject-SetStringField(TEXT(command), TEXT(spawn)); JsonObject-SetNumberField(TEXT(x), 100.0f); JsonObject-SetNumberField(TEXT(y), 200.0f); FString OutputString; TSharedRefTJsonWriter Writer TJsonWriterFactory::Create(OutputString); FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer); // 调用 SendToClient 或 BroadcastMessage SendToClient(ClientId, OutputString); // 接收消息时解析JSON TSharedPtrFJsonObject ParsedJson; TSharedRefTJsonReader Reader TJsonReaderFactory::Create(MessageString); if (FJsonSerializer::Deserialize(Reader, ParsedJson) ParsedJson.IsValid()) { FString Command ParsedJson-GetStringField(TEXT(command)); // ... 处理命令 }对于需要高性能、传输体积敏感的场景如频繁的实体位置同步可以考虑设计二进制协议。可以使用UE4自带的FMemoryReader/FMemoryWriter配合FArchive序列化或者使用更高效的第三方库如Google Protobuf。但这会增加客户端特别是网页端的复杂度需要引入对应的解析库。在项目初期强烈建议先用JSON快速迭代验证通信逻辑后期再针对性能瓶颈评估是否切换为二进制协议。4.3 多端应用示例网页控制台与移动端遥控器网页控制台利用HTML/JavaScript快速构建一个管理界面连接到UE4服务器。可以发送指令如/set gravity 0.5、广播公告、查看当前连接的客户端列表需要服务器暴露查询接口。这非常适合作为游戏运营或调试工具。移动端遥控器在手机端可用React Native、Flutter或原生开发做一个简易App通过WebSocket连接。将手机屏幕变成虚拟手柄发送方向、按键事件到UE4控制角色移动或触发技能。这为游戏提供了第二种输入方式适合演示或特定玩法。关键实现技巧心跳与保活WebSocket连接可能因网络波动或NAT超时而断开。需要在应用层实现心跳机制。客户端定期如每30秒发送一个ping消息服务器回复pong。如果一段时间未收到心跳则认为连接已失效进行清理。连接验证在OnOpen事件后不要立即将连接视为有效。可以设计一个握手或登录流程。客户端连接后必须发送一个包含令牌Token或身份信息的认证消息服务器验证通过后才将其加入正式的连接管理列表并触发OnClientConnected事件。这增加了安全性。5. 高级功能、调试与性能优化5.1 插件打包与分发开发完成后你可能希望将插件分享给团队其他成员或用于其他项目。清理中间文件删除Binaries、Intermediate、Saved等目录。处理依赖确保ThirdParty目录下的库源代码已包含。如果使用了需要编译的第三方库如某些SSL库则需要将编译好的.lib/.dll文件也放入合适的目录并在.Build.cs中正确配置。创建.uplugin文件这是一个JSON文件描述了插件元数据。{ FileVersion: 3, Version: 1, VersionName: 1.0, FriendlyName: WebSocket Server, Description: A WebSocket server plugin for UE4, enabling multi-end communication., Category: Networking, CreatedBy: YourName, Modules: [ { Name: WebSocketServerRuntime, Type: Runtime, LoadingPhase: Default } ] }分发将整个插件文件夹压缩其他人可以将其解压到自己项目的Plugins目录下重新生成项目文件即可使用。5.2 调试与问题排查常见问题1服务器启动失败端口被占用排查检查日志输出。确保没有其他程序如之前的游戏实例、其他服务占用了指定端口。可以在命令行用netstat -ano | findstr :9000Windows或lsof -i :9000macOS/Linux查看。解决在代码中增加端口占用时的重试逻辑或提供配置界面让用户修改端口。常见问题2客户端能连接但收不到消息或UE4收不到客户端消息排查防火墙确保操作系统防火墙允许该端口的入站连接。线程问题这是最可能的原因。检查所有从网络线程回调OnOpen,OnMessage,OnClose中是否直接调用了UE4蓝图或修改了UProperty。必须通过线程安全队列中转。委托未绑定在蓝图中确认OnMessageReceived等事件委托已经正确绑定到事件处理函数。消息队列未处理确认UWebSocketServerSubsystem的ProcessGameThreadMessages或类似的Tick函数被定期调用。调试工具使用浏览器的“开发者工具-网络Network-WS”标签页或使用独立的WebSocket调试助手如“Smart Websocket Client”等可以直观地看到连接状态和收发消息的原始数据是排查协议问题的利器。常见问题3打包后插件不工作排查确保插件的所有模块在打包时被正确包含。检查.uplugin文件中的Modules配置。对于运行时模块LoadingPhase设为Default或PostConfigInit通常没问题。解决有时需要将第三方库的DLL文件手动复制到打包后的可执行文件同级目录。可以在插件的PostBuildStep中配置自动复制。5.3 性能优化与扩展方向连接数上限WebSocket和Asio默认可以处理大量并发连接但UE4游戏逻辑本身是单线程的游戏线程。当连接数成千上万时消息队列的处理可能成为瓶颈。优化方法使用更高效的数据结构如无锁队列替换std::queuestd::mutex。将消息处理逻辑分散到多个游戏线程的Actor或任务中避免所有消息都在一个Tick里处理。对于广播消息避免在循环中多次序列化同一数据应先序列化好再循环发送。二进制协议压缩如果使用二进制协议可以考虑对Payload进行压缩如zlib减少网络带宽占用尤其对于移动网络环境。SSL/TLS支持WebSocket支持WSSWebSocket Secure。要启用它你需要使用asio_tls配置并引入OpenSSL库。这增加了复杂性但对于需要加密通信的生产环境是必要的。与UE4原生网络融合高级用法是将WebSocket连接与UE4的APlayerController或自定义的UNetConnection关联起来让通过WebSocket连接的“玩家”也能融入UE4的复制Replication系统。这需要深入理解UE4的网络框架但能实现更强大的功能如让网页玩家控制一个具有完整复制功能的游戏角色。开发这样一个插件从底层库集成到上层蓝图暴露再到多端协议设计是一个系统工程。它考验的不仅是C和网络编程能力更是对UE4引擎框架的理解。当你完成它看到网页上的一个按钮能实时控制UE4场景中的物体或者手机App能作为游戏的第二屏幕时那种成就感是巨大的。这个插件将成为你项目跨平台互联互通的坚实桥梁。
返回列表