因为大型多人在线游戏服务器理论上须要支持无限多的玩家,因此对服务器端是一个很是大的考验。服务器必须是安全的,可维护性高的,可伸缩性高的,可负载均衡的,支持高并发请求的。面对这些需求,咱们在设计服务器的时候就须要慎重考虑,特别是架构的设计,若是前期设计很差,最后面临的极可能是重构。算法
一款游戏服务器的架构都是慢慢从小变大的,不可能一会儿就上来一个完善的服务器构架,目前流行的说法是游戏先上线,再扩展。因此说咱们在作架构的时候,必定要把底层的基础组件作好,方便之后扩展,可是刚开始的时候留出一些接口,并不实现它,未来游戏业务的发展,再慢慢扩展。固然,若是前期设计的很差,后期业务扩展了,但架构没办法扩展,只能加班加点搞了。数据库
面对庞大的数据量咱们想到的惟一个解决方案就是分而治之,即采用分布式的方式去解决它。把紧凑独立的功能单独拿出来作。分担到不一样的物理服务器上面去运行。并且作到能够动态扩展。这就须要咱们考虑好模块的划分,尽可能要业务独立,关联性低。api
前期,因为游戏须要尽快上线,开发周期短,咱们须要把服务尽快的跑起来,这个时候的目标应该是尽快完成测试版本开发,单台服务器支持的人数能够稍微低一些,可是当人数暴涨时,咱们能够能过多开几组服务来支持新增涨的用户量,便可以平衡扩展就能够了。到后期咱们再把具体的模块单独拿出来支持,好比前期逻辑服务器上包括:活动,关卡,背包,技能,好友管理等。后期咱们能够把好友,背包管理或其它的单独作一个服务进程,部署在不一样的物理服务器上面。咱们先按分区的服务进行设计,后面在部署的时候能够部署为世界服务器,下面是一个前期的架构图,安全
本文来自游戏技术网:http://www.youxijishu.com服务器
下面咱们从每一个服务器的功能提及:网络
负责用户的登录验证,若是有注册功能的话,也能够放在这里。通常手机游戏直接走sdk验证。网页游戏和客户端游戏会有注册功能,也能够叫用户管理服务。架构
负责接收客户端的用户登录请求,验证帐号的合法性,是否在黑名单(被封号的用户),是否在白名单(通常是测试帐号,服务未开启时也能够进入)。若是是sdk登录,此服务向第三方服务发起回调请求。并发
使用加密的传输协议,见通讯协议部分。负载均衡
白名单是给内部测试人员使用的,在服务器未开启的状态下,白名单的用户能够提早进入游戏进行游戏测试。异步
黑名单的用户是禁止登录的,通常这是一些被封号的用户,拒绝登录。
服务器使用私钥解密密码,进行验证,若是是sdk登录,则直接向第三方服务发起回调。
当用户登录验证成功以后,服务器端须要生成一个登录令牌token,这个token具备时效性,当用户客户端拿到这个token以后,若是在必定时间内没有登录游戏成功,那么这个token将失败,用户须要从新申请token,token存储在登录服务这,向外提供用户是否已登录的接口,其它服务器想验证若是是否登录,就拿那个服务收到的token来此验证。
当用户登录成功以后,显示最近登录的角色信息。
用户登录成功以后,请求公告服务器,获取最新的公告,公告服务先根据token和Userid验证用户是否已登录,公告有可能根据渠道的不一样,显示不一样的公告。因此 公告必定是要能够根据渠道编辑的。
当用户登录成功以后,请求服务器分区列表服务器,显示当前全部的大区列表。
2.1 验证用户是否已登录
向登录服务器请求验证是否已登录。
2.1 大区列表显示
大区列表信息中只显示大区id和大区名称。这样作是为了安全考虑,不一次性把大区对应的网关ip和端口暴露出来,也能够减小网络的传输量。
2.2 用户点击选择某个大区,客户端拿到大区id再向选区服务请求获取此大区对应的网关ip地址和端口。根据负载算法计算得出。
2.3 网关的选择
选区服务会维护一份网关的配置列表。一个大区对应一到多个网关,当配置有多个网关时,须要定时检测各个网关是否链接正常,若是发现有网关链接不上,须要把大区对应的网关信息设置为无效,再也不参与网关的分配,并发出报警。
通常对于网关的选择,可使用用户id求余法加虚网关节点法。这样在网关节点数量固定的状况下,一个用户老是会被分配到同一个网关上面。可是若是只是使用求余法的话,可能会形成用户分布不均衡,这里能够经过增长网关的虚拟节点(其它就是增长某个网关的权重,让用户多来一些到这个网关上面),这个能够参考哈稀一致性算法。包括后面说到的一个网关对应多个逻辑服务器,也可使用一样的方法。这部分能够抽象出来一个模块使用。
2.4 选区服务对内要提供修改服务器状态的接口,好比维护中...
4.1 创建链接
收到客户端的创建链接请求以后,记录此channel和对应的链接创建时间。并设置若是在必定时间内未收到登录请求,则断开链接。返回给客户端登录超时。
4.2 登录请求
收到登录请求后,移除记录的channelid信息,向登录服务器验证用户是否已登录过,并向外广播用户角色登录成功的消息。
4.3 登录成功后,接收网关的其它的消息
4.4 客户端消息合法性验证
在向逻辑服务器转发消息以前验证消息的合法性,具体验证方法见协议安全验 证。
4.5 将客户端消息转发送到对应的逻辑服务器。
5.1 协议序列化和返回序列化
能够直接使用protobuf,直接对协议进行序列化和反序列化。
5.2 协议组成
5.2.1 包头构成
包总长度,加密字符串长度,加密字符串内容,userId,playerId,版本号,内包内容。
5.2.2 包体组成
请求的逻辑信息,是protobuf后对应的二进制数据。
包总长度 |
加密内容 |
UserId |
playerId |
请求序列id |
版本号 |
内包内容 |
|
Int |
64 |
Long |
Long |
Long |
int |
varchar |
|
4 |
64 |
8 |
8 |
8 |
4 |
变长 |
|
若是协议明文传输的话,被篡改的风险就很是大,因此咱们要对传输协议进行加密传输,因为协议内容大小不固定,为了保证效率,采用对称加密算法,首先客户端使用AES的公钥对消息内容加密(上表中userid以后的信息),客户端把加密后的报文发送到服务器端。AES的公钥在用户第一次链接时获取。
尽管咱们对消息作了加密,但也不是万无一失的,为了进一步确保消息没有被篡改,咱们须要对消息的完整性进行检测,使用数字摘要的方式,首先客户端对userid及以后的协议信息进行AES加密,加密以后取它的md5值,md5值用于验证数据的完整性。这个md5值会被传送到服务器,若是协议信息被修改了,那个md5就会不一样。
为了防止非法用户修改协议内容后,模拟客户端操做从新生成新的数字摘要信息,咱们对生成的数字摘要信息进行二次加密,此次使用RSA的公钥对md5的值进行加密,将加密的内容和其它信息一块儿发送到服务器。服务器根据ip向登录服务器拿到AES的公钥和RSA的私钥,先用RSA 私钥取出客户端加密的md5值,服务器端计算userid后面的数据的md5值,若是两个md5值同样,说明安全的。若是不同,说明用户是非法的,加入黑名单。由于RAS使用公钥加密,必须使用对应的私钥才能解密,并且不一样的公钥对应的私钥不一样,这样就算非法用户从新生成了数字摘要,在服务器端也是验证不经过的。
当服务器收到报文后,对报文进行数子摘要验证经过以后,服务器端使用用户本身对应的AES的公钥,解密数据,得到明文数据。为了保证安全,每一个用户的AES公钥可能不同。
发布订阅是一种分布式的解耦方式,它使用模块更加独立,模块间的数据交互更加方便,发布订阅模式是一种一对多的关系,发布方不关心谁订阅了它,只要想得到它发布的消息的服务,均可以去订阅它。发布方式是异步的,它加强了系统的处理性能,增长了系统的吞吐量。目前的大多数消息队列都支持发布订阅模式,好比rabbitmq,activemq,kafka等消息队列。发布订阅服务能够单独部署,加强了系统的扩展性和稳定性。
在服务器内部不一样的服务有时候须要信息交互。为了方便服务之间的调用,咱们引入了RPC的概念。客户端调用一个api以后,底层会把此调用发送到远程的服务上处理,远程服务处理完以后再返回结果。rpc的做用就是封装底层协议的序列化和反序列化,它让用户感受不到调用被发送到了远程服务,而感受仍是在本地同样
7.1 同步rpc
当调用一个同步的rpc以后,结果并非马上返回,而是在等待rpc服务器端的返回。同步rpc能够直接使用带同步的socket实现。或者http请求。另外一种方式是调用rpc方法以后,在本地自旋,直到服务端返回。
7.2 异步rpc
异步rpc调用以后,结果是马上返回的,它的处理方式是把业务放在回调方法里面,而不是一直占用线程在那里等待数据的返回,这样就能够记空闲的线程去处理另外的消息,当消息从服务器端返回后,会去调用那个回调方法。
如今大多数的游戏都是分区分服的,通过一段时间的运营以后,有些老的大区可能在线人数很是的少了,为了节约成本,首先会在一台物理机器上运行多个大区对应的进程,再过一段时间,可能须要把不一样区的数据合并起来到一个数据库中。而对用户来讲是感受不到变化的。
为何说合服要提交设计好呢?由于若是设计很差,后期在合服的时候会遇到不少问题, 好比用户惟一主键问题,表与表主键关联重复问题,那么在合服存在的状况下,如何保证用户的惟一性呢,也就是我一个用户在两个大区都创建了帐号,这个时候userid是同样的,还有一个角色id,若是角色id不是全局惟一的,也可能重复。而角色id若是参与了表外键设计,一重复数据就乱了。
首先,要保证用户的惟一性。并且各个表的外键引用也必须是惟一的,即合服以后不会再发生改变。那么有几个键须要全局惟一,userid(用户id),roleId(角色id),为了区分用户原来所在的区,须要记录角色所在的大区id,因此一个userid和一个大区id来肯定一个惟一的角色id,而角色的其它信息使用角色id作外键引用。这样合服就能够直接把两个库的数据合并到一块儿了。
这个只是用角色数据举个例子,在数据库中,凡是独立存在的,最好都使用全局惟一id,好比公会,每一个服都会有公会,但每一个服的公会id不能都是从一开始,即不能使用数据库自增的方式