特点
- 不分区不分服:具有随着用户量增加而扩展的需求
- 房间模式:同一局游戏就是在同一个房间中,同一个房间中的人可以接收到其他人的消息。
- 房间的操作具有顺序性:类似回合制游戏,每个玩家的操作都是由顺序的。
每个游戏服务器相当于一个模块,运行一个进程,在启动时会连接网关。
关键点
- 数据共享如何实现
由于棋牌类不分区不分服,一般都会按世界服的思路来设计,也就是说,服务器是一个由多台物理机组成的集群。当用户登录服务器后创建房间时,会根据负载均衡算法分布到任何一台服务器上。所以,不管用户登录的是哪一台服务器都是可以获取玩家的数据,一般都会采用Redis来做数据共享。
- 如何进入房间
在同一局游戏中,会要求所有人都在同一个房间中,可以规定在同一间房间中的用户必须登录到同一台物理机上。在创建房间完成之后,其他人根据房间号查找房间的时候,可以根据房间号换取房间所在服务器的IP和端口,判断当前登录用户的服务器的IP的端口是否和所在服务器是否一致,若相同则不做切换,若不同则需连接到房间所在的服务器上。
- 如何保证房间操作的顺序
创建房间成功之后接下来的操作都要保证它的顺序性,所以房间需要有一个自己的消息队列,可以把每个房间到达服务器的消息封装为一个任务,将这个任务放到消息队列中,然后有一个任务执行者去按顺序执行这些任务。
功能设计
- 登录
登录都需要接入第三方,所以这块是HTTP操作,需统一提供一个Web服务,用来做登录验证。因为在登录时调用第三方的HTTP服务的过程会很慢。如果放在逻辑服去做的话可能会卡住业务逻辑任务。因为不同的玩家业务请求可能在一个线程中,如果有任务卡那么这个任务之后新来的请求都会被卡住,进而会导致消息延迟。
- 公告通知
公告一般会在登录时向服务器获取一次,将它放在Web服务器与业务逻辑分离,当业务逻辑服维护或更新时不会影响用户的登录。
对于世界消息通知,统一由网关处理并发送给本网关中所有的用户。对于管理后台发送过来的通知,如玩家充值、自动广播等,可直接发送到网关提供的HTTP服务接口,由网管统一发送给网管中的所有用户。对于游戏内的通知,如结算通知等,可直接使用游戏中的通知模块处理之后,发送到网管在发送给各个客户端。
- 用户ID
由于棋牌类游戏是世界服无分区,所以用户的ID必须是全局唯一的,可以利用Redis的incr()
方法原子的递增,如果不想被别人根据递增的ID推算出有多少注册用户,递增的梯度可以随机,比如每次递增的值都是从1到1024中随机的一个。
- 创建房间
当房主房间房间时,房间ID需要在任何一台服务器都可以查询到,所以创建房间成功后,房间ID要存储在贡献内容Redis中,每个房间ID对应一个房间所在的IP或服务器ID。当有用户进入房间查询房间ID时,可以判断房间是否和自己登陆的游戏服是否相同。
- 查找并加入房间
根据房间ID查找到房间后,获取房间所在服务器的ID或IP,如果和所登录的服务器相同,就可以直接加入房间。如果不一样,则要把房间所在服务器的ID或IP返回给客户端,让客户端重新与房间所在服务器建立连接,并使用登录时的token验证用户。
- 游戏脚本调用
在验证游戏是否合法时,客户端和服务器都需要验证,验证的算法是一样的,使用Lua脚本编写一份脚本,在服务器和客户端都可以同时使用。同一个算法使用同一个脚本,在开发新的同类游戏时,只需要替换下脚本,也就不用再重复开发。
- 后台管理系统
棋牌类后台管理系统会根据运营需求开发,需求不一。不过后台管理系统是要和游戏服务器通信的,这种通信方式最好是采用Redis的订阅发布机制,这样可以把某个消息事件同时发送到所有业务服务器上,并根据用户所在服务器进行处理。
- 玩家同屏
玩家同屏是棋牌类游戏的一个重点,对应大型ARPG或MMO游戏来说,这并不是什么难事儿。因为同屏就是服务器对客户端的消息进行转发。由于棋牌游戏同步数据量比较小,常见的同步方式有两种:
- 客户端主动拉取
客户端定时主动向服务器请求一个用户的消息队列,当一个玩家有操作需要同步到其他玩家时,在服务器端先把消息放到用户的消息队列中,等待客户端的拉去操作。这种做法的好处是不需要考虑网络闪断或弱网环境,消息都是同步获取的。缺点是定时拉取的时间间隔很短,可能会不到一秒就要拉取一次。
- 服务器主动推送
当一个玩家出牌的消息需要同步给其他玩家时,服务器会获得这个玩家和服务器建立的socket连接,然后服务器使用socket主动向客户端发送消息。这种方式要考虑网络闪断造成消息丢失的问题。因为服务器推送的消息客户端可能会收不到,所以客户端需要根据心跳来判断网络是否断开连接,如果断开就需要重新从服务器拉取整个房间状态,或者是根据服务器发送的消息号,当客户端接收到的服务器消息号有跳号时,比如应该收到10却收到20说明中间有消息丢失,这个时候需要重新拉整个房间的状态信息。这种做法的缺点是开发复杂,需要考虑网络问题。优点是只有在有消息时才会推送,没有的话不推送,不占用带宽等网络资源,可以增加用户同时在线数量,也就增加了服务器的承载量。
- 共享内存
共享内存在系统中的地位看上去很像是数据库前端的缓存,和《天龙八部》的ShareMemory类似,sharedb也采用定长的数据结构,通过共享内存来实现进程间的数据共享。sharedb的存在使得游戏逻辑处理和数据保存逻辑得到很好的隔离,游戏逻辑不必关心后端的数据是如何保存的,只要sharedb挂上定期存盘的服务,在接口定义明确的情况下,后端到底采用什么样的数据库变得不是那么重要,从而降低了系统的耦合度。
- 数据同步和持久化
由于棋牌类游戏数据量少,计算量也很小,所以完全可以不使用内存缓存而直接使用Redis做共享内存,用户的所有数据都缓存到Redis中,更新也同步更新到Redis中,这样不管一个用户登录哪一台业务服务器都能获得自己的最新数据。
在更新数据库时由于数据第一缓存是Redis,所以活跃的用户数据都可以从Redis中直接获得,而不用查询数据库,所以数据库的更新可以采取异步更新,而不会产生数据延迟。但需要注意的是数据的异步更新必须是有序性的,那么这就产生了一个问题,如何保证用户的更新不会乱呢?
因为业务服务器是由多个的,用户可能连接到其中的任意一台,如果说登录的是服务器A加入的房间时服务器B,那么连接就会切换,为了保证数据更新的顺序,可以做一个数据持久化服务,将需要更新数据库的任务实时发送到这台服务器上,由数据库持久化服务执行对数据库的更新,这样不管用户连接到那台业务服务器它的更新都是由顺序的。
由于棋牌类的业务少数据更新少,所以查询可以有Redis缓存以减少数据库查询的压力,而更新实行实时更新到数据库,前期可以不需要开发数据持久化服务,等用户积累到一定数量后,发现更新数据库比较缓慢的时候再单独做一个数据库持久化服务。
Redis负责缓存,由于缓存的数据是最新的,通常会比数据库中的要新。例如在玩家登录游戏后,玩家所有的数据都以Redis中为准,Redis中的数据会定时同步到数据库中,同步的有游戏服务器中的模块同步,另外一般5分钟同步一次所有变更的玩家数据。如果Redis启动了RDB快照,则会自动定时同步数据到磁盘。
- 存储方案
网关维护一份当前启用的游戏数据并存储在Redis中,采用哈希的结构。游戏服连接到网关后,网关记录启用的游戏到Redis中,登录服在用户登录时需要从这里读取启用的游戏列表。
网关和游戏服共同维护一份在线玩家数据并采用哈希结构,玩家加入房间成功后由游戏服设置在线数据,玩家推出时由游戏服更新数据并同步到客户端。
玩家在游戏中的各种行为由游戏服保存在进程内存中,玩家账户变更时由游戏服直接更新Redis中的用户数据,玩家道具变更时由游戏服中的异步更新模块负责更新并同步到客户端。
玩家断线时,网关捕获到网关断线,直接通知游戏服进行相应逻辑处理并更新共同维护在Redis中的数据。
网关断线时游戏服捕获到网关断线,而对于扎金花、斗牛等这种超时自动棋牌的游戏,游戏服需要设置当前游戏中的所有玩家断线。对于斗地主这种超时则对服务器进行自动托管而不做任何操作,当游戏结束后30秒不准备则会自动踢人,并共同维护Redis中的数据。
网关启动时,当玩家重新连接网管,此时检查玩家是否在游戏中,如果在则立即返回并不做任何操作。
游戏服断线时网关捕获到,会从Redis中删除游戏数据并通知所有在断线游戏中的玩家游戏断线返回大厅,并删除用户在线数据中的游戏字段。
游戏服启动之后连接网关,网关注册游戏数据并保存到Redis中。
服务器划分
目前游戏服务器架构中多以功能和场景来切分,服务器划分的主要原则基于:
- 分离游戏中占用系统资源较多的功能独立成服务器
- 以多线程或多进程的编程方式适应多核处理器
- 在同一个服务器架构下因尽可能地复用某些服务器(进程级别的复用,如场景服务器)。
- 运行时玩家数据的保存于修改及数据流向应该是设计的焦点,它同时也决定了服务器应该如何划分。
- 服务器的划分应该适度,在保证清晰的数据流向的前提下根据游戏的类型和规模尽量减少服务器或服务器进程的个数,以减少服务器之间过多的复制数据和锁冲突(使用共享内存进行通讯时)。
- 主要按照场景划分进程,若按功能划分则必须保证整个逻辑足够简单,并满足1和3两点。
以云风的服务器架构为例:
- 登录服(LoginServer)
负责玩家的登录请求,验证玩家合法性,为合法的用户分配Session,与客户端采用短连接方式,可以有多个登录服来做负载均衡。玩家验证通过后,登录服会找到合适的网关发送给客户端。
登录服提供注册新玩家并处理玩家登录请求,需要和UserInfoDB交互,交互主要包括:在注册时写入玩家信息,玩家登录时与数据库玩家信息核对。登录服会定时向中心服发送更新游戏列表和房间信息的请求,因为这些信息在不断变化,而登录服需要在玩家登录时间这些数据返回给他们。
- 网关(Gateway)
网关服务器可有多个做负载均衡,与客户端保持长连接,客户端发送的消息都是通过网关转发给大厅服务器或游戏服务器,大厅服务器或游戏服返回给客户端的消息也都需要经过网关,网关充当着消息转发的中转站,防御网络恶意攻击的作用。网关会将来自不同客户端的消息格式转化为系统内部统一处理的消息格式,系统处理完消息后,再将返回的消息交由网关转化为客户端对应的格式并返回给客户端。
网关的作用是转发消息包、业务的负载均衡、维护与客户端的连接、贷款的整合等。棋牌类游戏是否需要网关呢?由于棋牌类游戏业务比较单一,最多的操作是消息同屏转发、任务、活动。因此前期可不用考虑。
- 中心服(CenterServer)
中心服不直接与玩家进行交互,主要负责管理游戏列表和房间信息,包括游戏类型、玩法种类、站点信息、房间信息等。
中心服中有关游戏列表的信息是在它启动的时候从数据库(ServerInfoDB)中加载的,房间信息来自于房间服务器(RoomServer),房间服务器在启动时将自己注册进入,关闭时从中心服里销毁自己,同时在线玩家进入房间时还会要求中心服更新在线人数。
中心服还应该响应登录服和房间服的请求,将游戏列表和房间信息返回给它们。
- 大厅服务器(LobbyServer)
负责大厅中的功能,例如牌桌数量,各游戏在线数量等。
- 游戏服(GameServer)
不同的游戏有不同的服务器,负责具体的游戏的逻辑实现。
- 数据服务器(DbMgr)
负责持久化数据,经过数据服务器与数据库进行交互,数据服务器通过数据缓存、批量事务、本地持久化等手段提升整体系统性能。对于同时在线数千人的系统数据服务器只需要1个就足够了,对于大型系统则可以使用分区方式,每个区使用一个数据服务器,系统根据玩家所属的区来选择对应的数据服务器。
- 日志服务器(LogServer)
有时玩家需要回顾整个游戏过程这就需要服务器将游戏的过程以日志的方式存储起来,供玩家检查用。日志服是用来响应玩家核查的请求,然后从LogDB中间整个游戏过程返回给玩家,客户端以视频的方式展现给玩家。
玩家在请求检查时,客户端会将某局游戏以及玩家ID发送给日志服,日志服根据玩家ID获取日志记录返回给玩家。游戏的过程可以使用结构化语言描述处理,也可以使用JSON形式存入数据库。但是由于可能会有字节序的问题,所以日志信息需要使用protobuf序列化后再存入数据库。日志服在从数据库中读出日志后不要反序列化即可直接返回给客户端反序列化。
服务器体系
- 登录时客户端向登录Web服务器发起请求,登录成功后返回token,为适应大规模Web请求和登录服的稳定,采用Nginx做负载均衡。
- 登陆成功后请求负载均衡服务器,获取一台连接的业务服务器,负载均衡服务器和Web登录服可以在一个进程中也可以独立出来。
- 获得登录成功的Token和需要连接的业务服务器的IP和端口后,再去连接业务服务器。
- 连接业务服务器成功后,使用Token到Web登录服去做验证,校验用户是否已登录。
- 相同房间的用户要连接同一台物理机
- 使用Redis做共享缓存
- 使用MySQL做持久化存储
8.数据库持久化服务器统一做数据入库操作
流程设计
注意事项
- 游戏服务器玩家账户维护
可以提供一个账户数据缓存模块,用户加入房间后将账户数据写入到缓存模块,玩家后续在游戏中的各种操作,都使用缓存模块中的数据。在牌局结算后统一修改Redis并同步数据到缓存模块。