UE和Spring Boot的通信

UE和Spring Boot的通信方式常见的有三种:REST API 、WebSocket和 gRPC。

核心特性对比

维度 REST API WebSocket gRPC
通信模式 请求-响应(单向) 全双工双向(长连接) 支持单向/双向流(基于 HTTP/2)
连接类型 短连接(每次请求新建) 长连接(一次握手,持久通信) 长连接(复用 HTTP/2 多路复用)
数据格式 JSON / XML(文本) 任意(通常 JSON 文本或二进制) Protocol Buffers(二进制,高效)
服务端主动推送 ❌ 不支持 ✅ 原生支持 ✅ 支持(通过 Server Streaming)
延迟 高(需完整 HTTP 请求) 极低(毫秒级) 极低(二进制 + HTTP/2)
带宽效率 低(冗余 HTTP 头) 高(仅 payload) 极高(紧凑二进制编码)
UE 支持 ✅ 内置 HttpModule ✅ 内置 WebSocketsModule ❌ 需集成第三方库(如 gRPC C++)
Spring Boot 支持 ✅ 原生(@RestController ✅ 原生(WebSocketHandler ✅ 需额外依赖(grpc-spring-boot-starter
跨平台兼容性 ✅ 极佳(所有设备支持 HTTP) ✅ 良好(现代浏览器/引擎支持) ⚠️ 较差(需编译 native 库)
调试难度 ✅ 简单(Postman、curl) ⚠️ 中等(需专用工具) ❌ 困难(二进制协议,需 proto 定义)

根据上述分析我们不难发现:

  • REST API适用的场景是客户端主动发起操作:登录,购买,提交表单等。不适用于高频通信和服务端主动推送等操作。
  • WebSocket适合服务端主动推送系统通知,任务更新;低频到中频的状态同步等。
  • gRP用于通讯有点大材小用,这里不再详细介绍

因此根据需求我们可以选择不同的通信方式来实现UE和Spring Boot的通信,这里我们只介绍WebSocket的通信方式。

使用WebSocket通信

UE端

1.启用模块(xx.Build.cs

1
2
3
PublicDependencyModuleNames.AddRange(new string[] {
"Core", "CoreUObject", "Engine", "WebSockets"
});

2.创建WebSocket管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// WebSocketManager.h
#pragma once
#include "CoreMinimal.h"
#include "WebSocketsModule.h"
#include "IWebSocket.h"

// 声明委托类型:接收原始消息字符串
DECLARE_MULTICAST_DELEGATE_OneParam(FOnWebSocketMessageReceived, const FString&);

class FWebSocketManager
{
public:
// 公共委托,供其他系统绑定
static FOnWebSocketMessageReceived OnMessageReceived;

static void Connect(const FString& Url);
static void Disconnect();
static void SendMessage(const FString& Message);

private:
static TSharedPtr<IWebSocket> WebSocket;
static void OnConnected();
static void OnMessage(const FString& Message);
static void OnClosed(int32 StatusCode, const FString& Reason, bool bWasClean);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// WebSocketManager.cpp
#include "WebSocketManager.h"
#include "Misc/Paths.h"
#include "Misc/CommandLine.h"
#include "Engine/Engine.h"

// 定义静态委托实例
FOnWebSocketMessageReceived FWebSocketManager::OnMessageReceived;
TSharedPtr<IWebSocket> FWebSocketManager::WebSocket = nullptr;

void FWebSocketManager::Connect(const FString& Url)
{
if (WebSocket.IsValid())
{
UE_LOG(LogTemp, Warning, TEXT("Already connected"));
return;
}

WebSocket = FWebSocketsModule::Get().CreateWebSocket(Url, TEXT(""));

WebSocket->OnConnected().AddStatic(&FWebSocketManager::OnConnected);
WebSocket->OnMessage().AddStatic(&FWebSocketManager::OnMessage);
WebSocket->OnClosed().AddStatic(&FWebSocketManager::OnClosed);

WebSocket->Connect();
}

void FWebSocketManager::Disconnect()
{
if (WebSocket.IsValid())
{
WebSocket->Close();
WebSocket.Reset();
}
}

void FWebSocketManager::SendMessage(const FString& Message)
{
if (WebSocket.IsValid() && WebSocket->IsConnected())
{
WebSocket->Send(Message);
}
}

void FWebSocketManager::OnConnected()
{
UE_LOG(LogTemp, Log, TEXT("WebSocket connected to server"));
// 可发送认证信息
// SendMessage(TEXT("{\"cmd\":\"auth\",\"token\":\"abc123\"}"));
}

void FWebSocketManager::OnMessage(const FString& Message)
{
UE_LOG(LogTemp, Log, TEXT("Received from server: %s"), *Message);

// 🔔 广播给所有监听者
OnMessageReceived.Broadcast(Message);

// 示例:解析 JSON
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Message);
if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid())
{
FString Type = JsonObject->GetStringField("type");
if (Type == TEXT("system"))
{
// 处理系统推送
FString Content = JsonObject->GetStringField("content");
GEngine->AddOnScreenDebugMessage(-1, 10.f, FColor::Yellow, Content);
}
}
}

void FWebSocketManager::OnClosed(int32 StatusCode, const FString& Reason, bool bWasClean)
{
UE_LOG(LogTemp, Warning, TEXT("WebSocket closed: %s (Code: %d)"), *Reason, StatusCode);
WebSocket.Reset();

// 可选:自动重连(避免频繁重试)
// FTimerHandle Handle;
// GWorld->GetTimerManager().SetTimer(Handle, []{ FWebSocketManager::Connect(TEXT("wss://...")); }, 5.0f, false);
}

3.在游戏逻辑中调用

例如在BeginPlay()中连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ChatSystem.cpp
void UChatSystem::BeginPlay()
{
Super::BeginPlay();

// 连接socket
FWebSocketManager::Connect(TEXT("ws://127.0.0.1:8080/ws"));
// 绑定到 WebSocket 接收消息的函数:HandleWebSocketMessage
FWebSocketManager::OnMessageReceived.AddUObject(this, &UChatSystem::HandleWebSocketMessage);
}

void UChatSystem::HandleWebSocketMessage(const FString& Message)
{
TSharedPtr<FJsonObject> Json;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Message);
if (FJsonSerializer::Deserialize(Reader, Json) && Json.IsValid())
{
if (Json->GetStringField("type") == TEXT("chat"))
{
FString Content = Json->GetStringField("content");
AddChatMessage(Content);
}
}
}

// 别忘了在销毁时解绑!
void UChatSystem::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
FWebSocketManager::OnMessageReceived.RemoveAll(this);
Super::EndPlay(EndPlayReason);
}

Spring Boot端

1.添加依赖(pom.xml

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

2.配置WebSocket(启用+路由)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// WebSocketConfig.java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

/**
* 注册WebSocket处理器
* 创建了一个GameWebSocketHandler对象,用它来处理WebSocket连接和消息
* 将处理器映射到 /ws 路径,客户端可以通过这个端口建立websocket连接,并允许跨域访问 (开发阶段)
*/

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 允许跨域(开发阶段),生产环境应限制 origin
registry.addHandler(new GameWebSocketHandler(), "/ws")
.setAllowedOrigins("*");
}
}

3.实现消息处理器

创建对象+JSON序列化库,来传输JSON数据

(1)定义消息DTO类

1
2
3
4
5
6
7
8
9
10
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SendSocketMessage {

private String type;
private String content;

}

(2)在消息处理器中使用定义的DTO类发送JSON数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// GameWebSocketHandler.java
@Component
public class GameWebSocketHandler extends TextWebSocketHandler {

@Autowired
private ObjectMapper objectMapper;

// 存储所有连接(实际项目建议按用户ID分组)
private static final Set<WebSocketSession> sessions = ConcurrentHashMap.newKeySet();

@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.add(session);
System.out.println("Client connected: " + session.getId());
session.sendMessage(new TextMessage("{\"type\":\"welcome\",\"msg\":\"Connected to server\"}"));
}

// 处理接收到的消息
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
System.out.println("Received from client: " + payload);

// 回显消息(或处理业务逻辑)
session.sendMessage(new TextMessage("{\"echo\":" + payload + "}"));
}

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
sessions.remove(session);
System.out.println("Client disconnected: " + session.getId());
}

// 提供全局广播方法(用于系统推送)
public void broadcast(Object message) {
try {
String json = objectMapper.writeValueAsString(message);
broadcastJSON(json);
} catch (JsonProcessingException e) {
System.out.println("Broadcast failed:"+e);
}
}

private static void broadcastJSON(String json) {
sessions.forEach(session -> {
try {
if (session.isOpen()) {
session.sendMessage(new TextMessage(json));
}
} catch (IOException e) {
// handle
}
});
}
}

4.消息推送示例

1
2
3
4
5
6
@PostMapping("/start")
public ResponseEntity<String> start() {
// 使用websocket 向UE端发送消息
gameWebSocketHandler.broadcast(new SendSocketMessage("moveMessage","开始移动"));
return ResponseEntity.ok("BoxMoveStart");
}

在主类添加@EnableScheduling

1
2
3
4
5
6
7
8
@SpringBootApplication
@MapperScan("org.wms.pre.mapper")
@EnableScheduling
public class BackendApplication {
public static void main(String[] args) {
SpringApplication.run(BackendApplication.class, args);
}
}

测试是否连接成功

1.UE端查看监听的OnConnected()回调函数

1
2
3
4
5
6
void FWebSocketManager::OnConnected()
{
UE_LOG(LogTemp, Log, TEXT("WebSocket connected to server"));
// 可发送认证信息
// SendMessage(TEXT("{\"cmd\":\"auth\",\"token\":\"abc123\"}"));
}

查看是否在控制台打印输出该字符串,只要 OnConnected() 被触发,就说明 TCP 握手 + WebSocket 协议升级成功,通信通道已建立。

2.Spring Boot服务端查看afterConnectionEstablished是否被触发

1
2
3
4
5
6
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.add(session);
System.out.println("Client connected: " + session.getId());
session.sendMessage(new TextMessage("{\"type\":\"welcome\",\"msg\":\"Connected to server\"}"));
}

只要这个方法执行,说明WebSocket握手完成,连接已经建立成功

同时在UE端查看是否收到了传来的确认消息,UE 客户端收到该消息后,可视为 双向通信验证成功

使用REST API通信

UE端

由于这个方法比较简单,且用的不多,介绍的并不详细。

简单来说就是UE端发送请求,Spring Boot接收请求并返回相应的数据。

原理与 Vue和Spring Boot通信机制相同

1.启动模块xx.Build.cs

1
2
3
PublicDependencyModuleNames.AddRange(new string[] {
"Http","Json"
});

2.创建Http请求函数和回调函数

SendGetHelloRequest为请求函数,OnGetHelloResponse为回调函数。

请求时只需调用SendGetHelloRequest即可,可以用tick调用或游戏刚开始运行时候启动均可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void AHttpCommunicationActor::SendGetHelloRequest()
{
if(!bisStartMove)
{
TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
Request->SetURL(TEXT("http://localhost:8080/api/setBoxMove"));
Request->SetVerb(TEXT("POST"));
Request->SetHeader(TEXT("User-Agent"), TEXT("UE5-Client"));
Request->OnProcessRequestComplete().BindUObject(this, &AHttpCommunicationActor::OnGetHelloResponse);
Request->ProcessRequest();
}

}

void AHttpCommunicationActor::OnGetHelloResponse(FHttpRequestPtr Request, FHttpResponsePtr Response,
bool bWasSuccessful)
{
if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode()))
{
// 停止每帧对后端接口数据的访问
bisStartMove = true;
UE_LOG(LogTemp, Log, TEXT("[GET] Request successful! Response code: %d"), Response->GetResponseCode());
// 查找场景中的ABoxMove Actor并调用MoveBySpline,实现人物移动
for (TActorIterator<ABoxMove> It(GetWorld()); It; ++It)
{
ABoxMove* BoxMoveActor = *It;
if (BoxMoveActor)
{
BoxMoveActor->MoveBySpline();
break; // 找到第一个就停止
}
}
}
else
{
// UE_LOG(LogTemp, Error, TEXT("[GET] Request failed! Response code: %d"),
// Response.IsValid() ? Response->GetResponseCode() : -1);
}
}

Spring Boot端

创建Controller请求处理即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@RestController
@RequestMapping("/api")
public class ueController {

@Autowired
private GameWebSocketHandler gameWebSocketHandler;

private boolean isStart = false;
private boolean isStop = false;

@CrossOrigin
@PostMapping("/setBoxMove")
public ResponseEntity<String> setBoxMove() {

if( isStart && !isStop){
// 5s后恢复isStart的值
// 创建一个定时任务,在5秒后将isStart设为false,用来重复测试移动效果
new Thread(() -> {
try {
Thread.sleep(5000);
isStart = false;
isStop = false; // 同时重置isStop状态
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
isStop = true;
return ResponseEntity.ok("BoxMoveStart");
}

// 否则返回错误信息
return ResponseEntity.badRequest().body("BoxMoveStart need isStart true");
}

@PostMapping("/start")
public ResponseEntity<String> start() {
// isStart = true;
// 使用websocket 向UE端发送消息
gameWebSocketHandler.broadcast(new SendSocketMessage("moveMessage","开始移动"));
return ResponseEntity.ok("BoxMoveStart");
}

}