欢迎光临
我们一直在努力

Unity HTTP 网络层硬核拆解:防重复、自动重试、双缓冲、WebGL,一次请求到底被框架接管了多少?

TCP、UDP、WebSocket 都属于持续通信,而游戏里还有大量功能更适合 HTTP:登录鉴权、活动接口、公告、支付、后台服务、文件接口……

如果每个业务模块都自己写 UnityWebRequest、自己处理 JSON、超时、重试和线程切换,很快就会出现一堆重复代码。

MyFramework 里的 NetConnectHttp 做的事情,就是把一次 HTTP 请求从协议对象创建,到发送、重试、回到主线程执行回调全部接管。

项目地址:

https://github.com/ZHOURUIH/MyFramework

一、HTTP 也被做成了 NetPacket

HTTP 消息的基类是:

public class NetPacketHttp : NetPacket
{
protected string mURL;
protected HTTP_METHOD mMethod;

public NetPacketHttp()
{
mMethod = HTTP_METHOD.POST;
}

public virtual string write() { return null; }
public virtual void read(string message) { }

public virtual int timeout()
{
return 10000;
}
}

所以业务并不是直接操作:

URL
JSON
UnityWebRequest

而是继续操作:

NetPacketHttp

一条 HTTP 协议自己定义:

请求地址
GET / POST
请求数据
返回数据
超时时间

这样 TCP、UDP、WebSocket、HTTP 在上层仍然保持同一种“协议对象”思想。


二、JSON 序列化也直接封装进协议

框架提供:

public class NetPacketHttpT<CSBody, SCBody> :
NetPacketHttp
where CSBody : IResetProperty, new()
{
public SCBody mBody;
public CSBody mSendBody = new();

public override string write()
{
return JsonConvert.SerializeObject(mSendBody);
}

public override void read(string message)
{
mBody =
JsonConvert.DeserializeObject<SCBody>(message);
}
}

业务只需要定义:

CSBody:发什么
SCBody:收什么

例如:

public class LoginSendBody : IResetProperty
{
public string account;
public string password;

public void resetProperty()
{
account = null;
password = null;
}
}

public class LoginResponseBody
{
public string token;
public long playerID;
}

然后协议类继承:

NetPacketHttpT<LoginSendBody, LoginResponseBody>

JSON 的序列化和反序列化就不需要每个接口重复写。


三、业务调用只负责“发协议,等结果”

发送入口:

mNetManager.sendPacket(
packet,
(NetPacketHttpXXX response) =>
{
// 处理结果
});

NetConnectHttp 收到请求后并不会马上联网。

首先创建:

HttpSendInfo

里面保存:

请求内容
请求类型
URL
GET / POST
回调
超时时间
剩余重试次数

然后放进:

DoubleBuffer<HttpSendInfo> mOutputBuffer;

整个流程变成:

主线程
NetPacketHttp

JSON序列化

HttpSendInfo

DoubleBuffer
================
HTTP发送线程

真正发请求

主线程不用自己等待网络。


四、同一种请求还没回来,直接禁止重复发送

NetConnectHttp 维护:

Dictionary<Type, HttpSendInfo>
mNotResponsePacket;

发送之前会先检查:

if (mNotResponsePacket.ContainsKey(
packet.GetType()))
{
logWarning(
"消息还未返回,不能重复发送:" +
packet.GetType());
return;
}

也就是说:

请求A发出

还没返回

再次发送请求A

直接拦截

这对很多游戏接口非常实用。

比如玩家连续狂点:

领取奖励
领取奖励
领取奖励
领取奖励

至少 HTTP 网络层不会同时把四个完全相同类型的请求全部送出去。

不过这里有一个很重要的语义:

限制的 Key 是协议 Type,而不是请求参数。

所以如果同一个 HTTP 消息类型本来就需要同时请求多个不同 ID:

QueryPlayer(1001)
QueryPlayer(1002)
QueryPlayer(1003)

当前机制也会认为它们是“同一种未完成请求”。

这种接口就不能直接套用这个防重复策略。


五、失败以后自动重试,不让业务层到处写 Retry

每条请求默认:

protected static int RETRY_COUNT = 3;

发送时会把完整的 HttpSendInfo 再备份一份:

mNotResponsePacket
.addClass(packet.GetType())
.cloneFrom(info);

如果收到的数据为空:

if (sendInfo.mRemainRetryCount > 0)
{
–sendInfo.mRemainRetryCount;

CLASS_THREAD(
out HttpSendInfo info)
.cloneFrom(sendInfo);

mOutputBuffer.add(info);
}

于是:

请求失败

还有Retry次数?
↓ Yes
重新放入OutputBuffer

发送线程再次请求

直到没有剩余次数:

pair.mCallback(null);

业务层最终只需要判断:

if (response == null)
{
// 最终失败
}

而不用每个接口都重新实现一遍重试状态机。


六、网络回调绝对不直接执行游戏逻辑

异步 HTTP 请求完成以后,并不会直接执行业务 Callback。

它只做:

mReceiveBuffer.add(new()
{
mCallback = callback,
mPacketType = type,
mData = data
});

也就是进入:

DoubleBuffer<ReceivedDataInfo>
mReceiveBuffer;

真正执行发生在 update():

HTTP线程

请求完成

ReceivedDataInfo

DoubleBuffer
================
Unity主线程

创建NetPacketHttp

JSON反序列化

执行Callback

回收Packet

这样 HTTP 回调中直接:

修改角色
打开UI
刷新背包
触发事件

仍然处于 Unity 主线程。

这一点和前面的 TCP、UDP 网络模型其实完全一致。


七、Header 和 Token 也统一管理

连接中长期维护:

Dictionary<string, string> mHttpHeader;

可以:

addHeader(name, value);
removeHeader(name);

Token 更进一步封装成:

mHttpServer.setToken(token);

内部实际上就是:

Token存在

加入Header

Token为空

删除Header

并且 Header 有:

ThreadLock mHeaderLock;

保护。

所以登录后不需要每发送一个 HTTP 请求,都重新手写:

Authorization
Token
Content-Type

公共 Header 由连接统一维护。


八、WebGL 下连发送模型都变了

普通平台初始化时会创建:

MyThread mSendThread =
new("HttpSend");

也就是说:

主线程生产请求

DoubleBuffer

HttpSend线程消费

但是 WebGL 没有这条线程。

源码直接变成:

#if UNITY_WEBGL
sendSocket();
#endif

也就是每帧 update() 主动处理发送队列。

POST 请求则切换到:

httpPostAsyncWebGL(…)

底层使用 UnityWebRequest + Coroutine。

所以又一次出现了 MyFramework 一贯的处理方式:

Native
线程 + HttpWebRequest

WebGL
主线程 + UnityWebRequest

上层仍然是
NetPacketHttp

平台差异继续被压在底层。


九、当前实现还有两个值得注意的细节

第一,自动重试当前主要依据的是:

ReceivedDataInfo.mData == null

ReceivedDataInfo 虽然也保存了 HTTP Status 和错误状态,但当前 update() 的重试逻辑并不是按照所有 HTTP Code 分类判断。

所以:

500
404
业务错误码
返回了非空Body

和:

真正没有返回数据

并不是同一套处理逻辑。

第二,当前 WebGL 对 POST 有明确的:

httpPostAsyncWebGL()

分支,而 NetConnectHttp 的 GET 路径仍然直接调用 httpGetAsync()。

如果项目要大量在 WebGL 上使用 GET,这一段最好根据实际目标平台再检查并补齐对应的 WebGL 路径。

这种地方反而很适合在读源码时提前发现,而不是上线以后再踩坑。


十、整个 HTTP 请求最终变成了一条流水线

完整流程:

NetPacketHttp

填写SendBody

JSON序列化

防止同Type重复请求

HttpSendInfo

Output DoubleBuffer

HTTP线程 / WebGL Update

POST / GET

收到Response

Receive DoubleBuffer

Unity主线程

JSON反序列化

Callback

失败则自动Retry

对象池回收

所以 NetConnectHttp 真正解决的并不是“怎么发一个 HTTP”。

UnityWebRequest 本身就能做到这一点。

框架真正做的是把:

协议定义、防重复、序列化、线程隔离、公共 Header、Token、超时、自动重试、主线程回调和对象生命周期

全部放进同一条可复用的 HTTP 请求流水线。

当项目只有三五个接口时,这些东西看起来并不重要。

但当后台接口逐渐变成几十个、上百个以后,真正能救命的往往不是“会不会发 HTTP”,而是所有 HTTP 请求是不是都在遵守同一套规则。

赞(0)
未经允许不得转载:171主机测评 » Unity HTTP 网络层硬核拆解:防重复、自动重试、双缓冲、WebGL,一次请求到底被框架接管了多少?
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址