應用程式配置,如何配置Pomelo架構?
Pomelo可以配置各個元件的選項,加載配置檔案,開啟Pomelo的特性等,這些配置都是在
game-server/app.js
檔案中進行的。實際上在Pomelo的應用中有兩個
app.js
,一個是在
game-server
目錄下,一個是在
web-server
目錄下。
game-server
下的
app.js
是整個遊戲伺服器的入口和配置點。
web-server
下的
app.js
是Web伺服器入口。
pomelo程序
pomelo架構是如何驅動的呢?
當應用啟動後,使用
pstree -au
得到程序樹,可發現
pomelo start
啟動指令調用程序會建立子程序,子程序執行的是
node app.js env=development
,然後這個子程序又會建立更多子程序,這些子程序執行的跟原程序同樣的檔案,隻是多個更多的參數。
$ cd game-server
$ pomelo start
$ pstree -au
pomelo start
指令的程序直接建立的子程序實際是master伺服器程序,由master伺服器建立的子程序執行
node <BasePath>/app.js env=development id=chat-server-1...
指令則是由master伺服器建立的子程序,這些子程序也就是應用伺服器。這裡所有的程序都是在一台主機上,所有會存在父子關系,也就是說master程序是其它應用伺服器的父程序。如果各個程序分布在不同的實體主機上的話,pomelo預設會使用ssh方式遠端啟動相應的伺服器,那麼master程序與應用程式程序不會再是父子關系。
使用
pomelo start
指令後指令行工具
pomelo
會檢查
start
後是否存在其它參數,比如是否需要
daemon
背景守護程序的形式啟動,是否指定
env
環境等。若沒有則會預設為其添加
env=development
環境參數後啟動node.js程序。
node <BasePath>/app.js env=development
此時
pomelo start
指令就啟動了
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
指令啟動pomelo應用時,若沒有傳入
--env
參數則會先檢查
process.env.NODE_ENV
環境變量是否設定。若沒有設定則預設為
development
。若通過
pomelo start --env production
方式啟動則env為
production
。
輔助指令
檢視端口
$ netstat -tln
$ netstat -anp
搜尋指定程序
$ netstat -anp | grep process_name
殺死指定PID的程序
$ kill -9 pid
遊戲伺服器啟動流程
-
建立應用執行個體createApp
-
加載配置和預設app.configure()
元件component
-
伺服器啟動通過配置和啟動參數啟動其它伺服器master
$ vim game-server/app.js
//加載pomelo
let pomelo = require("pomelo");
//建立app執行個體
let app = pomelo.createApp();
//通過app這個上下文對架構的配置以及一些初始化操作
app.configure(<env>, <serverType>, function(){});
app.configure(...);
app.set(...);
app.route(...);
//啟動應用
app.start();
例如:典型的啟動檔案包含内容
const pomelo = require('pomelo');
//建立應用執行個體
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()); // 配置内置過濾器: serialFilter
app.filter(pomelo.filters.time()); //開啟conn日志,對應pomelo-admin子產品下conn request
app.rpcFilter(pomelo.rpcFilters.rpcLog());//開啟rpc日志,對應pomelo-admin子產品下rpc request
//啟動系統監控
app.enable('systemMonitor');
//注冊admin module
//enable systemMonotor後 注冊的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
});
//遠端配置
app.set('remoteConfig', {
cacheMsg: true,
interval: 30
});
//設定内部connector元件: 心跳時長 通信協定
app.set('connectorConfig',{
connector: pomelo.connectors.hybridconnector,
heartbeat: 30,
useDict: true,
useProtobuf: true,
handshake: function (msg, cb) {
cb(null, {});
}
});
//設定變量
app.set(key, value);
//加載使用者自定義元件
//元件導出的都是工廠函數,app可自動識别,講其自身作為opt參數傳遞給元件,友善通路app上下文。
app.load(helloWorldComponent, opt);
//使用插件
const statusPlugin = require('pomelo-status-plugin');
app.use(statusPlugin, {
status:{
host: '127.0.0.1',
port: 6379
}
});
//啟動應用
app.start();
});
process.on('uncaughtException', function(err){
console.error('uncaughtException : ', err, err.stack());
});

Pemolo啟動流程
應用初始化 createApp
createApp
建立應用
- pomelo調用createApp()建立應用執行個體
const app = pomelo.createApp();
- pomelo的createApp調用中會調用app的init()方法完成對app的初始化
/**
* Create an pomelo application.
*
* @return {Application}
* @memberOf Pomelo
* @api public
*/
Pomelo.createApp = function (opts) {
var app = application;
app.init(opts);
self.app = app;
return app;
};
- app會使用appUtil提供的defaultConfiguration來完成自己的初始化配置
/**
* Initialize the server.
*
* - setup default configuration
*/
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會調用app的一些初始化方法,這些方法包括setEnv設定環境參數、loadMaster加載主伺服器、loadServers加載應用伺服器,parseArgs解析參數,configLogger配置日志。
/**
* 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操作會将目前的env設定為development
- loadMaster調用會加載maseter伺服器的配置資訊
- loadServers會加載所有的應用伺服器配置資訊
- parseArgs是一個關鍵性的操作,由于
啟動參數中僅僅指定了env,其它參數并未指定,此時pomelo認為目前啟動的不是應用伺服器而是master伺服器。是以,目前程序将使用master的配置資訊,并将自己的serverId、serverType等參數設定為master伺服器所有的。實際上,對于應用伺服器來說,如果啟動的是應用伺服器的話,pomelo start
後可帶有更多參數,包括id、serverType、port、clientPort等,這些參數在parseArgs這一步将會被處理,進而确定目前伺服器的ID、類型等其它必須的配置資訊。node app.js
5.執行完上訴操作後app進入INITED已初始化狀态,同時pomelo的createApp傳回。
當pomelo的createApp()方法傳回後在app.js中接下來會對app進行一系列的配置,比如調用app.set()設定上下文變量的值,app.route()調用配置路由等。
應用程式配置
app.js
是運作Pomelo項目的入口,在
app.js
檔案中首先會建立一個
app
執行個體,這個
app
作為整個架構的配置上下文來使用,使用者可以通過這個上下文,設定一些全局變量,加載配置資訊等操作。
configure
使用
app.configure
調用來配置
伺服器的配置主要由
configure()
方法完成,完整的
app.configure
配置參數格式:
app.configure([env], [serverType], [function]);
參數 | 描述 |
---|---|
env | 運作環境,可設定為 、 、 。 |
serverType | 伺服器類型,設定後隻會對目前參數類型伺服器做初始化,不設定則對所有伺服器執行初始化的 。比如 、 、 ... |
function | 具體的初始化操作,内部可以些任何對架構的配置操作邏輯。 |
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;
};
更多地可以在
configure
中針對不同的伺服器、不同的環境,對架構進行不同的配置。這些配置包括設定一個上下文變量供應用使用,開啟一些功能選項,配置加載一個自定義的元件
component
,針對不同的伺服器,配置過濾器
filter
等配置操作。
loadConfig
例如:全局配置MySQL參數
$ vim game-server/config/mysql.json
{
"development":
{
"host": "127.0.0.1",
"port": "3306",
"username": "root",
"password": "root",
"database": "pomelo"
}
}
加載配置檔案,使用者通過
loadConfig()
加載配置檔案後,加載後檔案中的參數将會直接挂載到
app
對象上,可直接通過
app
對象通路具體的配置參數。
$ vim game-server/app.js
const path = require('path');
//全局配置
app.configure('production|development', function(){
//加載MySQL資料庫
app.loadConfig("mysql", path.join(app.getBase(), "config/mysql.json"));
const host = app.get("mysql").host;//擷取配置
console.log("mysql config: host = %s",host);
});
使用者可以使用
loadConfig()
的調用加載任何
JSON
格式的配置檔案,用于其它的目的,并能通過
app
進行通路。需要注意的是所有的
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);
};
set/get
上下文變量存取是指上下文對象
app
提供了設定和擷取應用變量的方法,簽名為:
set 設定應用變量
app.set(name, value, [isAttach]);
參數 | 描述 |
---|---|
name | 變量名 |
value | 變量值 |
isAttach | 可選,預設為false,附加屬性,若isAttach為true則将變量attach到app對象上作為屬性。此後對此變量的通路,可直接通過 。 |
/**
* 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;
get 擷取應用變量
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];
};
例如:擷取項目根目錄,即
app.js
檔案所在的目錄。
const basepath = app.get("base");
// const basepath = app.getBase();
enable/disable
開發者可通過enable()/disable()方法來啟用或禁用Pomelo架構的一些特性,并通過enabled()/disabled()方法來檢查特性的可用狀态。
例如:禁用及啟用RPC調試日志并檢查其狀态
app.enabled("rpcDebugLog");//return true/false
app.disabled("rpcDebugLog");
app.enable("rpcDebugLog");
app.disable("rpcDebugLog");
例如:啟用systemMonitor以加載額外子產品
app.enable("systemMonitor");
route
route主要負責請求路由資訊的維護,路由計算,路由結果緩存等工作,并根據需要切換路由政策,更新路由資訊等。
/**
* 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;
};
使用者可自定義不同伺服器的不同路由規則,然後進行配置即可。在路由函數中,通過最後的回調函數中傳回伺服器的ID即可。
$ vim game-server/app.js
//聊天伺服器配置
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());
});
filter
實際應用中,往往需要在邏輯伺服器處理請求之前對使用者請求做一些前置處理,當請求被處理後又需要做一些善後處理,由于這是一種常見的情形。Pomelo對其進行了抽象,也就是filter。在Pomelo中filter分為before filter和after filter。在一個請求到達Handler被處理之前,可以經過多個before filter組成的filter鍊進行一些前置處理,比如對請求進行排隊,逾時處理。當請求被Handler處理完成後,又可以通過after filter鍊進行一些善後處理。這裡需要注意的是在after filter中一般隻做一些清理處理,而不應該再去修改到用戶端的響應内容。因為此時,對用戶端的響應内容已經發送給了用戶端。
filter鍊
filter
分為
before
和
after
兩類,每個
filter
都可以注冊多個形成一個
filter
鍊,所有用戶端請求都會經過
filter
鍊進行處理。
before filter
會對請求做一些前置處理,如檢查目前玩家是否已經登入,列印統計日志等。
after filter
是進行請求後置處理的地方,比如釋放請求上下文的資源,記錄請求總耗時等。
after filter
中不應該再出現修改響應内容的代碼,因為在進入
after filter
前響應就已經被發送給用戶端。
配置filter
當一個用戶端請求到達伺服器後,經過filter鍊和handler處理,最後生成響應傳回給用戶端。handler是業務邏輯實作的地方,filter則是執行業務前進行預處理和業務處理後清理的地方。為了開發者友善,系統内建提供了一些filter。比如serialFilter、timerFilter、timeOutFilter等,另外,使用者可以根據應用的需要自定義filter。
app.filter(pomelo.filters.serial());
如果僅僅是before filter,那麼調用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,則調用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調用。
/**
* 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調用,将其配置進架構。
filter對象
filter是一個對象,定義filter大緻代碼如下:
let Filter = function(){};
/**
* 前置過濾器
* @param msg 使用者請求原始内容或經前面filter鍊處理後的内容
* @param session 若在後端伺服器上則是BackendSession,若在前端伺服器則是FrontendSession
* @param next
*/
Filter.prototype.before = function(msg, session, next){
};
/**
* 後置過濾器
* @param err 錯誤資訊
* @param msg
* @param session
* @param resp 對用戶端的響應内容
* @param next
*/
Filter.prototype.after = function(err, msg, session, resp, next){
};
module.exports = function(){
return new Filter();
};
伺服器啟動 app.start()
app.start()
當執行完使用者編輯代碼後,将會進入
app.start()
調用,它首先會加載預設的元件,對于master伺服器來說加載的預設元件時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元件的啟動過程
master元件的啟動過程
- app.start()方法首先會加載預設元件,由于沒有指定伺服器類型,此時會預設為master伺服器類型,并擷取master伺服器的配置、加載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元件是以工廠函數的方式導出的,是以會建立master元件,master元件的建立過程中會建立MasterConsole,MasterConsole會建立MasterAgent,MasterAgent會建立監聽Socket用來監聽應用伺服器的監控和管理請求。
Pomelo啟動活動圖
轉自Pomelo Application - 簡書