Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

the code of pomelo

  • Loading branch information...
commit 163e6dbbce1598d2ef0d827f1f8240fdb740308a 0 parents
@xiecc xiecc authored
Showing with 9,810 additions and 0 deletions.
  1. +22 −0 LICENSE
  2. +9 −0 Makefile
  3. +59 −0 README.md
  4. +382 −0 bin/pomelo
  5. +1 −0  index.js
  6. +644 −0 lib/application.js
  7. +41 −0 lib/common/remote/backend/msgRemote.js
  8. +28 −0 lib/common/remote/frontend/channelRemote.js
  9. +26 −0 lib/common/remote/frontend/sessionRemote.js
  10. +281 −0 lib/common/service/channelService.js
  11. +73 −0 lib/common/service/connectionService.js
  12. +107 −0 lib/common/service/filterService.js
  13. +62 −0 lib/common/service/handlerService.js
  14. +157 −0 lib/common/service/localSessionService.js
  15. +424 −0 lib/common/service/sessionService.js
  16. +45 −0 lib/common/service/taskManager.js
  17. +8 −0 lib/components/channel.js
  18. +30 −0 lib/components/connection.js
  19. +169 −0 lib/components/connector.js
  20. +8 −0 lib/components/localSession.js
  21. +46 −0 lib/components/master.js
  22. +29 −0 lib/components/monitor.js
  23. +169 −0 lib/components/proxy.js
  24. +103 −0 lib/components/remote.js
  25. +55 −0 lib/components/server.js
  26. +59 −0 lib/components/session.js
  27. +88 −0 lib/components/sync.js
  28. +39 −0 lib/connectors/sioconnector.js
  29. +60 −0 lib/connectors/siosocket.js
  30. +38 −0 lib/filters/handler/serial.js
  31. +33 −0 lib/filters/handler/time.js
  32. +37 −0 lib/filters/handler/timeout.js
  33. +38 −0 lib/filters/rpc/rpcLog.js
  34. +1 −0  lib/index.js
  35. +113 −0 lib/master/master.js
  36. +166 −0 lib/master/starter.js
  37. +24 −0 lib/modules/afterStart.js
  38. +93 −0 lib/modules/console.js
  39. +81 −0 lib/monitor/monitor.js
  40. +95 −0 lib/pomelo.js
  41. +235 −0 lib/server/server.js
  42. +41 −0 lib/util/countDownLatch.js
  43. +10 −0 lib/util/log.js
  44. +58 −0 lib/util/pathUtil.js
  45. +103 −0 lib/util/utils.js
  46. +27 −0 package.json
  47. +13 −0 scripts/getSenceUser.js
  48. +1 −0  scripts/getServerId.js
  49. +5 −0 scripts/getSystemInfo.js
  50. +15 −0 template/game-server/app.js
  51. +19 −0 template/game-server/app/servers/connector/handler/entryHandler.js
  52. +64 −0 template/game-server/config/log4js.json
  53. +8 −0 template/game-server/config/master.json
  54. +12 −0 template/game-server/config/servers.json
  55. +9 −0 template/game-server/package.json
  56. +5 −0 template/npm-install.sh
  57. +27 −0 template/web-server/app.js
  58. +8 −0 template/web-server/package.json
  59. +24 −0 template/web-server/public/css/base.css
  60. BIN  template/web-server/public/image/logo.png
  61. BIN  template/web-server/public/image/sp.png
  62. +41 −0 template/web-server/public/index.html
  63. +456 −0 template/web-server/public/js/lib/pomeloclient.js
  64. +3,788 −0 template/web-server/public/js/lib/socket.io.js
  65. +175 −0 test/applicationTest.js
  66. +64 −0 test/config/log4js.json
  67. +11 −0 test/config/master.json
  68. +8 −0 test/config/servers.json
  69. +47 −0 test/filters/handler/serialTest.js
  70. +48 −0 test/filters/handler/timeTest.js
  71. +47 −0 test/filters/handler/timeoutTest.js
  72. +18 −0 test/filters/rpc/rpcLogTest.js
  73. +19 −0 test/pomeloTest.js
  74. +80 −0 test/service/channelServiceTest.js
  75. +82 −0 test/service/connectionServiceTest.js
  76. +159 −0 test/service/filterServiceTest.js
  77. +61 −0 test/service/handlerServiceTest.js
  78. +25 −0 test/service/taskManagerTest.js
  79. +54 −0 test/util/countDownLatchTest.js
22 LICENSE
@@ -0,0 +1,22 @@
+(The MIT License)
+
+Copyright (c) 2012 Netease, Inc. and other pomelo contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 Makefile
@@ -0,0 +1,9 @@
+TESTS = test/*
+REPORTER = spec
+TIMEOUT = 5000
+
+test:
+ @./node_modules/.bin/mocha \
+ --reporter $(REPORTER) --timeout $(TIMEOUT) $(TESTS)
+
+.PHONY: test
59 README.md
@@ -0,0 +1,59 @@
+## pomelo - a short description
+Pomelo is a fast, scalable game server framework for [node.js](http://nodejs.org).
+It provides the basic development framework and a lot of related components, including libraries and tools.
+Pomelo is also suitable for realtime web application, its distributed architecture makes pomelo scales better than other realtime web framework.
+
+## Features
+
+* High scalable multi-process architecture, supporting MMO based area partition and other partition strategies
+* Easy extention mechnisam, you can scale up your server types and server instances conveniently.
+* Easy request, response, broadcast and rpc mechnisam, almost zero configuration.
+* Focus on performance, a lot of stress testing and tune in performance and scalability
+* Providing a lot tools and libraries, which are quite useful for game development.
+* Providing full MMO demo code(html5 client), for good development reference.
+* Based on socket.io, which means it can support all the clients that compatible with socket.io.
+
+## Why should you use pomelo?
+Fast, scalable, realtime game server development is not an easy job. A good container or framework can reduce the complexity.
+Unfortunately, not like web, the game server framework solution is quite rare, especially open source. Pomelo will fill this blank, providing a full solution for building game server framework.
+The following are the advantages:
+* The architecture is scalable. It uses multi-process, single thread runtime architecture, which has been proved in industry and especially suitable for node.js thread model.
+* Easy to use, the development model is quite similiar to web, using convention over configuration, almost zero config. The api is also easy to use.
+* The framework is extensible. Based on node.js micro module principle, the core of pomelo is small. All the components, libraries and tools are individual npm modules, anyone can create their own module to extend the framework.
+* The reference is quite complete, we have complete documents.Besides documents, we also provide a full open source MMO demo(html5 client), which is a far more better reference than any books.
+
+## How to develop with pomelo?
+With the following references, we can quickly familiar the pomelo development process:
+* [the architecture overview of pomelo](https://github.com/NetEase/pomelo/wiki/Architecture-overview-of-pomelo)
+* [quick start guide](https://github.com/NetEase/pomelo/wiki/Quick-start-guide)
+* [tutoiral](https://github.com/NetEase/pomelo/wiki/Tutorial)
+* [FAQ](https://github.com/NetEase/pomelo/wiki/FAQ)
+
+You can also learn from our MMO demo:
+* [an introduction to demo --- lord of pomelo](https://github.com/NetEase/pomelo/wiki/Introduction-to--Lord-of-Pomelo)
+
+
+## License
+
+(The MIT License)
+
+Copyright (c) 2012 Netease, Inc. and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
382 bin/pomelo
@@ -0,0 +1,382 @@
+#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ */
+var fs = require('fs'),
+ path = require('path'),
+ util = require('util'),
+ cliff = require('cliff'),
+ cp = require('child_process'),
+ exec = cp.exec,
+ spawn = cp.spawn,
+ co = require('../lib/modules/console'),
+ utils = require('../lib/util/utils'),
+ adminClient = require('pomelo-admin').adminClient;
+
+/**
+ * Common Variables
+ */
+var DEV = 'development';
+var PRD = 'production';
+var DEM = '--daemon';
+var DAEMON = false;
+var TIME_INIT = 1 * 1000;
+var TIME_KILL_WAIT = 5 * 1000;
+
+var CUR_DIR = process.cwd();
+var IF_HOME = utils.endsWith(CUR_DIR, 'game-server');
+var IF_WORKSPACE = fs.existsSync('./app.js') || fs.existsSync('game-server/app.js');
+var FOREVER = fs.existsSync(path.dirname(process.execPath) + '/forever');
+var HOME = IF_HOME ? CUR_DIR : path.join(CUR_DIR, 'game-server');
+var LOGS_DIR = IF_HOME ? path.join(CUR_DIR, 'logs') : path.join(CUR_DIR, 'game-server/logs');
+var MASTER_CONFIG = path.resolve(HOME, 'config/master.json');
+var TMP_FILE = path.resolve(LOGS_DIR, 'tmp');
+var KILL_CMD = 'kill -9 `ps -ef|grep node|awk \'{print $2}\'`';
+var MASTER_JSON;
+
+var NOWORKSPACE_ERROR = 'Please go to your pomelo project workspace to operate the application.';
+var COMMAND_ERROR = 'The command is error format.';
+var INSTALLFOREVER_ERROR = 'Please install forever use command: npm install forever -g.';
+var CONNECT_ERROR = 'Fail to connect to admin console server.';
+var NODEINFO_ERROR = 'Fail to request node information.';
+var STOPSERVER_ERROR = 'Fail to stop server.';
+var FILEREAD_ERROR = 'Fail to read the file, please check if the application is started legally.';
+var RUNDAEMON_INFO = 'Application run in daemon.\nStop the application use the command:pomelo stop.';
+var CLOSEAPP_INFO = 'Closing the application......\nPlease wait......';
+/**
+ * Usage documentation.
+ */
+var usage = '' + '\n' + ' Usage: pomelo [action] [option]\n' + '\n' + ' Options:\n' + ' init [path] create new application\n' + ' start [option] start the application\n' + ' list list server information\n' + ' stop stop the application\n' + ' kill [--force] kill the application\n' + ' version output framework version\n' + ' help output help information\n' + ' [option] developement(default)/production --daemon';
+
+/**
+ * Framework version.
+ */
+var version = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'))).version;
+
+/**
+ * Parse command arguments.
+ */
+var args = process.argv.slice(2);
+
+(function() {
+ var arg = args.shift();
+ switch(arg) {
+ case 'help':
+ legalArgNum(0);
+ abort(usage);
+ break;
+ case 'version':
+ legalArgNum(0);
+ abort(version);
+ break;
+ case 'list':
+ legalArgNum(0);
+ list();
+ break;
+ case 'init':
+ legalArgNum(1);
+ init(args[0]);
+ break;
+ case 'start':
+ start();
+ break;
+ case 'stop':
+ legalArgNum(0);
+ terminal('stop');
+ break;
+ case 'kill':
+ terminal('kill');
+ break;
+ default:
+ abort(COMMAND_ERROR);
+ break;
+ }
+})();
+
+/**
+ * Init application at the given directory `path`.
+ *
+ * @param {String} path
+ */
+function init(path) {
+ emptyDirectory(path, function(empty) {
+ if(empty) {
+ createApplicationAt(path);
+ } else {
+ confirm('destination is not empty, continue? (y/n)', function(ok) {
+ if(ok) {
+ process.stdin.destroy();
+ createApplicationAt(path);
+ } else {
+ abort('aborting');
+ }
+ });
+ }
+ });
+};
+
+/**
+ * Create directory and files at the given directory `path`.
+ *
+ * @param {String} ph
+ */
+function createApplicationAt(ph) {
+ var name = path.basename(path.resolve(CUR_DIR, ph));
+ copy(path.join(__dirname, '../template/'), ph);
+ mkdir(path.join(ph, 'game-server/logs'), 0755);
+ mkdir(path.join(ph, 'shared'), 0755);
+ setTimeout(function() {
+ var repalcefile = [path.resolve(ph, 'game-server/app.js'), path.resolve(ph, 'game-server/package.json'), path.resolve(ph, 'web-server/package.json')];
+ for(var i = 0; i < repalcefile.length; i++) {
+ var str = fs.readFileSync(repalcefile[i]).toString();
+ fs.writeFileSync(repalcefile[i], str.replace('$', name));
+ }
+ var f = path.resolve(ph, 'game-server/package.json');
+ var content = fs.readFileSync(f).toString();
+ fs.writeFileSync(f, content.replace('#', version));
+ }, TIME_INIT);
+};
+
+/**
+ * Start application.
+ *
+ */
+function start() {
+ var mode = DEV;
+ switch(args.length) {
+ case 0:
+ break;
+ case 1:
+ if(args[0] == DEV || args[0] == PRD) mode = args[0];
+ else if(args[0] == DEM) DAEMON = true;
+ else abort(COMMAND_ERROR);
+ break;
+ case 2:
+ if(args[0] == DEV || args[0] == PRD) {
+ mode = args[0];
+ if(args[1] == DEM) DAEMON = true;
+ else abort(COMMAND_ERROR);
+ } else abort(COMMAND_ERROR);
+ break;
+ default:
+ abort(COMMAND_ERROR);
+ break;
+ };
+ if(IF_HOME) {
+ var ls;
+ if(!DAEMON) {
+ ls = spawn(process.execPath, [path.resolve(HOME, 'app.js'), 'env=' + mode]);
+ ls.stdout.on('data', function(data) {
+ console.log(data.toString());
+ });
+ ls.stderr.on('data', function(data) {
+ console.log(data.toString());
+ });
+ } else {
+ if(FOREVER) {
+ var cmd = 'forever start app.js env=' + mode;
+ ls = exec(cmd);
+ console.info(RUNDAEMON_INFO);
+ } else abort(INSTALLFOREVER_ERROR);
+ }
+ fs.writeFile(TMP_FILE, mode + ':' + DAEMON);
+ } else abort(NOWORKSPACE_ERROR);
+};
+
+/**
+ * List pomelo processes.
+ *
+ */
+function list() {
+ if(IF_WORKSPACE) {
+ fs.readFile(TMP_FILE, function(err, data) {
+ if(err) abort(FILEREAD_ERROR);
+ var client = new adminClient();
+ MASTER_JSON = require(MASTER_CONFIG);
+ var config = MASTER_JSON[data.toString().split(':')[0]];
+ var id = 'pomelo_list_' + Date.now();
+ client.connect(id, config.host, config.port, function(err) {
+ if(err) abort(CONNECT_ERROR);
+ else {
+ client.request(co.moduleId, {
+ signal: 'list'
+ }, function(err, data) {
+ if(err) {
+ console.error(err);
+ abort(NODEINFO_ERROR);
+ }
+ var msg = data.msg;
+ var rows = [];
+ rows.push(['serverId', 'serverType', 'pid', 'cpuAvg', 'memAvg', 'time']);
+ for(var key in msg) {
+ var server = msg[key];
+ rows.push([server['serverId'], server['serverType'], server['pid'], server['cpuAvg'], server['memAvg'], server['time']]);
+ }
+ console.log(cliff.stringifyRows(rows, ['red', 'blue', 'green', 'white', 'yellow', 'grey']));
+ process.exit(1);
+ });
+ }
+ });
+ });
+ } else abort(NOWORKSPACE_ERROR);
+};
+
+/**
+ * Terminal application.
+ *
+ * @param {String} signal stop/kill
+ *
+ */
+function terminal(signal) {
+ if(args.length > 1 || (args.length === 1 && args[0] !== '--force')) abort(COMMAND_ERROR);
+ if(IF_WORKSPACE) {
+ console.info(CLOSEAPP_INFO);
+ if(args[0] === '--force') {
+ exec(KILL_CMD);
+ process.exit(1);
+ }
+ fs.readFile(TMP_FILE, function(err, data) {
+ if(err) abort(FILEREAD_ERROR);
+ var client = new adminClient();
+ MASTER_JSON = require(MASTER_CONFIG);
+ var config = MASTER_JSON[data.toString().split(':')[0]];
+ var id = 'pomelo_terminal_' + Date.now();
+ if(data.toString().split(':')[1] == 'true') {
+ setTimeout(function() {
+ if(IF_HOME) exec('forever stop app.js');
+ else exec('cd game-server && forever stop app.js');
+ process.exit(1);
+ }, TIME_KILL_WAIT);
+ }
+ client.connect(id, config.host, config.port, function(err) {
+ if(err) abort(CONNECT_ERROR);
+ client.request(co.moduleId, {
+ signal: signal
+ }, function(err, msg) {
+ if(err) abort(STOPSERVER_ERROR);
+ if(msg.status === "error") abort(STOPSERVER_ERROR);
+ setTimeout(function() {
+ exec(KILL_CMD);
+ fs.unlinkSync(TMP_FILE);
+ process.exit(1);
+ }, TIME_KILL_WAIT);
+ });
+ });
+ });
+ } else abort(NOWORKSPACE_ERROR);
+};
+
+
+/**
+ * Check if the given directory `path` is empty.
+ *
+ * @param {String} path
+ * @param {Function} fn
+ */
+function emptyDirectory(path, fn) {
+ fs.readdir(path, function(err, files) {
+ if(err && 'ENOENT' != err.code) abort(FILEREAD_ERROR);
+ fn(!files || !files.length);
+ });
+};
+
+/**
+ * Prompt confirmation with the given `msg`.
+ *
+ * @param {String} msg
+ * @param {Function} fn
+ */
+function confirm(msg, fn) {
+ prompt(msg, function(val) {
+ fn(/^ *y(es)?/i.test(val));
+ });
+};
+
+/**
+ * Prompt input with the given `msg` and callback `fn`.
+ *
+ * @param {String} msg
+ * @param {Function} fn
+ */
+function prompt(msg, fn) {
+ if(' ' == msg[msg.length - 1]) process.stdout.write(msg);
+ else console.log(msg);
+ process.stdin.setEncoding('ascii');
+ process.stdin.once('data', function(data) {
+ fn(data);
+ }).resume();
+};
+
+/**
+ * Exit with the given `str`.
+ *
+ * @param {String} str
+ */
+function abort(str) {
+ console.error(str);
+ process.exit(1);
+};
+
+/**
+ * Check whether the number of arguments is legal.
+ *
+ * @param {Number} argNum
+ */
+function legalArgNum(argNum) {
+ if(args.length != argNum) abort(COMMAND_ERROR);
+};
+
+/**
+ * Copy template files to project.
+ *
+ * @param {String} origin
+ * @param {String} target
+ */
+function copy(origin, target) {
+ if(!fs.existsSync(origin)) abort(origin + 'is not exist.');
+ if(!fs.existsSync(target)) {
+ mkdir(target, 0);
+ console.log('Make dir: ' + target);
+ }
+ fs.readdir(origin, function(err, datalist) {
+ if(err) abort(FILEREAD_ERROR);
+ for(var i = 0; i < datalist.length; i++) {
+ var oCurrent = path.resolve(origin, datalist[i]);
+ var tCurrent = path.resolve(target, datalist[i]);
+ if(fs.statSync(oCurrent).isFile()) {
+ fs.writeFileSync(tCurrent, fs.readFileSync(oCurrent, ''), '');
+ console.log('Create file: ' + tCurrent);
+ } else if(fs.statSync(oCurrent).isDirectory()) copy(oCurrent, tCurrent);
+ }
+ });
+};
+
+/**
+ * Make directory for new project.
+ *
+ * @param {String} path
+ * @param {Number} mode
+ */
+function mkdir(url, mode) {
+ var arr = url.split('/');
+ mode = mode || 0755;
+ switch(arr[0]) {
+ case '.':
+ arr.shift();
+ break;
+ case '..':
+ arr.splice(0, 2, arr[0] + '/' + arr[1]);
+ break;
+ case '':
+ arr[0] = "/";
+ break;
+ };
+
+ function inner(cur) {
+ if(!fs.existsSync(cur)) fs.mkdirSync(cur, mode);
+ if(arr.length) inner(cur + '/' + arr.shift());
+ }
+ arr.length && inner(arr.shift());
+ console.log('Make dir: ' + url);
+};
1  index.js
@@ -0,0 +1 @@
+module.exports = require('./lib/pomelo');
644 lib/application.js
@@ -0,0 +1,644 @@
+/*!
+ * Pomelo -- proto
+ * Copyright(c) 2012 xiechengchao <xiecc@163.com>
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+var fs = require('fs');
+var utils = require('./util/utils');
+var logger = require('pomelo-logger').getLogger(__filename);
+var async = require('async');
+var log = require('./util/log');
+
+/**
+ * Application prototype.
+ *
+ * @module
+ */
+var Application = module.exports = {};
+
+/**
+ * Application states
+ */
+var STATE_INITED = 1; // app has inited
+var STATE_START = 2; // app start
+var STATE_STARTED = 3; // app has started
+var STATE_STOPED = 4; // app has stoped
+
+/**
+ * Initialize the server.
+ *
+ * - setup default configuration
+ *
+ * @api private
+ */
+Application.init = function(opts) {
+ opts = opts || {};
+ logger.info('app.init invoked');
+ this.loaded = [];
+ this.components = {};
+ this.settings = {};
+ this.set('base', opts.base);
+ this.defaultConfiguration();
+ this.state = STATE_INITED;
+ logger.info('application inited: %j', this.get('serverId'));
+};
+
+/**
+ * Get application base path
+ *
+ * // cwd: /home/game/
+ * pomelo start
+ * // app.getBase() -> /home/game
+ *
+ * @return {String} application base path
+ *
+ * @memberOf Application
+ */
+Application.getBase = function() {
+ return this.get('base') || process.cwd();
+};
+
+/**
+ * Initialize application configuration.
+ *
+ * @api private
+ */
+Application.defaultConfiguration = function () {
+ var args = utils.argsInfo(process.argv);
+ this.setupEnv(args);
+ this.loadServers();
+ this.loadConfig('master', this.getBase() + '/config/master.json');
+ this.processArgs(args);
+ this.configLogger();
+};
+
+/**
+ * Setup enviroment.
+ * @api private
+ */
+Application.setupEnv = function(args) {
+ this.set('env', args.env || process.env.NODE_ENV || 'development', true);
+};
+
+/**
+ * Load server info from configure file.
+ *
+ * @api private
+ */
+Application.loadServers = function() {
+ this.loadConfig('servers', this.getBase() + '/config/servers.json');
+ var servers = this.get('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;
+ serverMap[server.id] = server;
+ }
+ }
+
+ this.set('__serverMap__', serverMap);
+};
+
+/**
+ * Process server start command
+ *
+ * @return {Void}
+ *
+ * @api private
+ */
+Application.processArgs = function(args){
+ var serverType = args.serverType || 'master';
+ var serverId = args.serverId || this.get('master').id;
+ this.set('main', args.main, true);
+ this.set('serverType', serverType, true);
+ this.set('serverId', serverId, true);
+ if(serverType !== 'master') {
+ this.set('curServer', this.getServerById(serverId), true);
+ } else {
+ this.set('curServer', this.get('master'), true);
+ }
+};
+
+/**
+ * Load default components for application.
+ *
+ * @api private
+ */
+Application.loadDefaultComponents = function(){
+ var pomelo = require('./pomelo');
+ // load system default components
+ if (this.serverType === 'master') {
+ this.load(pomelo.master, this.get('masterConfig'));
+ } else {
+ this.load(pomelo.proxy, this.get('proxyConfig'));
+ if(this.getServerById(this.get('serverId')).port) {
+ this.load(pomelo.remote, this.get('remoteConfig'));
+ }
+ if(this.isFrontend()) {
+ this.load(pomelo.connection, this.get('connectionConfig'));
+ this.load(pomelo.connector, this.get('connectorConfig'));
+ this.load(pomelo.session, this.get('sessionConfig'));
+ } else {
+ this.load(pomelo.localSession, this.get('localSessionConfig'));
+ }
+ this.load(pomelo.channel, this.get('channelConfig'));
+ this.load(pomelo.server, this.get('serverConfig'));
+ }
+ this.load(pomelo.monitor, this);
+};
+
+Application.configLogger = function() {
+ if(process.env.POMELO_LOGGER !== 'off') {
+ log.configure(this, this.getBase() + '/config/log4js.json');
+ }
+};
+
+/**
+ * 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);
+ return this;
+};
+
+/**
+ * Add before filter.
+ *
+ * @param {Object|Function} bf before fileter, bf(msg, session, next)
+ *
+ * @memberOf Application
+ */
+Application.before = function (bf) {
+ var befores = this.get('__befores__');
+ if(!befores) {
+ befores = [];
+ this.set('__befores__', befores);
+ }
+ befores.push(bf);
+ return this;
+};
+
+/**
+ * Add after filter.
+ *
+ * @param {Object|Function} af after filter, `af(err, msg, session, resp, next)`
+ *
+ * @memberOf Application
+ */
+Application.after = function (af) {
+ var afters = this.get('__afters__');
+ if(!afters) {
+ afters = [];
+ this.set('__afters__', afters);
+ }
+ afters.push(af);
+ return this;
+};
+
+/**
+ * Load component
+ *
+ * @param {String} name (optional) name of the component
+ * @param {Object} component component instance or factory function of the component
+ * @param {[type]} opts (optional) construct parameters for the factory function
+ * @return {Object} app instance for chain invoke
+ *
+ * @memberOf Application
+ */
+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(!component) {
+ // maybe some component no need to join the components management
+ logger.info('load empty component');
+ return this;
+ }
+
+ 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;
+};
+
+/**
+ * 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('__routes__');
+ if(!routes) {
+ routes = {};
+ this.set('__routes__', routes);
+ }
+ routes[serverType] = routeFunc;
+ return this;
+};
+
+/**
+ * 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) {
+ if(this.state > STATE_INITED) {
+ utils.invokeCallback(cb, new Error('application has already start.'));
+ return;
+ }
+ this.loadDefaultComponents();
+ var self = this;
+ this._optComponents('start', function(err) {
+ self.state = STATE_START;
+ utils.invokeCallback(cb, err);
+ });
+};
+
+/**
+ * Lifecycle callback for after start.
+ *
+ * @param {Function} cb callback function
+ * @return {Void}
+ */
+Application.afterStart = function(cb) {
+ if(this.state !== STATE_START) {
+ utils.invokeCallback(cb, new Error('application is not running now.'));
+ return;
+ }
+
+ var self = this;
+ this._optComponents('afterStart', function(err) {
+ self.state = STATE_STARTED;
+ utils.invokeCallback(cb, err);
+ });
+};
+
+/**
+ * Stop components.
+ *
+ * @param {Boolean} force whether stop the app immediately
+ */
+Application.stop = function(force) {
+ if(this.state > STATE_STARTED) {
+ logger.warn('[pomelo application] application is not running now.');
+ return;
+ }
+ this.state = STATE_STOPED;
+ stopComps(this.loaded, 0, force, function() {
+ if(force) {
+ process.exit(0);
+ }
+ });
+
+};
+
+/**
+ * Stop components.
+ *
+ * @param {Array} comps component list
+ * @param {Number} index current component index
+ * @param {Boolean} force whether stop component immediately
+ * @param {Function} cb
+ */
+var stopComps = function(comps, index, force, cb) {
+ if(index >= comps.length) {
+ cb();
+ return;
+ }
+ var comp = comps[index];
+ if(typeof comp.stop === 'function') {
+ comp.stop(force, function() {
+ // ignore any error
+ stopComps(comps, index +1, force, cb);
+ });
+ } else {
+ stopComps(comps, index +1, force, cb);
+ }
+};
+
+/**
+ * Apply command to loaded components.
+ * This method would invoke the component {method} in series.
+ * Any component {method} return err, it would return err directly.
+ *
+ * @param {String} method component lifecycle method name, such as: start, afterStart, stop
+ * @param {Function} cb
+ * @api private
+ */
+Application._optComponents = function(method, cb) {
+ var i = 0;
+ async.forEachSeries(this.loaded, function(comp, done) {
+ i++;
+ if(typeof comp[method] === 'function') {
+ comp[method](done);
+ } else {
+ done();
+ }
+ }, function(err) {
+ if(err) {
+ logger.error('[pomelo application] fail to operate component, method:%s, err:' + err.stack, method);
+ }
+ cb(err);
+ });
+};
+
+/**
+ * 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;
+};
+
+/**
+ * Get property from setting
+ *
+ * @param {String} setting application setting
+ * @return {String} val
+ *
+ * @memberOf Application
+ */
+Application.get = function (setting) {
+ return this.settings[setting];
+};
+
+/**
+ * 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('env');
+ val = require(val);
+ if (val[env]) {
+ val = val[env];
+ }
+ this.set(key, val);
+};
+
+/**
+ * Check if `setting` is enabled.
+ *
+ * @param {String} setting application setting
+ * @return {Boolean}
+ * @memberOf Application
+ */
+Application.enabled = function (setting) {
+ return !!this.get(setting);
+};
+
+/**
+ * Check if `setting` is disabled.
+ *
+ * @param {String} setting application setting
+ * @return {Boolean}
+ * @memberOf Application
+ */
+Application.disabled = function (setting) {
+ return !this.get(setting);
+};
+
+/**
+ * Enable `setting`.
+ *
+ * @param {String} setting application setting
+ * @return {app} for chaining
+ * @memberOf Application
+ */
+Application.enable = function (setting) {
+ return this.set(setting, true);
+};
+
+/**
+ * Disable `setting`.
+ *
+ * @param {String} setting application setting
+ * @return {app} for chaining
+ * @memberOf Application
+ */
+Application.disable = function (setting) {
+ return this.set(setting, false);
+};
+
+/**
+ * Configure callback for the specified env and server type.
+ * When no env is specified that callback will
+ * be invoked for all environments and when no type is specified
+ * that callback will be invoked for all server types.
+ *
+ * Examples:
+ *
+ * app.configure(function(){
+ * // executed for all envs and server types
+ * });
+ *
+ * app.configure('development', function(){
+ * // executed development env
+ * });
+ *
+ * app.configure('development', 'connector', function(){
+ * // executed for development env and connector server type
+ * });
+ *
+ * @param {String} env application environment
+ * @param {Function} fn callback function
+ * @param {String} type server type
+ * @return {Application} for chaining
+ * @memberOf Application
+ */
+Application.configure = function (env, type, fn) {
+ var args = [].slice.call(arguments);
+ fn = args.pop();
+ env = 'all';
+ type = 'all';
+
+ if(args.length > 0) {
+ env = args[0];
+ }
+ if(args.length > 1) {
+ type = args[1];
+ }
+
+ if (env === 'all' || env.indexOf(this.settings.env) >= 0) {
+ if (type === 'all' || type.indexOf(this.settings.serverType) >= 0) {
+ fn.call(this);
+ }
+ }
+ return this;
+};
+
+/**
+ * Get all the server infos.
+ *
+ * @return {Object} server info map, key: server id, value: server info
+ *
+ * @memberOf Application
+ */
+Application.getServers = function() {
+ return this.get('__serverMap__');
+};
+
+/**
+ * Get server info by server id.
+ *
+ * @param {String} serverId server id
+ * @return {Object} server info or undefined
+ *
+ * @memberOf Application
+ */
+Application.getServerById = function(serverId) {
+ return this.get('__serverMap__')[serverId];
+};
+
+/**
+ * Get server infos by server type.
+ *
+ * @param {String} serverType server type
+ * @return {Array} server info list
+ *
+ * @memberOf Application
+ */
+Application.getServersByType = function(serverType) {
+ return this.get('servers')[serverType];
+};
+
+/**
+ * Check the server whether is a frontend server
+ *
+ * @param {server} server server info. it would check current server
+ * if server not specified
+ * @return {Boolean}
+ *
+ * @memberOf Application
+ */
+Application.isFrontend = function(server) {
+ server = server || Application.get('curServer');
+ return !!server && !!server.wsPort;
+};
+
+/**
+ * Check the server whether is a backend server
+ *
+ * @param {server} server server info. it would check current server
+ * if server not specified
+ * @return {Boolean}
+ *
+ * @memberOf Application
+ */
+Application.isBackend = function(server) {
+ server = server || Application.get('curServer');
+ return !!server && !server.wsPort;
+};
+
+/**
+ * Check whether current server is a master server
+ *
+ * @return {Boolean}
+ *
+ * @memberOf Application
+ */
+Application.isMaster = function() {
+ return Application.serverType === 'master';
+};
+
+/**
+ * Register admin modules. Admin modules is the extends point of the monitor system.
+ *
+ * @param {String} module (optional) module id or provoided by module.moduleId
+ * @param {Object} module module object or factory function for module
+ * @param {Object} opts construct parameter for module
+ *
+ * @memberOf Application
+ */
+Application.registerAdmin = function(moduleId, module, opts){
+ var modules = this.get('__modules__');
+ if(!modules) {
+ modules = [];
+ this.set('__modules__', modules);
+ }
+
+ if(typeof moduleId !== 'string') {
+ opts = module;
+ module = moduleId;
+ moduleId = module.moduleId;
+ }
+
+ modules.push({moduleId: moduleId, module: module, opts: opts});
+};
41 lib/common/remote/backend/msgRemote.js
@@ -0,0 +1,41 @@
+/**
+ * Remote service for backend servers.
+ * Receive and process request message that forward from frontend server.
+ */
+module.exports = function(app) {
+ return new Remote(app);
+};
+
+var Remote = function(app) {
+ this.app = app;
+};
+
+/**
+ * Forward message from frontend server to other server's handlers
+ *
+ * @param msg {Object} request message
+ * @param session {Object} session object for current request
+ * @param cb {Function} callback function
+ */
+Remote.prototype.forwardMessage = function(msg, session, cb) {
+ var server = this.app.components.__server__;
+ var sessionService = this.app.components.__localSession__;
+
+ if(!server) {
+ cb(new Error('server component not enable'));
+ return;
+ }
+
+ if(!sessionService) {
+ cb(new Error('local session component not enable'));
+ return;
+ }
+
+ // generate local session for current request
+ var lsession = sessionService.create(session);
+
+ // handle the request
+ server.handle(msg, lsession, function(err, resp) {
+ cb(err, resp);
+ });
+};
28 lib/common/remote/frontend/channelRemote.js
@@ -0,0 +1,28 @@
+/**
+ * Remote channel service for frontend server.
+ * Receive push request from backend servers and push it to clients.
+ */
+var logger = require('pomelo-logger').getLogger(__filename);
+
+module.exports = function(app) {
+ return new Remote(app);
+};
+
+var Remote = function(app) {
+ this.app = app;
+};
+
+/**
+ * Push message to client by uids
+ *
+ * @param msg {Object} message that would be push to clients
+ * @param uids {Array} user ids that would receive the message
+ * @param cb {Function} callback function
+ */
+Remote.prototype.pushMessage = function(msg, uids, cb) {
+ var sessionService = this.app.get('sessionService');
+ for(var i=0, l=uids.length; i<l; i++) {
+ sessionService.sendMessageByUid(uids[i], msg);
+ }
+ cb();
+};
26 lib/common/remote/frontend/sessionRemote.js
@@ -0,0 +1,26 @@
+/**
+ * Remote session service for frontend server.
+ * Set session info for backend servers.
+ */
+var logger = require('pomelo-logger').getLogger(__filename);
+
+
+module.exports = function(app) {
+ return new Remote(app);
+};
+
+var Remote = function(app) {
+ this.app = app;
+};
+
+Remote.prototype.bind = function(sid, uid, cb) {
+ this.app.get('sessionService').bind(sid, uid, cb);
+};
+
+Remote.prototype.push = function(sid, key, value, cb) {
+ this.app.get('sessionService').import(sid, key, value, cb);
+};
+
+Remote.prototype.pushAll = function(sid, settings, cb) {
+ this.app.get('sessionService').importAll(sid, settings, cb);
+};
281 lib/common/service/channelService.js
@@ -0,0 +1,281 @@
+var countDownLatch = require('../../util/countDownLatch');
+var utils = require('../../util/utils');
+var logger = require('pomelo-logger').getLogger(__filename);
+var async = require('async');
+
+/**
+ * constant
+ */
+var DEFAULT_GROUP_ID = 'default';
+
+var ST_INITED = 0;
+var ST_DESTROYED = 1;
+
+/**
+ * Create and maintain channels for server local.
+ *
+ * ChannelService is created by channel component which is a default loaded
+ * component of pomelo and channel service would be accessed by `app.get('channelService')`.
+ *
+ * @class
+ * @constructor
+ */
+var ChannelService = function(app) {
+ this.app = app;
+ this.channels = {};
+};
+
+module.exports = ChannelService;
+
+/**
+ * Create channel with name.
+ *
+ * @param {String} name channel's name
+ * @memberOf ChannelService
+ */
+ChannelService.prototype.createChannel = function(name) {
+ if(this.channels[name]) {
+ return this.channels[name];
+ }
+
+ var c = new Channel(name, this);
+ this.channels[name] = c;
+ return c;
+};
+
+/**
+ * Get channel by name.
+ *
+ * @param {String} name channel's name
+ * @param {Boolean} create if true, create channel
+ * @return {Channel}
+ * @memberOf ChannelService
+ */
+ChannelService.prototype.getChannel = function(name, create) {
+ var channel = this.channels[name];
+ if(!channel && !!create) {
+ channel = this.channels[name] = new Channel(name, this);
+ }
+ return channel;
+};
+
+/**
+ * Destroy channel by name.
+ *
+ * @param {String} name channel name
+ * @memberOf ChannelService
+ */
+ChannelService.prototype.destroyChannel = function(name) {
+ delete this.channels[name];
+};
+
+/**
+ * Push message by uids.
+ * Group the uids by group. ignore any uid if sid not specified.
+ *
+ * @param {Object} msg message that would be sent to client
+ * @param {Array} uids the receiver info list, [{uid: userId, sid: frontendServerId}]
+ * @param {Function} cb cb(err)
+ * @memberOf ChannelService
+ */
+ChannelService.prototype.pushMessageByUids = function(msg, uids, cb) {
+ if(!uids || uids.length === 0) {
+ utils.invokeCallback(cb, new Error('uids should not be empty'));
+ return;
+ }
+ var groups = {}, record;
+ for(var i=0, l=uids.length; i<l; i++) {
+ record = uids[i];
+ add(record.uid, record.sid, groups);
+ }
+
+ sendMessageByGroup(this, msg, groups, cb);
+};
+
+/**
+ * Channel maintains the receiver collection for a subject. You can
+ * add users into a channel and then broadcast message to them by channel.
+ *
+ * @class channel
+ * @constructor
+ */
+var Channel = function(name, service) {
+ this.name = name;
+ this.groups = {}; // group map for uids. key: sid, value: [uid]
+ this.records = {}; // member records. key: uid
+ this.__channelService__ = service;
+ this.state = ST_INITED;
+};
+
+/**
+ * Add user to channel.
+ *
+ * @param {Number} uid user id
+ * @param {String} sid frontend server id which user has connected to
+ */
+Channel.prototype.add = function(uid, sid) {
+ if(this.state > ST_INITED) {
+ return false;
+ } else {
+ var res = add(uid, sid, this.groups);
+ if(res) {
+ this.records[uid] = {sid: sid, uid: uid};
+ }
+ return res;
+ }
+};
+
+/**
+ * Remove user from channel.
+ *
+ * @param {Number} uid user id
+ * @param {String} sid frontend server id which user has connected to.
+ * @return [Boolean] true if success or false if fail
+ */
+Channel.prototype.leave = function(uid, sid) {
+ delete this.records[uid];
+ return deleteFrom(uid, sid, this.groups[sid]);
+};
+
+/**
+ * Get channel members.
+ *
+ * <b>Notice:</b> Heavy operation.
+ *
+ * @return {Array} channel member uid list
+ */
+Channel.prototype.getMembers = function() {
+ var res = [];
+ if(!this.groups) {
+ return res;
+ }
+
+ var group, i, l;
+ for(var sid in this.groups) {
+ group = this.groups[sid];
+ for(i=0, l=group.length; i<l; i++) {
+ res.push(group[i]);
+ }
+ }
+ return res;
+};
+
+/**
+ * Get Member info.
+ *
+ * @param {String} uid user id
+ * @return {Object} member info
+ */
+Channel.prototype.getMember = function(uid) {
+ return this.records[uid];
+};
+
+/**
+ * Destroy channel.
+ */
+Channel.prototype.destroy = function() {
+ this.state = ST_DESTROYED;
+ this.__channelService__.destroyChannel(this.name);
+};
+
+/**
+ * Push message to all the members in the channel
+ *
+ * @param {Object} msg message that would be sent to client
+ * @param {Functioin} cb callback function
+ */
+Channel.prototype.pushMessage = function(msg, cb) {
+ if(this.state !== ST_INITED) {
+ utils.invokeCallback(new Error('channel not init'));
+ return;
+ }
+ sendMessageByGroup(this.__channelService__, msg, this.groups, cb);
+};
+
+/**
+ * add uid and sid into group. ignore any uid that uid not specified.
+ *
+ * @param uid user id
+ * @param sid server id
+ * @param groups {Object} grouped uids, , key: sid, value: [uid]
+ */
+var add = function(uid, sid, groups) {
+ if(!sid) {
+ logger.warn('ignore uid %j for sid not specified.', uid);
+ return false;
+ }
+
+ var group = groups[sid];
+ if(!group) {
+ group = [];
+ groups[sid] = group;
+ }
+
+ group.push(uid);
+ return true;
+};
+
+/**
+ * delete element from array
+ */
+var deleteFrom = function(uid, sid, group) {
+ if(!group) {
+ return true;
+ }
+
+ for(var i=0, l=group.length; i<l; i++) {
+ if(group[i] === uid) {
+ group.splice(i, 1);
+ return true;
+ }
+ }
+
+ return false;
+};
+
+/**
+ * push message by group
+ *
+ * @param msg {Object} message that would be sent to client
+ * @param groups {Object} grouped uids, , key: sid, value: [uid]
+ * @param cb {Function} cb(err)
+ *
+ * @api private
+ */
+var sendMessageByGroup = function(channelService, msg, groups, cb) {
+ var app = channelService.app;
+ var namespace = 'sys';
+ var service = 'channelRemote';
+ var method = 'pushMessage';
+ var count = utils.size(groups);
+ var successFlag = false;
+ msg = JSON.stringify(msg);
+
+ if(count === 0) {
+ // group is empty
+ utils.invokeCallback(cb);
+ return;
+ }
+
+ var latch = countDownLatch.createCountDownLatch(count, function(){
+ if(!successFlag) {
+ utils.invokeCallback(cb, new Error('all uids push message fail'));
+ return;
+ }
+ utils.invokeCallback(cb);
+ });
+
+ for(var sid in groups) {
+ var uids = groups[sid];
+ app.rpcInvoke(sid, {namespace: namespace, service: service, method: method, args: [msg, uids]}, function(err) {
+ if(err) {
+ logger.error('[pushMessage] fail to dispatch msg, err:' + err.stack);
+ latch.done();
+ return;
+ }
+ successFlag = true;
+ latch.done();
+ });
+ }
+
+};
73 lib/common/service/connectionService.js
@@ -0,0 +1,73 @@
+/**
+ * connection statistics service
+ * record connection, login count and list
+ */
+var Service = function(app) {
+ this.serverId = app.get('serverId');
+ this.connCount = 0;
+ this.loginedCount = 0;
+ this.logined = {};
+};
+
+module.exports = Service;
+
+var pro = Service.prototype;
+
+
+/**
+ * Add logined user.
+ *
+ * @param uid {String} user id
+ * @param info {Object} record for logined user
+ */
+pro.addLoginedUser = function(uid, info) {
+ if(!this.logined[uid]) {
+ this.loginedCount++;
+ }
+ this.logined[uid] = info;
+};
+
+/**
+ * Increase connection count
+ */
+pro.increaseConnectionCount = function() {
+ this.connCount++;
+};
+
+/**
+ * Remote logined user
+ *
+ * @param uid {String} user id
+ */
+pro.removeLoginedUser = function(uid) {
+ if(!!this.logined[uid]) {
+ this.loginedCount--;
+ }
+ delete this.logined[uid];
+};
+
+/**
+ * Decrease connection count
+ *
+ * @param uid {String} uid
+ */
+pro.decreaseConnectionCount = function(uid) {
+ this.connCount--;
+ if(!!uid) {
+ this.removeLoginedUser(uid);
+ }
+};
+
+/**
+ * Get statistics info
+ *
+ * @return {Object} statistics info
+ */
+pro.getStatisticsInfo = function() {
+ var list = [];
+ for(var uid in this.logined) {
+ list.push(this.logined[uid]);
+ }
+
+ return {serverId: this.serverId, totalConnCount: this.connCount, loginedCount: this.loginedCount, loginedList: list};
+};
107 lib/common/service/filterService.js
@@ -0,0 +1,107 @@
+var logger = require('pomelo-logger').getLogger(__filename);
+
+/**
+ * Filter service.
+ * Register and fire before and after filters.
+ */
+var Service = function() {
+ this.befores = []; // before filters
+ this.afters = []; // after filters
+};
+
+module.exports = Service;
+
+Service.prototype.name = 'filter';
+
+/**
+ * Add before filter into the filter chain.
+ *
+ * @param filter {Object|Function} filter instance or filter function.
+ */
+Service.prototype.before = function(filter){
+ this.befores.push(filter);
+};
+
+/**
+ * Add after filter into the filter chain.
+ *
+ * @param filter {Object|Function} filter instance or filter function.
+ */
+Service.prototype.after = function(filter){
+ this.afters.unshift(filter);
+};
+
+/**
+ * TODO: other insert method for filter? such as unshift
+ */
+
+/**
+ * Do the before filter.
+ * Fail over if any filter pass err parameter to the next function.
+ *
+ * @param msg {Object} clienet request msg
+ * @param session {Object} a session object for current request
+ * @param cb {Function} cb(err) callback function to invoke next chain node
+ */
+Service.prototype.beforeFilter = function(msg, session, cb) {
+ var index = 0, self = this;
+ var next = function(err, resp) {
+ if(index >= self.befores.length) {
+ // if done
+ cb();
+ return;
+ }
+ if(err) {
+ // if error
+ cb(err, resp);
+ return;
+ }
+
+ var handler = self.befores[index++];
+ if(typeof handler === 'function') {
+ handler(msg, session, next);
+ } else if(typeof handler.before === 'function') {
+ handler.before(msg, session, next);
+ } else {
+ logger.warn('meet invalid before filter, ignore it.');
+ next(err);
+ }
+ }; //end of next
+
+ next();
+};
+
+/**
+ * Do after filter chain.
+ * Give server a chance to do clean up jobs after request responsed.
+ * After filter can not change the request flow before.
+ * After filter should call the next callback to let the request pass to next after filter.
+ *
+ * @param err {Object} error object
+ * @param session {Object} session object for current request
+ * @param {Object} resp response object send to client
+ * @param cb {Function} cb(err) callback function to invoke next chain node
+ */
+Service.prototype.afterFilter = function(err, msg, session, resp, cb) {
+ var index = 0, self = this;
+ function next(err) {
+ //if done
+ if(index >= self.afters.length) {
+ cb(err, resp);
+ return;
+ }
+
+ var handler = self.afters[index++];
+ if(typeof handler === 'function') {
+ handler(err, msg, session, resp, next);
+ } else if(typeof handler.after === 'function') {
+ handler.after(err, msg, session, resp, next);
+ } else {
+ logger.error('meet invalid after filter, ignore it.');
+ next(err, resp);
+ }
+ } //end of next
+
+ next(err, resp);
+};
+
62 lib/common/service/handlerService.js
@@ -0,0 +1,62 @@
+var logger = require('pomelo-logger').getLogger(__filename);
+var forward_logger = require('pomelo-logger').getLogger('forward-log');
+var utils = require('../../util/utils');
+
+/**
+ * Handler service.
+ * Dispatch request to the relactive handler.
+ *
+ * @param {Object} app current application context
+ * @param {Object} handlers handler map
+ */
+var Service = function(app, handlers) {
+ this.app = app;
+ this.handlers = handlers || {};
+};
+
+module.exports = Service;
+
+Service.prototype.name = 'handler';
+
+/**
+ * Handler the request.
+ */
+Service.prototype.handle = function(routeRecord, msg, session, cb){
+ // the request should be processed by current server
+ var handler = getHandler(this.handlers, routeRecord);
+ if(!handler) {
+ logger.error('[handleManager]: fail to find handler for %j', msg.__route__);
+ cb(new Error('fail to find handler for ' + msg.__route__));
+ return;
+ }
+ var start = Date.now();
+ handler[routeRecord.method](msg, session, function(err,resp){
+ var log = {
+ route : msg.__route__,
+ args : msg,
+ time : utils.format(new Date(start)),
+ timeUsed : new Date() - start
+ };
+ forward_logger.info(JSON.stringify(log));
+ cb(err,resp);
+ });
+ return;
+};
+
+/**
+ * Get handler instance by routeRecord.
+ *
+ * @param {Object} handlers handler map
+ * @param {Object} routeRecord route record parsed from route string
+ * @return {Object} handler instance if any matchs or null for match fail
+ */
+var getHandler = function(handlers, routeRecord) {
+ var handler = handlers[routeRecord.handler];
+ if(!handler) {
+ return null;
+ }
+ if(typeof handler[routeRecord.method] !== 'function') {
+ return null;
+ }
+ return handler;
+};
157 lib/common/service/localSessionService.js
@@ -0,0 +1,157 @@
+/**
+ * Mock session service for sessionService
+ */
+var EventEmitter = require('events').EventEmitter;
+var util = require('util');
+var utils = require('../../util/utils');
+
+var EXPORT_INCLUDE_FIELDS = ['id', 'frontendId', 'uid', 'settings'];
+
+/**
+ * Service the maintain local sessions and the communiation
+ * with frontend server.
+ */
+var LocalSessionService = function(app) {
+ this.app = app;
+};
+
+module.exports = LocalSessionService;
+
+LocalSessionService.prototype.create = function(opts) {
+ if(!opts) {
+ throw new Error('opts should not be empty.');
+ }
+ return new LocalSession(opts, this);
+};
+
+LocalSessionService.prototype.bind = function(frontendId, sid, uid, cb) {
+ var namespace = 'sys';
+ var service = 'sessionRemote';
+ var method = 'bind';
+ var args = [sid, uid];
+ rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
+};
+
+LocalSessionService.prototype.push = function(frontendId, sid, key, value, cb) {
+ var namespace = 'sys';
+ var service = 'sessionRemote';
+ var method = 'push';
+ var args = [sid, key, value];
+ rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
+};
+
+LocalSessionService.prototype.pushAll = function(frontendId, sid, settings, cb) {
+ var namespace = 'sys';
+ var service = 'sessionRemote';
+ var method = 'pushAll';
+ var args = [sid, settings];
+ rpcInvoke(this.app, frontendId, namespace, service, method, args, cb);
+};
+
+var rpcInvoke = function(app, sid, namespace, service, method, args, cb) {
+ app.rpcInvoke(sid, {namespace: namespace, service: service, method: method, args: args}, cb);
+};
+
+/**
+ * LocalSession is the proxy for global session passed to handlers and it helps to keep
+ * the key/value pairs for the server local. Global session locates in frontend server
+ * and should not be accessed directly.
+ *
+ * The mainly operation on local session should be read and any changes happen in local
+ * session is local and would be discarded in next request. You have to push the
+ * changes to the frontend manually if necessary. Any push would overwrite the last push
+ * of the same key silently and the changes would be saw in next request.
+ * And you have to make sure the transaction outside if you would push the session
+ * concurrently in different processes.
+ *
+ * See the api below for more details.
+ *
+ * @class
+ * @constructor
+ */
+var LocalSession = function(opts, service) {
+ EventEmitter.call(this);
+ for(var f in opts) {
+ this[f] = opts[f];
+ }
+ this.__sessionService__ = service;
+};
+
+util.inherits(LocalSession, EventEmitter);
+
+/**
+ * Bind current session with the user id. It would push the uid to frontend
+ * server and bind uid to the global session.
+ *
+ * @param {Number|String} uid user id
+ * @param {Function} cb callback function
+ *
+ * @memberOf LocalSession
+ */
+LocalSession.prototype.bind = function(uid, cb) {
+ var self = this;
+ this.__sessionService__.bind(this, uid, function(err) {
+ if(!err) {
+ self.uid = uid;
+ }
+ cb(err);
+ });
+};
+
+/**
+ * Set the key/value into local session.
+ *
+ * @param {String} key key
+ * @param {Object} value value
+ */
+LocalSession.prototype.set = function(key, value) {
+ this.settings[key] = value;
+};
+
+/**
+ * Get the value from local session by key.
+ *
+ * @param {String} key key
+ * @return {Object} value
+ */
+LocalSession.prototype.get = function(key) {
+ return this.settings[key];
+};
+
+/**
+ * Push the key/value in local session to the global session.
+ *
+ * @param {String} key key
+ * @param {Function} cb callback function
+ */
+LocalSession.prototype.push = function(key, cb) {
+ this.__sessionService__.push(this.frontendId, this.id, key, this.get(key), cb);
+};
+
+/**
+ * Push all the key/values in local session to the global session.
+ *
+ * @param {Function} cb callback function
+ */
+LocalSession.prototype.pushAll = function(cb) {
+ this.__sessionService__.pushAll(this.frontendId, this.id, this.settings, cb);
+};
+
+/**
+ * Export the key/values for serialization.
+ *
+ * @api private
+ */
+LocalSession.prototype.export = function() {
+ var res = {}, f;
+ for(var i=0, l=EXPORT_INCLUDE_FIELDS.length; i<l; i++) {
+ f = EXPORT_INCLUDE_FIELDS[i];
+ res[f] = this[f];
+ }
+
+ return res;
+};
+
+var rpcInvoke = function(app, sid, namespace, service, method, args, cb) {
+ app.rpcInvoke(sid, {namespace: namespace, service: service, method: method, args: args}, cb);
+};
424 lib/common/service/sessionService.js
@@ -0,0 +1,424 @@
+var EventEmitter = require('events').EventEmitter;
+var util = require('util');
+var logger = require('pomelo-logger').getLogger(__filename);
+var utils = require('../../util/utils');
+
+var MOCK_INCLUDE_FIELDS = ['id', 'frontendId', 'uid', '__sessionService__'];
+var EXPORT_INCLUDE_FIELDS = ['id', 'frontendId', 'uid', 'settings'];
+
+var ST_INITED = 0;
+var ST_CLOSED = 1;
+
+/**
+ * Session service manages the sessions for each client connection.
+ *
+ * Session service is created by session component and is only
+ * <b>available</b> in frontend servers. You can access the service by
+ * `app.get('sessionService')` in frontend servers.
+ *
+ * @param {Object} opts constructor parameters
+ * opts.sendDirectly - whether send the request to the client or cache them until next flush.
+ * @class
+ * @constructor
+ */
+var SessionService = function(opts) {
+ opts = opts || {};
+ this.sendDirectly = opts.sendDirectly;
+ this.sessions = {};
+ this.uidMap = {};
+ this.msgQueues = {};
+};
+
+module.exports = SessionService;
+
+/**
+ * Create and return session.
+ *
+ * @param {Object} opts {key:obj, uid: str, and etc.}
+ * @param {Boolean} force whether replace the origin session if it already existed
+ * @return {Session}
+ *
+ * @memberOf SessionService
+ * @api private
+ */
+SessionService.prototype.create = function(sid, frontendId, socket) {
+ var session = new Session(sid, frontendId, socket, this);
+ this.sessions[session.id] = session;
+
+ return session;
+};
+
+/**
+ * Bind the session with a user id.
+ *
+ * @memberOf SessionService
+ * @api private
+ */
+SessionService.prototype.bind = function(sid, uid, cb) {
+ var session = this.sessions[sid];
+
+ if(!session) {
+ cb(new Error('session not exist, sid: ' + sid));
+ return;
+ }
+
+ session.bind(uid);
+ cb();
+};
+
+/**
+ * Get session by id.
+ *
+ * @param {Number} id The session id
+ * @return {Session}
+ *
+ * @memberOf SessionService
+ * @api private
+ */
+SessionService.prototype.get = function(sid) {
+ return this.sessions[sid];
+};
+
+/**
+ * Get session by userId.
+ *
+ * @param {Number} uid User id associated with the session
+ * @return {Session}
+ *
+ * @memberOf SessionService
+ * @api private
+ */
+SessionService.prototype.getByUid = function(uid) {
+ return this.uidMap[uid];
+};
+
+/**
+ * Remove session by key.
+ *
+ * @param {Number} sid The session id
+ *
+ * @memberOf SessionService
+ * @api private
+ */
+SessionService.prototype.remove = function(sid) {
+ var session = this.sessions[sid];
+ if(session) {
+ delete this.sessions[session.id];
+ delete this.uidMap[session.uid];
+ delete this.msgQueues[session.id];
+ }
+};
+
+/**
+ * Import the key/value into session.
+ *
+ * @api private
+ */
+SessionService.prototype.import = function(sid, key, value, cb) {
+ var session = this.sessions[sid];
+ if(!session) {
+ utils.invokeCallback(cb, new Error('session not exist, sid: ' + sid));
+ return;
+ }
+ session.set(key, value);
+ utils.invokeCallback(cb);
+};
+
+/**
+ * Import new value for the existed session.
+ *
+ * @memberOf SessionService
+ * @api private
+ */
+SessionService.prototype.importAll = function(sid, settings, cb) {
+ var session = this.sessions[sid];
+ if(!session) {
+ utils.invokeCallback(cb, new Error('session not exist, sid: ' + sid));
+ return;
+ }
+
+ for(var f in settings) {
+ session.set(f, settings[f]);
+ }
+ utils.invokeCallback(cb);
+};
+
+/**
+ * Kick a user offline by user id.
+ *
+ * @param {Number} uid user id asscociated with the session
+ * @param {Function} cb callback function
+ *
+ * @memberOf SessionService
+ */
+SessionService.prototype.kick = function(uid, cb) {
+ var session = this.getByUid(uid);
+
+ if(session) {
+ // notify client
+ session.__socket__.send({route: 'onKick'});
+ process.nextTick(function() {
+ session.closed('kick');
+ utils.invokeCallback(cb);
+ });
+ } else {
+ process.nextTick(function() {
+ utils.invokeCallback(cb);
+ });
+ }
+};
+
+/**
+ * Send message to the client by session id.
+ *
+ * @param {String} sid session id
+ * @param {Object} msg message to send
+ *
+ * @memberOf SessionService
+ * @api private
+ */
+SessionService.prototype.sendMessage = function(sid, msg) {
+ var session = this.sessions[sid];
+
+ if(!session) {
+ logger.debug('fail to send message for session not exits');
+ return false;
+ }
+
+ return send(this, session, msg);
+};
+
+/**
+ * Send message to the client by user id.
+ *
+ * @param {String} uid userId
+ * @param {Object} msg message to send
+ *
+ * @memberOf SessionService
+ * @api private
+ */
+SessionService.prototype.sendMessageByUid = function(uid, msg) {
+ var session = this.uidMap[uid];
+
+ if(!session) {
+ logger.debug('fail to send message by uid for session not exist. uid: %j', uid);
+ return false;
+ }
+
+ return send(this, session, msg);
+};
+
+/**
+ * Send message to the client that associated with the session.
+ *
+ * @api private
+ */
+var send = function(service, session, msg) {
+ if(service.sendDirectly) {
+ session.__socket__.send(encode(msg));
+ return true;
+ }
+
+ var sid = session.id;
+ var queue = service.msgQueues[sid];
+ if(!queue) {
+ queue = [];
+ service.msgQueues[sid] = queue;
+ }
+
+ queue.push(msg);
+ return true;
+};
+
+/**
+ * Flush messages to clients.
+ *
+ * @memberOf SessionService
+ * @api private
+ */
+SessionService.prototype.flush = function() {
+ var queues = this.msgQueues, sessions = this.sessions, queue, session;
+ for(var sid in queues) {
+ queue = queues[sid];
+ if(!queue || queue.length === 0) {
+ continue;
+ }
+
+ session = sessions[sid];
+ if(session && session.__socket__) {
+ session.__socket__.send(encode(queue));
+ } else {
+ logger.debug('fail to send message for socket not exist.');
+ }
+
+ delete queues[sid];
+ }
+};
+
+/**
+ * Session maintains the relationship between client connect and user information.
+ * There is a session associated with each client connect. And it should bind to a
+ * user id after the client passes the identification.
+ *
+ * Session is generated in frontend server and should not be access in handler.
+ * There is a proxy class called LocalSession in backend servers and MockLocalSession
+ * in frontend servers.
+ */
+var Session = function(sid, frontendId, socket, service) {
+ EventEmitter.call(this);
+ this.id = sid; // r
+ this.frontendId = frontendId; // r
+ this.uid = null; // r
+ this.settings = {};
+
+ // private
+ this.__socket__ = socket;
+ this.__sessionService__ = service;
+ this.__state__ = ST_INITED;
+};
+
+util.inherits(Session, EventEmitter);
+
+/**
+ * Export current session as mock local session.
+ */
+Session.prototype.mockLocalSession = function() {
+ return new MockLocalSession(this);
+};
+
+/**
+ * Bind the session with the the uid.
+ *
+ * @param {Number} uid User id
+ * @api public
+ */
+Session.prototype.bind = function(uid) {
+ this.__sessionService__.uidMap[uid] = this;
+ this.uid = uid;
+ this.emit('bind', uid);
+};
+
+/**
+ * Set value for the session.
+ *
+ * @param {String} key session key
+ * @param {Object} value session value
+ * @api public
+ */
+Session.prototype.set = function(key, value) {
+ this.settings[key] = value;
+};
+
+/**
+ * Get value from the session.
+ *
+ * @param {String} key session key
+ * @return {Object} value associated with session key
+ * @api public
+ */
+Session.prototype.get = function(key, value) {
+ return this.settings[key];
+};
+
+/**
+ * Closed callback for the session.
+ *
+ * @api public
+ */
+Session.prototype.closed = function(reason) {
+ if(this.__state__ === ST_CLOSED) {
+ return;
+ }
+ this.__state__ = ST_CLOSED;
+ this.__sessionService__.remove(this.id);
+ this.emit('closed', this.mockLocalSession(), reason);
+ this.__socket__.disconnect();
+};
+
+/**
+ * Mock local session for frontend server.
+ * Local session is used as session in the backend servers(see
+ * lib/common/service/mockLocalSession.js).
+ */
+var MockLocalSession = function(session) {
+ EventEmitter.call(this);
+ clone(session, this, MOCK_INCLUDE_FIELDS);
+ // deep copy for settings
+ this.settings = dclone(session.settings);
+ this.__session__ = session;
+};
+
+util.inherits(MockLocalSession, EventEmitter);
+
+MockLocalSession.prototype.bind = function(uid, cb) {
+ var self = this;
+ this.__sessionService__.bind(this.id, uid, function(err) {
+ if(!err) {
+ self.uid = uid;
+ }
+ utils.invokeCallback(cb, err);
+ });
+};
+
+MockLocalSession.prototype.set = function(key, value) {
+ this.settings[key] = value;
+};
+
+MockLocalSession.prototype.get = function(key) {
+ return this.settings[key];
+};
+
+MockLocalSession.prototype.push = function(key, cb) {
+ this.__sessionService__.import(this.id, key, this.get(key), cb);
+};
+
+MockLocalSession.prototype.pushAll = function(cb) {
+ this.__sessionService__.importAll(this.id, this.settings, cb);
+};
+
+MockLocalSession.prototype.on = function(event, listener) {
+ EventEmitter.prototype.on.call(this, event, listener);
+ this.__session__.on(event, listener);
+};
+
+MockLocalSession.prototype.export = function() {
+ var res = {};
+ clone(this, res, EXPORT_INCLUDE_FIELDS);
+ return res;
+};
+
+var clone = function(src, dest, includes) {
+ var f;
+ for(var i=0, l=includes.length; i<l; i++) {
+ f = includes[i];
+ dest[f] = src[f];
+ }
+};
+
+var dclone = function(src) {
+ var res = {};
+ for(var f in src) {
+ res[f] = src[f];
+ }
+ return res;
+};
+
+/**
+ * Encode msg to client
+ */
+var encode = function(msgs){
+ var res = '[', msg;
+ for(var i=0, l=msgs.length; i<l; i++) {
+ if(i > 0) {
+ res += ',';
+ }
+ msg = msgs[i];