背景
实现一个可以完成游戏端/Web端与业务服务之间的跨语言协议交互的网关,主要实现以下功能:
- 基于 Cocos 运行的游戏
- 基于 Unity 运行的游戏
- 基于 WebView 运行的游戏或业务
协议相关
协议调用方式
目前协议调用方式有以下六种情况:
- 只要请求,无需协议实现方回复
- 请求和回复协议使用相同协议名
- 请求协议与回复协议使用不同协议名
- 请求协议与回复协议使用不同协议名,且回复协议有多个,会根据处理结果选择其中一个协议进行回复,1vN
- 由协议实现方(例如长连接、RTC等持续服务)主动向游戏业务发发送消息
- 多场景共存情况,协议调用结果不止要回复给调用方,还需发送给其他关注方。例如:在 Unity 游戏中打开 H5 充值兑换页面,操作结束后,将充值兑换结果告知 H5 和 Unity 游戏刷新 UI 显示。
上述 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;
}
架构设计
旧架构(改造之前,当时的架构)
- River 目前有 Native Cocos/Unity 游戏,暂无 Web 游戏,统一走 River 协议框架,具有自己的原生能力实现,以接入二方游戏为主,无鉴权功能。
- Panda 目前包含的业务场景有:旧 Native Cocos 游戏、旧 Web Cocos 游戏以及原生 WebView,对于协议调用有一部分是游戏容器内自己实现并响应,还有一部分是通过 Panda 来中转响应。
- Open Platform 是为三方 Web 游戏提供支持方案,具体调用能力鉴权功能。具有自己的协议定义及原生能力实现。
- 若在上述 1、2 的 Native 游戏场景中,打开网页,则该网页是由原生 WebView 提供,其原生交互能力是由 Panda 提供。 目前协议框架较多且均有各自的协议定义和原生能力实现,其中还有部分重复实现的能力。其中大部分协议均是通过字符串硬编码的方式供业务方调用,业务方若使用该功能需要根据协议文档定义才能正确使用。 随着业务的迭代和发展,出现各协议框架提供的能力、交互方式不一致的情况,无法快速的支持业务迭代。
新架构
流程图
依据不同的协议定义,网关提供三个调用入口:
- 原协议 CMD 调用入口
- 原协议 URI 调用入口
- 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 模块
对协议调用进行鉴权、用户授权处理。
用户授权对可能需求,设计了四种方式:
- 多次触发协议授权,不会因授权中状态而拦截。因此会出现连续授权弹框情况。弹框呈现分:FIFO(先进先出) 和 LIFO (后进先出)两种方式。例如:苹果手机 App 申请权限弹框是 LIFO 方式展示,App 依次申请网络、通知权限,用户看到的过程是,先弹出网络授权弹框,接着立即又弹出通知权限弹框。此时展示的是通知权限弹框。当用户通知弹框授权后,就会看到网络弹框。
- 多次触发协议授权,只拦截同一协议授权中状态
- 多次触发协议授权,若任一协议授权中,则拦截
- 统一授权,类似微信小程序。进入游戏后,可预览游戏。提示用户需要授权可以使用完整功能,或当触发到相关功能时,提示需要授权才能使用完整功能。通过专门的授权协议一次性授权所有需要的相关协议。例如:我们的微信内推小程序
方式 1 流程图
方式 2 流程图
方式 3 流程图
Broker 模式
在 ALPC协议
中,第 5 种方式:由协议实现方(p长连接、RTC等持续服务)主动向游戏业务发发送消息。以及第 6 种跨场景接收消息。
该类消息是无源消息,协议实现方产生消息,向游戏业务场景发送,但此时实现方并不知道要将消息具体发给谁。
该 Broker 模块,就是为解决这种问题。它采用发布/订阅(publish/subscribe)模式。将消息转发相关的订阅者。
在 5、6 方式中,不会平白无故向游戏发送消息,必定会有一个触发该事件的事件源头。比如要监听 App 前后台切换状态,必须先要告知 Native 侧,需要对 App 前后台状态变化进行监听。要接收某长连接的消息,必须先创建或订阅该连接。
而对于协议实现方而言,发送消息,并不关心该消息是发给 Cocos 游戏还是 Unity 游戏。本身是独立协议功能,无需关心调用方业务。因此发送消息时,这里并不知道需要发给 Cocos 还是 Unity。但是有初始化该服务或业务的事件源。因此该 Broker 的基本时序图如下:
业务种类
由原生提供的常驻、持续服务(服务方),均会产生消息,向游戏业务场景(消费方)直接发送。且服务方和消费方是多对多的关系,因此需要一个 标识 可以关联双方关系。
对于一个通过协议对外提供的业务,其 Publish、Subscribe 协议均是预先制定好的。所以为简化使用方流程,对于 1Publish - 1Subscribe 的采用自动订阅方式;1Publish - 多Subscribe 的采用手动订阅;
其中 Subscribe 分: Subscribe Once 和 Subscribe Multi ;Once 触发一次后,自动取消订阅。Multi 可多次触发,需要主动取消订阅
综上所述,可以分以下种类:
- 从标识来源划分:
- 服务方本身业务参数已有业务唯一标识,该参数消费方(调用方)和服务方均在服务初始化前已知。
- 例如长连接的 IP:PORT 就可标识一条连接
- 服务方本身业务参数无标识;或消费方(调用方)和服务方均在服务初始化前没有已知的可以作为标识的参数。
- 该情况需要服务方来提供标识,因此在调用方在 Subscribe 前,需要再向服务方获取一次标识
- 从订阅方式划分:
- 1P-1S:1 发布对应 1 订阅,会在发送 Publish 协议的时候,网关会通过 DSL 自动提取相关信息,进行 Subscribe 的订阅。
- 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,以建立长连接示例:
种类 1-b 和 2-b 示例:
Payload 数据转换
在原协议定义中,协议参数均以 JSON 字符串格式与原生交互。在新的协议实现中,该实现方法是与 Frontier 中一致的,为 Protobuf 模型实例。因此需要进行数据结构转换。
在这个阶段中 Payload 协议参数转换流程,以 URI 协议为例:
注:在 Protobuf 模型实例与 JSON 字符串互转,Android 端 Protobuf 官方组件提供互转方法;iOS 端没有提供,需要自己开发。
容器多开
目前对于 Cocos、Unity 均是不可以多开的,不能同时打开两个 Cocos 或 Unity 游戏。单对于基于 WebView 运行的游戏及业务是可以打开多个 WebView 的,存在多开情况。
因此对于 WebView 容器多开需要进行处理,在收到 ALPCLXType == Web 的 APLC 消息时,需要找到具体响应它的 WebView 容器实例。
WebView 架构图: 由 WebService 来统一维护 WebView 实例,每个实例都对应生成一个唯一标识。在调用协议时,该标识赋值给 ALPCRequset 中 tag 字段。
WebView 交互时序图:
图中 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 相关的复杂能力:
- 弹幕:含有一条长连接
- 聊天气泡:含有两条长连接,IM 和 游戏
- 礼物展示:含有一条长连接
- 礼物面板
注:目前涉及 UI 的协议中,其 UI 在容器中展示的位置均是有协议实现方提供的位置。没有由调用方自定义 UI 位置。因此本次不增加 Layout 相关能力。若后续有相关需求,再增加相关功能即可。
游戏容器统一
在原容器内实现的相关协议迁移完后,就可将原来的同一引擎但有多个容器的情况,进行合并。每个引擎对应一个根容器。我们只需要与根容器进行交互。若业务方需要定制容器,只需要继承根容器,进行自定义即可。
关于打开 URL 或 统一跳转情况
打开 URL 协议
- 保留通过协议可以打开任一 Web URL
- 对于在打开的 WebView 中会与原生交互,并会产生数据,需要同步给其他容器或业务的情况。需将相关功能均 定义为具体协议 ,而是不是使用 通用的打开 URL 协议 。以确保逻辑的严密性。
- 对于打开的 WebView 内相关的业务均是自身使用,不会跨容器交换数据,此类可以使用 通用的打开 URL 协议 。 统一跳转
- 对于通过 suite 组件(类似对业务方提供的SDK)接入的业务,均不提供使用统一跳转调用能力。只能使用 suite 提供的功能。
- 在 App 内非通过 suite 的常规业务,和原先一致,具备完整的统一跳转能力。例如使用统一跳转启动三方游戏等。
- 对于旧的不迁移改造的游戏,将统一跳转连接对接至网关,在网关内进行统一跳转解析及协议转换,走新的鉴权逻辑。
其他
完整调用一次,耗时估算:
- 原平台化调用一次2~3ms;
- 数据类型 JSON 与 PB 互转 共约 2ms
- 网关+DSL约2~3ms(其中包含模型转换,和 2~4次 DSL 执行);
合计就在 6~8 ms