using UnrealBuildTool ; public class UE4_Network : ModuleRules public UE4_Network ( ReadOnlyTargetRules Target ) : base ( Target ) PCHUsage = PCHUsageMode . UseExplicitOrSharedPCHs ; PublicDependencyModuleNames . AddRange ( new string [] "Core" , "CoreUObject" , "Engine" , "InputCore" , "WebSockets" }); // 增加WebSockets模块 PrivateDependencyModuleNames . AddRange ( new string [] { }); // Uncomment if you are using Slate UI // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); // Uncomment if you are using online features // PrivateDependencyModuleNames.Add("OnlineSubsystem"); // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true

1.2. 加载模块

wiki说In order to use this module, we need to load it. To do so, we will load it on game instance initialization(为了使用此模块,我们需要加载它。为此,我们将在游戏实例初始化时加载它),给出的例子是在GameInstance中Init的适合加载了模块。我尝试过在GameInstanceSubsystem的子类中加载,也可以正常使用,这两个类的生命周期相似。这次尝试在Actor中加载。(仅用来测试,不用在实际项目中,实际用的时候WS可能会需要作为单例)

新建一个Actor类,叫做ActorWebSocket。为了使用WS,需要包含头文件:

#include "WebSocketsModule.h"
#include "IWebSocket.h"

在BeginPlay时,加载WebSockets模块:

// Called when the game starts or when spawned
void AActorWebSocket::BeginPlay()
	Super::BeginPlay();
	FModuleManager::Get().LoadModuleChecked("WebSockets");

注意wifi中这个写法是错误的,没有使用Get:

// MyProjectGameInstance.cpp
#include "WebSocketsModule.h"
void UMyProjectGameInstance::Init()
    Super::Init();
    // Load the WebSockets module. An assertion will fail if it isn't found.
    FWebSocketsModule& Module = FModuleManager::LoadModuleChecked(TEXT("WebSockets"));

1.3. 创建Socket,绑定事件

先在ActorWebSocket.h中定义一些变量:

public:	
	// Sets default values for this actor's properties
	AActorWebSocket();
	const FString ServerURL="ws://127.0.0.1:23333";
	const FString ServerProtocol="ws";
	TSharedPtr<IWebSocket> Socket=nullptr;

在ActorWebSocket.h中声明一些方法,用于绑定事件:

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
	void OnConnected();
	void OnConnectionError(const FString& Error);
	void OnClosed(int32 StatusCode,const FString& Reason,bool bWasClean);
	void OnMessage(const FString& Message); // 接收消息时
	void OnMessageSent(const FString& MessageString); // 发送消息时

在ActorWebSocket.cpp中,实现这些方法。这里我仅简单的打印Log,具体实现的内容可以自由发挥:

void AActorWebSocket::OnConnected()
	UE_LOG(LogTemp,Warning,TEXT("%s"),*FString(__FUNCTION__));
void AActorWebSocket::OnConnectionError(const FString& Error)
	UE_LOG(LogTemp,Warning,TEXT("%s Error:%s"),*FString(__FUNCTION__),*Error);
void AActorWebSocket::OnClosed(int32 StatusCode, const FString& Reason, bool bWasClean)
	UE_LOG(LogTemp,Warning,TEXT("%s StatusCode:%d Reason:%s bWasClean:%d"),
		*FString(__FUNCTION__),StatusCode,*Reason,bWasClean);
void AActorWebSocket::OnMessage(const FString& Message)
	UE_LOG(LogTemp,Warning,TEXT("%s Message:%s"),*FString(__FUNCTION__),*Message);
void AActorWebSocket::OnMessageSent(const FString& MessageString)
	UE_LOG(LogTemp,Warning,TEXT("%s MessageString:%s"),*FString(__FUNCTION__),*MessageString);

在ActorWebSocket.cpp中的BeginPlay中,创建Socket并绑定这些事件:

// Called when the game starts or when spawned
void AActorWebSocket::BeginPlay()
	Super::BeginPlay();
	FModuleManager::Get().LoadModuleChecked("WebSockets");
	Socket=FWebSocketsModule::Get().CreateWebSocket(ServerURL,ServerProtocol);
	// Bind Events
	// Socket->OnConnectionError().AddLambda([](const FString& Error)->
	//         void{UE_LOG(LogTemp,Warning,TEXT("%s"),*Error)}); // Lambda不好看,改用绑定方法
	Socket->OnConnected().AddUObject(this,&AActorWebSocket::OnConnected);
	Socket->OnConnectionError().AddUObject(this,&AActorWebSocket::OnConnectionError);
	Socket->OnClosed().AddUObject(this,&AActorWebSocket::OnClosed);
	Socket->OnMessage().AddUObject(this,&AActorWebSocket::OnMessage);
	Socket->OnMessageSent().AddUObject(this,&AActorWebSocket::OnMessageSent);

1.4. 连接和关闭

一切都准备就绪,可以开始用起来了。连接和关闭的地方可以自由发挥,这里在BeginPlay和EndPlay(本来想直接放在BeginDestroy的,但有问题。其实Close用不用好像都行?)进行连接和关闭。EndPlay要先在.h中声明这个方法重写

// .h
protected:
    // ...
	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
// .cpp
void AActorWebSocket::BeginPlay()
    //...
	Socket->Connect();
void AActorWebSocket::EndPlay(const EEndPlayReason::Type EndPlayReason)
	Super::EndPlay(EndPlayReason);
	Socket->Close();

1.5. 发送和接收

使用Send发送信息。接收时会调用OnMessage,做相应的处理。新增一个方法MySend(),调用时发送当前游戏进行的时间,在BeginPlay中开启一个定时器,每隔1s调用一次MySend()。

// .h
public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;
	void MySend();
// .cpp
void AActorWebSocket::BeginPlay()
        // ...
	Socket->Connect();
	FTimerHandle TimerHandle;
	GetWorldTimerManager().SetTimer(TimerHandle,this,&AActorWebSocket::MySend,1,true,1);
void AActorWebSocket::MySend()
	if(Socket->IsConnected())
            Socket->Send(FString::SanitizeFloat(GetGameTimeSinceCreation()));

1.6. 使用

把这个ActorWebSocket直接拖到场景中,运行。发现在OutputLog中输出了连接失败的错误信息,触发了OnConnectionError事件,这说明WebSocket创建成功了,并进行了连接,但是没有连上Server。(当然了,因为还没有Server)

2. Python WebSocket Server

UE4那边的Client已经准备就绪,但一个巴掌拍不响,还需要建立Server才能测试。这里用Python建立Server,实现UE4使用WebSocket和外部进行通信。

2.1. 模块依赖

我使用的python3.7,为了接下来的测试顺利进行,需要包含下列模块。其中websockets版本为7.0,没有的话需要使用 pip install websockets 安装一下,其他是内置模块。

import websockets
import asyncio
import threading
import json
import time

2.2. 编写Server类

参考python-websockets这个库的文档,写出了下面这堆东西,也许不够优雅但能用...

private的部分,内部过程随便看看:

  • init():定义了类的成员变量
  • consumer_handler():执行consumer(),解析收到的消息
  • producer_handler():执行producer()获得要发送的消息,再执行send()发送消息
  • handler():管理消费者和生产者
  • consumer():解析收到的消息,现在只打印出来,并把isExecute=True
  • producer():遍历listcmd列表,pop首元素
  • connect():连接
  • add_cmd():接收输入,做成json字符串,保存在listcmd中

public的部分,用这里的就行了:

  • start_server():开启服务并等待连接
  • stop_server():关闭服务
  • send_time():发送时间
import websockets
import asyncio
import threading
import json
import time
# Server类
class MyServer:
    ##################################################
    # private
    ##################################################
    def __init__(self,host='127.0.0.1',port='23333'):
        self.__host=host # ip
        self.__port=port # 端口号
        self.__listcmd=[] # 要发送的信息的列表
        self.__server=None
        self.__isExecute=False # 是否执行完了上一条指令
        self.__message_value=None # client返回消息的value
    def __del__(self):
        self.stop_server()
    async def __consumer_handler(self,websocket,path):
        async for message in websocket:
            # await asyncio.sleep(0.001)
            await self.__consumer(message)
    async def __producer_handler(self,websocket,path):
        while True:
            await asyncio.sleep(0.000001)
            message = await self.__producer()
            if(message):
                await websocket.send(message)
    async def __handler(self,websocket, path):
        consumer_task = asyncio.ensure_future(self.__consumer_handler(websocket, path))
        producer_task = asyncio.ensure_future(self.__producer_handler(websocket, path))
        done, pending = await asyncio.wait([consumer_task, producer_task],return_when=asyncio.FIRST_COMPLETED,)
        for task in pending:
            task.cancel()
    # 接收处理
    async def __consumer(self,message):
        print('recv message: {0}'.format(message))
        self.__isExecute=True
        # jsonContent=json.loads(message)
        # self.__isExecute=jsonContent['IsExecute']
        # self.__message_value=jsonContent['Value']
    # 发送处理
    async def __producer(self):
        if len(self.__listcmd)>0:
            return self.__listcmd.pop(0)
        else:
            return None
    # 创建server
    def __connect(self):
        asyncio.set_event_loop(asyncio.new_event_loop())
        print('start connect')
        self.__isExecute=True
        if self.__server:
            print('server already exist')
            return
        self.__server=websockets.serve(self.__handler, self.__host, self.__port)
        asyncio.get_event_loop().run_until_complete(self.__server)
        asyncio.get_event_loop().run_forever()
    # 往要发送的命令列表中,添加命令
    def __add_cmd(self,topic,key,value=None):
        self.__message_value=None
        while self.__isExecute==False: # 没有收到处理
        content={'Topic':topic,'Data':{'Key':key,'Value':value}}
        jsonObj=json.dumps(content)
        self.__listcmd.append(jsonObj)
        print('add cmd: {0}'.format(content))
        self.__isExecute=False
    ##################################################
    # public
    ##################################################
    # 开启服务
    def start_server(self):
        print('start server at {0}:{1}'.format(self.__host,self.__port))
        t=threading.Thread(target=self.__connect)
        t.start()
    # 关闭服务
    def stop_server(self):
        print('stop server at {0}:{1}'.format(self.__host,self.__port))
        if self.__server is None:
            return
        self.__server.ws_server.close()
        self.__server=None
    # 发送时间
    def send_time(self):
        self.__add_cmd('Unreal','Time',time.strftime('%Y-%m-%d %H:%M:%S',time.localtime()))

2.3. 开启Server进行测试

创建这个类的实例,并调用public方法开启服务。在循环中,不断的发送时间(因为在add_cmd中做了判断,如果没收到从Client返回的消息,就不会发送下一条),发送5条后关闭Server。预期是发送一条时间,接收到Client发送的信息后,再发送下一条。

def main():
    s=MyServer('127.0.0.1',23333)
    s.start_server()
    count=0
    while count < 5:
        s.send_time()