游戏服务端区别于互联网服务端,互联网后台多是由多个微服务组成,如登录,商品,订单,支付服务等,后台数据持久化存储在DB中,并利用缓存提高数据访问速度。
游戏服务端通常使用一种称为ECS的架构,每个entity代表一个对象,可以是玩家,NPC,也可以是服务。
为了支持entity之间进行RPC通信,entity有MailBox,由IP端口和EntityID确定,集群间的Entity通过MailBox进行RPC调用。
客户端存在一个和服务端对应的entity,称之为avatar,代表玩家。客户端和服务端通过KCP(可靠UDP)协议传输数据。
AvatarEntity
代表玩家,是所有玩家控制的Entity的父类
持有一个ClientProxy的对象,通过该对象和客户端的entity通信,ClientProxy封装了对客户端entity对象的调用。
每个客户端在服务端都只有一个ClientProxy和它通信,所以同一个时间同一个客户端只能控制服务端的一个AvatarEntity,但客户端需要控制服务端不同的entity的时候,就需要服务端的entity通过give_client_to来转移ClientProxy的所有权。
下图为传统的单服结构,这里的单服并非单机,而是对应一组Gate,Game,GameManager组成的进程,每个进程都是单epoll线程。
服务发现
Game服务器通过GameManager发布自己的地址信息:Game服务启动之后都会注册自己到GameManager中,同时也会发送keep-alive给GameManager,因此Gate服务器可以通过GameManager得到所有Game服务器的地址。
客户端登陆
游戏开始流程
客户端第一次连接的时候,Gate服务器会选择一个Game服务器
该Game服务器会负责创建对应的Entity(Account)来进行认证
之后游戏逻辑需要自己根据用户Account的信息判断Avatar应该所在的场景以及Game服务器,并进行必要的entity迁移(从一个Game服务器迁移到另外一个Game服务器)
当Entity迁移的时候需要通知Gate服务器客户端的映射关系变化,Gate服务器会记住每个客户端当前所在的Game服务器,一个客户端同时只能在一个Game服务器上
Entity迁移
Entity的迁移过程由Entity当前所在Game服务器发起
老的Game服务器通知gate服务器缓存该entity的所有消息直到新的Game服务器ready
老的Game服务器得到gate服务器确认的rpc之后,通过Game Manager中转将entity迁移到新的game 服务器
让gate缓存转发消息,而不是让老的game服务器转发消息主要是希望保证消息的有序性。因为如果让老的game服务器转发消息,可能会出现gate服务器和老的game服务器同时在给新的game服务器发送客户端消息,容易出现消息乱序。
服务的实现
服务也是通过Entity实现的,可以指定某个服务有多少个shard(通过配置),调用时制定调用的方式:
单服结构的缺点是所有游戏逻辑(登录,社交,排行,工会,匹配,战斗等)都在一个服上实现,比如5V5 moba游戏,一个进程能承载的最多战斗为4场,要想实现10万人同时战斗,那这个服至少要有2500个进程,会导致GameManager负载过高,单点成为瓶颈。
于是出现了多服结构,如下图所示是一个大厅服,一个中心服,N个战斗服的架构,每个服都是一个单服结构,每个服都有Hostnum作为唯一的ID。
每个服的启动配置中指定了hostnum和服的类型,大厅服和战斗服配置中还指定了中心服的hostnum,这样可以调用到中心服的服务。
大厅服
所有玩家都在一个大厅服上登录,在玩家看来,都是在一个服进行登陆。因为大厅服只负责战斗外的一些逻辑,不会无上限的扩展Game进程,所以单服可以承载大量的玩家,也可以扩展大厅服的数量。
中心服
多服结构中只存在一个中心服,负责全局的玩法,如跨服匹配等,也负责管理战斗服的资源,负载均衡等。
战斗服
只负责战斗逻辑,无状态,不依赖存储,可以动态的增删战斗服。战斗服的状态(多少个game进程,每个进程目前有多少个战斗)需要定时向中心服上报,这样中心服就可以全局感知有多少个战斗服,每个战斗服的负载(能安排多少个战斗,以及目前有多少个战斗正在进行)。
当玩家匹配完成后请求分配战斗资源:
HUB服
实现跨服调用的一组服务,只做消息转发。entity的mailbox新增了一个hostnum,hub服会根据hostnum将消息投递到目标entity所在的服。
多服登录流程