Unreal 啟用 HTTP Listener 來處理社群登入
在 Unreal 中利用開啟 HTTP Server 方式來執行 HTTP Listener

前言
之前的文章有說到我的工作是處理 iOS、Android 的原生功能串接,但其實還負責了遊戲專案的會員系統功能,讓遊戲專案可以完全不用處理玩家的帳號、修改密碼、驗證等工作。
需求
原先我們提供的所有 UI 都是僅有雙手機平台的,Unity 跟 Unreal 要輸出非手機平台都是要由各專案自行實作,我們提供 API 呼叫而已,但不知道是什麼緣故下,長官們希望也可以提供 UI 讓專案選擇。
架構
整個設計的邏輯,由於考量到部分平台限制內嵌網頁的形式,所以使用了外開瀏覽器進行登入,再將資料以 localhost 的形式傳回給 Client,所以 Client 這邊需要開一個 Http Listner 來監聽回傳資料。
實作時發現相關的文章還蠻少的,ChatGPT 提供的方法也怪怪的,還常常給一堆不存在的 function,希望留下這篇文章給有需要的人,或日後還要再用時可以回顧。
由於我的專案是寫 Plugin ,所以可能有無法直接使用的地方。
Module
以下是我有使用到的 Module,先加進 Build.cs 裡
PublicDependencyModuleNames.AddRange(
new string[]
{
"HTTP",
"HTTPServer",
"Networking",
"Sockets",
"VaRest", // 我的專案是使用這個處理 Json
}
);
Launcher
在網路上有看過有人使用 Actor 實作,但因為我不是遊戲專案,沒辦法控制 Actor 是否存在,所以我改用另一種方式是寫 UGameInstanceSubsystem 物件來實作一個啟動器,再生成一個 HttpListener 的 UObject。
#pragma once
#include "CoreMinimal.h"
#include "Sybsystems/GameInstanceSybsystem.h"
#include "HttpLauncher.generated.h"
UCLASS()
class API_NAME UHttpLauncher : public UGameInstanceSubsystem
{
GENERATED_BODY()
private:
static TWeakObjectPtr<UHttpLauncher> _instance;
public:
static UHttpLauncher* Instance();
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
/// <summary>
/// 啟動
/// </summary>
void Touch();
public:
// 強指針,避免 GC
UPROPERTY()
class UHttpListener* _httpListener;
}
#include "HttpLauncher.h"
TWeakObjectPtr<UHttpLauncher> UHttpLauncher::_instance == nullptr;
UHttpLauncher* UHttpLauncher::Instance() { return _instance.Get(); }
void UHttpLauncher::Initialize(FSubsystemCollectionBase& Collection)
{
_instance = this;
}
void UHttpLauncher::Touch()
{
if (!_instance.IsValid())
{
return;
}
_httpListener = NewObject<UHttpListener>();
if (IsValid(_httpListener))
{
_httpListener->Initialize();
}
}
Listener
監聽器上可以針對需求來修改,因為我們的 port 不打算固定,主要是可以避免被佔用問題。
#pragma once
#include "CoreMinimal.h"
#include "HttpServerRequest.h"
#include "HttpResultCallback.h"
#include "HttpListener.generated.h"
UCLASS()
class API_NAME UHttpListener : public UObject
{
GENERATED_BODY()
private:
static TWeakObjectPtr<UHttpListener> _instance;
public:
static UHttpListener* Instance();
public:
UHttpListener();
~UHttpListener();
private:
int32 _serverPort = 0;
bool _isServerStarted = false;
public:
/// <summary>
/// 初始化
/// </summary>
void Initialize();
/// <summary>
/// 取得 HTTP Server 使用的 port
/// </summary>
/// <returns> HTTP Server port </returns>
int32 GetServerPort() const { return _serverPort; }
/// <summary>
/// 取得 HTTP Server 是否啟動中
/// </summary>
/// <returns> HTTP Server enable</returns>
bool IsServerStarted() const { return _isServerStarted; }
private:
/// <summary>
/// 啟動 HTTP Server
/// </summary>
void StartServer();
/// <summary>
/// 關閉 HTTP Server
/// </summary>
void StopServer();
/// <summary>
/// HTTP 請求回呼
/// </summary>
bool RequestCallback(const FHttpServerRequest& request, const FHttpResultCallback& OnComplete);
/// <summary>
/// 尋找可以使用的 Port
/// </summary>
/// <returns>可以使用的 Port </returns>
int32 FindAvaliablePort();
}
#include "HttpListener.h"
#include "HttpPath.h"
#include "IHttpRouter.h"
#include "HttpServerHttpVersion.h"
#include "HttpServerModule.h"
#include "HttpServerResponse.h"
#include "Sockets.h"
#include "SocketSubsystem.h"
UHttpListener::UHttpListener()
{
if (this->HasAnyFlags(RF_ClassDefaultObject)) { return; }
// 想在這裡加上偵測前景後景偵測,判斷玩家是否回到遊戲中
// 但還沒有找到方法
_instance = this;
}
UHttpListener::~UHttpListener()
{
StopServer();
}
TWeakObjectPtr<UHttpListener> UHttpListener::_instance;
UHttpListener* UHttpListener::Instance() { return _instance.Get(); }
void UHttpListener::Initialize()
{
StartServer();
}
void UHttpListener::StartServer()
{
// 取得可用的 Port
while (_serverPort == 0)
{
_serverPort = FindAvaliablePort();
}
FHttpServerModule& httpServerModule = FHttpServerModule::Get();
TSharedPtr<IHttpRouter> httpRouter = httpServerModule.GetHttpRouter(GetServerPort());
// 註冊或綁定路由
// 1. 方法一:綁定單一路由例如 www.example.com/data method = Get
httpRouter->BindRouter(
FHttpPath(TEXT("/data")),
EHttpServerRequestVerbs::VERB_GET,
[this](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete) { });
// 2. 方法二:綁定 root 路由
httpRouter->RegisterRequestPreprocessor(
[this](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete) {
return RequestCallback(Request, OnComplete);
}
);
httpServerModule.StartAllListeners();
_isServerStarted = true;
}
void UHttpListener::StopServer()
{
FHttpServerModule& httpServerModule = FHttpServerModule::Get();
_isServerStarted = false;
_serverPort = 0;
}
bool UHttpListener::RequestCallback(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
for (const auto& queryParam : Request.QueryParams)
{
// 針對需求處理 query key and value
}
// 處理 Response HTML,我們這裡是塞個轉址轉回伺服器指定頁面
FString responseHTML = FString::Printf(TEXT("<html></html>"));
TUniquePtr<FHttpServerResponse> response = FHttpServerResponse::Create(responseHTML, TEXT("text/html"));
OnComplete(MoveTemp(response));
return true;
}
int32 UHttpListener::FindAvaliablePort()
{
ISocketSubsystem* SocketSubsystem = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
TSharedRef<FInternetAddr> Addr = SocketSubsystem->CreateInternetAddr();
// 設定 Port = 0 會使 Socket 自動分配可使用的 Port
Addr->SetAnyAddress();
Addr->SetPort(0);
// 建立 Socket
FSocket* Socket = SocketSubsystem->CreateSocket(NAME_Stream, TEXT("FindAvailablePort"), flase);
// 綁定
bool bBindSuccess = Socket->Bind(*Addr);
// 如果端口已被佔用,會綁定失敗
if (!bBindSuccess)
{
Socket->Close();
return 0;
}
// 獲取 Socket 分配的端口
int32 Port = Socket->GetPortNo();
Socket->Close();
return Port;
}
最後開啟後端驗證網址,進行 Google、Facebook、Twitter、Apple 等相關登入,再將 token 等資訊加密傳回 localhost:Port 解密處理