-
游戏服务器架构设计的意义
- 良好的架构设计有助于团队协作开发
- 良好的架构设计有助于避免bug 的产生
- 良好的架构设计有助于制定合理的项目开发周期计划
- 良好的架构设计有利于测试
-
游戏服务器架构分类
-
游戏服务器架构基本模块
-
网络通信长连接与短连接
- 长连接是指,客户端与服务器一旦建立好连接之后,就维护这个连接,并保证这个连接不断开。长连接的优点是客户端每次发送请求都不用重新建立连接,有请求可以立即发送,从而节省消息发送的时间。而且长连接通信是双向的,服务器也可以主动给客户端发送消息,比如向其他客户端转发另一个客户端发送的聊天消息,或主动告知客广端一些数据状态的变化。而长连接的缺点是会一直占用计算杌资源(内存、文件句柄、网络VO);代表就是Socket
- 短连接是指客户端与服务器建立好连接,一次请求返回之后,这个连接就会断开,不需要维持连接。短连接的优点是连接断开之后资源就会释放,不再占用服务器资源,可以为更多的客户端提供服务。短连接的缺点是每次请求都需要等待建立新的连接,请求发送会慢一些,HTTP 就是常用的短连接协议之一
-
网关
网关就是网络通信的第一关,是服务器上所有服务的大门。它负责与外界联系,并且可以“辨正邪,识真伪,保证内部服务收到的都是合法消息,限制请求流量,防止请求超载。
-
服务消息交互——消息中间件
消息中间件是一类成熟的网络通信组件,它很好地屏蔽了网络的底层通信细节,比如网络连接建立、消息编码解码、消息发布与监听等。它具有高性能、低禺合、发布/ 订阅异步性、流量控制、最终一致性等一系列功能,既支持单点部署,又支持集群部署。也有一些 RPC,以它为通信基础,实现业务服务之间异步调用,使用起来非常方便。
-
业务处理框架
业务处理框架是为了便于业务功能开发,对影响业务功能开发的公共部分进行架构规划,尽量让开发人员专心于功能设计与开发,并提供底层的基础支持。一般有3个模块。
- 消息管理 游戏用户操作的过程,对服务器来讲就是修改游戏数据。在服务器架构中,网络层的数据接收和请求的业务处理会在不同的线程中,一个游戏用户的数据只有一份,用户有可能同时修改这份数据,这就有可能出现多线程操作共享数据的问题。解快这个问题的一种方式是对数据修改进行加锁。但是由于游戏用户数据很多,导致需要对服务器发送大量并发请求,多线程执行加锁代码时,为了获取执行权和保存线程执行状态,会导致 CPU 大量的上下文切换(指CPU 从正在执行的线程切换执行另一个线程),反而减少了服务器的消息处理的吞吐量,使性能下降。而且如果控制不好加锁,也容易出现死锁的现象。另一种方式是不加锁,让同一个用户的数据按顺序处理。一种实现方法是把同一个用户的请求先放到一个队列中,不同的用户的请求可以分到不同的队列中,再对每个队列固定启动一个对应的线程处理队列中的消息。这样可以避免加锁引1起 CPU 产生大量的上下文切换,也保证同一个用户的数据,都在同一个线程中处理,避免并发修改共享数据。
- 线程管理 在游戏服务器中,线程是一种非常珍贵且重要的资源,也是处理并发的唯一手段。但并不是说线程越多越好,线程太多,也会使 CPU 产生大量的上下文切换,使线程处理业务的能力下降。要合理地规划线程的使用,才能使线程的利用率最大化。对线程的合理分配,可以更好地优化服务器的性能。比如分配专门的线程池处理游戏业务,另外分配专门的线程池负 责数据的操作,把耗时操作隔离到固定的线程中,减少对业务线程的卡顿,增加业务消息的吞吐量。因此在游戏服务开发中,不能随意地创建线程,定要有规则和标准。
- 数据缓存与持久化 在业务服务中,所有的操作都是依赖于数据。数据存储于数据库,但是把数据缓存在内存之中,可以避免操作数据时查询数据库而浪费时间。因此游戏数据什么时候加载到内存,如何将内存中的数据更新到数据库之中,也是架构要解决的问题。这样可以把数据的管理统一化,业务开发中只需要操作内存的数据即可,即使不了解数据库相关的知识也能很快开发业务。
-
测试模块
测试模块主要包括单元测试和系统集成测试,单元测试可以使用目前流行的测试架构,比如 TestNG、JUnit 4 等。
-
Apache Maven 是最常用的 Java 项目管理工具之一。它制定了标准的 Java 项目结构,方便于依赖和引1用其他的 Jar 包,管理单元测试、打包流程、版本发布,也提供了各种开源的功能插件,提高项目的开发和管理效率。
-
数据持久化——MongoDB
MonsoDB 是目前游戏服务器开发中常用的数据库之一。它是文档型数据库,直接存储 JSON 串,插人和选择速度也相对较快,可以满足数据库的快速开发与管理。
-
内存型数据库——Redis
目前Redis 已经是游戏服务器架构中不可缺少的一个组件了。它是一种内存型数据库,所有存储在 Redis 中的数据在 Redis 启动之后会全部加载到内存中,所以从 Redis 中查询数据和存储数据的操作速度非常快,可以作为缓存和共享内存使用。
此设计针对HTTP/HTTPS短连接
-
游戏服务中心的作用
-
游戏服务中心提供游戏外围服务
一切和游戏性无关的功能,都可以称之为游戏外国服务。比如用户注册、登录、公告、获取服务器列表、角色创建、角色数据预加载等,这些都是在用户进人游戏之前的功能,在进人游戏之后,不会再被请求。这些功能本身和游戏操作没有什么关系,和游戏数据也没有什么关联,所以把这此功能集中在游戏服务中心统一管理和维护。
-
游戏服务中心方便动态扩展
一台服务器的资源是有限的。随着游戏用户的增多,并发请求会越来越多,服务器的压力会越来越大,导致CPU 处于长时间的忙碌状态,对客户端的响应越来越慢。此时服务的动态扩展是必不可少的。web 服务有非常成熟的扩展框架,比如使用 Nginx 或 Web服务器网关只需要添加少量的配置,即可实现 Web 服务的负载均衡,快速提升服务器的服务能力。
-
-
游戏服务中心开发准备
-
根据需求设计架构
- 可以动态伸缩 一台服务器的资源毕竟是有限的,所以它提供的服务也是有限的。随着公司的发展,用户越来越多,单台服务器的压力越来越大,逐渐不能满足服务的需要。这个时候,就要添加新的服务器,分担服务压力,增加并发量,以提高服务能力。这样的话架构就必须支持负载均衡,根据负载均衡的算法,将客户端的请求分发到不同的服务器上处理
- Web 服务器网关 Web 服务器网关的一个主要作用是转发客户端的请求,实现服务器的负载均衡、集群部署。如果处理同一接口请求的服务有多个,根据接口的参数,把某个指定的相同参数的请求转发到指定的同一个服务上面。比如为了防止同一个客户端连续请求并发修改用户数据的行为,可把同一个用户的请求都转发指定的相同的服务上面,在一个进程中保证请求的顺序处理,而且这个用户的数据还可以缓存在这个服务器上面,成为有状态的服务,提高数据处理效率。这就需要 Web 服务器网关的支持。在网关那里进行请求转发。网关也可以统一进行权限验证,请求流量限制,后面再添加新的 Web 服务就不需要开发这些功能了,可以减少重复开发
- 可以使服务高可用 服务高可用是指尽量避免服务停止,保证7×24 小时提供服务,这就需要相同的服务器至少要部署两台,也叫服务多实例部署。一台服务器出现问题了,会将这台服务器的服务自动转移到另外的服务器上面,另外的服务器可以正常提供服务。这样即使这台服务器出间题,在一定时问内,也只会影响一小部分用户,服务转移成功之后,就可以正常为所有用户提供服务了,期间不需要人为的干涉。
- 注册中心服务 在分布式架构中,服务治理是一个重要的功能。当新的服务启动时,需要被其他的服务感知到,这样其他的服务就可以向这个新的服务发送请求。当有服务被关闭时,也需委被其他的服务感知到,这样其他的服务就不会再向这个关闭的服务发送请求了。这样就需要一个注册中心服务:当业务服务启动时,向这个服务注册此业务服务的信息;当业务服务关闭时,注册中心服务检测到业务服务不可访问时,从注册中心服务中移除此业务服务信息。
根据以上需求,对游戏服务中心架构进行设计,如图 所示。
客户端通过域名,向DNS 负载获取某一个服务的网关的 卫P地址,然后再向这个发送请求。网关通过注册中心服务,知道当前有哪些服务可以使用,根据转发匹配规从请求的 URL 中获取匹配的参数,然后将请求转发到匹配成功的服务上面处理。
-
Spring Cloud
Spring Cloud 为微服务开发提供了一些现成的服务组件,让开发人员能够快速地搭建一套微服务开发环境,主要的组件如下。
-
Spring Cloud Consul 这个组件将服务注册中心----Consul 集成到 Spring Cloud 之中。通过简单的注解配置就可以实现服务的注册中心
-
Spring Cloud Gateway 这个组件在 Spring MvC 的基础之上,为构建web服务器网关提供了丰富的 API,它实现了对客户端请求的路由转发、安全验证、状态监控、服务发现、负载均衡、请求速率限制等功能。可以快速方便地构建 web 服务器网关。
-
Spring Cloud Bus 这是一个消息总线服务,它基于消息队列实现,利用消息队列的发布订单功能,给各个服务节点发送事件,修改服务的一些状态。在业务中,也可以利用它实现不同节点之间的通信,比如内部RPC 的实现等。
-
Spring Cloud Config 这是一个配置中心管理组件,它可以管理分布式系统中的配置信息。比如在application yml 或 application.properties 中的一些公共配置,只需要在 Spring Cloud Conig配置一份文件,就可以在所有的服务节点引用。它还可以对配置数据加密和解密,动态更新配置内容等。
-
-
-
用户登录注册功能开发 创建游戏服务中心项目、网络通信数据格式定义、添加数据库操作、实现登录注册、全局异常捕获处理、登录注册测试、实现角色创建、角色创建测试
-
Consul 服务注册中心 Consul 有多个组件,但总体而言,它是基础架构中的一款服务发现和配置的工具。它是一个分布式、高可用的系统,主要用于管理其他服务向它注册的服务信息。使用Consul可以在业务服务启动的时候,通过 HTTP 接口,向 Consul 服务发送一些服务的基本信息,比如IP地址、端口、服务名称、服务ID等。业务服务也可以通过 HTTP 接口,从 Consul 中获取其他服务的基本信息。
-
Web 服务器网关功能开发 Spring Cloud Gateway
-
统一验证请求权限 网关全局过滤组件——GlobalFilter
-
请求负载均衡 负载均衡组件——Spring Cloud Ribbon
-
网关流量限制 常见的限流算法
- 计数器算法 计数器是一种简单的基本算法,一般是限制在一段时间内,允许多少个请求通过。
- 漏桶算法 漏桶算法用来消除 “突刺现象”。漏桶算法是一个生产者一消费者模式,请求过来时先放人级存队列,另外启动一个定时线程(ScheduicdExecutorservice)以一定的时閒隔从队列中获取请求,可以一次性获取多个并发执行,如果队列满了就拒绝接收请求。这类似于一个漏斗,上面不停地倒水,下面以一定的速度出水。但是它也有一个问题无法应对短时间内的突发流量。比如一次来 100 个请求和一次来10个请求的处理速度是一样的,即无法自适应并发量。
- 令牌桶算法 令牌桶算法是对漏桶算法的一种升级。漏桶中存放的不再是请求消息,而是存放牌。有一个机制,以一定的速度往漏桶中存放令牌,如果桶中令牌数达到上限,就丟弃牌。每次请求过来需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择等待用的令牌或者直接拒绝。
-
HTTPS 请求配置
简单讲是 HTTP 的安全版。即 HTTP 下加人安全套接层 ( Secure Sockets Layer, SSL ), HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。SSL 及其“继任者”传输层安全 ( Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。
-
服务错误异常全局捕获
在web服务器网关转发的过程中,业务服务可能会因为某些原因发生异常,默认情况下,返回的错误信息都是以 HTML 的格式返回给客户端。但是用户中心是作为 Restful服务的,返回的消息统一是 JSON 格式,如果以 HITML 格式返回,通信协议就不统一了,客户端也解析不了。因此当有异常的时候,应该重新组织返回的错误信息,以JSON 的格式返回。这样就可以统一与客户端通信的协议格式,服务器也方便记录日志。
此设计针对Socket长连接
-
游戏服务器网关管理 游戏服务器网关必须支持动态伸缩 游戏服务器网关信息缓存管理 游戏服务器网关负载均衡策略
-
客户端与游戏服务器网关通信开发
-
网络通信数据粘包与断包原因:Nagle算法、MSS/MTU算法、socket缓冲区和滑动窗口
-
网络通信协议制定:根据需要添加需要的字段
- 消息总长度:用于记录每个数据包的大小,方便从TCP 数据流中截取完整的数据包
- 客户端消息序列号:用于消息的幂等处理,每个消息都有一个唯一的递增的序ID,同一个消息序列号只会被服务器处理一次。
- 消息请求ID:用于区分每个请求对应的业务处理。比如 1001 表示登录.1002示创建角色等。
- 服务ID:消息所要到达的服务ID。这个主要用于消息分发和负载均衡
- 客广端发送时间:与本地时间结合,可以检测数据包 在网络中的耗时。
- 服务器返回时间:游戏服务器处理完客户端请求,返回给客户端的发送时间。
- 协议版本:用于功能升级时对老版本的客户端兼容。比如某个功能,在A版本与B版本,需要的参数可能不一样,需要分开处理。也用于客户端版本的强制升级,比如可以强制指定只接收某个版本客户端的请求,如果不是这个版本,将强制客户端升级到这个版本。
- 是否压缩包体:标记包体是否压缩了。当包体达到了一定大小,为了节省带宽,可以对此包体进行压缩。相应的接收端接收之后需要解压
- 错误码:主要是服务器返回给客户端,如果错误码不为 0,则包体的内容为空。
- 消息内容:发送的业务数据,用于对业务的处理。
-
客户端消息与游戏服务器网关的编码与解码开发
-
-
请求消息参数与响应消息参数对象化
-
消息体对象序列化与反序列化
- 消息体使用JSON 序列化与反序列化
- 消息体使用Protocol Buffers 序列化与反序列化
-
消息自动分发处理
-
网络通信安全
- 连接认证
- 通信协议加密和解密
- 游戏服务器网关流量限制
-
网络连接管理
- 连接管理
- 连接心跳检测
- 消息幂等处理
-
游戏服务器网关与游戏业务服务通信定义
-
游戏服务器网关与游戏业务服务通信实现
- 消息序列化与反序列化实现
- 游戏服务器网关消息负载均衡
- 游戏服务器网关消息转发实现
- 游戏服务器网关监听接收响应消息
- 添加游戏业务服务项目
- 游戏服务接收并响应网关消息
-
游戏服务器中的多线程管理
在多核处理器上,充分利用每个 CPU 的处理能力,可以提高服务器的并发处理效率,而要充分利用多个 CPU,就需要在游戏服务中使用多线程。理论上,为了提高游戏服务的并发处理能力,每收到一个消息,就创建一个线程去处理这个消息,这就可以并发地处理N个消息。但是,在一台物理服务器上面,创建更多的线程就能提升服务器的并发性能吗?答案是不能。相反,创建过多的线程反而会降低服务器的处理能力。
-
Netty 线程池模型
Netty是一个异步网络通信框架,为了管理好线程池的应用,它在 Java 的线程模型上做了一些优化和封装,更加适用于事件异步处理。
-
客户端消息处理管理 借鉴Netty 的消息处理机制实现客户端消息事件处理框架模型
-
不同游戏用户之间的数据交互
- 多线程并发操作数据导致的错误或异常
- 在功能设计上避免用户数据之间的直接交互
- 在架构设计上解决用户数据之间的直接交互
- 游戏用户数据异步加载
加载游戏数据的时机
- 启动加载
- 登录加载
- 异步加载
- 游戏数据持久化到数据库
- 游戏数据持久化方式
- 第一种是实时更新,只要用广修改了数据,就同时将被修改的数据更新到数据库中更新成功之后再返回。这种方式的优点是,即使服务器突然宕机了,游戏用户修改的效格也不会天失,下次进入游戏的时候,所有的数据都会从数据库中加载。但是它的缺点也很明显,就是会产生大量的数据操作。
- 第二种方式是定时将缓存数据整体更新到数据库一次,这里说的数据库包括内存型数据库 Redis 和文档型数据库 MongoDB。定时更新的好处是可以减少操作数据库的次数。
- 于定时持久化数据的方式,还有一种中间方案,即先将数据实时更新到 Redis中(或者更短的定时间隔,比如每 1min 就更新一次数据到Redis 中),再定时更新到 MongODB中。因为 Redis 的操作速度要比 MongoDB 快得多,支持更高的 QPS。即使服务器容机了,等服务器重新启动之后,活跃的游戏用户进人游戏,会先从 Redis 获取最新的数据。过段时间之后,就会将最新的数据持久化到 MongODB 中,数据几乎没有损失或损失很小。既然是定时持久化数据,那么就需要有一个定时器来触发持久化游戏数据操作。定时器也有两种,一种是全局定时器,每隔固定的时间间隔触发一次持久化数据操作,定时触发的时候,就会遍历全部的游戏缓存数据,进行持久化操作。这种方式的缺点是会瞬间增加数据库的压力,定时触发的时候,数据库的压力是最高的;而在定时没有触发的时候,数据库的压力又是最低的。这样就出现了两个极限,不能最大化利用资源。另一种方式就是一个用户自己维护一个属于自己的定时器,从用户第一次进人游戏开始计时,每隔固定的间隔持久化一次数据,这样就不会导致瞬间发生-全服数据更新了,把每个用户持久化操作分散开来,增加数据库资源的利用率。
- 异步方式持久化数据的并发问题
- 一种方式是在数据持久化的时候,把原来的Player对象复制一份,这样业务逻辑和特久化的时候操作的都是独立的Player对象,不会出现并发操作共享数据的问题。这种方式的缺点是,在开发的时候,需要业务人员单独手动实现对象的复制,降低了开发效率。其优点是创建新对象效率高。
- 另一种方式是先把业务操作的 Plaver 对象在业务线程中序列化为 JSON 字符串,然后将JSON 字符串传到数据持久化任务事件中执行。这种方式的缺点就是牺牲了效率,对象在序列化的时候,可能由于数据量比较大而执行得比较慢。其优点是开发人员不用关注集合并发性的问题,可以提高开发效率。
- 还有一种方式是在Player 中所有的集合使用线程安全的集合,比如 ConcurrentHashVap,LinkedBlockingQueue。这种方式的缺点是在开发过程中,需要明确约定好。开发人员在使用到集合的时候,一定要声明为线程安全类型的集合,不能声明为接口类型。
- 游戏数据持久化方式
分布式架构永远绕不过的问题就是不同进程间的数据通信,即远程过程调用 (Remote Procedure Call, RPC)现在被统一称为内部RPC。现在有很多开源的RPC 库,比如gRPC, Dubbo, Hessian,Thrift 等。毋庸置疑,这些都是非常优秀的 RPC 框架,但是它们大部分都用于 Web 服务,且为短连接,满足不了游戏对高性能的需要。 虽然 Thrift 支持长连接双向通信,但是要把亡融人自己的架构系统,不得不做很多妥协。就像练习别人的武功,永远达不到最高境界,只有融人适合自己的元素,才能完全发挥它的威力。
- 事件系统在服务器开发中的重要性
- 事件系统可以解耦模块依赖
- 事件系统使代码更容易维护
-
游戏服务器自动化测试的重要性
- 单元测试使代码更简洁
- 单元测试保证方法的代码正确性
- 自动化测试保证代码重构的安全性
-
游戏服务器自动化测试的实现
TosNG是一款应用于 Java 开发的优秀的测试框架,它以 Junit 上借监了很多特性,号称“第二代单元测试框架”。相比Junit来说,它加人了一些新的功能,更加强大,使用更加方便,正所谓青出于蓝而胜于蓝。TestNG 的特点如下。
- 基于注解。
- 在具有各种可用策略的任意大线程池中运行测试(比如一个方法一个线程或一个 测试类一个线程等)。
- 运行的测试行为都是线程安全的。
- 灵活的测试配置,可以通过 xml 配置文件制订不同的测试策略。
- 支持数据驱动测试(使用 @DataProvider)。
- 支持给测试方法传递参数。
- 强大的执行模型。
- 被很多工具和插件支持,比如 Eclipse、IDEA、 Maven 等。
- 集成 BeanShell ( BeanShell 是一个小型联人式 Java 源码解释器,具有对象脚本语言特性,能够动态地执行标准 Java 语法),扩展性强。
- 默认自带运行时和日志的JDK 功能。
- 单服世界聊天系统实现 添加客户端命令 服务器实现消息转发 单服世界聊天测试
- 分布式世界聊天系统实现 分布式世界聊天系统设计 创建单独的聊天项目 实现聊天消息的发布与转发 分布式世界聊天服务测试