我们都知道王者荣耀是一款在线游戏,那什么是“在线游戏”呢?它是指以互联网为传输媒介,以游戏运营商服务器和用户计算机、手机等为处理终端,以游戏客户端软件为信息交互窗口的旨在实现娱乐、休闲、交流和取得虚拟成就的具有可持续性的个体性多人游戏。在线游戏都是网络游戏运营商采用专业的游戏服务器进行管理和运营,才能让在线游戏玩家在娱乐时改变其属性并实现数据的存储与修改(例如等级、攻击力、防御力等信息的变化)。因为在线游戏的终端并不是在本地,所以在线游戏必须依靠互联网才可以正常运转。但关于游戏服务器却并没有什么较好的评价,游戏进行不顺畅时也经常会说是“服务器不稳定”,如果存在卡壳现象首先责怪的也必定会是服务器。那现在就开始了解一下这个话题多毛病也多的,但又具有重量及存在感的服务器吧!
什么是服务器?我们可以将服务器分类为“主机游戏服务器”和“专用游戏服务器”。主机游戏服务器是指,在购买一个游戏后直接运行游戏中的服务器并与他人一起进行游戏的程序。在Package游戏当中可以看见这种游戏服务器。而专用服务器,游戏玩家无法直接在自己的电脑上运行服务器,而是由游戏制作商运行游戏服务器。专用服务器可以承载比主机服务器更多的同时在线人数,少至数十名多至数百万名可以同时进行游戏。游戏制作商保留专用服务器,因此无论是在技术上还是在法律上,游戏玩家直接运行游戏服务器是不可能的。主机游戏服务器只在Package游戏当中,而专用游戏服务器是在在线游戏当中。本书中,会将专用游戏服务器统称为游戏服务器。
游戏服务器的作用
如果要进行在线游戏,则首先需要下载并安装“游戏客户端程序”,但只有客户端也不能直接进行游戏,首先需要联网,之后还要连接到服务器。“在线游戏”一般不会只有一个人进行游戏,它需要与连接到服务器的其他玩家一同冒险、一起竞争,所以我们称其为多人游戏。但如果要与其他玩家进行游戏,则中间需要有一个管理游戏进度和数据的程序,这便是“游戏服务器”。在线游戏中,输入、处理游戏Logic、输出呈现这三大功能被分割到服务器和客户端中,游戏客户端负责输入和输出呈现,游戏服务器负责处理游戏Logic的功能(如下图)。这便是没有连接服务器便无法进行游戏的原因。
搭建服务器环境
我们已经知道了服务器要处理游戏逻辑,那么要想实现服务器的功能,首先需要做的就是搭建服务器环境。本书中我们就来讲解如何利用Node.js引擎搭建服务器,Node.js引擎也是当下最流行的服务器技术之一。
接下来,我们就先来了解一下Node.js。
Node.js开发
Node.js是一个Javascript运行时环境(runtime environment),发布于2009年5月,由Ryan Dahl开发,实质是对Chrome浏览器的V8引擎进行了封装。Node.js对一些特殊用例进行优化,提供替代的API,使得V8在非浏览器环境下运行得更好。
V8引擎执行Javascript的速度非常快,性能非常好。Node.js是一个基于Chrome JavaScript运行时建立的平台,用于方便地搭建响应速度快、易于扩展的网络应用。Node.js 使用事件驱动,非阻塞I/O 模型而得以轻量和高效,非常适合在分布式设备上运行数据密集型的实时应用。
Node.js优点
Node.js可以在不新增额外线程的情况下,依然可以对任务进行并发处理,Node.js是单线程的。它通过事件轮询(event loop)来实现并发操作,对此,我们应该要充分利用这一点,尽可能的避免阻塞操作,取而代之,多使用非阻塞操作。
Node.js模块
Node.js使用Module模块去划分不同的功能,以简化应用的开发,是模块化编程开发引擎。通过require(模块名)或import{模块名}引入模块。
模块是可重用的代码库,比如:用来与数据库交互的模块。说到模块必然会提到包的概念。
包是一个文件夹,他将模块封装起来,用于发布、更新、依赖管理和版本控制。通过package.json来描述包的信息,入口文件,依赖的外部包等等都在这里进行设置。我们可以通过npm install命令来安装包,并通过require语句使用包。
模块依赖架构
如上图所示:your code 为编辑代码,node.js 为核心,Host environment 为宿主环境(提供各种服务,如文件管理,多线程,多进程,IO等等)
1.node.js
这里重点介绍 nodejs 组成部分:v8 engine, libuv, builtin modules, native modules以及其他辅助服务。
v8 engine:主要有两个作用:1.虚拟机的功能,执行js代码(自己的代码,第三方的代码和native modules的代码);2.提供C++函数接口,为nodejs提供v8初始化,创建context,scope等。
libuv:它是基于事件驱动的异步IO模型库,我们的js代码发出请求,最终由libuv完成,而我们所设置的回调函数则是在libuv触发。
builtin modules:它是由C++代码写成各类模块,包含了crypto,zlib, file stream 等基础功能。(v8提供了函数接口,libuv提供异步IO模型库,以及一些nodejs函数,为builtin modules提供服务)。
native modules:它是由js写成,提供我们应用程序调用的库,同时这些模块又依赖builtin modules来获取相应的服务支持。
简单总结一下:如果把nodejs看做一个黑匣子,暴露给开发者的接口则是native modules,当我们发起请求时,请求自上而下,穿越native modules,通过builtin modules将请求传送至v8,libuv和其他辅助服务,请求结束,则从下回溯至上,最终调用我们的回调函数。
2.your code
当我们执行node.js的时候,node会先做一些v8初试化,libuv启动的工作,然后交由v8来执行native modules以及我们的js代码。
了解了Node.js的一些相关知识之后,现在开始搭建服务器的环境。
安装软件
上面我们提到了用Node.js引擎搭建服务器,那我们就需要先来安装Node.js,除此之外,我们还需要一个开发环境,我们选用轻量级的可跨平台开发的VS.Code开发工具。
说明:Node.js下载地址(https://nodejs.org/en/)
VS.Code下载地址(https://code.visualstudio.com/Download)
环境搭建
1. 搭建node.js环境
(1)创建一个工程文件夹
(2)打开Windows命令窗口,指到存放工程文件的盘
(3)通过“cd 工程文件夹目录”命令,进入到工程文件夹中
(4)通过npm init命令创建一个package.json文件
(5)完善package.json文件的相关信息
说明:
package name 包名(全部小写)
version 版本号(1.0.0)
description 描述(Honor of kings server by Ewonder)
point 入口(main.js)
command、git repository、keywords(这三项可以不用填写)
author 作者(填写自己即可)
license 同意许可协议(按下回车键)
Is this ok? (yes)这样就安装完成了。
我们同样可以使用命令行"code .”(注意有点,表示当前项目),通过VS.Code打开项目工程。如下图所示:
2. 安装包文件
通过”npm install -g typescript”命令,全局安装typescript语言包,typescript是javascript的超集。
typescript的用处:
1.为了规范我们的书写格式,typescript是强类型语言,可以使整体脚本有强类型检查的优势,同时,如果对象被声明为any类型,就会忽略所有的类型检查,使得在一些细节问题上保持弱类型的灵活;
2.由于typescript可以编译成明文javascript代码,通过tsc命令可进行编译。
3. 项目配置文件
在项目工程中创建tsconfig.json配置文件。tsconfig.json文件中指定了用来编译这个项目的根文件和编译选项。
通过“tsc --init”命令行,可以直接创建出tsconfig.json配置文件,文件中包含了所有的编译选项,可以根据的需求,选取其中选项。本项目现阶段所需的编译选项及说明如下:
编辑完成之后,在VS Code编辑器中启动调试(或点击快捷方式F5)执行,点击node.js选项会生成launch.json文件,这就是整个项目的启动入口。
游戏服务器特征
游戏服务器,是会长期运行的程序,并且它还要服务于多个不定时,不定点的网络请求。所以这类服务的特点是要特别关注稳定性和性能。这类程序如果需要多个协作来提高承载能力,则还要关注部署和扩容的便利性;同时,还需要考虑如何实现某种程度容灾需求。由于多进程协同工作,也带来了开发的复杂度,这也是需要关注的问题。
功能需求限制,是架构设计决定性因素。基于游戏业务的功能特征,对服务器端系统来说,有以下几个特殊的需求:
1.游戏和玩家的数据存储;
2.对玩家交互数据进行广播和同步;
3.重要逻辑要在服务器上运算,做好验证,防止外挂。
针对以上的需求特征,在服务器端,我们往往会关注对电脑内存和CPU的使用,以求在特定业务代码下,能尽量满足高承载低响应延迟的需求。最基本的做法就是“空间换时间”,用各种缓存的方式来以求得CPU和内存空间上的平衡。另外还有一个约束:带宽。网络带宽直接限制了服务器的处理能力,所以游戏服务器架构也必定要考虑这个因素。
游戏服务器架构要素
对于游戏服务端架构,最重要的三个部分就是,如何使用CPU、内存、网卡的设计:
内存架构:主要决定服务器如何使用内存,以最大化利用服务器端内存来提高承载量,降低服务延迟。
逻辑架构:设计如何使用进程、线程、协程这些对于CPU调度的方案。选择同步、异步等不同的编程模型,以提高服务器的稳定性和承载量。可以分区分服,也可以采用世界服的方式,将相同功能模块划分到不同的服务器来处理。
通信模式:决定使用何种方式通讯。基于游戏类型不同采用不同的通信模式,比如http,tcp,udp等。
服务器演化进程
1.卡牌等休闲游戏弱交互游戏
服务器基于游戏类型不同,所采用的架构也有所不同,我们先讲一下简单的模型,采用http通信模式架构的服务器:
这种服务器架构和我们常用的web服务器架构差不多,也是采用nginx负载集群支持服务器的水平扩展,memcache做缓存。唯一不同的点在于通信层需要对协议再加工和加密,一般每个公司都有自己的一套基于http的协议层框架,很少采用开源框架。
2.长连接游戏服务器
长连接游戏和弱联网游戏不同的地方在于,长连接中,玩家是有状态的,服务器可以时时和client交互,数据的传送,不像弱联网一般每次都需要重新创建一个连接,消息传送的频率以及速度上都快于弱联网游戏。长连接网游的架构经过几代的迭代,类型也变得日益丰富,以下为世界服无缝地图服务器的特点以及架构模式。
魔兽世界中的无缝地图,想必大家印象深刻,整个世界的移动没有像以往的游戏一样,在切换场景的时候需要loading等待,而是直接行走过去,体验流畅。
现在的游戏大地图采用无缝地图多数采用的是9宫格的样式来处理,由于地图没有魔兽世纪那么大,所以采用单台服务器多进程处理即可,不过类似魔兽世界这种大世界地图,必须考虑2个问题:
1. 多个地图节点如何无缝拼接,特别是当地图节点比较多的时候,如何保证无缝拼接。
2. 如何支持动态分布,有些区域人多,有些区域人少,保证服务器资源利用的最大化。
为了解决这个问题,比较以往按照地图来切割游戏而言,无缝世界并不存在一块地图上面的人有且只由一台服务器处理了,此时需要一组服务器来处理,每台 Node服务器用来管理一块地图区域,由 NodeMaster(NM)来为他们提供总体管理。更高层次的 World则提供大陆级别的管理服务。
一个 Node所负责的区域,地理上没必要连接在一起,可以统一交给一个Node去管理,而这些区块在地理上并没有联系在一起的必要性。一个 Node到底管理哪些区块,可以根据游戏实时运行的负载情况,定时维护的时候进行更改 NodeMaster 上面的配置。
3.房间服务器(游戏大厅)
房间类玩法和MMORPG有很大的不同,在于其在线广播单元的不确定性和广播数量很小。而且需要匹配一台房间服务器让少数人进入一个服务器。
这一类游戏最重要的是其“游戏大厅”的承载量,每个“游戏房间”受逻辑所限,需要维持和广播的玩家数据是有限的,但是“游戏大厅”需要维持相当高的在线用户数,所以一般来说,这种游戏还是需要做“分服”的。典型的游戏就是《英雄联盟》、《王者荣耀》这一类游戏了。而“游戏大厅”里面最有挑战性的任务,就是“自动匹配”玩家进入一个“游戏房间”,这需要对所有在线玩家做搜索和过滤。
玩家先登录“大厅服务器”,然后选择组队游戏的功能,服务器会通知参与的所有游戏客户端,新开一条连接到房间服务器上,这样所有参与的用户就能在房间服务器里进行游戏交互了。
构建服务器框架
无论是客户端开发还是服务器开发,在做每一款游戏时,必然都会存在一个框架,而这个框架往往会适用于很多同类型的游戏。在这里我们就来介绍如何搭建服务器框架,这个框架对于MOBA类型的游戏都是适用的。
程序运行环境
操作系统:win10
开发环境:Node.js
mysql数据库:5.7.22
开发工具:VScode
程序结构
首先,我们介绍下服务器编程中常常抽象出来的几个概念(这里以tcp连接为例):
1.TcpServer 即Tcp服务,服务器需要绑定ip地址和端口号,并在该端口号上侦听客户端的连接(往往由一个成员变量TcpListener来管理侦听细节)。所以一个TcpServer要做的就是这些工作。除此之外,每当有新连接到来时,TcpServer需要接收新连接,当多个新连接存在时,TcpServer需要有条不紊地管理这些连接:连接的建立、断开等,即产生和管理下文中说的TcpConnection对象。
2.一个连接创建一个socket句柄,管理着这个连接的一些信息:如连接状态、本端和对端的ip地址和端口号等。
3. Client对象,是将socket收取的数据进行处理解包,或者对准备好的数据进行处理装包,并通过socket发送。
归纳起来:一个TcpServer依靠listener对新连接进行侦听和处理,依靠client对连接上的数据进行管理,client实际依靠socket对数据进行收发,并对对数据进行装包和解包。也就是说一个TcpServer存在一个listener,对应多个client,有几个socket就有几个client。
拿数据的发送来说:当业务逻辑将数据交给client,client将数据装好包后(装包过程后可以有一些加密或压缩操作),交给client.send(),而client.send()实际是调用socket.write()方法将数据发出去。
对于数据的接收,稍微有一点不同:通过libuv的事件处理机制,等待接收socket数据到来的通知,确定client上有数据到来后,激活该client的socket去调用read()来读取数据。数据收到以后,将数据交由server来处理,最终交给业务层。
注意:数据收取、解包乃至交给业务层是一定要分开的,最好不要把解包并交给业务层和数据收取的逻辑放在一起。因为数据收取是IO操作,而解包并交给业务层是逻辑计算操作。IO操作一般比逻辑计算要慢。到底如何安排要根据服务器业务来取舍,也就是说你要想好你的服务器程序的性能瓶颈在网络IO还是逻辑计算,即使是网络IO,也可以分为上行操作和下行操作,上行操作即客户端发数据给服务器,下行即服务器发数据给客户端。有时候数据上行少,下行大。(如游戏服务器,一个npc移动了位置,上行是该客户端通知服务器自己最新位置,而下行确是服务器要告诉在场的每个客户端)。
如上图所示,我们的中心就是Server服务端模块,其它模块都与他进行交互处理。Client客户代理模块通过TCP协议(scoket)与服务端进行通信。Protocol协议模块主要是为了制定好客户端与服务端通信的协议消息,并在服务端实现对数据的打包与解包操作,而且可以接收客户端传来的消息并进行应答。Mysql数据库模块中记录了游戏用户数据,服务端可以根据数据库中的数据对用户的信息进行验证。Common公共模块主要是提供Log日志、通用方法以及一些常量等等,方便我们进行查错,统计信息,代码复用。