先说个容易踩的低级坑:群里经常有人问“untiy中 basthttp 插件怎么用”,我第一反应是这哥们到底用的什么插件,Asset Store 里翻半天都搜不到 BastHttp。聊到最后才发现,就是那个在商店里更新了很多年的 Best HTTP/2,BastHttp 基本是 Best 拼错后的手滑写法。最佳做法是你直接去搜 Best HTTP 或 Best HTTP/2,搜索结果里那个插件才是本文要讲的主角。
我自己的项目里,凡是涉及跟服务端稳定通信、登录鉴权、上传头像、下载更新包、拉取带进度的资源,几乎都和这个插件打交道。Unity 自带的 UnityWebRequest 不是不能用,但等你要处理自定义证书、流式下载、断点续传、连接超时细分、WebSocket 长连接这些场景时,自己封装的成本往往会超过直接引入成熟插件。这篇文章我会从导入激活开始,到跑通登录接口,再到大文件上传下载、HTTPS 证书处理、打包时常见冲突,把这两年实际用下来的完整流程和坑一次写清楚。内容偏实战,适合手头正在做客户端网络层、或者已经在用但还没完全跑顺的开发者参考。
1. 不是 UnityWebRequest 不够好,是有些场景它真的累
先给一个大的判断结论:如果项目只是发起几个简单的 GET 请求取 JSON,UnityWebRequest 完全够用,没必要为这种场景引入一个商业插件。真正让人决定换掉系统自带方案的时候,通常是遇到下面几类事:
- 需要同时对多个域名进行高并发请求,还要服务端主动推送消息,比如聊天、战斗同步、公告广播;
- 需要走 HTTP/2 多路复用,同一个域名大量请求并发时减少端口占用;
- 需要下载大文件,而且中途断网后能续传,而不是从头再来;
- 需要严格区分“连接超时”“读取超时”“状态码错误”“服务端不可达”“域名解析失败”等异常类型,方便做客户端上报和重试;
- 项目要同时兼容 Android、iOS、桌面和 WebGL,每种平台还有自己的证书或网络限制。
Best HTTP/2 在这类场景里解决问题的思路不是像 UnityWebRequest 那样每发一次请求就重新走一遍底层流程,而是维护了一套自己的连接池、Cookie 管理、缓存和后台调度。它对 HTTP/1.1 和 HTTP/2 都有不错的支持,同时还提供 WebSocket、Server-Sent Events、流式上传下载这些扩展能力。
这个插件底层是基于 C# 的 Socket 层自己实现的,并不是包了 WWW 或者 UnityWebRequest 的壳。所以它能拿到很多底层行为,比如连接复用、分块传输、代理、自定义 Header 的自由控制等,这种粒度是官方组件比较难露出来的。
1.1 什么时候我坚决建议用插件
我个人的经验是,只要项目里出现了下面三个标志,就不用再犹豫了:
第一,你要写一个统一的请求管理器,统一处理 token 注入、签名、日志、统计。UnityWebRequest 虽然也能做,但代码写着写着就容易变成几百行的静态管理器,每次加一种鉴权方式都要修改主流程。Best HTTP/2 因为提供了比较明确的请求对象和回调链路,封装起来更自然。
第二,服务端接口形式很杂。今天要 JSON 登录,明天要 multipart/form-data 上传图片,后天可能要下载一个几百 MB 的资源包并带真实进度条。这种情况下你用自带 API 做也行,但代码会分散在 MonoBehaviour 和工具类里,测试和纠错成本很高。
第三,针对弱网环境有要求。Unity 自带的 WebRequest 在弱网下的行为更多是“失败或超时”,要给玩家一个“重试”的体验,你得自己维护一套重试队列,而且无法很精细地知道到底怎么失败的。Best HTTP/2 能通过回调拿到更细的错误类型,方便你把“服务器已经收到但响应慢”和“压根没连上服务器”分开处理。对弱网友好的 App,这种区分特别加分。
1.2 插件包里到底给了你哪些模块
导入之后最好先看一眼插件目录结构,不然以后出了问题都不知道往哪儿查。比较关键的模块有:
- HTTPManager:全局管理类,负责连接调度、线程分发、请求生命周期维护,正常使用不需要频繁实例化;
- HTTPRequest:每次请求的核心对象,里面包含 URL、方法、请求头、请求体、超时、代理、回调等配置;
- HTTPResponse:请求完成后的响应对象,可以拿到状态码、响应头、响应体、下载进度等;
- CookieJar:自动管理服务端 Set-Cookie 的 Cookie 容器;
- WebSocket/SSE:如果只是用 HTTP 接口,这一类可以暂时忽略。
所以说这个插件没那么神秘,本质上还是围绕 Request 和 Response 这两个核心对象展开。理解清楚这一点,后续写任何接口心里都有数。
1.3 它在网上最常见的三个名字
由于插件版本更新和市场页调整,实际买的时候可能要花点心思。常见称呼有这三种,你看到的可能是同一个东西:
- Best HTTP/2:老版本时代最常见的名字,现在很多文章和分享用的还是这个叫法;
- Best HTTP:Asset Store 页面和代码包名里经常出现的简写;
- Best HTTP (Turbo):近几年的新版本发布名,底层 API 调整了不少,部分命名空间从
BestHTTP变成了Best.HTTP。
所以网上搜到脚本用 using BestHTTP; 也别急着说别人写错了,他只是用了老版本最普遍的接口。下文的代码我会以较新的命名空间为主,但尽量说得通俗一点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 导入、激活与最容易被忽略的工程配置
很多人安装这个插件后第一件事就急着写代码,结果连“请求发不出去”的问题都定位不到。我建议导入后先花十分钟把插件配置和平台依赖检查一遍,后面至少能省半天查错时间。
2.1 导入包时那些勾选项到底选什么
从 Asset Store 购买并在 Unity 中下载后,会弹出一个大大的 Importing 窗口。里面除了插件本体,往往会带一些可选项,比如第三方依赖解析、Play Services Resolver、示例场景等。我的习惯是:
- 插件必须的主程序文件全部导入,除非已经装过相同依赖;
- 依赖解析建议一并导入,尤其项目用了 Android 平台的时候,它能让 Gradle 自动拉取插件需要的库;
- 示例场景第一次先导入,好在编辑器里快速验证能不能通;熟练之后可以在发布包阶段把这些没用的示例剔除。
有些朋友喜欢“尽量少勾选”,觉得依赖越多越容易出问题。这个想法是好的,但 Best HTTP/2 在 Android 上确实对某些库有依赖,如果只是盲目去掉,可能很快触发“找不到类”的异常。
2.2 许可证激活和过期提示
正版插件首次运行时一般会检查许可证状态。有些版本在导入后进入 Play 模式弹一个授权提示,或者要求你登录 Asset Store 对应的账号再激活。注意激活需要联网,如果公司网络比较严格,先确认能访问插件商服务器,否则你将很难顺利跑起来。
这个点很容易被忽略:不少开发者下载的是商业授权,但一个月后某天突然发现“之前好好的请求变得不能用了”,运行日志里报许可证异常。这时候先不要怀疑代码,极大概率是许可证过期或账号切换了。重新在菜单栏找到插件自带的 License/Activation 面板再激活一次,基本都能恢复正常。
提示:许可证校验失败时,有的版本仍会发请求,但会往日志里写异常;有的版本则会直接让
request.Send()抛异常。如果遇到“发布前还能跑,发布后接口全部失败”,建议先看编辑器 Console 有没有 License 相关 Error。
2.3 Android 平台容易踩的依赖冲突
这是整个插件使用中我碰到过最多的问题。插件本身可能自带了部分加密或 TLS 相关依赖,如果你的项目里同时有 Firebase、部分 SDK、甚至某些推送库,就会出现重复类冲突。
典型表现是构建 Android 包时出现 Duplicate class,或者运行时在 dex 合并阶段直接失败。解决办法有两个方向:
第一,在插件导入目录里找到冲突的依赖,取消勾选/删除插件副本下的 .aar 或 .jar,改成只保留项目主 SDK 里的一份。第二,在主工程 Gradle 中配置 packagingOptions 的 pickFirst 或者 exclude,让编译器优先选择某一个版本的实现。
有一种情况格外隐蔽:不是插件主动引起的冲突,而是开发者同时引入多个库,这些库各自打包了同名类,Unity 编辑器里看不出来,只有打 Android 正式包才暴露。我曾经因为项目里集成了一个支付 SDK,和插件的某段加密代码冲突,来回折腾了两天才发现是重复库问题。建议你在刚开始集成这个插件时,就建立一个“Android 打包冲突排查”笔记,后续每次报错先检查一遍是不是新引入 SDK 引发的重复类。
2.4 iOS、WebGL 平台的特别设置
iOS 平台最大坑是 ATS(App Transport Security)。如果你的接口用的是明文 HTTP,或者没有配置合法 HTTPS 证书的域名,应用在 iPhone 上会直接请求失败;而在编辑器里跑却完全正常。解决方式是在 Info.plist 中针对开发域名添加例外,或者把接口整体切换到 HTTPS。
WebGL 平台则受浏览器沙箱限制。插件不能帮你绕过 CORS,所以服务端必须返回正确的跨域头。尤其是自定义 Header(比如 Authorization)或者 application/json 这类非简单请求,会触发预检 OPTIONS 请求,服务端需要提前处理,否则浏览器层就把请求拦了,你在 Unity 里只会看到网络失败。很多 WebGL 端疑难问题都不是插件本身造成的,而是服务端没处理 CORS。
有一个比较实用的编辑经验:插件版本更新后,菜单可能会多出 “HTTP Manager” 之类的设置界面,常见的超时时间、Cookie 开关、Cookies 持久化路径,都能在面板上可视化调整。如果代码运行结果和你预期不一致,可以先打开该面板检查一遍全局默认配置。
3. 第一套请求模板:登录接口我拿它跑了很久
任何网络插件的上手,我都推荐先把“发一个带 JSON 请求体并且解析返回 JSON”的登录接口跑通。因为它涵盖了请求头设置、字符串与字节转换、回调结果处理、错误码判断这几个最核心环节。
3.1 最基础的带 JSON 正文请求
先看一下脚本结构。我习惯把它写在一个独立的服务类里,而不是把请求分散在多个 UI 组件里:
csharp复制using System;
using UnityEngine;
using Best.HTTP;
using Best.HTTP.Request;
public class LoginService : MonoBehaviour
{
public void Login(string username, string password, Action<bool, string> onDone)
{
var payload = new LoginPayload
{
username = username,
password = password
};
string json = JsonUtility.ToJson(payload);
var req = new HTTPRequest(
new Uri("https://api.yourgame.com/v1/login"),
HTTPMethods.Post,
(request, response) => HandleLoginResponse(response, onDone));
req.SetHeader("Content-Type", "application/json; charset=utf-8");
req.SetHeader("Accept", "application/json");
req.SetHeader("X-Client-Version", Application.version);
req.UploadData = System.Text.Encoding.UTF8.GetBytes(json);
req.ConnectTimeout = TimeSpan.FromSeconds(5);
req.Timeout = TimeSpan.FromSeconds(10);
req.Send();
}
private void HandleLoginResponse(HTTPResponse response, Action<bool, string> onDone)
{
if (response == null)
{
onDone?.Invoke(false, "请求无响应,可能已取消");
return;
}
if (response.IsSuccess)
{
var result = JsonUtility.FromJson<LoginResult>(response.DataAsUTF8);
if (result.code == 0)
{
onDone?.Invoke(true, result.token);
}
else
{
onDone?.Invoke(false, result.message);
}
}
else
{
onDone?.Invoke(false, $"HTTP {response.StatusCode}");
}
}
}
[Serializable]
public class LoginPayload
{
public string username;
public string password;
}
[Serializable]
public class LoginResult
{
public int code;
public string message;
public string token;
}
这里面有两个非常容易忽略的细节。
第一个细节:Content-Type 写的是 application/json; charset=utf-8。有些服务端解析能力不强,少写 charset 可能把中文当乱码,虽然理论上 HTTP 规范对 UTF-8 有默认理解,但实际对接时多写这两个字符能省很多事。
第二个细节:response.IsSuccess 只代表 HTTP 状态码是 2xx,它并不代表业务成功。很多服务端返回结构是 { code: 0, message: "ok", data: {} },即使密码错误也会返回 HTTP 200。所以回调里不能只看 IsSuccess,要再解析业务 code。这是我见过最容易写错的地方。
关于 JsonUtility 的约束也提醒一句:它能直接序列化的数据结构必须是可序列化的类,而且注意字典不一定好用。如果项目需要更灵活的 JSON 处理,最省事的做法是引入 Newtonsoft Json 插件,然后把上面 FromJson 换成 JObject.Parse。这不算 Best HTTP 的职责,但配合起来会让开发效率提高不少。
3.2 表单提交、查询参数和请求头
除了 JSON,现在很多老接口还保留着传统的 application/x-www-form-urlencoded 表单方式。用这个插件处理也很直接:
csharp复制var req = new HTTPRequest(new Uri("https://api.yourgame.com/v1/token"), HTTPMethods.Post, callback);
req.SetHeader("Content-Type", "application/x-www-form-urlencoded");
string body = "grant_type=password&username=" + Uri.EscapeDataString(username) + "&password=" + Uri.EscapeDataString(password);
req.UploadData = Encoding.UTF8.GetBytes(body);
req.Send();
注意 Uri.EscapeDataString 对中文和特殊字符做了百分号编码。比如密码里有 & 或空格,你不做转义直接拼字符串,服务端拿到的基本是错乱的参数。
GET 请求通常不需要设置上传数据,但查询参数有两种常见写法:
- 直接把带
?key=value&key2=value2的完整 URL 放到new Uri里; - 或者在构造完请求对象后,用代码给 URL 参数赋值。
第一种写法最简单,服务器也兼容。不过参数值是中文或特殊符号时,同样需要先编码再拼进 URL。
“请求头”这个看似简单的东西,实际对接到不同后端团队真是五花八门。有的服务端要求 X-App-Id 是 int 型的字符串,有的要求 X-Sign 是经过 AES 加密的签名,有的要求毫秒时间戳字段不能少。用这个插件发自定义 Header 很灵活,关键点是签名计算一定基于最终实际发送的那份正文,而且签名不要写在 UI 层。我建议把 Header 注入逻辑做成一个公共方法,比如 ApplyCommonHeaders(HTTPRequest request),这样后面任何请求都走同一套注入逻辑。
3.3 Cookie、超时、重试与请求取消
服务端如果还在用 Session 方式保持登录状态,那绝大部分工作插件已经替你做了。它内部的 CookieJar 会自动读取 Set-Cookie 响应头,并在后续往同一域名的请求中带上 Cookie。你不需要手动把 JSESSIONID 存到 PlayerPrefs 再拼到 Header 里。
不过也要清楚一个边界:这个自动管理只对“插件发起的后续请求”有效。如果你在别的 SDK 里用了原生网络请求,它是不会自动带上这些 Cookie 的。如果项目某些功能必须让原生 SDK 和 Best HTTP 共享会话,你得把服务端返回的 Cookie 对象手动取出来,传给原生层。这个场景不多,但遇到时会非常头疼。
超时配置有两个层级需要注意。ConnectTimeout 控制的是建立 TCP 连接的等待时间,Timeout 控制的是整个请求从发出到收到完整响应的最长时间。如果你碰到“服务端接口处理要 30 秒,但客户端 10 秒就断掉了”,就把第二项调大一些;如果碰到“域名不可达时客户端卡了 30 秒才报错”,就检查第一项是不是没设。
取消请求则是调用 req.Abort()。特别要记住的是,Abort 之后不代表回调不会触发,很多版本仍然会以异常状态回调一次回调函数,只是 response 可能是空引用或状态码异常。所以回调里第一行就做空判断,不要直接访问 response.DataAsUTF8。
关于重试,这个插件默认行为是不自动重试,属于“把决策权交给你”的设计。一般我会在回调里判断:
- 如果是网络未连接、连接超时这类错误,等待 1 至 3 秒重试;
- 如果是服务端 5xx,可以重试,但要限制次数,避免雪崩;
- 如果是 4xx(请求本身有问题),就不要无脑重试,因为重试十次也一样。
3.4 回调执行顺序:Unity API 别乱访问
Best HTTP 的回调默认会派发到主线程,所以你在回调里直接操作 UI 是安全的。这也是它比很多原生库更好用的原因之一。但“安全”不等于“可以随便写”。
有一个真实场景:玩家进入游戏后立刻点击某按钮,发起一个请求;请求还没返回,玩家就切场景了,原始的 UI 对象被销毁了。等响应回来,回调想更新那个 UI 的 Text,结果访问到一个已经被销毁的组件,Unity 不会直接崩溃,但会抛 MissingReferenceException。老项目里这种日志经常刷屏。
我习惯的写法是:回调里不直接操作具体 UI,而是把结果放入一个简单的消息结构或者队列,由常驻的场景控制器统一处理。这样做的好处是,就算界面销毁了,数据逻辑部分也不会报错。
另一个要注意的点是:不要在 OnDestroy 里强行做“请求完成后弹窗”的操作。虽然主线程派发能让你安全访问大部分对象,但对象生命周期已经结束,该判空的还是要判空。
4. 大文件下载与上传:进度、断点续传、内存控制
如果说登录接口只是热身,那大文件传输才是真正体现实力的场景。很多项目的资源更新、语音聊天记录下载、头像上传,都会在这里遇到问题。
4.1 小文件可以这么写,但大文件千万别这么写
很多刚上手的同学会写下面这种代码:
csharp复制var req = new HTTPRequest(new Uri(url), HTTPMethods.Get, (response) =>
{
byte[] data = response.DataAsByteArray;
File.WriteAllBytes(path, data);
});
req.Send();
文件只有几十 KB 时,这么做完全没有问题。如果一次要下几百 MB 的资源包,这种做法就会面临两个无声的隐患:
第一,响应体会被一次性放进内存。等于多了一个几百 MB 的 byte[]。如果同时有多个下载任务并发,内存直接起飞,后续几秒的 GC 明显卡顿。
第二,一旦下载中断,你拿不到任何有效数据,只能从头再来。弱网环境中反复下载同一段大文件,浪费流量和时间。
所以只要文件大小超过几十 MB,我强烈的建议是采用流式下载的方式,把数据直接写入文件的字节流中,而不是先在内存里攒一份。
4.2 流式下载配置和下载进度
在不同版本中,流式下载的 API 名称可能有差别,但大体思路一致:给请求对象设置一个下载目标,让插件在接收数据的同时持续写入目标流,而不是等数据全部收完。
下面是一个相当典型的流式下载伪代码框架,具体类名请以你自己当前插件版本为准:
csharp复制var req = new HTTPRequest(new Uri(url), HTTPMethods.Get, OnDownloadFinished);
// 这里是在“下载到本地文件”与“只在内存里攒数据”之间做区分
var file = new FileStream(savePath, FileMode.Create, FileAccess.Write);
req.DownloadSettings = new DownloadSettings(file);
req.DownloadProgress += (r, downloaded, total) =>
{
if (total > 0)
{
float progress = downloaded / (float)total;
DispatchProgress(progress);
}
};
req.Send();
进度事件里拿到的两个数值,一个是已经下载的字节,一个是服务器提供的总长度。因为每次回调频率偏高,如果你直接用它刷 UI 进度条,会发现 UI 刷新特别频繁,结果就是电量消耗增大、UI 线程卡顿、手机发烫。通常做法是用一个策略:只在进度变化超过 1% 或者时间间隔超过 0.1 秒时才刷新一次界面。
还有一个细节:服务端不一定返回准确的总长度。比如使用 chunked 编码时 total 可能是 0。这种情况下你的进度条无法通过已下载/总大小算准,这时候要么服务端配合,在下发前定好 Content-Length;要么先发一个 HEAD 请求获取文件总大小,再发起真正的下载。
4.3 支持续传的更新包下载流程
断点续传并不是插件里一个简单的 bool 开关,它需要客户端和服务端同时支持。核心协议是 HTTP 的 Range 头。服务端只要返回正确,客户端就能从某个位置接着下。
客户端一般要做这么几件事:
- 下载前先看本地是否已经有临时文件,比如
hotfix_1.0.3.bin.tmp,记录它的字节长度; - 如果临时文件长度大于 0,就在发起请求时附带
Range: bytes=已下载长度-; - 服务端返回 206 Partial Content 时,客户端把响应体数据继续追加写入临时文件的末尾,而不是重新建文件;
- 全部完成以后,把临时文件改名成正式文件,并做一次完整性校验(长度、MD5 或 CRC32)再对外提供使用。
Best HTTP 的新版本里,DownloadSettings 可以配置 UseRange 相关字段,有的版本甚至带 Fragment 下载方式,把一个大文件切成多个并发块下载,最终合并成完整文件。这种能力对大型热更包特别有用,因为它能利用多条连接同时下载,重连代价也更小。
但我不建议项目一开始就上多线程分片下载。分片下载对服务器的要求较高,服务端需要支持 Range,并且可能有缓存服务器对 Range 请求的响应不一致的问题。更稳妥的路线是:先支持“单连接断点续传”,把弱网重试策略做稳;等日活规模大了,确实发现更新包下载速度是瓶颈,再考虑分片并发。
4.4 大文件上传怎么不走内存
上传方向上,很多人喜欢把本地文件一次性 File.ReadAllBytes 后放到 UploadData 里。小文件无所谓,文件一多或者单个文件超过 100 MB,内存又会紧张。
更合理的做法是使用流式上传,直接把文件流赋给请求的 UploadStream,让插件边读边传:
csharp复制using (var fileStream = new FileStream(uploadPath, FileMode.Open, FileAccess.Read))
{
var req = new HTTPRequest(new Uri(url), HTTPMethods.Post, OnUploadFinished);
req.UploadStream = fileStream;
req.SetHeader("Content-Type", "application/octet-stream");
req.UploadProgress += (r, uploaded, total) => { UpdateProgress(uploaded, total); };
req.Send();
}
需要注意文件的打开方式。文件作为流被传递给请求后,请求在处理期间会消费这个流。如果写完一个文件还要传给下一个接口,并且中间被 GC 回收,很可能会抛 ObjectDisposedException。正确思路是让请求负责流的生命周期,或者至少明确在回调里再释放。
如果文件本身需要做 SHA1/MD5 签名,一定不要先开 FileStream 读完再开第二次。比较好的做法是在传给请求前先计算校验值,并在请求头里带上:
Content-MD5 或自定义 X-File-MD5。有些旧服务端需要这个值来判断是否传输完整。
上传进度和下载进度有个类似的问题:插件返回的 total 如果为 0,进度百分比会显示 NaN。设置 UI 的时候要加判断,total <= 0 时就显示“上传中,请稍候”,不要直接把两个数字相除。
5. HTTPS、错误识别与自动重试的工程化处理
这个章节看起来比较理论,但只要你的项目面向真实玩家,几乎绕不开。很多客户端工程师上线前只顾着调通业务,却忽略了错误识别,结果线上出问题后才开始翻日志,效率非常低。
5.1 证书出错时先查这四类问题
最让人困惑的通常不是什么业务 Bug,而是服务端域名更换后,客户端明明逻辑没问题,请求却一直失败,日志里出现证书校验相关的错误。
我查证书问题有一个固定顺序:
- 看服务器证书是否过期。尤其是测试环境自己签发的证书,往往只有三个月有效期,过期之后客户端请求会失败;
- 看证书链是否完整。有些运维只部署了域名证书,没有把中间证书链配全,手机上有根证书时可能没问题,但部分 Android 客户端就会拒绝连接;
- 看域名是否跟证书里的 SAN 匹配。证书申请的是
www.test.com,你请求api.test.com,也会校验失败; - 看客户端系统时间是否严重错误。手机时间被调整到证书生效之前或过期之后,整套证书校验就可能出问题。
调试时可以打开插件的详细日志,看到底是证书链不完整,还是域名不匹配。有些开发者为了省事,选择“忽略所有证书校验”,这种做法用在内网测试也许没关系,但放在对外发布的包里等同给自己留了一个巨大的安全缺口。
5.2 错误码和 HTTPRequestStates 的判断顺序
很多新手写回调时只判断 response.IsSuccess,但真实环境的失败不只有 HTTP 状态码 4xx/5xx。网络断连、超时、DNS 解析失败、TLS 握手失败,这些情况可能连状态码都没有。
建议的判断顺序是:
- 先判断 response 是否为空,为空基本是请求被中止或没有收到任何响应;
- 再判断是否在连接阶段就失败,比如域名不可达、连接被拒绝、连接超时;
- 然后判断 HTTP 状态码,按 2xx、4xx、5xx 分类;
- 最后解析响应体里的业务状态,比如
code != 0。
这几种情况对应的日志处理、用户提示、重试策略都不同。连接失败可以提示“请检查网络”;5xx 可以提示“服务器繁忙,请稍后重试”;业务 code 失败则要看具体场景。
下面是我的一个简化处理表,可以在自己的网络管理器里固化下来:
| 判断条件 | 常见含义 | 处理建议 |
|---|---|---|
| response 为空 | 请求被中止或未收到响应 | 按取消处理,不提示重试 |
| 连接超时 | 域名/服务器无响应 | 等待后重试,最多 3 次 |
| 401 | 未登录或 token 失效 | 触发登录态刷新 |
| 403 | 服务器拒绝访问 | 检查用户权限,不要反复重试 |
| 404 | 接口路径错误 | 检查 URL 和路由,不重试 |
| 408 / 504 | 服务端超时 | 等待后重试,注意退避 |
| 429 | 服务器限流 | 拉长重试间隔 |
| 5xx | 服务端错误 | 重试 1 至 2 次,同时上报日志 |
| 业务 code 非 0 | 业务失败 | 按业务逻辑处理,和网络无关 |
5.3 可配置的重试机制:连接超时、服务端 5xx、网络切换
对于移动端来说,网络状况随时在变。玩家可能前一秒在电梯里根本没网,后一秒走出来网络又恢复了。所以重试不是简单的“失败了马上再来一次”,而是要有节奏。
我常用的指数退避重试方案是:第一次失败等 1 秒,第二次失败等 2 秒,第三次失败等 4 秒。如果连续失败超过 5 次,就不再自动重试,而是提示玩家手动点击“重试按钮”。这样可以在网络恢复的第一时间有机会自动连上,又不会在无网状态下一直空转消耗电量。
判断无网状态的代码不该频繁调用系统 API。比较好的做法是:在一轮请求失败导致重试时,通过底层获取一个简单的网络可达性状态,比如 UNET 的 Application.internetReachability。但注意它只能代表本机有没有接入网络,不能代表服务端是否可达,所以最终还是要以请求结果为准。
另外,如果同时有几十个请求因为一次网络切换而全部失败,你不要让每个请求都各自发起重试。否则网络一恢复,客户端同时向服务器打几十个请求,很容易触发服务端限流。建议加一个全局的“网络恢复后统一重放队列”:断网时进来的请求先挂起,待网络恢复后按顺序重放。这个设计能明显提升弱网表现和服务器友好度。
6. 最终打包前我要过的检查清单
在把项目交付给 QA 或提交 App Store/Play 商店前,我一般会花时间过一遍下面的检查单,避免重复踩一些低级坑。整个过程不复杂,但是每一条背后都是真实事故换出来的。
6.1 打包前我逐条核查的项目
| 检查项 | 具体操作 | 命中风险 |
|---|---|---|
| 许可证状态 | 确认授权在有效期内,且没有切换到错误账号 | 打包后接口全部失败或抛异常 |
| 依赖冲突 | 在 Android 平台上打一次 Release 包,看是否存在重复类 | 构建失败或运行期 crash |
| iOS ATS 配置 | 检查是否存在明文 HTTP 接口白名单 | 上线后苹果手机请求失败 |
| WebGL CORS | 用生产服务器地址测一次预检 OPTIONS | 浏览器请求被 CORS 拦截 |
| 超时参数 | 关键接口的连接/读取超时是否合理 | 弱网用户动辄失败 |
| 日志开关 | 正式包是否关闭详细级别的 HTTP 日志 | 日志刷屏、性能下降、敏感信息泄露 |
| 重试策略 | 是否每个请求都无限重试 | 服务端被触发限流 |
6.2 反复被问到的一个细节点:连接泄漏
有些后台日志里能发现插件建立的连接数不断上升,伴随内存升高。排查方法不复杂:重点看你是不是在短时间内频繁 new HTTPRequest 后不设置超时、不处理回调。
某些请求对象如果挂在变量上没有释放,内部句柄一直存在,时间久了就会变成泄漏。还有,下载流使用后没有主动关闭,文件锁也会一直存在。最典型的例子是 Windows 编辑器里下载完文件后再次写同名文件失败,提示“文件被另一个进程占用”,多半就是流没有释放。
我一般在请求回调的 finally 块里关流,而不是在 if (response.IsSuccess) 的分支里关流。这样即使请求失败,也不会把文件流悬空。插件虽然有自己的资源清理,但文件流这类外部资源建议你自己主动兜底。
6.3 实测阶段特别建议做的事
网络库和普通 UI 不一样,改动一次影响面往往很大。所以选型阶段不要“拿大号正式包做实验”,建议在项目早期建立一个专门的 NetworkPlayground 场景,只放几个按钮:
- 测 GET 一个字符串;
- 测 POST JSON;
- 测上传二进制;
- 测下载一个大文件;
- 测断网瞬间发请求;
- 测服务器返回 500 时重试。
这个场景的作用不是给玩家用,而是以后每次升级插件版本、调整网络公共参数时,可以先在这个场景里一键验证。等场景里的粗测全通过,再跑整个 App 的业务回归逻辑,能节省大量问“是不是插件出了问题”的时间。
我自己习惯把验证用例写成脚本自动化,因为手动按钮点一遍虽然能发现问题,但容易漏项。先用一个简单的集成测试脚本覆盖登录、拉取配置、上传、下载四个主链路,每次 CI 或者版本提测前跑一次,稳定性和信心都会高很多。
最后再分享一个我经常提醒项目组的习惯:一个网络请求的入口和出口要清晰,不要在十几个 UI 脚本里到处直接创建 HTTPRequest。把所有请求封装到一个 Service 层,至少这么做以后出现了问题,你可以快速看到是哪一个模块、哪一个接口、用的哪种请求方式,而不至于翻遍整个客户端代码再逐个试。Best HTTP/2 是个好工具,但真正让项目稳定的,还是你对连接的规划、超时的定义、失败的分类和可观测的日志。
