HttpServerがあったので使ってみた


はじめに

初めまして、peke2と申します。
こちらは、Unreal Engine 4 (UE4) Advent Calendar 2020の7日目の記事になります。

UE4の勉強としてエンジンのソースを見ている時に、HttpServerというソースを見かけました。動作しているアプリにデータを送れると便利そうだなと思ったので使ってみました。HTTPなら既存のツールを使えばリクエストを簡単に送れるので、確認用に何か作る必要も無く良さそうです。

APIの使用方法がわからず、ソースを追って調べたので、使い方が間違っている箇所があるかもしれませんがご容赦ください。突っ込みがありましたらお願いします。

確認環境

  • UE4.25.4
  • Editor起動上で動作
  • Httpリクエストを送信するためのPC(MacでもWinodwsでもiPadでも可)

実装の流れ

C++でソースを記述したことがある方を対象としていますので、細かい手順は省いています。

1 . 新規C++クラスを追加。親はActorを選択。



2 . 名前は「HttpServerActor」にする(他の名前にする場合は以降を置き換えて読んでください)。
3 . HttpServerActor.cpp / HttpServerActor.h ファイルができるので内容を記述。

HttpServerActor.cpp

※listenするポート、api名は適宜変更してください。既に使用されているポートを指定するとlistenに失敗します。
HttpServerActor.cpp
// Fill out your copyright notice in the Description page of Project Settings.

#include "HttpServerActor.h"
#include "HTTPServer/Public/HttpPath.h"

#include "HTTPServer/Public/HttpResultCallback.h"
#include "HTTPServer/Public/HttpServerRequest.h"
#include "HTTPServer/Public/HttpServerResponse.h"
#include "Core/Public/Modules/ModuleManager.h"

// Sets default values
AHttpServerActor::AHttpServerActor()
{
    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;
    bClosed = false;
}

// Called when the game starts or when spawned
void AHttpServerActor::BeginPlay()
{
    Super::BeginPlay();

    Server = &FHttpServerModule::Get();
    UE_LOG(LogTemp, Log, TEXT("サーバー開始"));

    FHttpPath HttpPath(TEXT("/test_api")); // 指定した名前をURLに使用してアクセス(http://127.0.0.1/test_api でアクセス)
    Router = Server->GetHttpRouter(1080);  // 使用しているポートと重複しない任意のポートを指定

    RouteHandle = Router->BindRoute(
        HttpPath,
        EHttpServerRequestVerbs::VERB_GET | EHttpServerRequestVerbs::VERB_POST,
        [this](const FHttpServerRequest &Request, const FHttpResultCallback &OnComplete) {
            UE_LOG(LogTemp, Log, TEXT("リクエスト検出"));

            // 変換は FHttpConnectionRequestReadContext::ParseHeader() を参照
            FUTF8ToTCHAR WByteBuffer(reinterpret_cast<const ANSICHAR *>(Request.Body.GetData()), Request.Body.Num());
            FString str = WByteBuffer.Get();

            UE_LOG(LogTemp, Log, TEXT("%s"), *str);

            // クエリも参照可能
            // URLの「?」の後ろの部分、この場合「abc」と「name」 http://127.0.0.1/test_api?abc=123&name=hello
            for (auto param : Request.QueryParams)
            {
                UE_LOG(LogTemp, Log, TEXT("%s->%s"), *param.Key, *param.Value);
            }

            if (OnRequestReceived.IsBound())
            {
                OnRequestReceived.Broadcast(str);
            }

            OnComplete(FHttpServerResponse::Ok());

            return true;
        });

    Server->StartAllListeners();
}

void AHttpServerActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    if (bClosed)
    {
        return;
    }

    UE_LOG(LogTemp, Log, TEXT("Stop HTTP"));
    Server->StopAllListeners();

    if (Router)
    {
        Router->UnbindRoute(RouteHandle);
    }

    bClosed = true;
}

// Called every frame
void AHttpServerActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}


HttpServerActor.h
HttpServerActor.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "HTTPServer/Public/HttpServerModule.h"
#include "HTTPServer/Public/IHttpRouter.h"

#include "CoreMinimal.h"
#include "HttpServerActor.generated.h"

UCLASS()
class UE4TESTSCRIPTING_API AHttpServerActor : public AActor
{
    GENERATED_BODY()

private:
    FHttpServerModule *Server;
    TSharedPtr<IHttpRouter> Router;
    FHttpRouteHandle RouteHandle;
    bool bClosed;

public:
    // Sets default values for this actor's properties
    AHttpServerActor();

    DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FRequestReceivedDelegate, FString, Body);

    UPROPERTY(BlueprintAssignable, Category = "Http")
    FRequestReceivedDelegate OnRequestReceived;

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;
    virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

public:
    // Called every frame
    virtual void Tick(float DeltaTime) override;
};


4 . 生成されたソースの中にある <プロジェクト名>.build.cs に HttpServer モジュールを追加。

<プロジェクト名>.build.cs
PrivateDependencyModuleNames.AddRange(new string[] {"HttpServer"});

5 . コンパイル。
6 . Config/DefaultEngine.ini ファイルに以下の設定を追加。

DefaultEngine.ini
[HTTPServer.Listeners]
DefaultBindAddress="0.0.0.0"

※この設定が無いと127.0.0.1からのアクセスのみ許可した状態で起動するため、起動したPC以外からアクセスできません

7 . UE4エディターを再起動(新規でクラスを追加後などは必須)。
8 . HttpServerActor を親クラスに指定してActorを作成(名前は BP_HttpServer とする)。

9 . 作成した BP_HttpServer をレベルに配置。

10 . レベルブループリントを開いて、リクエストの文字列を受け取るためのイベントバインドを記述する。

11 . エディターからプレイ。
12 . curlなどのhttpリクエストを送れるものからリクエストを送信。

$ curl http://127.0.0.1:1080/test_api -X POST -d "はろー"

受け付けたリクエストの文字列がBluePrintのPrintStringで表示されます。文字列をJsonにすれば、色々と応用ができると思います。

※同じLAN内に接続した他のPCからリクエストを送信する場合は、エディターを起動しているPCのIPアドレスを指定してください。
※今回はリクエストのボディのみを取得しているので、リクエストの送信時にヘッダーは不要です。
※curlの入手方法や使用方法は他サイトをご確認ください。

おわりに

通信周りを実装せずに使えるので、お手軽で便利ですが、現状ではエディターから起動した時のみ有効です。エンジン側のHttpServerモジュールの設定が、エディターのみにしか見当たらなかったです。この辺り、APIのドキュメントに詳細とか具体的な説明があると良いのですが…
もしかしたらPythonを使う方が簡単だったかもしれません(未確認)。

APIの使い方を間違えてアサートに引っかかり、何度もエディターが落ちたのは辛かったですが、良い勉強になりました。