├── .eslintrc.js ├── .gitignore ├── .idea └── jsLinters │ └── jshint.xml ├── .travis.yml ├── LICENSE ├── README.md ├── bin └── pomelo ├── coverage └── blanket.js ├── gruntfile.js ├── index.js ├── lib ├── application.js ├── common │ ├── manager │ │ ├── appManager.js │ │ └── taskManager.js │ ├── remote │ │ ├── backend │ │ │ └── msgRemote.js │ │ └── frontend │ │ │ ├── channelRemote.js │ │ │ └── sessionRemote.js │ └── service │ │ ├── backendSessionService.js │ │ ├── channelService.js │ │ ├── connectionService.js │ │ ├── filterService.js │ │ ├── handlerService.js │ │ └── sessionService.js ├── components │ ├── backendSession.js │ ├── channel.js │ ├── connection.js │ ├── connector.js │ ├── dictionary.js │ ├── master.js │ ├── monitor.js │ ├── protobuf.js │ ├── proxy.js │ ├── pushScheduler.js │ ├── remote.js │ ├── server.js │ └── session.js ├── connectors │ ├── commands │ │ ├── handshake.js │ │ ├── heartbeat.js │ │ └── kick.js │ ├── common │ │ ├── coder.js │ │ └── handler.js │ ├── hybrid │ │ ├── hybridSocket.js │ │ ├── switcher.js │ │ ├── tcpprocessor.js │ │ ├── tcpsocket.js │ │ └── wsprocessor.js │ ├── hybridConnector.js │ ├── mqtt │ │ ├── generate.js │ │ ├── mqttSocket.js │ │ ├── mqttadaptor.js │ │ └── protocol.js │ ├── mqttConnector.js │ ├── sio │ │ └── sioSocket.js │ ├── sioConnector.js │ ├── udp │ │ └── udpSocket.js │ └── udpConnector.js ├── filters │ ├── handler │ │ ├── serial.js │ │ ├── time.js │ │ ├── timeout.js │ │ └── toobusy.js │ └── rpc │ │ ├── rpcLog.js │ │ └── toobusy.js ├── index.js ├── master │ ├── master.js │ ├── starter.js │ └── watchdog.js ├── modules │ ├── console.js │ ├── masterwatcher.js │ └── monitorwatcher.js ├── monitor │ └── monitor.js ├── pomelo.js ├── pushSchedulers │ ├── buffer.js │ └── direct.js ├── server │ └── server.js └── util │ ├── constants.js │ ├── countDownLatch.js │ ├── events.js │ ├── log.js │ ├── moduleUtil.js │ ├── pathUtil.js │ └── utils.js ├── package.json ├── template ├── game-server │ ├── app.js │ ├── app.js.mqtt │ ├── app.js.sio │ ├── app.js.sio.wss │ ├── app.js.udp │ ├── app.js.wss │ ├── app │ │ └── servers │ │ │ └── connector │ │ │ └── handler │ │ │ └── entryHandler.js │ ├── config │ │ ├── adminServer.json │ │ ├── adminUser.json │ │ ├── clientProtos.json │ │ ├── dictionary.json │ │ ├── log4js.json │ │ ├── master.json │ │ ├── serverProtos.json │ │ └── servers.json │ └── package.json ├── npm-install.bat ├── npm-install.sh ├── shared │ ├── server.crt │ └── server.key └── web-server │ ├── app.js │ ├── app.js.https │ ├── bin │ ├── component.bat │ └── component.sh │ ├── package.json │ └── public │ ├── css │ └── base.css │ ├── image │ ├── logo.png │ └── sp.png │ ├── index.html │ ├── index.html.sio │ └── js │ └── lib │ ├── build │ ├── build.js │ └── build.js.wss │ ├── component.json │ ├── local │ └── boot │ │ ├── component.json │ │ └── index.js │ ├── pomeloclient.js │ ├── pomeloclient.js.wss │ └── socket.io.js └── test ├── application.js ├── config ├── log4js.json ├── master.json └── servers.json ├── filters ├── handler │ ├── serial.js │ ├── time.js │ ├── timeout.js │ └── toobusy.js └── rpc │ ├── rpcLog.js │ └── toobusy.js ├── manager ├── mockChannelManager.js └── taskManager.js ├── mock-base ├── .gitignore └── app │ ├── .gitignore │ └── servers │ ├── .file-start-with-dot │ ├── .folder-start-with-dot │ └── .gitignore │ ├── .gitignore │ ├── area │ └── .gitignore │ ├── connector │ ├── .gitignore │ ├── handler │ │ └── .gitignore │ └── remote │ │ └── .gitignore │ └── other-file ├── mock-plugin ├── components │ └── mockPlugin.js └── events │ └── mockEvent.js ├── modules └── console.js ├── pomelo.js ├── remote └── channelRemote.js ├── service ├── channel.js ├── channelService.js ├── connectionService.js ├── filterService.js ├── handlerService.js └── sessionService.js └── util ├── countDownLatch.js ├── pathUtil.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | */node-log.log 3 | logs/*.log 4 | *.log 5 | !.gitignore 6 | node_modules/* 7 | .project 8 | .settings/ 9 | **/*.svn 10 | *.svn 11 | *.swp 12 | *.sublime-project 13 | *.sublime-workspace 14 | lib/doc/ 15 | lib-cov/ 16 | coverage.html 17 | .DS_Store 18 | .idea/* -------------------------------------------------------------------------------- /.idea/jsLinters/jshint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "6.x" 5 | - "7.x" 6 | before_script: 7 | - npm install -g grunt-cli -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 frank198 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 网易 pomelo 1.2.3 2 | 3 | 修改内容 4 | - es6 语法 5 | - 依赖库升级 6 | - socket.io 1.7.x 7 | 8 | 9 | ## 使用说明 10 | 11 | - pomelo@1.x => pomelo-upgrade@1.x 12 | - pomelo@2.x => pomelo-upgrade@2.x 13 | 14 | ## 其他插件 15 | 16 | - 全局Channel: [pomeloGlobalChannel](https://github.com/frank198/pomeloGlobalChannel) 17 | 18 | ## Pomelo -- a fast, scalable game server framework for node.js 19 | 20 | Pomelo is a fast, scalable game server framework for [node.js](http://nodejs.org). 21 | It provides the basic development framework and many related components, including libraries and tools. 22 | Pomelo is also suitable for real-time web applications; its distributed architecture makes pomelo scale better than other real-time web frameworks. 23 | 24 | [](https://travis-ci.org/NetEase/pomelo) 25 | 26 | * Homepage: 27 | * Mailing list: 28 | * Documentation: 29 | * Wiki: 30 | * Issues: 31 | * Tags: game, nodejs 32 | 33 | 34 | ## Features 35 | 36 | ### Complete support of game server and realtime application server architecture 37 | 38 | * Multiple-player game: mobile, social, web, MMO rpg(middle size) 39 | * Realtime application: chat, message push, etc. 40 | 41 | ### Fast, scalable 42 | 43 | * Distributed (multi-process) architecture, can be easily scale up 44 | * Flexible server extension 45 | * Full performance optimization and test 46 | 47 | ### Easy 48 | 49 | * Simple API: request, response, broadcast, etc. 50 | * Lightweight: high development efficiency based on node.js 51 | * Convention over configuration: almost zero config 52 | 53 | ### Powerful 54 | 55 | * Many clients support, including javascript, flash, android, iOS, cocos2d-x, C 56 | * Many libraries and tools, including command line tool, admin tool, performance test tool, AI, path finding etc. 57 | * Good reference materials: full docs, many examples and [an open-source MMO RPG demo](https://github.com/NetEase/pomelo/wiki/Introduction-to--Lord-of-Pomelo) 58 | 59 | ### Extensible 60 | 61 | * Support plugin architecture, easy to add new features through plugins. We also provide many plugins like online status, master high availability. 62 | * Custom features, users can define their own network protocol, custom components very easy. 63 | 64 | ## Why should I use pomelo? 65 | Fast, scalable, real-time game server development is not an easy job, and a good container or framework can reduce its complexity. 66 | Unfortunately, unlike web, finding a game server framework solution is difficult, especially an open source solution. Pomelo fills this gap, providing a full solution for building game server frameworks. 67 | Pomelo has the following advantages: 68 | * The architecture is scalable. It uses a multi-process, single thread runtime architecture, which has been proven in the industry and is especially suited to the node.js thread model. 69 | * Easy to use, the development model is quite similar to web, using convention over configuration, with almost zero config. The [API](http://pomelo.netease.com/api.html) is also easy to use. 70 | * The framework is extensible. Based on the node.js micro module principle, the core of pomelo is small. All of the components, libraries and tools are individual npm modules, and anyone can create their own module to extend the framework. 71 | * The reference materials and documentation are quite complete. In addition to the documentation, we also provide [an open-source MMO RPG demo](https://github.com/NetEase/pomelo/wiki/Introduction-to--Lord-of-Pomelo) (HTML5 client), which is a far better reference material than any book. 72 | 73 | ## How can I develop with pomelo? 74 | With the following references, you can quickly familiarize yourself with the pomelo development process: 75 | * [Pomelo documents](https://github.com/NetEase/pomelo/wiki) 76 | * [Getting started](https://github.com/NetEase/pomelo/wiki/Welcome-to-Pomelo) 77 | * [Tutorial](https://github.com/NetEase/pomelo/wiki/Preface) 78 | 79 | 80 | ## Contributors 81 | * NetEase, Inc. (@NetEase) 82 | * Peter Johnson(@missinglink) 83 | * Aaron Yoshitake 84 | * @D-Deo 85 | * Eduard Gotwig 86 | * Eric Muyser(@stokegames) 87 | * @GeforceLee 88 | * Harold Jiang(@jzsues) 89 | * @ETiV 90 | * [kaisatec](https://github.com/kaisatec) 91 | * [roytan883](https://github.com/roytan883) 92 | * [wuxian](https://github.com/wuxian) 93 | * [zxc122333](https://github.com/zxc122333) 94 | * [newebug](https://github.com/newebug) 95 | * [jiangzhuo](https://github.com/jiangzhuo) 96 | * [youxiachai](https://github.com/youxiachai) 97 | * [qiankanglai](https://github.com/qiankanglai) 98 | * [xieren58](https://github.com/xieren58) 99 | * [prim](https://github.com/prim) 100 | * [Akaleth](https://github.com/Akaleth) 101 | * [pipi32167](https://github.com/pipi32167) 102 | * [ljhsai](https://github.com/ljhsai) 103 | * [zhanghaojie](https://github.com/zhanghaojie) 104 | * [airandfingers](https://github.com/airandfingers) 105 | 106 | ## License 107 | 108 | (The MIT License) 109 | 110 | -------------------------------------------------------------------------------- /coverage/blanket.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const srcDir = path.join(__dirname, '..', 'lib'); 3 | 4 | require('blanket')( 5 | { 6 | pattern : srcDir 7 | }); -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(grunt) 3 | { 4 | 5 | grunt.loadNpmTasks('grunt-mocha-test'); 6 | grunt.loadNpmTasks('grunt-contrib-clean'); 7 | require('load-grunt-tasks')(grunt); 8 | 9 | const src = ['test/manager/taskManager.js', 'test/filters/*.js', 10 | 'test/remote/*.js', 'test/service/*.js', 'test/modules/*.js', 'test/util/*.js', 'test/*.js']; 11 | 12 | // Project configuration. 13 | grunt.initConfig( 14 | { 15 | mochaTest : 16 | { 17 | test : { 18 | options : { 19 | reporter : 'spec' 20 | // timeout : 5000, 21 | // require : 'coverage/blanket' 22 | }, 23 | src : src 24 | }, 25 | coverage : { 26 | options : { 27 | reporter : 'html-cov', 28 | quiet : true, 29 | captureFile : 'coverage.html' 30 | }, 31 | src : src 32 | } 33 | }, 34 | clean : 35 | { 36 | 'coverage.html' : { 37 | src : ['coverage.html'] 38 | } 39 | }, 40 | eslint : 41 | { 42 | target : ['lib/*'] 43 | } 44 | 45 | }); 46 | 47 | // Default task. 48 | grunt.registerTask('default', ['eslint', 'clean', 'mochaTest']); 49 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/pomelo'); -------------------------------------------------------------------------------- /lib/common/manager/appManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by frank on 16-12-26. 3 | */ 4 | const _ = require('lodash'), 5 | async = require('async'), 6 | utils = require('../../util/utils'), 7 | logger = require('pomelo-logger').getLogger('pomelo', __filename), 8 | transactionLogger = require('pomelo-logger').getLogger('transaction-log', __filename), 9 | transactionErrorLogger = require('pomelo-logger').getLogger('transaction-error-log', __filename); 10 | 11 | class AppManager 12 | { 13 | static Transaction(name, conditions, handlers, retry) 14 | { 15 | if (!retry) 16 | { 17 | retry = 1; 18 | } 19 | if (!_.isString(name)) 20 | { 21 | logger.error('transaction name is error format, name: %s.', name); 22 | return; 23 | } 24 | if (!_.isObject(conditions) || !_.isObject(handlers)) 25 | { 26 | logger.error('transaction conditions parameter is error format, conditions: %j, handlers: %j.', conditions, handlers); 27 | return; 28 | } 29 | 30 | const cmethods = [], dmethods = [], cnames = [], dnames = []; 31 | _.forEach(conditions, (condition, key) => 32 | { 33 | if (!_.isString(key) || !_.isFunction(condition)) 34 | { 35 | logger.error('transaction conditions parameter is error format, condition name: %s, condition function: %j.', key, conditions[key]); 36 | return; 37 | } 38 | cnames.push(key); 39 | cmethods.push(condition); 40 | }); 41 | 42 | let i = 0; 43 | // execute conditions 44 | async.forEachSeries(cmethods, (method, cb) => 45 | { 46 | method(cb); 47 | transactionLogger.info('[%s]:[%s] condition is executed.', name, cnames[i]); 48 | i++; 49 | }, 50 | (err) => 51 | { 52 | if (err) 53 | { 54 | process.nextTick(() => 55 | { 56 | transactionLogger.error('[%s]:[%s] condition is executed with err: %j.', name, cnames[--i], err.stack); 57 | const log = { 58 | name : name, 59 | method : cnames[i], 60 | time : Date.now(), 61 | type : 'condition', 62 | description : err.stack 63 | }; 64 | transactionErrorLogger.error(JSON.stringify(log)); 65 | }); 66 | } 67 | else 68 | { 69 | // execute handlers 70 | process.nextTick(() => 71 | { 72 | _.forEach(handlers, (handle, key) => 73 | { 74 | if (!_.isString(key) || !_.isFunction(handle)) 75 | { 76 | logger.error('transcation handlers parameter is error format, handler name: %s, handler function: %j.', key, handlers[key]); 77 | return; 78 | } 79 | dnames.push(key); 80 | dmethods.push(handlers[key]); 81 | }); 82 | 83 | let flag = true; 84 | const times = retry; 85 | 86 | // do retry if failed util retry times 87 | async.whilst(() => 88 | { 89 | return retry > 0 && flag; 90 | }, 91 | callback => 92 | { 93 | let j = 0; 94 | retry--; 95 | async.forEachSeries(dmethods, (method, cb) => 96 | { 97 | method(cb); 98 | transactionLogger.info('[%s]:[%s] handler is executed.', name, dnames[j]); 99 | j++; 100 | }, 101 | (err) => 102 | { 103 | if (err) 104 | { 105 | process.nextTick(() => 106 | { 107 | transactionLogger.error('[%s]:[%s]:[%s] handler is executed with err: %j.', name, dnames[--j], times - retry, err.stack); 108 | const log = { 109 | name : name, 110 | method : dnames[j], 111 | retry : times - retry, 112 | time : Date.now(), 113 | type : 'handler', 114 | description : err.stack 115 | }; 116 | transactionErrorLogger.error(JSON.stringify(log)); 117 | utils.invokeCallback(callback); 118 | }); 119 | return; 120 | } 121 | flag = false; 122 | utils.invokeCallback(callback); 123 | process.nextTick(() => 124 | { 125 | transactionLogger.info('[%s] all conditions and handlers are executed successfully.', name); 126 | }); 127 | }); 128 | }, 129 | (err) => 130 | { 131 | if (err) 132 | { 133 | logger.error('transaction process is executed with error: %j', err); 134 | } 135 | // callback will not pass error 136 | }); 137 | }); 138 | } 139 | }); 140 | } 141 | } 142 | 143 | module.exports.transaction = AppManager.Transaction; -------------------------------------------------------------------------------- /lib/common/manager/taskManager.js: -------------------------------------------------------------------------------- 1 | const seQueue = require('seq-queue'); 2 | 3 | const queues = {}; 4 | 5 | let _timeout = 3000; 6 | 7 | class TaskManager 8 | { 9 | static set timeout(value) 10 | { 11 | _timeout = value; 12 | } 13 | 14 | static get timeout() 15 | { 16 | return _timeout; 17 | } 18 | 19 | /** 20 | * Add tasks into task group. Create the task group if it dose not exist. 21 | * 22 | * @param {String} key task key 23 | * @param {Function} fn task callback 24 | * @param {Function} onTimeout task timeout callback 25 | * @param {Number} timeout timeout for task 26 | */ 27 | static addTask(key, fn, onTimeout, timeout) 28 | { 29 | let queue = queues[key]; 30 | if (!queue) 31 | { 32 | queue = seQueue.createQueue(TaskManager.timeout); 33 | queues[key] = queue; 34 | } 35 | return queue.push(fn, onTimeout, timeout); 36 | } 37 | 38 | /** 39 | * Destroy task group 40 | * 41 | * @param {String} key task key 42 | * @param {Boolean} force whether close task group directly 43 | */ 44 | static closeQueue(key, force) 45 | { 46 | if (!queues[key]) 47 | { 48 | // ignore illeagle key 49 | return; 50 | } 51 | 52 | queues[key].close(force); 53 | delete queues[key]; 54 | } 55 | } 56 | 57 | module.exports = TaskManager; -------------------------------------------------------------------------------- /lib/common/remote/backend/msgRemote.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | utils = require('../../../util/utils'), 3 | logger = require('pomelo-logger').getLogger('forward-log', __filename); 4 | 5 | class MsgRemote 6 | { 7 | constructor(app) 8 | { 9 | this.app = app; 10 | } 11 | 12 | /** 13 | * Forward message from frontend server to other server's handlers 14 | * 15 | * @param msg {Object} request message 16 | * @param session {Object} session object for current request 17 | * @param cb {Function} callback function 18 | */ 19 | forwardMessage(msg, session, cb) 20 | { 21 | const components = this.app.components; 22 | const server = _.get(components, '__server__', null); 23 | 24 | if (!server) 25 | { 26 | logger.error(`server component not enable on ${this.app.serverId}`); 27 | utils.invokeCallback(cb, new Error('server component not enable')); 28 | return; 29 | } 30 | 31 | const sessionService = _.get(components, '__backendSession__', null); 32 | if (!sessionService) 33 | { 34 | logger.error(`backend session component not enable on ${this.app.serverId}`); 35 | utils.invokeCallback(cb, new Error('backend sesssion component not enable')); 36 | return; 37 | } 38 | 39 | // generate backend session for current request 40 | const backendSession = sessionService.create(session); 41 | 42 | // handle the request 43 | logger.debug(`backend server [${this.app.serverId}] handle message: ${msg}`); 44 | 45 | server.handle(msg, backendSession, function(err, resp, opts) 46 | { 47 | utils.invokeCallback(cb, err, resp, opts); 48 | }); 49 | } 50 | 51 | forwardMessage2(route, body, aesPassword, compressGzip, session, cb) 52 | { 53 | const components = this.app.components; 54 | const server = _.get(components, '__server__', null); 55 | 56 | if (!server) 57 | { 58 | logger.error('server component not enable on %s', this.app.serverId); 59 | utils.invokeCallback(cb, new Error('server component not enable')); 60 | return; 61 | } 62 | 63 | const sessionService = _.get(components, '__backendSession__', null); 64 | 65 | if (!sessionService) 66 | { 67 | logger.error('backend session component not enable on %s', this.app.serverId); 68 | utils.invokeCallback(cb, new Error('backend sesssion component not enable')); 69 | return; 70 | } 71 | 72 | // generate backend session for current request 73 | const backendSession = sessionService.create(session); 74 | 75 | // handle the request 76 | 77 | // logger.debug('backend server [%s] handle message: %j', this.app.serverId, msg); 78 | 79 | const dmsg = { 80 | route : route, 81 | body : body, 82 | compressGzip : compressGzip 83 | }; 84 | 85 | const socket = { 86 | aesPassword : aesPassword 87 | }; 88 | 89 | const connector = _.get(components, '__connector__.connector', null); 90 | connector.runDecode(dmsg, socket, (err, msg) => 91 | { 92 | if (err) 93 | { 94 | return cb(err); 95 | } 96 | 97 | server.handle(msg, backendSession, (err, resp, opts) => 98 | { 99 | utils.invokeCallback(cb, err, resp, opts); 100 | }); 101 | }); 102 | } 103 | 104 | } 105 | 106 | /** 107 | * Remote service for backend servers. 108 | * Receive and handle request message forwarded from frontend server. 109 | */ 110 | module.exports = function(app) 111 | { 112 | if (!(this instanceof MsgRemote)) 113 | { 114 | return new MsgRemote(app); 115 | } 116 | }; -------------------------------------------------------------------------------- /lib/common/remote/frontend/channelRemote.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Remote channel service for frontend server. 3 | * Receive push request from backend servers and push it to clients. 4 | */ 5 | const _ = require('lodash'), 6 | utils = require('../../../util/utils'), 7 | logger = require('pomelo-logger').getLogger('pomelo', __filename); 8 | 9 | class ChannelRemote 10 | { 11 | constructor(app) 12 | { 13 | this.app = app; 14 | } 15 | 16 | /** 17 | * Push message to client by uids. 18 | * 19 | * @param {String} route route string of message 20 | * @param {Object} msg message 21 | * @param {Array} uids user ids that would receive the message 22 | * @param {Object} opts push options 23 | * @param {Function} callback callback function 24 | */ 25 | pushMessage(route, msg, uids, opts, callback) 26 | { 27 | if (!msg) 28 | { 29 | logger.error(`Can not send empty message! route : ${route}, compressed msg : ${msg}`); 30 | utils.invokeCallback(callback, new Error('can not send empty message.')); 31 | return; 32 | } 33 | 34 | const connector = this.app.components.__connector__; 35 | 36 | const sessionService = this.app.get('sessionService'); 37 | const fails = [], sids = []; 38 | let sessions; 39 | _.forEach(uids, uid => 40 | { 41 | sessions = sessionService.getByUid(uid); 42 | if (!sessions) 43 | { 44 | fails.push(uid); 45 | } 46 | else 47 | { 48 | _.forEach(sessions, session => 49 | { 50 | sids.push(session.id); 51 | }); 52 | } 53 | }); 54 | logger.debug(`[${this.app.serverId}] pushMessage uids: ${uids}, msg: ${msg}, sids: ${sids}`); 55 | connector.send(null, route, msg, sids, opts, err => 56 | { 57 | utils.invokeCallback(callback, err, fails); 58 | }); 59 | } 60 | 61 | /** 62 | * Broadcast to all the client connected with current frontend server. 63 | * 64 | * @param {String} route route string 65 | * @param {Object} msg message 66 | * @param {Boolean} opts broadcast options. 67 | * @param {Function} cb callback function 68 | */ 69 | broadcast(route, msg, opts, cb) 70 | { 71 | const connector = this.app.components.__connector__; 72 | connector.send(null, route, msg, null, opts, cb); 73 | } 74 | } 75 | 76 | module.exports = function(app) 77 | { 78 | if (!(this instanceof ChannelRemote)) 79 | { 80 | return new ChannelRemote(app); 81 | } 82 | }; -------------------------------------------------------------------------------- /lib/common/remote/frontend/sessionRemote.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Remote session service for frontend server. 3 | * Set session info for backend servers. 4 | */ 5 | const _ = require('lodash'), 6 | utils = require('../../../util/utils'); 7 | 8 | class SessionRemote 9 | { 10 | constructor(app) 11 | { 12 | this.app = app; 13 | } 14 | 15 | bind(sid, uid, cb) 16 | { 17 | this.app.get('sessionService').bind(sid, uid, cb); 18 | } 19 | 20 | unbind(sid, uid, cb) 21 | { 22 | this.app.get('sessionService').unbind(sid, uid, cb); 23 | } 24 | 25 | push(sid, key, value, cb) 26 | { 27 | this.app.get('sessionService').import(sid, key, value, cb); 28 | } 29 | 30 | pushAll(sid, settings, cb) 31 | { 32 | this.app.get('sessionService').importAll(sid, settings, cb); 33 | } 34 | 35 | /** 36 | * Get front session info with session id. 37 | * 38 | * @param {String} sid session id binded with the session 39 | * @param {Function} callback(err, sinfo) callback function, session info would be null if the session not exist. 40 | */ 41 | getBackendSessionBySid(sid, callback) 42 | { 43 | const session = this.app.get('sessionService').get(sid); 44 | if (!session) 45 | { 46 | utils.invokeCallback(callback); 47 | return; 48 | } 49 | utils.invokeCallback(callback, null, session.toFrontendSession().export()); 50 | } 51 | 52 | /** 53 | * Get all the session informations with the specified user id. 54 | * 55 | * @param {String} uid user id binded with the session 56 | * @param {Function} cb(err, sinfo) callback function, session info would be null if the session does not exist. 57 | */ 58 | getBackendSessionsByUid(uid, cb) 59 | { 60 | const sessions = this.app.get('sessionService').getByUid(uid); 61 | if (!sessions) 62 | { 63 | utils.invokeCallback(cb); 64 | return; 65 | } 66 | 67 | const res = []; 68 | _.forEach(sessions, session => 69 | { 70 | res.push(session.toFrontendSession().export()); 71 | }); 72 | utils.invokeCallback(cb, null, res); 73 | } 74 | 75 | /** 76 | * Kick a session by session id. 77 | * 78 | * @param {Number} sid session id 79 | * @param {String} reason kick reason 80 | * @param {Function} cb callback function 81 | */ 82 | kickBySid(sid, reason, cb) 83 | { 84 | this.app.get('sessionService').kickBySessionId(sid, reason, cb); 85 | } 86 | 87 | /** 88 | * Kick sessions by user id. 89 | * 90 | * @param {Number|String} uid user id 91 | * @param {String} reason kick reason 92 | * @param {Function} cb callback function 93 | */ 94 | kickByUid(uid, reason, cb) 95 | { 96 | this.app.get('sessionService').kick(uid, reason, cb); 97 | } 98 | } 99 | 100 | module.exports = function(app) 101 | { 102 | return new SessionRemote(app); 103 | }; -------------------------------------------------------------------------------- /lib/common/service/connectionService.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | /** 3 | * connection statistics service 4 | * record connection, login count and list 5 | */ 6 | class ConnectionService 7 | { 8 | constructor(app) 9 | { 10 | this.serverId = app.getServerId(); 11 | this.connCount = 0; 12 | this.loginedCount = 0; 13 | this.logined = {}; 14 | } 15 | 16 | /** 17 | * Add logined user. 18 | * 19 | * @param uid {String} user id 20 | * @param info {Object} record for logined user 21 | */ 22 | addLoginedUser(uid, info) 23 | { 24 | if (!this.logined[uid]) 25 | { 26 | this.loginedCount++; 27 | } 28 | if (_.isObject(info)) 29 | info.uid = uid; 30 | this.logined[uid] = info; 31 | } 32 | 33 | /** 34 | * Update user info. 35 | * @param uid {String} user id 36 | * @param info {Object} info for update. 37 | */ 38 | updateUserInfo(uid, infos) 39 | { 40 | const user = this.logined[uid]; 41 | if (!user) 42 | { 43 | return; 44 | } 45 | _.forEach(infos, (info, key) => 46 | { 47 | if (infos.hasOwnProperty(key) && !_.isFunction(info)) 48 | { 49 | user[key] = info; 50 | } 51 | }); 52 | } 53 | 54 | /** 55 | * Increase connection count 56 | */ 57 | increaseConnectionCount() 58 | { 59 | this.connCount++; 60 | } 61 | 62 | /** 63 | * Remote logined user 64 | * 65 | * @param uid {String} user id 66 | */ 67 | removeLoginedUser(uid) 68 | { 69 | if (this.logined[uid]) 70 | { 71 | this.loginedCount--; 72 | } 73 | delete this.logined[uid]; 74 | } 75 | 76 | /** 77 | * Decrease connection count 78 | * 79 | * @param uid {String} uid 80 | */ 81 | decreaseConnectionCount(uid) 82 | { 83 | if (this.connCount) 84 | { 85 | this.connCount--; 86 | } 87 | if (uid) 88 | { 89 | this.removeLoginedUser(uid); 90 | } 91 | } 92 | 93 | /** 94 | * Get statistics info 95 | * 96 | * @return {Object} statistics info 97 | */ 98 | getStatisticsInfo() 99 | { 100 | const list = _.values(this.logined); 101 | 102 | return { 103 | serverId : this.serverId, 104 | totalConnCount : this.connCount, 105 | loginedCount : this.loginedCount, 106 | loginedList : list}; 107 | } 108 | } 109 | 110 | module.exports = ConnectionService; -------------------------------------------------------------------------------- /lib/common/service/filterService.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | logger = require('pomelo-logger').getLogger('pomelo', __filename); 3 | 4 | /** 5 | * Filter service. 6 | * Register and fire before and after filters. 7 | */ 8 | class FilterService 9 | { 10 | constructor() 11 | { 12 | this.befores = []; // before filters 13 | this.afters = []; // after filters 14 | this.name = 'filter'; 15 | } 16 | 17 | /** 18 | * Add before filter into the filter chain. 19 | * 20 | * @param filter {Object|Function} filter instance or filter function. 21 | */ 22 | before(filter) 23 | { 24 | this.befores.push(filter); 25 | } 26 | 27 | /** 28 | * Add after filter into the filter chain. 29 | * 30 | * @param filter {Object|Function} filter instance or filter function. 31 | */ 32 | after(filter) 33 | { 34 | this.afters.unshift(filter); 35 | } 36 | 37 | /** 38 | * TODO: other insert method for filter? such as unShift 39 | */ 40 | 41 | /** 42 | * Do the before filter. 43 | * Fail over if any filter pass err parameter to the next function. 44 | * 45 | * @param msg {Object} clienet request msg 46 | * @param session {Object} a session object for current request 47 | * @param cb {Function} cb(err) callback function to invoke next chain node 48 | */ 49 | beforeFilter(msg, session, cb) 50 | { 51 | let index = 0; 52 | const next = (err, resp, opts) => 53 | { 54 | if (err || index >= this.befores.length) 55 | { 56 | cb(err, resp, opts); 57 | return; 58 | } 59 | 60 | const handler = this.befores[index++]; 61 | if (_.isFunction(handler)) 62 | { 63 | handler(msg, session, next); 64 | } 65 | else if (_.isFunction(handler.before)) 66 | { 67 | handler.before(msg, session, next); 68 | } 69 | else 70 | { 71 | logger.error('meet invalid before filter, handler or handler.before should be function.'); 72 | next(new Error('invalid before filter.')); 73 | } 74 | }; // end of next 75 | next(); 76 | } 77 | 78 | /** 79 | * Do after filter chain. 80 | * Give server a chance to do clean up jobs after request responsed. 81 | * After filter can not change the request flow before. 82 | * After filter should call the next callback to let the request pass to next after filter. 83 | * 84 | * @param err {Object} error object 85 | * @param msg {*} msg object 86 | * @param session {Object} session object for current request 87 | * @param {Object} resp response object send to client 88 | * @param cb {Function} cb(err) callback function to invoke next chain node 89 | */ 90 | afterFilter(err, msg, session, resp, cb) 91 | { 92 | let index = 0; 93 | const next = (err) => 94 | { 95 | // if done 96 | if (index >= this.afters.length) 97 | { 98 | cb(err); 99 | return; 100 | } 101 | 102 | const handler = this.afters[index++]; 103 | if (_.isFunction(handler)) 104 | { 105 | handler(err, msg, session, resp, next); 106 | } 107 | else if (_.isFunction(handler.after)) 108 | { 109 | handler.after(err, msg, session, resp, next); 110 | } 111 | else 112 | { 113 | logger.error('meet invalid after filter, handler or handler.after should be function.'); 114 | next(new Error('invalid after filter.')); 115 | } 116 | }; // end of next 117 | next(err); 118 | } 119 | } 120 | 121 | module.exports = function(app) 122 | { 123 | if (!(this instanceof FilterService)) 124 | { 125 | return new FilterService(app); 126 | } 127 | }; -------------------------------------------------------------------------------- /lib/common/service/handlerService.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | fs = require('fs'), 3 | utils = require('../../util/utils'), 4 | Loader = require('pomelo-loader-upgrade'), 5 | pathUtil = require('../../util/pathUtil'), 6 | logger = require('pomelo-logger').getLogger('pomelo', __filename), 7 | forwardLogger = require('pomelo-logger').getLogger('forward-log', __filename); 8 | 9 | class HandlerService 10 | { 11 | /** 12 | * Handler service. 13 | * Dispatch request to the relactive handler. 14 | * 15 | * @param {Object} app current application context 16 | */ 17 | constructor(app, opts) 18 | { 19 | this.app = app; 20 | this.handlerMap = {}; 21 | this.name = 'handler'; 22 | if (opts.reloadHandlers) 23 | { 24 | handlerServiceUtility.WatchHandlers(app, this.handlerMap); 25 | } 26 | this.enableForwardLog = opts.enableForwardLog || false; 27 | } 28 | 29 | /** 30 | * Handler the request. 31 | */ 32 | handle(routeRecord, msg, session, cb) 33 | { 34 | // the request should be processed by current server 35 | const handler = this.getHandler(routeRecord); 36 | if (!handler) 37 | { 38 | logger.error(`[handleManager]: fail to find handler for ${msg.__route__}`); 39 | utils.invokeCallback(cb, new Error(`fail to find handler for ${msg.__route__}`)); 40 | return; 41 | } 42 | const start = Date.now(); 43 | 44 | const callback = (err, resp, opts) => 45 | { 46 | if (this.enableForwardLog) 47 | { 48 | const log = { 49 | route : msg.__route__, 50 | args : msg, 51 | time : utils.format(new Date(start)), 52 | timeUsed : new Date() - start 53 | }; 54 | forwardLogger.info(JSON.stringify(log)); 55 | } 56 | // resp = handlerServiceUtility.GetResp(arguments); 57 | utils.invokeCallback(cb, err, resp, opts); 58 | 59 | }; 60 | 61 | if (!Array.isArray(msg)) 62 | { 63 | handler[routeRecord.method](msg, session, callback); 64 | } 65 | else 66 | { 67 | msg.push(session); 68 | msg.push(callback); 69 | handler[routeRecord.method](...msg); 70 | } 71 | } 72 | 73 | /** 74 | * Get handler instance by routeRecord. 75 | * 76 | * @param {Object} routeRecord route record parsed from route string 77 | * @return {Object} handler instance if any matchs or null for match fail 78 | */ 79 | getHandler(routeRecord) 80 | { 81 | const serverType = routeRecord.serverType; 82 | if (!this.handlerMap[serverType]) 83 | { 84 | handlerServiceUtility.LoadHandlers(this.app, serverType, this.handlerMap); 85 | } 86 | const handlers = this.handlerMap[serverType] || {}; 87 | const handler = handlers[routeRecord.handler]; 88 | if (!handler) 89 | { 90 | logger.warn(`could not find handler for routeRecord: ${routeRecord}`); 91 | return null; 92 | } 93 | if (!_.isFunction(handler[routeRecord.method])) 94 | { 95 | logger.warn(`could not find the method ${routeRecord.method} in handler: ${routeRecord.handler}`); 96 | return null; 97 | } 98 | return handler; 99 | } 100 | } 101 | 102 | class handlerServiceUtility 103 | { 104 | /** 105 | * Load handlers from current application 106 | */ 107 | static LoadHandlers(app, serverType, handlerMap) 108 | { 109 | const p = pathUtil.getHandlerPath(app.getBase(), serverType); 110 | if (p) 111 | { 112 | handlerMap[serverType] = Loader.load(p, app); 113 | } 114 | } 115 | 116 | static WatchHandlers(app, handlerMap) 117 | { 118 | const p = pathUtil.getHandlerPath(app.getBase(), app.serverType); 119 | if (p) 120 | { 121 | fs.watch(p, function(event, name) 122 | { 123 | if (event === 'change') 124 | { 125 | handlerMap[app.serverType] = Loader.load(p, app); 126 | } 127 | }); 128 | } 129 | } 130 | 131 | static GetResp(args) 132 | { 133 | const len = args.length; 134 | if (len == 1) 135 | { 136 | return []; 137 | } 138 | 139 | if (len == 2) 140 | { 141 | return [args[1]]; 142 | } 143 | 144 | if (len == 3) 145 | { 146 | return [args[1], args[2]]; 147 | } 148 | 149 | if (len == 4) 150 | { 151 | return [args[1], args[2], args[3]]; 152 | } 153 | 154 | const r = new Array(len); 155 | for (let i = 1; i < len; i++) 156 | { 157 | r[i] = args[i]; 158 | } 159 | 160 | return r; 161 | } 162 | } 163 | 164 | module.exports = function(app, opts) 165 | { 166 | if (!(this instanceof HandlerService)) 167 | { 168 | return new HandlerService(app, opts); 169 | } 170 | }; -------------------------------------------------------------------------------- /lib/components/backendSession.js: -------------------------------------------------------------------------------- 1 | const BackendSessionService = require('../common/service/backendSessionService'); 2 | 3 | module.exports = function(app) 4 | { 5 | const service = new BackendSessionService(app); 6 | service.name = '__backendSession__'; 7 | // export backend session service to the application context. 8 | app.set('backendSessionService', service, true); 9 | 10 | // for compatibility as `LocalSession` is renamed to `BackendSession` 11 | app.set('localSessionService', service, true); 12 | 13 | return service; 14 | }; 15 | -------------------------------------------------------------------------------- /lib/components/channel.js: -------------------------------------------------------------------------------- 1 | const ChannelService = require('../common/service/channelService'); 2 | 3 | module.exports = function(app, opts) 4 | { 5 | const service = new ChannelService(app, opts); 6 | app.set('channelService', service, true); 7 | service.name = '__channel__'; 8 | return service; 9 | }; -------------------------------------------------------------------------------- /lib/components/connection.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | ConnectionService = require('../common/service/connectionService'); 3 | 4 | class Connection 5 | { 6 | constructor(app) 7 | { 8 | this.app = app; 9 | this.service = new ConnectionService(app); 10 | this.name = '__connection__'; 11 | // proxy the service methods except the lifecycle interfaces of component 12 | const getFun = (propertyName) => 13 | { 14 | return (() => 15 | { 16 | return (...args) => 17 | { 18 | return this.service[propertyName](...args); 19 | }; 20 | })(); 21 | }; 22 | const prototypeOf = Object.getPrototypeOf(this.service); 23 | const propertyNames = Object.getOwnPropertyNames(prototypeOf); 24 | _.forEach(propertyNames, propertyName => 25 | { 26 | if (propertyName !== 'start' && propertyName !== 'stop' && propertyName != 'constructor') 27 | { 28 | const method = prototypeOf[propertyName]; 29 | if (_.isFunction(method)) 30 | { 31 | this[propertyName] = getFun(propertyName); 32 | } 33 | } 34 | }); 35 | /** 36 | for(var m in this.service) { 37 | if(m !== 'start' && m !== 'stop') { 38 | const method = this.service[m]; 39 | if(typeof method === 'function') { 40 | this[m] = getFun(m); 41 | } 42 | } 43 | } 44 | **/ 45 | } 46 | } 47 | 48 | /** 49 | * Connection component for statistics connection status of frontend servers 50 | */ 51 | module.exports = function(app) 52 | { 53 | if (!(this instanceof Connection)) 54 | { 55 | return new Connection(app); 56 | } 57 | }; -------------------------------------------------------------------------------- /lib/components/dictionary.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | fs = require('fs'), 3 | path = require('path'), 4 | utils = require('../util/utils'), 5 | Loader = require('pomelo-loader-upgrade'), 6 | pathUtil = require('../util/pathUtil'), 7 | crypto = require('crypto'); 8 | 9 | class Dictionary 10 | { 11 | constructor(app, opts) 12 | { 13 | this.app = app; 14 | this.dict = {}; 15 | this.abbrs = {}; 16 | this.userDicPath = null; 17 | this.version = ''; 18 | 19 | // Set user dictionary 20 | let p = path.join(app.getBase(), '/config/dictionary.json'); 21 | if (opts && opts.dict) 22 | { 23 | p = opts.dict; 24 | } 25 | if (fs.existsSync(p)) 26 | { 27 | this.userDicPath = p; 28 | } 29 | this.name = '__dictionary__'; 30 | } 31 | 32 | start(callBack) 33 | { 34 | const servers = this.app.get('servers'); 35 | const routes = []; 36 | 37 | // 待测试 38 | // Load all the handler files 39 | _.forEach(servers, (server, serverType) => 40 | { 41 | const p = pathUtil.getHandlerPath(this.app.getBase(), serverType); 42 | if (p) 43 | { 44 | const handlers = Loader.load(p, this.app); 45 | _.forEach(handlers, (handler, handlerName) => 46 | { 47 | _.forEach(handler, (value, key) => 48 | { 49 | if (_.isFunction(value)) 50 | { 51 | routes.push(`${serverType}.${handlerName}.${key}`); 52 | } 53 | }); 54 | }); 55 | } 56 | }); 57 | 58 | // Sort the route to make sure all the routers abbr are the same in all the servers 59 | routes.sort(); 60 | let abbr; 61 | let i; 62 | for (i = 0; i < routes.length; i++) 63 | { 64 | abbr = i + 1; 65 | this.abbrs[abbr] = routes[i]; 66 | this.dict[routes[i]] = abbr; 67 | } 68 | 69 | // Load user dictionary 70 | if (this.userDicPath) 71 | { 72 | const userDic = require(this.userDicPath); 73 | 74 | abbr = routes.length + 1; 75 | for (i = 0; i < userDic.length; i++) 76 | { 77 | const route = userDic[i]; 78 | 79 | this.abbrs[abbr] = route; 80 | this.dict[route] = abbr; 81 | abbr++; 82 | } 83 | } 84 | 85 | this.version = crypto.createHash('md5') 86 | .update(JSON.stringify(this.dict)) 87 | .digest('base64'); 88 | 89 | utils.invokeCallback(callBack); 90 | } 91 | 92 | getDict() 93 | { 94 | return this.dict; 95 | } 96 | 97 | getAbbrs() 98 | { 99 | return this.abbrs; 100 | } 101 | 102 | getVersion() 103 | { 104 | return this.version; 105 | } 106 | } 107 | 108 | module.exports = function(app, opts) 109 | { 110 | if (!(this instanceof Dictionary)) 111 | { 112 | return new Dictionary(app, opts); 113 | } 114 | }; -------------------------------------------------------------------------------- /lib/components/master.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Component for master. 3 | */ 4 | const Master = require('../master/master'); 5 | 6 | class Component 7 | { 8 | /** 9 | * Master component class 10 | * 11 | * @param {Object} app current application context 12 | */ 13 | constructor(app, opts) 14 | { 15 | this.master = new Master(app, opts); 16 | this.name = '__master__'; 17 | } 18 | 19 | /** 20 | * Component lifecycle function 21 | * 22 | * @param {Function} cb 23 | * @return {Void} 24 | */ 25 | start(cb) 26 | { 27 | this.master.start(cb); 28 | } 29 | 30 | /** 31 | * Component lifecycle function 32 | * 33 | * @param {Boolean} force whether stop the component immediately 34 | * @param {Function} cb 35 | * @return {Void} 36 | */ 37 | stop(force, cb) 38 | { 39 | this.master.stop(cb); 40 | } 41 | } 42 | 43 | /** 44 | * Component factory function 45 | * 46 | * @param {Object} app current application context 47 | * @param {Object} opts 48 | * @return {Object} component instances 49 | */ 50 | module.exports = function(app, opts) 51 | { 52 | return new Component(app, opts); 53 | }; -------------------------------------------------------------------------------- /lib/components/monitor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Component for monitor. 3 | * Load and start monitor client. 4 | */ 5 | const Monitor = require('../monitor/monitor'); 6 | 7 | class Component 8 | { 9 | constructor(app, opts) 10 | { 11 | this.monitor = new Monitor(app, opts); 12 | this.name = '__monitor__'; 13 | } 14 | 15 | start(cb) 16 | { 17 | this.monitor.start(cb); 18 | } 19 | 20 | stop(force, cb) 21 | { 22 | this.monitor.stop(cb); 23 | } 24 | 25 | reconnect(masterInfo) 26 | { 27 | this.monitor.reconnect(masterInfo); 28 | } 29 | } 30 | 31 | /** 32 | * Component factory function 33 | * 34 | * @param {Object} app current application context 35 | * @param {Object} opts 36 | * @return {Object} component instances 37 | */ 38 | module.exports = function(app, opts) 39 | { 40 | return new Component(app, opts); 41 | }; 42 | -------------------------------------------------------------------------------- /lib/components/protobuf.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | fs = require('fs'), 3 | path = require('path'), 4 | protobuf = require('pomelo-protobuf'), 5 | Constants = require('../util/constants'), 6 | crypto = require('crypto'), 7 | logger = require('pomelo-logger').getLogger('pomelo', __filename); 8 | 9 | class ProtoBuff 10 | { 11 | constructor(app, opts) 12 | { 13 | this.app = app; 14 | opts = opts || {}; 15 | this.watchers = {}; 16 | this.serverProtos = {}; 17 | this.clientProtos = {}; 18 | this.version = ''; 19 | 20 | const env = app.get(Constants.RESERVED.ENV); 21 | const originServerPath = path.join(app.getBase(), Constants.FILEPATH.SERVER_PROTOS); 22 | const presentServerPath = path.join(Constants.FILEPATH.CONFIG_DIR, env, path.basename(Constants.FILEPATH.SERVER_PROTOS)); 23 | const originClientPath = path.join(app.getBase(), Constants.FILEPATH.CLIENT_PROTOS); 24 | const presentClientPath = path.join(Constants.FILEPATH.CONFIG_DIR, env, path.basename(Constants.FILEPATH.CLIENT_PROTOS)); 25 | 26 | this.serverProtosPath = opts.serverProtos || (fs.existsSync(originServerPath) ? Constants.FILEPATH.SERVER_PROTOS : presentServerPath); 27 | this.clientProtosPath = opts.clientProtos || (fs.existsSync(originClientPath) ? Constants.FILEPATH.CLIENT_PROTOS : presentClientPath); 28 | 29 | this.setProtos(Constants.RESERVED.SERVER, path.join(app.getBase(), this.serverProtosPath)); 30 | this.setProtos(Constants.RESERVED.CLIENT, path.join(app.getBase(), this.clientProtosPath)); 31 | 32 | protobuf.init({ 33 | encoderProtos : this.serverProtos, 34 | decoderProtos : this.clientProtos}); 35 | this.name = '__protobuf__'; 36 | } 37 | 38 | encode(key, msg) 39 | { 40 | return protobuf.encode(key, msg); 41 | } 42 | 43 | encode2Bytes(key, msg) 44 | { 45 | return protobuf.encode2Bytes(key, msg); 46 | } 47 | 48 | decode(key, msg) 49 | { 50 | return protobuf.decode(key, msg); 51 | } 52 | 53 | getProtos() 54 | { 55 | return { 56 | server : this.serverProtos, 57 | client : this.clientProtos, 58 | version : this.version 59 | }; 60 | } 61 | 62 | getVersion() 63 | { 64 | return this.version; 65 | } 66 | 67 | setProtos(type, path) 68 | { 69 | if (!fs.existsSync(path)) 70 | { 71 | return; 72 | } 73 | 74 | if (type === Constants.RESERVED.SERVER) 75 | { 76 | this.serverProtos = protobuf.parse(require(path)); 77 | } 78 | 79 | if (type === Constants.RESERVED.CLIENT) 80 | { 81 | this.clientProtos = protobuf.parse(require(path)); 82 | } 83 | 84 | const protoStr = JSON.stringify(this.clientProtos) + JSON.stringify(this.serverProtos); 85 | this.version = crypto.createHash('md5') 86 | .update(protoStr) 87 | .digest('base64'); 88 | 89 | // Watch file 90 | const watcher = fs.watch(path, this.onUpdate.bind(this, type, path)); 91 | if (this.watchers[type]) 92 | { 93 | this.watchers[type].close(); 94 | } 95 | this.watchers[type] = watcher; 96 | } 97 | 98 | onUpdate(type, path, event) 99 | { 100 | if (event !== 'change') 101 | { 102 | return; 103 | } 104 | 105 | fs.readFile(path, 'utf8', (err, data) => 106 | { 107 | if (err) 108 | { 109 | logger.error(`read file file error:${err} path : ${path}`); 110 | } 111 | try 112 | { 113 | const protos = protobuf.parse(JSON.parse(data)); 114 | if (type === Constants.RESERVED.SERVER) 115 | { 116 | protobuf.setEncoderProtos(protos); 117 | this.serverProtos = protos; 118 | } 119 | else 120 | { 121 | protobuf.setDecoderProtos(protos); 122 | this.clientProtos = protos; 123 | } 124 | 125 | const protoStr = JSON.stringify(this.clientProtos) + JSON.stringify(this.serverProtos); 126 | this.version = crypto.createHash('md5') 127 | .update(protoStr) 128 | .digest('base64'); 129 | logger.info(`change proto file , type : %${type}, path : %${path}, version : ${this.version}`); 130 | } 131 | catch (error) 132 | { 133 | logger.warn(`change proto file error:${error} path : ${path}`); 134 | } 135 | }); 136 | } 137 | 138 | stop(force, cb) 139 | { 140 | _.forEach(this.watchers, (watcher, type) => 141 | { 142 | watcher.close(); 143 | }); 144 | this.watchers = {}; 145 | process.nextTick(cb); 146 | } 147 | } 148 | 149 | module.exports = function(app, opts) 150 | { 151 | return new ProtoBuff(app, opts); 152 | }; -------------------------------------------------------------------------------- /lib/components/pushScheduler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Scheduler component to schedule message sending. 3 | */ 4 | 5 | const _ = require('lodash'), 6 | DefaultScheduler = require('../pushSchedulers/direct'), 7 | logger = require('pomelo-logger').getLogger('pomelo', __filename); 8 | 9 | class PushScheduler 10 | { 11 | constructor(app, opts) 12 | { 13 | this.app = app; 14 | opts = opts || {}; 15 | this.scheduler = PushSchedulerUtility.GetScheduler(this, app, opts); 16 | this.name = '__pushScheduler__'; 17 | } 18 | 19 | /** 20 | * Component lifecycle callback 21 | * @param {Function} cb 22 | */ 23 | afterStart(cb) 24 | { 25 | if (this.isSelectable) 26 | { 27 | const schedulers = this.scheduler; 28 | _.forEach(schedulers, scheduler => 29 | { 30 | if (_.isFunction(scheduler.start)) 31 | { 32 | scheduler.start(); 33 | } 34 | }); 35 | process.nextTick(cb); 36 | } 37 | else if (_.isFunction(this.scheduler.start)) 38 | { 39 | this.scheduler.start(cb); 40 | } 41 | else 42 | { 43 | process.nextTick(cb); 44 | } 45 | } 46 | 47 | /** 48 | * Component lifecycle callback 49 | * @param force 50 | * @param {Function} cb 51 | */ 52 | stop(force, cb) 53 | { 54 | if (this.isSelectable) 55 | { 56 | const schedulers = this.scheduler; 57 | _.forEach(schedulers, scheduler => 58 | { 59 | if (_.isFunction(scheduler.stop)) 60 | { 61 | scheduler.stop(); 62 | } 63 | }); 64 | process.nextTick(cb); 65 | } 66 | else if (_.isFunction(this.scheduler.stop)) 67 | { 68 | this.scheduler.stop(cb); 69 | } 70 | else 71 | { 72 | process.nextTick(cb); 73 | } 74 | } 75 | 76 | /** 77 | * Schedule how the message to send. 78 | * 79 | * @param {Number} reqId request id 80 | * @param {String} route route string of the message 81 | * @param {Object} msg message content after encoded 82 | * @param {Array} recvs array of receiver's session id 83 | * @param {Object} opts options 84 | * @param {Function} cb 85 | */ 86 | schedule(reqId, route, msg, recvs, opts, cb) 87 | { 88 | if (this.isSelectable) 89 | { 90 | if (_.isFunction(this.selector)) 91 | { 92 | this.selector(reqId, route, msg, recvs, opts, id => 93 | { 94 | if (this.scheduler[id] && _.isFunction(this.scheduler[id].schedule)) 95 | { 96 | this.scheduler[id].schedule(reqId, route, msg, recvs, opts, cb); 97 | } 98 | else 99 | { 100 | logger.error(`invalid pushScheduler id, id: ${id}`); 101 | } 102 | }); 103 | } 104 | else 105 | { 106 | logger.error(`the selector for pushScheduler is not a function, selector: ${this.selector}`); 107 | } 108 | } 109 | else 110 | { 111 | if (_.isFunction(this.scheduler.schedule)) 112 | { 113 | this.scheduler.schedule(reqId, route, msg, recvs, opts, cb); 114 | } 115 | else 116 | { 117 | logger.error(`the scheduler does not have a schedule function, scheduler: ${this.selector}`); 118 | } 119 | } 120 | } 121 | 122 | } 123 | 124 | class PushSchedulerUtility 125 | { 126 | static GetScheduler(pushSchedulerComp, app, opts) 127 | { 128 | const scheduler = opts.scheduler || DefaultScheduler; 129 | if (_.isFunction(scheduler)) 130 | { 131 | return scheduler(app, opts); 132 | } 133 | 134 | if (Array.isArray(scheduler)) 135 | { 136 | const res = {}; 137 | _.forEach(scheduler, sch => 138 | { 139 | if (_.isFunction(sch.scheduler)) 140 | { 141 | res[sch.id] = sch.scheduler(app, sch.options); 142 | } 143 | else 144 | { 145 | res[sch.id] = sch.scheduler; 146 | } 147 | }); 148 | pushSchedulerComp.isSelectable = true; 149 | pushSchedulerComp.selector = opts.selector; 150 | return res; 151 | } 152 | 153 | return scheduler; 154 | } 155 | } 156 | 157 | module.exports = function(app, opts) 158 | { 159 | return new PushScheduler(app, opts); 160 | }; 161 | -------------------------------------------------------------------------------- /lib/components/remote.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Component for remote service. 3 | * Load remote service and add to global context. 4 | */ 5 | const fs = require('fs'), 6 | pathUtil = require('../util/pathUtil'), 7 | RemoteServer = require('pomelo-rpc-upgrade').server; 8 | 9 | class Remote 10 | { 11 | /** 12 | * Remote component class 13 | * 14 | * @param {Object} app current application context 15 | * @param {Object} opts construct parameters 16 | */ 17 | constructor(app, opts) 18 | { 19 | this.app = app; 20 | this.opts = opts; 21 | this.name = '__remote__'; 22 | } 23 | 24 | /** 25 | * Remote component lifecycle function 26 | * 27 | * @param {Function} cb 28 | * @return {Void} 29 | */ 30 | start(cb) 31 | { 32 | this.opts.port = this.app.getCurServer().port; 33 | this.remote = RemoteUtility.GenRemote(this.app, this.opts); 34 | this.remote.start(); 35 | process.nextTick(cb); 36 | } 37 | 38 | /** 39 | * Remote component lifecycle function 40 | * 41 | * @param {Boolean} force whether stop the component immediately 42 | * @param {Function} cb 43 | * @return {Void} 44 | */ 45 | stop(force, cb) 46 | { 47 | this.remote.stop(force); 48 | process.nextTick(cb); 49 | } 50 | } 51 | 52 | class RemoteUtility 53 | { 54 | 55 | /** 56 | * Get remote paths from application 57 | * 58 | * @param {Object} app current application context 59 | * @return {Array} paths 60 | * 61 | */ 62 | static GetRemotePaths(app) 63 | { 64 | const paths = []; 65 | 66 | let role; 67 | // master server should not come here 68 | if (app.isFrontend()) 69 | { 70 | role = 'frontend'; 71 | } 72 | else 73 | { 74 | role = 'backend'; 75 | } 76 | 77 | const sysPath = pathUtil.getSysRemotePath(role), 78 | serverType = app.getServerType(); 79 | if (fs.existsSync(sysPath)) 80 | { 81 | paths.push(pathUtil.remotePathRecord('sys', serverType, sysPath)); 82 | } 83 | const userPath = pathUtil.getUserRemotePath(app.getBase(), serverType); 84 | if (fs.existsSync(userPath)) 85 | { 86 | paths.push(pathUtil.remotePathRecord('user', serverType, userPath)); 87 | } 88 | 89 | return paths; 90 | } 91 | 92 | /** 93 | * Generate remote server instance 94 | * 95 | * @param {Object} app current application context 96 | * @param {Object} opts contructor parameters for rpc Server 97 | * @return {Object} remote server instance 98 | */ 99 | static GenRemote(app, opts) 100 | { 101 | opts.paths = RemoteUtility.GetRemotePaths(app); 102 | opts.context = app; 103 | if (opts.rpcServer) 104 | { 105 | return opts.rpcServer.create(opts); 106 | } 107 | return RemoteServer.create(opts); 108 | } 109 | } 110 | 111 | /** 112 | * Remote component factory function 113 | * 114 | * @param {Object} app current application context 115 | * @param {Object} opts construct parameters 116 | * opts.acceptorFactory {Object}: acceptorFactory.create(opts, cb) 117 | * @return {Object} remote component instances 118 | */ 119 | module.exports = function(app, opts) 120 | { 121 | opts = opts || {}; 122 | // cacheMsg is deprecated, just for compatibility here. 123 | opts.bufferMsg = opts.bufferMsg || opts.cacheMsg || false; 124 | opts.interval = opts.interval || 30; 125 | if (app.enabled('rpcDebugLog')) 126 | { 127 | opts.rpcDebugLog = true; 128 | opts.rpcLogger = require('pomelo-logger').getLogger('rpc-debug', __filename); 129 | } 130 | return new Remote(app, opts); 131 | }; -------------------------------------------------------------------------------- /lib/components/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Component for server starup. 3 | */ 4 | const Server = require('../server/server'); 5 | 6 | /** 7 | * Server component class 8 | * 9 | * @param {Object} app current application context 10 | */ 11 | class Component 12 | { 13 | /** 14 | * Component factory function 15 | * 16 | * @param {Object} app current application context 17 | * @return {Object} component instance 18 | */ 19 | constructor(app, opts) 20 | { 21 | this.server = Server.create(app, opts); 22 | this.name = '__server__'; 23 | } 24 | 25 | /** 26 | * Component lifecycle callback 27 | * 28 | * @param {Function} cb 29 | * @return {Void} 30 | */ 31 | start(cb) 32 | { 33 | this.server.start(); 34 | process.nextTick(cb); 35 | } 36 | 37 | /** 38 | * Component lifecycle callback 39 | * 40 | * @param {Function} cb 41 | * @return {Void} 42 | */ 43 | afterStart(cb) 44 | { 45 | this.server.afterStart(); 46 | process.nextTick(cb); 47 | } 48 | 49 | /** 50 | * Component lifecycle function 51 | * 52 | * @param {Boolean} force whether stop the component immediately 53 | * @param {Function} cb 54 | * @return {Void} 55 | */ 56 | stop(force, cb) 57 | { 58 | this.server.stop(); 59 | process.nextTick(cb); 60 | } 61 | 62 | /** 63 | * Proxy server handle 64 | */ 65 | handle(msg, session, cb) 66 | { 67 | this.server.handle(msg, session, cb); 68 | } 69 | 70 | /** 71 | * Proxy server global handle 72 | */ 73 | globalHandle(msg, session, cb) 74 | { 75 | this.server.globalHandle(msg, session, cb); 76 | } 77 | } 78 | 79 | module.exports = function(app, opts) 80 | { 81 | return new Component(app, opts); 82 | }; -------------------------------------------------------------------------------- /lib/components/session.js: -------------------------------------------------------------------------------- 1 | const SessionService = require('../common/service/sessionService'), 2 | _ = require('lodash'); 3 | 4 | class Session 5 | { 6 | /** 7 | * Session component. Manage sessions. 8 | * 9 | * @param {Object} app current application context 10 | * @param {Object} opts attach parameters 11 | */ 12 | constructor(app, opts) 13 | { 14 | opts = opts || {}; 15 | this.app = app; 16 | this.service = new SessionService(opts); 17 | const getFun = m => 18 | { 19 | return (() => 20 | { 21 | return (...args) => 22 | { 23 | return this.service[m](...args); 24 | }; 25 | })(); 26 | }; 27 | 28 | const prototypeOf = Object.getPrototypeOf(this.service); 29 | const propertyNames = Object.getOwnPropertyNames(prototypeOf); 30 | _.forEach(propertyNames, propertyName => 31 | { 32 | if (propertyName !== 'start' && propertyName !== 'stop' && propertyName != 'constructor') 33 | { 34 | const method = prototypeOf[propertyName]; 35 | if (_.isFunction(method)) 36 | { 37 | this[propertyName] = getFun(propertyName); 38 | } 39 | } 40 | }); 41 | /** 42 | for(var m in this.service) { 43 | if(m !== 'start' && m !== 'stop') { 44 | const method = this.service[m]; 45 | if(typeof method === 'function') { 46 | this[m] = getFun(m); 47 | } 48 | } 49 | } 50 | **/ 51 | this.name = '__session__'; 52 | } 53 | } 54 | 55 | module.exports = function(app, opts) 56 | { 57 | const cmp = new Session(app, opts); 58 | app.set('sessionService', cmp, true); 59 | return cmp; 60 | }; -------------------------------------------------------------------------------- /lib/connectors/commands/handshake.js: -------------------------------------------------------------------------------- 1 | 2 | const pomelo = require('../../pomelo'), 3 | _ = require('lodash'), 4 | Package = require('pomelo-protocol').Package; 5 | 6 | const CODE_OK = 200; 7 | const CODE_USE_ERROR = 500; 8 | const CODE_OLD_CLIENT = 501; 9 | 10 | class Handshake 11 | { 12 | /** 13 | * Process the handshake request. 14 | * 15 | * @param {Object} opts option parameters 16 | * opts.handshake(msg, cb(err, resp)) handshake callback. msg is the handshake message from client. 17 | * opts.hearbeat heartbeat interval (level?) 18 | * opts.version required client level 19 | */ 20 | constructor(opts) 21 | { 22 | opts = opts || {}; 23 | this.userHandshake = opts.handshake; 24 | 25 | if (opts.heartbeat) 26 | { 27 | this.heartbeatSec = opts.heartbeat; 28 | this.heartbeat = opts.heartbeat * 1000; 29 | } 30 | 31 | this.checkClient = opts.checkClient; 32 | 33 | this.useDict = opts.useDict; 34 | this.useProtobuf = opts.useProtobuf; 35 | this.useCrypto = opts.useCrypto; 36 | } 37 | 38 | handle(socket, msg) 39 | { 40 | if (!msg.sys) 41 | { 42 | HandshakeUtility.ProcessError(socket, CODE_USE_ERROR); 43 | return; 44 | } 45 | // 验证客户端 版本 类型...... 46 | if (_.isFunction(this.checkClient)) 47 | { 48 | if (!msg || !msg.sys || !this.checkClient(msg.sys.type, msg.sys.version)) 49 | { 50 | HandshakeUtility.ProcessError(socket, CODE_OLD_CLIENT); 51 | return; 52 | } 53 | } 54 | 55 | const opts = { 56 | heartbeat : HandshakeUtility.SetupHeartbeat(this) 57 | }; 58 | 59 | const components = pomelo.app.components; 60 | // 采用 dic压缩 61 | if (this.useDict) 62 | { 63 | const dictionary = _.get(components, '__dictionary__', {}); 64 | const dictVersion = dictionary.getVersion(); 65 | if (!msg.sys.dictVersion || !_.isEqual(msg.sys.dictVersion, dictVersion)) 66 | { 67 | // may be deprecated in future 68 | opts.routeToCode = opts.dict = dictionary.getDict(); 69 | opts.codeToRoute = dictionary.getAbbrs(); 70 | opts.dictVersion = dictVersion; 71 | } 72 | opts.useDict = true; 73 | } 74 | 75 | if (this.useProtobuf) 76 | { 77 | const protoBuff = _.get(components, '__protobuf__', {}); 78 | const protoVersion = protoBuff.getVersion(); 79 | if (!msg.sys.protoVersion || !_.isEqual(msg.sys.protoVersion, protoVersion)) 80 | { 81 | opts.protos = protoBuff.getProtos(); 82 | } 83 | opts.useProto = true; 84 | } 85 | 86 | const decodeIOProtoBuff = _.get(components, '__decodeIO__protobuf__', null); 87 | if (!_.isNil(decodeIOProtoBuff)) 88 | { 89 | if (this.useProtobuf) 90 | { 91 | throw new Error('protobuf can not be both used in the same project.'); 92 | } 93 | const version = decodeIOProtoBuff.getVersion(); 94 | if (!msg.sys.protoVersion || msg.sys.protoVersion < version) 95 | { 96 | opts.protos = decodeIOProtoBuff.getProtos(); 97 | } 98 | opts.useProto = true; 99 | } 100 | 101 | if (this.useCrypto) 102 | { 103 | components.__connector__.setPubKey(socket.id, msg.sys.rsa); 104 | } 105 | 106 | if (_.isFunction(this.userHandshake)) 107 | { 108 | this.userHandshake(msg, (err, resp) => 109 | { 110 | if (err) 111 | { 112 | process.nextTick(() => 113 | { 114 | HandshakeUtility.ProcessError(socket, CODE_USE_ERROR); 115 | }); 116 | return; 117 | } 118 | process.nextTick(() => 119 | { 120 | HandshakeUtility.Response(socket, opts, resp); 121 | }); 122 | }, socket); 123 | return; 124 | } 125 | 126 | process.nextTick(() => 127 | { 128 | HandshakeUtility.Response(socket, opts); 129 | }); 130 | } 131 | } 132 | 133 | class HandshakeUtility 134 | { 135 | static SetupHeartbeat(handshake) 136 | { 137 | return handshake.heartbeatSec; 138 | } 139 | 140 | static Response(socket, sys, resp) 141 | { 142 | const res = { 143 | code : CODE_OK, 144 | sys : sys 145 | }; 146 | if (resp) 147 | { 148 | res.user = resp; 149 | } 150 | socket.handshakeResponse(Package.encode(Package.TYPE_HANDSHAKE, new Buffer(JSON.stringify(res)))); 151 | } 152 | 153 | static ProcessError(socket, code) 154 | { 155 | const res = { 156 | code : code 157 | }; 158 | socket.sendForce(Package.encode(Package.TYPE_HANDSHAKE, new Buffer(JSON.stringify(res)))); 159 | process.nextTick(function() 160 | { 161 | socket.disconnect(); 162 | }); 163 | } 164 | } 165 | 166 | module.exports = function(opts) 167 | { 168 | return new Handshake(opts); 169 | }; -------------------------------------------------------------------------------- /lib/connectors/commands/heartbeat.js: -------------------------------------------------------------------------------- 1 | const Package = require('pomelo-protocol').Package, 2 | logger = require('pomelo-logger').getLogger('pomelo', __filename); 3 | 4 | class Heartbeat 5 | { 6 | /** 7 | * Process heartbeat request. 8 | * 9 | * @param {Object} opts option request 10 | * opts.heartbeat heartbeat interval 11 | */ 12 | constructor(opts) 13 | { 14 | opts = opts || {}; 15 | this.heartbeat = null; 16 | this.timeout = null; 17 | this.disconnectOnTimeout = opts.disconnectOnTimeout; 18 | if (opts.heartbeat) 19 | { 20 | this.heartbeat = opts.heartbeat * 1000; // heartbeat interval 21 | this.timeout = opts.timeout * 1000 || this.heartbeat * 2; // max heartbeat message timeout 22 | this.disconnectOnTimeout = true; 23 | } 24 | 25 | this.timeouts = {}; 26 | this.clients = {}; 27 | 28 | } 29 | 30 | handle(socket) 31 | { 32 | if (!this.heartbeat) 33 | { 34 | // no heartbeat setting 35 | return; 36 | } 37 | 38 | const socketId = socket.id; 39 | 40 | if (!this.clients[socketId]) 41 | { 42 | // clear timers when socket disconnect or error 43 | this.clients[socketId] = 1; 44 | socket.once('disconnect', HeartbeatUtility.clearTimers.bind(null, this, socketId)); 45 | socket.once('error', HeartbeatUtility.clearTimers.bind(null, this, socketId)); 46 | } 47 | 48 | // clear timeout timer 49 | if (this.disconnectOnTimeout) 50 | { 51 | this.clear(socketId); 52 | } 53 | 54 | socket.sendRaw(Package.encode(Package.TYPE_HEARTBEAT)); 55 | 56 | if (this.disconnectOnTimeout) 57 | { 58 | this.timeouts[socketId] = setTimeout(() => 59 | { 60 | logger.info(`client ${socketId} heartbeat timeout.`); 61 | socket.disconnect(); 62 | }, this.timeout); 63 | } 64 | } 65 | 66 | clear(id) 67 | { 68 | const tid = this.timeouts[id]; 69 | if (tid) 70 | { 71 | clearTimeout(tid); 72 | delete this.timeouts[id]; 73 | } 74 | } 75 | 76 | } 77 | 78 | class HeartbeatUtility 79 | { 80 | static clearTimers(heartbeat, id) 81 | { 82 | delete heartbeat.clients[id]; 83 | const tid = heartbeat.timeouts[id]; 84 | if (tid) 85 | { 86 | clearTimeout(tid); 87 | delete heartbeat.timeouts[id]; 88 | } 89 | } 90 | } 91 | 92 | module.exports = function(opts) 93 | { 94 | return new Heartbeat(opts); 95 | }; 96 | -------------------------------------------------------------------------------- /lib/connectors/commands/kick.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by frank on 16-12-27. 3 | */ 4 | const isString = require('lodash').isString, 5 | Package = require('pomelo-protocol').Package; 6 | 7 | class Kick 8 | { 9 | static Handle(socket, reason) 10 | { 11 | if (isString(reason)) 12 | { 13 | const buffer = new Buffer(JSON.stringify({reason: reason})); 14 | socket.sendRaw(Package.encode(Package.TYPE_KICK, buffer)); 15 | } 16 | } 17 | } 18 | 19 | module.exports.handle = Kick.Handle; -------------------------------------------------------------------------------- /lib/connectors/common/coder.js: -------------------------------------------------------------------------------- 1 | const Message = require('pomelo-protocol').Message, 2 | Constants = require('../../util/constants'), 3 | logger = require('pomelo-logger').getLogger('pomelo', __filename); 4 | 5 | class Coder 6 | { 7 | static encode(reqId, route, msg) 8 | { 9 | if (reqId) 10 | { 11 | return CoderUtility.ComposeResponse(this, reqId, route, msg); 12 | } 13 | return CoderUtility.ComposePush(this, route, msg); 14 | } 15 | 16 | static decode(msg) 17 | { 18 | msg = Message.decode(msg.body); 19 | let route = msg.route; 20 | // decode use dictionary 21 | if (msg.compressRoute) 22 | { 23 | if (this.connector.useDict) 24 | { 25 | const abbrs = this.dictionary.getAbbrs(); 26 | if (!abbrs[route]) 27 | { 28 | logger.error(`dictionary error! no abbrs for route : ${route}`); 29 | return null; 30 | } 31 | route = msg.route = abbrs[route]; 32 | } 33 | else 34 | { 35 | logger.error(`fail to uncompress route code for msg: ${msg}, server not enable dictionary.`); 36 | return null; 37 | } 38 | } 39 | 40 | // decode use protobuf 41 | if (this.protobuf && this.protobuf.getProtos().client[route]) 42 | { 43 | msg.body = this.protobuf.decode(route, msg.body); 44 | } 45 | else if (this.decodeIO_protobuf && this.decodeIO_protobuf.check(Constants.RESERVED.CLIENT, route)) 46 | { 47 | msg.body = this.decodeIO_protobuf.decode(route, msg.body); 48 | } 49 | else 50 | { 51 | try 52 | { 53 | msg.body = JSON.parse(msg.body.toString('utf8')); 54 | } 55 | catch (ex) 56 | { 57 | msg.body = {}; 58 | } 59 | } 60 | return msg; 61 | } 62 | } 63 | 64 | class CoderUtility 65 | { 66 | static ComposeResponse(server, msgId, route, msgBody) 67 | { 68 | if (!msgId || !route || !msgBody) 69 | { 70 | return null; 71 | } 72 | msgBody = CoderUtility.EncodeBody(server, route, msgBody); 73 | return Message.encode(msgId, Message.TYPE_RESPONSE, 0, null, msgBody); 74 | } 75 | 76 | static ComposePush(server, route, msgBody) 77 | { 78 | if (!route || !msgBody) 79 | { 80 | return null; 81 | } 82 | msgBody = CoderUtility.EncodeBody(server, route, msgBody); 83 | // encode use dictionary 84 | let compressRoute = 0; 85 | if (server.dictionary) 86 | { 87 | const dict = server.dictionary.getDict(); 88 | if (server.connector.useDict && dict[route]) 89 | { 90 | route = dict[route]; 91 | compressRoute = 1; 92 | } 93 | } 94 | return Message.encode(0, Message.TYPE_PUSH, compressRoute, route, msgBody); 95 | } 96 | 97 | static EncodeBody(server, route, msgBody) 98 | { 99 | // encode use protobuf 100 | if (server.protobuf && server.protobuf.getProtos().server[route]) 101 | { 102 | msgBody = server.protobuf.encode(route, msgBody); 103 | } 104 | else if (server.decodeIO_protobuf && server.decodeIO_protobuf.check(Constants.RESERVED.SERVER, route)) 105 | { 106 | msgBody = server.decodeIO_protobuf.encode(route, msgBody); 107 | } 108 | else 109 | { 110 | msgBody = new Buffer(JSON.stringify(msgBody), 'utf8'); 111 | } 112 | return msgBody; 113 | } 114 | } 115 | 116 | module.exports = Coder; -------------------------------------------------------------------------------- /lib/connectors/common/handler.js: -------------------------------------------------------------------------------- 1 | const protocol = require('pomelo-protocol'), 2 | Package = protocol.Package, 3 | logger = require('pomelo-logger').getLogger('pomelo', __filename); 4 | 5 | const ST_INITED = 0; 6 | const ST_WAIT_ACK = 1; 7 | const ST_WORKING = 2; 8 | const ST_CLOSED = 3; 9 | 10 | class PackageType 11 | { 12 | static GetHandler(value) 13 | { 14 | switch (value) 15 | { 16 | case Package.TYPE_HANDSHAKE: 17 | return PackageType.HandleHandshake; 18 | case Package.TYPE_HANDSHAKE_ACK: 19 | return PackageType.HandleHandshakeAck; 20 | case Package.TYPE_HEARTBEAT: 21 | return PackageType.HandleHeartbeat; 22 | case Package.TYPE_DATA: 23 | return PackageType.HandleData; 24 | } 25 | } 26 | 27 | static HandleHandshake(socket, pkg) 28 | { 29 | if (socket.state !== ST_INITED) 30 | { 31 | return; 32 | } 33 | try 34 | { 35 | socket.emit('handshake', JSON.parse(protocol.strdecode(pkg.body))); 36 | } 37 | catch (ex) 38 | { 39 | socket.emit('handshake', {}); 40 | } 41 | } 42 | 43 | static HandleHandshakeAck(socket, pkg) 44 | { 45 | if (socket.state !== ST_WAIT_ACK) 46 | { 47 | return; 48 | } 49 | socket.state = ST_WORKING; 50 | socket.emit('heartbeat'); 51 | } 52 | 53 | static HandleHeartbeat(socket, pkg) 54 | { 55 | if (socket.state !== ST_WORKING) 56 | { 57 | return; 58 | } 59 | socket.emit('heartbeat'); 60 | } 61 | 62 | static HandleData(socket, pkg) 63 | { 64 | if (socket.state !== ST_WORKING) 65 | { 66 | return; 67 | } 68 | socket.emit('message', pkg); 69 | } 70 | } 71 | 72 | module.exports = function(socket, pkg) 73 | { 74 | const handler = PackageType.GetHandler(pkg.type); 75 | if (handler) 76 | { 77 | handler(socket, pkg); 78 | } 79 | else 80 | { 81 | logger.error('could not find handle invalid data package.'); 82 | socket.disconnect(); 83 | } 84 | }; -------------------------------------------------------------------------------- /lib/connectors/hybrid/hybridSocket.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | handler = require('../common/handler'), 3 | protocol = require('pomelo-protocol'), 4 | Package = protocol.Package, 5 | EventEmitter = require('events').EventEmitter, 6 | logger = require('pomelo-logger').getLogger('pomelo', __filename); 7 | 8 | const ST_INITED = 0; 9 | const ST_WAIT_ACK = 1; 10 | const ST_WORKING = 2; 11 | const ST_CLOSED = 3; 12 | 13 | /** 14 | * Socket class that wraps socket and websocket to provide unified interface for up level. 15 | */ 16 | class HyBirdSocket extends EventEmitter 17 | { 18 | constructor(id, socket) 19 | { 20 | super(); 21 | this.id = id; 22 | this.socket = socket; 23 | 24 | const remoteSocket = _.get(socket, '_socket', null); 25 | let port = null; 26 | let ip = null; 27 | 28 | if (_.isNil(remoteSocket)) 29 | { 30 | ip = _.get(socket.address(), 'address'); 31 | port = _.get(socket.address(), 'port'); 32 | } 33 | else 34 | { 35 | ip = _.get(remoteSocket, 'remoteAddress'); 36 | port = _.get(remoteSocket, 'remotePort'); 37 | } 38 | this.remoteAddress = { 39 | ip : ip, 40 | port : port 41 | }; 42 | 43 | socket.once('close', this.emit.bind(this, 'disconnect')); 44 | socket.on('error', this.emit.bind(this, 'error')); 45 | 46 | socket.on('message', msg => 47 | { 48 | if (msg) 49 | { 50 | msg = Package.decode(msg); 51 | handler(this, msg); 52 | } 53 | }); 54 | 55 | this.state = ST_INITED; 56 | // TODO: any other events? 57 | } 58 | 59 | /** 60 | * Send raw byte data. 61 | * 62 | * @api private 63 | */ 64 | sendRaw(msg) 65 | { 66 | if (this.state !== ST_WORKING) 67 | { 68 | return; 69 | } 70 | this.socket.send(msg, {binary: true}, err => 71 | { 72 | if (err) 73 | { 74 | logger.error(`websocket send binary data failed: ${err.stack}`); 75 | } 76 | }); 77 | } 78 | 79 | /** 80 | * Send byte data package to client. 81 | * 82 | * @param {Buffer} msg byte data 83 | */ 84 | send(msg) 85 | { 86 | if (msg instanceof String) 87 | { 88 | msg = new Buffer(msg); 89 | } 90 | else if (!(msg instanceof Buffer)) 91 | { 92 | msg = new Buffer(JSON.stringify(msg)); 93 | } 94 | this.sendRaw(Package.encode(Package.TYPE_DATA, msg)); 95 | } 96 | 97 | /** 98 | * Send byte data packages to client in batch. 99 | * 100 | * @param {Buffer} messages byte data 101 | */ 102 | sendBatch(messages) 103 | { 104 | const rs = []; 105 | let encodeResult; 106 | _.forEach(messages, message => 107 | { 108 | encodeResult = Package.encode(Package.TYPE_DATA, message); 109 | rs.push(encodeResult); 110 | }); 111 | this.sendRaw(Buffer.concat(rs)); 112 | } 113 | 114 | /** 115 | * Send message to client no matter whether handshake. 116 | * 117 | * @api private 118 | */ 119 | sendForce(msg) 120 | { 121 | if (this.state === ST_CLOSED) 122 | { 123 | return; 124 | } 125 | this.socket.send(msg, {binary: true}); 126 | } 127 | 128 | /** 129 | * Response handshake request 130 | * 131 | * @api private 132 | */ 133 | handshakeResponse(resp) 134 | { 135 | if (this.state !== ST_INITED) 136 | { 137 | return; 138 | } 139 | 140 | this.socket.send(resp, {binary: true}); 141 | this.state = ST_WAIT_ACK; 142 | } 143 | 144 | /** 145 | * Close the connection. 146 | * 147 | * @api private 148 | */ 149 | disconnect() 150 | { 151 | if (this.state === ST_CLOSED) 152 | { 153 | return; 154 | } 155 | 156 | this.state = ST_CLOSED; 157 | this.socket.emit('close'); 158 | this.socket.close(); 159 | } 160 | } 161 | 162 | module.exports = function(id, socket) 163 | { 164 | return new HyBirdSocket(id, socket); 165 | }; -------------------------------------------------------------------------------- /lib/connectors/hybrid/switcher.js: -------------------------------------------------------------------------------- 1 | 2 | const _ = require('lodash'), 3 | EventEmitter = require('events').EventEmitter, 4 | WSProcessor = require('./wsprocessor'), 5 | TCPProcessor = require('./tcpprocessor'), 6 | logger = require('pomelo-logger').getLogger('pomelo', __filename); 7 | 8 | const HTTP_METHODS = [ 9 | 'GET', 'POST', 'DELETE', 'PUT', 'HEAD' 10 | ]; 11 | 12 | const ST_STARTED = 1; 13 | const ST_CLOSED = 2; 14 | const DEFAULT_TIMEOUT = 90; 15 | 16 | class Switcher extends EventEmitter 17 | { 18 | /** 19 | * Switcher for tcp and websocket protocol 20 | * 21 | * @param {Object} server tcp server instance from node.js net module 22 | */ 23 | constructor(server, opts) 24 | { 25 | super(); 26 | this.server = server; 27 | this.wsprocessor = new WSProcessor(); 28 | this.tcpprocessor = new TCPProcessor(opts.closeMethod); 29 | this.id = 1; 30 | this.timeout = (opts.timeout || DEFAULT_TIMEOUT) * 1000; 31 | this.setNoDelay = opts.setNoDelay; 32 | 33 | if (!opts.ssl) 34 | { 35 | this.server.on('connection', this.newSocket.bind(this)); 36 | } 37 | else 38 | { 39 | this.server.on('secureConnection', this.newSocket.bind(this)); 40 | this.server.on('clientError', (e) => 41 | { 42 | logger.warn('an ssl error occured before handshake established: ', e); 43 | }); 44 | } 45 | 46 | this.wsprocessor.on('connection', this.emit.bind(this, 'connection')); 47 | this.tcpprocessor.on('connection', this.emit.bind(this, 'connection')); 48 | 49 | this.state = ST_STARTED; 50 | } 51 | 52 | newSocket(socket) 53 | { 54 | if (this.state !== ST_STARTED) 55 | { 56 | return; 57 | } 58 | 59 | socket.setTimeout(this.timeout, () => 60 | { 61 | logger.warn(`connection is timeout without communication, the remote ip is ${socket.remoteAddress} && port is ${socket.remotePort}`); 62 | socket.destroy(); 63 | }); 64 | 65 | socket.once('data', data => 66 | { 67 | // FIXME: handle incomplete HTTP method 68 | if (SwitcherUtility.IsHttp(data)) 69 | { 70 | SwitcherUtility.ProcessHttp(this, this.wsprocessor, socket, data); 71 | } 72 | else 73 | { 74 | if (this.setNoDelay) 75 | { 76 | socket.setNoDelay(true); 77 | } 78 | SwitcherUtility.ProcessTcp(this, this.tcpprocessor, socket, data); 79 | } 80 | }); 81 | } 82 | 83 | close() 84 | { 85 | if (this.state !== ST_STARTED) 86 | { 87 | return; 88 | } 89 | 90 | this.state = ST_CLOSED; 91 | this.wsprocessor.close(); 92 | this.tcpprocessor.close(); 93 | } 94 | 95 | } 96 | 97 | class SwitcherUtility 98 | { 99 | static IsHttp(data) 100 | { 101 | const head = data.toString('utf8', 0, 4); 102 | 103 | _.forEach(HTTP_METHODS, httpType => 104 | { 105 | if (_.indexOf(head, httpType) === 0) 106 | { 107 | return true; 108 | } 109 | }); 110 | return false; 111 | } 112 | 113 | static ProcessHttp(switcher, processor, socket, data) 114 | { 115 | processor.add(socket, data); 116 | } 117 | 118 | static ProcessTcp(switcher, processor, socket, data) 119 | { 120 | processor.add(socket, data); 121 | } 122 | } 123 | 124 | module.exports = function(server, opts) 125 | { 126 | return new Switcher(server, opts); 127 | }; -------------------------------------------------------------------------------- /lib/connectors/hybrid/tcpprocessor.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter, 2 | utils = require('../../util/utils'), 3 | TcpSocket = require('./tcpsocket'); 4 | 5 | const ST_STARTED = 1; 6 | const ST_CLOSED = 2; 7 | 8 | // private protocol, no need exports 9 | const HEAD_SIZE = 4; 10 | /** 11 | * websocket protocol processor 12 | */ 13 | class TcpProcessor extends EventEmitter 14 | { 15 | constructor(closeMethod) 16 | { 17 | super(); 18 | this.closeMethod = closeMethod; 19 | this.state = ST_STARTED; 20 | } 21 | 22 | add(socket, data) 23 | { 24 | if (this.state !== ST_STARTED) 25 | { 26 | return; 27 | } 28 | const tcpSocket = new TcpSocket(socket, { 29 | headSize : HEAD_SIZE, 30 | headHandler : utils.headHandler, 31 | closeMethod : this.closeMethod}); 32 | this.emit('connection', tcpSocket); 33 | socket.emit('data', data); 34 | } 35 | 36 | close() 37 | { 38 | if (this.state !== ST_STARTED) 39 | { 40 | return; 41 | } 42 | this.state = ST_CLOSED; 43 | } 44 | } 45 | 46 | module.exports = function(closeMethod) 47 | { 48 | return new TcpProcessor(closeMethod); 49 | }; -------------------------------------------------------------------------------- /lib/connectors/hybrid/wsprocessor.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | HttpServer = require('http').Server, 3 | EventEmitter = require('events').EventEmitter, 4 | WebSocketServer = require('ws').Server; 5 | 6 | const ST_STARTED = 1; 7 | const ST_CLOSED = 2; 8 | 9 | /** 10 | * websocket protocol processor 11 | */ 12 | class Processor extends EventEmitter 13 | { 14 | constructor() 15 | { 16 | super(); 17 | this.httpServer = new HttpServer(); 18 | 19 | this.wsServer = new WebSocketServer({server: this.httpServer}); 20 | this.wsServer.on('connection', socket => 21 | { 22 | // emit socket to outside 23 | this.emit('connection', socket); 24 | }); 25 | 26 | this.state = ST_STARTED; 27 | } 28 | 29 | add(socket, data) 30 | { 31 | if (this.state !== ST_STARTED) 32 | { 33 | return; 34 | } 35 | this.httpServer.emit('connection', socket); 36 | if (_.isFunction(socket.ondata)) 37 | { 38 | // compatible with stream2 39 | socket.ondata(data, 0, data.length); 40 | } 41 | else 42 | { 43 | // compatible with old stream 44 | socket.emit('data', data); 45 | } 46 | } 47 | 48 | close() 49 | { 50 | if (this.state !== ST_STARTED) 51 | { 52 | return; 53 | } 54 | this.state = ST_CLOSED; 55 | this.wsServer.close(); 56 | this.wsServer = null; 57 | this.httpServer = null; 58 | } 59 | 60 | } 61 | 62 | module.exports = function() 63 | { 64 | return new Processor(); 65 | }; -------------------------------------------------------------------------------- /lib/connectors/hybridConnector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by frank on 16-12-26. 3 | */ 4 | 5 | const net = require('net'), 6 | tls = require('tls'), 7 | _ = require('lodash'), 8 | HybridSocket = require('./hybrid/hybridSocket'), 9 | Switcher = require('./hybrid/switcher'), 10 | Handshake = require('./commands/handshake'), 11 | Heartbeat = require('./commands/heartbeat'), 12 | Kick = require('./commands/kick'), 13 | coder = require('./common/coder'), 14 | app = require('../pomelo').app, 15 | EventEmitter = require('events').EventEmitter; 16 | 17 | let curId = 1; 18 | 19 | class HybridConnector extends EventEmitter 20 | { 21 | constructor(port, host, opts) 22 | { 23 | super(); 24 | this.opts = opts || {}; 25 | this.port = port; 26 | this.host = host; 27 | this.useDict = opts.useDict; 28 | this.useProtobuf = opts.useProtobuf; 29 | this.handshake = new Handshake(opts); 30 | this.heartbeat = new Heartbeat(opts); 31 | this.distinctHost = opts.distinctHost; 32 | this.ssl = opts.ssl; 33 | 34 | this.switcher = null; 35 | this.listeningServer = null; 36 | } 37 | 38 | start(callBack) 39 | { 40 | const genSocket = (socket) => 41 | { 42 | const hybridSocket = new HybridSocket(curId++, socket); 43 | hybridSocket.on('handshake', this.handshake.handle.bind(this.handshake, hybridSocket)); 44 | hybridSocket.on('heartbeat', this.heartbeat.handle.bind(this.heartbeat, hybridSocket)); 45 | hybridSocket.on('disconnect', this.heartbeat.clear.bind(this.heartbeat, hybridSocket.id)); 46 | hybridSocket.on('closing', Kick.handle.bind(null, hybridSocket)); 47 | this.emit('connection', hybridSocket); 48 | }; 49 | 50 | const components = app.components; 51 | this.connector = _.get(components, '__connector__.connector', null); 52 | this.dictionary = _.get(components, '__dictionary__', null); 53 | this.protobuf = _.get(components, '__protobuf__', null); 54 | this.decodeIO_protobuf = _.get(components, '__decodeIO__protobuf__', null); 55 | 56 | if (!this.ssl) 57 | { 58 | this.listeningServer = net.createServer(); 59 | } 60 | else 61 | { 62 | this.listeningServer = tls.createServer(this.ssl); 63 | } 64 | this.switcher = new Switcher(this.listeningServer, this.opts); 65 | 66 | this.switcher.on('connection', (socket) => 67 | { 68 | genSocket(socket); 69 | }); 70 | 71 | if (this.distinctHost) 72 | { 73 | this.listeningServer.listen(this.port, this.host); 74 | } 75 | else 76 | { 77 | this.listeningServer.listen(this.port); 78 | } 79 | 80 | process.nextTick(callBack); 81 | } 82 | 83 | stop(force, callBack) 84 | { 85 | if (!_.isNil(this.switcher)) 86 | { 87 | this.switcher.close(); 88 | } 89 | if (!_.isNil(this.listeningServer)) 90 | { 91 | this.listeningServer.close(); 92 | } 93 | process.nextTick(callBack); 94 | } 95 | 96 | } 97 | 98 | HybridConnector.decode = HybridConnector.prototype.decode = coder.decode; 99 | 100 | HybridConnector.encode = HybridConnector.prototype.encode = coder.encode; 101 | 102 | module.exports = function(port, host, opts) 103 | { 104 | if (!(this instanceof HybridConnector)) 105 | { 106 | return new HybridConnector(port, host, opts); 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /lib/connectors/mqtt/generate.js: -------------------------------------------------------------------------------- 1 | 2 | const _ = require('lodash'), 3 | protocol = require('./protocol'); 4 | 5 | /* TODO: consider rewriting these functions using buffers instead 6 | * of arrays 7 | */ 8 | 9 | class Generate 10 | { 11 | static Publish(opts) 12 | { 13 | opts = opts || {}; 14 | const topic = opts.topic; 15 | /* Check required fields */ 16 | if (!_.isString(topic) || topic.length <= 0) return null; 17 | 18 | const qos = opts.qos || 0; 19 | if (!_.isNumber(qos) || qos < 0 || qos > 2) return null; 20 | const retain = opts.retain ? protocol.RETAIN_MASK : 0; 21 | 22 | const id = _.isUndefined(opts.messageId) ? GenerateUtility.RandInt() : opts.messageId; 23 | if (!_.isNumber(id) || id < 0 || id > 0xFFFF) return null; 24 | 25 | let payload = opts.payload || new Buffer(0); 26 | /* if payload is a string, we'll convert it into a buffer */ 27 | if (_.isString(payload)) 28 | { 29 | payload = new Buffer(payload); 30 | } 31 | /* accepting only a buffer for payload */ 32 | if (!Buffer.isBuffer(payload)) return null; 33 | 34 | const packet = { 35 | header : 0, 36 | payload : [] 37 | }; 38 | const dup = opts.dup ? protocol.DUP_MASK : 0; 39 | /* Generate header */ 40 | packet.header = protocol.codes.publish << protocol.CMD_SHIFT | dup | qos << protocol.QOS_SHIFT | retain; 41 | 42 | /* Topic name */ 43 | packet.payload = packet.payload.concat(GenerateUtility.GenerateString(topic)); 44 | 45 | /* Message ID */ 46 | if (qos > 0) packet.payload = packet.payload.concat(GenerateUtility.GenerateNumber(id)); 47 | 48 | const buf = new Buffer([packet.header] 49 | .concat(GenerateUtility.GenerateLength(packet.payload.length + payload.length)) 50 | .concat(packet.payload)); 51 | 52 | return Buffer.concat([buf, payload]); 53 | } 54 | } 55 | 56 | class GenerateUtility 57 | { 58 | /** 59 | * Requires length be a number > 0 60 | * @param length 61 | */ 62 | static GenerateLength(length) 63 | { 64 | if (!_.isNumber(length) || length < 0) return null; 65 | const len = []; 66 | let digit = 0; 67 | 68 | do 69 | { 70 | digit = length % 128 | 0; 71 | length = length / 128 | 0; 72 | if (length > 0) 73 | { 74 | digit = digit | 0x80; 75 | } 76 | len.push(digit); 77 | } while (length > 0); 78 | 79 | return len; 80 | } 81 | 82 | /* based on code in (from http://farhadi.ir/downloads/utf8.js) */ 83 | static GenerateString(str, withoutLength) 84 | { 85 | if (arguments.length < 2) withoutLength = false; 86 | if (!_.isString(str) || !_.isBoolean(withoutLength)) return null; 87 | 88 | const string = []; 89 | let length = 0; 90 | for (let i = 0; i < str.length; i++) 91 | { 92 | const code = str.charCodeAt(i); 93 | if (code < 128) 94 | { 95 | string.push(code); ++length; 96 | 97 | } 98 | else if (code < 2048) 99 | { 100 | string.push(192 + ((code >> 6))); ++length; 101 | string.push(128 + ((code) & 63)); ++length; 102 | } 103 | else if (code < 65536) 104 | { 105 | string.push(224 + ((code >> 12))); ++length; 106 | string.push(128 + ((code >> 6) & 63)); ++length; 107 | string.push(128 + ((code) & 63)); ++length; 108 | } 109 | else if (code < 2097152) 110 | { 111 | string.push(240 + ((code >> 18))); ++length; 112 | string.push(128 + ((code >> 12) & 63)); ++length; 113 | string.push(128 + ((code >> 6) & 63)); ++length; 114 | string.push(128 + ((code) & 63)); ++length; 115 | } 116 | else 117 | { 118 | throw new Error(`Can't encode character with code ${code}`); 119 | } 120 | } 121 | return withoutLength ? string : GenerateUtility.GenerateNumber(length).concat(string); 122 | } 123 | 124 | static GenerateNumber(num) 125 | { 126 | return [num >> 8, num & 0x00FF]; 127 | } 128 | 129 | static RandInt() 130 | { 131 | return Math.floor(Math.random() * 0xFFFF); 132 | } 133 | } 134 | 135 | /* Publish */ 136 | module.exports.publish = Generate.Publish; -------------------------------------------------------------------------------- /lib/connectors/mqtt/mqttSocket.js: -------------------------------------------------------------------------------- 1 | const forEach = require('lodash').forEach, 2 | EventEmitter = require('events').EventEmitter; 3 | 4 | const ST_INITED = 1; 5 | const ST_CLOSED = 2; 6 | 7 | /** 8 | * Socket class that wraps socket and websocket to provide unified interface for up level. 9 | */ 10 | class MQTTSocket extends EventEmitter 11 | { 12 | constructor(id, socket, adaptor) 13 | { 14 | super(); 15 | this.id = id; 16 | this.socket = socket; 17 | this.remoteAddress = { 18 | ip : socket.stream.remoteAddress, 19 | port : socket.stream.remotePort 20 | }; 21 | this.adaptor = adaptor; 22 | 23 | socket.on('close', this.emit.bind(this, 'disconnect')); 24 | socket.on('error', this.emit.bind(this, 'disconnect')); 25 | socket.on('disconnect', this.emit.bind(this, 'disconnect')); 26 | 27 | socket.on('pingreq', packet => 28 | { 29 | socket.pingresp(); 30 | }); 31 | 32 | socket.on('subscribe', this.adaptor.onSubscribe.bind(this.adaptor, this)); 33 | 34 | socket.on('publish', this.adaptor.onPublish.bind(this.adaptor, this)); 35 | 36 | this.state = ST_INITED; 37 | 38 | // TODO: any other events? 39 | } 40 | 41 | send(msg) 42 | { 43 | if (this.state !== ST_INITED) 44 | { 45 | return; 46 | } 47 | if (msg instanceof Buffer) 48 | { 49 | // if encoded, send directly 50 | this.socket.stream.write(msg); 51 | } 52 | else 53 | { 54 | this.adaptor.publish(this, msg); 55 | } 56 | } 57 | 58 | sendBatch(messages) 59 | { 60 | forEach(messages, message => 61 | { 62 | this.send(message); 63 | }); 64 | } 65 | 66 | disconnect() 67 | { 68 | if (this.state === ST_CLOSED) 69 | { 70 | return; 71 | } 72 | 73 | this.state = ST_CLOSED; 74 | this.socket.stream.destroy(); 75 | } 76 | } 77 | 78 | module.exports = function(id, socket, adaptor) 79 | { 80 | if (!(this instanceof MQTTSocket)) 81 | { 82 | return new MQTTSocket(id, socket, adaptor); 83 | } 84 | }; -------------------------------------------------------------------------------- /lib/connectors/mqtt/mqttadaptor.js: -------------------------------------------------------------------------------- 1 | 2 | class Adaptor 3 | { 4 | constructor(opts) 5 | { 6 | opts = opts || {}; 7 | this.subReqs = {}; 8 | this.publishRoute = opts.publishRoute; 9 | this.subscribeRoute = opts.subscribeRoute; 10 | } 11 | 12 | onPublish(client, packet) 13 | { 14 | if (!this.publishRoute) 15 | { 16 | throw new Error('unspecified publish route.'); 17 | } 18 | 19 | const req = { 20 | id : packet.messageId, 21 | route : this.publishRoute, 22 | body : packet 23 | }; 24 | 25 | client.emit('message', req); 26 | 27 | if (packet.qos === 1) 28 | { 29 | client.socket.puback({messageId: packet.messageId}); 30 | } 31 | } 32 | 33 | onSubscribe(client, packet) 34 | { 35 | if (!this.subscribeRoute) 36 | { 37 | throw new Error('unspecified subscribe route.'); 38 | } 39 | 40 | const req = { 41 | id : packet.messageId, 42 | route : this.subscribeRoute, 43 | body : { 44 | subscriptions : packet.subscriptions 45 | } 46 | }; 47 | 48 | this.subReqs[packet.messageId] = packet; 49 | 50 | client.emit('message', req); 51 | } 52 | 53 | onPubAck(client, packet) 54 | { 55 | const req = { 56 | id : packet.messageId, 57 | route : 'connector.mqttHandler.pubAck', 58 | body : { 59 | mid : packet.messageId 60 | } 61 | }; 62 | 63 | this.subReqs[packet.messageId] = packet; 64 | 65 | client.emit('message', req); 66 | } 67 | 68 | /** 69 | * Publish message or subscription ack. 70 | * 71 | * if packet.id exist and this.subReqs[packet.id] exist then packet is a suback. 72 | * Subscription is request/response mode. 73 | * packet.id is pass from client in packet.messageId and record in Pomelo context and attached to the subscribe response packet. 74 | * packet.body is the context that returned by subscribe next callback. 75 | * 76 | * if packet.id not exist then packet is a publish message. 77 | * 78 | * otherwise packet is a illegal packet. 79 | */ 80 | publish(client, packet) 81 | { 82 | const mid = packet.id; 83 | const subReq = this.subReqs[mid]; 84 | if (subReq) 85 | { 86 | // is suBack 87 | client.socket.suback({ 88 | messageId : mid, 89 | granted : packet.body}); 90 | delete this.subReqs[mid]; 91 | } 92 | else 93 | { 94 | client.socket.publish(packet.body); 95 | } 96 | } 97 | } 98 | 99 | module.exports = function(opts) 100 | { 101 | if (!(this instanceof Adaptor)) 102 | { 103 | return new Adaptor(opts); 104 | } 105 | }; -------------------------------------------------------------------------------- /lib/connectors/mqtt/protocol.js: -------------------------------------------------------------------------------- 1 | /* Protocol - protocol constants */ 2 | 3 | /* Command code => mnemonic */ 4 | module.exports.types = { 5 | 0 : 'reserved', 6 | 1 : 'connect', 7 | 2 : 'connack', 8 | 3 : 'publish', 9 | 4 : 'puback', 10 | 5 : 'pubrec', 11 | 6 : 'pubrel', 12 | 7 : 'pubcomp', 13 | 8 : 'subscribe', 14 | 9 : 'suback', 15 | 10 : 'unsubscribe', 16 | 11 : 'unsuback', 17 | 12 : 'pingreq', 18 | 13 : 'pingresp', 19 | 14 : 'disconnect', 20 | 15 : 'reserved' 21 | }; 22 | 23 | /* Mnemonic => Command code */ 24 | module.exports.codes = {}; 25 | for (const k in module.exports.types) 26 | { 27 | const v = module.exports.types[k]; 28 | module.exports.codes[v] = k; 29 | } 30 | 31 | /* Header */ 32 | module.exports.CMD_SHIFT = 4; 33 | module.exports.CMD_MASK = 0xF0; 34 | module.exports.DUP_MASK = 0x08; 35 | module.exports.QOS_MASK = 0x03; 36 | module.exports.QOS_SHIFT = 1; 37 | module.exports.RETAIN_MASK = 0x01; 38 | 39 | /* Length */ 40 | module.exports.LENGTH_MASK = 0x7F; 41 | module.exports.LENGTH_FIN_MASK = 0x80; 42 | 43 | /* Connect */ 44 | module.exports.USERNAME_MASK = 0x80; 45 | module.exports.PASSWORD_MASK = 0x40; 46 | module.exports.WILL_RETAIN_MASK = 0x20; 47 | module.exports.WILL_QOS_MASK = 0x18; 48 | module.exports.WILL_QOS_SHIFT = 3; 49 | module.exports.WILL_FLAG_MASK = 0x04; 50 | module.exports.CLEAN_SESSION_MASK = 0x02; -------------------------------------------------------------------------------- /lib/connectors/mqttConnector.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter, 2 | mqtt = require('mqtt'), 3 | constants = require('../util/constants'), 4 | MQTTSocket = require('./mqtt/mqttSocket'), 5 | Adaptor = require('./mqtt/mqttadaptor'), 6 | generate = require('./mqtt/generate'), 7 | logger = require('pomelo-logger').getLogger('pomelo', __filename); 8 | 9 | let curId = 1; 10 | /** 11 | * Connector that manager low level connection and protocol bewteen server and client. 12 | * Develper can provide their own connector to switch the low level prototol, such as tcp or probuf. 13 | */ 14 | class MQTTConnector extends EventEmitter 15 | { 16 | constructor(port, host, opts) 17 | { 18 | super(); 19 | this.port = port; 20 | this.host = host; 21 | this.opts = opts || {}; 22 | this.adaptor = new Adaptor(this.opts); 23 | } 24 | 25 | /** 26 | * Start connector to listen the specified port 27 | */ 28 | start(callBack) 29 | { 30 | this.mqttServer = mqtt.createServer(); 31 | this.mqttServer.on('client', client => 32 | { 33 | client.on('error', err => 34 | { 35 | logger.error(`mqttConnector 连接失败, errorInfo:${err}`); 36 | client.stream.destroy(); 37 | }); 38 | 39 | client.on('close', () => 40 | { 41 | client.stream.destroy(); 42 | }); 43 | 44 | client.on('disconnect', packet => 45 | { 46 | client.stream.destroy(); 47 | }); 48 | 49 | if (this.opts.disconnectOnTimeout) 50 | { 51 | const timeout = this.opts.timeout * 1000 || constants.TIME.DEFAULT_MQTT_HEARTBEAT_TIMEOUT; 52 | client.stream.setTimeout(timeout, () => 53 | { 54 | client.emit('close'); 55 | }); 56 | } 57 | 58 | client.on('connect', packet => 59 | { 60 | client.connack({returnCode: 0}); 61 | const mQTTSocket = new MQTTSocket(curId++, client, this.adaptor); 62 | this.emit('connection', mQTTSocket); 63 | }); 64 | }); 65 | 66 | this.mqttServer.listen(this.port); 67 | 68 | process.nextTick(callBack); 69 | } 70 | 71 | stop() 72 | { 73 | this.mqttServer.close(); 74 | process.exit(0); 75 | } 76 | 77 | encode(reqId, route, msgBody) 78 | { 79 | if (reqId) 80 | { 81 | return MQTTConnectorUtility.ComposeResponse(reqId, route, msgBody); 82 | } 83 | 84 | return MQTTConnectorUtility.ComposePush(route, msgBody); 85 | } 86 | 87 | close() 88 | { 89 | this.mqttServer.close(); 90 | } 91 | } 92 | 93 | class MQTTConnectorUtility 94 | { 95 | static ComposeResponse(msgId, route, msgBody) 96 | { 97 | return { 98 | id : msgId, 99 | body : msgBody 100 | }; 101 | } 102 | 103 | static ComposePush(route, msgBody) 104 | { 105 | const msg = generate.publish(msgBody); 106 | if (!msg) 107 | { 108 | logger.error(`invalid mqtt publish message: ${msgBody}`); 109 | } 110 | return msg; 111 | } 112 | } 113 | 114 | module.exports = function(port, host, opts) 115 | { 116 | if (!(this instanceof MQTTConnector)) 117 | { 118 | return new MQTTConnector(port, host, opts); 119 | } 120 | }; -------------------------------------------------------------------------------- /lib/connectors/sio/sioSocket.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | EventEmitter = require('events').EventEmitter; 3 | 4 | const ST_INITED = 0; 5 | const ST_CLOSED = 1; 6 | 7 | /** 8 | * Socket class that wraps socket.io socket to provide unified interface for up level. 9 | */ 10 | class SioSocket extends EventEmitter 11 | { 12 | constructor(id, socket) 13 | { 14 | super(); 15 | this.id = id; 16 | this.socket = socket; 17 | this.remoteAddress = { 18 | ip : socket.handshake.address.address, 19 | port : socket.handshake.address.port 20 | }; 21 | 22 | socket.on('disconnect', this.emit.bind(this, 'disconnect')); 23 | 24 | socket.on('error', this.emit.bind(this, 'error')); 25 | 26 | socket.on('message', msg => 27 | { 28 | this.emit('message', msg); 29 | }); 30 | 31 | this.state = ST_INITED; 32 | 33 | // TODO: any other events? 34 | } 35 | 36 | send(msg) 37 | { 38 | if (this.state !== ST_INITED) 39 | { 40 | return; 41 | } 42 | if (!_.isString(msg)) 43 | { 44 | msg = JSON.stringify(msg); 45 | } 46 | this.socket.send(msg); 47 | } 48 | 49 | disconnect() 50 | { 51 | if (this.state === ST_CLOSED) 52 | { 53 | return; 54 | } 55 | 56 | this.state = ST_CLOSED; 57 | this.socket.disconnect(); 58 | } 59 | 60 | sendBatch(msgs) 61 | { 62 | this.send(SioSocketUtility.EnCodeBatch(msgs)); 63 | } 64 | } 65 | 66 | class SioSocketUtility 67 | { 68 | /** 69 | * Encode batch msg to client 70 | */ 71 | static EnCodeBatch(msgs) 72 | { 73 | let res = '[', msg; 74 | for (let i = 0, l = msgs.length; i < l; i++) 75 | { 76 | if (i > 0) 77 | { 78 | res += ','; 79 | } 80 | msg = msgs[i]; 81 | if (_.isString(msg)) 82 | { 83 | res += msg; 84 | } 85 | else 86 | { 87 | res += JSON.stringify(msg); 88 | } 89 | } 90 | res += ']'; 91 | return res; 92 | } 93 | } 94 | 95 | module.exports = function(id, socket) 96 | { 97 | if (!(this instanceof SioSocket)) 98 | { 99 | return new SioSocket(id, socket); 100 | } 101 | }; -------------------------------------------------------------------------------- /lib/connectors/sioConnector.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter; 2 | const httpServer = require('http').createServer(); 3 | const SioSocket = require('./sio/sioSocket'); 4 | 5 | const PKG_ID_BYTES = 4; 6 | const PKG_ROUTE_LENGTH_BYTES = 1; 7 | const PKG_HEAD_BYTES = PKG_ID_BYTES + PKG_ROUTE_LENGTH_BYTES; 8 | 9 | let curId = 1; 10 | 11 | /** 12 | * Connector that manager low level connection and protocol bewteen server and client. 13 | * Develper can provide their own connector to switch the low level prototol, such as tcp or probuf. 14 | */ 15 | class SioConnector extends EventEmitter 16 | { 17 | constructor(port, host, opts) 18 | { 19 | super(); 20 | this.port = port; 21 | this.host = host; 22 | this.opts = opts; 23 | this.heartbeats = opts.heartbeats || true; 24 | this.closeTimeout = opts.closeTimeout || 60; 25 | this.heartbeatTimeout = opts.heartbeatTimeout || 60; 26 | this.heartbeatInterval = opts.heartbeatInterval || 25; 27 | } 28 | 29 | /** 30 | * Start connector to listen the specified port 31 | */ 32 | start(cb) 33 | { 34 | // issue https://github.com/NetEase/pomelo-cn/issues/174 35 | if (this.opts) 36 | { 37 | this.wsocket = sio.listen(this.port, this.opts); 38 | } 39 | else 40 | { 41 | // 'websocket', 'htmlfile', 'xhr-polling', 'jsonp-polling', 'flashsocket' 42 | this.wsocket = sio.listen(this.port, { 43 | transports : [ 44 | 'websocket', 'polling' 45 | ] 46 | }); 47 | } 48 | let opts = {}; 49 | if (this.opts) 50 | { 51 | opts = this.opts; 52 | } 53 | else 54 | { 55 | opts = { 56 | transports : ['websocket', 'polling-xhr', 'polling-jsonp', 'polling'] 57 | }; 58 | } 59 | 60 | const sio = require('socket.io')(httpServer, opts); 61 | 62 | const port = this.port; 63 | httpServer.listen(port, () => 64 | { 65 | console.log('sio Server listening at port %d', port); 66 | }); 67 | sio.set('path', '/socket.io'); 68 | // sio.set('transports', this.opts.transports); 69 | sio.set('close timeout', this.closeTimeout); 70 | // sio.set('heartbeat timeout', this.heartbeatTimeout); 71 | // sio.set('heartbeat interval', this.heartbeatInterval); 72 | // sio.set('heartbeats', this.heartbeats); 73 | // sio.set('log level', 1); 74 | 75 | sio.set('resource', '/socket.io'); 76 | sio.set('transports', this.opts.transports); 77 | sio.set('heartbeat timeout', this.heartbeatTimeout); 78 | sio.set('heartbeat interval', this.heartbeatInterval); 79 | 80 | sio.on('connection', socket => 81 | { 82 | // this.wsocket.sockets.on('connection', function (socket) { 83 | const sioSocket = new SioSocket(curId++, socket); 84 | this.emit('connection', sioSocket); 85 | sioSocket.on('closing', reason => 86 | { 87 | sioSocket.send({ 88 | route : 'onKick', 89 | reason : reason}); 90 | }); 91 | }); 92 | process.nextTick(cb); 93 | } 94 | 95 | /** 96 | * Stop connector 97 | */ 98 | stop(force, cb) 99 | { 100 | httpServer.close(); 101 | process.nextTick(cb); 102 | } 103 | } 104 | 105 | class SioConnectorUtility 106 | { 107 | static EnCode(reqId, route, msg) 108 | { 109 | if (reqId) 110 | { 111 | return SioConnectorUtility.composeResponse(reqId, route, msg); 112 | } 113 | return SioConnectorUtility.composePush(route, msg); 114 | } 115 | 116 | /** 117 | * Decode client message package. 118 | * 119 | * Package format: 120 | * message id: 4bytes big-endian integer 121 | * route length: 1byte 122 | * route: route length bytes 123 | * body: the rest bytes 124 | * 125 | * @param {String} msg socket.io package from client 126 | * @return {Object} message object 127 | */ 128 | static DeCode(msg) 129 | { 130 | let index = 0; 131 | 132 | const id = SioConnectorUtility.parseIntField(msg, index, PKG_ID_BYTES); 133 | index += PKG_ID_BYTES; 134 | 135 | const routeLen = SioConnectorUtility.parseIntField(msg, index, PKG_ROUTE_LENGTH_BYTES); 136 | 137 | const route = msg.substr(PKG_HEAD_BYTES, routeLen); 138 | const body = msg.substr(PKG_HEAD_BYTES + routeLen); 139 | 140 | return { 141 | id : id, 142 | route : route, 143 | body : JSON.parse(body) 144 | }; 145 | } 146 | 147 | static composeResponse(msgId, route, msgBody) 148 | { 149 | return { 150 | id : msgId, 151 | body : msgBody 152 | }; 153 | } 154 | 155 | static composePush(route, msgBody) 156 | { 157 | return JSON.stringify({ 158 | route : route, 159 | body : msgBody}); 160 | } 161 | 162 | static parseIntField(str, offset, len) 163 | { 164 | let res = 0; 165 | for (let i = 0; i < len; i++) 166 | { 167 | if (i > 0) 168 | { 169 | res <<= 8; 170 | } 171 | res |= str.charCodeAt(offset + i) & 0xff; 172 | } 173 | return res; 174 | } 175 | } 176 | 177 | SioConnector.encode = SioConnector.prototype.encode = SioConnectorUtility.EnCode; 178 | SioConnector.decode = SioConnector.prototype.decode = SioConnectorUtility.DeCode; 179 | 180 | module.exports = function(port, host, opts) 181 | { 182 | if (!(this instanceof SioConnector)) 183 | { 184 | return new SioConnector(port, host, opts); 185 | } 186 | }; -------------------------------------------------------------------------------- /lib/connectors/udp/udpSocket.js: -------------------------------------------------------------------------------- 1 | const forEach = require('lodash').forEach, 2 | handler = require('../common/handler'), 3 | protocol = require('pomelo-protocol'), 4 | Package = protocol.Package, 5 | EventEmitter = require('events').EventEmitter, 6 | logger = require('pomelo-logger').getLogger('pomelo', __filename); 7 | 8 | const ST_INITED = 0; 9 | const ST_WAIT_ACK = 1; 10 | const ST_WORKING = 2; 11 | const ST_CLOSED = 3; 12 | class UDPSocket extends EventEmitter 13 | { 14 | constructor(id, socket, peer) 15 | { 16 | super(); 17 | 18 | this.id = id; 19 | this.socket = socket; 20 | this.peer = peer; 21 | this.host = peer.address; 22 | this.port = peer.port; 23 | this.remoteAddress = { 24 | ip : this.host, 25 | port : this.port 26 | }; 27 | 28 | this.on('package', pkg => 29 | { 30 | if (pkg) 31 | { 32 | pkg = Package.decode(pkg); 33 | handler(this, pkg); 34 | } 35 | }); 36 | 37 | this.state = ST_INITED; 38 | } 39 | 40 | /** 41 | * Send byte data package to client. 42 | * 43 | * @param {Buffer} msg byte data 44 | */ 45 | send(msg) 46 | { 47 | if (this.state !== ST_WORKING) 48 | { 49 | return; 50 | } 51 | if (msg instanceof String) 52 | { 53 | msg = new Buffer(msg); 54 | } 55 | else if (!(msg instanceof Buffer)) 56 | { 57 | msg = new Buffer(JSON.stringify(msg)); 58 | } 59 | this.sendRaw(Package.encode(Package.TYPE_DATA, msg)); 60 | } 61 | 62 | sendRaw(msg) 63 | { 64 | this.socket.send(msg, 0, msg.length, this.port, this.host, function(err, bytes) 65 | { 66 | if (err) 67 | { 68 | logger.error('send msg to remote with err: %j', err.stack); 69 | return; 70 | } 71 | }); 72 | } 73 | 74 | sendForce(msg) 75 | { 76 | if (this.state === ST_CLOSED) 77 | { 78 | return; 79 | } 80 | this.sendRaw(msg); 81 | } 82 | 83 | handshakeResponse(resp) 84 | { 85 | if (this.state !== ST_INITED) 86 | { 87 | return; 88 | } 89 | this.sendRaw(resp); 90 | this.state = ST_WAIT_ACK; 91 | } 92 | 93 | sendBatch(messages) 94 | { 95 | if (this.state !== ST_WORKING) 96 | { 97 | return; 98 | } 99 | const rs = []; 100 | forEach(messages, message => 101 | { 102 | const src = Package.encode(Package.TYPE_DATA, message); 103 | rs.push(src); 104 | }); 105 | this.sendRaw(Buffer.concat(rs)); 106 | } 107 | 108 | disconnect() 109 | { 110 | if (this.state === ST_CLOSED) 111 | { 112 | return; 113 | } 114 | this.state = ST_CLOSED; 115 | this.emit('disconnect', 'the connection is disconnected.'); 116 | } 117 | } 118 | 119 | module.exports = function(id, socket, peer) 120 | { 121 | if (!(this instanceof UDPSocket)) 122 | { 123 | return new UDPSocket(id, socket, peer); 124 | } 125 | }; -------------------------------------------------------------------------------- /lib/connectors/udpConnector.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const dGram = require('dgram'); 3 | const utils = require('../util/utils'); 4 | const Constants = require('../util/constants'); 5 | const UdpSocket = require('./udp/udpSocket'); 6 | const Kick = require('./commands/kick'); 7 | const Handshake = require('./commands/handshake'); 8 | const Heartbeat = require('./commands/heartbeat'); 9 | const coder = require('./common/coder'); 10 | const EventEmitter = require('events').EventEmitter; 11 | const logger = require('pomelo-logger').getLogger('pomelo', __filename); 12 | 13 | let curId = 1; 14 | 15 | class UDPConnector extends EventEmitter 16 | { 17 | constructor(port, host, opts) 18 | { 19 | super(); 20 | this.opts = opts || {}; 21 | this.type = opts.udpType || 'udp4'; 22 | this.handshake = new Handshake(opts); 23 | if (!opts.heartbeat) 24 | { 25 | opts.heartbeat = Constants.TIME.DEFAULT_UDP_HEARTBEAT_TIME; 26 | opts.timeout = Constants.TIME.DEFAULT_UDP_HEARTBEAT_TIMEOUT; 27 | } 28 | this.heartbeat = new Heartbeat(utils.extends(opts, {disconnectOnTimeout: true})); 29 | this.clients = {}; 30 | this.host = host; 31 | this.port = port; 32 | } 33 | 34 | start(cb) 35 | { 36 | this.tcpServer = net.createServer(); 37 | this.socket = dGram.createSocket(this.type, (msg, peer) => 38 | { 39 | const key = genKey(peer); 40 | if (!this.clients[key]) 41 | { 42 | const udpSocket = new UdpSocket(curId++, this.socket, peer); 43 | this.clients[key] = udpSocket; 44 | 45 | udpSocket.on('handshake', 46 | this.handshake.handle.bind(this.handshake, udpSocket)); 47 | 48 | udpSocket.on('heartbeat', 49 | this.heartbeat.handle.bind(this.heartbeat, udpSocket)); 50 | 51 | udpSocket.on('disconnect', 52 | this.heartbeat.clear.bind(this.heartbeat, udpSocket.id)); 53 | 54 | udpSocket.on('disconnect', () => 55 | { 56 | delete this.clients[genKey(udpSocket.peer)]; 57 | }); 58 | 59 | udpSocket.on('closing', Kick.handle.bind(null, udpSocket)); 60 | 61 | this.emit('connection', udpSocket); 62 | } 63 | }); 64 | 65 | this.socket.on('message', (data, peer) => 66 | { 67 | const socket = this.clients[genKey(peer)]; 68 | if (socket) 69 | { 70 | socket.emit('package', data); 71 | } 72 | }); 73 | 74 | this.socket.on('error', (err) => 75 | { 76 | logger.error(`udp socket encounters with error: ${err.stack}`); 77 | return; 78 | }); 79 | 80 | this.socket.bind(this.port, this.host); 81 | this.tcpServer.listen(this.port); 82 | process.nextTick(cb); 83 | } 84 | 85 | stop(force, cb) 86 | { 87 | this.socket.close(); 88 | process.nextTick(cb); 89 | } 90 | 91 | } 92 | 93 | const genKey = peer => 94 | { 95 | return `${peer.address}:${peer.port}`; 96 | }; 97 | 98 | UDPConnector.decode = UDPConnector.prototype.decode = coder.decode; 99 | 100 | UDPConnector.encode = UDPConnector.prototype.encode = coder.encode; 101 | 102 | module.exports = function(port, host, opts) 103 | { 104 | if (!(this instanceof UDPConnector)) 105 | { 106 | return new UDPConnector(port, host, opts); 107 | } 108 | }; -------------------------------------------------------------------------------- /lib/filters/handler/serial.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filter to keep request sequence. 3 | */ 4 | const logger = require('pomelo-logger').getLogger('pomelo', __filename); 5 | const taskManager = require('../../common/manager/taskManager'); 6 | 7 | class Serial 8 | { 9 | constructor(timeout) 10 | { 11 | this.timeout = timeout; 12 | } 13 | 14 | /** 15 | * request serialization after filter 16 | */ 17 | before(msg, session, next) 18 | { 19 | taskManager.addTask(session.id, function(task) 20 | { 21 | session.__serialTask__ = task; 22 | next(); 23 | }, () => 24 | { 25 | logger.error(`[serial filter] msg timeout, msg:${JSON.stringify(msg)}`); 26 | }, this.timeout); 27 | } 28 | 29 | /** 30 | * request serialization after filter 31 | */ 32 | after(err, msg, session, resp, next) 33 | { 34 | const task = session.__serialTask__; 35 | if (task) 36 | { 37 | if (!task.done() && !err) 38 | { 39 | err = new Error(`task time out. msg:${JSON.stringify(msg)}`); 40 | } 41 | } 42 | next(err); 43 | } 44 | } 45 | 46 | module.exports = function(timeout) 47 | { 48 | return new Serial(timeout); 49 | }; -------------------------------------------------------------------------------- /lib/filters/handler/time.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filter for statistics. 3 | * Record used time for each request. 4 | */ 5 | const conLogger = require('pomelo-logger').getLogger('con-log', __filename); 6 | const utils = require('../../util/utils'); 7 | 8 | class Time 9 | { 10 | before(msg, session, next) 11 | { 12 | session.__startTime__ = Date.now(); 13 | next(); 14 | } 15 | 16 | after(err, msg, session, resp, next) 17 | { 18 | const start = session.__startTime__; 19 | if (typeof start === 'number') 20 | { 21 | const timeUsed = Date.now() - start; 22 | const log = { 23 | route : msg.__route__, 24 | args : msg, 25 | time : utils.format(new Date(start)), 26 | timeUsed : timeUsed 27 | }; 28 | conLogger.info(JSON.stringify(log)); 29 | } 30 | next(err); 31 | } 32 | } 33 | 34 | module.exports = () => {return new Time();}; 35 | -------------------------------------------------------------------------------- /lib/filters/handler/timeout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filter for timeout. 3 | * Print a warn information when request timeout. 4 | */ 5 | const logger = require('pomelo-logger').getLogger('pomelo', __filename); 6 | const utils = require('../../util/utils'); 7 | 8 | const DEFAULT_TIMEOUT = 3000; 9 | const DEFAULT_SIZE = 500; 10 | 11 | module.exports = function(timeout, maxSize) 12 | { 13 | return new TimeOut(timeout, maxSize); 14 | }; 15 | 16 | class TimeOut 17 | { 18 | constructor(timeout, maxSize) 19 | { 20 | this.timeout = timeout || DEFAULT_TIMEOUT; 21 | this.maxSize = maxSize || DEFAULT_SIZE; 22 | this.timeouts = {}; 23 | this.curId = 0; 24 | } 25 | 26 | before(msg, session, next) 27 | { 28 | const count = utils.size(this.timeouts); 29 | if (count > this.maxSize) 30 | { 31 | logger.warn('timeout filter is out of range, current size is %s, max size is %s', count, this.maxSize); 32 | next(); 33 | return; 34 | } 35 | this.curId++; 36 | this.timeouts[this.curId] = setTimeout(() => 37 | { 38 | logger.warn('request %j timeout.', msg.__route__); 39 | }, this.timeout); 40 | session.__timeout__ = this.curId; 41 | next(); 42 | } 43 | 44 | after(err, msg, session, resp, next) 45 | { 46 | const timeout = this.timeouts[session.__timeout__]; 47 | if (timeout) 48 | { 49 | clearTimeout(timeout); 50 | delete this.timeouts[session.__timeout__]; 51 | } 52 | next(err); 53 | } 54 | } -------------------------------------------------------------------------------- /lib/filters/handler/toobusy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filter for toobusy. 3 | * if the process is toobusy, just skip the new request 4 | */ 5 | const conLogger = require('pomelo-logger').getLogger('con-log', __filename); 6 | let toobusy = null; 7 | const DEFAULT_MAXLAG = 70; 8 | 9 | class TooBusy 10 | { 11 | constructor(maxLag) 12 | { 13 | try {toobusy = require('toobusy');} 14 | catch (e) {conLogger.error(`[toobusy] reject request msg: ${e}`);} 15 | 16 | if (toobusy) 17 | { 18 | toobusy.maxLag(maxLag); 19 | } 20 | } 21 | 22 | before(msg, session, next) 23 | { 24 | if (Boolean(toobusy) && new target()) 25 | { 26 | conLogger.warn(`[toobusy] reject request msg: ${msg}`); 27 | const err = new Error('Server toobusy!'); 28 | err.code = 500; 29 | next(err); 30 | } 31 | else 32 | { 33 | next(); 34 | } 35 | } 36 | } 37 | 38 | module.exports = function(maxLag) 39 | { 40 | return new TooBusy(maxLag || DEFAULT_MAXLAG); 41 | }; -------------------------------------------------------------------------------- /lib/filters/rpc/rpcLog.js: -------------------------------------------------------------------------------- 1 | const rpcLogger = require('pomelo-logger').getLogger('rpc-log', __filename); 2 | const utils = require('../../util/utils'); 3 | 4 | class RpcLog 5 | { 6 | constructor() 7 | { 8 | this.name = 'rpcLog'; 9 | } 10 | 11 | /** 12 | * Before filter for rpc 13 | */ 14 | before(serverId, msg, opts, next) 15 | { 16 | opts = opts || {}; 17 | opts.__start_time__ = Date.now(); 18 | next(serverId, msg, opts); 19 | } 20 | 21 | /** 22 | * After filter for rpc 23 | */ 24 | after(serverId, msg, opts, next) 25 | { 26 | if (opts && opts.__start_time__) 27 | { 28 | const start = opts.__start_time__; 29 | const end = Date.now(); 30 | const timeUsed = end - start; 31 | const log = { 32 | route : msg.service, 33 | args : msg.args, 34 | time : utils.format(new Date(start)), 35 | timeUsed : timeUsed 36 | }; 37 | rpcLogger.info(JSON.stringify(log)); 38 | } 39 | next(serverId, msg, opts); 40 | } 41 | } 42 | 43 | module.exports = function() 44 | { 45 | return new RpcLog(); 46 | }; -------------------------------------------------------------------------------- /lib/filters/rpc/toobusy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filter for rpc log. 3 | * Reject rpc request when toobusy 4 | */ 5 | const rpcLogger = require('pomelo-logger').getLogger('rpc-log', __filename); 6 | 7 | let toobusy = null; 8 | const DEFAULT_MAXLAG = 70; 9 | 10 | const Filter = function(maxLag) 11 | { 12 | try 13 | { 14 | toobusy = require('toobusy'); 15 | } 16 | catch (e) 17 | { 18 | // null 19 | } 20 | if (toobusy) 21 | { 22 | toobusy.maxLag(maxLag); 23 | } 24 | }; 25 | 26 | Filter.prototype.name = 'toobusy'; 27 | 28 | /** 29 | * Before filter for rpc 30 | */ 31 | Filter.prototype.before = function(serverId, msg, opts, next) 32 | { 33 | opts = opts || {}; 34 | if (toobusy && toobusy()) 35 | { 36 | rpcLogger.warn(`Server too busy for rpc request, serverId:${serverId} msg:${msg}`); 37 | const err = new Error(`Backend server : ${serverId} is too busy now!`); 38 | err.code = 500; 39 | next(err); 40 | } 41 | else 42 | { 43 | next(serverId, msg, opts); 44 | } 45 | }; 46 | 47 | module.exports = function(maxLag) 48 | { 49 | return new Filter(maxLag || DEFAULT_MAXLAG); 50 | }; 51 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./pomelo'); -------------------------------------------------------------------------------- /lib/master/master.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | starter = require('./starter'), 3 | util = require('util'), 4 | utils = require('../util/utils'), 5 | admin = require('pomelo-admin-upgrade'), 6 | moduleUtil = require('../util/moduleUtil'), 7 | Constants = require('../util/constants'), 8 | logger = require('pomelo-logger').getLogger('pomelo', __filename), 9 | crashLogger = require('pomelo-logger').getLogger('crash-log', __filename), 10 | adminLogger = require('pomelo-logger').getLogger('admin-log', __filename); 11 | 12 | class Master 13 | { 14 | constructor(app, opts) 15 | { 16 | this.app = app; 17 | this.masterInfo = app.getMaster(); 18 | this.registered = {}; 19 | this.modules = []; 20 | opts = opts || {}; 21 | 22 | opts.port = this.masterInfo.port; 23 | opts.env = this.app.get(Constants.RESERVED.ENV); 24 | this.closeWatcher = opts.closeWatcher; 25 | this.masterConsole = admin.createMasterConsole(opts); 26 | } 27 | 28 | start(cb) 29 | { 30 | moduleUtil.registerDefaultModules(true, this.app, this.closeWatcher); 31 | moduleUtil.loadModules(this, this.masterConsole); 32 | 33 | // start master console 34 | this.masterConsole.start((err) => 35 | { 36 | if (err) 37 | { 38 | process.exit(0); 39 | } 40 | moduleUtil.startModules(this.modules, err => 41 | { 42 | if (err) 43 | { 44 | utils.invokeCallback(cb, err); 45 | return; 46 | } 47 | 48 | if (this.app.get(Constants.RESERVED.MODE) !== Constants.RESERVED.STAND_ALONE) 49 | { 50 | starter.runServers(this.app); 51 | } 52 | utils.invokeCallback(cb); 53 | }); 54 | }); 55 | 56 | this.masterConsole.on('error', (err) => 57 | { 58 | if (err) 59 | { 60 | logger.error(`masterConsole encounters with error: ${err.stack}`); 61 | return; 62 | } 63 | }); 64 | 65 | this.masterConsole.on('reconnect', (info) => 66 | { 67 | this.app.addServers([info]); 68 | }); 69 | 70 | // monitor servers disconnect event 71 | this.masterConsole.on('disconnect', (id, type, info, reason) => 72 | { 73 | crashLogger.info(util.format('[%s],[%s],[%s],[%s]', type, id, Date.now(), reason || 'disconnect')); 74 | let count = 0; 75 | const time = 0; 76 | let pingTimer = null; 77 | const server = this.app.getServerById(id); 78 | const stopFlags = this.app.get(Constants.RESERVED.STOP_SERVERS) || []; 79 | if (!_.isNil(server) 80 | && (server[Constants.RESERVED.AUTO_RESTART] === 'true' || server[Constants.RESERVED.RESTART_FORCE] === 'true') 81 | && stopFlags.indexOf(id) < 0) 82 | { 83 | const setTimer = time => 84 | { 85 | pingTimer = setTimeout(() => 86 | { 87 | utils.ping(server.host, flag => 88 | { 89 | if (flag) 90 | { 91 | clearTimeout(pingTimer); 92 | utils.checkPort(server, status => 93 | { 94 | if (status === 'error') 95 | { 96 | utils.invokeCallback(cb, new Error('Check port command executed with error.')); 97 | return; 98 | } 99 | else if (status === 'busy') 100 | { 101 | if (server[Constants.RESERVED.RESTART_FORCE]) 102 | { 103 | starter.kill([info.pid], [server]); 104 | } 105 | else 106 | { 107 | utils.invokeCallback(cb, new Error('Port occupied already, check your server to add.')); 108 | return; 109 | } 110 | } 111 | setTimeout(() => 112 | { 113 | starter.run(this.app, server, null); 114 | }, Constants.TIME.TIME_WAIT_STOP); 115 | }); 116 | } 117 | else 118 | { 119 | count++; 120 | if (count > 3) 121 | { 122 | time = Constants.TIME.TIME_WAIT_MAX_PING; 123 | } 124 | else 125 | { 126 | time = Constants.TIME.TIME_WAIT_PING * count; 127 | } 128 | setTimer(time); 129 | } 130 | }); 131 | }, time); 132 | }; 133 | setTimer(time); 134 | } 135 | }); 136 | 137 | // monitor servers register event 138 | this.masterConsole.on('register', function(record) 139 | { 140 | starter.bindCpu(record.id, record.pid, record.host); 141 | }); 142 | 143 | this.masterConsole.on('admin-log', function(log, error) 144 | { 145 | if (error) 146 | { 147 | adminLogger.error(JSON.stringify(log)); 148 | } 149 | else 150 | { 151 | adminLogger.info(JSON.stringify(log)); 152 | } 153 | }); 154 | } 155 | 156 | stop(cb) 157 | { 158 | this.masterConsole.stop(); 159 | process.nextTick(cb); 160 | } 161 | } 162 | 163 | module.exports = Master; -------------------------------------------------------------------------------- /lib/master/watchdog.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | utils = require('../util/utils'), 3 | Constants = require('../util/constants'), 4 | CreateCountDownLatch = require('../util/countDownLatch'), 5 | EventEmitter = require('events').EventEmitter, 6 | logger = require('pomelo-logger').getLogger('pomelo', __filename); 7 | 8 | class Watchdog extends EventEmitter 9 | { 10 | constructor(app, service) 11 | { 12 | super(); 13 | this.app = app; 14 | this.service = service; 15 | this.isStarted = false; 16 | this.count = utils.size(app.getServersFromConfig()); 17 | 18 | this.servers = {}; 19 | this.listeners = {}; 20 | } 21 | 22 | addServer(server) 23 | { 24 | if (!server) 25 | { 26 | return; 27 | } 28 | this.servers[server.id] = server; 29 | this.notify({ 30 | action : 'addServer', 31 | server : server}); 32 | } 33 | 34 | removeServer(id) 35 | { 36 | if (!id) 37 | { 38 | return; 39 | } 40 | this.unsubscribe(id); 41 | delete this.servers[id]; 42 | this.notify({ 43 | action : 'removeServer', 44 | id : id}); 45 | } 46 | 47 | reconnectServer(server) 48 | { 49 | if (!server) 50 | { 51 | return; 52 | } 53 | if (!this.servers[server.id]) 54 | { 55 | this.servers[server.id] = server; 56 | } 57 | // replace server in reconnect server 58 | this.notifyById(server.id, { 59 | action : 'replaceServer', 60 | servers : this.servers}); 61 | // notify other server to add server 62 | this.notify({ 63 | action : 'addServer', 64 | server : server}); 65 | // add server in listener 66 | this.subscribe(server.id); 67 | } 68 | 69 | subscribe(id) 70 | { 71 | this.listeners[id] = 1; 72 | } 73 | 74 | unsubscribe(id) 75 | { 76 | delete this.listeners[id]; 77 | } 78 | 79 | query() 80 | { 81 | return this.servers; 82 | } 83 | 84 | record(id) 85 | { 86 | if (!this.isStarted && --this.count < 0) 87 | { 88 | const usedTime = Date.now() - this.app.startTime; 89 | logger.info('all servers startup in %s ms', usedTime); 90 | this.notify({action: 'startOver'}); 91 | this.isStarted = true; 92 | } 93 | } 94 | 95 | notifyById(id, msg) 96 | { 97 | this.service.agent.request(id, Constants.KEYWORDS.MONITOR_WATCHER, msg, function(signal) 98 | { 99 | if (signal !== Constants.SIGNAL.OK) 100 | { 101 | logger.error(`master watchdog fail to notify to monitor, id: ${id}, msg: ${msg}`); 102 | } 103 | else 104 | { 105 | logger.debug(`master watchdog notify to monitor success, id: ${id}, msg: ${msg}`); 106 | } 107 | }); 108 | } 109 | 110 | notify(msg) 111 | { 112 | const listeners = this.listeners; 113 | let success = true; 114 | const fails = []; 115 | const timeouts = []; 116 | const requests = {}; 117 | const count = utils.size(listeners); 118 | if (count === 0) 119 | { 120 | logger.warn('master watchdog listeners is none, msg: %j', msg); 121 | return; 122 | } 123 | const latch = CreateCountDownLatch.createCountDownLatch(count, {timeout: Constants.TIME.TIME_WAIT_COUNTDOWN}, isTimeout => 124 | { 125 | if (isTimeout) 126 | { 127 | _.forEach(requests, (request, key) => 128 | { 129 | if (!request) 130 | { 131 | timeouts.push(key); 132 | } 133 | }); 134 | logger.error('master watchdog request timeout message: %j, timeouts: %j, fails: %j', msg, timeouts, fails); 135 | } 136 | if (!success) 137 | { 138 | logger.error('master watchdog request fail message: %j, fails: %j', msg, fails); 139 | } 140 | }); 141 | 142 | _.forEach(listeners, (listener, id) => 143 | { 144 | requests[id] = 0; 145 | ((watchdog, id) => 146 | { 147 | watchdog.service.agent.request(id, Constants.KEYWORDS.MONITOR_WATCHER, msg, signal => 148 | { 149 | if (signal !== Constants.SIGNAL.OK) 150 | { 151 | fails.push(id); 152 | success = false; 153 | } 154 | requests[id] = 1; 155 | latch.done(); 156 | }); 157 | })(this, id); 158 | }); 159 | } 160 | } 161 | module.exports = function(app, service) 162 | { 163 | return new Watchdog(app, service); 164 | }; -------------------------------------------------------------------------------- /lib/modules/masterwatcher.js: -------------------------------------------------------------------------------- 1 | const utils = require('../util/utils'), 2 | Constants = require('../util/constants'), 3 | MasterWatchdog = require('../master/watchdog'), 4 | logger = require('pomelo-logger').getLogger('pomelo', __filename); 5 | 6 | class MasterWatcher 7 | { 8 | constructor(opts, consoleService) 9 | { 10 | this.app = opts.app; 11 | this.service = consoleService; 12 | this.id = this.app.getServerId(); 13 | 14 | this.watchdog = new MasterWatchdog(this.app, this.service); 15 | this.service.on('register', MasterWatcherUtility.onServerAdd.bind(null, this)); 16 | this.service.on('disconnect', MasterWatcherUtility.onServerLeave.bind(null, this)); 17 | this.service.on('reconnect', MasterWatcherUtility.onServerReconnect.bind(null, this)); 18 | } 19 | 20 | static get moduleId() 21 | { 22 | return Constants.KEYWORDS.MASTER_WATCHER; 23 | } 24 | 25 | // ----------------- module methods ------------------------- 26 | 27 | start(cb) 28 | { 29 | utils.invokeCallback(cb); 30 | } 31 | 32 | masterHandler(agent, msg, cb) 33 | { 34 | if (!msg) 35 | { 36 | logger.warn('masterwatcher receive empty message.'); 37 | return; 38 | } 39 | const func = masterMethods[msg.action]; 40 | if (!func) 41 | { 42 | logger.info('masterwatcher unknown action: %j', msg.action); 43 | return; 44 | } 45 | func(this, agent, msg, cb); 46 | } 47 | } 48 | 49 | class MasterWatcherUtility 50 | { 51 | // ----------------- bind methods ------------------------- 52 | 53 | static onServerAdd(module, record) 54 | { 55 | logger.debug(`masterwatcher receive add server event, with server: ${record}`); 56 | if (!record || record.type === 'client' || !record.serverType) 57 | { 58 | return; 59 | } 60 | module.watchdog.addServer(record); 61 | } 62 | 63 | static onServerReconnect(module, record) 64 | { 65 | logger.debug(`masterwatcher receive reconnect server event, with server: ${record}`); 66 | if (!record || record.type === 'client' || !record.serverType) 67 | { 68 | logger.warn('onServerReconnect receive wrong message: %j', record); 69 | return; 70 | } 71 | module.watchdog.reconnectServer(record); 72 | } 73 | 74 | static onServerLeave(module, id, type) 75 | { 76 | logger.debug('masterwatcher receive remove server event, with server: %s, type: %s', id, type); 77 | if (!id) 78 | { 79 | logger.warn('onServerLeave receive server id is empty.'); 80 | return; 81 | } 82 | if (type !== 'client') 83 | { 84 | module.watchdog.removeServer(id); 85 | } 86 | } 87 | 88 | // ----------------- monitor request methods ------------------------- 89 | 90 | static subscribe(module, agent, msg, cb) 91 | { 92 | if (!msg) 93 | { 94 | utils.invokeCallback(cb, new Error('masterwatcher subscribe empty message.')); 95 | return; 96 | } 97 | 98 | module.watchdog.subscribe(msg.id); 99 | utils.invokeCallback(cb, null, module.watchdog.query()); 100 | } 101 | 102 | static unsubscribe(module, agent, msg, cb) 103 | { 104 | if (!msg) 105 | { 106 | utils.invokeCallback(cb, new Error('masterwatcher unsubscribe empty message.')); 107 | return; 108 | } 109 | module.watchdog.unsubscribe(msg.id); 110 | utils.invokeCallback(cb); 111 | } 112 | 113 | static query(module, agent, msg, cb) 114 | { 115 | utils.invokeCallback(cb, null, module.watchdog.query()); 116 | } 117 | 118 | static record(module, agent, msg, cb) 119 | { 120 | if (!msg) 121 | { 122 | utils.invokeCallback(cb, new Error('masterwatcher record empty message.')); 123 | return; 124 | } 125 | module.watchdog.record(msg.id); 126 | } 127 | } 128 | 129 | const masterMethods = { 130 | 'subscribe' : MasterWatcherUtility.subscribe, 131 | 'unsubscribe' : MasterWatcherUtility.unsubscribe, 132 | 'query' : MasterWatcherUtility.query, 133 | 'record' : MasterWatcherUtility.record 134 | }; 135 | 136 | module.exports = function(opts, consoleService) 137 | { 138 | if (!(this instanceof MasterWatcher)) 139 | { 140 | return new MasterWatcher(opts, consoleService); 141 | } 142 | }; 143 | 144 | module.exports.moduleId = Constants.KEYWORDS.MASTER_WATCHER; -------------------------------------------------------------------------------- /lib/modules/monitorwatcher.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | utils = require('../util/utils'), 3 | events = require('../util/events'), 4 | Constants = require('../util/constants'), 5 | logger = require('pomelo-logger').getLogger('pomelo', __filename); 6 | 7 | class MonitorWatcher 8 | { 9 | constructor(opts, consoleService) 10 | { 11 | this.app = opts.app; 12 | this.service = consoleService; 13 | this.id = this.app.getServerId(); 14 | 15 | this.app.event.on(events.START_SERVER, MonitorWatcherUtility.finishStart.bind(null, this)); 16 | } 17 | 18 | static get moduleId() 19 | { 20 | return Constants.KEYWORDS.MONITOR_WATCHER; 21 | } 22 | 23 | start(cb) 24 | { 25 | MonitorWatcherUtility.subscribeRequest(this, this.service.agent, this.id, cb); 26 | } 27 | 28 | monitorHandler(agent, msg, cb) 29 | { 30 | if (!msg || !msg.action) 31 | { 32 | return; 33 | } 34 | const func = monitorMethods[msg.action]; 35 | if (!func) 36 | { 37 | logger.info('monitorwatcher unknown action: %j', msg.action); 38 | return; 39 | } 40 | func(this, agent, msg, cb); 41 | } 42 | } 43 | 44 | class MonitorWatcherUtility 45 | { 46 | // ----------------- monitor start method ------------------------- 47 | 48 | static subscribeRequest(monitorWatcher, agent, id, cb) 49 | { 50 | const msg = { 51 | action : 'subscribe', 52 | id : id}; 53 | agent.request(Constants.KEYWORDS.MASTER_WATCHER, msg, (err, servers) => 54 | { 55 | if (err) 56 | { 57 | logger.error(`subscribeRequest request to master with error: ${err.stack}`); 58 | utils.invokeCallback(cb, err); 59 | } 60 | const res = _.values(servers); 61 | MonitorWatcherUtility.addServers(monitorWatcher, res); 62 | utils.invokeCallback(cb); 63 | }); 64 | } 65 | 66 | // ----------------- monitor request methods ------------------------- 67 | 68 | static addServer(monitorWatcher, agent, msg, cb) 69 | { 70 | logger.debug('[%s] receive addServer signal: %j', monitorWatcher.app.serverId, msg); 71 | if (!msg || !msg.server) 72 | { 73 | logger.warn('monitorwatcher addServer receive empty message: %j', msg); 74 | utils.invokeCallback(cb, Constants.SIGNAL.FAIL); 75 | return; 76 | } 77 | MonitorWatcherUtility.addServers(monitorWatcher, [msg.server]); 78 | utils.invokeCallback(cb, Constants.SIGNAL.OK); 79 | } 80 | 81 | static removeServer(monitorWatcher, agent, msg, cb) 82 | { 83 | logger.debug('%s receive removeServer signal: %j', monitorWatcher.app.serverId, msg); 84 | if (!msg || !msg.id) 85 | { 86 | logger.warn('monitorwatcher removeServer receive empty message: %j', msg); 87 | utils.invokeCallback(cb, Constants.SIGNAL.FAIL); 88 | return; 89 | } 90 | MonitorWatcherUtility.removeServers(monitorWatcher, [msg.id]); 91 | utils.invokeCallback(cb, Constants.SIGNAL.OK); 92 | } 93 | 94 | static replaceServer(monitorWatcher, agent, msg, cb) 95 | { 96 | logger.debug('%s receive replaceServer signal: %j', monitorWatcher.app.serverId, msg); 97 | if (!msg || !msg.servers) 98 | { 99 | logger.warn('monitorwatcher replaceServer receive empty message: %j', msg); 100 | utils.invokeCallback(cb, Constants.SIGNAL.FAIL); 101 | return; 102 | } 103 | MonitorWatcherUtility.replaceServers(monitorWatcher, msg.servers); 104 | utils.invokeCallback(cb, Constants.SIGNAL.OK); 105 | } 106 | 107 | static startOver(monitorWatcher, agent, msg, cb) 108 | { 109 | const fun = monitorWatcher.app.lifecycleCbs[Constants.LIFECYCLE.AFTER_STARTALL]; 110 | if (fun) 111 | { 112 | utils.invokeCallback(fun, monitorWatcher.app); 113 | } 114 | monitorWatcher.app.event.emit(events.START_ALL); 115 | utils.invokeCallback(cb, Constants.SIGNAL.OK); 116 | } 117 | 118 | // ----------------- common methods ------------------------- 119 | 120 | static addServers(monitorWatcher, servers) 121 | { 122 | if (!servers || !servers.length) 123 | { 124 | return; 125 | } 126 | monitorWatcher.app.addServers(servers); 127 | } 128 | 129 | static removeServers(monitorWatcher, ids) 130 | { 131 | if (!ids || !ids.length) 132 | { 133 | return; 134 | } 135 | monitorWatcher.app.removeServers(ids); 136 | } 137 | 138 | static replaceServers(monitorWatcher, servers) 139 | { 140 | monitorWatcher.app.replaceServers(servers); 141 | } 142 | 143 | // ----------------- bind methods ------------------------- 144 | 145 | static finishStart(monitorWatcher, id) 146 | { 147 | const msg = { 148 | action : 'record', 149 | id : id}; 150 | monitorWatcher.service.agent.notify(Constants.KEYWORDS.MASTER_WATCHER, msg); 151 | } 152 | } 153 | 154 | const monitorMethods = { 155 | 'addServer' : MonitorWatcherUtility.addServer, 156 | 'removeServer' : MonitorWatcherUtility.removeServer, 157 | 'replaceServer' : MonitorWatcherUtility.replaceServer, 158 | 'startOver' : MonitorWatcherUtility.startOver 159 | }; 160 | 161 | module.exports = function(opts, consoleService) 162 | { 163 | return new MonitorWatcher(opts, consoleService); 164 | }; 165 | module.exports.moduleId = Constants.KEYWORDS.MONITOR_WATCHER; -------------------------------------------------------------------------------- /lib/monitor/monitor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Component for monitor. 3 | * Load and start monitor client. 4 | */ 5 | const logger = require('pomelo-logger').getLogger('pomelo', __filename), 6 | admin = require('pomelo-admin-upgrade'), 7 | moduleUtil = require('../util/moduleUtil'), 8 | utils = require('../util/utils'), 9 | Constants = require('../util/constants'); 10 | 11 | class Monitor 12 | { 13 | constructor(app, opts) 14 | { 15 | opts = opts || {}; 16 | this.app = app; 17 | this.serverInfo = app.getCurServer(); 18 | this.masterInfo = app.getMaster(); 19 | this.modules = []; 20 | this.closeWatcher = opts.closeWatcher; 21 | 22 | this.monitorConsole = admin.createMonitorConsole({ 23 | id : this.serverInfo.id, 24 | type : this.app.getServerType(), 25 | host : this.masterInfo.host, 26 | port : this.masterInfo.port, 27 | info : this.serverInfo, 28 | env : this.app.get(Constants.RESERVED.ENV), 29 | authServer : app.get('adminAuthServerMonitor') // auth server function 30 | }); 31 | } 32 | 33 | start(cb) 34 | { 35 | moduleUtil.registerDefaultModules(false, this.app, this.closeWatcher); 36 | this.startConsole(cb); 37 | } 38 | 39 | startConsole(cb) 40 | { 41 | moduleUtil.loadModules(this, this.monitorConsole); 42 | this.monitorConsole.start(err => 43 | { 44 | if (err) 45 | { 46 | utils.invokeCallback(cb, err); 47 | return; 48 | } 49 | moduleUtil.startModules(this.modules, function(err) 50 | { 51 | utils.invokeCallback(cb, err); 52 | return; 53 | }); 54 | }); 55 | 56 | this.monitorConsole.on('error', function(err) 57 | { 58 | if (err) 59 | { 60 | logger.error('monitorConsole encounters with error: %j', err.stack); 61 | return; 62 | } 63 | }); 64 | } 65 | 66 | stop(cb) 67 | { 68 | this.monitorConsole.stop(); 69 | this.modules = []; 70 | process.nextTick(function() 71 | { 72 | utils.invokeCallback(cb); 73 | }); 74 | } 75 | 76 | // monitor reconnect to master 77 | reconnect(masterInfo) 78 | { 79 | this.stop(() => 80 | { 81 | this.monitorConsole = admin.createMonitorConsole({ 82 | id : this.serverInfo.id, 83 | type : this.app.getServerType(), 84 | host : masterInfo.host, 85 | port : masterInfo.port, 86 | info : this.serverInfo, 87 | env : this.app.get(Constants.RESERVED.ENV) 88 | }); 89 | this.startConsole(() => 90 | { 91 | logger.info('restart modules for server : %j finish.', this.app.serverId); 92 | }); 93 | }); 94 | } 95 | } 96 | 97 | module.exports = function(app, opts) 98 | { 99 | if (!(this instanceof Monitor)) 100 | { 101 | return new Monitor(app, opts); 102 | } 103 | }; -------------------------------------------------------------------------------- /lib/pomelo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pomelo 3 | * MIT Licensed 4 | */ 5 | 6 | /** 7 | * Module dependencies. 8 | */ 9 | const _ = require('lodash'), 10 | fs = require('fs'), 11 | path = require('path'), 12 | dirName = __dirname, 13 | Package = require('../package.json'), 14 | application = require('./application'); 15 | 16 | const defineGetter = (obj, prop, get) => 17 | { 18 | if (Object.defineProperty) 19 | return Object.defineProperty(obj, prop, accessorDescriptor('get', get)); 20 | if (Object.prototype.__defineGetter__) 21 | return obj.__defineGetter__(prop, get); 22 | 23 | throw new Error('browser does not support getters'); 24 | }; 25 | 26 | const accessorDescriptor = (field, fun) => 27 | { 28 | const desc = { 29 | enumerable : true, 30 | configurable : true}; 31 | desc[field] = fun; 32 | return desc; 33 | }; 34 | 35 | const readDirByFS = (pathUrl, objectCollection, pomeloCollection = null) => 36 | { 37 | const readFiles = fs.readdirSync(`${dirName}${pathUrl}`); 38 | _.forEach(readFiles, filename => 39 | { 40 | if (!/\.js$/.test(filename)) return; 41 | const jsName = path.basename(filename, '.js'); 42 | const loadResult = load.bind(null, `.${pathUrl}/`, jsName); 43 | defineGetter(objectCollection, jsName, loadResult); 44 | if (pomeloCollection) 45 | { 46 | defineGetter(pomeloCollection, jsName, loadResult); 47 | } 48 | }); 49 | }; 50 | 51 | const load = (path, name) => 52 | { 53 | if (_.isNil(path)) 54 | { 55 | return require(name); 56 | } 57 | if (_.isNil(name)) 58 | { 59 | return require(path); 60 | } 61 | return require(`${path}${name}`); 62 | }; 63 | 64 | const pomelo = { 65 | version : Package.version, 66 | events : require('./util/events'), 67 | components : {}, // auto loaded components 68 | filters : {}, // auto loaded filters 69 | rpcFilters : {}, // auto loaded rpc filters 70 | connectors : {}, 71 | pushSchedulers : {} 72 | }; 73 | 74 | defineGetter(pomelo.connectors, 'sioconnector', load.bind(null, './connectors/sioConnector')); 75 | defineGetter(pomelo.connectors, 'hybridconnector', load.bind(null, './connectors/hybridConnector')); 76 | defineGetter(pomelo.connectors, 'udpconnector', load.bind(null, './connectors/udpConnector')); 77 | defineGetter(pomelo.connectors, 'mqttconnector', load.bind(null, './connectors/mqttConnector')); 78 | 79 | defineGetter(pomelo.pushSchedulers, 'direct', load.bind(null, './pushSchedulers/direct')); 80 | defineGetter(pomelo.pushSchedulers, 'buffer', load.bind(null, './pushSchedulers/buffer')); 81 | 82 | let app = null; 83 | pomelo.createApp = (opts) => 84 | { 85 | app = application; 86 | app.init(opts); 87 | pomelo.app = app; 88 | return app; 89 | }; 90 | 91 | defineGetter(pomelo, 'app', function() {return app;}); 92 | 93 | readDirByFS('/components', pomelo.components, pomelo); 94 | readDirByFS('/filters/handler', pomelo.filters, pomelo); 95 | readDirByFS('/filters/rpc', pomelo.rpcFilters); 96 | 97 | module.exports = pomelo; -------------------------------------------------------------------------------- /lib/pushSchedulers/buffer.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | utils = require('../util/utils'); 3 | 4 | const DEFAULT_FLUSH_INTERVAL = 20; 5 | class Buffer 6 | { 7 | constructor(app, opts) 8 | { 9 | opts = opts || {}; 10 | this.app = app; 11 | this.flushInterval = opts.flushInterval || DEFAULT_FLUSH_INTERVAL; 12 | this.sessions = {}; // sid -> msg queue 13 | this.tid = null; 14 | } 15 | 16 | start(callBack) 17 | { 18 | this.tid = setInterval(() => {BufferUtility.Flush(this);}, this.flushInterval); 19 | process.nextTick(() => 20 | { 21 | utils.invokeCallback(callBack); 22 | }); 23 | } 24 | 25 | stop(force, callBack) 26 | { 27 | if (this.tid) 28 | { 29 | clearInterval(this.tid); 30 | this.tid = null; 31 | } 32 | process.nextTick(() => 33 | { 34 | utils.invokeCallback(callBack); 35 | }); 36 | } 37 | 38 | schedule(reqId, route, msg, recvs, opts, callBack) 39 | { 40 | opts = opts || {}; 41 | if (opts.type === 'broadcast') 42 | { 43 | BufferUtility.DoBroadcast(this, msg, opts.userOptions); 44 | } 45 | else 46 | { 47 | BufferUtility.DoBatchPush(this, msg, recvs); 48 | } 49 | 50 | process.nextTick(() => 51 | { 52 | utils.invokeCallback(callBack); 53 | }); 54 | } 55 | } 56 | 57 | class BufferUtility 58 | { 59 | static Flush(buffer) 60 | { 61 | const sessionService = buffer.app.get('sessionService'); 62 | let queue, session; 63 | _.forEach(buffer.session, (value, sid) => 64 | { 65 | session = sessionService.get(sid); 66 | if (!session) 67 | { 68 | return; 69 | } 70 | queue = buffer.sessions[sid]; 71 | if (_.size(queue) > 0) 72 | { 73 | session.sendBatch(queue); 74 | buffer.sessions[sid] = []; 75 | } 76 | }); 77 | } 78 | 79 | static OnClose(buffer, session) 80 | { 81 | delete buffer.sessions[session.id]; 82 | } 83 | 84 | static DoBroadcast(buffer, msg, opts) 85 | { 86 | const channelService = buffer.app.get('channelService'); 87 | const sessionService = buffer.app.get('sessionService'); 88 | 89 | if (opts.binded) 90 | { 91 | sessionService.forEachBindedSession(session => 92 | { 93 | if (channelService.broadcastFilter && 94 | !channelService.broadcastFilter(session, msg, opts.filterParam)) 95 | { 96 | return; 97 | } 98 | 99 | BufferUtility.Enqueue(buffer, session, msg); 100 | }); 101 | } 102 | else 103 | { 104 | sessionService.forEachSession(session => 105 | { 106 | if (channelService.broadcastFilter && 107 | !channelService.broadcastFilter(session, msg, opts.filterParam)) 108 | { 109 | return; 110 | } 111 | 112 | BufferUtility.Enqueue(buffer, session, msg); 113 | }); 114 | } 115 | } 116 | 117 | static DoBatchPush(buffer, msg, recvs) 118 | { 119 | const sessionService = buffer.app.get('sessionService'); 120 | let session; 121 | _.forEach(recvs, recv => 122 | { 123 | session = sessionService.get(recv); 124 | if (session) 125 | { 126 | BufferUtility.Enqueue(buffer, session, msg); 127 | } 128 | }); 129 | } 130 | 131 | static Enqueue(buffer, session, msg) 132 | { 133 | let queue = buffer.sessions[session.id]; 134 | if (!queue) 135 | { 136 | queue = buffer.sessions[session.id] = []; 137 | session.once('closed', BufferUtility.OnClose.bind(null, buffer)); 138 | } 139 | queue.push(msg); 140 | } 141 | } 142 | 143 | module.exports = function(app, opts) 144 | { 145 | if (!(this instanceof Buffer)) 146 | { 147 | return new Buffer(app, opts); 148 | } 149 | }; -------------------------------------------------------------------------------- /lib/pushSchedulers/direct.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by frank on 16-12-26. 3 | */ 4 | const _ = require('lodash'), 5 | utils = require('../util/utils'); 6 | 7 | class Direct 8 | { 9 | constructor(app, opts) 10 | { 11 | opts = opts || {}; 12 | this.app = app; 13 | } 14 | 15 | schedule(reqId, route, msg, recvs, opts, callBack) 16 | { 17 | opts = opts || {}; 18 | if (opts.type === 'broadcast') 19 | { 20 | DirectUtility.DoBroadcast(this, msg, opts.userOptions); 21 | } 22 | else 23 | { 24 | DirectUtility.DoBatchPush(this, msg, recvs); 25 | } 26 | 27 | if (callBack) 28 | { 29 | process.nextTick(() => 30 | { 31 | utils.invokeCallback(callBack); 32 | }); 33 | } 34 | } 35 | } 36 | 37 | class DirectUtility 38 | { 39 | static DoBroadcast(buffer, msg, opts) 40 | { 41 | const channelService = buffer.app.get('channelService'); 42 | const sessionService = buffer.app.get('sessionService'); 43 | 44 | if (opts.binded) 45 | { 46 | sessionService.forEachBindedSession(session => 47 | { 48 | if (channelService.broadcastFilter && 49 | !channelService.broadcastFilter(session, msg, opts.filterParam)) 50 | { 51 | return; 52 | } 53 | sessionService.sendMessageByUid(session.uid, msg); 54 | }); 55 | } 56 | else 57 | { 58 | sessionService.forEachSession(session => 59 | { 60 | if (channelService.broadcastFilter && 61 | !channelService.broadcastFilter(session, msg, opts.filterParam)) 62 | { 63 | return; 64 | } 65 | sessionService.sendMessageByUid(session.uid, msg); 66 | }); 67 | } 68 | } 69 | 70 | static DoBatchPush(buffer, msg, recvs) 71 | { 72 | const sessionService = buffer.app.get('sessionService'); 73 | _.forEach(recvs, recv => 74 | { 75 | sessionService.sendMessage(recv, msg); 76 | }); 77 | } 78 | } 79 | 80 | module.exports = function(app, opts) 81 | { 82 | if (!(this instanceof Direct)) 83 | { 84 | return new Direct(app, opts); 85 | } 86 | }; -------------------------------------------------------------------------------- /lib/util/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | KEYWORDS : { 3 | BEFORE_FILTER : '__befores__', 4 | AFTER_FILTER : '__afters__', 5 | GLOBAL_BEFORE_FILTER : '__globalBefores__', 6 | GLOBAL_AFTER_FILTER : '__globalAfters__', 7 | ROUTE : '__routes__', 8 | BEFORE_STOP_HOOK : '__beforeStopHook__', 9 | MODULE : '__modules__', 10 | SERVER_MAP : '__serverMap__', 11 | RPC_BEFORE_FILTER : '__rpcBefores__', 12 | RPC_AFTER_FILTER : '__rpcAfters__', 13 | MASTER_WATCHER : '__masterwatcher__', 14 | MONITOR_WATCHER : '__monitorwatcher__' 15 | }, 16 | 17 | FILEPATH : { 18 | MASTER : '/config/master.json', 19 | SERVER : '/config/servers.json', 20 | CRON : '/config/crons.json', 21 | LOG : '/config/log4js.json', 22 | SERVER_PROTOS : '/config/serverProtos.json', 23 | CLIENT_PROTOS : '/config/clientProtos.json', 24 | MASTER_HA : '/config/masterha.json', 25 | LIFECYCLE : '/lifecycle.js', 26 | SERVER_DIR : '/app/servers/', 27 | CONFIG_DIR : '/config' 28 | }, 29 | 30 | DIR : { 31 | HANDLER : 'handler', 32 | REMOTE : 'remote', 33 | CRON : 'cron', 34 | LOG : 'logs', 35 | SCRIPT : 'scripts', 36 | EVENT : 'events', 37 | COMPONENT : 'components' 38 | }, 39 | 40 | RESERVED : { 41 | BASE : 'base', 42 | MAIN : 'main', 43 | MASTER : 'master', 44 | SERVERS : 'servers', 45 | ENV : 'env', 46 | CPU : 'cpu', 47 | ENV_DEV : 'development', 48 | ENV_PRO : 'production', 49 | ALL : 'all', 50 | SERVER_TYPE : 'serverType', 51 | SERVER_ID : 'serverId', 52 | CURRENT_SERVER : 'curServer', 53 | MODE : 'mode', 54 | TYPE : 'type', 55 | CLUSTER : 'clusters', 56 | STAND_ALONE : 'stand-alone', 57 | START : 'start', 58 | AFTER_START : 'afterStart', 59 | CRONS : 'crons', 60 | ERROR_HANDLER : 'errorHandler', 61 | GLOBAL_ERROR_HANDLER : 'globalErrorHandler', 62 | AUTO_RESTART : 'auto-restart', 63 | RESTART_FORCE : 'restart-force', 64 | CLUSTER_COUNT : 'clusterCount', 65 | CLUSTER_PREFIX : 'cluster-server-', 66 | CLUSTER_SIGNAL : '++', 67 | RPC_ERROR_HANDLER : 'rpcErrorHandler', 68 | SERVER : 'server', 69 | CLIENT : 'client', 70 | STARTID : 'startId', 71 | STOP_SERVERS : 'stop_servers', 72 | SSH_CONFIG_PARAMS : 'ssh_config_params' 73 | }, 74 | 75 | COMMAND : { 76 | TASKSET : 'taskset', 77 | KILL : 'kill', 78 | TASKKILL : 'taskkill', 79 | SSH : 'ssh' 80 | }, 81 | 82 | PLATFORM : { 83 | WIN : 'win32', 84 | LINUX : 'linux' 85 | }, 86 | 87 | LIFECYCLE : { 88 | BEFORE_STARTUP : 'beforeStartup', 89 | BEFORE_SHUTDOWN : 'beforeShutdown', 90 | AFTER_STARTUP : 'afterStartup', 91 | AFTER_STARTALL : 'afterStartAll' 92 | }, 93 | 94 | SIGNAL : { 95 | FAIL : 0, 96 | OK : 1 97 | }, 98 | 99 | TIME : { 100 | TIME_WAIT_STOP : 3 * 1000, 101 | TIME_WAIT_KILL : 5 * 1000, 102 | TIME_WAIT_RESTART : 5 * 1000, 103 | TIME_WAIT_COUNTDOWN : 10 * 1000, 104 | TIME_WAIT_MASTER_KILL : 2 * 60 * 1000, 105 | TIME_WAIT_MONITOR_KILL : 2 * 1000, 106 | TIME_WAIT_PING : 30 * 1000, 107 | TIME_WAIT_MAX_PING : 5 * 60 * 1000, 108 | DEFAULT_UDP_HEARTBEAT_TIME : 20 * 1000, 109 | DEFAULT_UDP_HEARTBEAT_TIMEOUT : 100 * 1000, 110 | DEFAULT_MQTT_HEARTBEAT_TIMEOUT : 90 * 1000 111 | } 112 | }; -------------------------------------------------------------------------------- /lib/util/countDownLatch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by frank on 16-12-27. 3 | */ 4 | const _ = require('lodash'); 5 | 6 | class CountDownLatch 7 | { 8 | constructor(count, opts, callBack) 9 | { 10 | this.count = count; 11 | this.cb = callBack; 12 | if (opts.timeout) 13 | { 14 | this.timerId = setTimeout(() => 15 | { 16 | this.cb(true); 17 | }, opts.timeout); 18 | } 19 | } 20 | 21 | done() 22 | { 23 | if (this.count <= 0) 24 | { 25 | throw new Error('illegal state.'); 26 | } 27 | 28 | this.count--; 29 | if (this.count === 0) 30 | { 31 | if (this.timerId) 32 | { 33 | clearTimeout(this.timerId); 34 | } 35 | this.cb(); 36 | } 37 | } 38 | 39 | static CreateCountDownLatch(count, opts, callBack) 40 | { 41 | if (!count || count <= 0) 42 | { 43 | throw new Error('count should be positive.'); 44 | } 45 | 46 | if (!callBack && _.isFunction(opts)) 47 | { 48 | callBack = opts; 49 | opts = {}; 50 | } 51 | 52 | if (!_.isFunction(callBack)) 53 | { 54 | throw new Error('cb should be a function.'); 55 | } 56 | 57 | return new CountDownLatch(count, opts, callBack); 58 | } 59 | } 60 | 61 | module.exports = 62 | { 63 | createCountDownLatch : CountDownLatch.CreateCountDownLatch 64 | }; -------------------------------------------------------------------------------- /lib/util/events.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ADD_SERVERS : 'add_servers', 3 | REMOVE_SERVERS : 'remove_servers', 4 | REPLACE_SERVERS : 'replace_servers', 5 | BIND_SESSION : 'bind_session', 6 | UNBIND_SESSION : 'unbind_session', 7 | CLOSE_SESSION : 'close_session', 8 | ADD_CRONS : 'add_crons', 9 | REMOVE_CRONS : 'remove_crons', 10 | START_SERVER : 'start_server', 11 | START_ALL : 'start_all' 12 | }; 13 | -------------------------------------------------------------------------------- /lib/util/log.js: -------------------------------------------------------------------------------- 1 | const logger = require('pomelo-logger'); 2 | 3 | class Log 4 | { 5 | static Configure(app, filename) 6 | { 7 | const serverId = app.getServerId(); 8 | const base = app.getBase(); 9 | logger.configure(filename, { 10 | serverId : serverId, 11 | base : base}); 12 | } 13 | } 14 | 15 | module.exports.configure = Log.Configure; -------------------------------------------------------------------------------- /lib/util/moduleUtil.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | os = require('os'), 3 | admin = require('pomelo-admin-upgrade'), 4 | Constants = require('./constants'), 5 | pathUtil = require('./pathUtil'), 6 | starter = require('../master/starter'), 7 | isFunction = _.isFunction, 8 | logger = require('pomelo-logger').getLogger('pomelo', __filename); 9 | 10 | class ModuleUtil 11 | { 12 | 13 | /** 14 | * Load admin modules 15 | */ 16 | static loadModules(parent, consoleService) 17 | { 18 | // load app register modules 19 | const _modules = parent.app.get(Constants.KEYWORDS.MODULE); 20 | 21 | if (!_modules) 22 | { 23 | return; 24 | } 25 | 26 | const modules = _.values(_modules); 27 | 28 | let record, moduleId, module; 29 | for (let i = 0, l = modules.length; i < l; i++) 30 | { 31 | record = modules[i]; 32 | if (isFunction(record.module)) 33 | { 34 | try 35 | { 36 | module = record.module(record.opts, consoleService); 37 | } 38 | catch (err) 39 | { 40 | module = new record.module(record.opts, consoleService); 41 | } 42 | } 43 | else 44 | { 45 | module = record.module; 46 | } 47 | 48 | moduleId = record.moduleId || module.moduleId; 49 | 50 | if (!moduleId) 51 | { 52 | logger.warn('ignore an unknown module.'); 53 | continue; 54 | } 55 | 56 | consoleService.register(moduleId, module); 57 | parent.modules.push(module); 58 | } 59 | } 60 | 61 | static startModules(modules, cb) 62 | { 63 | // invoke the start lifecycle method of modules 64 | 65 | if (!modules) 66 | { 67 | return; 68 | } 69 | ModuleUtilUtility.startModule(null, modules, 0, cb); 70 | } 71 | 72 | /** 73 | * Append the default system admin modules 74 | */ 75 | static registerDefaultModules(isMaster, app, closeWatcher) 76 | { 77 | if (!closeWatcher) 78 | { 79 | if (isMaster) 80 | { 81 | app.registerAdmin(require('../modules/masterwatcher'), {app: app}); 82 | } 83 | else 84 | { 85 | app.registerAdmin(require('../modules/monitorwatcher'), {app: app}); 86 | } 87 | } 88 | app.registerAdmin(admin.modules.watchServer, {app: app}); 89 | app.registerAdmin(require('../modules/console'), { 90 | app : app, 91 | starter : starter}); 92 | if (app.enabled('systemMonitor')) 93 | { 94 | if (os.platform() !== Constants.PLATFORM.WIN) 95 | { 96 | app.registerAdmin(admin.modules.systemInfo); 97 | app.registerAdmin(admin.modules.nodeInfo); 98 | } 99 | app.registerAdmin(admin.modules.monitorLog, {path: pathUtil.getLogPath(app.getBase())}); 100 | app.registerAdmin(admin.modules.scripts, { 101 | app : app, 102 | path : pathUtil.getScriptPath(app.getBase())}); 103 | if (os.platform() !== Constants.PLATFORM.WIN) 104 | { 105 | app.registerAdmin(admin.modules.profiler); 106 | } 107 | } 108 | } 109 | } 110 | 111 | class ModuleUtilUtility 112 | { 113 | static startModule(err, modules, index, cb) 114 | { 115 | if (err || index >= modules.length) 116 | { 117 | // utils.invokeCallback(cb, err); 118 | cb(err); 119 | return; 120 | } 121 | 122 | const module = modules[index]; 123 | if (module && isFunction(module.start)) 124 | { 125 | module.start(function(err) 126 | { 127 | ModuleUtilUtility.startModule(err, modules, index + 1, cb); 128 | }); 129 | } 130 | else 131 | { 132 | ModuleUtilUtility.startModule(err, modules, index + 1, cb); 133 | } 134 | } 135 | } 136 | 137 | module.exports = ModuleUtil; -------------------------------------------------------------------------------- /lib/util/pathUtil.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'), 2 | path = require('path'), 3 | Constants = require('./constants'); 4 | 5 | class PathUtil 6 | { 7 | /** 8 | * Get system remote service path 9 | * 10 | * @param {String} role server role: frontend, backend 11 | * @return {String} path string if the path exist else null 12 | */ 13 | static getSysRemotePath(role) 14 | { 15 | const p = path.join(__dirname, '/../common/remote/', role); 16 | return fs.existsSync(p) ? p : null; 17 | } 18 | 19 | /** 20 | * Get user remote service path 21 | * 22 | * @param {String} appBase application base path 23 | * @param {String} serverType server type 24 | * @return {String} path string if the path exist else null 25 | */ 26 | static getUserRemotePath(appBase, serverType) 27 | { 28 | const p = path.join(appBase, '/app/servers/', serverType, Constants.DIR.REMOTE); 29 | return fs.existsSync(p) ? p : null; 30 | } 31 | 32 | /** 33 | * Get user remote cron path 34 | * 35 | * @param {String} appBase application base path 36 | * @param {String} serverType server type 37 | * @return {String} path string if the path exist else null 38 | */ 39 | static getCronPath(appBase, serverType) 40 | { 41 | const p = path.join(appBase, '/app/servers/', serverType, Constants.DIR.CRON); 42 | return fs.existsSync(p) ? p : null; 43 | } 44 | 45 | /** 46 | * List all the subdirectory names of user remote directory 47 | * which hold the codes for all the server types. 48 | * 49 | * @param {String} appBase application base path 50 | * @return {Array} all the subdiretory name under servers/ 51 | */ 52 | static listUserRemoteDir(appBase) 53 | { 54 | const base = path.join(appBase, '/app/servers/'); 55 | const files = fs.readdirSync(base); 56 | return files.filter(function(fn) 57 | { 58 | if (fn.charAt(0) === '.') 59 | { 60 | return false; 61 | } 62 | 63 | return fs.statSync(path.join(base, fn)).isDirectory(); 64 | }); 65 | } 66 | 67 | /** 68 | * Get handler path 69 | * 70 | * @param {String} appBase application base path 71 | * @param {String} serverType server type 72 | * @return {String} path string if the path exist else null 73 | */ 74 | static getHandlerPath(appBase, serverType) 75 | { 76 | const p = path.join(appBase, '/app/servers/', serverType, Constants.DIR.HANDLER); 77 | return fs.existsSync(p) ? p : null; 78 | } 79 | 80 | /** 81 | * Compose remote path record 82 | * 83 | * @param {String} namespace remote path namespace, such as: 'sys', 'user' 84 | * @param {String} serverType 85 | * @param {String} path remote service source path 86 | * @return {Object} remote path record 87 | */ 88 | static remotePathRecord(namespace, serverType, path) 89 | { 90 | return { 91 | namespace : namespace, 92 | serverType : serverType, 93 | path : path}; 94 | } 95 | 96 | /** 97 | * Get admin script root path. 98 | * 99 | * @param {String} appBase application base path 100 | * @return {String} script path string 101 | */ 102 | static getScriptPath(appBase) 103 | { 104 | return path.join(appBase, Constants.DIR.SCRIPT); 105 | } 106 | 107 | /** 108 | * Get logs path. 109 | * 110 | * @param {String} appBase application base path 111 | * @return {String} logs path string 112 | */ 113 | static getLogPath(appBase) 114 | { 115 | return path.join(appBase, Constants.DIR.LOG); 116 | } 117 | } 118 | 119 | module.exports = PathUtil; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pomelo-upgrade", 3 | "version": "1.4.4", 4 | "description": "网易Pomelo升级版本", 5 | "private": false, 6 | "homepage": "https://github.com/frank198/pomelo-upgrade#readme", 7 | 8 | "scripts": { 9 | "test": "grunt" 10 | }, 11 | 12 | "bugs": { 13 | "url": "https://github.com/frank198/pomelo-upgrade/issues" 14 | }, 15 | 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/frank198/pomelo-upgrade.git" 19 | }, 20 | 21 | "licenses": [ 22 | { 23 | "type": "MIT", 24 | "url": "https://github.com/frank198/pomelo-upgrade#license" 25 | } 26 | ], 27 | 28 | "keywords": [ 29 | "pomelo", 30 | "framework", 31 | "game", 32 | "web", 33 | "server", 34 | "pomelo-upgrade" 35 | ], 36 | 37 | "bin": { 38 | "pomelo": "./bin/pomelo" 39 | }, 40 | 41 | "dependencies": { 42 | "async": ">=2.1.4", 43 | "seq-queue": ">=0.0.5", 44 | "crc": ">=0.2.0", 45 | "cliff": ">=0.1.10", 46 | "mkdirp": ">=0.5.1", 47 | "pomelo-loader-upgrade": "*", 48 | "pomelo-rpc-upgrade": ">1.0.10", 49 | "pomelo-protocol": "0.1.6", 50 | "pomelo-admin-upgrade": ">0.0.12", 51 | "pomelo-logger": "0.1.7", 52 | "pomelo-scheduler": "0.3.9", 53 | "ws": ">=1.1.1", 54 | "socket.io": ">=1.7.1", 55 | "pomelo-protobuf": "0.4.0", 56 | "node-bignumber": ">=1.2.1", 57 | "commander": ">=2.0.0", 58 | "mqtt": ">=0.3.9", 59 | "lodash":"*" 60 | }, 61 | 62 | "devDependencies": { 63 | "blanket": "~1.1.6", 64 | "grunt": "*", 65 | "grunt-contrib-clean": "*", 66 | "grunt-eslint": "*", 67 | "grunt-mocha-test": "*", 68 | "load-grunt-tasks": "^3.5.2", 69 | "mocha": "*", 70 | "muk": ">=0.0.1", 71 | "should": "*", 72 | "socket.io-client": ">=1.7.1" 73 | } 74 | 75 | 76 | } 77 | -------------------------------------------------------------------------------- /template/game-server/app.js: -------------------------------------------------------------------------------- 1 | var pomelo = require('pomelo'); 2 | 3 | /** 4 | * Init app for client. 5 | */ 6 | var app = pomelo.createApp(); 7 | app.set('name', '$'); 8 | 9 | // app configuration 10 | app.configure('production|development', 'connector', function(){ 11 | app.set('connectorConfig', 12 | { 13 | connector : pomelo.connectors.hybridconnector, 14 | heartbeat : 3, 15 | useDict : true, 16 | useProtobuf : true 17 | }); 18 | }); 19 | 20 | // start app 21 | app.start(); 22 | 23 | process.on('uncaughtException', function (err) { 24 | console.error(' Caught exception: ' + err.stack); 25 | }); 26 | -------------------------------------------------------------------------------- /template/game-server/app.js.mqtt: -------------------------------------------------------------------------------- 1 | var pomelo = require('pomelo'); 2 | 3 | /** 4 | * Init app for client. 5 | */ 6 | var app = pomelo.createApp(); 7 | app.set('name', '$'); 8 | 9 | // app configuration 10 | app.configure('production|development', 'connector', function(){ 11 | app.set('connectorConfig', 12 | { 13 | connector : pomelo.connectors.mqttconnector, 14 | publishRoute: 'connector.entryHandler.publish', 15 | subscribeRoute: 'connector.entryHandler.subscribe' 16 | }); 17 | }); 18 | 19 | // start app 20 | app.start(); 21 | 22 | process.on('uncaughtException', function (err) { 23 | console.error(' Caught exception: ' + err.stack); 24 | }); -------------------------------------------------------------------------------- /template/game-server/app.js.sio: -------------------------------------------------------------------------------- 1 | var pomelo = require('pomelo'); 2 | 3 | /** 4 | * Init app for client. 5 | */ 6 | var app = pomelo.createApp(); 7 | app.set('name', '$'); 8 | 9 | // app configuration 10 | app.configure('production|development', 'connector', function(){ 11 | app.set('connectorConfig', 12 | { 13 | connector : pomelo.connectors.sioconnector, 14 | //websocket, htmlfile, xhr-polling, jsonp-polling, flashsocket 15 | transports : ['websocket'], 16 | heartbeats : true, 17 | closeTimeout : 60, 18 | heartbeatTimeout : 60, 19 | heartbeatInterval : 25 20 | }); 21 | }); 22 | 23 | // start app 24 | app.start(); 25 | 26 | process.on('uncaughtException', function (err) { 27 | console.error(' Caught exception: ' + err.stack); 28 | }); 29 | -------------------------------------------------------------------------------- /template/game-server/app.js.sio.wss: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var pomelo = require('pomelo'); 3 | 4 | /** 5 | * Init app for client. 6 | */ 7 | var app = pomelo.createApp(); 8 | app.set('name', '$'); 9 | 10 | // app configuration 11 | app.configure('production|development', function(){ 12 | app.set('connectorConfig', 13 | { 14 | connector : pomelo.connectors.sioconnector, 15 | key: fs.readFileSync('../shared/server.key'), 16 | cert: fs.readFileSync('../shared/server.crt') 17 | }); 18 | }); 19 | 20 | // start app 21 | app.start(); 22 | 23 | process.on('uncaughtException', function (err) { 24 | console.error(' Caught exception: ' + err.stack); 25 | }); -------------------------------------------------------------------------------- /template/game-server/app.js.udp: -------------------------------------------------------------------------------- 1 | var pomelo = require('pomelo'); 2 | 3 | /** 4 | * Init app for client. 5 | */ 6 | var app = pomelo.createApp(); 7 | app.set('name', '$'); 8 | 9 | // app configuration 10 | app.configure('production|development', function(){ 11 | app.set('connectorConfig', 12 | { 13 | connector : pomelo.connectors.udpconnector, 14 | heartbeat : 3 15 | }); 16 | }); 17 | 18 | // start app 19 | app.start(); 20 | 21 | process.on('uncaughtException', function (err) { 22 | console.error(' Caught exception: ' + err.stack); 23 | }); -------------------------------------------------------------------------------- /template/game-server/app.js.wss: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var pomelo = require('pomelo'); 3 | 4 | /** 5 | * Init app for client. 6 | */ 7 | var app = pomelo.createApp(); 8 | app.set('name', '$'); 9 | 10 | // app configuration 11 | app.configure('production|development', 'connector', function(){ 12 | app.set('connectorConfig', 13 | { 14 | connector : pomelo.connectors.hybridconnector, 15 | heartbeat : 3, 16 | useDict : true, 17 | useProtobuf : true, 18 | ssl: { 19 | type: 'wss', 20 | key: fs.readFileSync('../shared/server.key'), 21 | cert: fs.readFileSync('../shared/server.crt') 22 | } 23 | }); 24 | }); 25 | 26 | // start app 27 | app.start(); 28 | 29 | process.on('uncaughtException', function (err) { 30 | console.error(' Caught exception: ' + err.stack); 31 | }); -------------------------------------------------------------------------------- /template/game-server/app/servers/connector/handler/entryHandler.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | return new Handler(app); 3 | }; 4 | 5 | var Handler = function(app) { 6 | this.app = app; 7 | }; 8 | 9 | /** 10 | * New client entry. 11 | * 12 | * @param {Object} msg request message 13 | * @param {Object} session current session object 14 | * @param {Function} next next step callback 15 | * @return {Void} 16 | */ 17 | Handler.prototype.entry = function(msg, session, next) { 18 | next(null, {code: 200, msg: 'game server is ok.'}); 19 | }; 20 | 21 | /** 22 | * Publish route for mqtt connector. 23 | * 24 | * @param {Object} msg request message 25 | * @param {Object} session current session object 26 | * @param {Function} next next step callback 27 | * @return {Void} 28 | */ 29 | Handler.prototype.publish = function(msg, session, next) { 30 | var result = { 31 | topic: 'publish', 32 | payload: JSON.stringify({code: 200, msg: 'publish message is ok.'}) 33 | }; 34 | next(null, result); 35 | }; 36 | 37 | /** 38 | * Subscribe route for mqtt connector. 39 | * 40 | * @param {Object} msg request message 41 | * @param {Object} session current session object 42 | * @param {Function} next next step callback 43 | * @return {Void} 44 | */ 45 | Handler.prototype.subscribe = function(msg, session, next) { 46 | var result = { 47 | topic: 'subscribe', 48 | payload: JSON.stringify({code: 200, msg: 'subscribe message is ok.'}) 49 | }; 50 | next(null, result); 51 | }; 52 | -------------------------------------------------------------------------------- /template/game-server/config/adminServer.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "type": "connector", 3 | "token": "agarxhqb98rpajloaxn34ga8xrunpagkjwlaw3ruxnpaagl29w4rxn" 4 | } 5 | ] -------------------------------------------------------------------------------- /template/game-server/config/adminUser.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "user-1", 4 | "username": "admin", 5 | "password": "admin", 6 | "level": 1 7 | }, 8 | 9 | { 10 | "id": "user-2", 11 | "username": "monitor", 12 | "password": "monitor", 13 | "level": 2 14 | }, 15 | 16 | { 17 | "id": "user-3", 18 | "username": "test", 19 | "password": "test", 20 | "level": 2 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /template/game-server/config/clientProtos.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /template/game-server/config/dictionary.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /template/game-server/config/log4js.json: -------------------------------------------------------------------------------- 1 | { 2 | "appenders": [ 3 | { 4 | "type": "console" 5 | }, 6 | { 7 | "type": "file", 8 | "filename": "${opts:base}/logs/con-log-${opts:serverId}.log", 9 | "pattern": "connector", 10 | "maxLogSize": 1048576, 11 | "layout": { 12 | "type": "basic" 13 | }, 14 | "backups": 5, 15 | "category": "con-log" 16 | }, 17 | { 18 | "type": "file", 19 | "filename": "${opts:base}/logs/rpc-log-${opts:serverId}.log", 20 | "maxLogSize": 1048576, 21 | "layout": { 22 | "type": "basic" 23 | }, 24 | "backups": 5, 25 | "category": "rpc-log" 26 | }, 27 | { 28 | "type": "file", 29 | "filename": "${opts:base}/logs/forward-log-${opts:serverId}.log", 30 | "maxLogSize": 1048576, 31 | "layout": { 32 | "type": "basic" 33 | }, 34 | "backups": 5, 35 | "category": "forward-log" 36 | }, 37 | { 38 | "type": "file", 39 | "filename": "${opts:base}/logs/rpc-debug-${opts:serverId}.log", 40 | "maxLogSize": 1048576, 41 | "layout": { 42 | "type": "basic" 43 | }, 44 | "backups": 5, 45 | "category": "rpc-debug" 46 | }, 47 | { 48 | "type": "file", 49 | "filename": "${opts:base}/logs/crash.log", 50 | "maxLogSize": 1048576, 51 | "layout": { 52 | "type": "basic" 53 | }, 54 | "backups": 5, 55 | "category":"crash-log" 56 | }, 57 | { 58 | "type": "file", 59 | "filename": "${opts:base}/logs/admin.log", 60 | "maxLogSize": 1048576, 61 | "layout": { 62 | "type": "basic" 63 | } 64 | ,"backups": 5, 65 | "category":"admin-log" 66 | }, 67 | { 68 | "type": "file", 69 | "filename": "${opts:base}/logs/pomelo-${opts:serverId}.log", 70 | "maxLogSize": 1048576, 71 | "layout": { 72 | "type": "basic" 73 | } 74 | ,"backups": 5, 75 | "category":"pomelo" 76 | }, 77 | { 78 | "type": "file", 79 | "filename": "${opts:base}/logs/pomelo-admin.log", 80 | "maxLogSize": 1048576, 81 | "layout": { 82 | "type": "basic" 83 | } 84 | ,"backups": 5, 85 | "category":"pomelo-admin" 86 | }, 87 | { 88 | "type": "file", 89 | "filename": "${opts:base}/logs/pomelo-rpc-${opts:serverId}.log", 90 | "maxLogSize": 1048576, 91 | "layout": { 92 | "type": "basic" 93 | } 94 | ,"backups": 5, 95 | "category":"pomelo-rpc" 96 | } 97 | ], 98 | 99 | "levels": { 100 | "rpc-log" : "ERROR", 101 | "forward-log": "ERROR" 102 | }, 103 | 104 | "replaceConsole": true, 105 | 106 | "lineDebug": false 107 | } 108 | -------------------------------------------------------------------------------- /template/game-server/config/master.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "id": "master-server-1", "host": "127.0.0.1", "port": 3005 4 | }, 5 | "production": { 6 | "id": "master-server-1", "host": "127.0.0.1", "port": 3005 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /template/game-server/config/serverProtos.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /template/game-server/config/servers.json: -------------------------------------------------------------------------------- 1 | { 2 | "development":{ 3 | "connector": [ 4 | {"id": "connector-server-1", "host": "127.0.0.1", "port": 3150, "clientHost": "127.0.0.1", "clientPort": 3010, "frontend": true} 5 | ] 6 | }, 7 | "production":{ 8 | "connector": [ 9 | {"id": "connector-server-1", "host": "127.0.0.1", "port": 3150, "clientHost": "127.0.0.1", "clientPort": 3010, "frontend": true} 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /template/game-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"$", 3 | "version":"0.0.1", 4 | "private":false, 5 | "dependencies":{ 6 | "pomelo-upgrade":"#" 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /template/npm-install.bat: -------------------------------------------------------------------------------- 1 | ::npm-install.bat 2 | @echo off 3 | ::install web server dependencies && game server dependencies 4 | cd web-server && npm install -d && cd .. && cd game-server && npm install -d -------------------------------------------------------------------------------- /template/npm-install.sh: -------------------------------------------------------------------------------- 1 | cd ./game-server && npm install -d 2 | echo '============ game-server npm installed ============' 3 | cd .. 4 | cd ./web-server && npm install -d 5 | echo '============ web-server npm installed ============' 6 | -------------------------------------------------------------------------------- /template/shared/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICSzCCAbQCCQCQVN8rD6MylDANBgkqhkiG9w0BAQUFADBqMQswCQYDVQQGEwJD 3 | TjERMA8GA1UECAwIemhlamlhbmcxETAPBgNVBAcMCGhhbmd6aG91MRAwDgYDVQQK 4 | DAdOZXRFYXNlMQ8wDQYDVQQLDAZwb21lbG8xEjAQBgNVBAMMCWxvY2FsaG9zdDAe 5 | Fw0xNDA0MjIwNjEwMDJaFw0xNDA1MjIwNjEwMDJaMGoxCzAJBgNVBAYTAkNOMREw 6 | DwYDVQQIDAh6aGVqaWFuZzERMA8GA1UEBwwIaGFuZ3pob3UxEDAOBgNVBAoMB05l 7 | dEVhc2UxDzANBgNVBAsMBnBvbWVsbzESMBAGA1UEAwwJbG9jYWxob3N0MIGfMA0G 8 | CSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMPe8oscKpTlQFZRrpbWmSO1UE+H65nq50 9 | l5+ptOVPMK3wgEj+YRyGWhBjugj9teVmLXY9ImWdZkBlvdAiQj7/S/1MxRbRtwEF 10 | GRE5ul/X1M6I+F0UyTGYA1Mo0jIlQaBDXAAyDujCWi+qlyZ28efNDUlO2KBY1H4r 11 | Xobm9hoEFQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAMIuL8KqEEtjbfL/tR2+5dQ5 12 | 958gtDtA62L7bMosl4hmuzdyWADu3IcKSaXAESLhIuIClt2Pwc14iFf9qRyB/cjY 13 | 4kLgwDGhK5EJw1kQS+Hs9NNSGxJTXUkoms3kEdRGy4hrZpTheJJNaKuv3oXrdvYQ 14 | 85yoc/P5OnJapB3huYL9 15 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /template/shared/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICWwIBAAKBgQDMPe8oscKpTlQFZRrpbWmSO1UE+H65nq50l5+ptOVPMK3wgEj+ 3 | YRyGWhBjugj9teVmLXY9ImWdZkBlvdAiQj7/S/1MxRbRtwEFGRE5ul/X1M6I+F0U 4 | yTGYA1Mo0jIlQaBDXAAyDujCWi+qlyZ28efNDUlO2KBY1H4rXobm9hoEFQIDAQAB 5 | AoGAXhaeCUIyqeoynLWh+yzzOHFqzjpnrr0iIwYCgJycEqobRzLh7YXxLRdqe3al 6 | U7Oq9TI2SR2CcEs9mWEi89VOzVvfu+4zRlvJLMzNjG8ncdvzmzWR288ORq6qmYVU 7 | 3KAEz/tbNaQMLrD43hkIb9BrSIb/cnwekl3pANo9dwytU5UCQQD4V6vTyzs/ob21 8 | +fO98tFkPtoHbt43S/1kDBSUyh6WWbS1KIQgtUSr2P5Ddtl6/vD3DW+XHCAhxyfV 9 | vuDvaP/fAkEA0oomFfmlpvzYejYNKPOz2PR+M0oRFVwn7lYyNwbRtUK1JYOMHwJ/ 10 | 3gwQEgAcYEkvgRlsxX0T5vHNmoR3U3OqiwJAIWkiG9devDvVWxMqoKZ3V0ZBbPiU 11 | etoFWB1r82yR2uZssmamCAR7HaeO5aKqtapw3rv3BFxrUkAJ8u7AMlVs/wJAVnpm 12 | MGqNjyyWIoSnHSYUvk2WtKx8neBvimcfUxja9HAFBfaljGszaFpeE3a2MRp+h7GQ 13 | ywGYNikmAYzdkoqVBwJAcOm/6u863pD2xA1mSFnmm3TulAMBfCULLdcY40w9m38b 14 | D89R1ISEy//N1fWa4KTsM0GpVOowEyluc53XNRUghw== 15 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /template/web-server/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express.createServer(); 3 | 4 | app.configure(function(){ 5 | app.use(express.methodOverride()); 6 | app.use(express.bodyParser()); 7 | app.use(app.router); 8 | app.set('view engine', 'jade'); 9 | app.set('views', __dirname + '/public'); 10 | app.set('view options', {layout: false}); 11 | app.set('basepath',__dirname + '/public'); 12 | }); 13 | 14 | app.configure('development', function(){ 15 | app.use(express.static(__dirname + '/public')); 16 | app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 17 | }); 18 | 19 | app.configure('production', function(){ 20 | var oneYear = 31557600000; 21 | app.use(express.static(__dirname + '/public', { maxAge: oneYear })); 22 | app.use(express.errorHandler()); 23 | }); 24 | 25 | console.log("Web server has started.\nPlease log on http://127.0.0.1:3001/index.html"); 26 | 27 | app.listen(3001); 28 | -------------------------------------------------------------------------------- /template/web-server/app.js.https: -------------------------------------------------------------------------------- 1 | var https = require('https'); 2 | var express = require('express'); 3 | 4 | var fs = require('fs'); 5 | 6 | var options = { 7 | key: fs.readFileSync('../shared/server.key'), 8 | cert: fs.readFileSync('../shared/server.crt') 9 | }; 10 | 11 | var app = express(); 12 | 13 | app.configure(function(){ 14 | app.use(express.methodOverride()); 15 | app.use(express.bodyParser()); 16 | app.use(app.router); 17 | app.set('view engine', 'jade'); 18 | app.set('views', __dirname + '/public'); 19 | app.set('view options', {layout: false}); 20 | app.set('basepath',__dirname + '/public'); 21 | }); 22 | 23 | app.configure('development', function(){ 24 | app.use(express.static(__dirname + '/public')); 25 | app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 26 | }); 27 | 28 | app.configure('production', function(){ 29 | var oneYear = 31557600000; 30 | app.use(express.static(__dirname + '/public', { maxAge: oneYear })); 31 | app.use(express.errorHandler()); 32 | }); 33 | 34 | console.log("Web server has started.\nPlease log on http://127.0.0.1:3001/index.html"); 35 | 36 | var httpsServer = https.createServer(options, app); 37 | 38 | httpsServer.listen(3001); -------------------------------------------------------------------------------- /template/web-server/bin/component.bat: -------------------------------------------------------------------------------- 1 | cd public/js/lib && component install -f && component build -v -------------------------------------------------------------------------------- /template/web-server/bin/component.sh: -------------------------------------------------------------------------------- 1 | cd public/js/lib && component install -f && component build -v -------------------------------------------------------------------------------- /template/web-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "$", 3 | "version": "0.0.1", 4 | "private": false, 5 | "dependencies": { 6 | "express": "3.4.8" 7 | } 8 | } -------------------------------------------------------------------------------- /template/web-server/public/css/base.css: -------------------------------------------------------------------------------- 1 | .g-doc { 2 | width: 1000px; 3 | margin: 0 auto; 4 | text-align: left; 5 | line-height: 18px; 6 | font-size: 12px; 7 | color: #555; 8 | font-family: arial; 9 | } 10 | 11 | .g-banner { 12 | position: relative; 13 | width: 1000px; 14 | height: 90px; 15 | margin: 0 auto; 16 | } 17 | 18 | .g-banner .logo { 19 | position: absolute; 20 | left: 20px; 21 | top: 13px; 22 | width: 153px; 23 | height: 60px; 24 | z-index: 101; 25 | } 26 | 27 | .g-banner .logo .img { 28 | width: 153px; 29 | height: 60px; 30 | background: url(../image/logo.png) no-repeat 0 0; 31 | } 32 | 33 | .g-background { 34 | height: 350px; 35 | background: url(../image/sp.png) no-repeat 326px 0; 36 | } 37 | 38 | .g-content { 39 | padding: 150px; 40 | width: 800px; 41 | margin-left: 50px; 42 | font-size: 50pt; 43 | font-style: italic; 44 | } 45 | 46 | .g-link { 47 | height: 100px; 48 | margin-top: 30px; 49 | margin-left: 300px; 50 | font-size: 15pt; 51 | } 52 | 53 | .g-button { 54 | margin-top: 10px; 55 | text-align: center; 56 | } 57 | 58 | a:link { 59 | color: #006699; 60 | text-decoration: none; 61 | } 62 | 63 | a:visited { 64 | color: #006699; 65 | text-decoration: none; 66 | } 67 | 68 | a:hover { 69 | color: #FF0000; 70 | text-decoration: underline; 71 | } 72 | 73 | a:active { 74 | color: #006699; 75 | text-decoration: underline; 76 | } 77 | -------------------------------------------------------------------------------- /template/web-server/public/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frank198/pomelo-upgrade/6d56e02d0e61b3ffd05bd69a2337ec3a4ed9b0e5/template/web-server/public/image/logo.png -------------------------------------------------------------------------------- /template/web-server/public/image/sp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frank198/pomelo-upgrade/6d56e02d0e61b3ffd05bd69a2337ec3a4ed9b0e5/template/web-server/public/image/sp.png -------------------------------------------------------------------------------- /template/web-server/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pomelo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Welcome to Pomelo 46 | 47 | 48 | 49 | Home: 50 | https://github.com/NetEase/pomelo 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /template/web-server/public/index.html.sio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pomelo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 19 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Welcome to Pomelo 47 | 48 | 49 | 50 | Home: 51 | https://github.com/NetEase/pomelo 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /template/web-server/public/js/lib/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pomelo-client", 3 | "description": "pomelo-client", 4 | "local": [ "boot" ], 5 | "paths": [ "local"] 6 | } -------------------------------------------------------------------------------- /template/web-server/public/js/lib/local/boot/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boot", 3 | "description": "Main app boot component", 4 | "dependencies": { 5 | "component/emitter":"master", 6 | "NetEase/pomelo-protocol": "master", 7 | "pomelonode/pomelo-protobuf": "*", 8 | "pomelonode/pomelo-jsclient-websocket": "master" 9 | }, 10 | "scripts": ["index.js"] 11 | } -------------------------------------------------------------------------------- /template/web-server/public/js/lib/local/boot/index.js: -------------------------------------------------------------------------------- 1 | var Emitter = require('emitter'); 2 | window.EventEmitter = Emitter; 3 | 4 | var protocol = require('pomelo-protocol'); 5 | window.Protocol = protocol; 6 | 7 | var protobuf = require('pomelo-protobuf'); 8 | window.protobuf = protobuf; 9 | 10 | var pomelo = require('pomelo-jsclient-websocket'); 11 | window.pomelo = pomelo; 12 | -------------------------------------------------------------------------------- /test/config/log4js.json: -------------------------------------------------------------------------------- 1 | { 2 | "appenders": [ 3 | { 4 | "type": "file", 5 | "filename": "./test/logs/node-log-${opts:serverId}.log", 6 | "fileSize": 1048576, 7 | "layout": { 8 | "type": "basic" 9 | }, 10 | "backups": 5 11 | }, 12 | { 13 | "type": "console" 14 | }, 15 | { 16 | "type": "file", 17 | "filename": "./test/logs/con-log-${opts:serverId}.log", 18 | "pattern": "connector", 19 | "maxLogSize": 1048576, 20 | "layout": { 21 | "type": "basic" 22 | }, 23 | "backups": 5, 24 | "category":"con-log" 25 | }, 26 | { 27 | "type": "file", 28 | "filename": "./test/logs/rpc-log-${opts:serverId}.log", 29 | "fileSize": 1048576, 30 | "layout": { 31 | "type": "basic" 32 | }, 33 | "backups": 5, 34 | "category":"rpc-log" 35 | }, 36 | { 37 | "type": "file", 38 | "filename": "./test/logs/forward-log-${opts:serverId}.log", 39 | "fileSize": 1048576, 40 | "layout": { 41 | "type": "basic" 42 | }, 43 | "backups": 5, 44 | "category":"forward-log" 45 | }, 46 | { 47 | "type": "file", 48 | "filename": "./test/logs/crash.log", 49 | "fileSize": 1048576, 50 | "layout": { 51 | "type": "basic" 52 | }, 53 | "backups": 5, 54 | "category":"crash-log" 55 | }, 56 | { 57 | "type": "file", 58 | "filename": "./test/logs/transaction.log", 59 | "fileSize": 1048576, 60 | "layout": { 61 | "type": "basic" 62 | }, 63 | "backups": 5, 64 | "category":"transaction-log" 65 | }, 66 | { 67 | "type": "file", 68 | "filename": "./test/logs/transaction-error.log", 69 | "fileSize": 1048576, 70 | "layout": { 71 | "type": "basic" 72 | }, 73 | "backups": 5, 74 | "category":"transaction-error-log" 75 | } 76 | ], 77 | 78 | "levels": { 79 | "rpc-log" : "ERROR", 80 | "forward-log": "ERROR" 81 | }, 82 | 83 | "replaceConsole": true 84 | } 85 | -------------------------------------------------------------------------------- /test/config/master.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "id": "master-server-1", "host": "127.0.0.1", "port": 3005 4 | }, 5 | 6 | "production": { 7 | "id": "master-server-1", "host": "172.17.2.237", "port": 3005 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/config/servers.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | }, 4 | "production": { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/filters/handler/serial.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var serialFilter = require('../../../lib/filters/handler/serial'); 3 | var FilterService = require('../../../lib/common/service/filterService'); 4 | var util = require('util'); 5 | 6 | var mockSession = { 7 | key : "123" 8 | }; 9 | 10 | var WAIT_TIME = 100; 11 | describe("#serialFilter",function(){ 12 | it("should do before filter ok",function(done){ 13 | var service = new FilterService(); 14 | var filter = serialFilter(); 15 | service.before(filter); 16 | 17 | service.beforeFilter(null,mockSession,function(){ 18 | should.exist(mockSession); 19 | 20 | should.exist(mockSession.__serialTask__); 21 | done(); 22 | }); 23 | }); 24 | 25 | it("should do after filter by doing before filter ok",function(done){ 26 | var service = new FilterService(); 27 | var filter = serialFilter(); 28 | var _session ; 29 | service.before(filter); 30 | 31 | service.beforeFilter(null,mockSession,function(){ 32 | should.exist(mockSession); 33 | should.exist(mockSession.__serialTask__); 34 | _session = mockSession; 35 | }); 36 | 37 | service.after(filter); 38 | 39 | service.afterFilter(null,null,mockSession,null,function(){ 40 | should.exist(mockSession); 41 | should.strictEqual(mockSession,_session); 42 | }); 43 | 44 | setTimeout(done,WAIT_TIME); 45 | }); 46 | }); -------------------------------------------------------------------------------- /test/filters/handler/time.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var serialFilter = require('../../../lib/filters/handler/time'); 3 | var FilterService = require('../../../lib/common/service/filterService'); 4 | var util = require('util'); 5 | var mockSession = { 6 | key : "123" 7 | }; 8 | 9 | var WAIT_TIME = 100; 10 | describe("#serialFilter",function(){ 11 | it("should do before filter ok",function(done){ 12 | var service = new FilterService(); 13 | var filter = serialFilter(); 14 | service.before(filter); 15 | 16 | 17 | service.beforeFilter(null,mockSession,function(){ 18 | should.exist(mockSession); 19 | 20 | should.exist(mockSession.__startTime__); 21 | done(); 22 | }); 23 | }); 24 | 25 | it("should do after filter by doing before filter ok",function(done){ 26 | var service = new FilterService(); 27 | var filter = serialFilter(); 28 | var _session ; 29 | service.before(filter); 30 | 31 | service.beforeFilter(null,mockSession,function(){ 32 | should.exist(mockSession); 33 | should.exist(mockSession.__startTime__); 34 | _session = mockSession; 35 | }); 36 | 37 | service.after(filter); 38 | 39 | service.afterFilter(null,{route:"hello"},mockSession,null,function(){ 40 | should.exist(mockSession); 41 | should.strictEqual(mockSession,_session); 42 | }); 43 | 44 | setTimeout(done,WAIT_TIME); 45 | done(); 46 | }); 47 | }); -------------------------------------------------------------------------------- /test/filters/handler/timeout.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var timeoutFilter = require('../../../lib/filters/handler/timeout'); 3 | var FilterService = require('../../../lib/common/service/filterService'); 4 | var util = require('util'); 5 | var mockSession = { 6 | key : "123" 7 | }; 8 | 9 | var WAIT_TIME = 100; 10 | describe("#serialFilter",function(){ 11 | it("should do before filter ok",function(done){ 12 | var service = new FilterService(); 13 | var filter = timeoutFilter(); 14 | service.before(filter); 15 | 16 | service.beforeFilter(null,mockSession,function(){ 17 | should.exist(mockSession); 18 | 19 | should.exist(mockSession.__timeout__); 20 | done(); 21 | }); 22 | }); 23 | 24 | it("should do after filter by doing before filter ok",function(done){ 25 | var service = new FilterService(); 26 | var filter = timeoutFilter(); 27 | var _session ; 28 | service.before(filter); 29 | 30 | service.beforeFilter(null,mockSession,function(){ 31 | should.exist(mockSession); 32 | should.exist(mockSession.__timeout__); 33 | _session = mockSession; 34 | }); 35 | 36 | service.after(filter); 37 | 38 | service.afterFilter(null,null,mockSession,null,function(){ 39 | should.exist(mockSession); 40 | should.strictEqual(mockSession,_session); 41 | }); 42 | 43 | setTimeout(done,WAIT_TIME); 44 | done(); 45 | }); 46 | }); -------------------------------------------------------------------------------- /test/filters/handler/toobusy.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var toobusyFilter = require('../../../lib/filters/handler/toobusy'); 3 | var FilterService = require('../../../lib/common/service/filterService'); 4 | var util = require('util'); 5 | var mockSession = { 6 | key : "123" 7 | }; 8 | 9 | describe("#toobusyFilter",function(){ 10 | it("should do before filter ok",function(done){ 11 | var service = new FilterService(); 12 | var filter = toobusyFilter(); 13 | service.before(filter); 14 | 15 | service.beforeFilter(null,mockSession,function(err){ 16 | should.not.exist(err); 17 | should.exist(mockSession); 18 | done(); 19 | }); 20 | }); 21 | 22 | it("should do before filter error because of too busy",function(done){ 23 | var service = new FilterService(); 24 | var filter = toobusyFilter(); 25 | service.before(filter); 26 | 27 | var exit = false; 28 | function load() { 29 | service.beforeFilter(null,mockSession,function(err, resp){ 30 | should.exist(mockSession); 31 | console.log('err: ' + err); 32 | if (!!err) { 33 | exit = true; 34 | } 35 | }); 36 | 37 | console.log('exit: ' + exit); 38 | if (exit) { 39 | return done(); 40 | } 41 | var start = new Date(); 42 | while ((new Date() - start) < 250) { 43 | for (var i = 0; i < 1e5;) i++; 44 | } 45 | setTimeout(load, 0); 46 | } 47 | load(); 48 | 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/filters/rpc/rpcLog.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var rpcLogFilter = require('../../../lib/filters/rpc/rpcLog')(); 3 | 4 | var mockData = { 5 | serverId : "connector-server-1", 6 | msg : "hello", 7 | opts : {} 8 | }; 9 | 10 | describe('#rpcLogFilter',function(){ 11 | it("should do after filter by before filter",function(done){ 12 | rpcLogFilter.before(mockData.serverId,mockData.msg,mockData.opts,function(serverId,msg,opts){ 13 | rpcLogFilter.after(serverId,msg,opts,function(serverId,msg,opts){ 14 | should.exist(opts.__start_time__); 15 | done(); 16 | }); 17 | }); 18 | }); 19 | }); -------------------------------------------------------------------------------- /test/filters/rpc/toobusy.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var toobusyFilter = require('../../../lib/filters/rpc/toobusy')(); 3 | 4 | var mockData = { 5 | serverId : "connector-server-1", 6 | msg : "hello", 7 | opts : {} 8 | }; 9 | 10 | 11 | describe('#toobusyFilter',function(){ 12 | it("should no callback for toobusy",function(done){ 13 | function load() { 14 | var callbackInvoked = false; 15 | toobusyFilter.before(mockData.serverId,mockData.msg,mockData.opts,function(serverId,msg,opts){ 16 | callbackInvoked = true; 17 | }); 18 | 19 | if (!callbackInvoked) { 20 | console.log(' logic of toobusy enterd, done!'); 21 | return done(); 22 | } 23 | var start = new Date(); 24 | while ((new Date() - start) < 250) { 25 | for (var i = 0; i < 1e5;) i++; 26 | } 27 | setTimeout(load, 0); 28 | } 29 | load(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/manager/mockChannelManager.js: -------------------------------------------------------------------------------- 1 | var DEFAULT_PREFIX = 'POMELO:CHANNEL'; 2 | var utils = require('../../lib/util/utils'); 3 | 4 | var MockManager = function(app, opts) { 5 | this.app = app; 6 | this.opts = opts || {}; 7 | this.prefix = opts.prefix || DEFAULT_PREFIX; 8 | }; 9 | 10 | module.exports = MockManager; 11 | 12 | MockManager.prototype.start = function(cb) { 13 | this.usersMap = {}; 14 | utils.invokeCallback(cb); 15 | }; 16 | 17 | MockManager.prototype.stop = function(force, cb) { 18 | this.usersMap = null; 19 | utils.invokeCallback(cb); 20 | }; 21 | 22 | MockManager.prototype.add = function(name, uid, sid, cb) { 23 | var key = genKey(this, name, sid); 24 | if(!this.usersMap[key]) { 25 | this.usersMap[key] = []; 26 | } 27 | this.usersMap[key].push(uid); 28 | utils.invokeCallback(cb); 29 | }; 30 | 31 | MockManager.prototype.leave = function(name, uid, sid, cb) { 32 | var key = genKey(this, name, sid); 33 | var res = deleteFrom(uid, this.usersMap[key]); 34 | if(this.usersMap[key] && this.usersMap[key].length === 0) { 35 | delete this.usersMap[sid]; 36 | } 37 | utils.invokeCallback(cb); 38 | }; 39 | 40 | MockManager.prototype.getMembersBySid = function(name, sid, cb) { 41 | var key = genKey(this, name, sid); 42 | if(!this.usersMap[key]) 43 | this.usersMap[key] = []; 44 | utils.invokeCallback(cb, null, this.usersMap[key]); 45 | }; 46 | 47 | MockManager.prototype.destroyChannel = function(name, cb) { 48 | var servers = this.app.getServers(); 49 | var server, removes = []; 50 | for(var sid in servers) { 51 | server = servers[sid]; 52 | if(this.app.isFrontend(server)) { 53 | removes.push(genKey(this, name, sid)); 54 | } 55 | } 56 | 57 | if(removes.length === 0) { 58 | utils.invokeCallback(cb); 59 | return; 60 | } 61 | 62 | for(var i = 0; i