网络层 Provider
从 v3.0.6 开始,KBEngine Nex C# SDK 通过网络层 Provider 接入自定义传输。Provider 只负责建立连接、收发有序字节和报告连接状态;KBEngine 协议解析、消息加密、Entity 同步、Loginapp 到 Baseapp 的切换以及重登录仍由 SDK 管理。
目前官方提供了面向 Unity 的 WebSocket Provider:
本文先说明为什么引入 Provider,再以 Unity WebSocket Provider 为例介绍完整接入流程,最后说明开发自定义 Provider 时必须遵守的接口契约。
为什么需要 Provider
传输能力与 SDK 协议不应绑定
不同客户端平台能使用的网络 API 并不相同:
- 桌面和移动端通常可以直接使用 TCP 或 UDP。
- WebGL 不能直接创建普通 TCP Socket,只能使用浏览器提供的 WebSocket。
- 微信小游戏、抖音小游戏等小游戏运行时通常不开放标准 .NET/系统 Socket,而是提供平台自己的 WebSocket 或 Socket API。
- Unity、Cocos Creator、Godot、Unreal Engine 及其他客户端引擎可能已经封装了自己的 Socket、WebSocket、连接调度和平台适配层。
- 主机平台、企业网关或项目自研网络库可能要求使用平台 SDK。
- 公网环境可能需要 WSS、反向代理、域名入口或端口映射。
如果每增加一种网络库都修改 SDK 的协议代码,传输生命周期、协议解析和业务状态会互相耦合。修改一个平台的连接逻辑也可能影响其他平台。Provider 将这条边界固定下来,让 SDK 面向统一接口工作。
游戏业务 / UI
|
KBEngine C# SDK
- 登录与重登录
- 协议编解码
- Entity 与事件系统
|
INetworkProvider
- 连接状态
- 有序字节收发
- 关闭与资源释放
|
TCP / KCP / WebSocket / 平台网络库Provider 解决什么问题
Provider 机制主要解决以下问题:
- 平台适配:Unity WebGL、原生平台或特定网络库可以实现自己的传输。
- 依赖隔离:第三方网络库放在独立 Provider 中,不进入 C# SDK 核心代码。
- 生命周期统一:连接、收发、断开、重登录和资源释放使用同一套契约。
- 线程边界明确:平台回调可以来自任意线程,但只允许在
Process()中进入 SDK。 - 部署适配:Provider 可以完成内网地址到公网域名、代理端口的转换。
- 独立演进:Provider 可以单独维护和升级,不必重新生成 Entity、协议及数据类型代码。
Provider 不负责修改 KBEngine 协议,也不应在传输层解析 Entity 消息。WebSocket Frame、TCP 读取块和 KBEngine 协议消息不是同一个概念:一次接收可能包含半条、一条或多条协议消息,消息拼接和拆分由 SDK 的 KBEMessageReader 完成。
典型适配场景
| 场景 | 为什么需要 Provider | 实现关注点 |
|---|---|---|
| Unity WebGL | 浏览器环境不能使用普通 TCP Socket | 使用 WebSocket/WSS、主线程驱动、保留 WebGL 构建期桥接文件 |
| 微信小游戏 | 网络能力由小游戏运行时提供,API、域名白名单和证书策略受平台约束 | 用微信小游戏提供的 WebSocket/Socket API 封装 Provider,并遵守平台合法域名与 WSS 要求 |
| 抖音小游戏 | 运行时网络 API 与标准 .NET Socket 不同,发布环境存在平台安全限制 | 用抖音小游戏网络 API 封装 Provider,验证真机回调线程、二进制数据类型和域名配置 |
| 客户端引擎自带 Socket | 项目可能已经统一连接调度、线程、日志、代理或网络诊断 | 在 Provider 内复用引擎网络模块,避免 SDK 再维护一套独立 Socket 生命周期 |
| 主机或渠道 SDK | 平台只允许使用认证过的网络接口 | 将平台连接、错误码和异步回调转换为 Provider 状态与事件 |
| 项目自研网络库 | 已有缓冲池、加速线路、QoS 或网络监控体系 | 复用已有基础设施,同时遵守 SDK 的字节流、内存所有权和背压契约 |
微信小游戏、抖音小游戏属于独立运行时,不等同于普通浏览器,也不等同于 Unity WebGL。不同引擎的导出方案还可能在平台 API 外再包一层适配。因此应针对实际使用的引擎版本、小游戏基础库版本和目标平台实现专用 Provider,不能直接假设 Unity 的 WebSocket.jslib 或 System.Net.Sockets 可以使用。
如果客户端引擎已经提供成熟的 Socket 或 WebSocket 模块,Provider 不需要重新实现底层协议栈。它只需要把引擎模块的连接、二进制收发、错误和关闭事件转换为 INetworkProvider 契约。这样既能复用引擎已有的平台兼容能力,也能让 KBEngine SDK 继续负责上层协议和 Entity 生命周期。
SDK 如何使用 Provider
工厂与实例
应用向 KBEngineArgs.customNetworkProviderFactory 注册一个 INetworkProviderFactory。SDK 每次建立连接时调用工厂,并传入当前连接的 NetworkProviderContext。
SDK 会为以下连接分别创建 Provider 实例:
- 初次连接 Loginapp。
- 登录成功后连接 Baseapp。
- Baseapp 断线后的 Relogin。
因此,工厂每次 Create() 都必须返回一个全新的 Provider。不要缓存并复用已经连接或关闭的实例。
NetworkProviderContext
NetworkProviderContext 提供当前连接需要的只读上下文:
| 属性 | 含义 |
|---|---|
Role | 当前连接角色,值为 Loginapp 或 Baseapp |
Endpoint | SDK 决定的目标地址,包含 Host、TCP 端口和 UDP 端口 |
ServerVersion | 当前已知的服务端版本 |
SendQueueSize | Provider 可使用的应用层发送队列上限 |
TcpSendBufferSize / TcpReceiveBufferSize | TCP 缓冲参数 |
UdpSendWindowSize / UdpReceiveWindowSize | UDP/KCP 窗口参数 |
Loginapp 的 Endpoint 来自启动参数 ip、port;Baseapp 的 Endpoint 来自 Loginapp 登录响应;Relogin 使用 SDK 保存的 Baseapp 地址。Provider 应以这个 Endpoint 为连接源,不能把 Baseapp 地址固定写死。
CUSTOM 与 CUSTOM_ALL
| 网络模式 | Loginapp | Baseapp / Relogin | 适用场景 |
|---|---|---|---|
CUSTOM | SDK 内置 TCP | 自定义 Provider | 只希望游戏连接使用自定义传输 |
CUSTOM_ALL | 自定义 Provider | 自定义 Provider | WebGL、小游戏,或整个连接链路都必须使用平台网络 API |
WebGL 以及不允许 Loginapp 使用普通 TCP 的小游戏运行时必须选择 CUSTOM_ALL。若选择 CUSTOM,Loginapp 阶段仍会创建 SDK 内置 TCP Provider,即使 Baseapp 已配置自定义网络也无法完成前置登录。
小游戏平台接入状态
本文提供的是通用 Provider 契约与 Unity WebSocket 的现成实现。微信小游戏、抖音小游戏以及不同客户端引擎自带 Socket 的具体 Provider,需要根据目标引擎和平台 API 单独实现与验证;当前 Unity WebSocket Provider 不能直接视为这些平台的通用插件。
接入 Unity WebSocket Provider
环境要求
- KBEngine Nex
v3.0.6或包含INetworkProvider接口的更新版本。 - Unity 2022.3 LTS,或兼容的更高版本。
- 服务端直连端口能够处理 WebSocket Upgrade,或者已经配置支持 WebSocket 的反向代理。
版本兼容
旧版 C# SDK 中的 UNITY_WEB_SOCKET、enableWSS、domainMapping 直连配置属于旧网络接口。新 Provider 接入应使用 CUSTOM/CUSTOM_ALL 和 customNetworkProviderFactory,不要混用两套实现。
1. 获取 Provider
从 KBEngineNex-CSharpSDKProvider 下载或克隆仓库,将 UnityWebSocketProvider 整个目录复制到 Unity 项目的 Assets 下。目录内运行文件包括:
UnityWebSocketProvider/
├── UnityWebSocketNetworkProvider.cs
├── WebSocket.cs
├── WebSocket.jslib
├── NativeWebSocket.LICENSE.txt
└── THIRD_PARTY_NOTICES.md不要只复制 UnityWebSocketNetworkProvider.cs。WebSocket.cs 是传输实现,WebSocket.jslib 是 WebGL 构建期桥接插件;缺少其中任何一个都会导致对应平台无法编译或运行。
2. 确认程序集位置
Provider 必须能够引用定义 INetworkProvider 的 C# SDK。推荐让 SDK 和 Provider 进入同一个程序集。
| C# SDK 位置 | Provider 位置 | 结果 |
|---|---|---|
Assets/Plugins | Assets/Plugins 或普通 Assets 源码目录 | 支持 |
普通 Assembly-CSharp | 同一个 Assembly-CSharp | 支持 |
| 热更新程序集 | 同一个热更新程序集 | 支持 |
| 预编译 SDK DLL | 能引用该 DLL 的程序集 | 支持 |
| 预定义程序集中的源码 SDK | 独立 .asmdef 程序集 | 不支持 |
| 热更新 SDK | Unity AOT/预定义程序集中的 Provider | 不支持 |
Unity 不允许 .asmdef 程序集反向引用 Assembly-CSharp 或 Assembly-CSharp-firstpass。Provider 仓库不自带 .asmdef,就是为了让接入项目自行决定程序集归属。
即使 C# SDK 和 Provider 使用运行时热更新,WebSocket.jslib 也必须在构建 WebGL Player 时已经位于 Unity 工程中,并启用 WebGL 平台。浏览器桥接代码不能在 Player 构建完成后再通过热更新补入。
3. 注册 Provider 工厂
继承 SDK 提供的 UnityKBEMain,覆盖 CreateCustomNetworkProviderFactory():
using System.Collections.Generic;
using KBEngine;
using KBEngine.UnityWebSocket;
using UnityEngine;
public sealed class ClientApp : UnityKBEMain
{
[Header("WebSocket Provider")]
public bool enableWss = true;
public string webSocketPath = "/";
private readonly Dictionary<string, string> _domainMapping =
new Dictionary<string, string>
{
// 把服务端下发的内网地址转换为客户端可访问的公网入口。
// Translate the private endpoint returned by the server to a public ingress.
{ "192.168.36.128", "wss.kbelab.com" },
};
private readonly Dictionary<int, int> _portMapping =
new Dictionary<int, int>
{
// Loginapp 与 Baseapp 通常映射到不同的公网代理端口。
// Loginapp and Baseapp usually map to different public proxy ports.
{ 20013, 443 },
{ 20015, 444 },
};
protected override INetworkProviderFactory CreateCustomNetworkProviderFactory()
{
// 非自定义模式仍使用 SDK 内置 Provider,不创建无效的第三方传输。
// Built-in modes continue to use SDK providers and need no custom transport.
if (networkType != KBEngineApp.NETWORK_TYPE.CUSTOM &&
networkType != KBEngineApp.NETWORK_TYPE.CUSTOM_ALL)
{
return null;
}
return new UnityWebSocketProviderFactory(
secure: enableWss,
path: webSocketPath,
domainMapping: _domainMapping,
portMapping: _portMapping);
}
}UnityKBEMain.initKBEngine() 会自动把返回的工厂写入 args.customNetworkProviderFactory。如果项目没有继承 UnityKBEMain,也可以在创建 KBEngineArgs 时直接注册:
var args = new KBEngineArgs
{
ip = "127.0.0.1",
port = 20013,
networkType = KBEngineApp.NETWORK_TYPE.CUSTOM_ALL,
isMultiThreads = false,
customNetworkProviderFactory = new UnityWebSocketProviderFactory(
secure: false,
path: "/",
domainMapping: null,
portMapping: null),
};
var app = new KBEngineApp(args);4. 选择网络模式
在 Unity Inspector 中把 networkType 设置为:
CUSTOM_ALL:推荐用于 WebGL;Loginapp、Baseapp 和 Relogin 全部使用 WebSocket。CUSTOM:Loginapp 使用 TCP,Baseapp 和 Relogin 使用 WebSocket。
如果选择自定义模式但工厂返回 null,SDK 初始化会抛出 CUSTOM network mode requires customNetworkProviderFactory.。这通常表示没有覆盖工厂方法、脚本组件类型不正确,或者 networkType 与工厂判断不一致。
5. 配置 ws、wss 与路径
工厂参数含义如下:
| 参数 | 说明 |
|---|---|
secure | false 生成 ws://,true 生成 wss:// |
path | WebSocket 路径,空值按 / 处理;可带查询参数,不能带 #fragment |
domainMapping | 将 SDK Endpoint 中的 Host 精确映射为公开域名 |
portMapping | 将 SDK Endpoint 中的 TCP 端口映射为公开端口 |
本地直连示例:
Endpoint: 127.0.0.1:20013
secure: false
mapping: 无
结果: ws://127.0.0.1:20013/公网 WSS 示例:
Endpoint: 192.168.36.128:20015
domainMapping["192.168.36.128"] = "wss.kbelab.com"
portMapping[20015] = 444
secure: true
结果: wss://wss.kbelab.com:444/映射只转换连接入口,不会改变 SDK 的 Loginapp 到 Baseapp 地址流转。未命中的 Host 和端口保持原值。Host 映射键按完整字符串匹配,但不区分大小写。
6. 设置线程模式
Provider 不会修改 isMultiThreads。线程模式仍由应用统一决定:
isMultiThreads = false:Unity 的FixedUpdate()调用gameapp.process();WebGL 和要求主线程访问网络 API 的平台应使用此模式。isMultiThreads = true:SDK 网络线程驱动Process();适合允许后台网络线程的平台,但仍需验证所用传输库的线程限制。
WebSocket 的平台回调只把事件放入队列,OnConnected()、OnDataReceived()、OnClosed() 最终都在 Provider 的 Process() 中按顺序通知 SDK。不要从 NativeWebSocket 回调直接调用 KBEngine Entity 或事件 API。
7. 配置服务端或反向代理
KBEngine Nex 外部 TCP Channel 可以在同一个监听端口识别 WebSocket Upgrade,ws 直连通常不需要额外端口。使用 wss 时有两种方式:
- 在服务端配置
channelCommon/sslCertificate与sslPrivateKey。 - 由 Nginx、负载均衡器或网关终止 TLS,再把 WebSocket 转发到 KBEngine 端口。
反向代理必须:
- 使用 HTTP/1.1,并保留
Upgrade和Connection请求头。 - 原样转发二进制 WebSocket Frame,不能转换为文本。
- 支持长连接,并设置足够长的读写及空闲超时。
- 分别为 Loginapp 和 Baseapp 提供可达入口,或采用能正确路由两类连接的统一网关。
完整 Nginx 示例参见《Nginx WSS 配置》。
TLS 与协议加密不是一回事
wss 保护 WebSocket 传输链路;networkEncryptType 控制 KBEngine 协议层加密。两者处于不同层级,不能用其中一个参数代替另一个。
运行时数据流
一次完整登录的连接流程如下:
Unity 创建 KBEngineArgs
-> 注册 UnityWebSocketProviderFactory
-> SDK 为 Loginapp 创建 NetworkProviderContext
-> Factory.Create(context) 返回 Provider A
-> Provider A 连接并在 Process() 报告 OnConnected
-> SDK 完成 Loginapp 协议并取得 Baseapp Endpoint
-> SDK 关闭 Provider A
-> Factory.Create(baseappContext) 返回全新 Provider B
-> Provider B 连接 Baseapp
-> 收到的字节交给 SDK 协议读取器远端断开时,Provider 在 Process() 中报告一次 OnClosed。SDK 随后执行 Session 清理、触发断线事件,并在需要重登录时使用保存的 Baseapp Endpoint 创建新 Provider。Provider 自己不应启动 KBEngine 重登录流程。
发送队列与内存所有权
SDK 调用 Send(IReadOnlyList<ArraySegment<byte>> packets) 时,数组片段可能指向 SDK 的对象池内存。Provider 返回 Accepted 前必须完成以下二选一操作:
- 把所有字节复制到 Provider 自己拥有的缓冲区。
- 明确接管一块生命周期独立的内存。
Provider 不能保存这些 ArraySegment<byte>,等异步发送时再读取。SDK 在 Send() 返回后可以立即复用底层内存,这会导致数据被覆盖、协议包随机损坏或泄露其他消息内容。
官方 WebSocket Provider 会把本次发送的所有片段合并为一个完整批次,并以 NetworkProviderContext.SendQueueSize 作为排队字节上限:
- 有容量:复制完整批次,返回
Accepted。 - 容量不足:不接受任何部分,返回
Backpressure。 - 尚未连接:返回
NotConnected。 - 同步发送处理失败:返回
Failed。
这种做法每批次增加一次分配和复制,但换来了确定的内存所有权、并发安全和完整 Bundle 语义。自研 Provider 若要减少 GC,应使用有上限的 Buffer Pool,并确保归还时机与异步发送完成严格对应。
自定义 Provider 接口契约
需要接入其他传输库时,实现 INetworkProviderFactory 和 INetworkProvider:
public interface INetworkProviderFactory
{
INetworkProvider Create(NetworkProviderContext context);
}
public interface INetworkProvider : System.IDisposable
{
NetworkProviderState State { get; }
void Connect(INetworkProviderListener listener);
NetworkSendResult Send(
System.Collections.Generic.IReadOnlyList<System.ArraySegment<byte>> packets);
void Process();
void Close();
}实现时必须遵守以下规则:
生命周期
- 新实例初始状态为
Created。 Connect()每个实例只能调用一次,进入Connecting。- 连接成功后先设置
Connected,再排队连接成功事件。 - 远端关闭或不可恢复错误进入
Failed,并只报告一次OnClosed。 - 主动
Close()进入Closing、释放资源后进入Closed;主动关闭不应再产生远端断开回调。 Close()、Dispose()应可重复调用,不能重复回调或重复释放资源。
回调与线程
- 工作线程、浏览器回调或平台回调只允许排队事件。
- Listener 的三个方法只能由
Process()同步调用。 - 连接、数据和关闭事件必须保持实际发生顺序。
Process()不应执行无界阻塞 IO,否则会卡住 SDK Tick 或 Unity 主线程。
接收语义
- 将收到的二进制字节原样传给
OnDataReceived(buffer, offset, count)。 - 不要假设一次回调等于一条 KBEngine 消息。
- 不要在 Provider 内解析、拼接或修改 KBEngine 协议。
- 如果底层库会复用接收缓冲区,要保证 Listener 调用期间数据有效。
背压与资源上限
- 使用
context.SendQueueSize限制未完成发送字节,避免慢连接导致内存无限增长。 - 队列已满时返回
Backpressure,不要静默丢包。 - 返回
Accepted必须表示整批数据已被接管,不能只接受部分片段。 - 连接失败、远端关闭和主动关闭时清空发送队列并释放缓冲。
并发安全
多线程模式下,Send()、平台回调、关闭和发送完成可能并发发生。状态、队列和资源释放必须具备同步保护,同时避免在持锁期间执行用户回调或长时间网络操作,以降低锁竞争和死锁风险。
验证清单
接入完成后,至少验证:
CUSTOM_ALL能连接 Loginapp,再切换并连接 Baseapp。- 使用
CUSTOM时,Loginapp TCP 与 Baseapp 自定义传输都能工作。 - 账号登录、Entity 创建、属性同步和远程方法调用正常。
- 主动退出、服务端关闭和网络中断只触发一次断线流程。
- Relogin 创建新 Provider,不复用旧连接状态。
- 发送队列达到上限时有明确背压,不会无限增加内存。
- WSS 证书链、域名和 SNI 正确,客户端不会因证书错误拒绝连接。
- 反向代理正确转发 Upgrade,空闲超时大于业务心跳间隔。
- Standalone 与 WebGL 分别构建和运行,因为两者使用不同的 NativeWebSocket 实现。
- WebGL 使用
CUSTOM_ALL、isMultiThreads = false,且WebSocket.jslib已进入 Player。
常见问题
SDK 初始化提示缺少 customNetworkProviderFactory
检查 networkType 是否为 CUSTOM/CUSTOM_ALL,以及 CreateCustomNetworkProviderFactory() 是否确实返回了工厂。Unity 场景中挂载的组件也必须是覆盖该方法的派生类,而不是原始 UnityKBEMain。
Loginapp 能连接,切换 Baseapp 后失败
优先检查 Loginapp 下发的 Baseapp Host 和 TCP 端口。若下发的是内网地址,需要在 domainMapping、portMapping 中配置对应公网入口,并确保 Baseapp 的代理端口已开放。不要只映射 Loginapp 的 20013。
WebGL 一开始就连接失败
确认使用 CUSTOM_ALL 而不是 CUSTOM,并设置 isMultiThreads = false。同时检查 WebSocket.jslib 是否进入 WebGL 构建,以及 HTTPS 页面是否错误连接了 ws://。HTTPS 页面应使用 wss://。
WSS 返回 400 或 502
400通常表示 Upgrade 请求头没有正确转发,或路径不匹配。502通常表示代理无法连接 KBEngine 后端端口。- 握手成功后立即关闭,应同时检查代理日志和 Loginapp/Baseapp 日志。
连接正常但收不到消息
确认 Unity 持续驱动 gameapp.process()。单线程模式如果停止调用 Process,平台事件会留在 Provider 队列中,连接可能已经建立,但 SDK 不会收到连接、数据和关闭通知。
运行一段时间后内存持续增长
检查发送速度是否长期高于网络出口速度、SEND_QUEUE_MAX 是否设置过大,以及自研 Provider 是否在关闭后释放发送和接收缓冲。官方 WebSocket Provider 会按 SendQueueSize 拒绝超限排队,不应通过设置无限大队列掩盖慢连接。
