Skip to content

网络层 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 面向统一接口工作。

text
游戏业务 / UI
      |
KBEngine C# SDK
  - 登录与重登录
  - 协议编解码
  - Entity 与事件系统
      |
INetworkProvider
  - 连接状态
  - 有序字节收发
  - 关闭与资源释放
      |
TCP / KCP / WebSocket / 平台网络库

Provider 解决什么问题

Provider 机制主要解决以下问题:

  1. 平台适配:Unity WebGL、原生平台或特定网络库可以实现自己的传输。
  2. 依赖隔离:第三方网络库放在独立 Provider 中,不进入 C# SDK 核心代码。
  3. 生命周期统一:连接、收发、断开、重登录和资源释放使用同一套契约。
  4. 线程边界明确:平台回调可以来自任意线程,但只允许在 Process() 中进入 SDK。
  5. 部署适配:Provider 可以完成内网地址到公网域名、代理端口的转换。
  6. 独立演进: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.jslibSystem.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当前连接角色,值为 LoginappBaseapp
EndpointSDK 决定的目标地址,包含 Host、TCP 端口和 UDP 端口
ServerVersion当前已知的服务端版本
SendQueueSizeProvider 可使用的应用层发送队列上限
TcpSendBufferSize / TcpReceiveBufferSizeTCP 缓冲参数
UdpSendWindowSize / UdpReceiveWindowSizeUDP/KCP 窗口参数

Loginapp 的 Endpoint 来自启动参数 ipport;Baseapp 的 Endpoint 来自 Loginapp 登录响应;Relogin 使用 SDK 保存的 Baseapp 地址。Provider 应以这个 Endpoint 为连接源,不能把 Baseapp 地址固定写死。

CUSTOM 与 CUSTOM_ALL

网络模式LoginappBaseapp / Relogin适用场景
CUSTOMSDK 内置 TCP自定义 Provider只希望游戏连接使用自定义传输
CUSTOM_ALL自定义 Provider自定义 ProviderWebGL、小游戏,或整个连接链路都必须使用平台网络 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_SOCKETenableWSSdomainMapping 直连配置属于旧网络接口。新 Provider 接入应使用 CUSTOM/CUSTOM_ALLcustomNetworkProviderFactory,不要混用两套实现。

1. 获取 Provider

KBEngineNex-CSharpSDKProvider 下载或克隆仓库,将 UnityWebSocketProvider 整个目录复制到 Unity 项目的 Assets 下。目录内运行文件包括:

text
UnityWebSocketProvider/
├── UnityWebSocketNetworkProvider.cs
├── WebSocket.cs
├── WebSocket.jslib
├── NativeWebSocket.LICENSE.txt
└── THIRD_PARTY_NOTICES.md

不要只复制 UnityWebSocketNetworkProvider.csWebSocket.cs 是传输实现,WebSocket.jslib 是 WebGL 构建期桥接插件;缺少其中任何一个都会导致对应平台无法编译或运行。

2. 确认程序集位置

Provider 必须能够引用定义 INetworkProvider 的 C# SDK。推荐让 SDK 和 Provider 进入同一个程序集。

C# SDK 位置Provider 位置结果
Assets/PluginsAssets/Plugins 或普通 Assets 源码目录支持
普通 Assembly-CSharp同一个 Assembly-CSharp支持
热更新程序集同一个热更新程序集支持
预编译 SDK DLL能引用该 DLL 的程序集支持
预定义程序集中的源码 SDK独立 .asmdef 程序集不支持
热更新 SDKUnity AOT/预定义程序集中的 Provider不支持

Unity 不允许 .asmdef 程序集反向引用 Assembly-CSharpAssembly-CSharp-firstpass。Provider 仓库不自带 .asmdef,就是为了让接入项目自行决定程序集归属。

即使 C# SDK 和 Provider 使用运行时热更新,WebSocket.jslib 也必须在构建 WebGL Player 时已经位于 Unity 工程中,并启用 WebGL 平台。浏览器桥接代码不能在 Player 构建完成后再通过热更新补入。

3. 注册 Provider 工厂

继承 SDK 提供的 UnityKBEMain,覆盖 CreateCustomNetworkProviderFactory()

csharp
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 时直接注册:

csharp
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 与路径

工厂参数含义如下:

参数说明
securefalse 生成 ws://true 生成 wss://
pathWebSocket 路径,空值按 / 处理;可带查询参数,不能带 #fragment
domainMapping将 SDK Endpoint 中的 Host 精确映射为公开域名
portMapping将 SDK Endpoint 中的 TCP 端口映射为公开端口

本地直连示例:

text
Endpoint: 127.0.0.1:20013
secure: false
mapping: 无
结果: ws://127.0.0.1:20013/

公网 WSS 示例:

text
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 时有两种方式:

  1. 在服务端配置 channelCommon/sslCertificatesslPrivateKey
  2. 由 Nginx、负载均衡器或网关终止 TLS,再把 WebSocket 转发到 KBEngine 端口。

反向代理必须:

  • 使用 HTTP/1.1,并保留 UpgradeConnection 请求头。
  • 原样转发二进制 WebSocket Frame,不能转换为文本。
  • 支持长连接,并设置足够长的读写及空闲超时。
  • 分别为 Loginapp 和 Baseapp 提供可达入口,或采用能正确路由两类连接的统一网关。

完整 Nginx 示例参见《Nginx WSS 配置》

TLS 与协议加密不是一回事

wss 保护 WebSocket 传输链路;networkEncryptType 控制 KBEngine 协议层加密。两者处于不同层级,不能用其中一个参数代替另一个。

运行时数据流

一次完整登录的连接流程如下:

text
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 接口契约

需要接入其他传输库时,实现 INetworkProviderFactoryINetworkProvider

csharp
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();
}

实现时必须遵守以下规则:

生命周期

  1. 新实例初始状态为 Created
  2. Connect() 每个实例只能调用一次,进入 Connecting
  3. 连接成功后先设置 Connected,再排队连接成功事件。
  4. 远端关闭或不可恢复错误进入 Failed,并只报告一次 OnClosed
  5. 主动 Close() 进入 Closing、释放资源后进入 Closed;主动关闭不应再产生远端断开回调。
  6. 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_ALLisMultiThreads = false,且 WebSocket.jslib 已进入 Player。

常见问题

SDK 初始化提示缺少 customNetworkProviderFactory

检查 networkType 是否为 CUSTOM/CUSTOM_ALL,以及 CreateCustomNetworkProviderFactory() 是否确实返回了工厂。Unity 场景中挂载的组件也必须是覆盖该方法的派生类,而不是原始 UnityKBEMain

Loginapp 能连接,切换 Baseapp 后失败

优先检查 Loginapp 下发的 Baseapp Host 和 TCP 端口。若下发的是内网地址,需要在 domainMappingportMapping 中配置对应公网入口,并确保 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 拒绝超限排队,不应通过设置无限大队列掩盖慢连接。