使用 HTTP 请求下载文件的坑和技巧

使用 HTTP 可以请求下载文件,response 的结果就是文件的内容。
在下载一个文件之前可以先使用 HEAD 请求来只获取头,可以从 Content-Length 头获取到文件的大小。

1
2
3
4
5
6
7
// head request
TSharedRef<IHttpRequest> HttpHeadRequest = FHttpModule::Get().CreateRequest();
HttpHeadRequest->OnHeaderReceived().BindUObject(this, &UDownloadProxy::OnRequestHeadHeaderReceived);
HttpHeadRequest->OnProcessRequestComplete().BindUObject(this, &UDownloadProxy::OnRequestHeadComplete);
HttpHeadRequest->SetURL(InternalDownloadFileInfo.URL);
HttpHeadRequest->SetVerb(TEXT("HEAD"));
HttpHeadRequest->ProcessRequest();

在 UE 中需要通过监听 HTTP 请求的 OnHeaderReceived 派发来获得想要的头数据:

1
2
3
4
5
6
7
void UDownloadProxy::OnRequestHeadHeaderReceived(FHttpRequestPtr RequestPtr, const FString& InHeaderName, const FString& InNewHeaderValue)
{
if (InHeaderName.Equals(TEXT("Content-Length")))
{
InternalDownloadFileInfo.Size = UKismetStringLibrary::Conv_StringToInt(InNewHeaderValue);
}
}

之后就可以用 Get 方法来请求文件了:

1
2
3
4
5
6
7
8
9
// get request
TSharedRef<IHttpRequest> HttpRequest = FHttpModule::Get().CreateRequest();
HttpRequest->OnRequestProgress().BindUObject(this, &UDownloadProxy::OnDownloadProcess, bIsSlice?EDownloadType::Slice:EDownloadType::Start);
HttpRequest->OnProcessRequestComplete().BindUObject(this, &UDownloadProxy::OnDownloadComplete);
HttpRequest->SetURL(InternalDownloadFileInfo.URL);
HttpRequest->SetVerb(TEXT("GET"));
RangeArgs = TEXT("bytes=0-")+FString::FromInt(FileTotalByte);
HttpRequest->SetHeader(TEXT("Range"), RangeArgs);
HttpRequest->ProcessRequest();

其中 Range 头的格式为:Byte=0-指请求整个文件的大小,Byte=0-99则是请求前 100byte,注意请求的范围不要超过文件大小 ,不然会有 400 错误。
通过控制 HTTP 请求的 Range 头,我们可以指定下载文件的任意部分,可以实现暂停继续 / 分片下载。

在 UE 中使用 HTTP 请求一个大文件的时候,如果该请求没有结束就去拿 response 的结果一定要注意一个问题:那就是 Response 的 Content 数据 Payload 是一个 TArray 动态数组,当 Content 的内容不断地被写入,会导致容器的 Reserve 也就是内存重新分配,获取该数组的内存地址是非常危险的。

所以建议在 HTTP 请求时先对 response 的 Content 的 Payload 进行 Reserve 使其能够容纳足够数量的数据,缺点就是会一次性占用整个文件的内存。
解决内存占用的办法就是通过 Http 请求的 Range 来实现分片下载(也就是把一个大文件分成数个小块,一块一块地下载),从而降低内存占用,

当下载文件后,通常还有进行文件校验的操作,等文件下载完之后再执行校验(如 MD5 计算)时间会很长,所以要解决校验的时间问题,想过开一个线程去计算,但是开线程只解决了不阻塞主线程,不会加速 MD5 的计算过程,后来想到 MD5 是摘要计算,进而联想到可不可以边下边进行 MD5 计算,根据 没有全新的轮子定理 (我瞎掰的),我查到了 OpenSSL 中的 MD5 实现支持使用MD5_Update 来增量计算,所以这个问题就迎刃而解了,具体看我前面的笔记MD5 的分片校验

基于上面这些内容,可以实现一个简陋的下载器功能了,可作为游戏中的下载组件,虽然看似简单,但是设计一个合理的结构和没有 bug 的版本还是要花点功夫的。
我把上面介绍的内容写成了一个插件:

HTTP 的分片请求在服务端的 Log:

资料文档: