环境准备
$ mkdir mmorpg && cd rpg
$ pomelo ini
$ npm-install.bat
服务器类型
游戏采用分布式设计,服务端是由一个服务器集群所组成。
序号 | 服务器类型 | 名称 | 数量 |
---|---|---|---|
1 | 网关服务器 | gate | 1 |
2 | 连接服务器 | connector | 1 |
3 | 聊天服务器 | chat | 1 |
4 | 认证服务器 | auth | 1 |
5 | 场景服务器 | area | n |
6 | 寻路服务器 | path | 1 |
7 | 管理服务器 | manager | 1 |
网关服务器gate
- 网关服务器为用户提供统一的
websocket
入口 - 网关服务器会向所有客户端暴露一个固定的
websocket
接口 - 网关服务器负责用户验证和连接服务器的分配
- 网关服务器一般只有一台
当用户登录时会首先连接网关服务器完成验证并获得由网关服务器分配的对应连接服务器的地址,之后客户端会断开与网关服务器的连接,通过获取的地址连接对应的连接服务器以获取对应的服务。
连接服务器 connector
与web短连接模式不同的是,网络游戏中客户端和服务器建立的都是长连接,长连接本身需要一定的资源来维持。
本游戏中使用websocket协议在客户端和服务器之间建立连接,连接服务器是用来维护这些连接,并中转客户端和服务器之间的消息。
本游戏中客户端和服务器的连接是通过一个抽象的会话来维护的,会话是一个客户端在服务端的标识,用来维护用户的登录状态、用户的基本信息、用户的websocket连接信息等。
认证服务器auth
- 认证服务器负责用户注册和验证,作为用户验证的统一入口。
- 认证服务器提供远程调用接口供其它服务器调用来进行用户身份验证
- 认证服务器的主要作用是屏蔽认证验证的细节为其它服务器提供统一的验证接口
聊天服务器 chat
- 聊天服务器是网游的基本服务之一,通过独立的服务器实现。
- 聊天服务器维护一份所有在线用户的数据,通过这些数据与连接服务器通讯,实现用户之间的即时通讯。
场景服务器 area
- 网游中处于性能和负载考量,大的游戏世界会被划分为多个区域即场景。
- 本游戏中一张地图即一个游戏场景与一台独立的场景服务器对应
- 场景是构成游戏世界的基本单位,不能进行分隔和合并扩展。
- 场景服务器负责维护场景中所有实体并驱动实体AI运行游戏逻辑
- 场景服务器负责处理游戏中几乎所有的逻辑同时为其它服务器提供操纵场景数据的接口
- 虽然场景本身不可分隔但可通过加入新的场景的方式来分散用户从而提高游戏服务器总体负载
- 一些与场景相关的服务通过独立运行的方式进行水平扩展
寻路服务器path
- 寻路服务器是游戏服务器基本服务之一
- 玩家跑动、怪物移动都需要寻路服务器提供支持
- 寻路服务器是根据地图的起点和重点得到两点之间的最优路径
- 由于寻路是典型的无状态、计算密集型服务,因此将寻路与场景逻辑分离,放在单独的服务器中。从而减轻场景服务器的压力。
- 寻路服务器可根据简单并行进行扩展
- 寻路算法使用AI实现并提供通用的计算接口,并封装为一个模块。
管理服务器manager
管理服务器是后端服务器集群中负责全局管理副本全生命周期和组队相关操作的功能服务器。
组队功能模块team
组队功能是玩家之间互动的一种方式,玩家可创建队伍并邀请其它玩家加入,其它玩家也可以主动向队长踢出申请加入队伍,队伍的人数上限为3。
teamHandler.js
为协议入口模块,负责负责队伍相关操作的前期判断和后期通知。teamHandler
完成前期判断后通过一个rpc将操作所需的参数传递给manager服务器。队伍对象的管理工作是由manager服务器所持有一个全局的teamManager.js
模块负责的,teamManager
中管理所有的team对象的创建、更改、销毁等操作。
队伍id从1开始递增,所有场景中的队伍使用统一的id序列,当manager服务器重启时,队伍id也重新初始化为1.
team.js
模块中维护一个队伍对象中的所有成员与成员身份,维护一个队伍频道来通知各个成员队伍相关的消息及进行队伍内的聊天。
同一个队伍中的玩家可以进入同一个组队副本。
副本功能模块instance
副本instance.js
本质上是一个临时的场景,并对进入该临时场景的玩家进行限制。instance由模块instancePool.js
来统一管理,这与队伍模块的结构是相同的。
目前有两种副本可供玩家进入分别是单人副本、组队副本。当队伍中有两个以上玩家时,队长点击进入组队副本时,队伍中的队员会同时拉入副本中,队伍中的队员也可以单独进入组队副本,但不会触发将队伍中的其它成员拉入副本的操作,其它成员可以分别进入该副本。
玩家进入副本也被视为场景切换,在areaService.js
模块中changeArea函数中进行目标场景类型判断,如果是普通场景则正常切换。如果是副本场景则通过一个rpc来创建副本。组队副本创建时的id与队伍id相关。单人副本创建时的id与玩家id相关。组队副本创建成功后,如果是队长触发的操作则向其它队员客户都拿发送进入副本的命令,如果是队员则直接进入副本中。
服务器配置
配置游戏服务器类型
$ vim game-server/config/adminServer.json
[
{
"type": "gate",
"token": "agarxhqb98rpajloaxn34ga8xrunpagkjwlaw3ruxnpaagl29w4rxn"
},
{
"type": "connector",
"token": "agarxhqb98rpajloaxn34ga8xrunpagkjwlaw3ruxnpaagl29w4rxn"
},
{
"type": "chat",
"token": "agarxhqb98rpajloaxn34ga8xrunpagkjwlaw3ruxnpaagl29w4rxn"
},
{
"type": "auth",
"token": "agarxhqb98rpajloaxn34ga8xrunpagkjwlaw3ruxnpaagl29w4rxn"
},
{
"type": "area",
"token": "agarxhqb98rpajloaxn34ga8xrunpagkjwlaw3ruxnpaagl29w4rxn"
},
{
"type": "path",
"token": "agarxhqb98rpajloaxn34ga8xrunpagkjwlaw3ruxnpaagl29w4rxn"
},
{
"type": "manager",
"token": "agarxhqb98rpajloaxn34ga8xrunpagkjwlaw3ruxnpaagl29w4rxn"
}
]
配置不同类型服务器的参数
$ vim game-server/config/servers.json
{
"development":{
"gate": [
{
"id": "gate-server-1",
"host": "127.0.0.1",
"clientPort": 3014,
"frontend": true
}
],
"connector": [
{
"id": "connector-server-1",
"host": "127.0.0.1",
"port": 3150,
"clientHost": "127.0.0.1",
"clientPort": 3010,
"frontend": true
},
{
"id": "connector-server-2",
"host": "127.0.0.1",
"port": 3151,
"clientHost": "127.0.0.1",
"clientPort": 3011,
"frontend": true
}
],
"area": [
{
"id": "area-server-1",
"host": "127.0.0.1",
"port": 3250,
"area": 1
},
{
"id": "area-server-2",
"host": "127.0.0.1",
"port": 3251,
"area": 2
},
{
"id": "area-server-3",
"host": "127.0.0.1",
"port": 3252,
"area": 3
},
{
"id": "instance-server-1",
"host": "127.0.0.1",
"port": 3260,
"instance": true
},
{
"id": "instance-server-2",
"host": "127.0.0.1",
"port": 3261,
"instance": true
},
{
"id": "instance-server-3",
"host": "127.0.0.1",
"port": 3262,
"instance": true
}
],
"chat": [
{
"id": "chat-server-1",
"host": "127.0.0.1",
"port": 3450
}
],
"path": [
{
"id": "path-server-1",
"host": "127.0.0.1",
"port": 3550
}
],
"auth": [
{
"id": "auth-server-1",
"host": "127.0.0.1",
"port": 3650
}
],
"manager": [
{
"id": "manager-server-1",
"host": "127.0.0.1",
"port": 3750
}
]
},
"production":{}
}
序号 | 服务器类型 | 名称 | 数量 | rpc ip | rpc port | client ip | client port |
---|---|---|---|---|---|---|---|
1 | 网关服务器 | gate | 1 | - | - | 127.0.0.1 | 3014 |
2 | 连接服务器 | connector | 2 | 127.0.0.1 | 315x | 127.0.0.1 | 301x |
3 | 场景服务器 | area | 6 | 127.0.0.1 | 325x | - | - |
4 | 聊天服务器 | chat | 1 | 127.0.0.1 | 3450 | - | - |
5 | 寻路服务器 | path | 1 | 127.0.0.1 | 3550 | - | - |
6 | 认证服务器 | auth | 1 | 127.0.0.1 | 3650 | - | - |
7 | 管理服务器 | manager | 1 | 127.0.0.1 | 3750 | - | - |
其中场景服务器又分为两种类型分别是普通的场景服务器和副本服务器
序号 | 服务器类型 | 服务器名称 | 是否副本 |
---|---|---|---|
1 | 场景服务器 | area-server-1 | 否 |
2 | 场景服务器 | area-server-2 | 否 |
3 | 场景服务器 | area-server-3 | 否 |
4 | 副本服务器 | instance-server-1 | 是 |
5 | 副本服务器 | instance-server-2 | 是 |
6 | 副本服务器 | instance-server-3 | 是 |
启动流程
app.js是游戏服务器的入口,主要负责所有服务器的配置,以及组件的加载和启动。本项目的启动主要分为两步:先启动pomelo的master服务器,再由pomelo的master服务器分别启动其它服务器。
项目启动采用pomelo的启动方式,即将master作为默认组件,在app.js中调用app.start()方法后加载并启动master服务。master组件会负责启动其它服务,启动过程分为两个阶段:
- 第一阶段
master服务启动其它所有服务,在服务器启动完毕后,其中monitor组件会连接到master对应的监听端口上,表明该服务器启动完毕。 - 第二阶段
当所有服务器启动完毕后,master会调用所有服务器上的afterStart接口来执行后续处理流程。
组件的加载和配置
本项目使用多个外部组件,这些组件在服务器启动时加载以提供各种服务,诸如数据统计、路由替换、游戏场景初始化等。
自定义在线统计模块
项目中使用了基于脚本的统计,组件通过运行自定义的脚本,收集服务器运行数据并生成报告。
$ vim game-server/modules/online.js
//自定义监控模块
let Module = function(opts){
console.log("online modules constructor");
opts = opts||{};
//当前监控模块所监测的服务器实例
this.app = opts.app;
//获取数据的方式
// pomelo-admin提供两种方式一种是pull拉取,一种是push推送
// pull拉取表示master服务器会主动从各个游戏服务器上拉取所需的监控数据
// push推送表示游戏服务器向master服务器推送监控数据
this.type = opts.type || "pull";
//每次push或pull的时间间隔
this.interval = opts.interval || 5;
};
// 监控模块标识
// 用于唯一标识监控模块,非常重要。
// 如果要向该模块获取和发送数据都需要依靠这个ID作为参数
// 如果需要让外部程序获取该模块的监控数据也必须依靠此参数
Module.moduleId = "online";
// 采用pull拉取 当接收到master主服务器拉通知时回调
// 采用push推送 每次到达interval间隔时间时回调
Module.prototype.monitorHandler = function(agent, msg){
console.log("online modules handler");
//获取连接组件服务
const connectionService = this.app.components.__connection__;
if(!connectionService){
console.log("not support connection: %j", agent.id);
return;
}
//代理通知连接统计信息
const info = connectionService.getStatisticsInfo();
//通知admin的消息给master服务器
agent.notify(Module.moduleId, info);
};
// 采用pull拉取 每次到达pull拉取时间间隔时会被调用
// 采用push推送 当接收到游戏服务器push推送数据回调
Module.prototype.masterHandler = function(agent, msg){
console.log("online master handler", msg, Module.moduleId);
//若无消息则通知所有的monitor监视器去获取数据
if(!msg){
//通知指定类型的监听器获取获取数据
const type = "connector";
const list = agent.typeMap[type];
if(!list || list.length===0){
agent.notifyByType(type, Module.moduleId);
}else{
//通知所有监视器去获取数据
agent.notifyAll(Module.moduleId);
}
return;
}
//从monitor监视器中收集数据
let data = agent.get(Module.moduleId);
if(!data){
data = {};
agent.set(Module.moduleId, data);
}
data[msg.serverId] = msg;
};
//当第三方程序调用时 获取监控数据接口时回调
Module.prototype.clientHandler = function(agent, msg, cb){
console.log("online client handler");
if(!!cb && typeof cb==="function"){
const param = agent.get(Module.moduleId) || {};
//处理客户端请求直接返回数据并缓存到master服务器
cb(null, param);
}
};
//导出模块
module.exports.moduleId = Module.moduleId;
module.exports = function(opts){
return new Module(opts);
};
启动脚本中为所有服务器添加在线统计,也可以单独为connector连接服务器添加。
$ game-server/app.js
//应用全局配置 针对所有服务器
app.configure("production|development", function(){
//开启系统监控 Linux环境有效
app.enable("systemMonitor");
//配置自定义监控:运行自定义统计脚本收集服务器运行数据,为服务器注册自定义监控模块。
if(typeof app.registerAdmin === "function"){
//将监控在线用户模块注册给所有服务器
app.registerAdmin("online", require("./app/modules/online"), {app:app});
}
});