了解一下吧,不然用起来总是会出现疑问不是?这是我一贯的习惯了。查看我们每个 pomelo 项目的 game-server 下的 app.js ,从头开始抓其流程吧。
就以lordofpomelo 为例来看。

前言

我更想把 pomelo 当做是一个 ECS 系统进行理解。 App 就是一个载体,承载了各个组件,各组件的协同运作,提供了一个 server。当然,我们也可以把它叫做一个上下文来理解。

需要明确一点的就是,App 并不提供任何服务,其加载的组件才是真正提供了各项功能的所在。

Application

Pomelo 有一个叫做 Application 的东西,在里面挂了很多的内容(加载了很多组件,配置)。我们也可以把他简称为 app,这是我们项目,游戏运行的一个全局上下文,很多内容可以从里面获取。

在程序的入口文件 app.js 中,我们会对每种类型的服务器,或者所有的服务器进行配置。包括 connector 的选择,组件的加载,甚至是自定义的插件等的加载。

//app.js

var app = pomelo.createApp();
app.set('name', 'lord of pomelo');

// configure for global
app.configure('production|development', function() {
app.before(pomelo.filters.toobusy());
app.enable('systemMonitor');
require('./app/util/httpServer');

//var sceneInfo = require('./app/modules/sceneInfo');
var onlineUser = require('./app/modules/onlineUser');
if(typeof app.registerAdmin === 'function'){
//app.registerAdmin(sceneInfo, {app: app});
app.registerAdmin(onlineUser, {app: app});
}
//Set areasIdMap, a map from area id to serverId.
if (app.serverType !== 'master') {
var areas = app.get('servers').area;
var areaIdMap = {};
for(var id in areas){
areaIdMap[areas[id].area] = areas[id].id;
}
app.set('areaIdMap', areaIdMap);
}
// proxy configures
app.set('proxyConfig', {
cacheMsg: true,
interval: 30,
lazyConnection: true
// enableRpcLog: true
});

// remote configures
app.set('remoteConfig', {
cacheMsg: true,
interval: 30
});

// route configures
app.route('area', routeUtil.area);
app.route('connector', routeUtil.connector);

app.loadConfig('mysql', app.getBase() + '/../shared/config/mysql.json');
app.filter(pomelo.filters.timeout());

/*
// master high availability
app.use(masterhaPlugin, {
zookeeper: {
server: '127.0.0.1:2181',
path: '/pomelo/master'
}
});
*/
});

// Configure for auth server
app.configure('production|development', 'auth', function() {
// load session congfigures
app.set('session', require('./config/session.json'));
});

// Configure for area server
app.configure('production|development', 'area', function(){
app.filter(pomelo.filters.serial());
app.before(playerFilter());

//Load scene server and instance server
var server = app.curServer;
if(server.instance){
instancePool.init(require('./config/instance.json'));
app.areaManager = instancePool;
}else{
scene.init(dataApi.area.findById(server.area));
app.areaManager = scene;
/*
kill -SIGUSR2 <pid>
http://localhost:3272/inspector.html?host=localhost:9999&page=0
*/
/*
// disable webkit-devtools-agent
var areaId = parseInt(server.area);
if(areaId === 3) { // area-server-3
require('webkit-devtools-agent');
var express = require('express');
var expressSvr = express.createServer();
expressSvr.use(express.static(__dirname + '/devtools_agent_page'));
var tmpPort = 3270 + areaId - 1;
expressSvr.listen(tmpPort);
}
*/
}

//Init areaService
areaService.init();
});

app.configure('production|development', 'manager', function(){
var events = pomelo.events;

app.event.on(events.ADD_SERVERS, instanceManager.addServers);

app.event.on(events.REMOVE_SERVERS, instanceManager.removeServers);
});

// Configure database
app.configure('production|development', 'area|auth|connector|master', function() {
var dbclient = require('./app/dao/mysql/mysql').init(app);
app.set('dbclient', dbclient);
// app.load(pomelo.sync, {path:__dirname + '/app/dao/mapping', dbclient: dbclient});
app.use(sync, {sync: {path:__dirname + '/app/dao/mapping', dbclient: dbclient}});
});

app.configure('production|development', 'connector', function(){
var dictionary = app.components['__dictionary__'];
var dict = null;
if(!!dictionary){
dict = dictionary.getDict();
}

app.set('connectorConfig',
{
connector : pomelo.connectors.hybridconnector,
heartbeat : 30,
useDict : true,
useProtobuf : true,
handshake : function(msg, cb){
cb(null, {});
}
});
});

app.configure('production|development', 'gate', function(){
app.set('connectorConfig',
{
connector : pomelo.connectors.hybridconnector,
useProtobuf : true
});
});
// Configure for chat server
app.configure('production|development', 'chat', function() {
app.set('chatService', new ChatService(app));
});

//start
app.start();

// Uncaught exception handler
process.on('uncaughtException', function(err) {
console.error(' Caught exception: ' + err.stack);
});

可以看到,先是建立一个 app ,然后调用 app.configure 配置了很多选项,还有,用 app.set 设置了一些键值。更多的细节我们后面再看。

配置文件

我们的服务启动都会进行很多的配置,配置文件的分布如下:
这些定义在 pomelo 的 constants.js 文件内。

// utils/constans.js
FILEPATH: {
MASTER: '/config/master.json',
SERVER: '/config/servers.json',
CRON: '/config/crons.json',
LOG: '/config/log4js.json',
SERVER_PROTOS: '/config/serverProtos.json',
CLIENT_PROTOS: '/config/clientProtos.json',
MASTER_HA: '/config/masterha.json',
LIFECYCLE: '/lifecycle.js',
SERVER_DIR: '/app/servers/',
CONFIG_DIR: '/config'
},

Pomelo.createApp()

Pomelo.createApp = function (opts) {
var app = application;
app.init(opts);
self.app = app;
return app;
};

Application.init()

Application.init = function(opts) {
opts = opts || {};
this.loaded = []; // loaded component list
this.components = {}; // name -> component map
this.settings = {}; // collection keep set/get
var base = opts.base || path.dirname(require.main.filename); // base path
this.set(Constants.RESERVED.BASE, base, true);
this.event = new EventEmitter(); // event object to sub/pub events

// current server info
this.serverId = null; // current server id
this.serverType = null; // current server type
this.curServer = null; // current server info
this.startTime = null; // current server start time

// global server infos
this.master = null; // master server info
this.servers = {}; // current global server info maps, id -> info
this.serverTypeMaps = {}; // current global type maps, type -> [info]
this.serverTypes = []; // current global server type list
this.lifecycleCbs = {}; // current server custom lifecycle callbacks
this.clusterSeq = {}; // cluster id seqence

appUtil.defaultConfiguration(this);

this.state = STATE_INITED;
logger.info('application inited: %j', this.getServerId());
};

其他都暂时不用看,我们关注后面的 appUtil.defaultConfiguration(this);

以上会对服务器进行默认的设置。

appUtil.defaultConfiguration(this);


module.exports.defaultConfiguration = function(app) {
var args = parseArgs(process.argv);
setupEnv(app, args);
loadMaster(app);
loadServers(app);
processArgs(app, args);
configLogger(app);
loadLifecycle(app);
};

根据其方法名称,可以得出其可能干了些什么。

  1. 解析命令行参数
  2. 设置环境
  3. 加载 Master
  4. 加载 Servers
  5. 解析参数
  6. 设置 Logger
  7. 加载生命周期

    parseArgs(process.argv)

此方法将命令行参数解析为一个map。

  1. -- 指定识别为选项
  2. key=value 指定的识别为参数。
var parseArgs = function(args) {
var argsMap = {};
var mainPos = 1;

while (args[mainPos].indexOf('--') > 0) {
mainPos++;
}
argsMap.main = args[mainPos];

for (var i = (mainPos + 1); i < args.length; i++) {
var arg = args[i];
var sep = arg.indexOf('=');
var key = arg.slice(0, sep);
var value = arg.slice(sep + 1);
if (!isNaN(Number(value)) && (value.indexOf('.') < 0)) {
value = Number(value);
}
argsMap[key] = value;
}

return argsMap;
};

loadMaster/loadServers

这两个函数,只是加载我们的配置文件。

var loadMaster = function(app) {
app.loadConfigBaseApp(Constants.RESERVED.MASTER, Constants.FILEPATH.MASTER);
app.master = app.get(Constants.RESERVED.MASTER);
};

让 app 全局上下文去加载配置文件 /config/master.json’ 内的内容。

var loadServers = function(app) {
app.loadConfigBaseApp(Constants.RESERVED.SERVERS, Constants.FILEPATH.SERVER);
var servers = app.get(Constants.RESERVED.SERVERS);
var serverMap = {}, slist, i, l, server;
for (var serverType in servers) {
slist = servers[serverType];
for (i = 0, l = slist.length; i < l; i++) {
server = slist[i];
server.serverType = serverType;
if(server[Constants.RESERVED.CLUSTER_COUNT]) {
utils.loadCluster(app, server, serverMap);
continue;
}
serverMap[server.id] = server;
if (server.wsPort) {
logger.warn('wsPort is deprecated, use clientPort in frontend server instead, server: %j', server);
}
}
}
app.set(Constants.KEYWORDS.SERVER_MAP, serverMap);
};

加载 /config/servers.json 的内容。

我们可以看到,这其实都是做了一些加载配置类的工作。

真正启动我们程序的,还是 app.start() 这个方法。

processArgs

这个函数所做的就是解析我们的命令行参数,来启动对应的服务。

当我们使用 pomelo start 不带任何命令行参数启动的时候,其实启动的是 master 服务器。后续,再由 master 服务器来启动其他服务。当然,事实上都是以 node app.js args.... 这样的形式来启动的。

我们可以通过 ps -e | grep node 命令来证实这一点:

45941 ttys000    0:00.51 node /usr/local/bin/pomelo start
45942 ttys000 0:00.86 /usr/local/Cellar/node/11.3.0_1/bin/node /Users/wodediannao/gitrepo/mygame/game-server/app.js env=development type=all
45943 ttys000 0:01.01 /usr/local/Cellar/node/11.3.0_1/bin/node /Users/wodediannao/gitrepo/mygame/game-server/app.js env=development id=connector-server-1 host=127.0.0.1 port=3150 clientPort=3010 frontend=true serverType=connector
45944 ttys000 0:01.04 /usr/local/Cellar/node/11.3.0_1/bin/node /Users/wodediannao/gitrepo/mygame/game-server/app.js env=development id=connector-server-2 host=127.0.0.1 port=3151 clientPort=3011 frontend=true serverType=connector
45945 ttys000 0:00.89 /usr/local/Cellar/node/11.3.0_1/bin/node /Users/wodediannao/gitrepo/mygame/game-server/app.js env=development id=chat-server-1 host=127.0.0.1 port=3450 serverType=chat
45946 ttys000 0:00.90 /usr/local/Cellar/node/11.3.0_1/bin/node /Users/wodediannao/gitrepo/mygame/game-server/app.js env=development id=auth-server-1 host=127.0.0.1 port=3650 serverType=auth
45947 ttys000 0:01.00 /usr/local/Cellar/node/11.3.0_1/bin/node /Users/wodediannao/gitrepo/mygame/game-server/app.js env=development id=gate-server-1 host=127.0.0.1 clientPort=3014 frontend=true serverType=gate
var processArgs = function(app, args) {
var serverType = args.serverType || Constants.RESERVED.MASTER; // 默认当我们的命令行不包括任何参数的时候,启动 master 服务器
var serverId = args.id || app.getMaster().id; // ID 默认的也是 master 的ID
var mode = args.mode || Constants.RESERVED.CLUSTER;
var masterha = args.masterha || 'false';
var type = args.type || Constants.RESERVED.ALL; // master 服务器会有一个 type 为 all 的东西
var startId = args.startId;

app.set(Constants.RESERVED.MAIN, args.main, true); // args.main 实际上是由 -- 选项进行指定
app.set(Constants.RESERVED.SERVER_TYPE, serverType, true);
app.set(Constants.RESERVED.SERVER_ID, serverId, true);
app.set(Constants.RESERVED.MODE, mode, true);
app.set(Constants.RESERVED.TYPE, type, true);
if(!!startId) {
app.set(Constants.RESERVED.STARTID, startId, true);
}

if (masterha === 'true') {
app.master = args;
app.set(Constants.RESERVED.CURRENT_SERVER, args, true);
} else if (serverType !== Constants.RESERVED.MASTER) {
app.set(Constants.RESERVED.CURRENT_SERVER, args, true);
} else {
app.set(Constants.RESERVED.CURRENT_SERVER, app.getMaster(), true);
}
};

Application.start()

服务的启动,其实就分为两步:

  1. 加载各组件
  2. 启动各组件
 Application.start = function(cb) {
this.startTime = Date.now();
if(this.state > STATE_INITED) {
utils.invokeCallback(cb, new Error('application has already start.'));
return;
}

var self = this;
appUtil.startByType(self, function() {
appUtil.loadDefaultComponents(self);
var startUp = function() {
appUtil.optComponents(self.loaded, Constants.RESERVED.START, function(err) {
self.state = STATE_START;
if(err) {
utils.invokeCallback(cb, err);
} else {
logger.info('%j enter after start...', self.getServerId());
self.afterStart(cb);
}
});
};
var beforeFun = self.lifecycleCbs[Constants.LIFECYCLE.BEFORE_STARTUP];
if(!!beforeFun) {
beforeFun.call(null, self, startUp);
} else {
startUp();
}
});
};

appUtil.startByType()

module.exports.startByType = function(app, cb) {
if(!!app.startId) {
if(app.startId === Constants.RESERVED.MASTER) {
utils.invokeCallback(cb);
} else {
starter.runServers(app);
}
} else {
if(!!app.type && app.type !== Constants.RESERVED.ALL && app.type !== Constants.RESERVED.MASTER) {
starter.runServers(app);
} else {
utils.invokeCallback(cb);
}
}
};

这个函数会根据是否是 master 服务器来执行不同的启动流程。在这里,我们默认使用 pomelo start 的情况下,会执行到第一个 utils.invokeCallback(cb);

starter.runServers ()

MASTER 服务器会启动其他所有的服务器。通过 starter.js 来执行的。

  1. 判断是不是本地服务。
  2. 本地服务通过 spanProcess 来执行
  3. 不是本地服务通过 ssh 命令来启动
// lib/master/starter.js
starter.runServers = function(app) {
var server, servers;
var condition = app.startId || app.type;
switch(condition) {
case Constants.RESERVED.MASTER:
break;
case Constants.RESERVED.ALL:
servers = app.getServersFromConfig();
for (var serverId in servers) {
this.run(app, servers[serverId]);
}
break;
default:
server = app.getServerFromConfig(condition);
if(!!server) {
this.run(app, server);
} else {
servers = app.get(Constants.RESERVED.SERVERS)[condition];
for(var i=0; i<servers.length; i++) {
this.run(app, servers[i]);
}
}
}
};

starter.localrun = function (cmd, host, options, callback) {
logger.info('Executing ' + cmd + ' ' + options + ' locally');
spawnProcess(cmd, host, options, callback);
};

在这个函数中,如果是 master 就不再进行什么逻辑操作了。

接下来,就会从 配置文件中(在 loadConfigBaseApp 中我们已经加载内容)加载所有的服务器,然后进行 run 操作。最终,会派生出新再进程,来启动我们定义的服务器。

appUtil.loadDefaultComponents()

当我们执行完毕 startByServerType 后就会进行这个回调。

顾名思义,加载默认的组件到 app 内来。

module.exports.loadDefaultComponents = function(app) {
var pomelo = require('../pomelo');
// load system default components
if (app.serverType === Constants.RESERVED.MASTER) {
app.load(pomelo.master, app.get('masterConfig'));
} else {
app.load(pomelo.proxy, app.get('proxyConfig'));
if (app.getCurServer().port) {
app.load(pomelo.remote, app.get('remoteConfig'));
}
if (app.isFrontend()) {
app.load(pomelo.connection, app.get('connectionConfig'));
app.load(pomelo.connector, app.get('connectorConfig'));
app.load(pomelo.session, app.get('sessionConfig'));
// compatible for schedulerConfig
if(app.get('schedulerConfig')) {
app.load(pomelo.pushScheduler, app.get('schedulerConfig'));
} else {
app.load(pomelo.pushScheduler, app.get('pushSchedulerConfig'));
}
}
app.load(pomelo.backendSession, app.get('backendSessionConfig'));
app.load(pomelo.channel, app.get('channelConfig'));
app.load(pomelo.server, app.get('serverConfig'));
}
app.load(pomelo.monitor, app.get('monitorConfig'));
};

这些很多地方就会很明了了

  1. Master 服务器只会加载 masterConfig 配置 (/config/master.json)。
  2. 其他所有服务器都会加载 proxy,channel,server, backendSession 组件。
  3. 有监听端口的服务器会加载 remote 组件
  4. 所有的前端服务器会加载 connection, connector, session;还有可能会加载 pushScheduler 组件。
  5. 所有都会加载 Monitor 组件。

app.load()

我们以加载的 connector 组件为例来看。 connector 定义在 pomelo 模块内:

Pomelo.connectors = {};
Pomelo.connectors.__defineGetter__('sioconnector', load.bind(null, './connectors/sioconnector'));
Pomelo.connectors.__defineGetter__('hybridconnector', load.bind(null, './connectors/hybridconnector'));
Pomelo.connectors.__defineGetter__('udpconnector', load.bind(null, './connectors/udpconnector'));
Pomelo.connectors.__defineGetter__('mqttconnector', load.bind(null, './connectors/mqttconnector'));

定义了四种方式。 socket.io, hybrid, udp, mqtt。

我们在 app.js 内定义了:

app.configure('production|development', 'connector', function(){
var dictionary = app.components['__dictionary__'];
var dict = null;
if(!!dictionary){
dict = dictionary.getDict();
}

app.set('connectorConfig',
{
connector : pomelo.connectors.hybridconnector,
heartbeat : 30,
useDict : true,
useProtobuf : true,
handshake : function(msg, cb){
cb(null, {});
}
});
});

当执行 app.load(pomelo.connector, app.get('connectorConfig')); 时,就会很明了

// app.load({}, {});
Application.load = function(name, component, opts) {
if(typeof name !== 'string') {
opts = component;
component = name;
name = null;
if(typeof component.name === 'string') {
name = component.name;
}
}

if(typeof component === 'function') {
component = component(this, opts);
}

if(!name && typeof component.name === 'string') {
name = component.name;
}

if(name && this.components[name]) {
// ignore duplicat component
logger.warn('ignore duplicate component: %j', name);
return;
}

this.loaded.push(component);
if(name) {
// components with a name would get by name throught app.components later.
this.components[name] = component;
}

return this;
};

startUp

在线我们把组件都加载完毕了,就开始启动这些组件了。

var startUp = function() {
appUtil.optComponents(self.loaded, Constants.RESERVED.START, function(err) {
self.state = STATE_START;
if(err) {
utils.invokeCallback(cb, err);
} else {
logger.info('%j enter after start...', self.getServerId());
self.afterStart(cb);
}
});
};
var beforeFun = self.lifecycleCbs[Constants.LIFECYCLE.BEFORE_STARTUP];
if(!!beforeFun) {
beforeFun.call(null, self, startUp);
} else {
startUp();
}

如果有 beforeFun 函数,则会先执行,执行完毕后,还会调用 afterStart 函数。

appUtil.optComponents

这个函数,是将所有的组件进行启动,其中, method 传递的是 start 函数。 comps 就是已加载的组件列表。

module.exports.optComponents = function(comps, method, cb) {
var i = 0;
async.forEachSeries(comps, function(comp, done) {
i++;
if (typeof comp[method] === 'function') {
comp[method](done);
} else {
done();
}
}, function(err) {
if (err) {
if(typeof err === 'string') {
logger.error('fail to operate component, method: %s, err: %j', method, err);
} else {
logger.error('fail to operate component, method: %s, err: %j', method, err.stack);
}
}
utils.invokeCallback(cb, err);
});
};

async.forEachSeries([], (), cb)。这个函数的意思是,对 [] 列表中的每个 item,的应用 () 定义的函数。cb 回调是可选的。

在我们的例子中,会对每个组件应用我们一个异步函数 function(comp, done) , done 是回调函数。

以我们的 connector 中的 hybridconnector.js 为例:

Connector.prototype.start = function(cb) {
var app = require('../pomelo').app;
var self = this;

var gensocket = function(socket) {
var hybridsocket = new HybridSocket(curId++, socket);
hybridsocket.on('handshake', self.handshake.handle.bind(self.handshake, hybridsocket));
hybridsocket.on('heartbeat', self.heartbeat.handle.bind(self.heartbeat, hybridsocket));
hybridsocket.on('disconnect', self.heartbeat.clear.bind(self.heartbeat, hybridsocket.id));
hybridsocket.on('closing', Kick.handle.bind(null, hybridsocket));
self.emit('connection', hybridsocket);
};

// 从 app 处获取加载的组件
this.connector = app.components.__connector__.connector;
this.dictionary = app.components.__dictionary__;
this.protobuf = app.components.__protobuf__;
this.decodeIO_protobuf = app.components.__decodeIO__protobuf__;

if(!this.ssl) {
this.listeningServer = net.createServer();
} else {
this.listeningServer = tls.createServer(this.ssl);
}
this.switcher = new Switcher(this.listeningServer, self.opts);

this.switcher.on('connection', function(socket) {
gensocket(socket);
});

if(!!this.distinctHost) {
this.listeningServer.listen(this.port, this.host);
} else {
this.listeningServer.listen(this.port);
}

process.nextTick(cb);
};

这样的话,一个组件就启动起来了,当然这个组件里面其实有更多的细节,但那不是本文关注的重点了。