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)
{
}
'게임 엔진 > Unreal' 카테고리의 다른 글
[Unreal] [DateTime] 시간 관리 구조체 (0) | 2023.09.05 |
---|---|
[Unreal] [Networking] TCP Socket을 이용한 Client 개발 (0) | 2022.08.27 |
[Unreal] [Example] Editor에 Asset 생성 및 저장 (7) | 2021.08.10 |
[Unreal] 플러그인과 모듈 (0) | 2021.03.24 |
[Unreal] Android 디버깅 방법들 (0) | 2021.03.18 |