1. 项目概述:为什么我们需要一个专业的HTTP库?
在Unity里做网络请求,很多人的第一反应可能是用Unity自带的UnityWebRequest。这确实是个选择,但当你真正开始处理复杂的网络交互时——比如需要处理Cookie、管理连接池、实现文件分块上传下载、或者应对各种网络异常和重试逻辑——你就会发现,UnityWebRequest提供的功能更像是“毛坯房”,而我们需要的是一个“精装修”的工具包。这就是BestHTTP/3库存在的意义。
我最近在重构一个Unity项目,其中涉及大量的REST API调用、实时WebSocket通信以及资源的热更新下载。最初使用原生方案,代码里充斥着各种回调地狱、手动拼接Header、以及脆弱的错误处理。直到我重新审视并深度使用了BestHTTP/3(以下简称BestHTTP),整个网络层的代码才变得清晰、健壮且高效。这个库并非Unity官方出品,但它由社区资深开发者Tivadar György Nagy维护多年,在Asset Store上拥有极高的评价,其设计哲学完全围绕着游戏开发的实际需求展开:高性能、低开销、功能全面、以及最重要的——稳定可靠。
简单来说,BestHTTP是一个为Unity量身打造的全功能网络层解决方案。它不仅仅是一个HTTP客户端,更是一个涵盖了HTTP/1.1、HTTP/2、WebSocket、SignalR、Socket.IO、甚至包括一个轻量级HTTP服务器的完整网络栈。对于需要与后端服务器进行频繁、复杂数据交换的联网游戏、应用或工具,它能极大地提升开发效率和运行时的稳定性。今天,我就结合官方示例,带你深入这个库的核心,看看它如何解决我们实际开发中的那些痛点。
2. 核心功能与架构设计解析
BestHTTP的设计非常模块化,理解其架构是高效使用它的关键。它不是一个黑盒,而是一套你可以按需组合的工具集。
2.1 核心组件分层
整个库可以粗略分为以下几个层次:
- 协议层:这是最底层,负责实现HTTP/1.1、HTTP/2、WebSocket等协议的具体细节。作为使用者,我们通常不直接接触这一层。
- 核心请求/响应层:这是最常用的部分,以
HTTPRequest和HTTPResponse类为核心。你创建一个HTTPRequest对象,设置好URL、方法、回调,然后发送它。库会处理连接建立、数据发送、响应接收、并将结果封装在HTTPResponse中传递给你的回调函数。 - 高级功能模块:建立在核心层之上,提供了更便捷的封装。
- HTTPManager:单例类,是整个库的调度中心。它管理着全局的连接池、代理设置、Cookie存储、请求队列和生命周期(如
Update驱动)。很多全局配置都在这里进行。 - 连接池与复用:这是BestHTTP性能优异的关键。它会自动复用到达同一主机的TCP连接,避免了为每个请求都进行三次握手的开销,对于高频请求的场景提升巨大。
- Cookie引擎:自动管理会话Cookie,你无需手动从响应头中提取
Set-Cookie再设置到后续请求中,库会自动完成,行为与浏览器一致。 - 缓存系统:支持可配置的HTTP缓存,对于静态资源(如图片、配置文件)可以显著减少网络流量和加载时间。
- HTTPManager:单例类,是整个库的调度中心。它管理着全局的连接池、代理设置、Cookie存储、请求队列和生命周期(如
- 扩展协议支持:这是BestHTTP的杀手锏之一。
- WebSocket:提供了完整的WebSocket客户端实现,支持二进制和文本帧,事件驱动,使用起来比原生
System.Net.WebSockets在Unity中要方便得多。 - SignalR:直接支持微软的SignalR协议,对于需要实时双向通信的应用程序(如游戏聊天、实时状态同步)来说是开箱即用的解决方案。
- Socket.IO:同样为流行的Socket.IO库提供了原生支持,处理了其复杂的协议握手和消息包装。
- WebSocket:提供了完整的WebSocket客户端实现,支持二进制和文本帧,事件驱动,使用起来比原生
2.2 与UnityWebRequest的对比思考
为什么选择BestHTTP而不是UnityWebRequest?我们可以从几个维度来看:
- API设计:
UnityWebRequest的API更底层,需要你处理DownloadHandler和UploadHandler,虽然灵活但繁琐。BestHTTP的API更接近开发者直觉,一个Callback处理所有结果,对于常见的JSON、表单数据、文件上传都有便捷方法。 - 性能与开销:BestHTTP的连接池和高度优化的内部实现,在发起大量小请求时,其开销和速度通常优于
UnityWebRequest。这在手机网络环境下尤其重要。 - 功能完整性:
UnityWebRequest只是一个HTTP客户端。BestHTTP则是一个网络套件,WebSocket、高级缓存、Cookie管理、自动重试、超时控制等都是内置功能,无需自己再造轮子。 - 稳定性与维护:
UnityWebRequest在不同Unity版本间偶有行为差异。BestHTTP作为一个独立的、持续更新的资产包,其行为更加一致和可预测,并且有活跃的社区和论坛支持。
当然,UnityWebRequest是免费的且与Unity引擎集成度最高。但对于严肃的商业项目,尤其是重度依赖网络服务的项目,投资一个像BestHTTP这样的专业工具,从长期来看节省的开发和调试时间远超其成本。
3. 官方示例深度实操与解读
官方示例是学习BestHTTP的最佳入口。通常,在导入Asset包后,你会在Assets/Best HTTP/Examples目录下找到一系列场景。我们挑几个最核心的来拆解。
3.1 基础HTTP请求示例
我们从一个最简单的GET请求开始。官方示例中通常会有一个SimpleGET脚本。
using BestHTTP; using System; public class SimpleGETExample : MonoBehaviour { void Start() { // 1. 创建请求对象 var request = new HTTPRequest(new Uri("https://httpbin.org/get"), OnRequestFinished); // 2. (可选)设置方法,默认为GET request.Method = HTTPMethods.Get; // 3. (可选)添加请求头 request.AddHeader("User-Agent", "MyUnityGame/1.0"); // 4. 发送请求 request.Send(); } // 5. 请求完成回调 private void OnRequestFinished(HTTPRequest originalRequest, HTTPResponse response) { // 检查请求状态 switch (originalRequest.State) { case HTTPRequestStates.Finished: if (response.IsSuccess) // 例如状态码为2xx { Debug.Log($"请求成功!\n响应内容:{response.DataAsText}"); // 处理响应数据,例如解析JSON // var jsonObj = JSON.Parse(response.DataAsText); } else { Debug.LogError($"服务器返回错误。状态码:{response.StatusCode}, 消息:{response.Message}"); } break; case HTTPRequestStates.Error: Debug.LogError($"请求发生错误:{originalRequest.Exception?.Message}"); break; case HTTPRequestStates.Aborted: Debug.LogWarning("请求被中止。"); break; case HTTPRequestStates.ConnectionTimedOut: Debug.LogError("连接超时。"); break; case HTTPRequestStates.TimedOut: Debug.LogError("请求超时。"); break; } } }关键点解析:
- 状态(State)优先:回调中首先检查
originalRequest.State。Finished只代表HTTP事务完成(连接建立、请求发送、响应接收完毕),不意味着业务成功。必须再结合response.IsSuccess或response.StatusCode来判断业务逻辑是否成功。 - 异常处理:
Error、ConnectionTimedOut、TimedOut等状态对应了网络层的各种故障,必须妥善处理,给用户适当的反馈。 - 数据获取:
response.DataAsText获取文本响应,response.Data获取原始的字节数组。对于大文件,应使用流式处理,后面会提到。
3.2 处理JSON与POST请求
与后端API交互,JSON和POST是最常见的组合。
public class JSONPostExample : MonoBehaviour { [System.Serializable] // 让这个类可被Unity序列化,方便在Inspector中编辑,也方便JsonUtility使用 public class LoginPayload { public string username; public string password; } void Start() { var payload = new LoginPayload { username = "player1", password = "secret123" }; string jsonBody = JsonUtility.ToJson(payload); // 使用Unity内置的JsonUtility var request = new HTTPRequest(new Uri("https://api.yourserver.com/login"), HTTPMethods.Post, OnLoginFinished); // 关键:设置Content-Type头 request.SetHeader("Content-Type", "application/json"); // 设置请求体 request.RawData = System.Text.Encoding.UTF8.GetBytes(jsonBody); // 或者使用更便捷的辅助方法(如果库版本支持) // request.AddField("json", jsonBody); // 注意:这是表单格式,不是纯JSON request.Send(); } private void OnLoginFinished(HTTPRequest req, HTTPResponse resp) { if (req.State == HTTPRequestStates.Finished && resp.IsSuccess) { // 假设返回 { "token": "abc123", "userId": 1001 } string responseJson = resp.DataAsText; // 使用JsonUtility或第三方库(如Newtonsoft.Json)解析 Debug.Log($"登录成功,响应:{responseJson}"); } } }注意:这里有一个常见的坑。
request.AddField()方法通常用于添加表单字段(application/x-www-form-urlencoded),其内部会构建key=value&格式的字符串。如果你需要发送标准的JSON,应该直接设置RawData并指定Content-Type: application/json。很多后端框架(如Spring Boot, Express)会根据这个Header来决定如何解析请求体。
3.3 文件上传与下载(流式处理)
对于大文件,内存中一次性加载所有数据是不可取的。BestHTTP提供了流式接口。
文件上传(分块):
public class FileUploadExample : MonoBehaviour { public string filePath; // 例如 Application.persistentDataPath + "/bigfile.zip" void Start() { if (!File.Exists(filePath)) { Debug.LogError("文件不存在!"); return; } var request = new HTTPRequest(new Uri("https://yourserver.com/upload"), HTTPMethods.Post, OnUploadFinished); // 使用Stream作为请求体,库会以分块方式读取和发送 using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { request.SetHeader("Content-Type", "application/octet-stream"); // 设置流和其长度,这对服务器处理很有帮助 request.UploadStream = stream; request.UploadStreamLength = stream.Length; // 可以设置上传进度回调 request.OnUploadProgress = (req, uploaded, total) => { float progress = (float)uploaded / total; Debug.Log($"上传进度:{progress:P0}"); }; request.Send(); // 注意:Send()是异步的,不能在此处关闭stream。库会在完成后自动处理。 } // using块结束,但stream被request引用,不会立即关闭。 } private void OnUploadFinished(HTTPRequest req, HTTPResponse resp) { /* ... */ } }文件下载(流式保存):
public class FileDownloadExample : MonoBehaviour { public string downloadUrl; public string savePath; void Start() { savePath = Path.Combine(Application.persistentDataPath, "downloadedFile.zip"); var request = new HTTPRequest(new Uri(downloadUrl), OnDownloadFinished); // 启用流式响应,数据会一边接收一边写入文件,而不是全部缓存在内存 request.UseStreaming = true; request.StreamFragmentSize = 1024 * 64; // 64KB的片段大小 // 设置下载进度回调 request.OnDownloadProgress = (req, downloaded, total) => { if (total > 0) // 注意:服务器可能不返回Content-Length,此时total为-1 { float progress = (float)downloaded / total; Debug.Log($"下载进度:{progress:P0}"); } }; request.Send(); } private void OnDownloadFinished(HTTPRequest req, HTTPResponse resp) { if (req.State == HTTPRequestStates.Finished && resp.IsSuccess) { // 因为启用了UseStreaming,resp.Data可能为空或不全 // 正确的做法是在回调中处理已经流式写入的文件 Debug.Log($"文件已下载到:{savePath}"); // 如果你需要将整个响应体作为内存中的字节数组处理(仅适用于小文件),则不要启用UseStreaming // byte[] allData = resp.Data; } else { // 如果下载失败,删除可能已部分创建的文件 if (File.Exists(savePath)) { File.Delete(savePath); } } } }关键技巧:对于下载,UseStreaming = true结合OnDownloadProgress是实现进度条和避免大内存占用的标准做法。但请注意,启用流式后,你不能在回调中直接访问完整的resp.Data。如果你既想要进度又需要在内存中处理结果(例如下载一个JSON配置文件),一个折中的办法是不启用流式,但对于大文件要非常小心。
3.4 WebSocket连接实战
实时游戏功能离不开WebSocket。BestHTTP的WebSocket API是事件驱动的,非常清晰。
public class WebSocketChatClient : MonoBehaviour { private WebSocket.WebSocket webSocket; public string serverAddress = "ws://echo.websocket.org"; // 一个公开的测试服务器 void Start() { // 创建WebSocket实例 webSocket = new WebSocket.WebSocket(new Uri(serverAddress)); // 订阅事件 webSocket.OnOpen += OnWebSocketOpen; webSocket.OnMessage += OnWebSocketMessageReceived; webSocket.OnBinary += OnWebSocketBinaryReceived; webSocket.OnClosed += OnWebSocketClosed; webSocket.OnError += OnWebSocketError; // 开始连接 webSocket.Open(); } void OnDestroy() { // 务必在对象销毁时关闭连接,清理资源 if (webSocket != null && webSocket.IsOpen) { webSocket.Close(); } } private void OnWebSocketOpen(WebSocket.WebSocket ws) { Debug.Log("WebSocket 连接已打开!"); // 连接成功后,发送一条消息 ws.Send("Hello Server from Unity!"); } private void OnWebSocketMessageReceived(WebSocket.WebSocket ws, string message) { Debug.Log($"收到文本消息:{message}"); // 处理聊天消息、游戏指令等 } private void OnWebSocketBinaryReceived(WebSocket.WebSocket ws, byte[] data) { Debug.Log($"收到二进制数据,长度:{data.Length}"); // 处理二进制协议,如Protobuf // var parsedMessage = YourProtoParser.Parse(data); } private void OnWebSocketClosed(WebSocket.WebSocket ws, ushort code, string message) { Debug.Log($"WebSocket 连接关闭。代码:{code}, 原因:{message}"); webSocket = null; } private void OnWebSocketError(WebSocket.WebSocket ws, string error) { Debug.LogError($"WebSocket 错误:{error}"); } // 示例:从UI按钮调用发送消息 public void SendChatMessage(string msg) { if (webSocket != null && webSocket.IsOpen) { webSocket.Send(msg); } else { Debug.LogWarning("WebSocket未连接,无法发送消息。"); } } }核心要点:
- 生命周期管理:
OnDestroy中关闭连接至关重要,否则可能引起资源泄漏或服务器端连接残留。 - 线程安全:WebSocket的回调(
OnMessage,OnError等)可能在非主线程触发。如果你需要在回调中更新Unity的UI或操作GameObject,必须使用MainThreadDispatcher(BestHTTP提供)或UnityEngine.Dispatcher等方式将操作派发到主线程。 - 重连逻辑:生产环境必须实现重连机制。可以在
OnClosed或OnError事件中启动一个延迟计时器,尝试重新连接,并设置最大重试次数和指数退避策略。
4. 高级配置与性能调优
仅仅会用API还不够,要让BestHTTP在你的项目中发挥最大效能,必须了解其全局配置。
4.1 HTTPManager全局配置
HTTPManager是一个静态类,在应用启动时(如Awake)进行配置。
void Awake() { // 1. 连接池设置 - 对性能影响最大 HTTPManager.MaxConnectionPerServer = 10; // 默认4。增加到10-20可提升向同一主机并发请求的能力。 HTTPManager.KeepAliveDefaultValue = true; // 保持连接活跃,默认就是true,不要改。 HTTPManager.MaxPathLength = 512; // 最大URL路径长度 // 2. 超时与重试 HTTPManager.ConnectTimeout = TimeSpan.FromSeconds(20); // 连接超时 HTTPManager.RequestTimeout = TimeSpan.FromSeconds(60); // 请求总超时 HTTPManager.MaxRetries = 2; // 请求失败后自动重试次数(对非幂等操作如POST要小心) // 3. 代理与Cookie // HTTPManager.Proxy = new HTTPProxy(new Uri("http://proxy.example.com:8080"), "username", "password"); HTTPManager.IsCookiesEnabled = true; // 启用Cookie引擎 HTTPManager.CookieJarSize = 1024 * 10; // Cookie jar大小(字节) // 4. 日志与调试(开发阶段启用,发布时关闭) #if DEVELOPMENT_BUILD || UNITY_EDITOR HTTPManager.Logger.Level = BestHTTP.Logger.Loglevels.All; HTTPManager.RequestLogger.Level = BestHTTP.Logger.Loglevels.All; #else HTTPManager.Logger.Level = BestHTTP.Logger.Loglevels.Error; HTTPManager.RequestLogger.Level = BestHTTP.Logger.Loglevels.None; #endif // 5. 心跳(用于保持连接,特别是WebSocket) HTTPManager.HeartbeatManager.IsEnabled = true; HTTPManager.HeartbeatManager.PingFrequency = TimeSpan.FromSeconds(30); }调优建议:
MaxConnectionPerServer:这是最重要的参数之一。如果你的游戏需要同时从CDN下载多个小资源(如图集、配置文件),增加此值可以并行下载,显著减少总等待时间。但设置过高会占用过多系统资源,一般建议在6-15之间。MaxRetries:对于GET请求可以设置2-3次重试。对于POST、PUT等非幂等操作,务必设置为0,并在业务逻辑层手动处理重试,以避免重复提交订单、创建重复角色等问题。- 超时时间:移动网络环境不稳定,连接超时可以设长一些(如20-30秒),但请求总超时要根据具体业务设定。一个长时间的文件上传,可能需要几分钟的超时。
4.2 请求级别的精细控制
除了全局配置,每个HTTPRequest也可以单独设置。
var request = new HTTPRequest(...); request.ConnectTimeout = TimeSpan.FromSeconds(15); request.Timeout = TimeSpan.FromSeconds(45); // 此请求单独超时 request.DisableRetry = true; // 对此请求禁用重试 request.EnableTimoutForStreaming = false; // 对流式下载禁用超时(因为下载大文件本身就很耗时) request.Tag = "UserAvatarDownload"; // 给请求打标签,便于在日志或全局事件中识别 request.MaxRedirects = 5; // 最大重定向次数 // 启用缓存(如果服务器响应头允许) request.IsCacheable = true;4.3 使用连接复用提升性能
BestHTTP默认启用连接复用。你几乎不需要做额外工作,但理解其行为有助于调试。当你向https://api.example.com发起第一个请求时,库会建立一个TCP+TLS连接。在接下来的短时间内(由服务器Keep-Alive头或默认超时控制),向同一主机(api.example.com:443)发起的后续请求会复用这个连接,省去了昂贵的TLS握手和TCP慢启动过程。
你可以通过查看详细日志来确认连接复用是否生效。如果看到大量“Connecting to...”日志,可能意味着连接未被有效复用,需要检查是否频繁创建和销毁HTTPRequest对象,或者服务器端主动关闭了连接。
5. 实战避坑指南与疑难排查
在实际项目中踩过坑,才能积累真正有用的经验。下面是我总结的几个典型问题和解决方案。
5.1 问题一:在WebGL平台上请求失败或行为异常
现象:在编辑器和移动端运行正常的网络代码,发布到WebGL后出现CORS(跨域)错误、请求被阻塞或根本无法发出。
根因与解决: WebGL环境基于浏览器的XMLHttpRequest或Fetch API,受到严格的同源策略和CORS限制。
- CORS:如果你的后端API和WebGL游戏不在同一个域名下,服务器必须在响应头中设置
Access-Control-Allow-Origin: *或你的游戏域名。这是服务器端的配置,Unity端无法绕过。 - Credentials:如果请求需要携带Cookie或认证头,需要额外设置。在BestHTTP中:
同时,服务器的var request = new HTTPRequest(...); #if UNITY_WEBGL && !UNITY_EDITOR request.WithCredentials = true; // 告诉浏览器发送凭据(如Cookie) request.SetHeader("X-Requested-With", "XMLHttpRequest"); // 有时需要这个头 #endifAccess-Control-Allow-Origin不能是*,必须是具体的域名,并且需要设置Access-Control-Allow-Credentials: true。 - HTTPS/WS:WebGL要求所有非本地(
localhost)通信必须使用安全的HTTPS和WSS协议,HTTP和WS会被浏览器阻止。
5.2 问题二:移动设备(iOS/Android)上后台或锁屏后网络请求失败
现象:游戏切到后台或手机锁屏一段时间后,再切回前台,网络请求超时或直接失败。
根因与解决: 移动操作系统为了省电,可能会暂停应用的网络活动或强制关闭套接字。
- 连接保活:对于WebSocket或长连接,实现一个简单的心跳包(Ping/Pong)机制。BestHTTP的WebSocket有内置的Ping支持,确保定期发送数据包以保持连接活跃。
- 应用生命周期处理:在Unity的
OnApplicationPause事件中,主动关闭所有活跃的网络连接(特别是WebSocket),并在OnApplicationFocus恢复时重新连接。void OnApplicationPause(bool pauseStatus) { if (pauseStatus) { // 进入后台,关闭WebSocket if (webSocket != null && webSocket.IsOpen) { webSocket.Close(1000, "App Paused"); } // 也可以考虑取消所有未完成的HTTP请求 // HTTPManager.AbortAll(); } else { // 回到前台,尝试重连 StartCoroutine(ReconnectAfterResume()); } } - 请求超时设置:为移动设备设置更长的超时时间,以应对网络切换(Wi-Fi到4G)带来的短暂中断。
5.3 问题三:内存泄漏与对象生命周期管理
现象:游戏运行一段时间后内存持续增长,尤其是在频繁进行网络请求的场景。
根因与解决:
- 未取消的请求:如果你在场景切换或对象销毁时没有取消未完成的请求,这些请求及其回调可能仍然被库持有,导致关联的游戏对象无法被垃圾回收。
void OnDestroy() { if (_pendingRequest != null && !_pendingRequest.IsCancelled) { _pendingRequest.Abort(); // 中止请求 _pendingRequest = null; } // 同样,关闭WebSocket } - 回调中捕获的上下文:Lambda表达式或匿名方法如果捕获了当前类的成员(如
this),会形成闭包,阻止this被释放。确保在不需要时解除事件订阅。// 不好:lambda捕获了this request.Callback = (req, resp) => { this.ProcessResponse(resp); }; // 更好:使用弱引用或确保在对象销毁时清空Callback void OnDestroy() { if (request != null) { request.Callback = null; request.Abort(); } } - 大响应体的缓存:如果你下载了大文件到内存(
resp.Data),务必在处理完后及时释放引用,或直接使用流式下载保存到文件。
5.4 问题四:性能瓶颈诊断
现象:网络操作感觉卡顿,或者大量请求时帧率下降。
排查步骤:
- 开启详细日志:在开发阶段,将
HTTPManager.Logger.Level设为All。观察日志中是否有大量“Creating new connection...”或“Waiting for a free connection...”。前者表示连接复用失败,后者表示达到MaxConnectionPerServer限制,请求在排队。 - 使用性能分析器:在Unity Profiler的CPU模块中,查看
BestHTTP相关的函数调用耗时。如果Socket或Stream相关操作占用大量时间,可能是网络延迟本身的问题。 - 检查主线程阻塞:BestHTTP的回调默认在主线程执行。如果你的回调函数中进行了复杂的计算(如解析巨大的JSON),会阻塞游戏渲染。将耗时操作移到后台线程(如使用
Task.Run或ThreadPool),完成后再派发回主线程更新UI。 - 限制并发量:对于非紧急的请求(如日志上报、非关键数据拉取),可以实现一个简单的请求队列,控制同时活跃的请求数量,避免瞬间爆发拖慢系统。
5.5 一个完整的、健壮的请求封装示例
最后,分享一个我项目中常用的请求封装工具方法,它集成了超时、重试、日志和基本的错误处理:
using System; using System.Collections; using UnityEngine; public static class NetworkUtility { public delegate void RequestSuccessCallback<T>(T result); public delegate void RequestFailCallback(string error); public static IEnumerator SendRequest<T>( string url, HTTPMethods method, string jsonBody, RequestSuccessCallback<T> onSuccess, RequestFailCallback onFail, int maxRetries = 1, float timeoutSeconds = 30f) { int retryCount = 0; bool succeeded = false; while (retryCount <= maxRetries && !succeeded) { var request = new HTTPRequest(new Uri(url), method, (req, resp) => { if (req.State == HTTPRequestStates.Finished) { if (resp.IsSuccess) { try { T result = JsonUtility.FromJson<T>(resp.DataAsText); onSuccess?.Invoke(result); succeeded = true; } catch (Exception ex) { Debug.LogError($"JSON解析失败: {ex.Message}\nResponse: {resp.DataAsText}"); if (retryCount == maxRetries) onFail?.Invoke("数据解析错误"); } } else { Debug.LogWarning($"请求失败,状态码: {resp.StatusCode}. 第{retryCount+1}次重试。"); if (retryCount == maxRetries) onFail?.Invoke($"服务器错误: {resp.StatusCode}"); } } else if (req.State == HTTPRequestStates.Error || req.State == HTTPRequestStates.ConnectionTimedOut || req.State == HTTPRequestStates.TimedOut) { Debug.LogWarning($"网络错误: {req.State}. 第{retryCount+1}次重试。"); if (retryCount == maxRetries) onFail?.Invoke($"网络错误: {req.State}"); } else { // Aborted 等其他状态 if (retryCount == maxRetries) onFail?.Invoke($"请求被中止: {req.State}"); } }); if (!string.IsNullOrEmpty(jsonBody) && (method == HTTPMethods.Post || method == HTTPMethods.Put)) { request.SetHeader("Content-Type", "application/json"); request.RawData = System.Text.Encoding.UTF8.GetBytes(jsonBody); } request.Timeout = TimeSpan.FromSeconds(timeoutSeconds); request.DisableRetry = true; // 禁用库自带重试,我们自己控制 request.Tag = $"Retry_{retryCount}"; request.Send(); // 等待此请求完成或超时 float startTime = Time.time; while (request.State < HTTPRequestStates.Finished && (Time.time - startTime) < timeoutSeconds + 5) // 多等5秒缓冲 { yield return null; } if (!succeeded) { retryCount++; if (retryCount <= maxRetries) { Debug.Log($"开始第{retryCount}次重试..."); yield return new WaitForSeconds(Mathf.Pow(2, retryCount)); // 指数退避 } } } } }使用这个协程,你可以这样调用:
StartCoroutine(NetworkUtility.SendRequest<LoginResponse>( "https://api.example.com/login", HTTPMethods.Post, JsonUtility.ToJson(loginData), (response) => { Debug.Log($"登录成功,Token: {response.token}"); }, (error) => { Debug.LogError($"登录失败: {error}"); }, maxRetries: 2 ));这个封装处理了基本的重试逻辑、超时、JSON解析和错误分类,可以作为你项目网络层的一个坚实起点。记住,网络编程没有银弹,最重要的是理解原理、处理好异常、并在真实网络环境下充分测试。BestHTTP给了你一套强大的工具,而如何用好它,则取决于你对网络通信和Unity引擎本身的理解深度。