跳到主要内容

GM 平台

世游 GM 平台是负责和游戏后端进行数据交互的中间系统,面向世游的各个发行运营系统提供统一的游戏数据接口。

系统架构

img

从上图可见,世游各个直接面向运营、客服和玩家的系统,不直接和游戏后端进行数据交互,而是统一对接世游 GM 平台。

图中的 Game GM,是每个游戏项目的研发方负责实现的服务。

世游 GM 平台不对 Game GM 的技术实现方式做任何假设,只要符合通信协议要求,就可以接入。

当世游的发行运营系统需要访问游戏数据时,会通过世游 GM 平台向 Game GM 发送 GM 命令。

Game GM 负责执行 GM 命令,并返回命令的执行结果。如下图:

img

通信协议

世游 GM 平台和 Game GM 之间通过 HTTP + JSON 进行通信,请求和响应均为 JSON Object,且总是采用 UTF-8 编码。

世游 GM 服务总是使用 HTTP POST 请求 Game GM,且 Content-Type 总是 application/json

Game GM 响应的 Content-Type 应当总是 application/json

GM 请求

请求体的结构固定如下:

PropertyTypeDescription
versionstring[1,16]GM 请求的的版本号。当前版本固定为 2.0
request_idstring[1,64]本次 GM 请求的唯一 ID。用于日志记录、调试、问题排查。
idempotency_key string[1,64]本次 GM 请求的 Idempotency Key。如果有值则应当执行幂等处理逻辑。参见 幂等性
commandstring[1,64]GM 命令的标识, args 的结构随着 command 有区别。
argsobjectcommand 对应的 GM 命令参数,是一个异构的 JSON Object。

请求示例(获取角色列表):

{
"version": "2.0",
"request_id": "018f570c30b473138a450c5f836b3480",
"idempotency_key": "01905d83-ce30-73d6-8b6b-c62150276ee2",
"command": "ListRoles",
"args": {
    "combo_id": "1240321074980005",
    "server_id": 10001
}
}

成功响应

如果 Game GM 执行命令成功,则返回 HTTP 200。

此时在 response body 中返回一个 JSON Object,其结构和接入协议中 GM 命令的响应定义一致。

响应示例(获取角色列表):

{
"roles": [
{
"role_id": "845284226758233306",
"role_name": "洪文泽",
"level": 37,
"status": 1
},
{
"role_id": "844716741320391248",
"role_name": "擎天-豆腐",
"level": 5,
"status": 2
}
]
}

错误响应

如果 Game GM 执行命令失败,则返回非 200 的 HTTP status code。

此时在 response body 中返回 ErrorResponse 结构来提供错误信息:

PropertyTypeDescription
errorstringGame GM 返回的 错误类型
messagestringGame GM 返回的错误描述信息。
uncertainbool如果为 true,表示无法确定 GM 命令是否被执行。
不确定 GM 命令是否执行的情况

错误响应的默认语义是 GM 命令未被执行(否则应当返回成功响应)。

取决于游戏侧的后端实现,在特定场景下有可能会出现 Game GM 无法准确判定 GM 命令是否执行的情况,例如 Game GM 发送了 GM 命令给游戏服务器,但没有收到返回结果。

此时 Game GM 可以将错误响应中的 uncertain 显式设置为 true,用来明确指示这种情况。

发送方会根据业务场景的需求来决定是允许重发命令,还是记录错误走人工确认的流程。

响应示例:

{
"error": "invalid_args",
"message": "server_id 10001 does not exist."
}

响应示例(不确定是否执行):

{
"error": "timeout_error",
"message": "Unable to get response from game server.",
"uncertain": true
}
警告

Game GM 执行 GM 命令时,无论命令执行是否成功,都需要按照通信协议返回响应,不应当出现无响应或者空响应体的情况。

世游 GM 平台预期 Game GM 在 10 秒内返回响应,否则按请求超时对待。

错误类型

游戏研发方应当优先使用以下预定义的 error 类型。

如果这些错误类型不能满足需求,也可以自定义错误类型。自定义错误类型时,请遵循相同 snake_case 风格。

提示

Combo SDK for GoCombo SDK for Node.js 中已经内置了这些错误类型。

invalid_http_method

请求中的 HTTP method 不正确,没有按照预期使用 POST。

HTTP status code: 405

invalid_content_type

请求中的 Content-Type 不是 application/json

HTTP status code: 415

invalid_signature

对 HTTP 请求的签名验证不通过。这意味着 HTTP 请求不可信。

HTTP status code: 401

invalid_request

请求的结构不正确。例如,缺少必要的字段,或字段类型不正确。

HTTP status code: 400

invalid_command

游戏侧不认识请求中的 GM 命令。

HTTP status code: 400

invalid_args

GM 命令的参数不正确。例如,参数缺少必要的字段,或参数的字段类型不正确。

HTTP status code: 400

throttling_error

GM 命令发送频率过高,被游戏侧限流,命令未被处理。

HTTP status code: 429

idempotency_conflict

幂等处理重试请求时,但 idempotency_key 所对应的原始请求尚未处理完毕。

参见 幂等性

HTTP status code: 409

idempotency_mismatch

幂等处理重试请求时,请求内容和 idempotency_key 所对应的原始请求内容不一致。

参见 幂等性

HTTP status code: 422

maintenance_error

游戏当前处于停服维护状态,无法处理收到的 GM 命令。

HTTP status code: 503

network_error

网络通信错误,导致 GM 命令执行失败。

HTTP status code: 500

database_error

数据库访问异常,导致 GM 命令执行失败。

HTTP status code: 500

timeout_error

GM 命令处理超时。

HTTP status code: 500

internal_error

处理 GM 命令时内部出错。可作为兜底的通用错误类型。

HTTP status code: 500

通信安全

世游 GM 平台到 Game GM 的 HTTP 调用是典型的 B2B 数据通信,通信两端均有高度的确定性。

我们从以下方面确保通信的安全性。

网络层

Game GM 的安全组(防火墙)中 443 端口仅对世游 GM 平台所在 VPC 的出口 IP 开放。

提示

这部分由世游运维团队负责配置。

传输层

世游 GM 平台和 Game GM 之间总是使用 HTTPS 进行加密通信,且 TLS >= 1.2。

提示

这部分由世游运维团队负责配置。

应用层

Game GM 需要对 HTTP 请求做签名验证。签名密钥沿用游戏的 Secret Key,签名方式沿用 REST API服务端通知 使用的 签名算法

签名验证功能在 Combo SDK for GoCombo SDK for Node.js 中已经内置。游戏研发方也可以选择请参照签名算法的文档和 SDK 的代码自行实现。

提示

这部分由游戏研发方负责实现。

接入协议

通信交互时发送的 GM 命令,以及请求、响应的数据格式,通过接入协议文件进行描述。

  • 世游业务需求方、世游技术团队、游戏研发方针对业务需求进行讨论与评估,确定需求的合理性与可行性。
  • 世游技术团队负责基于业务需求编写接入协议文件,游戏研发方负责实现协议文件中定义的 GM 命令。
  • 协议文件会提供给游戏研发方,用于在实现 GM 命令的开发过程中进行参考。
  • 协议文件的内容,根据业务需求的增加不断迭代,重复上述过程。
信息

世游技术团队负责编写接入协议文件。

游戏研发方需要注意,一定不能单方面修改接入协议。

协议示例

GM 协议文件的示例如下:

syntax = "proto3";
package demo;

import "gm.proto";

service Demo {
rpc ListRoles(ListRolesRequest) returns (ListRolesResponse) {
option (combo.cmd_name) = "获取角色列表";
option (combo.cmd_desc) = "获取 Combo ID 在指定区服下的游戏角色列表";
}
rpc SendRoleMail(SendRoleMailRequest) returns (SendRoleMailResponse) {
option (combo.cmd_name) = "发送角色邮件";
option (combo.cmd_desc) = "向单个角色发送邮件,邮件可携带附件。";
}
}

enum RoleStatus {
option (combo.enum_name) = "角色状态";

UNKNOWN = 0 [(combo.value_name) = "未知"];
ONLINE = 1 [(combo.value_name) = "在线"];
OFFLINE = 2 [(combo.value_name) = "离线"];
}

message Role {
string role_id = 1 [(combo.field_name) = "角色 ID"];
string role_name = 2 [(combo.field_name) = "角色名称"];
int32 level = 3 [(combo.field_name) = "角色等级"];
RoleStatus status = 4 [(combo.field_name) = "角色状态"];
}

message MailAttachment {
string item_id = 1 [(combo.field_name) = "道具 ID"];
int32 item_num = 2 [(combo.field_name) = "道具数量"];
}

message ListRolesRequest {
string combo_id = 1 [(combo.field_name) = "Combo ID", (combo.required) = true];
int32 server_id = 2 [(combo.field_name) = "区服 ID", (combo.field_desc) = "游戏服务器的唯一 ID", (combo.required) = true];
}

message ListRolesResponse {
repeated Role roles = 1 [(combo.field_name) = "角色列表"];
}

message SendRoleMailRequest {
int32 server_id = 1 [(combo.field_name) = "服务器 ID"];
string role_id = 2 [(combo.field_name) = "角色 ID"];
string sender = 3 [(combo.field_name) = "发件人"];
string title = 4 [(combo.field_name) = "邮件标题"];
string content = 5 [(combo.field_name) = "邮件内容"];
int32 ttl = 6 [(combo.field_name) = "邮件有效期", (combo.field_desc) = "单位为秒,到期后删除邮件。0 表示永久有效。"];
repeated MailAttachment attachments = 7 [(combo.field_name) = "邮件附件列表", (combo.field_desc) = "可包含 0-5 个附件。"];
}

协议格式

从示例中可以看出,GM 接入协议采用了 gRPC 的 Protobuf IDL 对 GM 命令进行描述,每个游戏一个 .proto 协议文件。

每个 Unary RPC 对应一个 GM 命令,GM 命令的请求和响应的数据结构,分别对应 rpc 请求和响应的 message 定义。

Protobuf 的数据结构是编程语言中立的,和 JSON 存在确定性的映射关系

游戏研发方应当对照 .proto 文件编写 Game GM 的代码实现,也可以使用 Protobuf Compiler (protoc).proto 文件生成代码,然后使用生成出的代码中的数据结构,从而简化代码编写,降低在数据结构和序列化上出错的可能性。

协议文件通过 Protobuf 的 Custom Options 功能,为 GM 命令添加了扩展元数据。这些元数据会被 GM 平台使用,也变相起到注释的作用,供游戏研发方参考。

提示

如果游戏研发方使用 GM 接入协议文件做 protoc 代码生成,需要下载 gm.proto 一起生成代码。

协议说明

为了保持系统的简单性,也便于游戏研发方实现,以下 Protobuf 的高级特性被有意裁减掉。GM 接入协议中不会包含它们。

  • map<K, V>
  • oneof
  • google.protobuf.Any

枚举 (enum) 类型在 JSON 序列化时,使用的是枚举定义中的整数值,而非枚举值的字符串名称。

Game GM 需要按照整数值来处理请求和响应中的 enum。


在 JSON 标准中,number 类型是 IEEE 754 的双精度浮点数 (double),无法完整表示 64 位的大整数。

世游 GM 平台在对 64 位大整数做 JSON 序列化时,默认使用 number 类型,但如果整数值超出 double 的表示范围,则会使用 string 类型。

Game GM 需要能够正确处理这种情况。


协议中的时间字段,统一使用 int64 类型,取值为 Unix 秒级时间戳,没有时区、本地时间的概念。


查询类的 GM 命令,如果返回的数据条数非固定,有可能返回较多的数据,则需要做分页处理。

GM 接入协议中会为请求和响应定义分页所需的数据字段。

幂等性

背景

有些 GM 命令会导致游戏内重要的数据变化,且相同请求的多次执行会导致多次数据变化。例如:

  • 给游戏角色增加游戏币。
  • 发送全服邮件,邮件中携带奖励。

如果世游侧的运营系统通过 GM 平台向 Game GM 发送此类命令后,出现了网络连接中断、请求超时、程序崩溃等情况,导致没有接收到来自 Game GM 的响应,发送方无法区分以下两种异常情况:

  1. Game GM 并未收到 GM 请求,所以命令没有被执行。
  2. Game GM 收到了 GM 请求并执行了 GM 命令,但返回响应的时候出现了异常,导致 GM 平台没有收到响应。

此时发送方就会陷入两难境地:

  • 如果无视异常,不重试发送 GM 请求,但遇到的是情况 1,则此命令从来就没有被执行过(少了)。
  • 如果重试发送命令,但遇到的是情况 2,则此命令会被执行多于一次(多了)。

为了解决此类问题,我们需要某种机制来保证幂等性,使得发送方能够安全地重试发送 GM 请求,且每个 GM 请求在游戏侧最多执行一次。

方案

世游 GM 平台参考 IETF 的标准化草案 The Idempotency-Key HTTP Header Field 中描述的方案进行幂等性处理。

GM 请求体中的 idempotency_key 字段用于幂等处理。

idempotency_key 字段是 string 类型,由世游侧发送 GM 命令的系统负责生成,生成算法为 UUID v7

Game GM 收到 GM 请求后,如果发现 idempotency_key 有值,则应当执行幂等处理逻辑。分为两种情况:

首次请求

Game GM 没见过本次请求的 idempotency_key

处理流程如下:

  1. 计算出 GM 请求的指纹 F。建议对请求体中的 command + args 计算校验和,可以采用 SHA256、MD5 等哈希算法。
  2. 保存 idempotency_key 和步骤 1 中计算出的指纹 F 保存至数据存储(例如 Redis),并记录状态 S 为 "处理中"。
  3. 处理 GM 请求中的 GM 命令,例如转发给对应的游戏服务器进行处理,或直接在 Game GM 进行处理。
  4. GM 命令处理完毕,得到结果 R。这里的结果可能是成功响应,也可能是错误响应。
  5. 将步骤 4 中得到的结果 R 更新至数据存储中 idempotency_key 对应的记录,并更新状态 S 为 "处理完毕"。
  6. 返回结果 R 给 GM 平台。

重复请求

Game GM 已经见过本次请求的 idempotency_key

处理流程如下:

  1. 从数据存储中取得 idempotency_key 对应的原始请求的状态 S、指纹 F、结果 R。
  2. 如果状态 S 为 "处理中",中止处理流程并返回错误 idempotency_conflict
  3. 计算出本次 GM 请求的指纹 F1,和原始请求的指纹 F 对比,如果指纹不匹配,中止处理流程并返回错误 idempotency_mismatch
  4. 如果状态 S 为 "处理完毕" 且指纹 F 匹配,则返回原始请求的结果 R 给 GM 平台。

额外说明

有效期

游戏侧不需要永久存储 idempotency_key 及其关联的状态数据。

对于每个 idempotency_key,游戏侧需要保证有效期 (TTL) 不低于 24 小时。

世游侧的运营系统会:

  1. 没有接收到来自 Game GM 的响应时,使用同一个 idempotency_key 进行重试。
  2. 如果 24 小时内的重试都无法获得一个明确的响应,则停止重试并记录错误。
数据存储

建议游戏侧使用 Redis 作为 Idempotency Key 的数据存储:

  • Redis 中的存储状态可在多个 Game GM 实例之间共享,提高 Game GM 的可用性。
  • Redis 的 TTL 机制,便于实现 Idempotency Key 的到期自动清理。
  • Redis 的 SET key value NX GET EX seconds 可以简洁地解决 Idempotency Key 读写的并发问题 (Redis >= 7.0.0)。
无需幂等处理的情况

并非所有的 GM 命令都需要做幂等处理。

有些 GM 命令是天然幂等的,例如查询类的命令、封禁游戏角色等等。

在发送这些命令时,请求体中的 idempotency_key 不会有值,Game GM 也无需对它们进行幂等处理。

参考资料

关于上述幂等处理的方案,可以进一步阅读以下链接。

这些链接中包括详细方案,技术讨论,以及各种流程图、时序图,本文档就不再赘述了。

测试与验收

游戏研发方可以登录 世游发行平台 Console,在 GM 平台 功能中:

  • 开发者 -> 协议管理:查看并下载 GM 接入协议文件。
  • 开发者 -> 命令调试:向 Game GM 发送 GM 命令,自测功能的正确性。

Game GM 要能够支持高并发。游戏侧在功能测试通过后,还需要对 Game GM 进行性能测试,并提供测试报告。

后续世游发行平台 Console 中会增加性能测试的功能。

信息

世游侧会对 Game GM 的功能进行验收。

验收通过才能发布。验收不合格需要打回重新开发。

发货命令的重点说明

涉及到游戏内发放物品(货币、道具)性质的 GM 命令,是世游侧和游戏侧都需要重点关注的。

此类命令处理不当容易引起线上事故,也容易引起玩家的客诉。

对发货命令的处理,必须满足以下要求。

实时性

发货功能发生时,游戏侧必须做到实时到帐:

  1. 道具、货币等游戏内物品,到帐的最大延时不应超过 30 秒。
  2. 在游戏中有明确提示。例如新邮件、新道具的提示,使得玩家能够立即感知到。
  3. 已经在线的玩家,需要保持在线,不能被踢下线。
  4. 已经在线的玩家,无需重新登录客户端,便能看到最新的数据。

幂等性

游戏侧必须确保发货功能是幂等的,即每个发货的 GM 请求最多向玩家发放一次。

具体机制请参见上文的 幂等性 部分。