目录
前言
本文以请求网络/本地时间API为例,介绍如何实现HTTP异步请求。
步骤
一、 搭建异步蓝图节点框架
1. 在“xxx.Build.cs”中添加“HTTP”模块
2. 新建一个空白C++类
添加反射
按UE规范命名
让创建的C++类继承自 UBlueprintAsyncActionBase
类
添加元数据标签,替代默认执行引脚
定义一个枚举类型 ESampleNetTimeType
,包含TaoBao
和 Local
枚举值,分别代表淘宝时间和本地时间。
定义一个蓝图可调用的静态函数 SampleHttpTimeAsyncAction,
该静态函数接受一个 UObject*
类型的世界上下文对象和一个 ESampleNetTimeType
枚举类型的参数,返回值是一个指向 USampleHttpTime
类对象的指针。
编译后可以看到此时蓝图节点 SampleHttpTimeAsyncAction,
如下所示,该节点已经隐藏了默认的Then执行引脚
在添加自定义输出引脚之前,需要先申明一个动态多播。其中bResult表示请求是否成功;Message用于传递错误信息或者成功提示;RespInfo是一个结构体,用于传递网络时间响应的详细信息。
定义三个可在蓝图中赋值的动态多播委托:Then
、OnSuccess
和 OnFail
,分别用于在操作执行后、操作成功和操作失败时触发
此时运行可以看到蓝图节点 SampleHttpTimeAsyncAction,
如下所示:
二、异步蓝图节点嵌入到引擎的执行流程
1. 定义成员变量:AsyncID
用于唯一标识异步操作,NetTimeType
用于存储网络时间类型,默认值为 ESampleNetTimeType::Local
构造函数传入初始化对象所需的各种信息
通过 Super(ObjectInitializer)
,将接收到的 FObjectInitializer
参数传递给基类的构造函数,以确保基类也能正确初始化
继续实现 SampleHttpTimeAsyncAction
静态函数,该函数创建一个 USampleHttpTime
对象,并设置其网络时间类型。RegisterWithGameInstance
函数用于将该异步操作对象注册到游戏实例中,确保其生命周期与游戏实例同步
在创建USampleHttpTime
对象后,“UBlueprintAsyncActionBase”类的“Activate”方法将被调用。接下来重写“UBlueprintAsyncActionBase”类的“Activate”方法
先调用父类的激活逻辑,然后在游戏线程上异步执行 Activate_Internal
方法,最后广播一个事件通知异步任务已经启动,需要等待完成。
此时运行如下:
此时当异步任务执行完毕后,我们希望“SampleHttpTimeAsyncAction”的“On Success”/“On Fail”引脚执行输出,需要继续在“Activate_Internal”函数中补充异步任务内容。
三、获取本地时间并异步返回
补充函数“Activate_Internal”实现如下,依据 NetTimeType
的值来确定是从网络获取时间,还是使用本地时间。若从网络获取时间,会设置相应的网络时间服务器 URL;若使用本地时间,则获取当前本地时间和 UTC 时间赋值给结构体“RespInfo”,并调用成功回调函数“CallOnSuccess”将“RespInfo”传递出去,最后释放资源。
函数“CallOnSuccess”、“CallOnFaild”、“RealeaseResources”定义和实现如下。其中:
“CallOnSuccess”函数的作用是在操作成功时,将成功的相关信息(异步任务 ID、结果状态、消息和时间响应信息)广播给所有绑定到 OnSuccess
委托的函数。
“CallOnFaild”函数的作用是在操作失败时,将失败的相关信息(异步任务 ID、结果状态、消息和时间响应信息)广播给所有绑定到 OnFail
委托的函数。
“RealeaseResources”函数的作用是释放 USampleHttpTime
对象占用的资源,具体包括清除委托绑定和标记对象为可销毁状态。其中, Clear
方法会移除所有绑定到 Then
委托的函数,释放委托占用的资源; SetReadyToDestroy
方法,将 USampleHttpTime
对象标记为可销毁状态,引擎会在合适的时候销毁该对象,释放占用的内存和其他资源。
结构体“RespInfo”定义如下
选择本地时间
打印返回的北京时间和UTC时间
此时打印结果如下,可以看到成功打印时间信息
四、获取网络时间并异步返回
获取网络时间的api接口地址:https://acs.m.taobao.com/gw/mtop.common.getTimestamp/
响应信息如下:
{
"api": "mtop.common.getTimestamp",
"v": "*",
"ret": [
"SUCCESS::接口调用成功"
],
"data": {
"t": "1744101991020"
}
}
导入所需头文件
实现发送HTTP请求的函数如下,需要设置请求的 URL、请求方法、请求头和请求体内容,当 HTTP 请求完成后执行回调函数“OnHttpRespReceived”。
定义返回时间信息的结构体
定义嵌套的结构体
实现接收HTTP响应的函数“OnHttpRespReceived”如下,如果请求和响应成功,就会解析响应中的 JSON 数据,提取时间戳并转换为 UTC 时间和北京时间,然后通过函数“CallOnSuccess”通知请求成功。
在蓝图中改使用网络api接口
此时运行就可以获取到网络返回的时间了
五、源码
SampleHttpTime.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Interfaces/IHttpRequest.h"
#include "HttpModule.h"
#include "Interfaces/IHttpResponse.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "SampleHttpTime.generated.h"
UENUM(BlueprintType)
enum class ESampleNetTimeType :uint8
{
TaoBao UMETA(DisplayName = "TaobaoTimeAPI"),
Local UMETA(DisplayName = "LocalTimeAPI"),
MaxNum UMETA(Hidden)
};
USTRUCT(BlueprintType)
struct STUDY_API FNetTimeRespDataInfo
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NetTime")
FString t = TEXT("");
};
USTRUCT(BlueprintType)
struct STUDY_API FSampleNetTimeRespMessage
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NetTime")
FString api = TEXT("");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NetTime")
FString v = TEXT("");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NetTime")
TArray<FString> ret;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NetTime")
FNetTimeRespDataInfo data;
};
USTRUCT(BlueprintType)
struct STUDY_API FSampleNetTimeRespInfo
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="NetTime")
FDateTime BeijingDateTime = FDateTime();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NetTime")
FDateTime UTCDateTime = FDateTime();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NetTime")
FSampleNetTimeRespMessage RespMessage;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NetTime")
FString RawMessage = TEXT("");
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(
FSampleNetTimeDelegate,
FGuid, AsyncID,
bool, bResult,
FString, Message,
FSampleNetTimeRespInfo, RespInfo
);
UCLASS(meta = (HideThen = true)) //隐藏默认的then引脚
class STUDY_API USampleHttpTime : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
USampleHttpTime(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
virtual ~USampleHttpTime();
UFUNCTION(
BlueprintCallable,
meta = (
BlueprintInternalUseOnly = "true", //表明该函数主要用于蓝图内部使用,通常不希望在蓝图库中直接显示。
WorldContext = "WorldContextObject",
DisplayName = "SampleHttpTimeAsyncAction", //指定函数在蓝图中的显示名称
Keywords = "Sample Net Time" //为函数添加搜索关键词,方便在蓝图中搜索该函数
),
Category = "Sample|NetTime"
)
static USampleHttpTime* SampleHttpTimeAsyncAction(UObject* WorldContextObject, ESampleNetTimeType InNetTimeType);
public:
virtual void Activate() override;
virtual void Activate_Internal();
public:
UPROPERTY(BlueprintAssignable)
FSampleNetTimeDelegate Then;
UPROPERTY(BlueprintAssignable)
FSampleNetTimeDelegate OnSuccess;
UPROPERTY(BlueprintAssignable)
FSampleNetTimeDelegate OnFail;
protected:
void SendHttp(const FString& InServerURL);
void OnHttpRespReceived(FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded);
void CallOnSuccess(FGuid InAsyncID, bool bInResult, FString InMessage, FSampleNetTimeRespInfo RespInfo);
void CallOnFaild(FGuid InAsyncID, bool bInResult, FString InMessage, FSampleNetTimeRespInfo RespInfo);
void RealeaseResources();
protected:
FGuid AsyncID = FGuid::NewGuid();
ESampleNetTimeType NetTimeType = ESampleNetTimeType::Local;
};
SampleHttpTime.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "HttpTime/SampleHttpTime.h"
#include "JsonObjectConverter.h"
#include "Async/Async.h"
#include "Async/TaskGraphInterfaces.h"
USampleHttpTime::USampleHttpTime(const FObjectInitializer& ObjectInitializer)
:Super(ObjectInitializer)
{
}
USampleHttpTime::~USampleHttpTime()
{
}
USampleHttpTime* USampleHttpTime::SampleHttpTimeAsyncAction(UObject* WorldContextObject, ESampleNetTimeType InNetTimeType)
{
USampleHttpTime* AsyncAction = NewObject<USampleHttpTime>();
AsyncAction->NetTimeType = InNetTimeType;
AsyncAction->RegisterWithGameInstance(WorldContextObject);
return AsyncAction;
}
void USampleHttpTime::Activate()
{
Super::Activate();
AsyncTask(ENamedThreads::GameThread, [this]() {
this->Activate_Internal();
});
FSampleNetTimeRespInfo RespInfo;
Then.Broadcast(AsyncID, false, TEXT("USampleHttpTime is just started, please wait to be finished!"), RespInfo);
}
void USampleHttpTime::Activate_Internal()
{
FString TimeServerURL = TEXT("");
switch (NetTimeType)
{
case ESampleNetTimeType::TaoBao:
TimeServerURL = TEXT("https://acs.m.taobao.com/gw/mtop.common.getTimestamp/");
break;
case ESampleNetTimeType::Local:
break;
case ESampleNetTimeType::MaxNum:
break;
default:
break;
}
if (!TimeServerURL.IsEmpty()) //如果TimeServerURL不是空字符串就使用网络时间,否则使用本地时间
{
SendHttp(TimeServerURL);
return;
}
else
{
FSampleNetTimeRespInfo RespInfo;
RespInfo.BeijingDateTime = FDateTime::Now();
RespInfo.UTCDateTime = FDateTime::UtcNow();
CallOnSuccess(AsyncID, true, TEXT("这是本地时间"), RespInfo);
RealeaseResources();
}
}
void USampleHttpTime::SendHttp(const FString& InServerURL)
{
FHttpRequestRef Request = FHttpModule::Get().CreateRequest();
Request->OnProcessRequestComplete().BindUObject(this, &USampleHttpTime::OnHttpRespReceived);
Request->SetURL(InServerURL);
Request->SetVerb("Post");
Request->SetHeader("Guid", AsyncID.ToString());
Request->SetHeader("Content-Type", "application/json");
Request->SetContentAsString(TEXT(""));
Request->ProcessRequest();
}
void USampleHttpTime::OnHttpRespReceived(FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded)
{
FString BackAsyncID = HttpRequest->GetHeader(TEXT("Guid"));
FSampleNetTimeRespInfo RespInfo;
if (bSucceeded && HttpRequest->GetStatus() == EHttpRequestStatus::Succeeded && HttpResponse->GetResponseCode() == 200)
{
FString ResponseJson = HttpResponse->GetContentAsString();
FSampleNetTimeRespMessage RespMessage;
bool bParseJson = FJsonObjectConverter::JsonObjectStringToUStruct(ResponseJson, &RespMessage);
if (bParseJson)
{
FString TickString = RespMessage.data.t;
int64 TickTime = FCString::Atoi64(*TickString) / 1000; //将时间戳字符串转换为 int64 类型,并将其转换为以秒为单位的时间戳
RespInfo.UTCDateTime = FDateTime::FromUnixTimestamp(TickTime); //将时间戳转换为 FDateTime 类型的 UTC 时间
RespInfo.BeijingDateTime = FDateTime::FromUnixTimestamp(TickTime + 8 * 60 * 60); //将时间戳转换为 FDateTime 类型的北京时间
RespInfo.RawMessage = ResponseJson;
RespInfo.RespMessage = RespMessage;
CallOnSuccess(AsyncID, true, TEXT("Succeed!"), RespInfo);
}
}
else
{
FString ErrorMessage = FString::Printf(TEXT("[%s],AsyncID:[%s],Receive NetTimeAsyncAction HttpResonpse Failed, Check Your Net"),
*FString(__FUNCTION__), *(AsyncID.ToString()));
UE_LOG(LogTemp, Error, TEXT("[%s]"), *ErrorMessage);
CallOnFaild(AsyncID, false, ErrorMessage, RespInfo);
}
}
void USampleHttpTime::CallOnSuccess(FGuid InAsyncID, bool bInResult, FString InMessage, FSampleNetTimeRespInfo RespInfo)
{
FSampleNetTimeDelegate TempDelegate = OnSuccess;
AsyncTask(ENamedThreads::GameThread, [=]() {
TempDelegate.Broadcast(InAsyncID, bInResult, InMessage, RespInfo);
});
}
void USampleHttpTime::CallOnFaild(FGuid InAsyncID, bool bInResult, FString InMessage, FSampleNetTimeRespInfo RespInfo)
{
FSampleNetTimeDelegate TempDelegate = OnFail;
AsyncTask(ENamedThreads::GameThread, [=]() {
TempDelegate.Broadcast(InAsyncID, bInResult, InMessage, RespInfo);
});
}
void USampleHttpTime::RealeaseResources()
{
Then.Clear();
OnSuccess.Clear();
OnFail.Clear();
SetReadyToDestroy();
}