게임 엔진/Unreal

[Unreal] [Networking] Http Module 사용 및 API Handler 개발

AlgorFati 2022. 10. 3. 00:12

Unreal HTTP Request

HttpModule은 언리얼에서 Rest API를 편하게 이용할 수 있도록 설계될 모듈이다.

현재 프로젝트의 MyExample.Build.cs 파일에서 다음 모듈들을 포함시켜준다.

 

PrivateDependencyModuleNames.AddRange(
	new string[] 
	{ 
		"HTTP", "JsonUtilities", "Json" 
	});

 

Header

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "Runtime/Online/HTTP/Public/Http.h"
#include "TestProjectGameModeBase.generated.h"


UCLASS()
class TESTPROJECT_API ATestProjectGameModeBase : public AGameModeBase
{
	GENERATED_BODY()
	
public:

	ATestProjectGameModeBase(const class FObjectInitializer& ObjectInitializer);

	virtual void StartPlay() override;

	FHttpModule* Http;

	// http 요청에 사용할 함수
	UFUNCTION(BlueprintCallable)
		void HttpCall(const FString& InURL, const FString& InVerb);

	void OnResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);

};

 

Source

#include "TestProjectGameModeBase.h"
#include "Dom/JsonObject.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"

ATestProjectGameModeBase::ATestProjectGameModeBase(const class FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	// 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;
	Http = &FHttpModule::Get();
}

void ATestProjectGameModeBase::StartPlay()
{
	Super::StartPlay();
}

void ATestProjectGameModeBase::HttpCall(const FString& InURL, const FString& InVerb)
{
	TSharedRef<IHttpRequest> Request = Http->CreateRequest();
	Request->OnProcessRequestComplete().BindUObject(this, &ATestProjectGameModeBase::OnResponseReceived);

	//This is the url on which to process the request

	Request->SetURL(InURL);
	Request->SetVerb(InVerb);
	Request->SetHeader("Content-Type", TEXT("application/json"));
	
	TSharedRef<FJsonObject> RequestObj = MakeShared<FJsonObject>();
	RequestObj->SetStringField("input", "obj");
	RequestObj->SetStringField("voice", "obj");


	FString RequestBody;
	TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&RequestBody);
	FJsonSerializer::Serialize(RequestObj, Writer);

	Request->SetContentAsString(RequestBody);

	Request->ProcessRequest();

	UE_LOG(LogTemp, Warning, TEXT("HttpCall   url : %s\nrequestBody : %s"), *InURL, *RequestBody);
}


void ATestProjectGameModeBase::OnResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
	UE_LOG(LogTemp, Warning, TEXT("OnResponseReceived   url : %s\nrecv : %s"), *Request->GetURL(), *Response->GetContentAsString());

	//Create a pointer to hold the json serialized data
	TSharedPtr<FJsonObject> JsonObject;

	//Create a reader pointer to read the json data
	TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());

	//Deserialize the json data given Reader and the actual object to deserialize
	if (FJsonSerializer::Deserialize(Reader, JsonObject))
	{
		//Get the value of the json object by field name
		int32 recievedInt = JsonObject->GetIntegerField("customInt");

		UE_LOG(LogTemp, Warning, TEXT("HTTP request result   customInt : %d"), recievedInt);
	}
}

 

 

바이너리 파일을 보내야하는 경우도 있을 것이다. 다음 예제를 참고하자.

void FAPIHandler::Request_ASRRecognizeFormData(FString FileName)
{
	FHttpModule* Http = &FHttpModule::Get();
	const FString URL = GetURL();
	const FString Verb = POST;

	FString Path = FPaths::Combine(FPaths::ProjectContentDir(), FString(TEXT("Data")), FileName);

	TArray<uint8> UpFileRawData;
	FFileHelper::LoadFileToArray(UpFileRawData, *Path);

	// this part is up to you to define
	FString a = "-----BOUNDARY\r\n";
	FString b = "Content-Disposition: form-data; name=\"file\"; filename=\"" + FileName + "\"\r\n";
	FString c = "Content-Type: application/octet-stream\r\n\r\n";
	FString e = "\r\n-----BOUNDARY--\r\n";

	TArray<uint8> data;
	data.Append((uint8*)TCHAR_TO_UTF8(*a), a.Len());
	data.Append((uint8*)TCHAR_TO_UTF8(*b), b.Len());
	data.Append((uint8*)TCHAR_TO_UTF8(*c), c.Len());
	data.Append(UpFileRawData);
	data.Append((uint8*)TCHAR_TO_UTF8(*e), e.Len());


	TSharedRef<IHttpRequest> Request = Http->CreateRequest();
	Request->OnProcessRequestComplete().BindRaw(this, &FAPIHandler::OnResponseRecieved);
	Request->SetURL(URL);
	Request->SetVerb(Verb);
	Request->SetHeader(ContentType, ContentTypeFormData);
	Request->SetContent(data);
	Request->ProcessRequest();

	FString FuncName(__FUNCTION__);
	LogRequest(FuncName, URL, Verb, TEXT(""));
}

 

 

동작 방식 및 주의점

HttpModule을 통해 생성된 HttpRequest는 ProcessRequest 함수를 통해 실행되는데, 이 작업은 내부적으로 비동기로 처리된다. HttpModule 내부에서 사용하는 다른 스레드에서 이 작업이 수행되고, 작업이 완료되면 Request의 콜백으로 돌아오는 형태이다. 여기서 알아두어야 하는 부분은 Response 콜백이 호출되는 스레드는 GameThread라는 점이다. 그러므로 콜백에서 처리한 직렬화와 같은 작업들은 비동기로 처리되지 않을 수 있다.

 

// ..
// GameThread ..

AsyncTask(ENamedThreads::AnyThread, [this]()
{
	// AnyThread ..
	TSharedRef<IHttpRequest> Request = Http->CreateRequest();
    
	Request->OnProcessRequestComplete().BindUObject(this, &ATestProjectGameModeBase::OnResponseReceived);
	
	Request->SetURL(InURL);
	Request->SetVerb(InVerb);
	Request->SetHeader("Content-Type", TEXT("application/json"));
	
	TSharedRef<FJsonObject> RequestObj = MakeShared<FJsonObject>();
	RequestObj->SetStringField("input", "obj");
	RequestObj->SetStringField("voice", "obj");


	FString RequestBody;
	TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&RequestBody);
	FJsonSerializer::Serialize(RequestObj, Writer);

	Request->SetContentAsString(RequestBody);

	Request->ProcessRequest();
});

// ..

 

 

 

API Handler 개발

먼저 APIHandler를 정의한다.

APIHandler는 API의 Request, Response에 대한 인터페이스를 제공한다. 어떤 데이터 타입들이 Request, Response를 통해 전달되는지 APIDataTypes에 정의하고, Handler는 실제 Request 코드와 Response의 콜백으로 구성된다.

 

APIDataTypes

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

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "APIDataTypes.generated.h"


class UNREALEXAMPLES_API APIDataTypes
{
public:

};


USTRUCT(BlueprintType)
struct UNREALEXAMPLES_API FData
{
	GENERATED_USTRUCT_BODY()
public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		int32 Number;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		int64 Begin;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		int64 End;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		FString Text;
};

USTRUCT(BlueprintType)
struct UNREALEXAMPLES_API FSampleRequestBody
{
	GENERATED_USTRUCT_BODY()
public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		FString Data;
};


USTRUCT(BlueprintType)
struct UNREALEXAMPLES_API FSampleResponseBody
{
	GENERATED_USTRUCT_BODY()
public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		TArray<FData> DataList;
};

 

 

APIHandler

APIHandler는 HTTPRequest 오브젝트의 OnProcessRequestComplete 콜백에 귀찮은 파싱작업을 처리해주는 함수를 연결시켜서 API 응답을 받는다. 하지만 API 응답이 늦게 오는 경우 응답이 오기 전에 프로그램이 종료될 수 있다. (ex. 에디터를 끄는 경우) 이 경우 콜백을 BindRaw를 통해 연결시켰다면 Raw 포인터가 dangling 되기 때문에 문제가 생긴다. 이 문제를 해결하기 위해 APIHandler는 TSharedFromThis를 상속받는 형태로 구현하고, OnProcessRequestComplete 에 콜백을 연결시킬때는 BindRaw가 아니라  BindSP로 연결시킨다. 이렇게 하면 OnResponseRecieved 함수에서 this 포인터가 이미 파괴된 경우를 this->DoesSharedInstanceExist() 와 같은 방법으로 걸러낼 수 있다. (물론 APIHandler를 컴포넌트나 Actor 형태로 구현한다면 언리얼 메모리 관리 시스템을 타기 때문에 이 귀찮은 작업이 필요없다.)

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

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "Runtime/Online/HTTP/Public/Http.h"
#include "APIDataTypes.h"
#include "Templates/SharedPointer.h"

DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnResponseRecievedSignature, FHttpRequestPtr, FHttpResponsePtr, bool);

DECLARE_MULTICAST_DELEGATE_OneParam(FSamepleResponseSignature, const FSampleResponseBody&);

struct UNREALEXAMPLES_API FAPIHandler : public TSharedFromThis<FAPIHandler>
{
public:
	const static FString GET;
	const static FString POST;
	const static FString ContentType;
	const static FString ContentTypeJson;
	const static FString ContentTypeFormData;
	const static int Success = 200;
	const static int Fail = 500;

public:

	void RequestSample(const FSampleRequestBody& InBodyData);

private:
	void OnResponseRecieved(FHttpRequestPtr InRequest, FHttpResponsePtr InResponse, bool bWasSuccessful);
	void ProcessResponses(FHttpRequestPtr InRequest, FHttpResponsePtr InResponse, bool bWasSuccessful);

	void ResponseSample(FHttpRequestPtr InRequest, FHttpResponsePtr InResponse);

public:
	FString GetURL() const;
	FString GetURL_Sample() const;

public:
	FSamepleResponseSignature OnResponseSample;

private:

	void LogRequest(const FString& InFuncName, const FString& InURL, const FString& InVerb, const FString& InBody) const;

	void LogResponse(const FString& InFuncName, const FString& InURL, const FString& InVerb, int32 InResponseCode, const FString& InBody) const;

private:
};

 

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


#include "APIHandler.h"
#include "Dom/JsonObject.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "JsonObjectConverter.h"
#include "Serialization/BufferArchive.h"

const FString FAPIHandler::GET = TEXT("GET");
const FString FAPIHandler::POST = TEXT("POST");
const FString FAPIHandler::ContentType = TEXT("Content-Type");
const FString FAPIHandler::ContentTypeJson = TEXT("application/json");
const FString FAPIHandler::ContentTypeFormData = TEXT("multipart/form-data; boundary=---BOUNDARY");

// requests
void FAPIHandler::RequestSample(const FSampleRequestBody& InBodyData)
{
	FHttpModule* Http = &FHttpModule::Get();

	const FString URL = GetURL_Sample();
	const FString Verb = POST;

	FString OutJsonString;
	FJsonObjectConverter::UStructToJsonObjectString(InBodyData, OutJsonString);

	TSharedRef<IHttpRequest> Request = Http->CreateRequest();
	Request->OnProcessRequestComplete().BindSP(this, &FAPIHandler::OnResponseRecieved);
	Request->SetURL(URL);
	Request->SetVerb(Verb);
	Request->SetHeader(ContentType, ContentTypeJson);
	Request->SetContentAsString(OutJsonString);
	Request->ProcessRequest();

	FString FuncName(__FUNCTION__);
	LogRequest(FuncName, URL, Verb, OutJsonString);
}



void FAPIHandler::OnResponseRecieved(
	FHttpRequestPtr InRequest, FHttpResponsePtr InResponse, bool bWasSuccessful)
{
	if (InRequest == nullptr)
	{
		UE_LOG(LogTemp, Error, TEXT("Request is nullptr."));
		return;
	}

	if (!InRequest->DoesSharedInstanceExist())
	{
		UE_LOG(LogTemp, Error, TEXT("Request does not exists."));
		return;
	}

	if (!this->DoesSharedInstanceExist())
	{
		UE_LOG(LogTemp, Error, TEXT("this does not exists."));
		return;
	}

	FHttpModule* Http = &FHttpModule::Get();
	const FString URL = InRequest->GetURL();

	FString FuncName(__FUNCTION__);
	LogResponse(FuncName, URL, InRequest->GetVerb(), InResponse->GetResponseCode(), InResponse->GetContentAsString());

	ProcessResponses(InRequest, InResponse, bWasSuccessful);
}

void FAPIHandler::ProcessResponses(FHttpRequestPtr InRequest, FHttpResponsePtr InResponse, bool bWasSuccessful)
{
	const FString URL = InRequest->GetURL();

	if (URL == GetURL_Sample())
	{
		ResponseSample(InRequest, InResponse);
	}
}


// responses
void FAPIHandler::ResponseSample(FHttpRequestPtr InRequest, FHttpResponsePtr InResponse)
{
	if (InResponse->GetResponseCode() == Success)
	{
		FSampleResponseBody ResponseBody;
		if (FJsonObjectConverter::JsonArrayStringToUStruct(
			InResponse->GetContentAsString(), &ResponseBody.DataList))
		{
			OnResponseSample.Broadcast(ResponseBody);
		}
	}
	else
	{

	}
}



FString FAPIHandler::GetURL() const
{
	return FString(TEXT("https://jsonplaceholder.typicode.com/"));
}

FString FAPIHandler::GetURL_Sample() const
{
	return GetURL() + FString(TEXT("todos/1"));
}


void FAPIHandler::LogRequest(const FString& InFuncName, const FString& InURL, const FString& InVerb, const FString& InBody) const
{
	UE_LOG(LogTemp, Log, TEXT("%s   url : %s   verb : %s\nbody : %s"), *InFuncName, *InURL, *InVerb, *InBody);
}

void FAPIHandler::LogResponse(const FString& InFuncName, const FString& InURL, const FString& InVerb, int32 InResponseCode, const FString& InBody) const
{
	UE_LOG(LogTemp, Log, TEXT("%s   url : %s   verb : %s  responseCode : %d  \nbody : %s"),
		*InFuncName, *InURL, *InVerb, InResponseCode, *InBody);
}

 

 

APITestGameModeBase

GameMode에서 APIHandler를 이용한다. 

 

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "Sound/SoundWave.h"
#include "API/APIDataTypes.h"
#include "API/APIHandler.h"
#include "APITestGameModeBase.generated.h"


UCLASS()
class UNREALEXAMPLES_API AAPITestGameModeBase : public AGameModeBase
{
	GENERATED_BODY()

public:

	AAPITestGameModeBase(const class FObjectInitializer& ObjectInitializer);

	virtual void StartPlay() override;
	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

public:

	UFUNCTION(BlueprintCallable)
	void TestRequest(const FSampleRequestBody& InRequestBody);
	void OnResponse(const FSampleResponseBody& InResponseBody);
	

public:


private:
	FAPIHandler APIHandler;
	FDelegateHandle ResponseHandle;
};

 

// Copyright Epic Games, Inc. All Rights Reserved.


#include "APITestGameModeBase.h"
#include "Dom/JsonObject.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "JsonObjectConverter.h"

#include "Dom/JsonObject.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"

#include "Misc/Base64.h"

AAPITestGameModeBase::AAPITestGameModeBase(const class FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	// 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;
}

void AAPITestGameModeBase::StartPlay()
{
	Super::StartPlay();

	ResponseHandle = APIHandler.OnResponseSample.AddUObject(
		this, &AAPITestGameModeBase::OnResponse);
	
}

void AAPITestGameModeBase::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	APIHandler.OnResponseSample.Remove(ResponseHandle);

	Super::EndPlay(EndPlayReason);
}

// tests
void AAPITestGameModeBase::TestRequest(const FSampleRequestBody& InRequestBody)
{
	APIHandler.RequestSample(InRequestBody);
}




void AAPITestGameModeBase::OnResponse(const FSampleResponseBody& InResponseBody)
{
}