pomelo app

应用程序配置,如何配置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());
});
Pemolo启动流程

pomelo.createApp

创建应用

调用createApp()创建应用实例

const app = pomelo.createApp();

createApp()中会调用app.init()方法完成对应用初始化

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

app会使用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());
};

appUtildefaultConfiguration会调用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 设置环境参数,比如将当前的env设定为development。
loadMaster 加载主服务器,加载maseter服务器的配置信息。
loadServers 加载应用服务器
parseArgs 解析参数
configLogger 配置日志

parseArgs是一个关键性的操作,由于pomelo start启动参数中仅仅指定了env,其它参数并未指定,此时pomelo认为目前启动的不是应用服务器而是master服务器。因此,当前进程将使用master的配置信息,并将自己的serverIdserverType等参数设置为master服务器所有的。

实际上对于应用服务器来说,如果启动的是应用服务器的话,node app.js后可带有更多参数,包括id、serverType、port、clientPort等,这些参数在parseArgs这一步将会被处理,从而确定当前服务器的ID、类型等其它必须的配置信息。

执行完上诉操作后app进入INITED已初始化状态,同时createApp()返回。当createApp()方法返回后,会在app.js中接下来会对app进行一系列的配置,比如调用app.set()设置上下文变量的值,app.route()调用配置路由等。

app.configure

  • 服务器配置主要由app.configure()完成

app.js是运行Pomelo项目的入口,在app.js文件中首先会创建一个app实例,这个app作为整个框架的配置上下文来使用,用户可使用app.configure()来配置,通过上下文设置全局变量,加载配置信息等操作。

完整的app.configure配置参数格式:

app.configure([env], [serverType], [function]);
参数 描述
env 运行环境,可设置为developmentproductiondevelopment|production
serverType 服务器类型,设置后只会对当前参数类型服务器做初始化,不设置则对所有服务器执行初始化的function。比如gateconnectorchat...
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;
};

app.configure()针对不同的服务器和环境,对框架进行不同的配置。

配置功能 描述
app.loadConfig 加载应用配置
app.set/app.get 设置上下文变量供应用使用
app.enable/app.disable 开启功能选项
app.route 路由管理
app.filter 针对不同的服务器,配置过滤器filter等配置操作。
- 配置加载自定义的组件component
app.registerAdmin

app.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配置文件中都需要指定具体的模式,也就是developmentproduction

/**
 * 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 设置应用变量
  • app.get 获取应用变量

上下文变量存取是指上下文对象app提供了设置和获取应用变量的方法,签名为:

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;

app.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();

app.enable/app.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");

app.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());
});

app.filter

实际应用中,往往需要在逻辑服务器处理请求之前对用户请求做一些前置处理,当请求被处理后又需要做一些善后处理,由于这是一种常见的情形。Pomelo对其进行了抽象,也就是filter。在Pomelo中filter分为before filter和after filter。在一个请求到达Handler被处理之前,可以经过多个before filter组成的filter链进行一些前置处理,比如对请求进行排队,超时处理。当请求被Handler处理完成后,又可以通过after filter链进行一些善后处理。这里需要注意的是在after filter中一般只做一些清理处理,而不应该再去修改到客户端的响应内容。因为此时,对客户端的响应内容已经发送给了客户端。

filter链

filter分为beforeafter两类,每个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组件的启动过程

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启动活动图
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342