« skynet 的一个简单范例 | 返回首页 | 一元购庄家如何作弊 »

如何只基于请求回应模式实现 MMO 级别的场景服务

在上一篇 blog 里,我谈到游戏服务器其实只需要使用 req/resp 模式就够了。有同学表示不太理解,认为服务器主动推送,或者说 pub/sub 的消息模式必不可少。

在聊天中我解释了很多,是时候记录一下了。

从本质上来说,如果你只是想把一系列消息发送到客户端,req/resp 请求回应模式和 pub/sub 发布订阅模式并没有什么不同。你可以把 req/resp 理解成一次性的,只订阅一条(或有限条)消息;如果需要维持这类消息的推送,需要客户端在收到消息后再次发起订阅。

这和 timer 定时器系统一样,订阅一个定期触发的定时器这种功能并非必要。如果你需要一个每分钟触发一次的定时器,完全可以在触发一次定时操作后,立刻发起下一次定时任务请求。它在功能上是等价的。

潜在的实时性影响是:如果规定必须在收到回应消息后,才允许发起下一次请求;那么一个事件发生后推送到客户端的延迟可能增加了最多 2 倍。即,如果你刚刚回应了一条消息,这个时候又发生了新的事件;客户端需要在收到前一个事件后再提起新请求,然后你才可以把后发生的事件通过新请求的回应包发过去。

降低这个实时性影响的方法也很简单,协议上允许在客户端发起请求后未收到回应之前再次发起请求即可。对应同类消息许多少并发的请求,要根据这类消息的实时性要求有多高来决定。无限提高允许的并发量没有太大意义,毕竟物理上的延迟不可能完全消除。

从客户端来看,MMO 这样的游戏,客户端仅仅是一个呈现设备,它不断的接收到服务器发过来的游戏世界的状态变化,而做出表现。客户端主要靠回应包来分发业务逻辑,使用请求回应模式,无非是根据回应包里的 session 号匹配之前请求包(通常包的类型在请求包中,不必放在回应包数据里),将两者(请求/回应)合并起来调用消息分发函数而已。

既然效果一样,请求回应模式有更大的数据冗余(一些看起来没有意义的请求包,在订阅模式中,你可能只需要订阅一次)那么请求回应模式带来的好处是什么呢?

  1. 它天然可以解决过载问题。如果服务器不考虑客户端的带宽/硬件处理能力而推送数据,客户端很可能无法及时处理,而无谓的浪费了双方的带宽。而请求/回应模式中,如果我还没处理完,就不会要更多,很自然的回避了问题。

  2. 有选择性的发送消息这件事,对服务器来说,要么是一个时间复杂度为 O(n2) 的操作:你需要对每个用户遍历对比其他用户来决定这条消息对方是否关心;要么,会把数据结构的复杂度提高一个数量级:你需要把对象进行各种分类、索引,以提高检索的效率。而如果把选择我关心的消息的权利放在客户端,那么时间复杂度就只有 O(n) 了。客户端总能更了解自己的情况。我是 iphone6s ,所以我可以关心我周围 50 个人的变化;你用个两年前的红米,屏蔽了周围所有人,服务器就不要把我旁边的人跳了个舞的事件发给我了。

  3. 消息优先级更好处理。我正在战斗,那些在世界聊天频道对撕的消息晚点给我,不要耽误了我的对手正在搓火球这种我更关心的事件。

  4. 无论是客户端还是服务器,只要想清楚,业务逻辑可以写得更清晰。怎么做呢,下面展开讲。


在 MMO 的场景服务中,只使用请求回应模式该怎么做?

首先把场景中的玩家、NPC 等都抽象成一样的对象。同时将场景打上合适大小网格(比如一格 100 米为边长,这取决于你允许的玩家视野),将每个格子也看成一个对象。

每个对象都带有一个(或多个)事件队列。

对于格子对象来说,事件只记录有别的对象来到这个格子,以及一个对象离开格子。

玩家对象的事件队列则记录这个对象自己做了什么动作,收到了什么伤害,增加或减少了什么 buf 等等。

你的场景应该是以某个心跳周期(通常不会超过 10Hz)来工作的,也就是游戏服务器上,所有对象的变化都是离散的。

所有的攻击、移动、技能都会以事件的方式,加上时间戳放到所属对象的事件队列中。

客户端的查询协议中的来说只有两种:

  1. 查询一个对象的当前状态以及该对象最新事件的时间戳。

  2. 查询一个对象从某个时间戳开始,以后发生的所有事件列表。

如果客户端还不了解一个对象时,它发起查询 1 ;如果已经了解了这个对象,发起查询 2 。或者根据需要发起查询 1 (例如曾经查询过,但很久都不关心了)

在查询没有回应前,限制再次提起查询的次数。

这样总能保持对某个对象最新状态的了解。

当一个玩家进入场景,他要做的:

  1. 查询进入点。

  2. 根据进入点坐标,查询所在格和周围格的发起查询。

  3. 根据周围格的查询,获知了周围存在的对象。然后分别对这些对象做进一步查询。作为 MMO 可能存在大量的对象,通常筛选一部分就够了。

  4. 一旦感兴趣的对象走远,就不再监视它;这点不必依赖格子对象的状态变化。所以格子的状态反馈只需要有新增对象即可,而不必有对象离开的消息。所谓监视一个对象,指在每次查询回应后,再次提起下一次查询。有些对象,比如自己,需要响应更及时。应该同时向服务器发起多个查询请求,这样,服务器一旦判定你被攻击或被某些事件改变,总能有可用的回应 session 供发送回应包。同理,如果你在和人/ NPC 战斗,那么对手也是重点关注对象。而周围人跑来跑去、做了个动作,可能你就不那么关心了。这个可以由客户端来控制哪些该保持追踪、哪些暂时不必理会。

  5. 最后关注自己(在场景中那个对象)。这样如果有人攻击你,你就能知道这个事件的发生。在攻击发生后,还可以立刻去关注打你的这个人(如果之前没有关注的话)。


可以做,且容易的做的优化有:

  1. 事件合并。当用户请求某个时间戳之后的所有事件时,有些事件是可以合并的,比如多次移动可以合并成一个;格子里一个对象进进出出,可以合并掉,并只回应新增事件。

  2. 回应包缓存。把事件列表序列化为网络包有一定的开销。有些信息肯定会有很多玩家来索要,你可以只做一次序列化工作,把数据缓存起来,等多个人来请求时,直接回应给他,省去了将信息反复序列化的流程。

  3. 多查询合并。查询多个格子的时候,可以合并在同一个请求中,这样,对象在临近格子间的移动消息可能就被合并掉了。

从协议层,可以允许一次提起对很多对象的查询请求,它们的查询 session 一定是连续的,所以不必逐个编码而只需要给个区间就够了;同时也不妨碍单独回应。在上面的查询约定中,如果发现时间戳之后到当下,对象事件列表中若没有事件,服务器是不回应的。

例如,如果你来到一个无人区,对所在格做了一次查询,除了你自己外,没有其它对象;你若再次查询,结果不会有变化,所以服务器会挂起你的请求,直到附近有新对象出现。


由于请求和回应是一一对应的,所以不用担心比单纯的数据推送(订阅模式)多出太多不必要的流量。

Comments

云风你居然用iphone而不用我大华为😦

没做个游戏不懂,请教个问题。上面说道“同时将场景打上合适大小网格(比如一格 100 米为边长,这取决于你允许的玩家视野)”这里格子的大小刚好包住玩家的视野对吗,这样可以通过九宫格的方式查询玩家视野内所有的可见对象了?

云风你怎么能鄙视我的红米

这个模式有一定优点,但是并不能完全代替pub/sub模式,原因很简单,IO的消耗可能会增加很多,也不能使用boardcast机制,
对于上面提到的好处,完全可以通过增加数据结构来完成。大多数情况下我更倾向于后台来判定,而不是前台

每个对象都绑定一个事件队列,有意思的做法,受教了。关于“多查询合并”有点疑问,玩家的移动在玩家事件队列里面,不需要通过多查询合并。另外“回应包缓存”已经把每个单位的事件系列化了,“多查询合并”不是不能利用这个缓存?

如果你可以很好的用其它模式解决,也可以不用,云风给大家介绍了一种可以解决问题的方法,比如前面的人做moba游戏,我个人觉得,你可以不用这个,选择适合自己的

那就不要使用这个模式

這模式不錯阿 尤其打野王時 可以依照玩家的硬體狀態與網路 選擇合適的請求 例如硬體夠好 網路延遲低 選擇呈現全部玩家的動作 硬體低階 網路延遲高 只顯示王的 其他玩家用黑煙遮蔽

对于高频流式的数据,例如玩家的位置变化,使用请求响应模式,还是不太适合,请求响应难免会增大延迟,如果使用多个请求并行,客户端就需要去管理这些请求状态,难免增加客户端的复杂度;并且对于位置信息来讲,一般只关心最新的数据,之前的session不可靠也是没有关系的;请求响应模式,还是比较适合低频,可靠通信

好像大家没太理解云风这么做的意图。他的上一篇应该是在短连接那篇文章。
req/reps方式很方便做断线重连,而不会出现服务端推送消息此时连接断开而丢失的情况,因为一旦丢失,只要客户端将会话id没有响应的重新发送就可以了,比较适合哪种请求响应式居多,UI类的游戏。
不过我依旧觉得这种方式对于mmo可能用处不是很大,毕竟客户端要预发一部分session给服务端用,如果服务端session不够用了,此时要开个队列存储,等待客户端发回足够的session id,造成本不应当出现的延迟。
对于这种游戏断线还是直接重新登录方便。

感觉把没有必要的复杂度引进来了,但是好处没有这么明显。client端带宽是问题吗?从来只听说渲染有问题

对于查询模式,无返回,导致一种不可信任的状态,
需要考虑,延时过高? 服务出错? 忽略的请求?
多数场景,似乎会把简单问题复杂化

long polling 模型不适合用协程实现,因为它总是等事件发生才回应,而不是依赖在收到请求时的上下文。

所以你在实现 long polling 时就应该回避使用额外的协程。

例如在 skynet 里是基于 skynet.response 这个 api 完成的。

请求回应模型的服务端框架往往会为每个请求分配一个线程或者协程。

如果用这样的框架来模拟订阅模型,会额外浪费很多内存。

开了眼界,格子对象的事件列表是个好注意,客户端自定义负载很好。
有点疑问,如果是moba这种对同步要求非常高的,每秒10次的同步,用这种方法就无谓的多了10次请求包啊

一看到网格,技能 就 想起了大学时候做的游戏 :)

沙发

Post a comment

非这个主题相关的留言请到:留言本