天天看點

Pomelo Application

應用程式配置,如何配置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
           

遊戲伺服器啟動流程

  1. createApp

    建立應用執行個體
  2. app.configure()

    加載配置和預設

    component

    元件
  3. 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());
});
           
Pomelo Application

Pemolo啟動流程

應用初始化

createApp

Pomelo Application

建立應用

  1. pomelo調用createApp()建立應用執行個體
const app = pomelo.createApp();
           
  1. 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;
};
           
  1. 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());
};
           
  1. 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是一個關鍵性的操作,由于

    pomelo start

    啟動參數中僅僅指定了env,其它參數并未指定,此時pomelo認為目前啟動的不是應用伺服器而是master伺服器。是以,目前程序将使用master的配置資訊,并将自己的serverId、serverType等參數設定為master伺服器所有的。實際上,對于應用伺服器來說,如果啟動的是應用伺服器的話,

    node app.js

    後可帶有更多參數,包括id、serverType、port、clientPort等,這些參數在parseArgs這一步将會被處理,進而确定目前伺服器的ID、類型等其它必須的配置資訊。

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 運作環境,可設定為

development

production

development|production

serverType 伺服器類型,設定後隻會對目前參數類型伺服器做初始化,不設定則對所有伺服器執行初始化的

function

。比如

gate

connector

chat

...
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對象上作為屬性。此後對此變量的通路,可直接通過

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;
           

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()

調用,它首先會加載預設的元件,對于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元件的啟動過程

Pomelo Application

master元件的啟動過程

  1. 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 Application

Pomelo啟動活動圖

轉自Pomelo Application - 簡書

繼續閱讀