Unreal 啟用 HTTP Listener 來處理社群登入
前言
之前有提過,我的工作內容包含 iOS、Android 原生功能串接。不過除了原生能力之外,其實也有負責遊戲專案的會員系統,像是帳號登入、密碼修改、驗證流程等,都希望能整理成遊戲端盡量少碰平台細節的形式。
這篇記錄的,就是我在 Unreal 內處理社群登入 callback 的做法。

需求
原先我們提供的 UI 主要都是雙手機平台版本。至於 Unity 跟 Unreal 若要輸出到非手機平台,通常是由各專案自己實作 UI,我們只提供 API 呼叫方式。
但後來因為需求調整,希望連 UI 流程也能一起提供,所以就需要在 Unreal 端補上這段登入回傳的處理。
架構
整體設計邏輯是這樣:
由於部分平台對內嵌網頁有限制,所以登入流程改成外開瀏覽器進行。登入完成後,再透過 localhost callback 把資料傳回 client,因此 client 端必須啟動一個 HTTP listener 來接收回傳內容。
實作過程中發現相關文章不算多,很多範例也不太完整,所以把這次的做法整理成一篇,之後自己回頭看也比較方便。
由於我的專案是寫成 Plugin,因此有些地方未必能直接照貼到所有遊戲專案中,但整體思路是一樣的。
Module
先把會用到的 module 加進 Build.cs。
PublicDependencyModuleNames.AddRange(
new string[]
{
"HTTP",
"HTTPServer",
"Networking",
"Sockets",
"VaRest", // 我的專案是使用這個處理 Json
}
);Launcher
網路上有些做法會用 Actor 來實作,但因為我這裡不是直接控制遊戲專案本體,無法保證某個 actor 一定存在,所以改成用 UGameInstanceSubsystem 做一個啟動器,再由它生成一個 UHttpListener 的 UObject。
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "HttpLauncher.generated.h"
UCLASS()
class API_NAME UHttpLauncher : public UGameInstanceSubsystem
{
GENERATED_BODY()
private:
static TWeakObjectPtr<UHttpLauncher> InstanceRef;
public:
static UHttpLauncher* Instance();
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
/// <summary>
/// 啟動
/// </summary>
void Touch();
public:
// 強指針,避免被 GC 回收
UPROPERTY()
class UHttpListener* HttpListener;
};#include "HttpLauncher.h"
#include "HttpListener.h"
TWeakObjectPtr<UHttpLauncher> UHttpLauncher::InstanceRef = nullptr;
UHttpLauncher* UHttpLauncher::Instance()
{
return InstanceRef.Get();
}
void UHttpLauncher::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
InstanceRef = this;
}
void UHttpLauncher::Touch()
{
if (!InstanceRef.IsValid())
{
return;
}
HttpListener = NewObject<UHttpListener>(this);
if (IsValid(HttpListener))
{
HttpListener->Initialize();
}
}Listener
監聽器本身可以依需求再擴充。這邊我沒有把 port 寫死,主要是想避免 port 被佔用造成啟動失敗。
#pragma once
#include "CoreMinimal.h"
#include "HttpResultCallback.h"
#include "HttpServerRequest.h"
#include "HttpListener.generated.h"
UCLASS()
class API_NAME UHttpListener : public UObject
{
GENERATED_BODY()
private:
static TWeakObjectPtr<UHttpListener> InstanceRef;
public:
static UHttpListener* Instance();
public:
UHttpListener();
virtual ~UHttpListener();
private:
int32 ServerPort = 0;
bool bIsServerStarted = false;
public:
/// <summary>
/// 初始化
/// </summary>
void Initialize();
/// <summary>
/// 取得 HTTP Server 使用的 port
/// </summary>
int32 GetServerPort() const { return ServerPort; }
/// <summary>
/// 取得 HTTP Server 是否啟動中
/// </summary>
bool IsServerStarted() const { return bIsServerStarted; }
private:
/// <summary>
/// 啟動 HTTP Server
/// </summary>
void StartServer();
/// <summary>
/// 關閉 HTTP Server
/// </summary>
void StopServer();
/// <summary>
/// HTTP 請求 callback
/// </summary>
bool RequestCallback(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete);
/// <summary>
/// 尋找可用的 port
/// </summary>
int32 FindAvailablePort();
};#include "HttpListener.h"
#include "HttpPath.h"
#include "HttpServerHttpVersion.h"
#include "HttpServerModule.h"
#include "HttpServerResponse.h"
#include "IHttpRouter.h"
#include "SocketSubsystem.h"
#include "Sockets.h"
TWeakObjectPtr<UHttpListener> UHttpListener::InstanceRef;
UHttpListener::UHttpListener()
{
if (HasAnyFlags(RF_ClassDefaultObject))
{
return;
}
// 這裡原本想接前景 / 背景切換偵測,判斷玩家是否回到遊戲
InstanceRef = this;
}
UHttpListener::~UHttpListener()
{
StopServer();
}
UHttpListener* UHttpListener::Instance()
{
return InstanceRef.Get();
}
void UHttpListener::Initialize()
{
StartServer();
}
void UHttpListener::StartServer()
{
while (ServerPort == 0)
{
ServerPort = FindAvailablePort();
}
FHttpServerModule& HttpServerModule = FHttpServerModule::Get();
TSharedPtr<IHttpRouter> HttpRouter = HttpServerModule.GetHttpRouter(GetServerPort());
// 1. 綁定單一路由,例如 /data
HttpRouter->BindRoute(
FHttpPath(TEXT("/data")),
EHttpServerRequestVerbs::VERB_GET,
[this](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
return RequestCallback(Request, OnComplete);
});
// 2. 或者也可以註冊成 request preprocessor,接所有 request
HttpRouter->RegisterRequestPreprocessor(
[this](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
return RequestCallback(Request, OnComplete);
});
HttpServerModule.StartAllListeners();
bIsServerStarted = true;
}
void UHttpListener::StopServer()
{
FHttpServerModule& HttpServerModule = FHttpServerModule::Get();
HttpServerModule.StopAllListeners();
bIsServerStarted = false;
ServerPort = 0;
}
bool UHttpListener::RequestCallback(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
for (const auto& QueryParam : Request.QueryParams)
{
// 依照需求處理 query key / value
}
// 回傳 HTML,這裡可以改成導回指定頁面或顯示成功提示
FString ResponseHtml = TEXT("<html></html>");
TUniquePtr<FHttpServerResponse> Response =
FHttpServerResponse::Create(ResponseHtml, TEXT("text/html"));
OnComplete(MoveTemp(Response));
return true;
}
int32 UHttpListener::FindAvailablePort()
{
ISocketSubsystem* SocketSubsystem = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
TSharedRef<FInternetAddr> Addr = SocketSubsystem->CreateInternetAddr();
// Port = 0 時會自動分配可用 port
Addr->SetAnyAddress();
Addr->SetPort(0);
FSocket* Socket = SocketSubsystem->CreateSocket(NAME_Stream, TEXT("FindAvailablePort"), false);
const bool bBindSuccess = Socket->Bind(*Addr);
if (!bBindSuccess)
{
Socket->Close();
SocketSubsystem->DestroySocket(Socket);
return 0;
}
const int32 Port = Socket->GetPortNo();
Socket->Close();
SocketSubsystem->DestroySocket(Socket);
return Port;
}最後就是開啟後端驗證網址,讓使用者進行 Google、Facebook、Twitter、Apple 等登入,再把 token 或其他必要資訊加密後回傳到 localhost:Port,由 Unreal 端接收並解密處理。