應(yīng)用程序配置,如何配置Pomelo框架?
Pomelo可以配置各個(gè)組件的選項(xiàng),加載配置文件,開啟Pomelo的特性等,這些配置都是在game-server/app.js文件中進(jìn)行的。實(shí)際上在Pomelo的應(yīng)用中有兩個(gè)app.js,一個(gè)是在game-server目錄下,一個(gè)是在web-server目錄下。game-server下的app.js是整個(gè)游戲服務(wù)器的入口和配置點(diǎn)。web-server下的app.js是Web服務(wù)器入口。
pomelo進(jìn)程
pomelo框架是如何驅(qū)動(dòng)的呢?
當(dāng)應(yīng)用啟動(dòng)后,使用pstree -au得到進(jìn)程樹,可發(fā)現(xiàn)pomelo start啟動(dòng)命令調(diào)用進(jìn)程會(huì)創(chuàng)建子進(jìn)程,子進(jìn)程執(zhí)行的是node app.js env=development,然后這個(gè)子進(jìn)程又會(huì)創(chuàng)建更多子進(jìn)程,這些子進(jìn)程執(zhí)行的跟原進(jìn)程同樣的文件,只是多個(gè)更多的參數(shù)。
$ cd game-server
$ pomelo start
$ pstree -au
pomelo start命令的進(jìn)程直接創(chuàng)建的子進(jìn)程實(shí)際是master服務(wù)器進(jìn)程,由master服務(wù)器創(chuàng)建的子進(jìn)程執(zhí)行node <BasePath>/app.js env=development id=chat-server-1...命令則是由master服務(wù)器創(chuàng)建的子進(jìn)程,這些子進(jìn)程也就是應(yīng)用服務(wù)器。這里所有的進(jìn)程都是在一臺(tái)主機(jī)上,所有會(huì)存在父子關(guān)系,也就是說master進(jìn)程是其它應(yīng)用服務(wù)器的父進(jìn)程。如果各個(gè)進(jìn)程分布在不同的物理主機(jī)上的話,pomelo默認(rèn)會(huì)使用ssh方式遠(yuǎn)程啟動(dòng)相應(yīng)的服務(wù)器,那么master進(jìn)程與應(yīng)用程序進(jìn)程不會(huì)再是父子關(guān)系。
使用pomelo start命令后命令行工具pomelo會(huì)檢查start后是否存在其它參數(shù),比如是否需要daemon后臺(tái)守護(hù)進(jìn)程的形式啟動(dòng),是否指定env環(huán)境等。若沒有則會(huì)默認(rèn)為其添加env=development環(huán)境參數(shù)后啟動(dòng)node.js進(jìn)程。
node <BasePath>/app.js env=development
此時(shí)pomelo start命令就啟動(dòng)了app.js腳本
$ cat /lib/util/appUtils.js
var setEnv = function(app, args){
app.set(Constants.RESERVED.ENV, args.env || process.env.NODE_ENV || Constants.RESERVED.ENV_DEV, true);
};
使用pomelo start命令啟動(dòng)pomelo應(yīng)用時(shí),若沒有傳入--env參數(shù)則會(huì)先檢查process.env.NODE_ENV環(huán)境變量是否設(shè)置。若沒有設(shè)置則默認(rèn)為development。若通過pomelo start --env production方式啟動(dòng)則env為production。
輔助命令
查看端口
$ netstat -tln
$ netstat -anp
搜索指定進(jìn)程
$ netstat -anp | grep process_name
殺死指定PID的進(jìn)程
$ kill -9 pid
啟動(dòng)流程
-
createApp創(chuàng)建應(yīng)用實(shí)例 -
app.configure()加載配置和默認(rèn)component組件 - 啟動(dòng)
master服務(wù)器,然后通過配置和啟動(dòng)參數(shù)和其它服務(wù)器。
$ vim game-server/app.js
//加載pomelo
let pomelo = require("pomelo");
//創(chuàng)建app實(shí)例
let app = pomelo.createApp();
//通過app這個(gè)上下文對(duì)框架的配置以及一些初始化操作
app.configure(<env>, <serverType>, function(){});
app.configure(...);
app.set(...);
app.route(...);
//啟動(dòng)應(yīng)用
app.start();
例如:典型的啟動(dòng)文件包含內(nèi)容
const pomelo = require('pomelo');
//創(chuàng)建應(yīng)用實(shí)例
const app = pomelo.createApp();
//加載配置和組件
//app.configure(<env>, <serverType>, function(){});
//env:development|production
//serverType: master/gate/connector/...
app.configure("development|production", "connector", function(){
//過濾器配置
app.before(pomelo.filters.toobusy());//接口訪問限制
app.filter(pomelo.filters.serial()); // 配置內(nèi)置過濾器: serialFilter
app.filter(pomelo.filters.time()); //開啟conn日志,對(duì)應(yīng)pomelo-admin模塊下conn request
app.rpcFilter(pomelo.rpcFilters.rpcLog());//開啟rpc日志,對(duì)應(yīng)pomelo-admin模塊下rpc request
//啟動(dòng)系統(tǒng)監(jiān)控
app.enable('systemMonitor');
//注冊(cè)admin module
//enable systemMonotor后 注冊(cè)的admin module才可使用
var onlineUser = require('./app/modules/onlineUser');
if (typeof app.registerAdmin === 'function') {
app.registerAdmin(onlineUser, {app: app});
}
//加載配置
app.loadConfig('mysql', app.getBase() + '/config/mysql.json');
//配置路由
app.route('chat', routeUtil.chat);
//配置代理
app.set('proxyConfig', {
cacheMsg: true,
interval: 30,
lazyConnection: true,
enableRpcLog: true
});
//遠(yuǎn)程配置
app.set('remoteConfig', {
cacheMsg: true,
interval: 30
});
//設(shè)置內(nèi)部connector組件: 心跳時(shí)長(zhǎng) 通信協(xié)議
app.set('connectorConfig',{
connector: pomelo.connectors.hybridconnector,
heartbeat: 30,
useDict: true,
useProtobuf: true,
handshake: function (msg, cb) {
cb(null, {});
}
});
//設(shè)置變量
app.set(key, value);
//加載用戶自定義組件
//組件導(dǎo)出的都是工廠函數(shù),app可自動(dòng)識(shí)別,講其自身作為opt參數(shù)傳遞給組件,方便訪問app上下文。
app.load(helloWorldComponent, opt);
//使用插件
const statusPlugin = require('pomelo-status-plugin');
app.use(statusPlugin, {
status:{
host: '127.0.0.1',
port: 6379
}
});
//啟動(dòng)應(yīng)用
app.start();
});
process.on('uncaughtException', function(err){
console.error('uncaughtException : ', err, err.stack());
});

pomelo.createApp

調(diào)用createApp()創(chuàng)建應(yīng)用實(shí)例
const app = pomelo.createApp();
createApp()中會(huì)調(diào)用app.init()方法完成對(duì)應(yīng)用初始化
Pomelo.createApp = function (opts) {
var app = application;
app.init(opts);
self.app = app;
return app;
};
app會(huì)使用appUtil提供的defaultConfiguration來完成自己的初始化配置
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);
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會(huì)調(diào)用app的一些初始化方法
/**
* Initialize application configuration.
*/
module.exports.defaultConfiguration = function(app) {
var args = parseArgs(process.argv);
setupEnv(app, args);
loadMaster(app);
loadServers(app);
processArgs(app, args);
configLogger(app);
loadLifecycle(app);
};
| 初始化方法 | 說明 |
|---|---|
setEnv |
設(shè)置環(huán)境參數(shù),比如將當(dāng)前的env設(shè)定為development。 |
loadMaster |
加載主服務(wù)器,加載maseter服務(wù)器的配置信息。 |
loadServers |
加載應(yīng)用服務(wù)器 |
parseArgs |
解析參數(shù) |
configLogger |
配置日志 |
parseArgs是一個(gè)關(guān)鍵性的操作,由于pomelo start啟動(dòng)參數(shù)中僅僅指定了env,其它參數(shù)并未指定,此時(shí)pomelo認(rèn)為目前啟動(dòng)的不是應(yīng)用服務(wù)器而是master服務(wù)器。因此,當(dāng)前進(jìn)程將使用master的配置信息,并將自己的serverId、serverType等參數(shù)設(shè)置為master服務(wù)器所有的。
實(shí)際上對(duì)于應(yīng)用服務(wù)器來說,如果啟動(dòng)的是應(yīng)用服務(wù)器的話,node app.js后可帶有更多參數(shù),包括id、serverType、port、clientPort等,這些參數(shù)在parseArgs這一步將會(huì)被處理,從而確定當(dāng)前服務(wù)器的ID、類型等其它必須的配置信息。
執(zhí)行完上訴操作后app進(jìn)入INITED已初始化狀態(tài),同時(shí)createApp()返回。當(dāng)createApp()方法返回后,會(huì)在app.js中接下來會(huì)對(duì)app進(jìn)行一系列的配置,比如調(diào)用app.set()設(shè)置上下文變量的值,app.route()調(diào)用配置路由等。
app.configure
- 服務(wù)器配置主要由
app.configure()完成
app.js是運(yùn)行Pomelo項(xiàng)目的入口,在app.js文件中首先會(huì)創(chuàng)建一個(gè)app實(shí)例,這個(gè)app作為整個(gè)框架的配置上下文來使用,用戶可使用app.configure()來配置,通過上下文設(shè)置全局變量,加載配置信息等操作。
完整的app.configure配置參數(shù)格式:
app.configure([env], [serverType], [function]);
| 參數(shù) | 描述 |
|---|---|
| env | 運(yùn)行環(huán)境,可設(shè)置為development、production、development|production。 |
| serverType | 服務(wù)器類型,設(shè)置后只會(huì)對(duì)當(dāng)前參數(shù)類型服務(wù)器做初始化,不設(shè)置則對(duì)所有服務(wù)器執(zhí)行初始化的function。比如gate、connector、chat... |
| function | 具體的初始化操作,內(nèi)部可以些任何對(duì)框架的配置操作邏輯。 |
查看源碼
Application.configure = function (env, type, fn) {
var args = [].slice.call(arguments);
fn = args.pop();
env = type = Constants.RESERVED.ALL;
if(args.length > 0) {
env = args[0];
}
if(args.length > 1) {
type = args[1];
}
if (env === Constants.RESERVED.ALL || contains(this.settings.env, env)) {
if (type === Constants.RESERVED.ALL || contains(this.settings.serverType, type)) {
fn.call(this);
}
}
return this;
};
app.configure()針對(duì)不同的服務(wù)器和環(huán)境,對(duì)框架進(jìn)行不同的配置。
| 配置功能 | 描述 |
|---|---|
| app.loadConfig | 加載應(yīng)用配置 |
| app.set/app.get | 設(shè)置上下文變量供應(yīng)用使用 |
| app.enable/app.disable | 開啟功能選項(xiàng) |
| app.route | 路由管理 |
| app.filter | 針對(duì)不同的服務(wù)器,配置過濾器filter等配置操作。 |
| - | 配置加載自定義的組件component
|
| app.registerAdmin |
app.loadConfig
例如:全局配置MySQL參數(shù)
$ vim game-server/config/mysql.json
{
"development":
{
"host": "127.0.0.1",
"port": "3306",
"username": "root",
"password": "root",
"database": "pomelo"
}
}
加載配置文件,用戶通過loadConfig()加載配置文件后,加載后文件中的參數(shù)將會(huì)直接掛載到app對(duì)象上,可直接通過app對(duì)象訪問具體的配置參數(shù)。
$ vim game-server/app.js
const path = require('path');
//全局配置
app.configure('production|development', function(){
//加載MySQL數(shù)據(jù)庫
app.loadConfig("mysql", path.join(app.getBase(), "config/mysql.json"));
const host = app.get("mysql").host;//獲取配置
console.log("mysql config: host = %s",host);
});
用戶可以使用loadConfig()的調(diào)用加載任何JSON格式的配置文件,用于其它的目的,并能通過app進(jìn)行訪問。需要注意的是所有的JSON配置文件中都需要指定具體的模式,也就是development或production。
/**
* Load Configure json file to settings.
*
* @param {String} key environment key
* @param {String} val environment value
* @return {Server|Mixed} for chaining, or the setting value
* @memberOf Application
*/
Application.loadConfig = function(key, val) {
var env = this.get(Constants.RESERVED.ENV);
val = require(val);
if (val[env]) {
val = val[env];
}
this.set(key, val);
};
app.set/app.get
- app.set 設(shè)置應(yīng)用變量
- app.get 獲取應(yīng)用變量
上下文變量存取是指上下文對(duì)象app提供了設(shè)置和獲取應(yīng)用變量的方法,簽名為:
app.set(name, value, [isAttach]);
| 參數(shù) | 描述 |
|---|---|
| name | 變量名 |
| value | 變量值 |
| isAttach | 可選,默認(rèn)為false,附加屬性,若isAttach為true則將變量attach到app對(duì)象上作為屬性。此后對(duì)此變量的訪問,可直接通過app.name。 |
/**
* Assign `setting` to `val`, or return `setting`'s value.
*
* Example:
*
* app.set('key1', 'value1');
* app.get('key1'); // 'value1'
* app.key1; // undefined
*
* app.set('key2', 'value2', true);
* app.get('key2'); // 'value2'
* app.key2; // 'value2'
*
* @param {String} setting the setting of application
* @param {String} val the setting's value
* @param {Boolean} attach whether attach the settings to application
* @return {Server|Mixed} for chaining, or the setting value
* @memberOf Application
*/
Application.set = function (setting, val, attach) {
if (arguments.length === 1) {
return this.settings[setting];
}
this.settings[setting] = val;
if(attach) {
this[setting] = val;
}
return this;
};
例如:
app.set("name", "project_name");
const name = app.get("name);//project_name
app.set("name", name, true);
const name = app.name;
app.get 獲取應(yīng)用變量
app.get(name);
/**
* Get property from setting
*
* @param {String} setting application setting
* @return {String} val
* @memberOf Application
*/
Application.get = function (setting) {
return this.settings[setting];
};
例如:獲取項(xiàng)目根目錄,即app.js文件所在的目錄。
const basepath = app.get("base");
// const basepath = app.getBase();
app.enable/app.disable
開發(fā)者可通過enable()/disable()方法來啟用或禁用Pomelo框架的一些特性,并通過enabled()/disabled()方法來檢查特性的可用狀態(tài)。
例如:禁用及啟用RPC調(diào)試日志并檢查其狀態(tài)
app.enabled("rpcDebugLog");//return true/false
app.disabled("rpcDebugLog");
app.enable("rpcDebugLog");
app.disable("rpcDebugLog");
例如:?jiǎn)⒂胹ystemMonitor以加載額外模塊
app.enable("systemMonitor");
app.route
route主要負(fù)責(zé)請(qǐng)求路由信息的維護(hù),路由計(jì)算,路由結(jié)果緩存等工作,并根據(jù)需要切換路由策略,更新路由信息等。
/**
* Set the route function for the specified server type.
*
* Examples:
*
* app.route('area', routeFunc);
*
* var routeFunc = function(session, msg, app, cb) {
* // all request to area would be route to the first area server
* var areas = app.getServersByType('area');
* cb(null, areas[0].id);
* };
*
* @param {String} serverType server type string
* @param {Function} routeFunc route function. routeFunc(session, msg, app, cb)
* @return {Object} current application instance for chain invoking
* @memberOf Application
*/
Application.route = function(serverType, routeFunc) {
var routes = this.get(Constants.KEYWORDS.ROUTE);
if(!routes) {
routes = {};
this.set(Constants.KEYWORDS.ROUTE, routes);
}
routes[serverType] = routeFunc;
return this;
};
用戶可自定義不同服務(wù)器的不同路由規(guī)則,然后進(jìn)行配置即可。在路由函數(shù)中,通過最后的回調(diào)函數(shù)中返回服務(wù)器的ID即可。
$ vim game-server/app.js
//聊天服務(wù)器配置
app.configure("production|development", "chat",function(){
//路由配置
app.route("chat", function(session, msg, app, cb){
const servers = app.getServersByType("chat");
if(!servers || servers.length===0){
cb(new Error("can not find chat servers"));
return;
}
const val = session.get("rid");
if(!val){
cb(new Error("session rid is not find"));
return;
}
const index = Math.abs(crc.crc32(val)) % servers.length;
const server = servers[index];
cb(null, server.id);
});
//過濾配置
app.filter(pomelo.timeout());
});
app.filter
實(shí)際應(yīng)用中,往往需要在邏輯服務(wù)器處理請(qǐng)求之前對(duì)用戶請(qǐng)求做一些前置處理,當(dāng)請(qǐng)求被處理后又需要做一些善后處理,由于這是一種常見的情形。Pomelo對(duì)其進(jìn)行了抽象,也就是filter。在Pomelo中filter分為before filter和after filter。在一個(gè)請(qǐng)求到達(dá)Handler被處理之前,可以經(jīng)過多個(gè)before filter組成的filter鏈進(jìn)行一些前置處理,比如對(duì)請(qǐng)求進(jìn)行排隊(duì),超時(shí)處理。當(dāng)請(qǐng)求被Handler處理完成后,又可以通過after filter鏈進(jìn)行一些善后處理。這里需要注意的是在after filter中一般只做一些清理處理,而不應(yīng)該再去修改到客戶端的響應(yīng)內(nèi)容。因?yàn)榇藭r(shí),對(duì)客戶端的響應(yīng)內(nèi)容已經(jīng)發(fā)送給了客戶端。
filter鏈
filter分為before和after兩類,每個(gè)filter都可以注冊(cè)多個(gè)形成一個(gè)filter鏈,所有客戶端請(qǐng)求都會(huì)經(jīng)過filter鏈進(jìn)行處理。before filter會(huì)對(duì)請(qǐng)求做一些前置處理,如檢查當(dāng)前玩家是否已經(jīng)登錄,打印統(tǒng)計(jì)日志等。after filter是進(jìn)行請(qǐng)求后置處理的地方,比如釋放請(qǐng)求上下文的資源,記錄請(qǐng)求總耗時(shí)等。after filter中不應(yīng)該再出現(xiàn)修改響應(yīng)內(nèi)容的代碼,因?yàn)樵谶M(jìn)入after filter前響應(yīng)就已經(jīng)被發(fā)送給客戶端。
配置filter
當(dāng)一個(gè)客戶端請(qǐng)求到達(dá)服務(wù)器后,經(jīng)過filter鏈和handler處理,最后生成響應(yīng)返回給客戶端。handler是業(yè)務(wù)邏輯實(shí)現(xiàn)的地方,filter則是執(zhí)行業(yè)務(wù)前進(jìn)行預(yù)處理和業(yè)務(wù)處理后清理的地方。為了開發(fā)者方便,系統(tǒng)內(nèi)建提供了一些filter。比如serialFilter、timerFilter、timeOutFilter等,另外,用戶可以根據(jù)應(yīng)用的需要自定義filter。
app.filter(pomelo.filters.serial());
如果僅僅是before filter,那么調(diào)用app.before。
/**
* Add before filter.
*
* @param {Object|Function} bf before fileter, bf(msg, session, next)
* @memberOf Application
*/
Application.before = function (bf) {
addFilter(this, Constants.KEYWORDS.BEFORE_FILTER, bf);
};
如果是after filter,則調(diào)用app.after。
/**
* Add after filter.
*
* @param {Object|Function} af after filter, `af(err, msg, session, resp, next)`
* @memberOf Application
*/
Application.after = function (af) {
addFilter(this, Constants.KEYWORDS.AFTER_FILTER, af);
};
如果即定義了before filter,又定義了after filter,可以使用app.filter調(diào)用。
/**
* add a filter to before and after filter
*
* @param {Object} filter provide before and after filter method.
* A filter should have two methods: before and after.
* @memberOf Application
*/
Application.filter = function (filter) {
this.before(filter);
this.after(filter);
};
用戶可以自定義filter,然后通過app.filter調(diào)用,將其配置進(jìn)框架。
filter對(duì)象
filter是一個(gè)對(duì)象,定義filter大致代碼如下:
let Filter = function(){};
/**
* 前置過濾器
* @param msg 用戶請(qǐng)求原始內(nèi)容或經(jīng)前面filter鏈處理后的內(nèi)容
* @param session 若在后端服務(wù)器上則是BackendSession,若在前端服務(wù)器則是FrontendSession
* @param next
*/
Filter.prototype.before = function(msg, session, next){
};
/**
* 后置過濾器
* @param err 錯(cuò)誤信息
* @param msg
* @param session
* @param resp 對(duì)客戶端的響應(yīng)內(nèi)容
* @param next
*/
Filter.prototype.after = function(err, msg, session, resp, next){
};
module.exports = function(){
return new Filter();
};
app.start
當(dāng)執(zhí)行完用戶編輯代碼后,將會(huì)進(jìn)入app.start()調(diào)用,它首先會(huì)加載默認(rèn)的組件,對(duì)于master服務(wù)器來說加載的默認(rèn)組件時(shí)master組件和monitor組件。
/**
* Start application. It would load the default components and start all the loaded components.
*
* @param {Function} cb callback function
* @memberOf Application
*/
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();
}
});
};
master組件的啟動(dòng)過程

- app.start()方法首先會(huì)加載默認(rèn)組件,由于沒有指定服務(wù)器類型,此時(shí)會(huì)默認(rèn)為master服務(wù)器類型,并獲取master服務(wù)器的配置、加載master組件。
$ vim game-server/config/master.json
{
"development": {
"id": "master-server-1", "host": "127.0.0.1", "port": 3005
},
"production": {
"id": "master-server-1", "host": "127.0.0.1", "port": 3005
}
}
由于Master組件是以工廠函數(shù)的方式導(dǎo)出的,因此會(huì)創(chuàng)建master組件,master組件的創(chuàng)建過程中會(huì)創(chuàng)建MasterConsole,MasterConsole會(huì)創(chuàng)建MasterAgent,MasterAgent會(huì)創(chuàng)建監(jiān)聽Socket用來監(jiān)聽?wèi)?yīng)用服務(wù)器的監(jiān)控和管理請(qǐng)求。
