客户端网关技术方案

 

背景

实现一个可以完成游戏端/Web端与业务服务之间的跨语言协议交互的网关,主要实现以下功能:

  1. 基于 Cocos 运行的游戏
  2. 基于 Unity 运行的游戏
  3. 基于 WebView 运行的游戏或业务

协议相关

协议调用方式

目前协议调用方式有以下六种情况:

  1. 只要请求,无需协议实现方回复

alt text

  1. 请求和回复协议使用相同协议名

alt text

  1. 请求协议与回复协议使用不同协议名

alt text

  1. 请求协议与回复协议使用不同协议名,且回复协议有多个,会根据处理结果选择其中一个协议进行回复,1vN

alt text

  1. 由协议实现方(例如长连接、RTC等持续服务)主动向游戏业务发发送消息

alt text

  1. 多场景共存情况,协议调用结果不止要回复给调用方,还需发送给其他关注方。例如:在 Unity 游戏中打开 H5 充值兑换页面,操作结束后,将充值兑换结果告知 H5 和 Unity 游戏刷新 UI 显示。

alt text

上述 6 种场景,均为现有交互协议定义及使用的情况汇总。其中 3、4 、6 相较比较特殊,其中 Request时使用的协议定义和Response时的并不相同功能,本次不会进行统一,而是进行映射兼容处理。因为涉及很多历史业务且存量较大。

协议定义

目前协议有 Panda、River、OpenPlatform,根据协议定义,可以划分为:CMD 和 URI 两类:

River 采用 URI 方式
URI 字符串:scheme://user:password@host/path?_info=base64(jsonstring)
结构说明:
scheme:用来区分统一跳转来源
user:password:为鉴权所需参数,在我们的项目中对应 appKey 和 secretKey
host:(target)为所在业务层,比如 chat、feed 等
path:(action)为该统一跳转所代表含义
info:为参数,在进行传递前需进行 base64 加密 
Panda、OpenPlatform 采用 CMD 方式
CMD Map:    
{
    identifier: "OpenPlatformThirdLogin", // 交互 CMD
    parameter: {
        appKey: string,
        randomNumber: number,
        dynamicKey: string,
        secretKey: string
    } // 交互所需参数
}

新的协议采用 ALPC 协议,与 Frontier 对接

ALPC 协议更新

// 跨平台语言类型
enum ALPCLXType {
    UNKNOWN = 0;
    UNIVERSAL = 1;
    RUST = 2;
    OBJC = 3;
    SWIFT = 4;
    JAVA = 5;
    KOTLIN = 6;
    CPP = 7;
    CSHARP = 8;
    JAVASCRIPT = 9;    
    PYTHON = 10;
    DART = 11;
    COCOS = 12; // [新增] 对应 Cocos 引擎
    UNITY = 13; // [新增] 对应 Unity 引擎
    WEB = 14;   // [新增] 对应 WebView 交互
};


// 请求实体
message ALPCRequest {
    // 消息 id,由 Mediator 生成
    uint32 id = 1;
    // 发送者
    ALPCLXType tx = 2;
    // 接收者
    ALPCLXType rx = 3;
    // 接收者模块
    string target = 4;
    // 接收者模块功能
    string action = 5;
    // [新增] 来源 0=星桥 1=网关
    int32 from = 6;
    // [新增] 版本:0=>未知,1~10=>老协议(1=cmd, 2=uri),11=>新协议
    int32 ver = 7;
    // [新增][可选] 标签,由业务方定义和使用,可以用于标识内部场景
    optional string tag = 8;
}


// 响应实体
message ALPCResponse {
    // 消息 id,由 ALPCRequset 赋值
    uint32 id = 1;
    // 发送者
    ALPCLXType tx = 2;
    // 接收者:ALPCRequset.tx
    ALPCLXType rx = 3;
    // 响应码
    ALPCStatusCode code = 4;
    // [新增]  版本:0=>未知,1~10=>老协议(1=cmd, 2=uri),11=>新协议
    int32 ver = 5;
    // [新增][可选] 标签:ALPCRequset.tag
    optional string tag = 6;
}


// 基础消息协议
message ALPCMessage {
    // 消息 id,由 StarBridge 生成
    oneof id_oneof { string id = 1; };
    // 载荷
    oneof payload_oneof { bytes payload = 2; };
    // 需处理的实体
    oneof body_oneof {
        ALPCRequest request = 3;
        ALPCResponse response = 4;
        ALPCBroadcast broadcast = 5;
    };
}

交互协议:(APLC 中的 Payload)

message OPMessage {
    // 内容
    string val = 1;
    // [可选] 场景页面信息,涉及到 UI 操作及埋点时选用
    option OPPageInfo page = 2;
}


message OPPageInfo {
    // 类名
    string cls = 1;
    // 实例标识,可以通过该值,筛选页面实例
    string identify = 2;
    // 页面名称
    string name = 3;
}

架构设计

旧架构(改造之前,当时的架构)

alt text

  1. River 目前有 Native Cocos/Unity 游戏,暂无 Web 游戏,统一走 River 协议框架,具有自己的原生能力实现,以接入二方游戏为主,无鉴权功能。
  2. Panda 目前包含的业务场景有:旧 Native Cocos 游戏、旧 Web Cocos 游戏以及原生 WebView,对于协议调用有一部分是游戏容器内自己实现并响应,还有一部分是通过 Panda 来中转响应。
  3. Open Platform 是为三方 Web 游戏提供支持方案,具体调用能力鉴权功能。具有自己的协议定义及原生能力实现。
  4. 若在上述 1、2 的 Native 游戏场景中,打开网页,则该网页是由原生 WebView 提供,其原生交互能力是由 Panda 提供。 目前协议框架较多且均有各自的协议定义和原生能力实现,其中还有部分重复实现的能力。其中大部分协议均是通过字符串硬编码的方式供业务方调用,业务方若使用该功能需要根据协议文档定义才能正确使用。 随着业务的迭代和发展,出现各协议框架提供的能力、交互方式不一致的情况,无法快速的支持业务迭代。

新架构

alt text

流程图

alt text

依据不同的协议定义,网关提供三个调用入口:

  1. 原协议 CMD 调用入口
  2. 原协议 URI 调用入口
  3. ALPC 协议调用入口

Pipeline Scheduler:调度器,根据 Input Message 的类型,来选择并运行 Worker。 Worker:一个完整的处理流程单元;由代码编排好的 pipeline。

  • 输入:协议数据,分 Request 和 Response 类型
  • 输出:处理后的新数据

Worker 中的处理节点: 遵循节点接口实现,按需将节点编排到 Worker 中。每个节点为独立功能,节点实现可以是一个单例也可以是普通实例。

DSL: 一些与具体协议相关的处理逻辑,编写相应的 DSL 语句,通过执行 DSL 来处理逻辑 Auth:鉴权、用户授权

Broker: 处理消息订阅和转发推送

DSL 模块

用来处理具体协议相关的一些逻辑,如数据过滤、新旧协议转换、Tag提取等。

在原 Panda、River、OpenPlatform 中,具体的协议名及参数等的定义,有些是同一能力协议名相同,实现是多份,且入参字段不同;有些是协议名不同,实现却是复用的;等等之类情况,在统一协议实现后,这些情况均需要去做转换进行兼容。

因此将转换兼容等相关逻辑进行抽象,提取为 DSL 动作。

DSL 模块:主要由分支判断(IF-ELSE)、条件和动作三部分组成,基本形式如下

if
    条件1
then
    动作1
elsif
    条件2
then
  动作2
else
  动作3
end

详细介绍:

  • 条件语句:即判断语句,用来判断 true 或 false 。条件语句由连接符(and、or)、操作符、因子(环境变量)组成。运算优先级遵循常规高级语言规则,鉴于 Python 的可读性较好,因此相关命名均采用 Python 中的运算符
    • 连接符: and 和 or 分别是与和或连接运算
    • 操作符:用于来连接因子、变量、常量进行相关的逻辑运算,计划暂支持如下操作符号:
操作符 说明
== 等于
!= 不等于
> 大于
>= 大于等于
< 小于
<= 小于等于
in 包含于
not in 不包含于
isBlank 为空
isNotBlank 不为空
  • 因子:环境变量,获取网关、App等相关上下文信息。具体内容暂定。

  • 动作语句:满足 if 条件之后执行的行为,该类是处理与协议相关逻辑的定义。格式定义暂定。
  • 函数:在条件语句和动作语句均可以使用。格式定义暂定。
    • 内置函数:内置基本功能函数,例如:拼接字符串、字符串包含、集合大小等等
    • 扩展函数:处理一些非通过逻辑的函数
Auth 模块

对协议调用进行鉴权、用户授权处理。

用户授权对可能需求,设计了四种方式:

  1. 多次触发协议授权,不会因授权中状态而拦截。因此会出现连续授权弹框情况。弹框呈现分:FIFO(先进先出) 和 LIFO (后进先出)两种方式。例如:苹果手机 App 申请权限弹框是 LIFO 方式展示,App 依次申请网络、通知权限,用户看到的过程是,先弹出网络授权弹框,接着立即又弹出通知权限弹框。此时展示的是通知权限弹框。当用户通知弹框授权后,就会看到网络弹框。
  2. 多次触发协议授权,只拦截同一协议授权中状态
  3. 多次触发协议授权,若任一协议授权中,则拦截
  4. 统一授权,类似微信小程序。进入游戏后,可预览游戏。提示用户需要授权可以使用完整功能,或当触发到相关功能时,提示需要授权才能使用完整功能。通过专门的授权协议一次性授权所有需要的相关协议。例如:我们的微信内推小程序

方式 1 流程图 alt text

方式 2 流程图 alt text

方式 3 流程图 alt text

Broker 模式

ALPC协议 中,第 5 种方式:由协议实现方(p长连接、RTC等持续服务)主动向游戏业务发发送消息。以及第 6 种跨场景接收消息。

该类消息是无源消息,协议实现方产生消息,向游戏业务场景发送,但此时实现方并不知道要将消息具体发给谁。

该 Broker 模块,就是为解决这种问题。它采用发布/订阅(publish/subscribe)模式。将消息转发相关的订阅者。

在 5、6 方式中,不会平白无故向游戏发送消息,必定会有一个触发该事件的事件源头。比如要监听 App 前后台切换状态,必须先要告知 Native 侧,需要对 App 前后台状态变化进行监听。要接收某长连接的消息,必须先创建或订阅该连接。

而对于协议实现方而言,发送消息,并不关心该消息是发给 Cocos 游戏还是 Unity 游戏。本身是独立协议功能,无需关心调用方业务。因此发送消息时,这里并不知道需要发给 Cocos 还是 Unity。但是有初始化该服务或业务的事件源。因此该 Broker 的基本时序图如下: alt text

业务种类

由原生提供的常驻、持续服务(服务方),均会产生消息,向游戏业务场景(消费方)直接发送。且服务方和消费方是多对多的关系,因此需要一个 标识 可以关联双方关系。

对于一个通过协议对外提供的业务,其 Publish、Subscribe 协议均是预先制定好的。所以为简化使用方流程,对于 1Publish - 1Subscribe 的采用自动订阅方式;1Publish - 多Subscribe 的采用手动订阅;

其中 Subscribe 分: Subscribe Once 和 Subscribe Multi ;Once 触发一次后,自动取消订阅。Multi 可多次触发,需要主动取消订阅

综上所述,可以分以下种类:

  1. 从标识来源划分:
    1. 服务方本身业务参数已有业务唯一标识,该参数消费方(调用方)和服务方均在服务初始化前已知。
    2. 例如长连接的 IP:PORT 就可标识一条连接
    3. 服务方本身业务参数无标识;或消费方(调用方)和服务方均在服务初始化前没有已知的可以作为标识的参数。
    4. 该情况需要服务方来提供标识,因此在调用方在 Subscribe 前,需要再向服务方获取一次标识
  2. 从订阅方式划分:
    1. 1P-1S:1 发布对应 1 订阅,会在发送 Publish 协议的时候,网关会通过 DSL 自动提取相关信息,进行 Subscribe 的订阅。
    2. 1P-NS:1 发布对应 N 订阅,在发送 Publish 协议的时候,网关不会进行订阅操作。
订阅者结构

订阅者存储使用二级结构,外层使用 Map,Value 为 Array,Array 中元素为订阅者实例 其中存储 key 为 由 DSL 提取出的 唯一标识 或要接收相应数据协议的 target/action

type SubscribeCache = Arc<DashMap<String, Vec<Subscribe>>>;


struct Subscribe {
  target: String, // 接收者模块
  action: String, // 接收者模块功能
  rx: ALPCLXType, // 接收者:由 ALPCRequset.tx 赋值
  tag: Option<String>, // 标签:由 ALPCRequset.tag 赋值
  ver: i32, // 协议版本:由 ALPCRequset.ver 赋值
}
时序图

种类 1-a 和 2-a,以建立长连接示例: alt text

种类 1-b 和 2-b 示例: alt text

Payload 数据转换

在原协议定义中,协议参数均以 JSON 字符串格式与原生交互。在新的协议实现中,该实现方法是与 Frontier 中一致的,为 Protobuf 模型实例。因此需要进行数据结构转换。

在这个阶段中 Payload 协议参数转换流程,以 URI 协议为例: alt text

注:在 Protobuf 模型实例与 JSON 字符串互转,Android 端 Protobuf 官方组件提供互转方法;iOS 端没有提供,需要自己开发。

容器多开

目前对于 Cocos、Unity 均是不可以多开的,不能同时打开两个 Cocos 或 Unity 游戏。单对于基于 WebView 运行的游戏及业务是可以打开多个 WebView 的,存在多开情况。

因此对于 WebView 容器多开需要进行处理,在收到 ALPCLXType == Web 的 APLC 消息时,需要找到具体响应它的 WebView 容器实例。

WebView 架构图: 由 WebService 来统一维护 WebView 实例,每个实例都对应生成一个唯一标识。在调用协议时,该标识赋值给 ALPCRequset 中 tag 字段。

WebView 交互时序图: alt text

图中 DSL 提取标识自动订阅 和 DSL 提取标识查询订阅者 部分,原有提取的标识是业务标识,在此处新增了实例标识,该值是存储在 ALPC 协议中的 tag 字段。

各类标识区别如下:

序号 名称 来源 作用
1 业务标识,使用 DSL 提取 协议参数中获取 标识具体业务
2 实例标识,ALPC 协议中的 tag 字段 由业务方定义和生成 用于标识内部场景
3 订阅者标识 使用业务标识 作为存储订阅者的 Key;
业务标识作为订阅者实例 tag 字段的值

涉及 UI 相关的协议

该类协议原先均是在游戏自身容器内实现,可以直接获得容器视图,在此视图上展示新的 UI。

新的协议实现和游戏容器是各自独立的,不同容器共用一个实现。因此在显示 UI 时,需要获得容器视图。

在 OPMessage 中 OPPageInfo 记录页面信息,通过该信息来获取容器视图:

  • Cocos、Unity 容器均不可以多开,因此可以通过类名,筛选容器视图
  • 由于 WebView 容器可以多开,所以无法只通过类名筛选,增加实例地址(iOS) 或 hash(Android) 的比较方式,二次筛选具体视图实例

OPPageInfo 中各值的来源:

  • 对于 Cocos、Unity 不会多开的类型,在初始化容器时,将容器视图信息传递给引擎,引擎向网关发消息时,初始化 OPPageInfo 实例,传递给网关
  • 对于 WebView 可以多开的类型,在 WebService 中收到 WebView 实例调用协议时,使用该实例的视图信息初始化 OPPageInfo 实例,再而传递给网关

涉及到 长连接 的 UI 协议实现: 目前 UI 相关的协议均为原生实现,不同的业务可能会关注同一个长链接,使用的是相应的 Topic 的消息。而一个长链接收到消息均是在一个方法内收到,因此需要一个 消息分发器 。其架构可以借鉴 iOS 中对 IM 消息处理方式,此处不再赘述。

目前 UI 相关的复杂能力:

  1. 弹幕:含有一条长连接
  2. 聊天气泡:含有两条长连接,IM 和 游戏
  3. 礼物展示:含有一条长连接
  4. 礼物面板

注:目前涉及 UI 的协议中,其 UI 在容器中展示的位置均是有协议实现方提供的位置。没有由调用方自定义 UI 位置。因此本次不增加 Layout 相关能力。若后续有相关需求,再增加相关功能即可。

游戏容器统一

在原容器内实现的相关协议迁移完后,就可将原来的同一引擎但有多个容器的情况,进行合并。每个引擎对应一个根容器。我们只需要与根容器进行交互。若业务方需要定制容器,只需要继承根容器,进行自定义即可。

关于打开 URL 或 统一跳转情况

打开 URL 协议

  • 保留通过协议可以打开任一 Web URL
  • 对于在打开的 WebView 中会与原生交互,并会产生数据,需要同步给其他容器或业务的情况。需将相关功能均 定义为具体协议 ,而是不是使用 通用的打开 URL 协议 。以确保逻辑的严密性。
  • 对于打开的 WebView 内相关的业务均是自身使用,不会跨容器交换数据,此类可以使用 通用的打开 URL 协议 。 统一跳转
  • 对于通过 suite 组件(类似对业务方提供的SDK)接入的业务,均不提供使用统一跳转调用能力。只能使用 suite 提供的功能。
  • 在 App 内非通过 suite 的常规业务,和原先一致,具备完整的统一跳转能力。例如使用统一跳转启动三方游戏等。
  • 对于旧的不迁移改造的游戏,将统一跳转连接对接至网关,在网关内进行统一跳转解析及协议转换,走新的鉴权逻辑。

其他

完整调用一次,耗时估算:

  1. 原平台化调用一次2~3ms;
  2. 数据类型 JSON 与 PB 互转 共约 2ms
  3. 网关+DSL约2~3ms(其中包含模型转换,和 2~4次 DSL 执行);

合计就在 6~8 ms

相关资料