UEC++和Spring Boot的通信

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

核心特性对比

维度REST APIWebSocketgRPC
通信模式请求-响应(单向)全双工双向(长连接)支持单向/双向流(基于 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

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

2.创建WebSocket管理器

// 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);
};
// 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()中连接:

// 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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

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

// 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类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SendSocketMessage {

    private String type;
    private String content;

}

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

// 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.消息推送示例

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

在主类添加@EnableScheduling

@SpringBootApplication
@MapperScan("org.wms.pre.mapper")
@EnableScheduling
public class BackendApplication {
    public static void main(String[] args) {
        SpringApplication.run(BackendApplication.class, args);
    }
}

测试是否连接成功

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

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是否被触发

@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

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

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

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

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

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请求处理即可

@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");
    }

}
UE4中可以使用C++socket实现UDP通信。首先,你需要包含UE4的Networking模块,可以通过以下方式引入: ```cpp #include "Runtime/Networking/Public/Networking.h" ``` 然后,你可以创建一个socket对象来进行通信。你可以使用以下代码创建一个UDP socket: ```cpp FSocket* Socket = FUdpSocketBuilder(TEXT("SocketName")) .AsReusable() .BoundToAddress(FIPv4Address(0, 0, 0, 0)) .BoundToPort(12345) .Build(); ``` 在这个例子中,我们创建了一个名为"SocketName"的UDP socket,并将其绑定到本地IP地址的12345端口上。 接下来,你可以使用socket对象发送接收数据。以下是一个发送接收数据的示例: ```cpp TArray<uint8> SendData; SendData.Add('H'); SendData.Add('e'); SendData.Add('l'); SendData.Add('l'); SendData.Add('o'); int32 BytesSent = 0; Socket->SendTo(SendData.GetData(), SendData.Num(), BytesSent, *RemoteAddress); TArray<uint8> ReceivedData; FIPv4Endpoint RemoteEndpoint; int32 BytesRead = 0; if (Socket->HasPendingData(BytesRead)) { ReceivedData.SetNumUninitialized(BytesRead); Socket->RecvFrom(ReceivedData.GetData(), ReceivedData.Num(), BytesRead, *RemoteEndpoint); } for (uint8 Data : ReceivedData) { UE_LOG(LogTemp, Warning, TEXT("Received data: %c"), Data); } ``` 在这个例子中,我们首先创建了一个包含字符串"Hello"的字节数组SendData,并使用Socket对象的SendTo方法将数据发送到指定的远程地址。 然后,我们使用Socket对象的HasPendingData方法检查是否有待处理的数据,并使用RecvFrom方法接收数据。接收到的数据存储在ReceivedData字节数组中,并通过循环打印每个字节的值。 请注意,上述代码只是一个简单的示例,你可以根据自己的需求进行修改扩展。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值