【极客日常】在UE4插件中编写一个HTTP Web Server

在某些游戏研发or测试的需求中,需要在Unreal增加一个插件或者模块,里面启动一个服务器作为SDK,然后外部通过直连或者adb forward可以连接到客户端中,获取客户端实时的场景、actor信息等等。UE4本身除了socket server支持之外,也支持简单的HTTP Web Server。由于网上没有比较好的范例,因此这里给出一个例子。

本文以Unreal 4.24为例。搭建HTTP Server,需要在.Build.cs中引入如下模块:

PrivateDependencyModuleNames.AddRange(
    new string[]
    {
        "CoreUObject",
        "Engine",
        "Slate",
        "SlateCore",
        // ... add private dependencies that you statically link with here ...
        "HTTP",
        "HttpServer",
        "JsonUtilities",
        "Json",
    }
    );

通过FHttpServerModule::Get()方法,可以获得内置的HTTP Server模块的一个单例,该instance负责管理内置private的socket listeners。我们可以通过该单例获取HTTPRouter,然后绑定路由跟handler,然后调用StartAllListeners,就能够启动Web服务器。具体代码如下:

#include "Runtime/Online/HTTPServer/Public/HttpServerModule.h"
#include "Runtime/Online/HTTPServer/Public/HttpPath.h"

void Start()
{
    // 如果插件module type为runtime之类,只要包括编辑器的,就加这个判断,这样编辑器里不会直接启动server,而standalone时候可以启动
    // 如果编辑器里直接启动了,那么改代码重新编译会卡住
    if (!GIsEditor)
    {
        StartServer(Port);
    }
}

void StartServer(uint32 Port)
{
    auto HttpServerModule = &FHttpServerModule::Get();
    TSharedPtr<IHttpRouter> HttpRouter = HttpServerModule->GetHttpRouter(Port);
    // 这里注意一个点,就是底层不支持相同http path配置不同的request method
    HttpRouter->BindRoute(FHttpPath(TEXT("/health")), EHttpServerRequestVerbs::VERB_GET, HEALTH_CHECK_HANDLER);
    HttpServerModule->StartAllListeners();
}

其中,HEALTH_CHECK_HANDLER需要传进来一个TFunction,可以通过相关代码查阅到。

// Runtime\Online\HTTPServer\Private\HttpRouter.cpp

FHttpRouteHandle FHttpRouter::BindRoute(const FHttpPath& HttpPath,  const EHttpServerRequestVerbs& HttpVerbs,  const FHttpRequestHandler& Handler)
{
    check(HttpPath.IsValidPath());
    check(EHttpServerRequestVerbs::VERB_NONE != HttpVerbs);

    if (RequestHandlerRegistrar->Contains(HttpPath.GetPath()))
    {
        return nullptr;
    }

    auto RouteHandle = MakeShared<FHttpRouteHandleInternal>(HttpPath.GetPath(), HttpVerbs, Handler);
    RequestHandlerRegistrar->Add(HttpPath.GetPath(), RouteHandle);

    return RouteHandle;
}

// Runtime\Online\HTTPServer\Public\HttpRequestHandler.h

/**
 * FHttpRequestHandler
 *
 *  NOTE - Returning true implies that the delegate will eventually invoke OnComplete
 *  NOTE - Returning false implies that the delegate will never invoke OnComplete
 * 
 * @param Request The incoming http request to be handled
 * @param OnComplete The callback to invoke to write an http response
 * @return True if the request has been handled, false otherwise
 */
typedef TFunction<bool(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)> FHttpRequestHandler;

// Runtime\Online\HTTPServer\Public\HttpResultCallback.h

/**
* FHttpResultCallback
* This callback is intended to be invoked exclusively by FHttpRequestHandler delegates
* 
* @param Response The response to write
*/
typedef TFunction<void(TUniquePtr<FHttpServerResponse>&& Response)> FHttpResultCallback;

在FHttpRequestHandler函数内部中,如果调用了OnComplete(Response)return false的话,会CHECK不过造成程序crash。因此,可以封装一个生成FHttpRequestHandler的函数,使得实际只需要根据Request返回一个Response就可以。我们把这种函数自定义为FHttpResponser

typedef TFunction<TUniquePtr<FHttpServerResponse>(const FHttpServerRequest& Request)> FHttpResponser;

FHttpRequestHandler CreateHandler(const FHttpResponser& HttpResponser)
{
    return [HttpResponser](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
    {
        auto Response = HttpResponser(Request);
        if (Response == nullptr)
        {
            return false;
        }
        OnComplete(MoveTemp(Response));
        return true;
    };
}

然后我们实际只需要编写FHttpResponser就可以了。比如上面的HEALTH_CHECK_HANDLER例子如下:

TUniquePtr<FHttpServerResponse> HealthCheck(const FHttpServerRequest& Request)
{
    UE_LOG(UALog, Log, TEXT("Health Check"));
    if (GEngine != nullptr)
    {
        GEngine->AddOnScreenDebugMessage(-1, 10.0f, FColor::Green, TEXT("Health Check Successfully!"));
    }
    return SuccessResponse("Health Check Successfully!");
}

// HttpRouter->BindRoute(
//     FHttpPath(TEXT("/health")),
//     EHttpServerRequestVerbs::VERB_GET,
//     CreateHandler(&FBaseHandler::HealthCheck));


TUniquePtr<FHttpServerResponse> SuccessResponse(TSharedPtr<FJsonObject> Data, FString Message)
{
    return JsonResponse(Data, Message, true, SUCCESS_CODE);
}

TUniquePtr<FHttpServerResponse> JsonResponse(TSharedPtr<FJsonObject> Data, FString Message, bool Success, int32 Code)
{
    TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject());
    JsonObject->SetObjectField(TEXT("data"), Data);
    JsonObject->SetStringField(TEXT("message"), Message);
    JsonObject->SetBoolField(TEXT("success"), Success);
    JsonObject->SetNumberField(TEXT("code"), (double)Code);
    FString JsonString;
    TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&JsonString);
    FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer);
    return FHttpServerResponse::Create(JsonString, TEXT("application/json"));
}

由于一般HTTP返回的body是json,因此可以像上述一样另外封装作为模板response body的函数。对于request body,要转换为json,可以另外加函数去获取TSharedPtr<FJsonObject>的request json body实例。首先检查header是否为json格式,然后转化为json。采用UTF8_TO_TCHAR方法可以支持转换中文。

TSharedPtr<FJsonObject> GetRequestJsonBody(const FHttpServerRequest& Request)
{
    // check if content type is application/json
    bool IsUTF8JsonContent = IsUTF8JsonRequestContent(Request);
    if (!IsUTF8JsonContent)
    {
        UE_LOG(UALog, Warning, TEXT("caught request not in utf-8 application/json body content!"));
        return nullptr;
    }

    // body to utf8 string
    TArray<uint8> RequestBodyBytes = Request.Body;
    FString RequestBodyString = FString(UTF8_TO_TCHAR(RequestBodyBytes.GetData()));

    // string to json
    TSharedRef<TJsonReader<>> JsonReader = TJsonReaderFactory<>::Create(RequestBodyString);
    TSharedPtr<FJsonObject> RequestBody;
    if (!FJsonSerializer::Deserialize(JsonReader, RequestBody))
    {
        UE_LOG(UALog, Warning, TEXT("failed to parse request string to json: %s"), *RequestBodyString);
        return nullptr;
    }
    return RequestBody;
}

bool IsUTF8JsonRequestContent(const FHttpServerRequest& Request)
{
    bool bIsUTF8JsonContent = false;
    for (auto& HeaderElem : Request.Headers)
    {
        if (HeaderElem.Key == TEXT("Content-type"))
        {
            for (auto& Value : HeaderElem.Value)
            {
                auto LowerValue = Value.ToLower();
                if (LowerValue.StartsWith(TEXT("charset=")) && LowerValue != TEXT("charset=utf-8"))
                {
                    return false;
                }
                if (LowerValue == TEXT("application/json") || LowerValue == TEXT("text/json"))
                {
                    bIsUTF8JsonContent = true;
                }
            }
        }
    }
    return bIsUTF8JsonContent;
}

这样,UE4的一个基本的C++ HTTP Web Server就成型了。笔者因之做了一个简单的模板,传送门在UnrealHttpAutomator~

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
要将从串口读取的数据显示在Web页面上,可以使用WebServer.h库的send()方法将数据作为响应发送到Web客户端。以下是一个示例代码,演示了如何从串口读取数据,并将其显示在Web页面上: ``` #include <WiFi.h> #include <WebServer.h> // WiFi网络信息 const char* ssid = "your_SSID"; const char* password = "your_PASSWORD"; WebServer server(80); // 创建Web服务器实例,监听端口80 void setup() { Serial.begin(9600); // 连接WiFi网络 WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi..."); } Serial.println(""); Serial.println("WiFi connected."); Serial.println("IP address: "); Serial.println(WiFi.localIP()); // 打印本地IP地址 // 声明Web服务器路由 server.on("/", handleRoot); server.begin(); // 启动Web服务器 Serial.println("Web server started."); } void loop() { server.handleClient(); // 处理Web客户端请求 // 从串口读取数据,发送到Web客户端 if (Serial.available()) { String data = Serial.readString(); server.send(200, "text/plain", data); } } // 处理根路由请求 void handleRoot() { server.send(200, "text/html", "<html><body><h1>Arduino Web Server</h1><p>Data from Serial Port: <span id='data'></span></p><script>setInterval(function() {var xhr = new XMLHttpRequest();xhr.onreadystatechange = function() {if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {document.getElementById('data').innerHTML = xhr.responseText;}};xhr.open('GET', '/data', true);xhr.send();}, 1000);</script></body></html>"); } // 处理数据路由请求 void handleData() { // 返回从串口读取的数据 if (Serial.available()) { String data = Serial.readString(); server.send(200, "text/plain", data); } else { server.send(200, "text/plain", ""); } } ``` 此代码创建一个Web服务器,其根路由(/)返回一个包含从串口读取的数据的HTML页面。在loop()函数,通过检查Serial.available()方法来检查串口是否有可用的数据。如果有,使用Serial.readString()方法读取数据,并使用server.send()方法将数据作为响应发送到Web客户端。在handleRoot()函数,返回一个HTML页面,其包含一个span元素,用于显示从串口读取的数据。使用JavaScript定时器定期向服务器发送一个GET请求,从而更新span元素的内容。需要注意的是,此代码仅用于演示如何将从串口读取的数据显示在Web页面上,实际应用可能需要根据具体需求进行修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值