news 2026/8/8 16:15:29

Unity网络开发实战:BestHTTP/3库核心功能与性能优化指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity网络开发实战:BestHTTP/3库核心功能与性能优化指南

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 核心组件分层

整个库可以粗略分为以下几个层次:

  1. 协议层:这是最底层,负责实现HTTP/1.1、HTTP/2、WebSocket等协议的具体细节。作为使用者,我们通常不直接接触这一层。
  2. 核心请求/响应层:这是最常用的部分,以HTTPRequestHTTPResponse类为核心。你创建一个HTTPRequest对象,设置好URL、方法、回调,然后发送它。库会处理连接建立、数据发送、响应接收、并将结果封装在HTTPResponse中传递给你的回调函数。
  3. 高级功能模块:建立在核心层之上,提供了更便捷的封装。
    • HTTPManager:单例类,是整个库的调度中心。它管理着全局的连接池、代理设置、Cookie存储、请求队列和生命周期(如Update驱动)。很多全局配置都在这里进行。
    • 连接池与复用:这是BestHTTP性能优异的关键。它会自动复用到达同一主机的TCP连接,避免了为每个请求都进行三次握手的开销,对于高频请求的场景提升巨大。
    • Cookie引擎:自动管理会话Cookie,你无需手动从响应头中提取Set-Cookie再设置到后续请求中,库会自动完成,行为与浏览器一致。
    • 缓存系统:支持可配置的HTTP缓存,对于静态资源(如图片、配置文件)可以显著减少网络流量和加载时间。
  4. 扩展协议支持:这是BestHTTP的杀手锏之一。
    • WebSocket:提供了完整的WebSocket客户端实现,支持二进制和文本帧,事件驱动,使用起来比原生System.Net.WebSockets在Unity中要方便得多。
    • SignalR:直接支持微软的SignalR协议,对于需要实时双向通信的应用程序(如游戏聊天、实时状态同步)来说是开箱即用的解决方案。
    • Socket.IO:同样为流行的Socket.IO库提供了原生支持,处理了其复杂的协议握手和消息包装。

2.2 与UnityWebRequest的对比思考

为什么选择BestHTTP而不是UnityWebRequest?我们可以从几个维度来看:

  • API设计UnityWebRequest的API更底层,需要你处理DownloadHandlerUploadHandler,虽然灵活但繁琐。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.StateFinished只代表HTTP事务完成(连接建立、请求发送、响应接收完毕),不意味着业务成功。必须再结合response.IsSuccessresponse.StatusCode来判断业务逻辑是否成功。
  • 异常处理ErrorConnectionTimedOutTimedOut等状态对应了网络层的各种故障,必须妥善处理,给用户适当的反馈。
  • 数据获取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等方式将操作派发到主线程。
  • 重连逻辑:生产环境必须实现重连机制。可以在OnClosedOnError事件中启动一个延迟计时器,尝试重新连接,并设置最大重试次数和指数退避策略。

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次重试。对于POSTPUT等非幂等操作,务必设置为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环境基于浏览器的XMLHttpRequestFetch API,受到严格的同源策略和CORS限制。

  1. CORS:如果你的后端API和WebGL游戏不在同一个域名下,服务器必须在响应头中设置Access-Control-Allow-Origin: *或你的游戏域名。这是服务器端的配置,Unity端无法绕过。
  2. Credentials:如果请求需要携带Cookie或认证头,需要额外设置。在BestHTTP中:
    var request = new HTTPRequest(...); #if UNITY_WEBGL && !UNITY_EDITOR request.WithCredentials = true; // 告诉浏览器发送凭据(如Cookie) request.SetHeader("X-Requested-With", "XMLHttpRequest"); // 有时需要这个头 #endif
    同时,服务器的Access-Control-Allow-Origin不能是*,必须是具体的域名,并且需要设置Access-Control-Allow-Credentials: true
  3. HTTPS/WS:WebGL要求所有非本地(localhost)通信必须使用安全的HTTPSWSS协议,HTTPWS会被浏览器阻止。

5.2 问题二:移动设备(iOS/Android)上后台或锁屏后网络请求失败

现象:游戏切到后台或手机锁屏一段时间后,再切回前台,网络请求超时或直接失败。

根因与解决: 移动操作系统为了省电,可能会暂停应用的网络活动或强制关闭套接字。

  1. 连接保活:对于WebSocket或长连接,实现一个简单的心跳包(Ping/Pong)机制。BestHTTP的WebSocket有内置的Ping支持,确保定期发送数据包以保持连接活跃。
  2. 应用生命周期处理:在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()); } }
  3. 请求超时设置:为移动设备设置更长的超时时间,以应对网络切换(Wi-Fi到4G)带来的短暂中断。

5.3 问题三:内存泄漏与对象生命周期管理

现象:游戏运行一段时间后内存持续增长,尤其是在频繁进行网络请求的场景。

根因与解决

  1. 未取消的请求:如果你在场景切换或对象销毁时没有取消未完成的请求,这些请求及其回调可能仍然被库持有,导致关联的游戏对象无法被垃圾回收。
    void OnDestroy() { if (_pendingRequest != null && !_pendingRequest.IsCancelled) { _pendingRequest.Abort(); // 中止请求 _pendingRequest = null; } // 同样,关闭WebSocket }
  2. 回调中捕获的上下文:Lambda表达式或匿名方法如果捕获了当前类的成员(如this),会形成闭包,阻止this被释放。确保在不需要时解除事件订阅。
    // 不好:lambda捕获了this request.Callback = (req, resp) => { this.ProcessResponse(resp); }; // 更好:使用弱引用或确保在对象销毁时清空Callback void OnDestroy() { if (request != null) { request.Callback = null; request.Abort(); } }
  3. 大响应体的缓存:如果你下载了大文件到内存(resp.Data),务必在处理完后及时释放引用,或直接使用流式下载保存到文件。

5.4 问题四:性能瓶颈诊断

现象:网络操作感觉卡顿,或者大量请求时帧率下降。

排查步骤

  1. 开启详细日志:在开发阶段,将HTTPManager.Logger.Level设为All。观察日志中是否有大量“Creating new connection...”或“Waiting for a free connection...”。前者表示连接复用失败,后者表示达到MaxConnectionPerServer限制,请求在排队。
  2. 使用性能分析器:在Unity Profiler的CPU模块中,查看BestHTTP相关的函数调用耗时。如果SocketStream相关操作占用大量时间,可能是网络延迟本身的问题。
  3. 检查主线程阻塞:BestHTTP的回调默认在主线程执行。如果你的回调函数中进行了复杂的计算(如解析巨大的JSON),会阻塞游戏渲染。将耗时操作移到后台线程(如使用Task.RunThreadPool),完成后再派发回主线程更新UI。
  4. 限制并发量:对于非紧急的请求(如日志上报、非关键数据拉取),可以实现一个简单的请求队列,控制同时活跃的请求数量,避免瞬间爆发拖慢系统。

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引擎本身的理解深度。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/8 16:11:35

Claude Code上下文压缩实战:突破AI编程助手的Token限制

1. 项目概述&#xff1a;为什么我们需要关注Claude Code的上下文压缩&#xff1f; 如果你最近在折腾AI编程助手&#xff0c;尤其是Claude Code&#xff0c;那你大概率已经遇到了那个让人头疼的“上下文窗口”问题。无论是Claude 3.5 Sonnet还是其他大模型&#xff0c;它们都有一…

作者头像 李华
网站建设 2026/8/8 16:11:30

筑玻璃隔声性能研究(一)

筑玻璃隔声性能研究(一) 建筑玻璃对门窗隔声性能起着决定性作用。常见建筑玻璃隔声性能是怎么样的,不同频段隔声性能有何区别呢? 1、噪声来源 人耳可识别的声音

作者头像 李华
网站建设 2026/8/8 16:10:59

AI赋能PPT制作:半小时高效出稿的智能工作流实战

1. 从“熬夜肝稿”到“半小时出稿”&#xff1a;我的PPT制作流程革命做PPT这件事&#xff0c;对很多人来说&#xff0c;可能比写代码、做设计还要头疼。我过去也是其中一员&#xff0c;每次接到任务&#xff0c;从找模板、搜素材、排版、调色到写文案&#xff0c;一套流程下来&…

作者头像 李华
网站建设 2026/8/8 16:10:54

为什么选择ML-NOTE?这份机器学习笔记的独特之处与优势分析

为什么选择ML-NOTE&#xff1f;这份机器学习笔记的独特之处与优势分析 【免费下载链接】ML-NOTE :orange_book:慢慢整理所学的机器学习算法&#xff0c;并根据自己所理解的样子叙述出来。(注重数学推导) 项目地址: https://gitcode.com/gh_mirrors/ml/ML-NOTE ML-NOTE是…

作者头像 李华
网站建设 2026/8/8 16:08:44

Shell脚本用户输入处理:从位置参数到getopts的实战指南

1. 从命令行到交互&#xff1a;Shell脚本用户输入处理的核心价值 在Linux世界里&#xff0c;Shell脚本是我们与系统对话、实现自动化任务的瑞士军刀。但一把真正好用的刀&#xff0c;不仅要锋利&#xff0c;更要“称手”。脚本的“称手”&#xff0c;很大程度上就体现在它处理用…

作者头像 李华
网站建设 2026/8/8 16:07:36

Windows驱动清理神器DriverStoreExplorer:新手也能轻松管理系统驱动

Windows驱动清理神器DriverStoreExplorer&#xff1a;新手也能轻松管理系统驱动 【免费下载链接】DriverStoreExplorer Driver Store Explorer 项目地址: https://gitcode.com/gh_mirrors/dr/DriverStoreExplorer 你是否曾经因为C盘空间不足而烦恼&#xff1f;是否遇到过…

作者头像 李华