原神,启动!开放大世界登录系统设计

本文最后更新于1 年前,文中所描述的信息可能已发生改变。

登录系统是基础性系统,和上层逻辑不一样,可以考虑消息驱动。

热身阶段

将游戏服务器结构分为上层逻辑层和基础组件层。不同游戏的区别在于逻辑层,玩法的不同。基础组件层,例如登录、DB 都可以实现为通用的。

随着游戏服务端的逻辑层越来越复杂,逻辑层也开始了分层。越来越复杂,首先得感谢外挂。国内的大部分游戏,对于使用外挂的玩家,处于放养的状态,基本上是不管的,如果逮到一个就封号,也不现实,假如一半的玩家都在用外挂,那么我把一半的玩家都封了,我的游戏也没办法运营了。因此国内的游戏服务端跟国外的服务端有一个重大的区别就是,国内的游戏的逻辑是跑在服务器上的,所有牵扯到玩家校验、财产的逻辑都是放在服务器上的。

分层后,逻辑层增加了不同的场景服务器,目的是承载上万用户同时在线。服务端不像客户端,客户端就是玩的那个用户作为它的主要用户,客户端可以看到其他用户,但不会对它实际操作,而服务器是上万人都要操作。这是服务器相对于客户端的一个挑战。和登录相关的就是我们会拆非常多的进程,这些进程根据功能分为不同模块,有接入模块、逻辑模块、存储模块。这就导致了一个听起来非常简单的登录系统变为了一个大规模的分布式系统。

什么是登录

玩家通过客户端与服务器建立连接,在服务器各个进程上完成需要的账号和角色数据创建,并进入游戏场景开始游戏的过程。

准备工作

  • 实现接入服务器
  • 实现 DB 存取
  • 制定协议

登录的基本流程

  • 建立/断开连接
  • 用户登入/登出
  • 注册/注销/修改角色
  • 进入/离开场景(真的开始玩游戏了)

建立/断开连接面向的主体是客户端和服务器,类似网络七层协议,而用户登入/登出面向的是用户。

简单的登录流程

简单的系统无法兼容复杂的系统,但是复杂的系统可以兼容简单的系统。当然,我们一开始做的时候肯定是从简单到复杂。

状态机
  • 分布式
  • 消息驱动
  • 多层状态机

首先发起登录请求,然后从 DB 中将玩家数据 load 出来,进入一个 Login 状态机。玩家首先进入一个 Free 状态,从玩家的角度直观感受就是进入了选择角色的界面或者选择区服的界面,还没有进入场景。假如玩家就看一眼,就退出了,那么就清除它的上下文,退出状态机。如果继续玩,玩家就会进入了一个 Scene 的状态,进入这个状态后,玩家开始从 World 往 Scene 进程进,但是这就产生了一个异步的过程,我往 Scene 发消息,因为 Scene 是并发的,所以它不会阻塞式的立即回复发送的消息,它可能会把消息放到它处理的缓存池里,等它有能力处理这个消息的时候,才会返回结果。这就是为什么我们需要状态机,我们等消息来驱动,而不是客户端主动去轮询,这样的话服务器的负载就太高了。为什么 Scene 不能立即响应?假设 Scene 承载了某个场景,这个场景有多个地图,玩家进入这个场景后,需要创建这个 Scene 中地图的多个实例等数据,这个数据可能会非常大,因此我们不可能在玩家没有进入位面的时候,提前把所有玩家可能进入的位面都创建好,这样服务器完全扛不住,所以实际情况是,只有玩家实际进入某个位面的时候,我们才根据我们的负载均衡策略选择一个负载比较低的 Scene,来创建进入这个 Scene 需要的数据,创建这些数据是需要时间的,我们上层的状态机就需要等待 Scene 来创建这些数据的过程,即进入 Scene 状态。进入成功后,会进入一个 Game 状态,玩家开始在场景里玩游戏了,这个时候跟上层逻辑,也就是基础性系统的交互已经结束了,状态机就会一直处于 Game 状态。玩家不想玩了,下线后,客户端会发一个 Leave Scene 请求,告诉服务器,服务器处理完退出逻辑后,进入 Free 状态,因为玩家有可能想换个角色,进入了选角的界面,并没有真正的退出。真正的退出游戏后,状态机销毁,上下文销毁。

可以看出,当我们搭建完一个登录系统后,一个最最简单的游戏服务器的框架就完成了。这个时候就是往 Game 状态下装游戏的玩法逻辑了。

状态机是放在引擎层的,与玩法层隔离。假设我们把所有 Game 状态下的玩法逻辑都放到引擎层,或者说在 Scene 状态的时候,初始化一些玩家数据。那么不同项目要复用就不可能了。如果通过消息机制,那么我这套游戏的逻辑可以被其他游戏复用,不需要改太多逻辑。

说了这么多,重点其实就是,我是一个状态机,有各种各样五花八门的消息来驱动我,我每个状态下可能还有各种分状态机。

功能角度

现代计算机,用穷举法破解你的口令可能会是一件很轻松的事。从开发者的角度来做设计口令这件事,应该考虑如下规则。

  • 限制玩家输入简单的口令。可以像 twitter 一样做一个口令的黑名单,也可以做一些简单的限制,例如是否有大小写字母、数字、符号等。这可能会让你的用户很不爽,所以现在很多登录都提供了 UX 让用户知道他的口令强度是怎么样的,目的就一个,告诉玩家,你的口令给我整复杂一点。 有趣的设计

在便池上放一个假苍蝇会导致男人撒尿的时候会不由自主地瞄准它,有证据表明,这样的用户体验可以减少80%的小便溅出便池。

  • DB 中不要明文保存玩家的口令。如果你的数据被你的不良员工流传出去那对用户是灾难性的。所以,用户的口令一定要加密保存,最好是用不可逆的加密,如MD5或是SHA1之类的有hash算法的不可逆的加密算法。
  • 正确的设计“记住密码”。对玩家来说是一件挺爽的事,但是时间一长,玩家可能就会忘记自己的口令,要考虑记住密码的过期时间。
  • 找回密码不要使用安全问答。这个环节很烦人,而且用户并不能很好的设置安全问答。例如我父亲姓什么,我最好的朋友是谁等等,因为今天的互联网和以前不一样了,今天的互联网比以前更真实了,我可能可以通过微博、LinkedIn等社交网站查询到你真实信息。

性能问题

在实际开发中,需要提前计算好每一个进程最多会用到多少内存,之前提到服务器跟客户端的区别,服务器会承载上万人同时在线,还有一个区别就是服务器是 24 小时在线跑的,客户端程序跑着跑着崩了,重启就行,但是服务器不行,重启后所有玩家都拉闸了。服务端崩掉,BUG、内存爆掉、断电、地震。因为游戏的开发模式是快速上线、迭代,因此出现 BUG ,进程宕掉是很正常的情况。

服务器物理性能的边界:流量、CPU、内存、DB 访问。如果前面四个瓶颈被突破,那么不会有后面提到的两个问题:性能波动、毛刺与雪崩。性能波动,例如短期内大规模用户登录,如果不做平滑策略的话,来一个请求我就响应,CPU 可能就会被打满,引起雪崩,雪崩后,玩家就会变卡,变卡后玩家就会重新试,本来可能发了一次请求,现在变成了十次、二十次请求,积累的越来越多,处理量越来越大。平时通过计算内存的占用可以 Hold 住,但是一定要有应急策略,不能让 CPU 打满,雪崩就停服,因为这种原因停服我们就停职。

总结

登录系统,起到了一个奠定框架的作用。因为复杂性,游戏服务器逐渐从一个单体变为一个分布式系统。导致登录系统要在各个分布式的进程之间创建数据,同时在每个进程中产生了一个基础状态机的概念,在顶层也有一个状态机。我们还需要知道登录是个多层分布式状态机,对基本流程有印象,知道考虑常见的四个边界情况。还需要考虑性能、容灾问题,宕机后游戏还能恢复。

不同业务场景下,Redis数据结构选型
2022总结与2023年计划