构建一个更好的 Unity HTTP 客户端
作者: Eric_Lian
摘要
HTTP 是服务之间通信极其重要的协议,因此 Unity 提供了 UnityWebRequest 类来发起 HTTP 请求。然而 UnityWebRequest 在某些场景相当难用。此外,我们也知道 .NET 提供的 HttpClient 比 UnityWebRequest 更强大灵活,但根据 Unity 社区的讨论,IL2CPP 环境下 HttpClient 在某些情况下处理请求时可能会卡住、没有响应。
本文介绍 Milthm 团队在 Unity 中使用 HTTP 时遇到的问题、我们采取的手段、背后的原因,以及最终的解决办法。
背景
Milthm 自 2023 年 11 月 22 日 起引入了 HTTP 协议栈,当时底层使用的是 UnityWebRequest。说实话,UnityWebRequest 使用体验相当差,尤其是很难组织出一个合理的请求处理管线来向上层业务提供服务。
另一个常用的库是 RestSharp,它是 .NET 上很流行的 HTTP 客户端库,提供了简单直观的 API 来发起请求与处理响应。Milthm 从 1.10.0 版本 开始,将所有的 HTTP 请求都迁移到了 RestSharp,并维护了一个 Unity 版本的 RestSharp 分支,尽量保持与上游差异最小。
看起来好像解决了所有的烦恼,但在 Unity 中使用 RestSharp 时我们遇到了一些新的问题。
奇怪的问题
用了一段时间 RestSharp 后,有用户反馈 Milthm 打不开,具体表现为一直卡在加载界面,没有任何报错。
这个反馈令我感到莫名其妙,因为在构建 Milthm 的网络协议栈时候,我们的行为完全遵守了常见的客户端开发规范, 比如说设置了请求超时,并处理了所有可能出现的异常。按理说不应该卡住。
using var client = new RestClient(configureRestClient: cfg =>
{
cfg.FollowRedirects = false;
cfg.MaxTimeout = 2000;
});
var req = new RestRequest("https://google.com");
var resp = await client.ExecuteAsync(req);并且更惨的是,没人能稳定复现这个问题,也就是说找不到必现条件。唯一的解决办法就是重启游戏、重启设备网络、更换网络环境(从 WLAN 切到蜂窝数据,或者换个 WiFi)、重启设备等。但这些方法都不可靠。
从那时起,Milthm 与 HTTP 请求之间开始了一场持久战。
一些尝试
这个问题非常难复现。我只在我的电脑上调试 Milthm 时亲眼看到过一次。 那次我抓紧机会用调试器对他进行断点。 单步执行,进入 ExecuteAsync 行后发现代码挂在了 RestSharp 的内部。但另一个问题是:被卡住的函数外围包了 CancellationToken,而且 RestSharp 的整体流程写得也不错,从逻辑上看,RestSharp 的代码不像会有任何卡住的可能。 所以我猜可能是 RestSharp 或 Unity IL2CPP 运行时,或者 .NET 网络库里的某些 bug。
由于 2024 年 Milthm 对 HTTP 的需求不高,我决定暂时忽略这个问题,只是在有新版本时升级 RestSharp,并尽量跟进 Unity 的最新版本。
分析
很遗憾,到了 2025 年 8 月问题依旧存在。此时别无选择,只能正面对决。
Milthm 自 4.0.0 起提供了更多在线功能,包括用户账户、云存档、歌曲兑换等。 如果这些功能不稳定,用户体验会大受影响,我必须想办法。
略加思索,我梳理了 RestSharp 运行时的架构。Restsharp 是 HttpClient 的包装,而 HttpClient 会根据平台选择 HttpMessageHandler 来处理请求。
flowchart TB
App[Application Code]
App --> RestSharp
RestSharp --> RestClient
RestClient --> HttpClient
HttpClient --> HttpMessageHandler
HttpMessageHandler -->|Non-Browser| SocketsHttpHandler
HttpMessageHandler -->|Browser / WASM| BrowserHttpHandler
SocketsHttpHandler --> OSStack[OS Networking Stack]
BrowserHttpHandler --> BrowserAPI[Browser Networking APIs]
subgraph RS[RestSharp Layer]
RestClient
end
subgraph NET[System.Net.Http]
HttpClient
HttpMessageHandler
end
subgraph PLATFORM[Platform Specific Handlers]
SocketsHttpHandler
BrowserHttpHandler
end从上图可以看出,HttpClient 会根据平台选择不同的 HttpMessageHandler。于是我想,如果我直接把 HttpMessageHandler 换成一个由 UnityWebRequest 组成的版本会怎样,能成吗?
再深入查资料,发现可行!在使用时候可以通过 HttpClient 的构造函数 里配置自定义 HttpMessageHandler。
继续往下看,HttpMessageHandler 本质是一个处理 HTTP 请求的简单委托,只需要实现接收 HttpRequestMessage、返回 HttpResponseMessage 的 SendAsync 方法。
// https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Net.Http/src/System/Net/Http/HttpMessageHandler.cs
namespace System.Net.Http
public abstract class HttpMessageHandler : IDisposable
{
protected internal abstract Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
}一路绿灯,直接三下五除二实现一个 UnityWebRequest 版的 HttpMessageHandler,然后给他们拼起来就齐活了。
实现
下面是一个 UnityWebRequest 版的 UnityHttpMessageHandler 实现。
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks; // UniTask
using UnityEngine.Networking; // Unity Networking
public class UnityWebRequestHandler : HttpMessageHandler
{
public class Option
{
public int MaxRedirects { get; set; } = 32;
}
private readonly Option _option;
public UnityWebRequestHandler(Action<Option> option)
{
_option = new Option();
option.Invoke(_option);
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
await UniTask.SwitchToMainThread(cancellationToken);
using var uwr = await CreateUnityWebRequest(request);
// Process the request
var operation = uwr.SendWebRequest();
// Support CancellationToken
while (!operation.isDone)
{
if (cancellationToken.IsCancellationRequested)
{
uwr.Abort();
throw new TaskCanceledException();
}
await Task.Yield();
}
// After request is done, construct HttpResponseMessage
// Construct HttpResponseMessage
var response = new HttpResponseMessage((HttpStatusCode)uwr.responseCode);
// Respons Body
if (uwr.downloadHandler != null)
{
var bytes = uwr.downloadHandler.data ?? Array.Empty<byte>();
response.Content = new ByteArrayContent(bytes);
}
// Clone Headers
foreach (var kv in uwr.GetResponseHeaders())
{
response.Headers.TryAddWithoutValidation(kv.Key, kv.Value);
}
// Finish Response
response.RequestMessage = request;
response.ReasonPhrase = uwr.error;
return response;
}
/// <summary>
/// Construct Unity Web Request
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private async Task<UnityWebRequest> CreateUnityWebRequest(HttpRequestMessage request)
{
UnityWebRequest uwr;
// Method
var method = request.Method.Method;
// Constuct UnityWebRequest instance and set body
if (request.Content != null)
{
var body = await request.Content.ReadAsByteArrayAsync();
uwr = new UnityWebRequest(request.RequestUri, method)
{
uploadHandler = new UploadHandlerRaw(body),
downloadHandler = new DownloadHandlerBuffer()
};
}
else
{
uwr = new UnityWebRequest(request.RequestUri, method)
{
downloadHandler = new DownloadHandlerBuffer()
};
}
// Copy Settings
uwr.redirectLimit = _option.MaxRedirects;
// Copy Headers
foreach (var header in request.Headers)
{
foreach (var value in header.Value)
{
uwr.SetRequestHeader(header.Key, value);
}
}
if (request.Content != null)
{
foreach (var header in request.Content.Headers)
{
foreach (var value in header.Value)
{
uwr.SetRequestHeader(header.Key, value);
}
}
}
return uwr;
}
}有了这个 UnityWebRequestHandler,我们可以创建一个使用 UnityWebRequest 处理 HTTP 请求的 HttpClient 实例。
// 先创建 UnityWebRequestHandler 实例
using var handler = new UnityWebRequestHandler((it) =>
{
it.MaxRedirects = 0;
});
// 再创建 HttpClient 实例
using var httpClient = new HttpClient(handler);
// 如果你喜欢 RestSharp,最后拼装一个 RestClient
var entrestClient = new RestClient(httpClient, false,
configureRestClient: cfg =>
{
cfg.FollowRedirects = false;
// 注意: 这个设置是没用的,他不会复制到 HttpClient 里
// 原因见下
// https://github.com/restsharp/RestSharp/blob/261d94eba7e6ddd2ed59a17f420809d1346e92e7/src/RestSharp/RestClient.cs#L93
// https://github.com/restsharp/RestSharp/blob/261d94eba7e6ddd2ed59a17f420809d1346e92e7/src/RestSharp/RestClient.cs#L232
}
);这样,所有关于在 Unity IL2CPP 环境中使用 HttpClient 而偶发卡死的问题的问题就解决了。
结论
该特性已在 Milthm 4.4 中作为实验性功能发布。启用基于 UnityWebRequest 的 HttpMessageHandler 可以稳定规避 IL2CPP/HttpClient 出现的间歇性卡死问题。这个方案在大量设备上验证有效,问题得以解决。