跳到主要内容
SDK 之外

本页尚未翻译,当前显示英文版本。

深入底层:传输与 hub

这是一份架构参考,不是你要调用的接口。本页内容都不会出现在你编程所用的 API 里:没有需要你管理的 socket、通道、信封(envelope)或重试。核心概念列出了这套技术栈的各层名称;具体机制只在这里讲,方便架构师核查 SDK 如何处理断开的连接、被禁用的模块,以及必须恰好送达一次的消息。

分层结构

user spacethe SDK's own businessyour codemodules: rooms, matchmaking, inventory, leaderboards…primitives: events · RPC · data · groupsthe hub: DI and mounting · the user session · state recovery · message QoS · routingtransport adapters: WebSocket · PlayServ UDP · HTTP
分层结构:线上是你的代码和模块,线下是原语、hub 和传输层

自上而下共五层:用户空间、模块、原语、hub,以及 hub 之下的传输适配器。最上面两层属于用户空间,以下各层都是 SDK 自己的事。

  • 用户空间就是你的代码。它能看到模块,它接触的概念也就到此为止。
  • 模块(module)是应用层:房间、匹配、背包、排行榜。它们构成一张图,而不是一棵树;其中某个模块被关闭时,hub 要解析的正是这张图(这一结构本身见继承与组合)。
  • 原语(primitive)是其他一切所依赖的第一层实现:数据、事件、RPC、群组。模块就是一组具名的原语组合,再加上它自己的规则。
  • hub 是控制器:负责依赖注入、模块挂载、用户会话(session)、状态恢复、消息服务质量,并把每条入站消息路由到为它挂载的模块。
  • 传输层是面向某种协议的适配器。传输有好几种,hub 一视同仁。

传输只是适配器

传输不止一种,它们之间的差异如果不加隔离,就会渗透到每个模块里:

维度取值范围
形态消息驱动或请求驱动
通道单通道或多通道
状态支持或不支持连接状态恢复
协议TCP 或 UDP

目前有 WebSocket、PlayServ UDP 传输和普通 HTTP 三种。每种都是一个适配器,实现细节各自封装,向上暴露的都是同一样东西:传输会话。hub 持有的是会话,从不是 socket,所以适配器之上没有任何一层需要考虑网络接口。

同一时间只有一个传输会话

同一时间只有一个传输通道和一个传输会话。一边用主客户端(master-client)传输承载实时会话,一边为排行榜另开一条后端传输,这不在支持范围内,也没有任何 API 签名依赖于同时打开多个通道。在后续版本另有说明之前,单会话就是契约。

hub 把传输完全隐藏起来

hub 向下使用传输接口,向上提供状态、事件和用户会话。无论模块代码还是游戏代码,都无从知道底层是哪种传输、hub 如何批量合并一次调用,或者连接中断后它做了什么来恢复到一致状态。

  • 用户会话属于 hub,而不属于某个模块。重连、恢复会话和状态恢复只在 hub 上发生一次,覆盖挂载在它上面的所有模块。
  • 消息 QoS 属于 hub,而不属于数据模块。信封、重试和打包都是 hub 的机制。

消息 QoS:三个级别

模块只需声明自己需要的投递保证:

级别含义
at least once在收到确认前反复投递;接收方需要容忍重复
at most once只发送一次,从不重试;可以接受丢失
exactly once去重并确认;开销最大,只在确实需要时使用

如何兑现这一保证是 hub 的事,与模块无关,也与你无关。

依赖注入、挂载与被禁用的模块

hub 实例化模块,并把它们挂载到根节点或某个命名空间下,同时解析每个模块对原语和其他模块的依赖。构建用不到的模块就不挂载。挂载按命名空间划分;如果第二个模块要占用已被占用的挂载点(mount point),挂载时就会被拒绝:组合在这一步失败,而不是等到第一次调用时才失败。

由于模块构成一张图,关闭其中一个会影响下游,hub 会在两条路径中选且只选一条:

  1. 禁用依赖链。所有依赖缺失模块的模块也一并关闭,它们的接口直接不存在,而不是调用时报错。
  2. 声明功能降级。依赖方保持挂载,并声明自己不再能做哪些事。

没有第三条路:已挂载的模块绝不会悄悄丢弃自己已无法执行的操作,所以被禁用的依赖总是可以观察到的。

线上保留什么

模块页面上的每一项承诺都在这条线之上兑现:修改实体(entity)就是网络操作,hook 是类型化函数,加入房间只需一次调用。你可能会听到这套技术栈各层的名称(核心概念就指向本页),但线下没有任何一层是你的代码要调用的。这些层的存在,就是为了在更换传输时,线上的承诺依然成立。

你确实会碰到的内容,包括处理器运行所在的投递上下文、句柄(handle)何时结束,以及测试时使用的内存实现,都在上一层的页面里:线程、生命周期与测试。