使用Protobuf+Websocket构建Unreal与Python服务器的通信

构建Unreal可用的protobuf库

这里有一篇现成可用的教程
UE4随笔:接入 Google Protobuf 库
需要注意的点:

  1. Protobuf和Protoc的版本要一样,Protoc可以不用按照上面教程中自己生成解决方案(只生成libprotobuf就可以),直接从git下载然后配置环境变量就可以

  2. 这个方法不适用 3.22之后的版本,因为下载的文件内容差别比较大,以最新的3.23.4为例
    (1)3.22之后不再有单个语言的版本,需要下载完整版
    (2)完整版的CMakeList是在根目录下,不在cmake文件夹中,所以cmake的时候src要改成根目录
    (3)新版本的Protobuf不再自带googletestabseil,需要单独下载到third_party下对应的文件夹里
    ·在这里插入图片描述

    (4)cmake的时候可能会报缺少zlib,就需要再下载zlib,然后cmake并生成库文件(操作和构建protobuf完全一样),再将生成的Debug和Release库路径添加到当前cmake的配置选项中,具体可以看这篇文章的前半部分
    (5)cmake完生成的文件夹中不再有extract_include.bat文件,无法提取头文件库,lib的生成按照上述步骤是可以正常生成的

  3. 上面的文章中是将Protobuf构建为了第三方库,如果要构建为插件的话,build.cs应该这么配置,注意include和lib文件夹路径和实际路径匹配就可以了

	public Protobuf(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
		//Type = ModuleType.External;
		if (Target.Platform == UnrealTargetPlatform.Win64)
		{
			PublicSystemIncludePaths.Add(Path.Combine(ModuleDirectory, "include"));
			PublicSystemIncludePaths.Add(Path.Combine(ModuleDirectory, "lib"));
			PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "lib", "libprotobuf.lib"));
		}

		ShadowVariableWarningLevel = WarningLevel.Off;
		bEnableUndefinedIdentifierWarnings = false;
		bEnableExceptions = true;
		PublicDefinitions.Add("_CRT_SECURE_NO_WARNINGS");
		PublicDefinitions.Add("GOOGLE_PROTOBUF_NO_RTTI=1");
		PublicDefinitions.Add("GOOGLE_PROTOBUF_CMAKE_BUILD");
	}

Protobuf使用注意事项

  1. Protobuf的版本在客户端和服务端一定要保持一致,不然可能会出现各种奇怪的问题
  2. Protobuf的消息序列化和反序列化在c++端api有两个,但只是存储形式有差别而已,实际存储的内容都是以bytes字节流的格式;而在python端只有一个api,只接收bytes类型的参数,所以要注意在Unreal端使用Websocket或其他协议进行发送时要注意以字节流形式发送,如果以string形式发送的话python端就会解析报错
bool SerializeToArray(
    void* data, 
    int size) const
bool SerializeToString(
    std::string* output) const
(function) ParseFromString: Any

以webscoket为例,unreal端的发送应该是这样,如果不将isBinary设置为true或者使用SerializeToString +Send(FString&)以字符串形式发送都会在python服务器端解析出错

char buf[2048];
ClientMsg.SerializeToArray(buf, ClientMsg.ByteSizeLong());
WebSocket->Send(Data, ClientMsg.ByteSizeLong(), true);

同理,因为protobuf消息为字节流形式,所以unreal端在接收时需要使用OnBinaryMessage而不是OnMessage,需要处理下一个包过大被分成多个Fragment的情况

WebSocket->OnBinaryMessage().AddRaw(this, &AIGameCoreNetwork::OnReceiveMessage);

void AIGameCoreNetwork::OnReceiveMessage(const void* Data, SIZE_T Size, bool bIsLastFragment)
{
	//UE_LOG(LogAIGameCoreNetwork, Log, TEXT("[ReceiveMessage]: %s"), *ReceivedMsg);
	
	if(ReceivedIndex + Size >= MAX_RECEIVE_BUFFER_SIZE)
	{
		UE_LOG(LogAIGameCoreNetwork, Error, TEXT("Receive Buffer runs out!"));
		return;
	}
	memcpy(&ReceiveBuffer[ReceivedIndex], Data, Size);
	ReceivedIndex += Size;
	
	if(bIsLastFragment)
	{
		ReceiveAIServerMsgEvent.Broadcast(ReceiveBuffer, ReceivedIndex);
		ReceivedIndex = 0;
	}
}
  1. Debug打印pb消息可以使用message.DebugString()获取debug字符串,格式和proto文件中定义的类似,非常易读
  2. 可以利用MessageToJson和JsonStringToMessage两个库函数将pb消息和json进行转换
  3. protobuf消息中表示任意类型的消息一定要用google.protobuf.Any类型,使用Pack和Unpack解析,不能使用string然后自行ParseFromString解析,原因应该和前面一样消息是以bytes格式存储而不是字符串
  4. python访问pb中的枚举时不需要加枚举类型名,像下面这种直接使用Msg.A就可以了
message Msg{
    enum MsgType{
        A = 0;
        B = 1;
    }
    
    MsgType MsgType = 1;
    google.protobuf.Any MsgContent = 2;
}
  1. Unreal端不建议直接include生成的proto生成的类文件使用对应的类型,一方面是过多类型都直接依赖proto类文件可能存在隐患,另一方面是proto生成的是c++类型,但unreal中很多类型都和c++有区别,直接使用起来会很不方便。所以建议加一层转译,由某个管理类负责,通过构造函数将proto类型转为传统的unreal类型(比如NPCAction(protoClass)转为FNPCAction(UStruct))

Websocket注意事项

  1. Unreal引擎中自带了Websocket模块,在build.cs中添加依赖就可以直接使用
  2. 在创建websocket前需要先加载模块,可以在GameInstance或者Gamemode中加载
	FModuleManager::Get().LoadModuleChecked(TEXT("WebSockets"));

	WebSocket = FWebSocketsModule::Get().CreateWebSocket(ServerURL, ServerProtocol);
  1. python服务器调用serve函数后,只有当收到消息后handler函数才会被调用(包括客户端连接消息),然后可以把对应的websocket存起来,之后就可以用这个websocket主动发消息给客户端
self.__server = websockets.serve(self.__handler, self.__host, self.__port)

async def __handler(self, websocket):
	self.__websocket = websocket

    async for message in websocket:
    	await self.msglistener(message)
  1. python server端需要以异步方式调用发送消息函数,可以将消息加入队列中用单独的线程发送
    async def send(self, message):
        await self.__websocket.send(message)
  1. websocket可以通过try-except的方式来检测客户端断开连接,当客户端断开连接时send和recv都会抛出websockets.exceptions.ConnectionClosed
        try:
            await self.__websocket.send(msg)
        except websockets.exceptions.ConnectionClosed:
            self.__on_client_disconnect()
        try:
        	async for message in websocket:
            	self.msglistener(message)
        except websockets.exceptions.ConnectionClosed:
            self.__on_client_disconnect()

总结

在unreal客户端这边,主要需要注意下protobuf的解析为字节流形式的问题,以及websocket使用binarymessage进行发送和接收
python服务器不需要关心类型问题,注意下需要先收到客户端消息才能获取到对应的websocket,然后需要保存下来用于之后主动发消息就可以了。因为发消息本身是异步,如果直接由逻辑类型发送就需要大部分函数都是async,可以考虑逻辑类型仅将消息放进队列中,然后有一个独立的线程以一定时间间隔发送消息

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用WebSocketprotobuf与设备连接通信的具体步骤如下: 1.定义protobuf格式的消息类 首先,需要定义protobuf格式的消息类。这个类将被用于序列化和反序列化数据。例如,可以定义一个名为DeviceMessage的类: ```protobuf syntax = "proto3"; package com.example.proto; message DeviceMessage { int32 id = 1; string name = 2; bool enabled = 3; } ``` 2.使用WebSocket与设备建立连接 使用WebSocket与设备建立连接。在Java中,可以使用Spring框架提供的WebSocket支持来实现WebSocket通信。例如,可以定义一个名为DeviceWebSocketHandler的类: ```java @Component public class DeviceWebSocketHandler extends TextWebSocketHandler { @Override public void handleTextMessage(WebSocketSession session, TextMessage message) { // 处理接收到的消息 String payload = message.getPayload(); System.out.println("Received message: " + payload); } @Override public void afterConnectionEstablished(WebSocketSession session) { // 建立连接后的处理 System.out.println("Connection established"); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { // 关闭连接后的处理 System.out.println("Connection closed"); } } ``` 3.将数据序列化为protobuf格式并发送 在处理接收到的消息时,可以将数据反序列化为protobuf格式。在发送消息时,需要将数据序列化为protobuf格式。例如,可以定义一个名为DeviceService的类: ```java @Service public class DeviceService { private final DeviceWebSocketHandler webSocketHandler; @Autowired public DeviceService(DeviceWebSocketHandler webSocketHandler) { this.webSocketHandler = webSocketHandler; } public void sendDeviceMessage(DeviceMessage message) throws Exception { byte[] data = message.toByteArray(); String payload = Base64.getEncoder().encodeToString(data); TextMessage textMessage = new TextMessage(payload); webSocketHandler.handleTextMessage(textMessage); } } ``` 4.在客户端接收数据并反序列化为protobuf格式 在处理接收到的消息时,可以将数据反序列化为protobuf格式。例如,可以定义一个名为DeviceClient的类: ```java public class DeviceClient extends WebSocketClient { private final DeviceMessageHandler messageHandler; public DeviceClient(String url, DeviceMessageHandler messageHandler) { super(URI.create(url)); this.messageHandler = messageHandler; } @Override public void onOpen(ServerHandshake handshakedata) { System.out.println("Connection established"); } @Override public void onClose(int code, String reason, boolean remote) { System.out.println("Connection closed"); } @Override public void onMessage(String message) { byte[] data = Base64.getDecoder().decode(message); try { DeviceMessage deviceMessage = DeviceMessage.parseFrom(data); messageHandler.handleMessage(deviceMessage); } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } @Override public void onError(Exception ex) { ex.printStackTrace(); } public static void main(String[] args) throws Exception { DeviceClient client = new DeviceClient("ws://localhost:8080/devices", new DeviceMessageHandler() { @Override public void handleMessage(DeviceMessage message) { System.out.println("Received message: " + message); } }); client.connect(); DeviceMessage deviceMessage = DeviceMessage.newBuilder() .setId(1) .setName("Device 1") .setEnabled(true) .build(); byte[] data = deviceMessage.toByteArray(); String payload = Base64.getEncoder().encodeToString(data); client.send(payload); } public interface DeviceMessageHandler { void handleMessage(DeviceMessage message); } } ``` 以上就是使用WebSocketprotobuf与设备连接通信的基本步骤。具体实现可能会因为业务场景的不同而有所不同,但是大致流程是相似的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值