2013年07月02日 Tue

关于"请求-应答"式协议在游戏上的应用

前言

这篇水文只是我对过去工作中某个问题的复盘和总结,描述了“请求-应答”式协议的另类用法。 尽管本文并不是吹 RPC 实现,但原理与其类似,比如用 {方法标识 + 参数数据} 构造 TCP 字节流。 甚至可以有更多花样,比如用 UID + 上下文标识就能跟踪请求栈,但这超出了本文讨论范围。

正文

本文描述的应用场景仅限于“请求-应答”式的数据交换方式下(如 HTTP)。 例如一个普通 web 应用的运作流程( 不要在意细节 ;b )

  1. Client 向 Server 发出请求;
  2. Server 收到请求,处理并发回应答;
  3. Client 收到应答,并反馈给用户;

普通 web 应用的运作过程就是不断地重复以上步骤。这有点像我们人与人之间对话的过程。 但奈何程序员能力有限,还无法让 Client 与 Server 如人一样自然对话。 双方需要约定 Client 和 Server 之间传递的各项数据的含义。

数据格式种类繁多,二进制如 MsgPack, Protobuf ,文本如 JSON, XML,可另起话题。

如此,项目一开始程序员们分分钟就约定好了数据的含义:“接口 A 返回数据 dataA,接口 B 返回数据 dataB……”。Client 和 Server 轻松快乐地进行着“你问我答”,程序员们便安心回家睡觉觉了,皆大欢喜……

意淫结束。游戏毕竟不同于 web 应用,Client 与 Server 之间存在频繁交互,大量的接口重复交换着有限的几坨数据。来来去去就是些玩家属性、装备、钱包、背包 blahblah…… 用 JSON 举个例子,数据来自商店购买接口:

{
    "_code_": 0,  # 操作码
    "coin": 1000, # 银币
    "gold": 1,    # 金币
    "income": [   # 获得收益
        "equip-10001-1",
        "equip-10002-1",
        "prop-20001-10"
    ]
}

有着类似数据约定的接口有很多,如副本结算、奖品兑换、PVP奖励、活动奖励等等。 不止如此,说不准哪天策划一拍脑门,还会多几种货币或物品类型。

现在面临的问题很清楚了,有两点:

  1. 数据约定难以复用。客户端需要在在不同的上下文处理同样的数据。
  2. 数据约定与 Server 接口绑死。一旦需求变化,又得重新约定。而因为问题 1 的存在,还会导致更多修改。

玩命领加班费很没意思,是时候加点设计。我给每个数据集合加入唯一标识(非绝对唯一,仅用于区分不同数据集合),Server 根据唯一标识提供数据,Client 则根据唯一标 识来解读。如此 Server 可以在一次响应里返回多个数据集合,而 Client 都能正确解读且只需实现一次。至此,这个另类的应用层协议便有了雏形。

仍然用 JSON 描述改良后的数据约定(不代表具体实现):

[
    {
        "_code_": 0          # 当前接口的返回状态
    },
    {
        "_msgid_": "money",  # 玩家钱包状态变化
        "coin": 1000,
        "gold": 1
    },
    {
        "_msgid_": "bag",    # 玩家背包状态变化
        "income": [
            "equip-10001-1",
            "equip-10002-1",
            "prop-20001-10"
        ]
    }
]

Client 只需根据 _msgid_ 绑定到不同的方法上即可,细节不表。现在处理服务端的响应就会容易得多,比如这样:

public void buyShopItems(string shopId) {
    GameResponse resp = this.gameserver.buyShopItem(shopId);
    if (!resp.isOK()){
        throw OperateFail(resp.Code);
    }
    this.saveStates(resp.msgs);  // 同步状态
    this.showNewItem(resp.msgs.bag)
    // do game blahblah……
    return;
}

public void saveStates(StateChange states[]) {
    for(StateChange s: states) {
        MsgHandler h = this.player.getHandler(s.msgid)
        h(changes) // 根据 _msgid_ 执行具体的方法
    }
}

如此这般,开发起来会顺手不少。

完。