,

Unreal 啟用 HTTP Listener 來處理社群登入

在 Unreal 中利用開啟 HTTP Server 方式來執行 HTTP Listener

Unreal 啟用 HTTP Listener 來處理社群登入
Unreal Engine

前言

之前的文章有說到我的工作是處理 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
  }
);
Buils.cs

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;
}
HttpLauncher.h
#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();
  }
}
HttpLauncher.cpp

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();
}
HttpListener.h
#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;
}
HttpListener.cpp

最後開啟後端驗證網址,進行 Google、Facebook、Twitter、Apple 等相關登入,再將 token 等資訊加密傳回 localhost:Port 解密處理