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

5 min

前言

之前有提過,我的工作內容包含 iOS、Android 原生功能串接。不過除了原生能力之外,其實也有負責遊戲專案的會員系統,像是帳號登入、密碼修改、驗證流程等,都希望能整理成遊戲端盡量少碰平台細節的形式。

這篇記錄的,就是我在 Unreal 內處理社群登入 callback 的做法。

Unreal Engine
Unreal Engine

需求

原先我們提供的 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 做一個啟動器,再由它生成一個 UHttpListenerUObject

#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 端接收並解密處理。

Comments

Loading comments...