BIGWORLD 服务端概述
BIGWORLD_04_Server Overview
1. 概述
BigWorld Technology是BigWorld实现大型多人在线游戏的中间件。本文概述了BigWorld技术的当前实现。
2. Rules of Thumb(经验法则)
这是设计中使用的规则/理念/哲学的列表。
- 可延展性,可靠性,效率高
- 简单化
- 改善最坏的体验
- 合理分配利用客户端/服务端带宽
- 保持信息,避免双向调用
- 避免瓶颈;使系统分布式
- 在可行的情况下,分批发送消息
3. Concepts(概念)
本节解释与设计相关的一般概念和问题
Location of an Object’s Data
对象数据的位置,一个对象的活动数据有四个主要位置:
- 与实体的单元部分相关联。
- 与实体的基本部分关联。
- 在持久世界数据库中。
- 客户端
与单元上的实体相关联的数据可以分为:
- 内部数据, 仅在其所在的cell中使用和储存。
- 服务器(或者 ghosted)数据, 对服务器上的其他实体可用。
- 客户端数据,对(至少部分)客户机可用。
3.1 Actions
从客户端的角度来看,在概念上有四种类型的行为:
1. Server action
这是一个来自服务器的未经请求的操作,不是由此客户机生成的。
例如,另一个角色跳跃。
2. Local action
这是一个只在客户端本地发生的操作,不需要与服务器或其他客户端通信。
例如,粒子弹跳或火焰轻微燃烧的特效。
3. Undoable action
这是客户端在假定正确的情况下立即采取的操作,然后与服务器通信。服务器可以禁止该操作,并让客户机回滚该操作。
例如,向前迈进的客户端。
4. Server-confirmed action
这是一个需要从服务器接收确认的操作,然后在客户端上执行。
例如,玩家想要与另一个玩家握手。另一个例子可能是击中一个玩家(并认为他已经死了),但直到服务器确认后才显示出来(这与服务器动作有一些相似之处,只是动作的来源是这个客户端)。
3.3 Latency
延迟,游戏设计师需要使用尽可能多的延迟隐藏技巧来隐藏玩家的延迟。延迟有两个主要来源:
1. 网络延迟
所有在客户端之间传输的信息都要通过服务器,因此必须在Internet上进行两次传输。
2. 服务器延迟
这是服务器接收到信息、对其进行处理以及响应回客户机之间的时间间隔。在拥有大量玩家的MMOG环境中,带宽是一种宝贵的资源,所以并不是所有的新信息都能立即发送出去。在BigWorld中,优先队列管理这个。
可以对优先队列进行调优,以减少服务器对关键信息造成的延迟。不过,这不会影响互联网延迟。
3.4 Spaces and Cells
游戏世界是由多个空间组成的。每个空间都是一个连续的欧几里得区域,由一个单一的坐标系横跨。
cell存在于更物理的层面。它们以几何形式划分大型游戏空间,以便在多个cellapp之间实现负载平衡。
对于很小的空间,一个单元格就足以覆盖。
3.5 Coordinate System
坐标系系统。BigWorld使用左手坐标系。x轴指向“左”,y轴指向“上”,z轴指向“前”。
- yaw,绕y轴的旋转。正的是右边,负的是左边.
- pitch,绕x轴的旋转。正的是向下,负的是向上.
- roll 绕z轴的旋转。正的是左边,负的是右边.
4. Design Introduction
本节讨论BigWorld Server环境的设计,简要概述其组件和一系列用例。
4.1 Hardware Components
硬件部分:
BaseApps和LoginApps是唯一需要连接到互联网的服务器组件。
下图显示了服务器硬件的不同组件之间的连接。
4.2 Software Components
软件部分,服务器的主要软件组件有:
- CellApp
- CellAppMgr
- BaseApp
- BaseAppMgr
- ServiceApp
- LoginApp
- DBMgr
- Reviver
还有一个名为BWMachined的特殊程序进程,它运行在每台机器上。
在生产环境中,每个CellApp运行在自己的CPU上,每个BaseApp和ServiceApp也是如此。
所有其他进程(除了BWMachined)可以根据系统负载在一个或多个服务器上以不同的组合运行。
只要求BaseApps和LoginApps连接到互联网。
ServiceApps和Revivers是可选的。所有其他过程都是必需的。
下图展示了多个软件组件之间的通信路径:
4.2.1 CellApp
每个CellApp负责运行在其上的所有cell。
CellApps可能是架构中最重要的部分。每个cell负责一个大空间的一部分或整个空间。在游戏世界中,单元格并不重叠,它们共同覆盖所有游戏空间。
每个cell负责维护位于其边界内的实体。一个细胞也可以维护在该实体附近的ghosts(副本),但在自己的边界之外。
4.2.2 CellAppMgr
CellAppMgr的主要职责是指导cell和Cellapp。
它协调哪些cell运行在哪些CellApps上,并通过改变cell的大小来平衡每个cell上的负载。
4.2.3 BaseApp
在某些方面,BaseApps可以被视为服务器的防火墙。
对于客户端来说,它们的主要目的是将其与CellApps之间的实体转换隔离开来。
每个BaseApp都包含许多base。
连接的客户机由称为代理的增强基础服务。每个代理最多负责一个客户端。每个客户机与一个代理通信。此代理负责将消息从客户机重定向到正确的cell。
一般来说,BaseApp维护base。基表示不需要在世界上有位置的对象或函数。例如,用于群聊的对象是一个没有相应单元实体的base。
4.2.4 BaseAppMgr
游戏世界有一个单独的BaseAppMgr在运行,它负责管理BaseApps和ServiceApps。
它的主要工作是将新的客户端连接分配给最合适的BaseApp,并跟踪它们。
4.2.5 ServiceApp
ServiceApps是支持运行服务的进程。
一个Service可以被认为是一个Base-only实体,除了它不是代表一个对象,它提供了一个游戏服务器和一些外部服务之间的接口。
一个ServiceApp是一个专门的BaseApp,它包含Service片段,而不是Base实体。一个Service可以在多个ServiceApps上有多个片段,允许该服务的功能在多台机器上可用。
ServiceApp可执行文件实际上是一个到BaseApp二进制文件的符号链接。它会导致BaseApp以不同的模式运行。默认情况下,基本实体不会在ServiceApps上创建。与BaseApps不同,ServiceApps不需要直接连接到互联网。
4.2.6 LoginApp
客户端与该组件通信以发起与服务器的会话。LoginApp然后添加player作为BaseApp的代理,BaseApp可以继续在CellApp上创建一个实体。
此组件的多个实例可以同时运行。
4.2.7 DBMgr
DBMgr是数据库的接口,其中存储了世界的持久状态。
当玩家登录时,登录过程从数据库请求该玩家实体的全部属性集。这个数据用于在BaseApp上实例化这个玩家的代理。
当玩家登出时,BaseApp向数据库发送一个登出消息。这条消息包含玩家实体的属性,可能已经被base(或cell)实体修改。
4.2.8 Reviver
Reviver是一个监视进程,用于重新启动其他失败的进程,这些失败的进程可能是因为它们运行的机器发生了故障,也可能是因为进程本身崩溃或无响应。
4.2.9 BWMachimed
BWMachined是一个运行在服务器集群中的每台机器上的守护进程。它可以用于远程启动、停止和定位服务器组件。它还监视CPU、内存和网络使用情况,并向网络用户提供这些信息。
这是服务器组件在启动期间通过广播消息相互定位的方式。
4.3 Use Cases
本节描述服务器的一些功能是如何工作的。本文的目的是对服务器中的组件以及它们之间的相互关系提供一个介绍性的视图。
4.3.1 Server Startup
关于启动的重要问题是进程之间的依赖关系,以及不同进程如何找到彼此。
当一个进程启动时,它将自己注册到本地机器上运行的守护进程。注册包含接口名称。
例如,CellApp需要知道CellAppMgr的位置,以便它可以注册到CellAppMgr。所以,当CellApp启动时,它通过在BWMachined的端口上发送广播消息来询问CellAppMgr的位置,所有BWMachined进程都监听该消息。如果一个BWMachined有一个CellAppMgr(与正确的用户关联),它将发送CellAppMgr的联系详细信息作为响应。
4.3.2 Logging In
当客户端启动时,它会与LoginApp通信。
登录流程如下:
- 客户端发送一个登录请求(需要知道LoginApp的IP地址)。
- 当监听它的固定(用户指定)端口时,LoginApp接收请求。
- LoginApp将请求转发给DBMgr,这样它就可以检查登录详细信息是否有效。
- DBMgr查询数据库关于登录的详细信息。
- 如果详细信息有效,DBMgr指示BaseAppMgr创建一个新的实体。
- BaseAppMgr将实体创建请求转发给负载最少的BaseApp。
- BaseApp创建了一个新的代理,它是一个派生自Base类的对象。
- 新创建的代理现在可以在CellApp上创建单元格实体(例如,这一步可能会被代理延迟,直到客户端选择一个字符)。当创建单元实体时,代理可以通过CellAppMgr或直接向CellApp发送消息。
- 代理的UDP端口返回给客户端(通过BaseAppMgr, DBMgr,然后LoginApp)。
具体流程:
4.3.3. Data From Clients
从客户机到服务器的所有通信都发送到登录期间发送给它的客户机代理的地址。
该通信在UDP之上使用Mercury (BigWorld通信机制)。一旦proxy接收到数据包,它就会将消息(如果不是全部,也是大部分)转发给相应的CellApp。然后,这将更新适当实体的数据。如果实体有任何ghosts,这些也会被更新。
4.3.4. Data To Clients
客户端的单元实体以10赫兹(可配置)的频率周期性地构造一个包,其中充满了关于客户端的AOI中实体的消息。然后这个包通过代理发送到客户端。
4.3.5. Ghosting
每个客户端的实体在其AoI中保持一个其他实体的优先队列。一个问题是,并非所有这些实体都可能在同一个单元上。解决这个问题的方法就是“ghosting”。
ghost是相邻单元的实体的副本。当其他实体通过它们的优先队列构造它们的更新包时,ghost拷贝包含可能需要的所有数据。如果存在一个实体位于另一个实体的AoI中的可能性,那么它必须在那个cell中有ghost。为了实现这一点,如果一个实体在cell格边界的AoI距离(或虚距离)内,BigWorld会在该cell格上创建实体的ghost。
当一个真正的实体(即主实体,非ghost实体)被更新时,它也会更新它的ghost。
4.3.6. Changing Cells
当实体移动时,它可能会离开当前所在单元格的边界。当发生这种情况时,实体被移动到相关的新单元格。然后它通知基地它的新地址,以便基地在未来可以找到它(在代理的情况下,以便代理可以继续正确地转发消息)。
4.3.7. Load Balancing
和其他机器一样,CellApp的机器能够处理的负载也有限制。为了避免一些CellApps超载,有一种机制来平衡负载。一般来说,如果单元格超载,它就会减小其大小,从而卸载一些实体。如果它的负载充足,它就会增加它的大小以控制更多的实体。
5. Server Conponents
本节描述BigWorld Technology服务器端的各种组件,以及它们如何协同工作以支持游戏环境。
5.1 CellApp
5.1.1 Cell Application and Cells
Cell Application (CellApp)是运行在CellApp机器上的实际进程或可执行文件。
BigWorld将大空间划分为多个cell,以便平均分担负载。它通常会为每个CellApp分配一个大的cell。在处理较小的空间时,比如动态创建的任务空间,BigWorld通常会为每个CellApp分配几个cell。
在BigWorld中,CellApp机器只运行BWMachined和每个CPU一个CellApp,其他什么都不运行。
5.1.2 Entities
每个实体都了解与其类型相关联的消息。CellApp将实体消息传递给适当的实体,并由它来解释它们。实现方法是让每个实体包含一个指向实体类型对象的指针。该对象具有与实体类型相关的信息,例如该类型可以处理的消息以及与之关联的脚本。
每种实体类型都有自己的脚本文件。脚本在特定实体的上下文中执行(例如:this或self),并且能够访问其他实体选择提供的数据(服务器和客户端数据),以及每个实体的基本成员(id、位置等等)。
当任何服务器或客户端数据发生更改时,更改将自动转发到相关实体。
5.1.3 Real and Ghost Entities
为了有效地更新它的客户端,每个角色在它的AoI中保持一个实体列表,这通常是一个围绕着玩家500米的轴对齐的正方形。由此产生的一个问题是,并非所有实体都可以由同一个cell控制。
一般让cell管理ghost实体。如果一个实体在cell格边界的ghost距离内,cell就会在附近的cell上创建一个实体的虚影。
引入实体这个术语来区分主表示和(导入的)ghosts。下图显示了cell1维护的所有实体
每隔一秒,一个cell就会检查它的真实实体,检查它是否应该在邻近单元格上为它们添加或删除ghost。它向邻居发送消息来添加和删除ghost。
5.1.4. Transitioning Between Spaces
在不同空间之间进行转换的最简单方法就是“pop”,即从旧空间中移除实体并在新空间中重新创建它。
一个稍微复杂一些的技术是有一个封闭的过渡区域,如电梯,这是精确地复制在两个空间。当玩家进入过渡区域时,它便会变得封闭起来,开发者便能够将玩家从旧空间中“弹出”到新空间中区域的副本中。
5.1.5. Witness Priority List
每当一个实体有一个客户端连接到它时,一个称为witness的子对象就会与该实体相关联,以便在其AoI中维护实体列表。
witness构建更新包发送给它的客户端,它必须优先发送最重要的信息。客户端要求最接近它的其他实体的位置和其他信息是最准确和最新的。
5.1.5.1. Event History
每个实体都保存其最近事件的历史。历史既包括改变其状态的事件(如武器的改变),也包括不改变状态的动作(如跳跃和射击)。历史列表中不保留影响易变数据的频繁操作(如移动)。
这些类型的事件的历史相当短(大约60秒),因为我们预计在最坏的情况下,优先队列中的每个实体将(大约)每30秒被考虑一次。
5.1.5.2 Level of Detail
当一个实体的AoI中有许多实体时,关于每个实体的信息发送的频率会降低,但事件的总数保持不变。
为了解决这个问题,BigWorld使用详细级别(LOD)系统来处理基于事件的更改。它的原则是,一个实体离角色越近,它就应该向它的客户端发送越多的细节。例如,当玩家最初进入角色的AoI时,角色不需要知道其他玩家的所有细节。可见的细节可能只在100米以内才需要。更精致的细节,比如衣服上的徽章,可能只需要在20米以内。
• Non-state-changing events
在每个实体类型的描述中,都有它可以生成的所有消息(非状态改变事件)的描述,以及与每个消息相关联的优先级。
当角色添加关于实体的信息时,它只添加优先级大于角色当前对该实体的兴趣的消息。因此,根据化身和实体之间的距离,消息将被忽略。
例如,可能有一种聊天只能在50米内听到,或者有一种跳跃只能在100米内看到。
• State-changing events
BigWorld不能忽略发生在范围之外的状态更改事件,因为如果实体随后足够接近,客户端将拥有一个错误的实体状态副本。
例如,客户端可能对一个实体携带的武器类型感兴趣,但只有当它在100米以内时才会感兴趣。如果实体在这个范围之外改变了它的武器,那么这个信息还没有发送给客户端。BigWorld只会在实体进入范围时发送它。
当一个具有关联见证的实体出现在另一个实体的LOD环内时,与该环相关联的任何状态(自上次如此接近以来发生了变化)都会被发送给客户端。BigWorld使用时间戳来实现这一点。时间戳与每个实体的每个属性一起存储。此时间戳表示该属性最后一次更改的时间。
5.1.6. Scripting and Entities
下面的小节描述了如何为实体使用脚本
5.1.6.1 Entity Classes
实体类描述了一种实体类型。下面的列表描述了它的组成部分:
• An XML definition file(定义文件)
<res>/scripts/entity_defs/<entity>.def
• A Python cell script(cell脚本)
<res>/scripts/cell/<entity>.py
• A Python base script(base脚本)
<res>/scripts/base/<entity>.py
• A Python client script(客户端脚本)
XML定义文件可以被视为Cell、Base和Client脚本之间的接口。它定义了所有可用的公共方法和实体的所有属性。此外,它还定义了类的全局特征,例如:
- 实体是否需要不稳定的位置更新。
- 实体如何在客户端实例化。
- 该实体的基类。
- 实体实现的接口,如果有的话。
5.1.6.2. Example Entity Definition File
示例参考:实体定义文件<res>/scripts/entity_defs/Seat.def
5.1.6.3. Properties
需要定义所有单元实体属性,即使它们是类的私有属性。这是因为CellApps需要将实体卸载给其他CellApps,并且定义所有属性的数据类型允许数据尽可能有效地传输。
在XML定义中使用以下标志来确定应该如何复制每个属性:
• ALL_CLIENTS
表示CELL_PUBLIC标志
只与有关联客户端的实体相关,例如玩家实体
对附近的所有客户(包括所有者)可见的属性。对应于设置
OWN_CLIENT和OTHER_CLIENT标志。
• BASE
在Base上使用的属性
• BASE_AND_CLIENT
在Base和clients上都使用的属性。客户机在登录时接收该属性
• CELL_PRIVATE
包含实体内部状态信息的属性。它们对所有者是可用的,而且只在cell上可用。它们不会被ghost,因此,其他cell上的实体将无法看到它们。
• CELL_PUBLIC
对服务器上的其他实体可见的属性。它们将被“ghost”,而附近的任何其他实体将能够读取它们,无论它们是否在同一个cell中(只要它们在AoI距离内)。
• CELL_PUBLIC_AND_OWN
cell上的其他实体以及cell和client上的这个实体可用的属性。
• OTHER_CLIENTS
表示CELL_PUBLIC标志。
对附近的客户端可见,但对所有者不可见的属性。
• OWN_CLIENT
只与有关联客户端的实体相关,例如玩家实体。
仅对拥有该实体的客户端可见的属性。
当属性发生变化时,服务器使用标志OWN_CLIENT和ALL_CLIENTS来确定是否应该将更新发送到实体自己的客户端,并使用标志OTHER_CLIENTS和ALL_CLIENTS来确定是否应该将更新发送到其他客户端。
5.1.6.4 Built-in Properties
除了XML文件中定义的属性之外,单元实体脚本还可以访问单元脚本环境提供的几个内置属性。这些包括:
• id (Read-only)
整数实体ID。
• spaceID (Read-only)
实体所在空间ID。
• vehicle (Read-only)
该实体所依赖的其他实体(或None)。
• position (Read/write)
实体的位置,作为3个浮点数的元组。
• direction (Read/write)
实体的方向,作为x、y和z的元组
• isOnGround (Read/write)
整型标志,如果实体在地上,值为True,否则为False。
可以通过改变位置属性来移动实体。但是,对于连续移动,建议使用movetoppoint方法。spaceID和vehicle分别受到teleport和boardVehicle方法的影响。
5.1.6.5 Methods
与属性一样,方法也在实体类型的XML定义文件中定义(命名为<res>/scripts/ entity_defs/<entity>.def)。客户端方法、cell方法和base方法有不同的部分,每个方法都包含它的名称和参数列表。
单元格和基本方法也可以有一个可选的
方法定义还可以具有一个
5.1.6.6 Calling Clinet methods
客户端引用其AoI中的所有实体。实体的cell组件可以调用存在于此AoI中的其他客户端实体(包括它自己的)的方法。
cell脚本有四个属性可用,以方便调用客户端方法。这些都是:
- ownClient
- otherClients
- allClients
- clientEntity
对这些对象之一调用的方法将被交付给指定的客户端。
例如,为这个对象在客户端脚本中调用chat方法,针对所有附近的客户端:
1 | self.allClients.chat( "hi!" ) |
5.1.6.7 Built Methods
cell脚本可以访问许多内置的脚本方法,其中一些在下面的列表中描述:
- destroy — 删除实体。
- addTimer — 添加异步计时器回调。
- moveToPoint — 将实体沿直线移动到一个点。
- moveToEntity — 将实体移向另一个实体。
- navigate — 将实体移向一个点,避开障碍。
- cancel — 取消控制器(计时器,移动等)。
- entitiesInRange — 查找与实体给定距离内的实体。
5.1.6.8 Controllers
控制器是一个与单元实体相关联的c++对象。它通常执行的任务是低效的或难以通过脚本完成的。它通常也有状态信息,如果实体被卸载到另一个cell,这些信息必须通过网络发送。
目前实现的控制器的例子有:
- TimerController — 提供异步定时回调。
- MoveToPointController — 以给定的速度将实体移动到一个位置。
由于控制器通常异步执行操作,它们通过调用一个命名的脚本回调来通知脚本对象完成操作。例如,TimerController总是调用onTimer事件处理程序,而MoveToPointController总是调用onMove事件处理程序。
5.1.6.9. Inheritance
一个实体类可以从另一个实体类派生。它接收基类的所有属性和方法,并添加自己的接口。.def文件中的<Implements>节可用于描述多个接口。
5.1.6.10. Example Script File
下面是Seat对象的示例单元实体脚本文件:
1 | "This module implements the Seat entity." |
5.1.7 Directed Messages
并非所有事件都通过事件历史记录进行传播。如果某一事件是针对特定玩家或少数玩家,那么它便能够立即通过下一个包传达给玩家。
5.1.8 Forwarding From Ghosts
每个“ghost”都知道它真正的cell,所以它可以把信息转发给它真正的cell进行处理。
消息分为两种形式:
- 拉取式,大多数消息都是拉取式,比如当另一名玩家跳跃时——这是由角色(通过优先队列)决定是否以及何时发送给它的客户端。
- 推送式,少数信息是推送类型,如当玩家A想要与玩家握手时。
5.1.9. Offloading Entities
每个单元大约每秒钟检查一次是否将任何实体卸载给其他单元。根据当前单元格的边界来考虑每个实体。边界被人为地增加(约10米),以避免滞后。如果发现一个实体在当前单元格的边界之外,它将被卸载到最合适的单元格。
5.1.10. Adding and Removing Cells
当一个cell在运行时被添加到一个大的多cell空间时,它是通过逐渐增加其面积来完成的,以避免试图改变单元的实体出现大的峰值。同样地,移除一个cell时,它的面积会慢慢减少,直到所有实体都消失。
5.1.11. Load Balancing
移动计算单元边界以调整单个计算单元负责的服务器负载比例。
基本的(简化的)想法是,如果一个cell与其他cell相比超载,那么它的面积就会减少。相反,如果它负载充足,那么它的面积就会增加。
5.1.12. Physics
BigWorld只在客户端实现了实体到实体的碰撞检测系统,当客户端试图占用相同的空间时,它们会被撞到一边。当碰撞严重时,可以将此扩展到向服务器发送消息,例如,服务器可以检查情况并可能改变每个角色的位置/速度。
5.1.13. Navigation System
导航系统的主要特点是:
- 分为室内和室外chunks
- 室内外无缝切换。
- navpoly图的动态加载。
- 路径缓存,提高效率。
5.1.14. Range Triggers and Range Queries
当(特定类型的)实体与另一个足够接近时,通常需要触发事件。
这些被称为范围触发器(Range Triggers)。
我们还需要找到特定区域中的所有实体。这被称为范围查询(Range Queries)。
为了支持这些功能,每个cell维护其所有实体的两个列表,按X和Z排序。
要执行范围查询,软件只搜索这些列表中的一个,直到查询范围的距离,然后几何地选择范围内的实体。
要执行范围触发器,对特定范围感兴趣的实体将在X和Z列表中插入高触发器和低触发器。当实体移动时,列表和触发器将被更新。当其他实体移动时,列表将被更新。如果一个实体跨越了一个触发器,或者一个触发器跨越了一个实体,那么软件将检查其他维度,以测试它是否真的是一个触发器事件。
5.1.15. Fault Tolerance
CellApp上的每个实体都会定期备份到其基本实体。如果CellApp不可用,CellAppMgr确保每个空间仍然有至少一个cell。基实体检测它们的单元实体是否丢失,并将它们恢复到它们空间的另一个单元。脚本方法可以确保恢复的实体与游戏世界保持一致。
5.2 CellAppMgr
CellAppMgr主要负责:
- 维护Cell之间的正确邻接。
- 向正确的cell中添加新的与角色相关联的实体。
- 充当全局实体ID代理。
5.2.1 CellApp Registration
当一个新的CellApp启动时,它通过向所有BWMachined daemon广播一条消息来找到CellAppMgr的位置。
类似地,当CellApp停止时,它会在CellAppMgr中注销自己,并逐渐删除所有的cell
5.2.2 Load Balancing
BigWorld动态平衡所有空间的单元负载,从小的单cell空间到潜在的大的多cell空间。
一个单元所覆盖的欧氏空间的大小是由它的CellApp所能支持的任何负载所决定的。这可能因CPU或实体密度而异。
5.2.3 Adding and Removing Cells
CellAppMgr的工作是根据CellApps当前的负载报告和它们正在管理的空间区域来划分空间。CellAppMgr使用这些信息来决定CellApp是否应该调整其职责范围,或者创建一个新的单元或删除一个现有的单元。无论结果如何,它都会与CellApp通信。
5.2.4 Adding an Entity
CellAppMgr可以很容易地确定向哪个单元格添加新实体,因为它控制空间的镶嵌。
然而,一个新的单元实体通常会绕过CellAppMgr(从而减少它的负载),如果它知道在同一空间中有另一个实体,就在它想要创建的地方附近。在这种情况下,它将简单地在现有实体的同一个CellApp上创建自己。
5.2.5 Load Balancing for Multiple Spaces
在多个空间的情况下,BigWorld使用一种最优拟合的单一算法,从大到小,动态地、连续地平衡它们的负载。
该算法的基本目标是为每个空间分配足够的处理能力,并将每个处理分配到尽可能少的CellApps(机器)。如果一个空间必须分布在多台机器上,则使用一种算法将该空间分解为多个单元
5.2.6 Fault Tolerance
如果失败,一个Reviver将重新启动CellAppMgr。它会找到所有的cellapp,并向它们查询它们的细胞和它们所覆盖的区域。
从这一点开始,它可以继续维护cell、CellApps和空间,并能够向正确的cell格添加新实体。通过从存储的检查点或cell维护的ID范围恢复数据,它可以继续扮演空间ID和实体ID代理的角色。
5.3 BaseApp
BaseApp管理实体bases和代理。代理是实体基础的专门化。
5.3.1 Proxies
当一个玩家登录到服务器上时,一个代理在一个BaseApp上创建,它可能决定在CellApp上的一个单元中创建一个实体。BaseAppMgr在最少加载的BaseApp上创建代理。
当客户端希望向cell上的实体发送消息时,它首先将消息发送给proxies,然后proxies将消息转发给cell。这将客户端与cell开关隔离开来。为了实现这一点,proxies需要知道它的关联实体所在的CellApp的地址。每当cell实体更改CellApps时,它都会与proxies通信。
单元实体还通过关联的代理与其客户端通信。此方法的主要好处是允许代理负责处理消息可靠性。其他好处包括屏蔽CellApps直接与互联网通话,以及限制客户端接收数据的地址。
代理还可以聚合来自多个源的消息,并将它们作为一个包传递给客户机。
5.3.2 Bases
Base具有以下特点:
Base可以存储不需要在cell上移动的实体属性–一个很好的例子是在base上存储玩家的库存,在cell上存储活动物品。
当cell实体(或任何其他服务器对象)不知道实体当前在哪个cell上时,它可以与base实体通信。base充当锚点。
一些属性被更有效地保存在base上,因为它们被“远程”对象访问(或订阅)。
团队聊天系统就是一个很好的例子。
base可以是一个没有位置感的实体,例如控制世界事件的实体。
代理是一种特殊类型的base。它有一个相关联的客户端,并控制进出该客户端的消息。
与cell上的实体一样,base也有一个与之相关联的脚本。因此,可以只在脚本中实现不同的base类型。一个典型的例子是群聊系统。
5.3.3 Fault Tolerance
BaseApp支持一个容错方案,每个BaseApp都将自己的实体备份到其他(不止一个)常规BaseApp上。
每个BaseApp都通过许多其他真正的BaseApp来备份它的实体。每个BaseApp都被分配一组其他的BaseApp和一个从实体ID到这些备份的哈希函数。
如果一个BaseApp死亡,它的每个实体都会在备份它的BaseApp上恢复。
此外,任何附加的客户机将在意外的BaseApp故障时断开连接,需要重新登录。
但是,如果BaseApp已经撤离,那么附加的客户端会迁移到另一个运行的BaseApp上。
5.3.4 Secondary Databases
为了减少主数据库和DBMgr上的负载,BaseApp将持久数据临时存储在本地磁盘上的辅助数据库中。当实体处于活动状态时,对实体持久数据的更改只存储在辅助数据库中。当实体被卸载时,它的持久数据将从辅助数据库传输到主数据库。
5.4 BaseAppMgr
5.4.1 Implementation
当一个BaseApp启动时,它将自己注册到BaseAppMgr中。BaseAppMgr维护所有BaseApps的列表。
5.4.2 Logging In
当客户端登录时,LoginApp向BaseAppMgr发出一个请求(通过DBMgr),以向系统添加一个播放器。然后BaseAppMgr将一个代理添加到最少加载的BaseApp中。BaseAppMgr将代理的IP地址发送给LoginApp。它被发送给客户端,然后客户端使用它与服务器进行所有未来的通信。
5.4.3 Fault Tolerance
在失败的情况下,一个Reviver会重启BaseAppMgr。BaseApps重新注册到BaseAppMgr,允许它正常继续。
5.5 ServiceApp
ServiceApp是一个专用的BaseApp,用于运行服务而不是Base实体。
分离服务和基础实体的处理在BaseApps在ServiceApp崩溃的情况下完全不受影响。
ServiceApp可执行文件实际上是一个连接到BaseApp二进制文件。它只是让BaseApp以不同的模式运行。默认情况下,基本实体不会在ServiceApps上创建。
5.5.1 Services
一个service是一个脚本化的对象,类似于Base-only实体。与基本实体不同,它们没有属性。
5.5.2 Examples of Services
服务是实现功能的理想选择,否则这些功能可能由单一的基础实体实现。通常,它们用于将服务器与外部服务集成。例如,服务可以创建为:
- 为服务器提供HTTP web服务接口。这将允许外部服务(如网站)能够调用游戏实体。
- 允许游戏脚本访问外部web服务
- 允许游戏脚本访问外部数据库
5.5.3 Starting services
默认情况下,每个ServiceApp上都启动一个Service片段。也就是说,每个服务自动分布在所有可能的ServiceApps上。
5.6. LoginApp
5.6.1. Implementation
每个LoginApp侦听一个固定的(可配置的)端口,用于从客户端登录请求。
登录细节查看4.3.2 Logging in
LoginApp发出的所有请求都是非阻塞的,因此它可以处理许多同时进行的登录。
5.6.2 Multiple LoginApps
就负载而言,一个LoginApp应该足以满足大多数目的。然而,由于这仍然是一个潜在的瓶颈和单点故障,BigWorld支持运行多个LoginApps。
剩下的问题是如何跨多个登录服务器分发客户机登录请求。标准的方法是使用DNS解决方案,类似于流行的网站在多台机器上平衡流量负载的方式。
5.7 DBMgr
DBMgr是主数据库的接口。目前有两种数据库接口组件的实现:
- XML
- MySQL
还可以添加其他参数。
5.7.1 XML
XML实现将数据保存在单个XML文件中—
在这个实现中,DBMgr在启动时读取XML文件,并将所有的球员描述缓存到内存中。在关闭之前不会执行磁盘I/O,关闭时将整个XML文件写入磁盘。
XML数据库有几个已知的限制,使得它不适合生产环境或重度的开发环境。
5.7.2 MySQL
MySQL实现通过本地MySQL接口与MySQL数据库通信。MySQL可以运行在同一台机器上,也可以运行在不同的机器上,因为该协议是通过TCP运行的。
实体的每个类都存储在一个单独的SQL表中,类的每个属性都是一个列。这些表由 DatabaseIDs建立索引。
在启动时,将根据XML实体定义文件(命名为
如果任何实体类没有数据库表,那么将自动创建一个数据库表。如果XML文件中有任何类的新属性不在数据库中,那么列将自动添加到适当的表中。这个功能在开发过程中非常有用,因为在开发过程中模式可能会频繁变化。
5.7.3 Fault Tolerance
在失败的情况下,DBMgr可以通过Reviver重新启动,它可能在不同的物理机器上。一旦DBMgr重新启动,它就会通过BWMachined发布它的存在,然后所有感兴趣的参与者将开始与它通信。
XML数据库不具有容错性,因为所有数据在关闭之前都保存在RAM中。
5.8 Reviver
Reviver是一个监控进程,用于重新启动其他失败的进程。Reviver在为容错目的而保留的机器上启动,并等待进程将自己附加到它。
使用Reviver的进程包括:
- CellAppMgr
- BaseAppMgr
- DBMgr
- LoginApp
当每个进程启动时,它会在网络中搜索一个Reviver,然后附加它自己-如果它是由Reviver支持的(这个配置是通过/etc/bwmachine .conf完成的)
Reviver会定期ping这个进程,检查它是否可用——如果它没有回复,Reviver会作为失败的进程重新启动自己,替换进程和机器。然后可以关闭故障机器/进程以进行服务。
由于Revivers通常会在恢复进程后停止运行,所以通常需要几个(最好是在不同的机器上)。Revivers使用BWMachined来启动新进程。
如果一个Reviver崩溃了,那么请求监控的进程将检测到丢失的ping,并寻找新的Reviver。由操作员来确保在适当的机器上运行足够的Revivers。
5.9 BWMachined
BWMachined进程在服务器集群中的每台机器上运行,并在启动时自动启动。其职责是:
- 启动和停止服务器组件。
- 定位服务器组件。
- 提供机器统计数据(CPU速度,网络统计等)。
- 提供进程统计信息(内存和CPU使用率)。
下面的小节描述了每一项职责。
5.9.1. Start and Stop Server Components
BWMachined可以使用任何用户ID启动组件,也可以指定应该使用哪个构建配置(Debug、Hybrid或Release)。
因为不是所有用户都有自己的服务器构建,所以可以根据每个用户指定要使用的服务器二进制文件的位置。这可以在$HOME/.bwmachined.conf文件中完成,也可以在/etc/bwmachined.conf文件中完成。
5.9.2. Locate Server Components
当一个服务器组件启动时,它可以选择公开它的Mercury接口。如果是这样,Mercury将向本地机器上的BWMachined发送一个注册包。
下面的列表描述了数据包中包含的字段:
- UID - User ID
- PID - Process ID
- Name - Name(例如: CellAppMgr)
- ID - 可选的唯一ID
BWMachined会将该进程添加到它的注册进程列表中。它每秒钟轮询/proc文件系统,以确定每个进程的CPU和内存使用情况。
当一个公开的服务器组件关闭时,它必须向本地BWMachined发送一个注销报文。如果一个进程在没有注销注册的情况下死亡,BWMachined会发现这一点,当它下次轮询和更新它的已知组件列表。
可以通过发送一个查询包到BWMachined来搜索匹配某些字段的服务器组件。它将通过对每个匹配过程发送一个响应包来进行响应。
例如,当一个CellApp启动时,它需要找到当前UID下运行的CellAppMgr进程的地址。它向网络上的每个BWMachined发送一个广播查询,请求所有与当前UID匹配的进程,以及名称“CellAppMgr”。
5.9.3. Provide Machine Statistics
在启动时,BWMachined检查/proc/cpuinfo文件,以确定cpu的数量和它们的速度。它还定期监控整个机器的CPU、内存和网络使用情况。这些信息可以通过UDP请求获得,并显示在WebConsole的StatGrapher组件中。
5.9.4. Provide Process Statistics
BWMachined每秒轮询/proc文件系统,并收集所有注册进程的内存和CPU统计信息。这些信息可以通过UDP请求获得,并显示在WebConsole的StatGrapher组件中。
5.9.5. BWMachined Interface Discovery
为了帮助对错误配置的缺省广播地址进行错误检测,减少所需的配置量,BWMachined会在启动时确定哪个接口被配置为缺省广播路由。
如果bw.xml配置文件中没有定义<internalInterface>标记,则服务器组件将通过本地主机接口查询BWMachined,并请求该接口作为内部接口使用。
如果<monitoringInterface> tag在bw.xml中未定义,,然后MessageLogger将向BWMachined查询内部接口,而任何服务器组件将恢复使用它们在启动期间已经发现的内部接口来监视接口。
<internalInterface>标记已弃用,虽然它们的行为仍与以前的版本一致,但建议不要使用它们。
6. Other Features
6.1. IDs
无论何时CellApp创建一个新的实体,它都需要一个唯一的实体ID,需要一个ID代理或签发服务。CellAppMgr目前提供了这个功能。
6.1.1. ID Allocation
CellAppMgr目前充当ID的中央ID代理。它处理一个id块的请求,并允许将未使用的id返回“回收”。
如果CellAppMgr在崩溃后由Reviver重新启动,那么它将通过查询单元当前使用的最高ID来恢复其状态。序列将从最高的ID重新开始,添加一个。在失败的情况下,少量的id可能会被浪费。
多余的未使用id存储在特殊的表bigworldNewID和bigworldUsedIDs中。
6.2. Inter-Process Communication (Mercury)
6.2.1. Overview
Mercury是用于客户端和服务器之间以及所有服务器组件之间通信的网络层。它基于UDP,允许可靠和不可靠的通信。
6.2.2 Nub
Nub是Mercury的核心。它负责发送和接收数据包,发送时间消息,和通用的socket通知。
BigWorld所有的通信使用UDP协议,因此对所有的连接使用一个单一的的socket,这远比对每个连接使用一个socket高效,在大量的socket上调用select是很缓慢的。
Nub通常控制应用程序的事件循环,因此它也提供一个时间队列和用户套接字通知。时间队列用于周期性的调用回调函数,或者在时间结束后调用一个单一的回调。当用户套接字准备好读取数据时,能用于监听和调用回调函数。
6.2.3. Messages
消息(Message)是通信的基本单元。它包括:
- 消息类型 …. 1byte.
- 消息大小 …. 0-4 bytes.
- 消息数据 …. 可变大小.
对于固定长度的消息,不需要消息大小。消息数据只是由客户机和服务器解释的字节流(stream of bytes)。为方便编组,大多数数据类型都提供了流操作符。
6.2.4. Requests
一个请求是一个期望应答的消息。可以发出一个请求给客户端,并在没有阻塞进程运行的情况下接收一个和它相关联的回复。本质上,Mercury将分配一个唯一的请求ID,用于和请求的回复关联。当请求发出,一个回复的处理也会被创建。它也处理超时,如果没有在定义好的时间内收到应答会调用一个超时回调。
6.2.5. Bundles
Bundle是作为一个单元被发送和接收的消息的集合。把多个消息组合起成一个单一的数据包减少了UDP的包头开销(overhead)。如果一个bundle超过了包的最大尺寸,它将被分割成多个包,在它被接收到时重新装配。
每个在bundle中的消息包都可选额外数据,包括队列数量,请求ID和确认ID。
6.2.6. Channels
通道用于提供两个Nub之间的可靠通信。通道可能有不同的特征,这取决于它是客户机/服务器通道还是服务器/服务器通道,以及它连接的组件之间。
下表描述了一些通道的特征:
Channel | Latency | Bandwidth | Loss |
---|---|---|---|
Client/server | High | Low | High |
Server/server | Low | High | Low |
一个通道的特性可以在创建时指定,这样可以正确的选择它的可靠算法。
如果数据包中的消息被标记为可靠,那么整个数据包也会被视为可靠。发送者会给它分配一个队列数字并存储到滑动窗口。一旦接收者收到数据包会马上确认,会放置一个ACK到下一个发出的数据包。通道会被假定为定期的双向通信(10Hz for client/server, 50Hz for server/server)不会立即发送ACK,而其他没有常规通信的信道会立即发送ACK。
如果接收到一个乱序的ACK包,它会假定前序包丢失了,会重新发送。在低延迟的情况下,存在一个阈值,在一个确定的时间内哪些包在它们第一次发送后将不会被重新发送。
低带宽(low‐bandwidth)的连接中(client/server),所有不可靠的消息在重新发送之前都会被从数据包剥离,为了节省带宽,如果可能,这个数据包会携带下一个准备发出的数据包。
6.2.7. Interfaces
从Nub接收的消息会被单个接口处理。因为消息类型是单一的字节,一个接口的限制能达到256个消息(实际上会稍微小一点,因为Mercury保留了一些内部使用信息)。
接口通常被使用宏定义在 src/lib/network/interface_minder.hpp。这样的设计是为了隐藏消息解码的调用细节和调度(dispatching)。对于简单的固定长度消息,结构包含了消息参数,结果是一个对象的方法被调用。对于更复杂的可变长度消息,参须必须手动解析字节流,结果是一个stream对象和方法被调用。
接口也定义消息是否是可变的还是固定长度的。对于固定长度消息,不需要发送长度数据,因为接收者知道有多少字节。
6.3. Fault Tolerance and Disaster Recovery
服务器的每个组件都被设计为容错。任何服务器进程或机器都可能意外死亡,服务器应该继续运行,而不会受到什么影响。
BigWorld服务器还提供了第二级容错功能,称为灾难恢复。可以定期将服务器的状态写入数据库。在整个服务器故障的情况下,可以使用此信息重新启动服务器。
6.4. Packed Files
XML文件因为它的灵活性和易用性而被引擎使用,XML文件也被广泛认为很臃肿,当在一个电脑游戏中使用,很容易被终端用户修改。文件打包是把XML文件转换为压缩的二进制格式。当需要一个XML文件时,客户端和所有的服务端组件都能加载被打包文件。使用打包文件取代XML文件提高了性能,这在一定程度上的混淆也可以阻止一般用户修改文件内容。
打包文件是只读的并不适用于开发中。因为它的只读属性,大多数工具(无论是在客户端还是服务端)无法使用打包文件。