├── .editorconfig ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── bin └── start ├── examples ├── client.js ├── server-channel.js ├── server-session.js └── util.js ├── index.js ├── lib ├── daemon │ ├── daemon-handler.js │ └── daemon-process.js ├── node-manager │ ├── consistent-hashing.js │ ├── constants.js │ └── node-manager.js ├── pid.js ├── session-manager │ ├── redis-manager.js │ ├── redis-scan.js │ ├── session-manager.js │ └── session-subscriber.js ├── util │ ├── global-config.js │ ├── logging.js │ ├── ps.js │ └── utils.js ├── xpush-channel-server.js ├── xpush-session-server.js └── xpush.js ├── logo.png └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | # https://www.gitignore.io/api/osx,windows,intellij,node 3 | 4 | ### OSX ### 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | 31 | 32 | ### Windows ### 33 | # Windows image file caches 34 | Thumbs.db 35 | ehthumbs.db 36 | 37 | # Folder config file 38 | Desktop.ini 39 | 40 | # Recycle Bin used on file shares 41 | $RECYCLE.BIN/ 42 | 43 | # Windows Installer files 44 | *.cab 45 | *.msi 46 | *.msm 47 | *.msp 48 | 49 | # Windows shortcuts 50 | *.lnk 51 | 52 | 53 | ### Intellij ### 54 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 55 | 56 | *.iml 57 | 58 | ## Directory-based project format: 59 | .idea/ 60 | # if you remove the above rule, at least ignore the following: 61 | 62 | # User-specific stuff: 63 | # .idea/workspace.xml 64 | # .idea/tasks.xml 65 | # .idea/dictionaries 66 | 67 | # Sensitive or high-churn files: 68 | # .idea/dataSources.ids 69 | # .idea/dataSources.xml 70 | # .idea/sqlDataSources.xml 71 | # .idea/dynamic.xml 72 | # .idea/uiDesigner.xml 73 | 74 | # Gradle: 75 | # .idea/gradle.xml 76 | # .idea/libraries 77 | 78 | # Mongo Explorer plugin: 79 | # .idea/mongoSettings.xml 80 | 81 | ## File-based project format: 82 | *.ipr 83 | *.iws 84 | 85 | ## Plugin-specific files: 86 | 87 | # IntelliJ 88 | /out/ 89 | 90 | # mpeltonen/sbt-idea plugin 91 | .idea_modules/ 92 | 93 | # JIRA plugin 94 | atlassian-ide-plugin.xml 95 | 96 | # Crashlytics plugin (for Android Studio and IntelliJ) 97 | com_crashlytics_export_strings.xml 98 | crashlytics.properties 99 | crashlytics-build.properties 100 | 101 | 102 | ### Node ### 103 | # Logs 104 | logs 105 | *.log 106 | 107 | # Runtime data 108 | pids 109 | *.pid 110 | *.seed 111 | 112 | # Directory for instrumented libs generated by jscoverage/JSCover 113 | lib-cov 114 | 115 | # Coverage directory used by tools like istanbul 116 | coverage 117 | 118 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 119 | .grunt 120 | 121 | # node-waf configuration 122 | .lock-wscript 123 | 124 | # Compiled binary addons (http://nodejs.org/api/addons.html) 125 | build/Release 126 | 127 | # Dependency directory 128 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 129 | node_modules 130 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples/ 2 | .DS_Store 3 | .git* 4 | node_modules/ 5 | test/ 6 | logo.png 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 xpush 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | visit [XPUSH](http://xpush.github.io) (for version 0.0.23) 6 | 7 | 8 | **We are preparing the documents for version 0.1.x** 9 | 10 | 11 | xpush 12 | ======= 13 | 14 | **XPUSH** (eXtensional PUSH) is a real-time communication server and a programmable library that supports websocket, GCM and APN. It is suitable for implementing components such as messengers and push system. 15 | 16 | 17 | Features 18 | ======= 19 | 20 | You can easily build real-time services with HTML5 Websocket and socket.io, the module on node.js. 21 | 22 | But, there are a lot of things to consider on building the real-time services. You need to consider various things such as distributed server configuration in consideration of the users who are rapidly increasing, network traffic processing of large capacity and scale-out strategy. 23 | 24 | **XPUSH** is a server platform that provides such as real-time message communication, message storage, user and device management and Mobile Push Notification. Everyone will be able to install and use after you downloaded. 25 | And, XPUSH is a service platform for developing a wide range of real-time services and applications over a single **XPUSH** server platform. 26 | 27 | Without having to directly implement the real-time communication servers, After you install **XPUSH** platform, try adding a real-time data communication features of your service. 28 | 29 | ### 1. Real-time Web Communication Platform 30 | 31 | " *We help you easily build real-time applications. XPUSH was designed for developers.* " 32 | 33 | **XPUSH** consists of real-time communication servers that has been developed as a Web technology. You can build a variety of real-time messaging applications such as your own messenger and chat services 34 | 35 | Currently, **XPUSH** developers are provided Javascript libraries for developing Web services and JAVA libraries for developing JAVA applications and Android applications. We are continually developing additional libraries. 36 | 37 | Real-time data communication, while maintaining the network connection between the Client and the Server, can transmit and receive data bidirectionally. If the connection to the server is lost, or even if some servers fails, it guarantees the service availability. 38 | 39 | ### 2. Works Everywhere 40 | 41 | " *At the core of XPUSH is the HTML5 WebSocket protocol, but we also have fallback mechanisms so that XPUSH just works anywhere, anytime.* " 42 | 43 | **XPUSH** was developed by node.js to implement high-performance servers. It uses Socket.IO, one of the modules for web-based real-time messaging. And we continue to update the modules to use the latest version. 44 | 45 | Socket.IO enables real-time bidirectional event-based communication. It works on every platform, browser or device, focusing equally on reliability and speed. 46 | 47 | ### 3. Scalable Web Architecture 48 | 49 | " *XPUSH was designed to work with commodity servers, an elastic virtualised environments saving you money and headaches. A scalable web application can handle growth – of users or work – without requiring changes to the source code and stoping existed servers* " 50 | 51 | Real time server platform has to be designed to be able to handle a large amount of network traffic to a sharply rising splice. It has to run a large number of servers for load balancing, needs to designed to be non-disruptive expansion. **XPUSH** developers designed the **Scalable Web Architecture** for a long time, and continue to optimize the architecture design. 52 | 53 | **XPUSH** manages the real-time status of the distributed servers through a **zookeeper** and use **Redis** to store connection information of the visitors and meta datas in the memory. And XPUSH stores various types of unstructured messages sent or received in **MongoDB**. 54 | 55 | **XPUSH** server platform run each in the Session server and Channel server. 56 | Channel server is responsible to authenticate users, managing user and device information, and assigning distribution server for load balancing. Since relatively Channel servers are easy to increase the load on the network traffic, so that it can be added separately as an expansion only Channel server 57 | 58 | 59 | ## 1. Prepare 60 | 61 | To use the XPUSH is, [nodejs](http://nodejs.org/), [zookeeper](http://zookeeper.apache.org/), [redis](http://redis.io/) is required . 62 | 63 | The following is how to install 64bit linux. Please install to suit your environment. 64 | If you have already been installed, please skip these preparation steps. 65 | 66 | ### Install nodejs 67 | [nodejs installation](http://nodejs.org/download/) by referring to Download and unzip the nodejs. 68 | 69 | cd $HOME/xpush 70 | wget http://nodejs.org/dist/v5.0.0/node-v5.0.0-linux-x64.tar.gz 71 | tar zvf node-v5.0.0-linux-x64.tar.gz 72 | cd node-v5.0.0-linux-x64 73 | ./configure 74 | make 75 | sudo make install 76 | 77 | ### Install zookeeper 78 | Install and run zookeeper with reference [Zookeeper installation](http://zookeeper.apache.org/doc/trunk/zookeeperStarted.html). 79 | 80 | The following is the code to install and run the zookeeper3.4.9. 81 | 82 | cd $HOME/xpush 83 | wget http://apache.mirror.cdnetworks.com/zookeeper/stable/zookeeper-3.4.9.tar.gz 84 | tar xvf zookeeper-3.4.9.tar.gz 85 | cp zookeeper-3.4.9/conf/zoo_sample.cfg zookeeper-3.4.9/conf/zoo.cfg 86 | cd zookeeper-3.4.9/bin 87 | ./zkServer.sh start 88 | 89 | 90 | ### Install redis 91 | Install and run redis with reference [Redis installation](http://zookeeper.apache.org/doc/trunk/zookeeperStarted.html). 92 | 93 | The follow is the code to install and run redis 3.2.6. 94 | 95 | cd $HOME/xpush 96 | wget http://download.redis.io/releases/redis-3.2.6.tar.gz 97 | tar xzf redis-3.2.6.tar.gz 98 | cd redis-3.2.6 99 | make 100 | src/redis-server 101 | (daemon : $ nohup src/redis-server & ) 102 | 103 | 104 | ## 2. Run your application with push module 105 | 106 | 107 | ### clone xpush repository 108 | 109 | git clone https://github.com/xpush/node-xpush.git 110 | cd xpush 111 | npm install 112 | 113 | 114 | ### run session server 115 | 116 | bin/start --session 117 | 118 | ### run channel server 119 | 120 | bin/start --channel 121 | 122 | ### with config.json 123 | 124 | bin/start ---config config.json 125 | 126 | ### Config Options 127 | 128 | ``` 129 | - host : bind ip 130 | - port : bind port 131 | ``` -------------------------------------------------------------------------------- /bin/start: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var net = require('net'); 5 | var argv = require('optimist').argv; 6 | var xpushServer = require('../lib/xpush'); 7 | var utils = require('../lib/util/utils'); 8 | 9 | var daemon = require('../lib/daemon/daemon-handler'); 10 | 11 | var help = [ 12 | "usage: start [type] [options] ", 13 | "", 14 | "Starts a XPUSH Server using the specified command-line options", 15 | "", 16 | "Examples:", 17 | " $ start --session", 18 | " $ start --session --config ../config.json", 19 | " $ HOST=im.stalk.io PORT=8000 start --session ", 20 | " $ start --channel", 21 | " $ HOST=channel.stalk.io PORT=9000 start session ", 22 | "", 23 | "Options:", 24 | " --port PORT Port that the channel server should run on", 25 | " --config OUTFILE Location of the configuration file for the server", 26 | " --host DOMAIN Hostname", 27 | " --home HOME_DIR Home Directory", 28 | " -h, --help You're staring at it", 29 | "", 30 | "Environments:", 31 | " TYPE server type [session | channel] (default : session)", 32 | " HOST hostname (default : 0.0.0.0)", 33 | " PORT port (default : 0.0.0.0)", 34 | " ZOOKEEPER zookeeper address (default : 127.0.0.1:2181)", 35 | " REDIS redis address (default : 127.0.0.1:6379)", 36 | " HOME_DIR log directory (default : ~/.xpush)", 37 | "" 38 | ].join('\n'); 39 | 40 | if (argv.h || argv.help) { 41 | return console.log(help); 42 | } 43 | 44 | // (optional) load configutation file 45 | 46 | var config = {}; 47 | if( argv.config ){ 48 | try { 49 | var data = fs.readFileSync(argv.config); 50 | config = JSON.parse(data.toString()); 51 | } catch (ex) { 52 | console.error('Error starting session server: ' + ex); 53 | process.exit(1); 54 | } 55 | } 56 | 57 | // setting options 58 | 59 | var options = {}; 60 | options['host'] = argv.host || config.host || process.env.HOST || utils.getIP(); 61 | options['port'] = argv.port || config.port || process.env.PORT || 8080; 62 | 63 | options['zookeeper'] = config.zookeeper || process.env.ZOOKEEPER; 64 | options['redis'] = config.redis || process.env.REDIS; 65 | 66 | var homeDir = config.home || process.env.HOME_DIR; 67 | if (homeDir){ 68 | if(homeDir.startsWith("/")){ 69 | options['home'] = homeDir; 70 | }else{ 71 | return console.error('\n\n [ERROR] home directory must to be full paths from root(/) \n\n'); 72 | } 73 | } 74 | 75 | options['type'] = argv.type || config.type || process.env.TYPE || 'session'; 76 | if(argv.session) options['type'] = 'session'; 77 | if(argv.channel) options['type'] = 'channel'; 78 | 79 | // start server 80 | console.log('\n ##### Options #####\n',options,'\n\n'); 81 | 82 | var checkPort = function (callback, port) { 83 | var port = port || options['port']; 84 | 85 | var tester = net.createServer() 86 | .once('error', function (err) { 87 | 88 | checkPort(callback, port + 100); 89 | 90 | }) 91 | .once('listening', function () { 92 | tester.once('close', function () { 93 | callback(port); 94 | }) 95 | .close(); 96 | }) 97 | .listen(port); 98 | }; 99 | 100 | 101 | if( options['type'] == 'channel' ){ 102 | 103 | checkPort(function (port) { 104 | 105 | options['port'] = port; 106 | 107 | daemon.startDaemon(options, function (err) { 108 | if (err) { 109 | console.error('\n\n process daemon was not yet finished \n'); 110 | if (err.code == 'PID_EXISTED') { 111 | console.info(err.message); 112 | } 113 | } else { 114 | server = xpushServer.createChannelServer(options); 115 | } 116 | }); 117 | 118 | }); 119 | 120 | }else{ 121 | 122 | var pidFilePath = utils.getPidFilePath(options['home'], 'SESSION', options['port']); 123 | if (fs.existsSync(pidFilePath)) fs.unlinkSync(pidFilePath); 124 | var pid = require('../lib/pid').create(pidFilePath); 125 | pid.removeOnExit(); 126 | 127 | server = xpushServer.createSessionServer(options); 128 | 129 | } 130 | -------------------------------------------------------------------------------- /examples/client.js: -------------------------------------------------------------------------------- 1 | var io = require('socket.io-client'), 2 | util = require('./util'), 3 | faker = require('faker'); 4 | 5 | var address = process.argv[2]; 6 | var count = process.argv[3] || 1; 7 | var channel = process.argv[4] || "zztv01-20150612185253"; 8 | 9 | var _host = address.substr(0, address.indexOf(':')); 10 | var _port = Number(address.substr(address.indexOf(':') + 1)); 11 | 12 | var count_connected = 1; 13 | var count_error = 1; 14 | var count_disconnected = 1; 15 | 16 | var run = function () { 17 | 18 | var app = "P-00001"; 19 | var userId = faker.internet.userName(); 20 | 21 | console.log('CHANNEL : ', address + '/node/' + app + '/' + channel); 22 | 23 | util.get(_host, _port, '/node/' + app + '/' + channel, function (err, data) { 24 | 25 | console.log(data); 26 | 27 | var uid = userId.replace(/\./g, ''); 28 | uid = "zztv01"; 29 | var DT = {"U": uid, "roomLevel": "0"}; 30 | var query = 31 | 'A=' + app + '&' + 32 | 'C=' + channel + '&' + 33 | 'U=' + uid + '&' + 34 | 'D=DEVAPP&' + 35 | 'S=' + data.result.server.name + '&' + 36 | 'DT=' + JSON.stringify(DT) + '&' + 37 | 'MD=CHANNEL_ONLY'; 38 | ; 39 | 40 | var socketOptions = {transsessionPorts: ['websocket'], 'force new connection': true}; 41 | console.log('CHANNEL : ', data.result.server.url + '/channel?' + query); 42 | 43 | var channelSocket = io.connect(data.result.server.url + '/channel?' + query, socketOptions); 44 | 45 | channelSocket.on('connect', function () { 46 | console.log(count_connected + '. connected'); 47 | count_connected = count_connected + 1; 48 | setInterval(function () { 49 | var DT = {id: "zztv01", message: "dGVzdA%3D%3D"}; 50 | channelSocket.emit('send', {'NM': 'message', 'DT': DT}); 51 | }, 1000); 52 | }); 53 | 54 | channelSocket.on('message', function (data) { 55 | }); 56 | 57 | channelSocket.on('error', function (data) { 58 | console.error(count_error + " " + data); 59 | count_error = count_error + 1; 60 | }); 61 | 62 | channelSocket.on('disconnect', function () { 63 | console.log(count_disconnected + '. disconnected'); 64 | count_disconnected = count_disconnected + 1; 65 | }); 66 | 67 | }); 68 | 69 | }; 70 | 71 | for (var a = 0; a < count; a++) { 72 | run(); 73 | } -------------------------------------------------------------------------------- /examples/server-channel.js: -------------------------------------------------------------------------------- 1 | var xpush = require('../lib/xpush'); 2 | 3 | var config = { 4 | "zookeeper": {}, 5 | "redis": {}, 6 | "port": 9000 7 | }; 8 | 9 | var port = process.argv[2]; 10 | if (port) config.port = port; 11 | 12 | var server = xpush.createChannelServer(config); 13 | 14 | // Customizing connection events 15 | server.onConnection(function (socket) { 16 | var query = socket.handshake.query; 17 | 18 | console.log('CONNECTION - ' + query.A + " : " + query.C + " : " + query.U); 19 | 20 | // add customized socket events 21 | socket.on('sessionCount', function (callback) { 22 | server.getSessionCount(socket, function (err, data) { 23 | 24 | callback({ 25 | status: 'ok', 26 | result: data 27 | }); 28 | 29 | }); 30 | }); 31 | 32 | }); 33 | 34 | server.on('started', function (url, port) { 35 | console.log(' >>>>>> Channel SERVER is started ' + url + ':' + port); 36 | }); 37 | -------------------------------------------------------------------------------- /examples/server-session.js: -------------------------------------------------------------------------------- 1 | var xpush = require('../lib/xpush'); 2 | 3 | var config = { 4 | "zookeeper": {}, 5 | "redis": {}, 6 | "port": 8000 7 | }; 8 | 9 | var port = process.argv[2] 10 | if (port) config.port = port; 11 | 12 | var server = xpush.createSessionServer(config); 13 | 14 | 15 | function foo(req, res, next) { 16 | res.send({hello: 'Hello xpush world.' + config.port}); 17 | next(); 18 | } 19 | 20 | function bar(req, res, next) { 21 | res.send({hello: 'Hello xpush world.' + config.port}); 22 | next(); 23 | } 24 | 25 | function foobar(req, res, next) { 26 | res.send({hello: 'Hello xpush world.' + config.port}); 27 | next(); 28 | } 29 | 30 | 31 | server.on('started', function (url, port) { 32 | 33 | //You can add request events to xpush session server. 34 | server.onGet('/foo', foo); 35 | 36 | server.onPut('/bar', bar); 37 | 38 | server.onDelete('/foobar', foobar); 39 | 40 | console.log(' >>>>>> SESSION SERVER is started ' + url + ':' + port); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /examples/util.js: -------------------------------------------------------------------------------- 1 | var http = require('http'), fs = require('fs'), path = require('path'), mime = require('mime'); 2 | exports.post = function (host, port, path, data, cb) { 3 | var dataObject = JSON.stringify(data); 4 | var postheaders = {'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(dataObject, 'utf8')}; 5 | var optionspost = {host: host, port: port, path: path, method: 'POST', headers: postheaders}; 6 | var reqPost = http.request(optionspost, function (res) { 7 | var result = ""; 8 | res.on('data', function (chunk) { 9 | result += chunk; 10 | }); 11 | res.on('end', function () { 12 | cb(null, JSON.parse(result)); 13 | }); 14 | }); 15 | reqPost.write(dataObject); 16 | reqPost.end(); 17 | reqPost.on('error', function (e) { 18 | console.error(e); 19 | }); 20 | }; 21 | exports.postFile = function (host, port, path, data, cb) { 22 | var options = {host: host, port: port, path: path, method: 'POST'}; 23 | options.headers = { 24 | 'XP-A': data.appId, 25 | 'XP-C': data.channel, 26 | 'XP-U': JSON.stringify({U: data.userId, D: data.deviceId}) 27 | }; 28 | function getFormDataForPost(value) { 29 | function encodeFilePart(boundary, type, name, filename) { 30 | var return_part = "--" + boundary + "\r\n"; 31 | return_part += "Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + filename + "\"\r\n"; 32 | return_part += "Content-Type: " + type + "\r\n\r\n"; 33 | return return_part; 34 | } 35 | 36 | var boundary = Math.random(); 37 | var post_data = []; 38 | if (value) { 39 | post_data.push(new Buffer(encodeFilePart(boundary, value.type, value.keyname, value.valuename), 'ascii')); 40 | post_data.push(new Buffer(value.data, 'utf8')) 41 | } 42 | post_data.push(new Buffer("\r\n--" + boundary + "--"), 'ascii'); 43 | var length = 0; 44 | for (var i = 0; i < post_data.length; i++) { 45 | length += post_data[i].length; 46 | } 47 | var params = { 48 | postdata: post_data, 49 | headers: {'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': length} 50 | }; 51 | return params; 52 | } 53 | 54 | var fileUri = data.fileUri; 55 | var fileUris = fileUri.split(path.sep); 56 | var fileNm = fileUri.split(path.sep)[fileUris.length - 1]; 57 | options.headers['XP-FU-org'] = fileNm; 58 | var result = ''; 59 | var filecontents; 60 | fs.readFile(fileUri, function read(err, filecontents) { 61 | if (err) { 62 | throw err; 63 | } 64 | var type = mime.lookup(fileUri); 65 | var keyname = ""; 66 | if (type.indexOf("/") > 0) { 67 | keyname = type.split("/")[0]; 68 | } 69 | var fileValue = {type: type, keyname: keyname, valuename: fileNm, data: filecontents}; 70 | var headerparams = getFormDataForPost(fileValue); 71 | var totalheaders = headerparams.headers; 72 | for (var key in totalheaders)options.headers[key] = totalheaders[key]; 73 | var request = http.request(options, function (res) { 74 | res.setEncoding('utf8'); 75 | res.on("data", function (chunk) { 76 | result = result + chunk; 77 | }); 78 | res.on("end", function () { 79 | cb(null, JSON.parse(result)); 80 | }); 81 | }).on('error', function (e) { 82 | debug("ajax error: " + e.message); 83 | cb(null, JSON.parse(result)); 84 | }); 85 | for (var i = 0; i < headerparams.postdata.length; i++) { 86 | request.write(headerparams.postdata[i]); 87 | } 88 | request.end(); 89 | }); 90 | }; 91 | exports.get = function (host, port, path, cb) { 92 | var optionsget = {host: host, port: port, path: path, method: 'GET'}; 93 | var reqGet = http.request(optionsget, function (res) { 94 | var result = ""; 95 | res.on('data', function (chunk) { 96 | result += chunk; 97 | }); 98 | res.on('end', function () { 99 | cb(null, JSON.parse(result)); 100 | }); 101 | }); 102 | reqGet.end(); 103 | reqGet.on('error', function (e) { 104 | console.error(e); 105 | }); 106 | }; 107 | exports.deleteFolderRecursive = function (path) { 108 | var self = this; 109 | var files = []; 110 | if (fs.existsSync(path)) { 111 | files = fs.readdirSync(path); 112 | files.forEach(function (file, index) { 113 | var curPath = path + "/" + file; 114 | if (fs.lstatSync(curPath).isDirectory()) { 115 | self.deleteFolderRecursive(curPath); 116 | } else { 117 | fs.unlinkSync(curPath); 118 | } 119 | }); 120 | fs.rmdirSync(path); 121 | } 122 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/xpush'); -------------------------------------------------------------------------------- /lib/daemon/daemon-handler.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var spawn = require('child_process').spawn; 4 | var utils = require('../util/utils'); 5 | 6 | exports.startDaemon = function (options, callback) { 7 | 8 | var monitorFilePath = path.resolve(__dirname) + '/daemon-process'; 9 | 10 | var pidFilePath = utils.getPidFilePath(options['home'], 'CHANNEL', options['port']); 11 | var basePath = utils.getBaseDirPath(options['home']); 12 | var logFilePath = utils.getDaemonLogFilePath(options['home'], 'CHANNEL', options['port']); 13 | 14 | if (fs.existsSync(pidFilePath)) { 15 | 16 | if (callback) callback({ 17 | code: 'PID_EXISTED', 18 | message: ' Check the status of server is running. \n - process id file was already existed : ' + pidFilePath + '\n - log file : ' + logFilePath + '\n' 19 | }); 20 | 21 | } else { 22 | 23 | console.log(' [ monitoring daemon ]'); 24 | console.log(' -- log : ' + logFilePath); 25 | 26 | var 27 | out = fs.openSync(logFilePath, 'a'), 28 | err = fs.openSync(logFilePath, 'a'); 29 | 30 | var paramEnv = { 31 | X_TYPE: 'CHANNEL', 32 | X_PID: process.pid, 33 | X_PATH: basePath, 34 | X_HOST: options['host'], 35 | X_PORT: options['port'], 36 | X_SERVER_NAME : options['serverName'], 37 | X_ZOOKEEPER: options['zookeeper'] 38 | }; 39 | 40 | spawn(process.execPath, [monitorFilePath], { 41 | stdio: ['ignore', out, err], 42 | detached: true, 43 | env: paramEnv 44 | }).unref(); 45 | 46 | if (callback) callback(null); 47 | 48 | } 49 | 50 | }; 51 | -------------------------------------------------------------------------------- /lib/daemon/daemon-process.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var xpush = require('xpush'); 4 | var async = require('async'); 5 | var utils = require('../utils'); 6 | 7 | var envType = process.env.X_TYPE; // SESSION, CHANNEL 8 | var envPid = process.env.X_PID; // Process Id 9 | var envPath = process.env.X_PATH; // home dir path 10 | var envHost = process.env.X_HOST; 11 | var envPort = process.env.X_PORT; 12 | var envZookeeper = process.env.X_ZOOKEEPER; 13 | var envServerName = process.env.X_SERVER_NAME; 14 | 15 | var serverName; 16 | 17 | var pidFilePath = utils.getPidFilePath(envPath, envType, envPort); 18 | 19 | console.log('\n [ Daemon Process got started. ]'); 20 | 21 | console.log(' - TYPE : ' + envType); 22 | console.log(' - PID : ' + envPid + ' (' + pidFilePath + ')'); 23 | console.log(' - PATH : ' + envPath); 24 | console.log(' - HOST : ' + envHost); 25 | console.log(' - PORT : ' + envPort); 26 | console.log(' - SERVER_NAME : ' + envServerName); 27 | 28 | function exit() { 29 | console.warn('What the hell ... ? By the way, bye ... :) '); 30 | process.exit(0); 31 | } 32 | 33 | process.on('SIGINT', exit); 34 | process.on('SIGTERM', exit); 35 | 36 | var afterProcess = function () { 37 | 38 | var zkClient; 39 | var redisClient; 40 | 41 | async.series([ 42 | 43 | function (callback) { 44 | 45 | zkClient = xpush.createZookeeperClient(envZookeeper); 46 | zkClient.once('connected', function () { 47 | 48 | zkClient.getChildren( 49 | '/xpush/servers', 50 | function (error, nodes, stats) { 51 | if (error) { 52 | console.error(error.stack); 53 | callback(error); 54 | return; 55 | } 56 | 57 | var server = envHost + ':' + envPort; 58 | var isExisted = false; 59 | 60 | for (var i = 0; i < nodes.length; i++) { 61 | 62 | var ninfo = nodes[i].split('^'); // 0: name, 1:ip&Port, 2: replicas 63 | 64 | var isEqual = false; 65 | if( server == ninfo[1] ){ 66 | isEqual = true; 67 | 68 | if( envServerName && envServerName != ninfo[0] ){ 69 | isEqual = false; 70 | } 71 | } 72 | 73 | if (isEqual) { // address (1) 74 | 75 | isExisted = true; 76 | 77 | // 1. ServerName 78 | if( !serverName ){ 79 | serverName = ninfo[0]; 80 | } 81 | 82 | // 2. Remove ZNode 83 | zkClient.remove( 84 | '/xpush/servers/' + nodes[i], 85 | -1, 86 | function (err) { 87 | if (err) { 88 | console.log('Failed to remove node due to: %s.', err); 89 | callback(err); 90 | } else { 91 | callback(null); 92 | } 93 | } 94 | ); 95 | 96 | break; 97 | } 98 | 99 | } 100 | 101 | if (!isExisted) { // 존재하지 않으면 다음을 진행 할 수 없음 102 | callback('Zookeeper node was not existed.'); 103 | } 104 | 105 | } 106 | ); 107 | 108 | }); 109 | 110 | zkClient.connect(); 111 | }, 112 | function (callback) { 113 | console.info('"ServerName" on terminating : [' + serverName + ']'); 114 | callback(null); 115 | } 116 | ], function (err, results) { 117 | 118 | if (err) { 119 | console.error(err, results); 120 | } 121 | 122 | process.nextTick(function () { 123 | 124 | zkClient.close(); 125 | 126 | console.log('Bye ... :) '); 127 | 128 | process.exit(0); 129 | 130 | }); 131 | 132 | }); 133 | 134 | 135 | }; 136 | 137 | var checkProcess = function () { 138 | 139 | var isRunning = utils.checkProcess(envPid); 140 | 141 | if (isRunning) { 142 | process.stdout.write('.'); 143 | setTimeout(checkProcess, 700); 144 | 145 | } else { 146 | process.stdout.write('\n'); 147 | afterProcess(); 148 | } 149 | 150 | }; 151 | 152 | var pid = require('./../pid').create(pidFilePath); 153 | pid.removeOnExit(); 154 | 155 | console.log('\n' + new Date()); 156 | checkProcess(); 157 | -------------------------------------------------------------------------------- /lib/node-manager/consistent-hashing.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | 3 | var ConsistentHashing = exports.ConsistentHashing = function (servers, options) { 4 | this.replicas = 160; 5 | this.algorithm = 'md5'; 6 | this.ring = {}; 7 | this.keys = []; 8 | this.nodes = []; 9 | 10 | this.nodemap = {}; 11 | 12 | if (options && options.replicas) this.replicas = options.replicas; 13 | if (options && options.algorithm) this.algorithm = options.algorithm; 14 | 15 | if (!servers) servers = {}; 16 | 17 | for (var key in servers) { 18 | var ninfo = servers[key].split('^'); 19 | this.addNode(ninfo[0], servers[key], ninfo[2]); 20 | } 21 | }; 22 | 23 | 24 | ConsistentHashing.prototype.addNode = function (name, node, re) { 25 | this.nodes.push(node); 26 | 27 | if (re) { 28 | 29 | } else { 30 | re = this.replicas; 31 | } 32 | 33 | for (var i = 0; i < re; i++) { 34 | var key = this.crypto((node.id || node) + ':' + i); 35 | 36 | this.keys.push(key); 37 | this.ring[key] = node; 38 | } 39 | 40 | this.keys.sort(); 41 | this.nodemap[name] = node; 42 | }; 43 | 44 | /* 45 | ConsistentHashing.prototype.removeNode = function(name, weight) { 46 | 47 | var re = this.replicas; 48 | if(weight){ 49 | re = parseInt(re * weight); 50 | } 51 | 52 | var node = this.nodemap[name]; 53 | for (var i = 0; i < this.nodes.length; i++) { 54 | if (this.nodes[i] == node) { 55 | this.nodes.splice(i, 1); 56 | i--; 57 | } 58 | } 59 | 60 | for (var i = 0; i < re; i++) { 61 | var key = this.crypto((node.id || node) + ':' + i); 62 | delete this.ring[key]; 63 | 64 | for (var j = 0; j < this.keys.length; j++) { 65 | if (this.keys[j] == key) { 66 | this.keys.splice(j, 1); 67 | j--; 68 | } 69 | } 70 | } 71 | 72 | delete this.nodemap[name]; 73 | }; */ 74 | 75 | ConsistentHashing.prototype.getNodeMap = function () { 76 | return this.nodemap; 77 | }; 78 | 79 | ConsistentHashing.prototype.getNode = function (key) { 80 | if (this.getRingLength() == 0) return 0; 81 | 82 | var hash = this.crypto(key); 83 | var pos = this.getNodePosition(hash); 84 | 85 | var result = this.ring[this.keys[pos]]; 86 | return {name: result.split('^')[0], url: result.split('^')[1], replicas: result.split('^')[2]}; 87 | 88 | }; 89 | 90 | ConsistentHashing.prototype.getNodeByName = function (name) { 91 | if (this.getRingLength() == 0) return 0; 92 | 93 | var result = this.nodemap[name]; 94 | 95 | if (result) { 96 | return {name: result.split('^')[0], url: result.split('^')[1], replicas: result.split('^')[2]}; 97 | } else { 98 | return null; 99 | } 100 | }; 101 | 102 | ConsistentHashing.prototype.getNodePosition = function (hash) { 103 | var upper = this.getRingLength() - 1; 104 | var lower = 0; 105 | var idx = 0; 106 | var comp = 0; 107 | 108 | if (upper == 0) return 0; 109 | 110 | while (lower <= upper) { 111 | idx = Math.floor((lower + upper) / 2); 112 | comp = this.compare(this.keys[idx], hash); 113 | 114 | if (comp == 0) { 115 | return idx; 116 | } else if (comp > 0) { 117 | upper = idx - 1; 118 | } else { 119 | lower = idx + 1; 120 | } 121 | } 122 | 123 | if (upper < 0) { 124 | upper = this.getRingLength() - 1; 125 | } 126 | 127 | return upper; 128 | }; 129 | 130 | 131 | ConsistentHashing.prototype.getRingLength = function () { 132 | return Object.keys(this.ring).length; 133 | }; 134 | 135 | 136 | ConsistentHashing.prototype.compare = function (v1, v2) { 137 | return v1 > v2 ? 1 : v1 < v2 ? -1 : 0; 138 | }; 139 | 140 | 141 | ConsistentHashing.prototype.crypto = function (str) { 142 | return crypto.createHash(this.algorithm).update(str).digest('hex'); 143 | }; 144 | -------------------------------------------------------------------------------- /lib/node-manager/constants.js: -------------------------------------------------------------------------------- 1 | /** base znode for the xpush server lists. **/ 2 | exports.BASE_ZNODE_PATH = '/xpush'; 3 | exports.SERVERS_PATH = '/servers'; 4 | exports.META_PATH = '/meta'; 5 | exports.APP_PATH = '/app'; 6 | exports.GW_SERVER_PATH = '/session'; 7 | exports.CONFIG_PATH = '/config'; 8 | -------------------------------------------------------------------------------- /lib/node-manager/node-manager.js: -------------------------------------------------------------------------------- 1 | var events = require('events'), 2 | util = require('util'), 3 | zookeeper = require('node-zookeeper-client'), 4 | shortId = require('shortid'), 5 | 6 | constants = require('./constants'), 7 | async = require('async'), 8 | ConsistentHashing = require('./consistent-hashing').ConsistentHashing; 9 | 10 | function wait(msecs) { 11 | var start = new Date().getTime(); 12 | var cur = start; 13 | while (cur - start < msecs) { 14 | cur = new Date().getTime(); 15 | } 16 | } 17 | 18 | /** 19 | * 서버정보를 Zookeeper에 등록 후 watching 하면서 사용가능한 서버를 동적으로 관리하기 위한 모듈 20 | * @module 21 | * @name NodeManager 22 | */ 23 | var NodeManager = exports.NodeManager = function (addr, isWatching, callback) { 24 | 25 | this.address = addr || 'localhost:2181'; 26 | this.ready = false; 27 | this.isWatching = isWatching; 28 | 29 | events.EventEmitter.call(this); 30 | 31 | var self = this; 32 | 33 | this.nodeRing = new ConsistentHashing(); 34 | this.appInfos = {}; 35 | this.servers = {}; 36 | this.serverArray = []; 37 | this.appArray = []; 38 | 39 | this.connected = false; 40 | this.connectionTryNum = 0; 41 | 42 | this._connect(isWatching, callback); 43 | 44 | var connectTry = function () { 45 | 46 | if (!self.connected) { 47 | if (self.connectionTryNum > 3) { 48 | if (callback) callback('ERR-ZOOKEEPER', 'zookeeper - failed to connect to [' + self.address + ']'); 49 | } else { 50 | 51 | if (self.connectionTryNum > 1) console.warn(' (init) ZOOKEEPER connection retry ' + (self.connectionTryNum - 1)); 52 | self.connectionTryNum++; 53 | setTimeout(connectTry, 2000); 54 | } 55 | } 56 | }; 57 | connectTry(); 58 | }; 59 | 60 | 61 | util.inherits(NodeManager, events.EventEmitter); 62 | 63 | NodeManager.prototype._connect = function (isWatching, callback) { 64 | 65 | var self = this; 66 | 67 | this.zkClient = zookeeper.createClient(this.address, {retries: 2}); 68 | 69 | this.zkClient.once('connected', function () { 70 | 71 | self.connected = true; 72 | 73 | // 주키퍼에 노드를 생성함. '/xpush/servers/meta/app/session' 74 | self._initPath('', function () { 75 | self._initPath(constants.SERVERS_PATH, function () { 76 | self._initPath(constants.META_PATH, function () { 77 | self._initPath(constants.META_PATH + constants.APP_PATH, function () { 78 | self._initPath(constants.META_PATH + constants.GW_SERVER_PATH, function () { 79 | 80 | if (isWatching) { 81 | self._watchServerNodes(); 82 | } 83 | 84 | self._watchAppNodes(); 85 | self.ready = true; 86 | 87 | if (callback) callback(); 88 | }); 89 | }); 90 | }); 91 | }); 92 | }); 93 | }); 94 | 95 | this.zkClient.connect(); 96 | }; 97 | 98 | /** 99 | * User 정보가 있는지 확인 후에 있는 경우 수정한다. deviceId를 필수로 입력받아야 한다. 100 | * @name isReady 101 | * @function 102 | */ 103 | NodeManager.prototype.isReady = function () { 104 | return this.ready; 105 | }; 106 | 107 | /** 108 | * Zookeeper에 nodePath가 있는지 확인 후 없는 경우 생성함. 109 | * @private 110 | * @name _initPath 111 | * @function 112 | * @param {string} nodePath - node path 113 | * @param {callback} done - 초기화 후 수행할 callback function 114 | */ 115 | NodeManager.prototype._initPath = function (nodePath, callback) { 116 | 117 | 118 | // TODO server configuration 을 기본 값으로 만들어 줌 ( 없는 경우만, ) - max-connection, expired-time 119 | var self = this; 120 | 121 | self.zkClient.exists( 122 | constants.BASE_ZNODE_PATH + nodePath, 123 | function (error, stat) { 124 | if (error) { 125 | console.error(error.stack); 126 | return; 127 | } 128 | 129 | if (!stat) { 130 | self._createZnode(nodePath, callback); 131 | } else { 132 | if (callback) callback(null); 133 | } 134 | }); 135 | 136 | }; 137 | 138 | /** 139 | * Zookeeper에 node를 persistent 모드로 생성함. 140 | * @private 141 | * @name _createZnode 142 | * @function 143 | * @param {string} nodePath - node path 144 | * @param {callback} done - 초기화 후 수행할 callback function 145 | */ 146 | NodeManager.prototype._createZnode = function (nodePath, callback) { 147 | this.zkClient.create( 148 | constants.BASE_ZNODE_PATH + nodePath, 149 | zookeeper.CreateMode.PERSISTENT, 150 | function (error) { 151 | if (error) { 152 | console.error('Failed to create node: %s due to: %s.', constants.BASE_ZNODE_PATH + nodePath, error); 153 | if (callback) callback(error); 154 | } else { 155 | if (callback) callback(null, constants.BASE_ZNODE_PATH + nodePath); 156 | } 157 | } 158 | ); 159 | }; 160 | 161 | /** 162 | * Zookeeper에 node를 EPHEMERAL 모드로 생성함. 163 | * @private 164 | * @name _createEphemeralZnode 165 | * @function 166 | * @param {string} nodePath - node path 167 | * @param {data} data - node data 168 | * @param {callback} done - 초기화 후 수행할 callback function 169 | */ 170 | NodeManager.prototype._createEphemeralZnode = function (nodePath, data, callback) { 171 | 172 | var nodeData; 173 | 174 | if (data && !callback) { 175 | callback = data; 176 | } else if (data && callback) { 177 | nodeData = new Buffer(data); 178 | } 179 | 180 | this.zkClient.create( 181 | constants.BASE_ZNODE_PATH + nodePath, 182 | nodeData, 183 | zookeeper.CreateMode.EPHEMERAL, 184 | function (error) { 185 | 186 | 187 | if (error) { 188 | if (error.getCode() == zookeeper.Exception.NODE_EXISTS) { 189 | if (callback) callback(null, constants.BASE_ZNODE_PATH + nodePath, data); 190 | } else { 191 | console.error('Failed to create node: %s due to: %s.', constants.BASE_ZNODE_PATH + nodePath, error.getName()); 192 | if (callback) callback(error); 193 | } 194 | 195 | 196 | } else { 197 | if (callback) callback(null, constants.BASE_ZNODE_PATH + nodePath, data); 198 | } 199 | } 200 | ); 201 | }; 202 | 203 | /** 204 | * Zookeeper에 node를 persistent 모드로 생성함. 205 | * @private 206 | * @name _createZnodeWithData 207 | * @function 208 | * @param {string} nodePath - node path 209 | * @param {object} - node에 저장할 data 210 | * @param {callback} callback - 초기화 후 수행할 callback function 211 | */ 212 | NodeManager.prototype._createZnodeWithData = function (nodePath, data, callback) { 213 | 214 | this.zkClient.create( 215 | constants.BASE_ZNODE_PATH + nodePath, 216 | new Buffer(data), 217 | zookeeper.CreateMode.PERSISTENT, 218 | function (error) { 219 | if (error) { 220 | console.error('Failed to create node: %s due to: %s.', constants.BASE_ZNODE_PATH + nodePath, error); 221 | if (callback) callback(error); 222 | } else { 223 | if (callback) callback(null, constants.BASE_ZNODE_PATH + nodePath); 224 | } 225 | } 226 | ); 227 | }; 228 | 229 | /** 230 | * Zookeeper에서 node를 삭제함 231 | * @private 232 | * @name _removeZnode 233 | * @function 234 | * @param {string} nodePath - node path 235 | * @param {callback} callback - 초기화 후 수행할 callback function 236 | */ 237 | NodeManager.prototype._removeZnode = function (nodePath, callback) { 238 | this.zkClient.remove( 239 | constants.BASE_ZNODE_PATH + nodePath, 240 | -1, 241 | function (err) { 242 | if (err) { 243 | console.error('Failed to remove node: %s due to: %s.', constants.BASE_ZNODE_PATH + nodePath, err); 244 | if (callback) callback(err); 245 | } else { 246 | if (callback) callback(null); 247 | } 248 | } 249 | ); 250 | }; 251 | 252 | /** 253 | * Zookeeper에서 node를 삭제함 254 | * @name addAppNode 255 | * @function 256 | * @param {string} appId - application id 257 | * @param {string} appNm - application name 258 | * @param {object} data - json 259 | * @param {callback} callback - 초기화 후 수행할 callback function 260 | */ 261 | NodeManager.prototype.addAppNode = function (appId, appNm, data, callback) { 262 | 263 | if (!appId) { 264 | appId = shortId.generate(); 265 | } 266 | var appInfo = appId + '^' + appNm; 267 | var nodePath = constants.META_PATH + constants.APP_PATH + '/' + appInfo; 268 | data.id = appId; 269 | 270 | var self = this; 271 | 272 | self.zkClient.exists( 273 | constants.BASE_ZNODE_PATH + nodePath, 274 | function (error, stat) { 275 | if (error) { 276 | console.error(error.stack); 277 | return; 278 | } 279 | 280 | if (!stat) { 281 | self._createZnodeWithData(nodePath, JSON.stringify(data), callback); 282 | } else { 283 | if (callback) callback(null); 284 | } 285 | } 286 | ); 287 | }; 288 | 289 | /** 290 | * Zookeeper에서 서버 node를 등록함 291 | * @name addServerNode 292 | * @function 293 | * @param {config} address - 서버의 config 294 | * @param {replicas} port - 서버의 port 295 | * @param {callback} callback - 초기화 후 수행할 callback function 296 | */ 297 | NodeManager.prototype.addServerNode = function (config, replicas, callback) { 298 | 299 | var self = this; 300 | var address = config.host; 301 | var port = config.port; 302 | var serverName = config.serverName; 303 | 304 | this.zkClient.getChildren( 305 | constants.BASE_ZNODE_PATH + constants.SERVERS_PATH, 306 | function (error, nodes, stats) { 307 | if (error) { 308 | console.error(error.stack); 309 | callback(error); 310 | return; 311 | } 312 | 313 | var server = address + ':' + port; 314 | var isExisted = false; 315 | var names = []; 316 | 317 | var existedPathName; 318 | 319 | for (var i = 0; i < nodes.length; i++) { 320 | 321 | var ninfo = nodes[i].split('^'); // 0: name, 1:ip&Port, 2: replicas 322 | 323 | if (server == ninfo[1]) { // address (1) 324 | existedPathName = nodes[i]; 325 | isExisted = true; 326 | break; 327 | } 328 | 329 | if ( typeof names[inx] == 'number' ){ 330 | names.push(Number(ninfo[0])); // server name (0) 331 | } else { 332 | names.push(ninfo[0]); // server name (0) 333 | } 334 | 335 | } 336 | 337 | if (!isExisted) { 338 | 339 | if( !serverName ){ 340 | serverName = 10; 341 | if (names.length > 0) { 342 | var maxBefore = 0; 343 | for( var inx in names ){ 344 | if ( typeof names[inx] == 'number' ){ 345 | maxBefore = names[inx] ; 346 | } 347 | } 348 | 349 | serverName = maxBefore + Math.floor(Math.random() * (20 - 10 + 1)) + 10; 350 | } 351 | } 352 | 353 | var nodePath = constants.SERVERS_PATH + '/' + serverName + '^' + server; 354 | 355 | self._createEphemeralZnode(nodePath, replicas + "", callback); 356 | 357 | } else { 358 | if (callback) callback(null, constants.SERVERS_PATH + '/' + existedPathName, replicas); 359 | } 360 | } 361 | ); 362 | }; 363 | 364 | /** 365 | * Zookeeper에 node Data를 변경함 366 | * @private 367 | * @name _setNodeData 368 | * @function 369 | * @param {string} path - path 370 | * @param {data} data - node data 371 | * @param {callback} done - 초기화 후 수행할 callback function 372 | */ 373 | NodeManager.prototype._setNodeData = function (path, data, callback) { 374 | 375 | this.zkClient.setData( 376 | path, 377 | new Buffer(data + ""), 378 | function (error, stat) { 379 | if (error) { 380 | console.error('Failed to set node data: %s due to: %s.', path, error.getName()); 381 | if (callback) callback(error); 382 | 383 | } else { 384 | if (callback) callback(null, path, data); 385 | } 386 | } 387 | ); 388 | }; 389 | 390 | /** 391 | * Zookeeper에서 서버 node를 삭제함 392 | * @name removeServerNode 393 | * @function 394 | * @param {string} address - 서버의 address 395 | * @param {number} port - 서버의 port 396 | * @param {callback} callback - 초기화 후 수행할 callback function 397 | */ 398 | NodeManager.prototype.removeServerNode = function (address, port, callback) { 399 | var self = this; 400 | 401 | this.zkClient.getChildren( 402 | constants.BASE_ZNODE_PATH + constants.SERVERS_PATH, 403 | function (error, nodes, stats) { 404 | if (error) { 405 | console.error(error.stack); 406 | return; 407 | } 408 | 409 | nodes.forEach(function (node) { 410 | var n = node.split('^')[0]; 411 | var s = node.split('^')[1]; 412 | if (s === address + ":" + port) { 413 | self._removeZnode(constants.SERVERS_PATH + "/" + node, function (err) { 414 | if (err) { 415 | if (callback) callback(err); 416 | } else { 417 | callback(null); 418 | } 419 | }); 420 | } 421 | }); 422 | } 423 | ); 424 | }; 425 | 426 | /** 427 | * Zookeeper에 등록된 서버 노드를 watching하여 변경이 있을 경우, 새로운 Consisstent Hash를 생성한다. /xpush/servers 428 | * @private 429 | * @name _watchServerNodes 430 | * @function 431 | */ 432 | NodeManager.prototype._watchServerNodes = function () { 433 | 434 | var self = this; 435 | this.zkClient.getChildren( 436 | constants.BASE_ZNODE_PATH + constants.SERVERS_PATH, 437 | function (event) { 438 | //console.log(' Got watcher event: ', event); 439 | self._watchServerNodes(); 440 | }, 441 | function (error, children, stat) { 442 | if (error) { 443 | 444 | console.warn('Failed to list children due to: %s.', error); 445 | if (error.getCode() == zookeeper.Exception.CONNECTION_LOSS) { // TODO 나중에 좀더 확인이 필요함! 446 | self.zkClient.close(); 447 | self._connect(self.isWatching, function (err) { 448 | if (err) console.error(err); 449 | }) 450 | } 451 | 452 | } else { 453 | 454 | var max = children.length; 455 | 456 | var nodeTask = function (taskId, value, callback) { 457 | 458 | self._getServerNode(children[taskId], function () { 459 | taskId++ 460 | if (taskId < max) { 461 | function_array.splice(function_array.length - 1, 0, nodeTask); 462 | } 463 | callback(null, taskId, ++value); 464 | }); 465 | }; 466 | 467 | var startTask = function (callback) { 468 | self.servers = {}; 469 | function_array.splice(function_array.length - 1, 0, nodeTask); 470 | callback(null, 0, 0); 471 | }; 472 | 473 | var finalTask = function (taskId, value, callback) { 474 | callback(null, value); 475 | }; 476 | 477 | var function_array = [startTask, finalTask]; 478 | 479 | if (max > 0) { 480 | 481 | console.log(' [event] server nodes [' + max + '] : ' + children); 482 | 483 | async.waterfall(function_array, function (err, result) { 484 | this.serverArray = []; 485 | this.serverArray = children; 486 | self.nodeRing = new ConsistentHashing(self.servers); 487 | }); 488 | } else { 489 | 490 | // reset nodeRing 491 | self.nodeRing = new ConsistentHashing(); 492 | console.warn(' [event] server nodes [0] : NOT EXISTED'); 493 | } 494 | } 495 | } 496 | ); 497 | }; 498 | 499 | /** 500 | * Zookeeper에 등록된 Application 노드를 watching하여 변경이 있을 경우 appInfos를 수정한다. 501 | * @private 502 | * @name _watchAppNodes 503 | * @function 504 | */ 505 | NodeManager.prototype._watchAppNodes = function () { 506 | 507 | var self = this; 508 | this.zkClient.getChildren( 509 | constants.BASE_ZNODE_PATH + constants.META_PATH + constants.APP_PATH, 510 | function (event) { 511 | //console.log(' Got app watcher event: %s', event); 512 | self._watchAppNodes(); 513 | }, 514 | function (error, children, stat) { 515 | if (error) { 516 | console.error('Failed to app list children due to: %s.', error); 517 | } else { 518 | 519 | var max = children.length; 520 | 521 | var nodeTask = function (taskId, value, callback) { 522 | 523 | self._getAppNode(children[taskId], function () { 524 | taskId++ 525 | if (taskId < max) { 526 | function_array.splice(function_array.length - 1, 0, nodeTask); 527 | } 528 | callback(null, taskId, ++value); 529 | }); 530 | }; 531 | 532 | var startTask = function (callback) { 533 | function_array.splice(function_array.length - 1, 0, nodeTask); 534 | callback(null, 0, 0); 535 | }; 536 | 537 | var finalTask = function (taskId, value, callback) { 538 | callback(null, value); 539 | }; 540 | 541 | var function_array = [startTask, finalTask]; 542 | 543 | if (max > 0) { 544 | async.waterfall(function_array, function (err, result) { 545 | this.appArray = []; 546 | this.appArray = children; 547 | }); 548 | } 549 | 550 | } 551 | } 552 | ); 553 | }; 554 | 555 | /** 556 | * Zookeeper에서 서버 node 정보를 가져옴 557 | * @name _getServerNode 558 | * @function 559 | * @param {number} childPath - childPath 560 | * @param {callback} callback - 초기화 후 수행할 callback function 561 | */ 562 | NodeManager.prototype._getServerNode = function (childPath, cb) { 563 | var self = this; 564 | var path = constants.BASE_ZNODE_PATH + constants.SERVERS_PATH + '/' + childPath; 565 | 566 | var _w = function (event) { 567 | //NODE_DATA_CHANGED 568 | if (event.type == 3) { 569 | self._getServerNode(childPath); 570 | } 571 | }; 572 | 573 | if (cb && self.serverArray.indexOf(childPath) > -1) { 574 | _w = undefined; 575 | } 576 | 577 | self.zkClient.getData(path, 578 | _w, 579 | function (error, data, stat) { 580 | 581 | if (error) { 582 | console.error('Fail retrieve server datas: %s.', error); 583 | } else { 584 | 585 | var replicas = 160; 586 | if (data !== undefined && data !== null) { 587 | replicas = data.toString('utf8'); 588 | } 589 | 590 | self.servers[childPath] = childPath + "^" + replicas; 591 | 592 | if (self.serverArray.indexOf(childPath) < 0) { 593 | self.serverArray.push(childPath); 594 | } 595 | 596 | if (cb) { 597 | cb(); 598 | } else { 599 | self.nodeRing = new ConsistentHashing(self.servers); 600 | //console.log(self.nodeRing); 601 | } 602 | } 603 | } 604 | ); 605 | }; 606 | 607 | /** 608 | * Zookeeper에서 app node 정보를 가져옴 609 | * @name _getAppNode 610 | * @function 611 | * @param {string} childPath - childPath 612 | */ 613 | NodeManager.prototype._getAppNode = function (childPath, cb) { 614 | var self = this; 615 | var path = constants.BASE_ZNODE_PATH + constants.META_PATH + constants.APP_PATH + '/' + childPath; 616 | 617 | var _w = function (event) { 618 | //NODE_DATA_CHANGED 619 | if (event.type == 3) { 620 | self._getAppNode(childPath); 621 | } 622 | }; 623 | 624 | if (cb && self.appArray.indexOf(childPath) > -1) { 625 | _w = undefined; 626 | } 627 | 628 | self.zkClient.getData(path, 629 | _w, 630 | function (error, data, stat) { 631 | 632 | if (error) { 633 | console.error('Fail retrieve app datas: %s.', error); 634 | } 635 | 636 | var tmp = data.toString('utf8'); 637 | var appDatas = JSON.parse(tmp); 638 | 639 | if (self.appArray.indexOf(childPath) < 0) { 640 | self.appArray.push(childPath); 641 | } 642 | 643 | self.appInfos[appDatas.id] = appDatas; 644 | 645 | if (cb) { 646 | cb(); 647 | } 648 | } 649 | ); 650 | }; 651 | 652 | /** 653 | * Zookeeper에서 config node 정보를 가져옴 654 | * @name _getConfigNode 655 | * @function 656 | * @param {string} key - key 657 | */ 658 | NodeManager.prototype._getConfigNode = function (key, cb) { 659 | var self = this; 660 | var path = constants.BASE_ZNODE_PATH + constants.CONFIG_PATH + '/' + key; 661 | 662 | var _w = function (event) { 663 | //NODE_DATA_CHANGED 664 | if (event.type == 3) { 665 | self._getConfigNode(key, cb); 666 | } 667 | }; 668 | 669 | self.zkClient.getData(path, 670 | _w, 671 | function (error, data, stat) { 672 | 673 | if (error) { 674 | 675 | if (error.name == "NO_NODE") { 676 | if (cb) { 677 | cb(configData); 678 | } 679 | } else { 680 | console.error(error); 681 | } 682 | } 683 | 684 | if (data) { 685 | 686 | var tmp = data.toString('utf8'); 687 | var configData = JSON.parse(tmp); 688 | 689 | if (cb) { 690 | cb(configData); 691 | } 692 | 693 | } else { 694 | if (cb) { 695 | cb(); 696 | } 697 | } 698 | } 699 | ); 700 | }; 701 | 702 | NodeManager.prototype.getServerNode = function (key) { 703 | return this.nodeRing.getNode(key); 704 | }; 705 | 706 | NodeManager.prototype.getServerNodeByName = function (name) { 707 | return this.nodeRing.getNodeByName(name); 708 | }; 709 | 710 | NodeManager.prototype.createPath = function (path, callback) { 711 | this._createZnode(path, callback); 712 | }; 713 | 714 | NodeManager.prototype.createEphemeralPath = function (path, data, callback) { 715 | this._createEphemeralZnode(path, data, callback); 716 | }; 717 | 718 | NodeManager.prototype.setNodeData = function (path, data, callback) { 719 | this._setNodeData(path, data, callback); 720 | }; 721 | 722 | NodeManager.prototype.getNodeMap = function () { 723 | return this.nodeRing.getNodeMap(); 724 | }; 725 | 726 | NodeManager.prototype.removePath = function (path, callback) { 727 | this._removeZnode(path, callback); 728 | }; 729 | 730 | NodeManager.prototype.getAppInfo = function (id) { 731 | return this.appInfos[id]; 732 | }; 733 | 734 | NodeManager.prototype.getAppInfos = function () { 735 | return this.appInfos; 736 | }; 737 | 738 | NodeManager.prototype.getConfigInfo = function (key, cb) { 739 | return this._getConfigNode(key, cb); 740 | }; 741 | -------------------------------------------------------------------------------- /lib/pid.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | function Pid(path) { 4 | this.path_ = path; 5 | } 6 | 7 | Pid.prototype.remove = function () { 8 | return module.exports.remove(this.path_); 9 | }; 10 | 11 | Pid.prototype.removeOnExit = function () { 12 | process.on('exit', this.remove.bind(this)); 13 | }; 14 | 15 | function create(path, force) { 16 | 17 | try { 18 | 19 | var pid = new Buffer(process.pid + '\n'); 20 | 21 | var fd = fs.openSync(path, force ? 'w' : 'wx'); 22 | var offset = 0; 23 | 24 | while (offset < pid.length) { 25 | offset += fs.writeSync(fd, pid, offset, pid.length - offset); 26 | } 27 | 28 | fs.closeSync(fd); 29 | 30 | } catch (e) { 31 | console.error('\n ** Error on startup ** \n', e, '\n'); 32 | process.exit(0); //throw new Error(e); 33 | } 34 | 35 | return new Pid(path); 36 | } 37 | 38 | function remove(path) { 39 | try { 40 | fs.unlinkSync(path); 41 | return true; 42 | } catch (err) { 43 | return false; 44 | } 45 | } 46 | 47 | module.exports.create = create; 48 | module.exports.remove = remove; -------------------------------------------------------------------------------- /lib/session-manager/redis-manager.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | redis = require('redis'), 3 | async = require('async'), 4 | RedisShard = require('redis-shard'); 5 | 6 | module.exports = function RedisManager(config) { 7 | 8 | var self = {}; 9 | var clients = []; 10 | var masterAddr; 11 | var hasSlaveNode = false; 12 | 13 | if (config) { 14 | if (typeof config === 'string' || config instanceof String) { 15 | masterAddr = config; 16 | 17 | } else { 18 | 19 | if (config.address) { 20 | if (config.address.master) { 21 | masterAddr = config.address.master; 22 | } else { 23 | masterAddr = config.address; 24 | } 25 | 26 | hasSlaveNode = config.address && config.address.slave; 27 | } 28 | 29 | } 30 | } 31 | 32 | if(!masterAddr) masterAddr = "127.0.0.1:6379"; 33 | 34 | var client = redis.createClient(masterAddr.split(':')[1], masterAddr.split(':')[0]); 35 | clients.push(client); 36 | 37 | var shardedClient; 38 | 39 | if (hasSlaveNode) { 40 | var options = {servers: config.address.slave}; 41 | shardedClient = new RedisShard(options); 42 | clients.push(shardedClient); 43 | } 44 | 45 | var WRITES = [ 46 | "del", "publish", "subscribe", "hset", "hdel", "set", "expire", "hmset", "sadd", "smembers", "hsetnx", "incr", "decr", "hincrby", "lpush", "lrem", "rpop", "zadd", "zrem", "zincrby" 47 | ]; 48 | 49 | WRITES.forEach(function (command) { 50 | self[command] = function () { 51 | client[command].apply(client, arguments); 52 | }; 53 | }); 54 | 55 | var READS = [ 56 | "hget", "hgetall", "get", "hlen", "hscan", "hexists", "mget", "exists", "llen", "lrange", "zcard", "zscore", "zrank", "zrange", "zscore" 57 | ]; 58 | 59 | READS.forEach(function (command) { 60 | self[command] = function () { 61 | if (shardedClient) { 62 | shardedClient[command].apply(client, arguments); 63 | } else { 64 | client[command].apply(client, arguments); 65 | } 66 | }; 67 | }); 68 | 69 | self.on = function (event, listener) { 70 | clients.forEach(function (c) { 71 | c.on(event, function () { 72 | // append server as last arg passed to listener 73 | var args = Array.prototype.slice.call(arguments); 74 | listener.apply(undefined, args); 75 | }); 76 | }); 77 | }; 78 | 79 | // Note: listener will fire once per shard, not once per cluster 80 | self.once = function (event, listener) { 81 | 82 | async.parallel( 83 | [ 84 | function (callback) { 85 | client.once(event, function () { 86 | callback(undefined, 'one'); 87 | }); 88 | }, 89 | function (callback) { 90 | if (hasSlaveNode) { 91 | var connected = 0; 92 | shardedClient.once(event, function () { 93 | connected++; 94 | if (connected == config.address.slave.length) { 95 | callback(undefined, 'two'); 96 | } 97 | }); 98 | } else { 99 | callback(undefined, 'two'); 100 | } 101 | } 102 | ], 103 | function (err, results) { 104 | var args = Array.prototype.slice.call(arguments).concat("result"); 105 | listener.apply(undefined, args); 106 | } 107 | ); 108 | }; 109 | 110 | self.batchWrite = function(){ 111 | return client.batch(); 112 | }; 113 | 114 | self.batchRead = function(){ 115 | if( shardedClient ){ 116 | return shardedClient.batch(); 117 | } else { 118 | return client.batch(); 119 | } 120 | }; 121 | 122 | return self; 123 | }; 124 | -------------------------------------------------------------------------------- /lib/session-manager/redis-scan.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | 3 | function genericScan(redis, cmd, key, pattern, each_callback, done_callback) { 4 | var iter = '0'; 5 | async.doWhilst( 6 | function (acb) { 7 | //scan with the current iterator, matching the given pattern 8 | var args = [iter]; 9 | if (cmd === 'SCAN') { 10 | if (pattern) { 11 | args = args.concat(['MATCH', pattern]); 12 | } 13 | } else if (cmd === 'HSCAN') { 14 | args = [key].concat(args); 15 | if (pattern) { 16 | args = args.concat(['MATCH', pattern]); 17 | } 18 | } else { 19 | args = [key].concat(args); 20 | } 21 | console.log(cmd, args); 22 | redis.send_command(cmd, args, function (err, result) { 23 | var idx = 0; 24 | var keys; 25 | if (err) { 26 | acb(err); 27 | } else { 28 | //update the iterator 29 | iter = result[0]; 30 | //each key, limit to 5 pending callbacks at a time 31 | if (['SCAN', 'SSCAN'].indexOf(cmd) !== -1) { 32 | async.eachSeries(result[1], function (subkey, ecb) { 33 | if (cmd === 'SCAN') { 34 | redis.type(subkey, function (err, sresult) { 35 | var value; 36 | if (err) { 37 | ecb(err); 38 | } else { 39 | if (sresult === 'string') { 40 | redis.get(subkey, function (err, value) { 41 | if (err) { 42 | ecb(err); 43 | } else { 44 | each_callback('string', subkey, null, null, value, ecb); 45 | } 46 | }); 47 | } else if (sresult === 'hash') { 48 | genericScan(redis, 'HSCAN', subkey, null, each_callback, ecb); 49 | } else if (sresult === 'set') { 50 | genericScan(redis, 'SSCAN', subkey, null, each_callback, ecb); 51 | } else if (sresult === 'zset') { 52 | genericScan(redis, 'ZSCAN', subkey, null, each_callback, ecb); 53 | } else if (sresult === 'list') { 54 | //each_callback('list', subkey, null, null, ecb); 55 | redis.llen(subkey, function (err, length) { 56 | var idx = 0; 57 | length = parseInt(length); 58 | if (err) { 59 | ecb(err); 60 | } else { 61 | async.doWhilst( 62 | function (wcb) { 63 | redis.lindex(subkey, idx, function (err, value) { 64 | each_callback('list', subkey, idx, length, value, wcb); 65 | }); 66 | }, 67 | function () { 68 | idx++; 69 | return idx < length; 70 | }, 71 | function (err) { 72 | ecb(err) 73 | } 74 | ); 75 | } 76 | }); 77 | } 78 | } 79 | }); 80 | } else if (cmd === 'SSCAN') { 81 | each_callback('set', key, idx, null, subkey, ecb); 82 | } 83 | idx++; 84 | }, 85 | function (err) { 86 | //done with this scan iterator; on to the next 87 | acb(err); 88 | }); 89 | } else { 90 | var idx = 0; 91 | async.doWhilst( 92 | function (ecb) { 93 | var subkey = result[1][idx]; 94 | var value = result[1][idx + 1]; 95 | if (cmd === 'HSCAN') { 96 | each_callback('hash', key, subkey, null, value, ecb); 97 | } else if (cmd === 'ZSCAN') { 98 | each_callback('zset', key, value, null, subkey, ecb); 99 | } 100 | }, 101 | function () { 102 | idx += 2; 103 | return idx < result[1].length; 104 | }, 105 | function (err) { 106 | acb(err); 107 | } 108 | ); 109 | } 110 | } 111 | }); 112 | }, 113 | //test to see if iterator is done 114 | function () { 115 | return iter != '0'; 116 | }, 117 | //done 118 | function (err) { 119 | done_callback(err); 120 | } 121 | ); 122 | } 123 | 124 | module.exports = function (args) { 125 | genericScan(args.redis, args.cmd || 'SCAN', args.key || null, args.pattern, args.each_callback, args.done_callback); 126 | }; -------------------------------------------------------------------------------- /lib/session-manager/session-manager.js: -------------------------------------------------------------------------------- 1 | var events = require('events'), 2 | util = require('util'), 3 | redis = require('redis'), 4 | redisScan = require('./redis-scan'), 5 | RedisManager = require('./redis-manager'); 6 | 7 | var Constants = { 8 | XPUSH_CONNECTION: 'XPUSH_C' 9 | } 10 | 11 | var SessionManager = exports.SessionManager = function (config, callback) { 12 | 13 | this.conf = {}; 14 | 15 | if (typeof(config) == 'function' && !callback) { 16 | callback = config; 17 | 18 | // default configurations 19 | //this.conf.expire = 120; // redis expire TTL (seconds) 20 | 21 | } else { 22 | if (config) this.conf = config; 23 | } 24 | 25 | this.redisClient = new RedisManager(this.conf); 26 | 27 | events.EventEmitter.call(this); 28 | 29 | this.redisClient.on("error", function (err) { 30 | console.error("Redis error encountered : " + err); 31 | }); 32 | 33 | this.redisClient.on("end", function (err) { 34 | console.warn("Redis connection closed"); 35 | if (callback) callback('ERR-REDIS', 'failed to connect to Redis server(s). '); 36 | }); 37 | 38 | this.redisClient.once("connect", function () { 39 | if (callback) callback(null); 40 | }); 41 | }; 42 | 43 | util.inherits(SessionManager, events.EventEmitter); 44 | 45 | 46 | /** 47 | * Get the server number according to channel name from redis hash table. 48 | 49 | * @name retrieve 50 | * @function 51 | * @param {string} app - application key 52 | * @param {string} channel - channel name 53 | * @param {callback} callback - callback function 54 | */ 55 | SessionManager.prototype.retrieveConnectedNode = function (app, channel, callback) { 56 | this.redisClient.hgetall(Constants.XPUSH_CONNECTION + ':' + app + ':' + channel, function (err, res) { 57 | callback(res); 58 | }); 59 | }; 60 | 61 | /** 62 | * Update connection informations into redis server. 63 | * If the number of connections in this channel is ZERO, delete data from redis hash table. 64 | * 65 | * @name update 66 | * @function 67 | * @param {string} app - application key 68 | * @param {string} channel - channel name 69 | * @param {string} server - server number (auth-generated into zookeeper) 70 | * @param {number} count - the number of connections 71 | * 72 | */ 73 | SessionManager.prototype.updateConnectedNode = function (app, channel, server, count, callback) { 74 | 75 | var hkey = Constants.XPUSH_CONNECTION + ':' + app + ':' + channel; 76 | 77 | //console.log(hkey, server, count, this.conf.expire); 78 | 79 | if (callback) { 80 | if (count > 0) { 81 | this.redisClient.hset(hkey, server, count, callback); 82 | if (this.conf && this.conf.expire) { 83 | this.redisClient.expire(hkey, this.conf.expire, function (err, res) { 84 | }); 85 | } 86 | } else { 87 | this.redisClient.hdel(hkey, server, callback); 88 | } 89 | 90 | } else { 91 | if (count > 0) { 92 | this.redisClient.hset(hkey, server, count); 93 | if (this.conf && this.conf.expire) { 94 | this.redisClient.expire(hkey, this.conf.expire, function (err, res) { 95 | }); 96 | } 97 | 98 | } else { 99 | this.redisClient.hdel(hkey, server); 100 | } 101 | } 102 | 103 | }; 104 | 105 | 106 | /** 107 | * Remove server datas from redis hash table 108 | 109 | * @name remove 110 | * @function 111 | * @param {string} app - application key 112 | * @param {string} channel - channel name 113 | */ 114 | SessionManager.prototype.remove = function (app, channel) { 115 | this.redisClient.hdel(app, channel); 116 | }; 117 | 118 | 119 | /** 120 | * Publish data to another server. 121 | * @name publish 122 | * @function 123 | * @param {string} server - server number 124 | * @param {object} dataObj - Data to send 125 | */ 126 | SessionManager.prototype.publish = function (server, dataObj) { 127 | 128 | console.log('#### REDIS Publish : ' + 'C-' + server + '-' + JSON.stringify(dataObj)); 129 | 130 | this.redisClient.publish('C-' + server, JSON.stringify(dataObj)); 131 | 132 | }; 133 | 134 | /** 135 | * Retrieve channel list with hscan @ TODO 확인해봐야 함. 136 | * 137 | * @name retrieveChannelList 138 | * @function 139 | * @param {string} key - application key 140 | * @param {string} pattern - channel name 141 | * @param {callback} callback - callback function 142 | */ 143 | SessionManager.prototype.retrieveChannelList = function (key, pattern, callback) { 144 | var reVa = []; 145 | redisScan({ 146 | redis: this.redisClient, 147 | cmd: 'HSCAN', 148 | key: key, 149 | pattern: pattern, 150 | each_callback: function (type, key, subkey, cursor, value, cb) { 151 | //console.log(key,subkey,value); 152 | if (subkey) { 153 | reVa.push({key: subkey, value: value}); 154 | } 155 | cb(); 156 | }, 157 | done_callback: function (err) { 158 | callback(err, reVa); 159 | } 160 | }); 161 | }; 162 | -------------------------------------------------------------------------------- /lib/session-manager/session-subscriber.js: -------------------------------------------------------------------------------- 1 | var events = require('events'), 2 | util = require('util'), 3 | RedisManager = require('./redis-manager'); 4 | 5 | var SessionSubscriber = exports.SessionSubscriber = function (config, server, callback) { 6 | 7 | this.redisClient = new RedisManager(config); 8 | 9 | events.EventEmitter.call(this); 10 | 11 | var self = this; 12 | 13 | this.redisClient.on("error", function (err) { 14 | console.error("Redis Subscriber error encountered : " + err); 15 | }); 16 | 17 | this.redisClient.on("end", function () { 18 | console.warn("Redis Subscriber connection closed"); 19 | }); 20 | 21 | this.redisClient.once("connect", function () { 22 | 23 | // 해당 서버로 pulbish 되는 메세지를 subscribe 한다. 24 | self.redisClient.subscribe('C-' + server); 25 | // console.info(' (init) REDIS subscribe channel[C-'+server+']'); 26 | if (callback) callback(null); 27 | 28 | }); 29 | 30 | this.redisClient.on('message', function (c, data) { 31 | 32 | var dataObj = JSON.parse(data); 33 | 34 | console.log('### REDIS Subscribe event : ' + c, dataObj); 35 | 36 | // publish 받은 data를 현재 server에 emit한다. 37 | self.emit('_message', dataObj); 38 | 39 | }); 40 | 41 | }; 42 | 43 | util.inherits(SessionSubscriber, events.EventEmitter); 44 | -------------------------------------------------------------------------------- /lib/util/global-config.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var util = require('util'); 3 | var zookeeper = require('node-zookeeper-client'); 4 | 5 | /** base znode for the xpush server lists. **/ 6 | var CONSTANTS = { 7 | CONFIG_PATH: '/config' 8 | }; 9 | 10 | var ALL_EVENT_LISTEN_KEY = "41caa633d99dd3df7910951bc26fab7e"; 11 | 12 | /** 13 | * Zookeeper 를 이용해서 config 정보들을 관리하고 사용할 수 있는 모듈 14 | * @name Configure 15 | */ 16 | var Configure = function () { 17 | this._configureData = {}; 18 | this._beforeConnected = []; // function, data 19 | }; 20 | util.inherits(Configure, EventEmitter); 21 | 22 | /** 23 | * Zookeeper Client를 지정해서 사용, 24 | * @public 25 | * @name setZKClient 26 | * @function 27 | * @param {object} client - Zookeeper client 28 | * @param {string} serviceName - Zookeeper 에서 사용될 root node 이름 29 | */ 30 | Configure.prototype.setZKClient = function (client, serviceName) { 31 | var self = this; 32 | if (!client) { 33 | return console.log("No Zookeeper Client in Configuration!"); 34 | } 35 | if (!serviceName || serviceName.length < 1) { 36 | return console.log("You must pass argument2(ServiceName)!"); 37 | } 38 | 39 | if (serviceName[0] != '/') { 40 | serviceName = '/' + serviceName 41 | } 42 | 43 | this.serviceName = serviceName; 44 | this.basePath = this.serviceName + CONSTANTS.CONFIG_PATH; 45 | 46 | if (!self.zkClient) { 47 | self.zkClient = client; 48 | self._init(true); 49 | } 50 | }; 51 | 52 | Configure.prototype.setZKClientOnce = function (client, serviceName) { 53 | var self = this; 54 | if (!client) { 55 | return console.log("No Zookeeper Client in Configuration!"); 56 | } 57 | if (!serviceName || serviceName.length < 1) { 58 | return console.log("You must pass argument2(ServiceName)!"); 59 | } 60 | 61 | if (serviceName[0] != '/') { 62 | serviceName = '/' + serviceName 63 | } 64 | ; 65 | 66 | this.serviceName = serviceName; 67 | this.basePath = this.serviceName + CONSTANTS.CONFIG_PATH; 68 | 69 | if (!self.zkClient) { 70 | self.zkClient = client; 71 | self._init(false); 72 | } 73 | }; 74 | 75 | /** 76 | * Zookeeper client 가 설정되면 connection유무를 판단하여 데이터들을 관리할 준비를 한다. 77 | * @private 78 | * @name _init 79 | * @function 80 | */ 81 | Configure.prototype._init = function (isWatching) { 82 | var self = this; 83 | var state = self.zkClient.getState(); 84 | 85 | if (state == zookeeper.State.SYNC_CONNECTED) { 86 | self._initFullPath(function () { 87 | if (isWatching) self._globalWatchConfig(); 88 | }); 89 | } else { 90 | self.zkClient.once('connected', function () { 91 | self._initFullPath(function () { 92 | if (isWatching) self._globalWatchConfig(); 93 | self._executeStack(); 94 | }); 95 | self.zkClient.once('disconnected', function () { 96 | console.log("Configuration Zookeeper disconnected!"); 97 | self._init(); 98 | }); 99 | }); 100 | } 101 | } 102 | 103 | /** 104 | * Zookeeper client 가 접속 되기 전에 뭔가 조작한다면 미리 예방해 둔다. 105 | * @private 106 | * @name _executeStack 107 | * @function 108 | */ 109 | Configure.prototype._executeStack = function () { 110 | var self = this; 111 | while (self._beforeConnected.length > 0) { 112 | var item = self._beforeConnected.splice(0, 1)[0]; 113 | console.log(item); 114 | switch (item.method) { 115 | case "get": 116 | self.getConfig(item.k, item.cb); 117 | break; 118 | 119 | case "set": 120 | self.setConfig(item.k, item.v, item.cb); 121 | break; 122 | 123 | case "setOnce": 124 | self.setConfigOnce(item.k, item.v, item.cb); 125 | break; 126 | 127 | case "remove": 128 | self.deleteConfig(item.k, item.cb); 129 | break; 130 | } 131 | } 132 | } 133 | 134 | /** 135 | * Config 정보를 저장한다. 136 | * @public 137 | * @name setConfig 138 | * @function 139 | * @param {string} key - config key 140 | * @param {string} value - config value 141 | * @param {function} cb - 설정이 완료되면 에러 유무를 호출 142 | */ 143 | Configure.prototype.setConfig = function (key, value, cb) { 144 | var self = this; 145 | var newPath = this.basePath + '/' + key; 146 | 147 | if (self.zkClient.getState() != zookeeper.State.SYNC_CONNECTED) { 148 | return self._beforeConnected.push({method: "set", cb: cb, k: key, v: value}); 149 | //return console.log('Fail set config data ( zookeeper is offline )'); 150 | } 151 | console.log("============== setConfig"); 152 | self.zkClient.exists( 153 | newPath, 154 | function (error, stat) { 155 | if (error) { 156 | console.log(error.stack); 157 | return; 158 | } 159 | 160 | if (!stat) { 161 | self._createZnodeWithData(newPath, JSON.stringify(value), function (err) { 162 | //self._configureData.key = value; 163 | self._watchConfig(key); 164 | if (cb) cb(err); 165 | }); 166 | } else { 167 | self.zkClient.setData(newPath, new Buffer(JSON.stringify(value)), -1, 168 | function (error, stat) { 169 | if (error) { 170 | console.log(error.stack); 171 | return; 172 | } 173 | self._watchConfig(key); 174 | if (cb) cb(error); 175 | }); 176 | } 177 | } 178 | ); 179 | }; 180 | 181 | Configure.prototype.setConfigOnce = function (key, value, cb) { 182 | var self = this; 183 | var newPath = this.basePath + '/' + key; 184 | 185 | if (self.zkClient.getState() != zookeeper.State.SYNC_CONNECTED) { 186 | return self._beforeConnected.push({method: "setOnce", cb: cb, k: key, v: value}); 187 | } 188 | self.zkClient.exists( 189 | newPath, 190 | function (error, stat) { 191 | if (error) { 192 | console.log(error.stack); 193 | return; 194 | } 195 | 196 | if (!stat) { 197 | self._createZnodeWithData(newPath, JSON.stringify(value), function (err) { 198 | if (cb) cb(err); 199 | }); 200 | } else { 201 | self.zkClient.setData(newPath, new Buffer(JSON.stringify(value)), -1, 202 | function (error, stat) { 203 | if (error) { 204 | console.log(error.stack); 205 | return; 206 | } 207 | if (cb) cb(error); 208 | }); 209 | } 210 | } 211 | ); 212 | }; 213 | 214 | /** 215 | * Config 정보를 읽어온다. 216 | * @public 217 | * @name getConfig 218 | * @function 219 | * @param {string} key - config key 220 | * @param {function} cb - 읽어온 설정 정보를 건네준다. 221 | */ 222 | Configure.prototype.getConfig = function (key, cb) { 223 | var self = this; 224 | if (self.zkClient.getState() != zookeeper.State.SYNC_CONNECTED) { 225 | return self._beforeConnected.push({method: 'get', cb: cb, k: key}); 226 | } 227 | 228 | self.zkClient.getData( 229 | this.basePath + '/' + key, 230 | function (error, data, stat) { 231 | if (error) { 232 | if (error.code == -101) { // Exception: NO_NODE[-101]. 233 | // @ TODO do something? 234 | } else { 235 | console.error('Fail retrieve config data: %s.', error); 236 | } 237 | 238 | if (cb) cb(); 239 | 240 | } else { 241 | var tmp = data.toString('utf8'); 242 | var datas = JSON.parse(tmp); 243 | 244 | if (cb) cb(datas); 245 | } 246 | } 247 | ); 248 | }; 249 | 250 | /** 251 | * Config 정보를 삭제한다. 252 | * @public 253 | * @name deleteConfig 254 | * @function 255 | * @param {string} key - config key 256 | * @param {function} cb - 삭제가 정상으로 종료되었는지 판단하여 호출. 257 | */ 258 | Configure.prototype.deleteConfig = function (key, cb) { 259 | var self = this; 260 | if (self.zkClient.getState() != zookeeper.State.SYNC_CONNECTED) { 261 | return self._beforeConnected.push({method: 'get', cb: cb, k: key}); 262 | } 263 | 264 | self._removeZnode(this.basePath + '/' + key, function (err) { 265 | if (err) { 266 | console.log('Fail delete config data (%s) : %s.', key, error); 267 | } else { 268 | //delete self.configureData.key; 269 | if (cb) cb(err); 270 | } 271 | }); 272 | } 273 | 274 | /** 275 | * 변경된 모든 config 정보를 수신한다. 276 | * @private 277 | * @name onAll 278 | * @function 279 | * @param {function} cb - config 설정이 모두 변경되면 호출 280 | */ 281 | Configure.prototype.onAll = function (cb) { 282 | var self = this; 283 | var listeners = self.listeners(ALL_EVENT_LISTEN_KEY); 284 | var isExist = false; 285 | 286 | for (var i = 0; i < listeners.length; i++) { 287 | var l = listeners[i]; 288 | if (l == cb) { 289 | isExist = true; 290 | break; 291 | } 292 | } 293 | if (!isExist) self.on(ALL_EVENT_LISTEN_KEY, cb); 294 | } 295 | 296 | /** 297 | * Zookeeper 의 기본 노드를 생성한다. 298 | * @private 299 | * @name _initFullPath 300 | * @function 301 | * @param {function} cb - node 생성이 완료되면 호출 302 | */ 303 | Configure.prototype._initFullPath = function (cb) { 304 | var self = this; 305 | self._initPath(self.serviceName, function () { 306 | self._initPath(self.serviceName + CONSTANTS.CONFIG_PATH, function () { 307 | if (cb)cb(); 308 | }); 309 | }); 310 | }; 311 | 312 | /** 313 | * Zookeeper 의 노드를 생성한다. 314 | * @private 315 | * @name _createZnode 316 | * @function 317 | * @param {string} nodePath - 생성할 노드 정보 318 | * @param {function} cb - node 생성이 완료되면 호출 319 | */ 320 | Configure.prototype._createZnode = function (nodePath, callback) { 321 | var self = this; 322 | self.zkClient.create( 323 | nodePath, 324 | zookeeper.CreateMode.PERSISTENT, 325 | function (error) { 326 | if (error) { 327 | console.log('Failed to create node: %s due to: %s.', nodePath, error); 328 | if (callback) callback(error); 329 | } else { 330 | if (callback) callback(null, nodePath); 331 | } 332 | } 333 | ); 334 | }; 335 | 336 | /** 337 | * Zookeeper 의 노드를 생성한다.(없는 경우에만) 338 | * @private 339 | * @name _initPath 340 | * @function 341 | * @param {string} nodePath - 생성할 노드 정보 342 | * @param {function} cb - node 생성이 완료되면 호출 343 | */ 344 | Configure.prototype._initPath = function (nodePath, callback) { 345 | // TODO server configuration 을 기본 값으로 만들어 줌 ( 없는 경우만, ) - max-connection, expired-time 346 | var self = this; 347 | self.zkClient.exists( 348 | nodePath, 349 | function (error, stat) { 350 | if (error) { 351 | console.log(error.stack); 352 | return; 353 | } 354 | 355 | if (!stat) { 356 | self._createZnode(nodePath, callback); 357 | } else { 358 | if (callback) callback(null); 359 | } 360 | }); 361 | }; 362 | 363 | /** 364 | * Zookeeper 의 노드를 데이터와 함께 생성한다. 365 | * @private 366 | * @name _createZnodeWithData 367 | * @function 368 | * @param {string} nodePath - 생성할 노드 정보 369 | * @param {json} data - 생성한 노드에 저장할 데이터 370 | * @param {function} cb - node 생성이 완료되면 호출 371 | */ 372 | Configure.prototype._createZnodeWithData = function (nodePath, data, callback) { 373 | var self = this; 374 | self.zkClient.create( 375 | nodePath, 376 | new Buffer(data), 377 | zookeeper.CreateMode.PERSISTENT, 378 | function (error) { 379 | if (error) { 380 | console.log('Failed to create node: %s due to: %s.', nodePath, error); 381 | if (callback) callback(error); 382 | } else { 383 | if (callback) callback(null, nodePath); 384 | } 385 | } 386 | ); 387 | }; 388 | 389 | /** 390 | * Zookeeper 의 노드를 삭제한다. 391 | * @private 392 | * @name _removeZnode 393 | * @function 394 | * @param {string} nodePath - 삭제할 노드 정보 395 | * @param {function} cb - node 삭제 완료되면 호출 396 | */ 397 | Configure.prototype._removeZnode = function (nodePath, callback) { 398 | var self = this; 399 | self.zkClient.remove( 400 | nodePath, 401 | -1, 402 | function (err) { 403 | if (err) { 404 | console.log('Failed to remove node: %s due to: %s.', nodePath, err); 405 | if (callback) callback(err); 406 | } else { 407 | if (callback) callback(null); 408 | } 409 | } 410 | ); 411 | }; 412 | 413 | /** 414 | * 최초에 모든 설정 정보를 Zookeeper 에서 watching 하기 시작한다. 415 | * @private 416 | * @name _globalWatchConfig 417 | * @function 418 | */ 419 | Configure.prototype._globalWatchConfig = function () { 420 | var self = this; 421 | 422 | this.zkClient.getChildren( 423 | this.basePath, 424 | function (event) { 425 | self._globalWatchConfig(); 426 | }, 427 | function (error, children, stat) { 428 | if (error) { 429 | console.log('Failed to app list children due to: %s.', error); 430 | } else { 431 | for (var i = 0; i < children.length; i++) { 432 | self._watchConfig(children[i]); 433 | } 434 | } 435 | } 436 | ); 437 | } 438 | 439 | /** 440 | * Zookeeper에서 해당 key 에 해당되는 config 정보를 watching 한다. 441 | * @private 442 | * @name _watchConfig 443 | * @function 444 | * @param {string} key - config key 445 | */ 446 | Configure.prototype._watchConfig = function (key) { 447 | var self = this; 448 | var newPath = this.basePath + '/' + key; 449 | this.zkClient.getData( 450 | newPath, 451 | function (event) { 452 | self._watchConfig(key); 453 | }, 454 | function (error, data, stat) { 455 | 456 | if (error) { 457 | 458 | if (error.code == -101) { 459 | // @ TODO 백부석님 이거 어떻게 해야 할끼? do something ???? 460 | } else { 461 | console.error('Failed to list children due to: %s.', error); 462 | } 463 | 464 | } else { 465 | 466 | var dataObject = JSON.parse(data); 467 | self.emit(key, dataObject); 468 | self.emit(ALL_EVENT_LISTEN_KEY, key, dataObject); 469 | 470 | } 471 | } 472 | ); 473 | } 474 | 475 | /** 476 | * Zookeeper에서 모든 config정보의 key들을 가져온다. 477 | * @private 478 | * @name getConfigKeyList 479 | * @function 480 | * @param {function} cb - 모든 key값을 가져오면 호출한다. 481 | */ 482 | Configure.prototype.getConfigKeyList = function (cb) { 483 | this.zkClient.getChildren( 484 | this.basePath, 485 | function (error, children, stat) { 486 | if (error) { 487 | console.log('Failed to app list children due to: %s.', error); 488 | } else { 489 | cb(children); 490 | } 491 | } 492 | ); 493 | } 494 | 495 | var config = new Configure(); 496 | module.exports = config; 497 | -------------------------------------------------------------------------------- /lib/util/logging.js: -------------------------------------------------------------------------------- 1 | var winston = require('winston'); 2 | var fs = require('fs'); 3 | 4 | module.exports = function (conf) { 5 | var port = conf.port; 6 | var type = conf.type; 7 | var xpushDataPath = conf.path; 8 | 9 | if (!fs.existsSync(xpushDataPath + '/log')) fs.mkdirSync(xpushDataPath + '/log', 0766); 10 | 11 | winston.loggers.add('error', { 12 | file: { 13 | filename: xpushDataPath + '/log/XPUSH.ERR.' + type + '.' + port + '.log' 14 | } 15 | }); 16 | winston.loggers.add('log', { 17 | file: { 18 | filename: xpushDataPath + '/log/XPUSH.' + type + '.' + port + '.log' 19 | } 20 | }); 21 | 22 | var transports = [ 23 | new (winston.transports.Console)({ 24 | colorize: true, 25 | prettyPrint: true, 26 | timestamp: true 27 | //, level : 'debug' 28 | }), 29 | new (winston.transports.File)({ 30 | filename: xpushDataPath + '/log/XPUSH.' + type + '.' + port + '.log' 31 | })]; 32 | 33 | var log = new (winston.Logger)({ 34 | 'transports': transports 35 | }); 36 | 37 | function newTC(n) { 38 | if (isNaN(n) || n < 0) n = 1; 39 | n += 1; 40 | var s = (new Error()).stack.split('\n'); 41 | 42 | var a = s[n]; 43 | var s = 0; 44 | if (a.indexOf('\\') > 0) { 45 | s = a.lastIndexOf('\\'); 46 | } 47 | else if (a.indexOf('/') > 0) { 48 | s = a.lastIndexOf('/'); 49 | } 50 | a = a.substr(s + 1); 51 | a = a.substr(0, a.lastIndexOf(':')); 52 | return a; 53 | } 54 | 55 | function traceCaller(n) { 56 | if (isNaN(n) || n < 0) n = 1; 57 | n += 1; 58 | var s = (new Error()).stack, 59 | a = s.indexOf('n', 5); 60 | while (n--) { 61 | a = s.indexOf('n', a + 1); 62 | if (a < 0) { 63 | a = s.lastIndexOf('n', s.length); 64 | break; 65 | } 66 | } 67 | b = s.indexOf('n', a + 1); 68 | if (b < 0) b = s.length; 69 | a = Math.max(s.lastIndexOf(' ', b), s.lastIndexOf('/', b)); 70 | b = s.lastIndexOf(':', b); 71 | s = s.substring(a + 1, b); 72 | return s; 73 | } 74 | 75 | for (var k in winston.levels) { 76 | (function (key) { 77 | var oldFunc = log[key]; 78 | log[key] = function () { 79 | var args = Array.prototype.slice.call(arguments); 80 | args.unshift(newTC(2)); 81 | oldFunc.apply(log, args); 82 | } 83 | })(k); 84 | } 85 | 86 | //console.log = winston.loggers.get('log').info; 87 | console.log = log.info; 88 | //console.info = winston.loggers.get('log').info; 89 | console.info = log.info; 90 | console.warn = log.warn; 91 | console.error = log.error; 92 | //console.error = winston.loggers.get('error').error; 93 | 94 | }; 95 | -------------------------------------------------------------------------------- /lib/util/ps.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | 3 | module.exports = function() { 4 | return { 5 | lookup: lookup 6 | }; 7 | 8 | function lookup(pid, options, callback) { 9 | if(typeof options == 'function') { 10 | callback = options; 11 | options = {}; 12 | } 13 | options = options || {}; 14 | 15 | exec('ps -o "rss,vsize,pcpu" -p ' + pid, function(err, stdout, stderr) { 16 | if (err || stderr) return callback(err || stderr); 17 | 18 | try { 19 | callback(null, parsePS(pid, stdout)); 20 | } catch(ex) { 21 | callback(ex); 22 | } 23 | }); 24 | } 25 | }; 26 | 27 | function parsePS(pid, output) { 28 | var lines = output.trim().split('\n'); 29 | if (lines.length !== 2) { 30 | throw new Error('INVALID_PID'); 31 | } 32 | 33 | var matcher = /[ ]*([0-9]*)[ ]*([0-9]*)[ ]*([0-9\.]*)/; 34 | var result = lines[1].match(matcher); 35 | 36 | if(result) { 37 | return { 38 | memory: parseInt(result[1]) * 1024, 39 | memoryInfo: { 40 | rss: parseFloat(result[1]) * 1024, 41 | vsize: parseFloat(result[2]) * 1024 42 | }, 43 | cpu: parseFloat(result[3]) 44 | }; 45 | } else { 46 | throw new Error('PS_PARSE_ERROR'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/util/utils.js: -------------------------------------------------------------------------------- 1 | var restify = require('restify'), 2 | crypto = require("crypto"), 3 | fs = require("fs"); 4 | 5 | 6 | exports.getHomePath = function (options) { 7 | return options.data || options.home || process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'] + '/.xpush'; 8 | }; 9 | 10 | exports.getIP = function () { 11 | 12 | var interfaces = require('os').networkInterfaces(); 13 | for (var devName in interfaces) { 14 | var iface = interfaces[devName]; 15 | 16 | for (var i = 0; i < iface.length; i++) { 17 | var alias = iface[i]; 18 | if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) return alias.address; 19 | } 20 | } 21 | 22 | return '0.0.0.0'; 23 | }; 24 | 25 | exports.setHttpProtocal = function (_url, protocol) { 26 | if( protocol ){ 27 | return protocol +"://"+ _url; 28 | } else if (!/^http:\/\//.test(_url) && !/^https:\/\//.test(_url)) { 29 | return 'http://' + _url; 30 | } 31 | }; 32 | 33 | exports.validEmptyParams = function (req, paramArray) { 34 | 35 | for (var i in paramArray) { 36 | if (!req.params[paramArray[i]]) { 37 | return new restify.InvalidArgumentError('[' + paramArray[i] + '] must be supplied'); 38 | } 39 | } 40 | 41 | return false; 42 | }; 43 | 44 | exports.validSocketParams = function (params, paramArray) { 45 | 46 | for (var i in paramArray) { 47 | if (!params[paramArray[i]]) { 48 | return { 49 | status: 'error', 50 | message: '', 51 | detail: '[' + paramArray[i] + '] must be supplied' 52 | }; 53 | } 54 | } 55 | 56 | return false; 57 | }; 58 | 59 | exports.validJsonParams = function (params, paramArray) { 60 | for (var i in paramArray) { 61 | var param = params[paramArray[i]]; 62 | 63 | if (param && typeof param == 'object') { 64 | return false; 65 | } else if (param && typeof param == 'string') { 66 | 67 | var json = parseJson(param); 68 | 69 | if (!json) { 70 | return { 71 | status: 'error', 72 | message: '[' + paramArray[i] + '] must be JSON format' 73 | }; 74 | } 75 | 76 | return false; 77 | } else if (param) { 78 | return { 79 | status: 'error', 80 | message: '[' + paramArray[i] + '] must be JSON format' 81 | }; 82 | } 83 | } 84 | 85 | return false; 86 | }; 87 | 88 | var parseJson = function (instance) { 89 | var json; 90 | try { 91 | 92 | json = JSON.parse(instance); 93 | 94 | if (typeof json == 'string') { 95 | json = parseJson(json); 96 | } 97 | 98 | } catch (err) { 99 | json = null; 100 | } 101 | 102 | return json; 103 | }; 104 | 105 | exports.parseJson = function (instance) { 106 | return parseJson(instance); 107 | }; 108 | 109 | exports.regExpEscape = function (s) { 110 | return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 111 | }; 112 | 113 | exports.encrypto = function (s, t) { 114 | if (!t) t = "sha256"; 115 | var _c = crypto.createHash(t); 116 | _c.update(s, "utf8"); //utf8 here 117 | return _c.digest("base64"); 118 | }; 119 | 120 | var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'; 121 | 122 | exports.randomString = function (length) { 123 | length = length ? length : 32; 124 | 125 | var string = ''; 126 | 127 | for (var i = 0; i < length; i++) { 128 | var randomNumber = Math.floor(Math.random() * chars.length); 129 | string += chars.substring(randomNumber, randomNumber + 1); 130 | } 131 | 132 | return string; 133 | }; 134 | 135 | exports.parseCookies = function (request) { 136 | var list = {}, 137 | rc = request.headers.cookie; 138 | 139 | rc && rc.split(';').forEach(function (cookie) { 140 | var parts = cookie.split('='); 141 | list[parts.shift().trim()] = unescape(parts.join('=')); 142 | }); 143 | 144 | return list; 145 | }; 146 | 147 | exports.sendErr = function (response, err) { 148 | response.send({status: 'ERR-INTERNAL', message: err}); 149 | }; 150 | 151 | exports.likeQueryMaker = function (data) { 152 | 153 | if (typeof data == 'string' || data instanceof String) { 154 | if (data.indexOf('%') == 0 && (data.lastIndexOf('%') + 1) == data.length) { 155 | return new RegExp(data.substring(1, data.lastIndexOf('%')), 'i'); 156 | } else { 157 | return data; 158 | } 159 | } else if (typeof data == 'object') { 160 | if (Array.isArray(data)) { 161 | var newArray = []; 162 | for (var inx in data) { 163 | newArray.push(this.likeQueryMaker(data[inx])); 164 | } 165 | return newArray; 166 | } else { 167 | var result = {}; 168 | for (var k in data) { 169 | result[k] = this.likeQueryMaker(data[k]); 170 | } 171 | return result; 172 | } 173 | } 174 | }; 175 | 176 | exports.getBaseDirPath = function (home) { 177 | 178 | var homePath = home || process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'] + '/.xpush'; 179 | 180 | try { 181 | if (!fs.existsSync(homePath)) fs.mkdirSync(homePath, parseInt('0766', 8)); 182 | } catch (e) { 183 | console.log('Error creating xpush directory: ' + e); 184 | } 185 | 186 | return homePath; 187 | }; 188 | 189 | exports.getPidFilePath = function (home, envType, envPort) { 190 | var basePath = this.getBaseDirPath(home); 191 | return basePath + '/XPUSH.' + envType + '.' + envPort + '.pid'; 192 | }; 193 | 194 | exports.getDaemonLogFilePath = function (home, envType, envPort) { 195 | var basePath = this.getBaseDirPath(home) + '/log'; 196 | try { 197 | if (!fs.existsSync(basePath)) fs.mkdirSync(basePath, parseInt('0766', 8)); 198 | } catch (e) { 199 | console.log('Error creating xpush directory: ' + e); 200 | } 201 | 202 | return basePath + '/DEAMON.' + envType + '.' + envPort + '.log'; 203 | }; -------------------------------------------------------------------------------- /lib/xpush-channel-server.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var util = require('util'); 3 | var async = require('async'); 4 | var shortId = require('shortid'); 5 | 6 | var Utils = require('./util/utils'); 7 | var NodeManager = require('./node-manager/node-manager.js').NodeManager; 8 | var SessionManager = require('./session-manager/session-manager.js').SessionManager; 9 | var SessionSubscriber = require('./session-manager/session-subscriber.js').SessionSubscriber; 10 | var usage = require('./util/ps')(); 11 | 12 | var NAMESPACE = '/channel'; 13 | 14 | function ChannelServer() { 15 | 16 | if (!(this instanceof ChannelServer)) return new ChannelServer(); 17 | 18 | // inner storage for channels 19 | this.channels = {}; // {U, D, N} 20 | this.multiChannels = {}; 21 | 22 | this.methods = { 23 | CHANNEL_SOCKET: {} 24 | }; 25 | 26 | EventEmitter.call(this); 27 | 28 | } 29 | 30 | util.inherits(ChannelServer, EventEmitter); 31 | 32 | ChannelServer.prototype.init = function(server, io, options, cb) { 33 | 34 | this.server = server; 35 | 36 | this.conf = { 37 | host: options.host, 38 | port: options.port, 39 | weight: options.weight, 40 | zookeeper: options.zookeeper, 41 | redis: options.redis, 42 | serverName: options.serverName 43 | }; 44 | 45 | this.conf.balancing = { 46 | SCALE: 60, // 단계별 Connection 수 47 | BUFFER_COUNT: 10, // replica 수정에 대한 인계치 버퍼 48 | MAX_LEVEL: 4, // scale 배수 49 | REPLICA_BASE_NUMBER: 4 // 50 | }; 51 | 52 | this.options = this.conf; 53 | 54 | var self = this; 55 | 56 | try { 57 | 58 | async.parallel( 59 | [ // ASYNC ARRAY (START) 60 | function(callback) { // # 1 61 | 62 | var startReplicas = Math.pow(Number(self.conf.balancing['REPLICA_BASE_NUMBER']), Number(self.conf.balancing['MAX_LEVEL'])); 63 | 64 | var address = ''; 65 | if (self.conf.zookeeper) { 66 | if (typeof self.conf.zookeeper === 'string' || self.conf.zookeeper instanceof String) { 67 | address = self.conf.zookeeper; 68 | } else { 69 | if (self.conf.zookeeper.address) address = self.conf.zookeeper.address; 70 | } 71 | } 72 | 73 | self.nodeManager = new NodeManager( 74 | address, 75 | false, 76 | function(err, message) { 77 | if (!err) { 78 | console.info(' (init) ZOOKEEPER is connected'); 79 | self.nodeManager.addServerNode(self.conf, startReplicas, function(err, path, replicas) { 80 | 81 | //if (!err) console.info(' ZOOKEEPER /' + self.conf.host + ':' + self.conf.port); 82 | 83 | var serverName = path.substring(path.lastIndexOf('/') + 1, path.length); 84 | self.serverNodePath = path; 85 | 86 | self.serverName = serverName.split('^')[0]; 87 | 88 | if (replicas) { 89 | self.replicas = replicas; 90 | } else { 91 | self.replicas = startReplicas; 92 | } 93 | 94 | // init balacing config 95 | self.nodeManager.getConfigInfo('balancing', function(data) { 96 | if (data) { 97 | 98 | self.conf.balancing = data; 99 | var configReplica = Math.pow(Number(self.conf.balancing['REPLICA_BASE_NUMBER']), Number(self.conf.balancing['MAX_LEVEL'])); 100 | 101 | if (self.replicas != configReplica) { 102 | self._changeReplicas(configReplica); 103 | } 104 | } 105 | }); 106 | 107 | callback(err); 108 | }); 109 | } else { 110 | callback(err, message); 111 | } 112 | } 113 | ); 114 | }, 115 | 116 | function(callback) { // # 2 117 | 118 | self.sessionManager = new SessionManager( 119 | self.conf && self.conf.redis ? self.conf.redis : undefined, 120 | function(err, message) { 121 | if (!err) { 122 | console.info(' (init) REDIS is connected (for session data)'); 123 | } 124 | callback(err, message); 125 | } 126 | ); 127 | 128 | } 129 | 130 | ], // ASYNC ARRAY (END) 131 | 132 | function(err, results) { 133 | 134 | if (!err) { 135 | 136 | self.sessionSubscriber = new SessionSubscriber( 137 | self.conf && self.conf.redis && self.conf.redis ? self.conf.redis : undefined, 138 | self.serverName, 139 | function(err) { 140 | if (!err) { 141 | 142 | console.info(' (init) REDIS is connected (for pub/sub)'); 143 | console.info(' REDIS is subscribed (C-' + self.serverName + ')'); 144 | 145 | self._startup(server, io); 146 | 147 | if (cb) cb(); 148 | 149 | } else { 150 | console.error(err); 151 | process.exit(1); 152 | } 153 | } 154 | ); 155 | 156 | } else { 157 | 158 | for (var errNum in results) { 159 | if (results.hasOwnProperty(errNum) && results[errNum]) { 160 | console.error(' - ' + results[errNum] + '\n'); 161 | } 162 | } 163 | process.exit(1); 164 | } 165 | } 166 | ); 167 | } catch (err) { 168 | console.error('Channel server startup ERROR : ' + err); 169 | } 170 | }; 171 | 172 | 173 | ChannelServer.prototype._startup = function(server, io) { 174 | 175 | this.io = io; 176 | 177 | var self = this; 178 | 179 | server.get('/status/ping', function(req, res, next) { 180 | res.send({ 181 | status: 'ok', 182 | result: { 183 | message: 'pong' 184 | } 185 | }); 186 | 187 | next(); 188 | }); 189 | 190 | /************************************************************************* 191 | * CHANNEL SOCKET 192 | *************************************************************************/ 193 | this.io.of(NAMESPACE).use(function(socket, next) { 194 | var handshakeData = socket.request; 195 | // TODO 196 | // Check the channel is available (Existed? ) ? 197 | // or this is wasted ? 198 | var _app = handshakeData._query.A; 199 | var _channel = handshakeData._query.C; 200 | var _server = handshakeData._query.S; 201 | var _userId = handshakeData._query.U; 202 | var _deviceId = handshakeData._query.D || 'N'; 203 | //var _data = handshakeData._query.DT; 204 | 205 | if (!_app || !_channel || !_server) { 206 | console.error('Parameter is not corrected. (A, C, S) : ', _app, _channel, _server); 207 | next('Parameter is not corrected. (A, C, S) ', false); 208 | return; 209 | } 210 | 211 | var _us = self.channels[_app + '^' + _channel]; 212 | 213 | if (!_us) { 214 | self.channels[_app + '^' + _channel] = [{ 215 | U: _userId, 216 | D: _deviceId 217 | }]; 218 | } else { 219 | var _u = _us.filter(function(_uu) { 220 | return (_uu.U == _userId); 221 | }); 222 | 223 | if (_u.length === 0) { 224 | self.channels[_app + '^' + _channel].push({ 225 | U: _userId, 226 | D: _deviceId 227 | }); 228 | } 229 | } 230 | 231 | socket.handshake.query = { 232 | A: handshakeData._query.A, 233 | C: handshakeData._query.C, 234 | S: handshakeData._query.S, 235 | U: handshakeData._query.U, 236 | D: handshakeData._query.D || 'N' 237 | }; 238 | 239 | next(null, true); 240 | }).on('connection', function(socket) { 241 | 242 | var _room = socket.handshake.query.A + '^' + socket.handshake.query.C; 243 | //console.log('channel socket connection : ' + socket.id + ' / ' + _room); 244 | 245 | socket.join(_room); 246 | 247 | // DT 248 | var err = Utils.validJsonParams(socket.handshake.query, ['DT']); 249 | if (err) { 250 | socket.emit("connect_error", err); 251 | socket.disconnect(); 252 | return; 253 | } 254 | 255 | socket._userId = socket.handshake.query.U; 256 | socket._deviceId = socket.handshake.query.D; 257 | 258 | var _count_of_this_channel = socket.adapter.rooms[_room].length; 259 | 260 | // sessionManager의 channel 정보를 update한다. 261 | self.sessionManager.updateConnectedNode( 262 | socket.handshake.query.A, 263 | socket.handshake.query.C, 264 | socket.handshake.query.S, 265 | _count_of_this_channel); 266 | 267 | if (_count_of_this_channel == 1) { 268 | 269 | //console.log('_count_of_this_channel : ', _count_of_this_channel); 270 | 271 | self.sessionManager.retrieveConnectedNode(socket.handshake.query.A, socket.handshake.query.C, function(res) { 272 | 273 | if (res) { 274 | for (var key in res) { 275 | //console.log(key, socket.handshake.query.S, (key != socket.handshake.query.S)); 276 | if (key != socket.handshake.query.S) { 277 | 278 | //console.log(socket.handshake.query.S + ' --> ' + key); 279 | self.sessionManager.publish(key, { 280 | _type: 'add-channel-server', 281 | A: socket.handshake.query.A, 282 | C: socket.handshake.query.C, 283 | S: socket.handshake.query.S 284 | }); 285 | 286 | if (self.channels[_room]) { 287 | if (!self.multiChannels[_room]) { 288 | self.multiChannels[_room] = [key]; 289 | } else { 290 | if (self.multiChannels[_room].indexOf(key) == -1) self.multiChannels[_room].push(key); 291 | } 292 | } 293 | 294 | } 295 | } 296 | } 297 | 298 | }); 299 | 300 | } 301 | 302 | self.calculateReplicas( NAMESPACE, 'CONNECTION' ); 303 | 304 | var _msgObj = { 305 | event: 'CONNECTION', 306 | id: socket.id, 307 | count: _count_of_this_channel, 308 | A: socket.handshake.query.A, 309 | C: socket.handshake.query.C, 310 | S: socket.handshake.query.S, 311 | U: socket.handshake.query.U, 312 | D: socket.handshake.query.D 313 | }; 314 | 315 | self.emit('channel', _msgObj); 316 | 317 | if (self.methods.CHANNEL_SOCKET.hasOwnProperty('connection')) self.methods.CHANNEL_SOCKET.connection(_msgObj, socket); 318 | 319 | // 동일한 socket을 사용 중인 user에게 `CONNECTION` EVENT를 발생시킨다. 320 | socket.broadcast.to(_room).emit('_event', _msgObj); 321 | socket.emit('_event', _msgObj); 322 | 323 | for (var key in self.methods.CHANNEL_SOCKET) { 324 | if (key != 'connection' && key != 'send') { 325 | socket.on(key, self.methods.CHANNEL_SOCKET[key]); 326 | } 327 | } 328 | 329 | socket.on('send', function(params, callback) { 330 | 331 | var err = Utils.validSocketParams(params, ['NM', 'DT']); 332 | if (err) { 333 | if (callback) callback({ 334 | status: 'ERR-PARAM', 335 | message: err 336 | }); 337 | return; 338 | } 339 | 340 | // socket Id가 존재하면 현재 server에서 전송한다. 341 | if (params.SS) { 342 | 343 | self._sendPrivate( 344 | params.S, // server name 345 | params.SS, // socketId 346 | params.NM, 347 | params.DT, 348 | NAMESPACE); 349 | 350 | } else { 351 | 352 | self._send( 353 | socket.handshake.query.A, 354 | socket.handshake.query.C, 355 | params.NM, 356 | params.DT, 357 | NAMESPACE); 358 | } 359 | 360 | if (self.methods.CHANNEL_SOCKET.hasOwnProperty('send')) self.methods.CHANNEL_SOCKET.send(params, socket); 361 | 362 | if (callback) callback({ 363 | status: 'ok', 364 | params: params 365 | }); 366 | 367 | }); 368 | 369 | // DISCONNECT 370 | socket.on('disconnect', function() { 371 | self._channel_disconnect(socket); 372 | }); 373 | 374 | }); 375 | 376 | /************************************************************************* 377 | * ADMIN SOCKET 378 | *************************************************************************/ 379 | 380 | this.io.of('/admin').use(function(socket, callback) { 381 | 382 | if (self.options.admin && self.options.admin.token) { 383 | var handshakeData = socket.request; 384 | if (handshakeData._query.token == self.options.admin.token) { 385 | callback(null, true); 386 | } else { 387 | callback('unauthorized access blocked', false); 388 | } 389 | } else { 390 | callback(null, true); 391 | } 392 | 393 | }).on('connection', function(socket) { 394 | 395 | var _default = { 396 | pid: process.pid 397 | }; 398 | 399 | socket.on('info', function(callback) { 400 | var result = _default; 401 | 402 | result.arch = process.arch; 403 | result.platform = process.platform; 404 | result.server = { 405 | name: self.serverName, 406 | host: self.options.host, 407 | port: self.options.port 408 | }; 409 | 410 | callback(result); 411 | }); 412 | 413 | socket.on('usage', function(callback) { 414 | var result = _default; 415 | 416 | result.name = self.serverName; 417 | result.host = self.options.host; 418 | result.uptime = process.uptime(); 419 | result.port = self.options.port; 420 | result.memory = process.memoryUsage(); 421 | // rss: Resident set size 422 | // heapTotal: Heap size sampled immediately after a full garbage collection, 423 | // heapUsed: Current heap size 424 | 425 | var beforeCpu = 0; 426 | usage.lookup(process.pid, {}, function(err, stat) { 427 | 428 | result.client = { 429 | socket: Object.keys(self.io.of(NAMESPACE).connected).length, 430 | channel: Object.keys(self.channels).length, 431 | bigchannel: Object.keys(self.multiChannels).length, 432 | }; 433 | 434 | if (!err) { 435 | result.cpu = stat.cpu; 436 | beforeCpu = stat.cpu; 437 | } else { 438 | result.cpu = beforeCpu; 439 | } 440 | 441 | callback(result); 442 | }); 443 | }); 444 | 445 | }); 446 | 447 | this.sessionSubscriber.on('_message', function(receivedData) { 448 | 449 | if (receivedData._type == 'send-once') { 450 | 451 | if (receivedData.SS) { 452 | self._sendPrivate(null, receivedData.SS, receivedData.NM, receivedData.DT, receivedData.NSP); 453 | } else { 454 | self._sendOnce(receivedData.A, receivedData.C, receivedData.NM, receivedData.DT, receivedData.NSP); 455 | } 456 | 457 | } else if (receivedData._type == 'add-channel-server') { 458 | 459 | if (self.channels[receivedData.A + '^' + receivedData.C]) { 460 | 461 | var _mc = self.multiChannels[receivedData.A + '^' + receivedData.C]; 462 | 463 | if (!_mc) { 464 | self.multiChannels[receivedData.A + '^' + receivedData.C] = [receivedData.S]; 465 | } else { 466 | if (_mc.indexOf(receivedData.S) == -1) self.multiChannels[receivedData.A + '^' + receivedData.C].push(receivedData.S); 467 | } 468 | } 469 | 470 | } else if (receivedData._type == 'del-channel-server') { 471 | 472 | var _mcd = self.multiChannels[receivedData.A + '^' + receivedData.C]; 473 | if (_mcd) { 474 | self.multiChannels[receivedData.A + '^' + receivedData.C].splice(_mcd.indexOf(receivedData.S), 1); 475 | if (_mcd.length == 0) delete self.multiChannels[receivedData.A + '^' + receivedData.C]; 476 | } 477 | 478 | if (receivedData.NM && receivedData.DT) { 479 | self._sendOnce(receivedData.A, receivedData.C, receivedData.NM, receivedData.DT, receivedData.NSP); 480 | } 481 | } 482 | 483 | self.emit('subscribe', receivedData); 484 | 485 | }); 486 | }; 487 | 488 | ChannelServer.prototype._changeReplicas = function(replicas) { 489 | var self = this; 490 | self.isNodeChanging = true; 491 | self.nodeManager.setNodeData(self.serverNodePath, replicas, function(err, path, data) { 492 | self.isNodeChanging = false; 493 | if (!err) { 494 | self.replicas = data; 495 | } 496 | }); 497 | }; 498 | 499 | ChannelServer.prototype.calculateReplicas = function(nsp, event){ 500 | 501 | var self = this; 502 | 503 | if( event == 'CONNECTION'){ 504 | 505 | var connectionCount = Object.keys(self.io.of(nsp).connected).length; 506 | var currentLevel = Math.floor(connectionCount / Number(self.conf.balancing['SCALE'])); 507 | var stage = Number(self.conf.balancing['MAX_LEVEL']) - currentLevel; 508 | if (stage < 0) stage = 0; 509 | 510 | var nextReplicas = Math.pow(Number(self.conf.balancing['REPLICA_BASE_NUMBER']), stage); 511 | 512 | // Over 513 | if (!self.isNodeChanging && 514 | nextReplicas != self.replicas && 515 | connectionCount >= (Number(self.conf.balancing['SCALE'] * currentLevel) + Number(self.conf.balancing['BUFFER_COUNT']))) { 516 | self._changeReplicas(nextReplicas); 517 | } 518 | 519 | } else if( event == 'DISCONNECT') { 520 | var connectionCount = Object.keys(self.io.of(NAMESPACE).connected).length; 521 | var currentLevel = Math.floor(connectionCount / Number(self.conf.balancing['SCALE'])); 522 | var stage = Number(self.conf.balancing['MAX_LEVEL']) - currentLevel; 523 | if (stage < 0) stage = 0; 524 | 525 | var nextReplicas = Math.pow(Number(self.conf.balancing['REPLICA_BASE_NUMBER']), stage); 526 | 527 | // Under 528 | if (!self.isNodeChanging && 529 | nextReplicas != self.replicas && 530 | connectionCount <= (Number(self.conf.balancing['SCALE'] * Number(currentLevel + 1)) - Number(self.conf.balancing['BUFFER_COUNT']))) { 531 | self._changeReplicas(nextReplicas); 532 | } 533 | } 534 | } 535 | 536 | ChannelServer.prototype.generateId = function() { 537 | return shortId.generate(); 538 | }; 539 | 540 | ChannelServer.prototype._send = function(_app, _channel, _name, _data, _namespace) { 541 | var self = this; 542 | self._sendOnce(_app, _channel, _name, _data, _namespace); 543 | 544 | var _room = _app + '^' + _channel; 545 | 546 | var _m = self.multiChannels[_room]; 547 | if (_m) { 548 | var _ml = _m.length; 549 | for (var i = 0; i < _ml; i++) { 550 | 551 | self.sessionManager.publish(_m[i], { 552 | _type: 'send-once', 553 | A: _app, 554 | C: _channel, 555 | NM: _name, 556 | DT: _data, 557 | NSP: _namespace 558 | }); 559 | 560 | } 561 | } 562 | 563 | }; 564 | 565 | ChannelServer.prototype._sendPrivate = function(_server, _socketId, _name, _data, _namespace) { 566 | 567 | if (_server) { 568 | 569 | self.sessionManager.publish(_server, { 570 | _type: 'send-once', 571 | SS: _socketId, 572 | NM: _name, 573 | DT: _data, 574 | NSP: _namespace 575 | }); 576 | 577 | } else { 578 | if(!_namespace) _namespace = NAMESPACE; 579 | var _socket = this.io.of(_namespace).connected[_socketId]; 580 | if (_socket && _socket.id != undefined) { 581 | var currentTimestamp = Date.now(); 582 | _data.TS = currentTimestamp; 583 | _socket.emit(_name, _data); 584 | } 585 | } 586 | 587 | }; 588 | 589 | ChannelServer.prototype._sendOnce = function(_app, _channel, _name, _data, _namespace) { 590 | 591 | var _room = _app + '^' + _channel; 592 | var currentTimestamp = Date.now(); 593 | if(!_namespace) _namespace = NAMESPACE; 594 | if (this.io.of(_namespace).in(_room) != undefined) { 595 | _data.C = _channel; 596 | _data.TS = currentTimestamp; 597 | this.io.of(_namespace).in(_room).emit(_name, _data); 598 | 599 | this.emit('message', { 600 | A: _app, 601 | C: _channel, 602 | TS: currentTimestamp, 603 | NM: _name, 604 | DT: _data 605 | }); 606 | 607 | } 608 | }; 609 | 610 | ChannelServer.prototype._channel_disconnect = function(_this) { 611 | 612 | var socket = _this; 613 | 614 | var self = this; 615 | 616 | var _a = socket.handshake.query.A; 617 | var _c = socket.handshake.query.C; 618 | var _u = socket.handshake.query.U; 619 | var _s = socket.handshake.query.S; 620 | 621 | var _room = _a + '^' + _c; 622 | 623 | socket.leave(_room); 624 | 625 | var _count_of_this_channel = 0; 626 | if (socket.adapter.rooms[_room]) { 627 | _count_of_this_channel = socket.adapter.rooms[_room].length; 628 | } 629 | 630 | // DISCONNECT Data 631 | var _data = { 632 | event: 'DISCONNECT', 633 | count: _count_of_this_channel, 634 | A: _a, 635 | C: _c, 636 | U: _u 637 | }; 638 | 639 | self.calculateReplicas( NAMESPACE, 'DISCONNECT' ); 640 | 641 | // channel 내에 아무도 없으면 local cache에서 channel을 삭제함. 642 | if (_count_of_this_channel == 0) { 643 | 644 | delete self.channels[_a + '^' + _c]; 645 | 646 | var _m = self.multiChannels[_a + '^' + _c]; 647 | if (_m) { 648 | 649 | var _ml = _m.length; 650 | for (var i = 0; i < _ml; i++) { 651 | self.sessionManager.publish(_m[i], { 652 | _type: 'del-channel-server', 653 | A: _a, 654 | C: _c, 655 | S: _s, 656 | NM: '_event', 657 | DT: _data 658 | }); 659 | } 660 | delete self.multiChannels[_a + '^' + _c]; 661 | } 662 | 663 | } else { 664 | 665 | var _m = self.multiChannels[_a + '^' + _c]; 666 | if (_m) { 667 | 668 | var _ml = _m.length; 669 | 670 | for (var i = 0; i < _ml; i++) { 671 | 672 | self.sessionManager.publish(_m[i], { 673 | _type: 'send-once', 674 | A: _a, 675 | C: _c, 676 | NM: '_event', 677 | DT: _data 678 | }); 679 | } 680 | 681 | } 682 | 683 | socket.broadcast.to(_room).emit('_event', { 684 | event: 'DISCONNECT', 685 | count: _count_of_this_channel, 686 | A: _a, 687 | C: _c, 688 | U: _u 689 | }); 690 | 691 | } 692 | 693 | // sessionManager의 channel 정보를 update한다. 694 | self.sessionManager.updateConnectedNode(_a, _c, socket.handshake.query.S, _count_of_this_channel); 695 | 696 | self.emit('channel', { 697 | 'event': 'disconnect', 698 | 'count': _count_of_this_channel, 699 | 'option': socket.handshake.query.option, 700 | 'A': _a, 701 | 'C': _c, 702 | 'S': socket.handshake.query.S, 703 | 'U': socket.handshake.query.U, 704 | 'D': socket.handshake.query.D 705 | }); 706 | 707 | if (self.methods.CHANNEL_SOCKET.hasOwnProperty('disconnect')) self.methods.CHANNEL_SOCKET.disconnect(_data); 708 | 709 | }; 710 | 711 | 712 | ChannelServer.prototype.channel_on = function(_event, _fn) { 713 | this.methods.CHANNEL_SOCKET[_event] = _fn; 714 | }; 715 | 716 | ChannelServer.prototype.onSend = function(_fn) { 717 | this.methods.CHANNEL_SOCKET['send'] = _fn; 718 | }; 719 | 720 | ChannelServer.prototype.onDisconnect = function(_fn) { 721 | this.methods.CHANNEL_SOCKET['disconnect'] = _fn; 722 | }; 723 | 724 | ChannelServer.prototype.onConnection = function(_fn) { 725 | this.methods.CHANNEL_SOCKET['connection'] = _fn; 726 | }; 727 | 728 | ChannelServer.prototype.getChannels = function(_app, _channel) { 729 | return this.channels[_app + '^' + _channel]; 730 | }; 731 | 732 | ChannelServer.prototype.getServerName = function() { 733 | return this.serverName; 734 | }; 735 | 736 | ChannelServer.prototype.onGet = function(_url, _fn) { 737 | var searchIndex = -1; 738 | for (var inx = 0; searchIndex < 0 && inx < this.server.router.routes.GET.length; inx++) { 739 | if (this.server.router.routes.GET[inx].spec.path + "" === _url + "") { 740 | searchIndex = inx; 741 | } 742 | } 743 | 744 | if (searchIndex > -1) { 745 | this.server.router.routes.GET.splice(searchIndex, 1); 746 | } 747 | 748 | this.server.get(_url, _fn); 749 | }; 750 | 751 | 752 | ChannelServer.prototype.onPost = function(_url, _fn) { 753 | var searchIndex = -1; 754 | for (var inx = 0; searchIndex < 0 && inx < this.server.router.routes.POST.length; inx++) { 755 | if (this.server.router.routes.POST[inx].spec.path + "" === _url + "") { 756 | searchIndex = inx; 757 | } 758 | } 759 | 760 | if (searchIndex > -1) { 761 | this.server.router.routes.POST.splice(searchIndex, 1); 762 | } 763 | 764 | this.server.post(_url, _fn); 765 | }; 766 | 767 | ChannelServer.prototype._addNamespace = function(__INTERNAL_ONLY) { 768 | 769 | if (this.nsps) { 770 | var _self = this; 771 | this.nsps.forEach(function(entry) { 772 | _self.io.of(entry.nsp).use(entry.fnUse).on('connection', entry.fnConnection); 773 | }); 774 | } 775 | 776 | } 777 | 778 | ChannelServer.prototype.addNamespace = function(nsp, fnUse, fnConnection) { 779 | 780 | if (!this.nsps) this.nsps = []; 781 | 782 | this.nsps.push({ 783 | nsp: nsp, 784 | fnUse: fnUse, /* function (socket, next) { ... } */ 785 | fnConnection: fnConnection /* function (socket) {...} */ 786 | }); 787 | 788 | } 789 | 790 | ChannelServer.prototype.send = function(argJson) { 791 | 792 | if (argJson.socketId) { 793 | this._sendPrivate(argJson.server, argJson.socketId, argJson.name, argJson.data, argJson.namespace); 794 | } else { 795 | this._sendOnce(argJson.app, argJson.channel, argJson.name, argJson.data, argJson.namespace); 796 | } 797 | 798 | }; 799 | 800 | // exports 801 | exports = module.exports = new ChannelServer(); 802 | exports.ChannelServer = ChannelServer; 803 | -------------------------------------------------------------------------------- /lib/xpush-session-server.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var util = require('util'); 3 | var async = require('async'); 4 | var shortId = require('shortid'); 5 | 6 | var Utils = require('./util/utils'); 7 | var NodeConstants = require('./node-manager/constants'); 8 | var NodeManager = require('./node-manager/node-manager.js').NodeManager; 9 | var SessionManager = require('./session-manager/session-manager.js').SessionManager; 10 | 11 | function SessionServer() { 12 | 13 | if (!(this instanceof SessionServer)) return new SessionServer(); 14 | 15 | EventEmitter.call(this); 16 | 17 | } 18 | 19 | util.inherits(SessionServer, EventEmitter); 20 | 21 | SessionServer.prototype.init = function (server, options, cb) { 22 | 23 | this.conf = { 24 | host: options.host, 25 | port: options.port, 26 | zookeeper: options.zookeeper, 27 | redis: options.redis 28 | }; 29 | 30 | if( options.channelProtocol || options.protocol ){ 31 | this.conf.channelProtocol= options.channelProtocol || options.protocol; 32 | } 33 | 34 | // SERVER CONFIGURATION 35 | this.conf.balancing = { 36 | SCALE: 60, 37 | BUFFER_COUNT: 10, 38 | MAX_LEVEL: 4, 39 | REPLICA_BASE_NUMBER: 4 40 | }; 41 | 42 | this.replicaMap = {}; 43 | 44 | var self = this; 45 | 46 | try { 47 | 48 | async.parallel( 49 | [ // ASYNC ARRAY (START) 50 | 51 | function (callback) { // # 1 52 | 53 | var address; 54 | if (self.conf.zookeeper) { 55 | if (typeof self.conf.zookeeper === 'string' || self.conf.zookeeper instanceof String) { 56 | address = self.conf.zookeeper; 57 | } else { 58 | if (self.conf.zookeeper.address) address = self.conf.zookeeper.address; 59 | } 60 | } 61 | 62 | self.nodeManager = new NodeManager( 63 | address, 64 | true, 65 | function (err, message) { 66 | if (!err) { 67 | console.info(' (init) ZOOKEEPER is connected'); 68 | self.nodeManager.createEphemeralPath( 69 | NodeConstants.META_PATH + NodeConstants.GW_SERVER_PATH + '/' + self.conf.host + ':' + self.conf.port, 70 | function (err, message) { 71 | console.info(' ZOOKEEPER /' + self.conf.host + ':' + self.conf.port); 72 | 73 | // init balacing config 74 | self.nodeManager.getConfigInfo('balancing', function (data) { 75 | if (data) { 76 | self.conf.balancing = data; 77 | } 78 | }); 79 | 80 | callback(err, message); 81 | } 82 | ); 83 | } else { 84 | callback(err, message); 85 | } 86 | } 87 | ); 88 | 89 | }, 90 | 91 | function (callback) { // # 2 92 | 93 | self.sessionManager = new SessionManager( 94 | self.conf && self.conf.redis ? self.conf.redis : undefined, 95 | function (err, message) { 96 | if (!err) { 97 | console.info(' (init) REDIS is connected'); 98 | } 99 | callback(err, message); 100 | } 101 | ); 102 | 103 | } 104 | 105 | ], // ASYNC ARRAY (END) 106 | 107 | function (err, results) { 108 | 109 | if (!err) { 110 | 111 | self._startup(server); 112 | 113 | if (cb) cb(); 114 | 115 | } else { 116 | 117 | for (var errNum in results) { 118 | if (results.hasOwnProperty(errNum)) { 119 | if (results[errNum]) { 120 | console.error(' - ' + results[errNum] + '\n'); 121 | } 122 | } 123 | } 124 | 125 | process.exit(1); 126 | } 127 | } 128 | ); 129 | } catch (err) { 130 | console.error('Session server startup ERROR : ' + err); 131 | } 132 | }; 133 | 134 | 135 | SessionServer.prototype._startup = function (server) { 136 | 137 | this.server = server; 138 | 139 | var self = this; 140 | 141 | server.get('/status/ping', function (req, res, next) { 142 | res.send({ 143 | status: 'ok', 144 | result: { 145 | message: 'pong' 146 | } 147 | }); 148 | 149 | next(); 150 | }); 151 | 152 | server.get('/node/:app/:channel', function (req, res, next) { 153 | 154 | self.getNewChannelServer(req.params.app, req.params.channel, function (serverInfo) { 155 | 156 | if (!serverInfo) { 157 | res.send({ 158 | status: 'error', 159 | result: { 160 | message: 'Channel server is not available.' 161 | } 162 | }); 163 | 164 | } else { 165 | res.send({ 166 | status: 'ok', 167 | result: { 168 | seq: self.generateId(), 169 | channel: serverInfo.channel, 170 | server: serverInfo 171 | } 172 | }); 173 | 174 | } 175 | 176 | next(); 177 | }); 178 | 179 | }); 180 | 181 | }; 182 | 183 | SessionServer.prototype.getNewChannelServer = function (_app, _channel, fn) { 184 | 185 | var self = this; 186 | 187 | var maxConnection = Number(self.conf.balancing['SCALE']) * ( Number(self.conf.balancing['MAX_LEVEL']) / 2 ); 188 | 189 | this.sessionManager.retrieveConnectedNode(_app, _channel, function (res) { 190 | 191 | var server = ""; 192 | 193 | if (res) { // already existed in redis. 194 | 195 | var mServer = ""; 196 | var count = -1; 197 | 198 | for (var key in res) { 199 | if (res.hasOwnProperty(key)) { 200 | 201 | var _c = parseInt(res[key]); 202 | //console.log('STEP 1.', key, _c); 203 | if (_c < maxConnection) { // MAX_CONNECTION 204 | server = key; 205 | count = _c; 206 | break; 207 | 208 | } else { 209 | 210 | if (count > -1) { 211 | if (_c < count) { 212 | count = _c; 213 | mServer = key; 214 | } 215 | } else { 216 | count = _c; 217 | mServer = key; 218 | } 219 | 220 | } 221 | } 222 | } 223 | 224 | if (!server) { 225 | var nodeMap = self.nodeManager.getNodeMap(); 226 | //console.log('STEP 2-1.', nodeMap); 227 | 228 | for (var name in nodeMap) { 229 | //console.log('STEP 2-2.', name, parseInt(res[name]), !res[name]); 230 | if (!res[name]) { 231 | server = name; 232 | break; 233 | } 234 | } 235 | } 236 | 237 | if (!server) server = mServer; 238 | 239 | } 240 | 241 | var serverInfo = ''; 242 | if (server) { 243 | serverInfo = self.nodeManager.getServerNodeByName(server); 244 | 245 | if (!serverInfo) { // remove the server data from redis session storage. 246 | //self.sessionManager.remove(_app, _channel, server); 247 | } 248 | } 249 | 250 | if (serverInfo 251 | && serverInfo.replicas != self.replicaMap[server]) { 252 | self.replicaMap[server] = serverInfo.replicas; 253 | 254 | serverInfo = 0; 255 | } 256 | 257 | // Max reached 258 | if (serverInfo && serverInfo.replicas == 1) { 259 | serverInfo = 0; 260 | } 261 | 262 | // TODO In the case Not Existed serverNode Object !! 263 | var serverNode = {}; 264 | if (!serverInfo) { 265 | 266 | serverNode = self.nodeManager.getServerNode(_channel); 267 | 268 | if (!serverNode) { 269 | 270 | return fn(); 271 | 272 | } 273 | } else { 274 | serverNode = serverInfo; 275 | } 276 | 277 | fn({ 278 | channel: _channel, 279 | name: serverNode.name, 280 | url: Utils.setHttpProtocal(serverNode.url, self.conf.channelProtocol) 281 | }); 282 | 283 | }); 284 | }; 285 | 286 | SessionServer.prototype.onGet = function (_url, _fn) { 287 | var searchIndex = -1; 288 | for (var inx = 0; searchIndex < 0 && inx < this.server.router.routes.GET.length; inx++) { 289 | if (this.server.router.routes.GET[inx].spec.path + "" === _url + "") { 290 | searchIndex = inx; 291 | } 292 | } 293 | 294 | if (searchIndex > -1) { 295 | this.server.router.routes.GET.splice(searchIndex, 1); 296 | } 297 | 298 | this.server.get(_url, _fn); 299 | }; 300 | 301 | SessionServer.prototype.onPost = function (_url, _fn) { 302 | var searchIndex = -1; 303 | for (var inx = 0; searchIndex < 0 && inx < this.server.router.routes.POST.length; inx++) { 304 | if (this.server.router.routes.POST[inx].spec.path + "" === _url + "") { 305 | searchIndex = inx; 306 | } 307 | } 308 | 309 | if (searchIndex > -1) { 310 | this.server.router.routes.POST.splice(searchIndex, 1); 311 | } 312 | 313 | this.server.post(_url, _fn); 314 | }; 315 | 316 | SessionServer.prototype.onPut = function (_url, _fn) { 317 | var searchIndex = -1; 318 | for (var inx = 0; searchIndex < 0 && inx < this.server.router.routes.PUT.length; inx++) { 319 | if (this.server.router.routes.PUT[inx].spec.path + "" === _url + "") { 320 | searchIndex = inx; 321 | } 322 | } 323 | 324 | if (searchIndex > -1) { 325 | this.server.router.routes.PUT.splice(searchIndex, 1); 326 | } 327 | 328 | this.server.put(_url, _fn); 329 | }; 330 | 331 | SessionServer.prototype.onDelete = function (_url, _fn) { 332 | var searchIndex = -1; 333 | for (var inx = 0; searchIndex < 0 && inx < this.server.router.routes.DELETE.length; inx++) { 334 | if (this.server.router.routes.DELETE[inx].spec.path + "" === _url + "") { 335 | searchIndex = inx; 336 | } 337 | } 338 | 339 | if (searchIndex > -1) { 340 | this.server.router.routes.DELETE.splice(searchIndex, 1); 341 | } 342 | 343 | this.server.del(_url, _fn); 344 | }; 345 | 346 | SessionServer.prototype.generateId = function () { 347 | return shortId.generate(); 348 | }; 349 | 350 | // exports 351 | exports = module.exports = new SessionServer(); 352 | exports.SessionServer = SessionServer; 353 | -------------------------------------------------------------------------------- /lib/xpush.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var https = require('https'); 6 | var http = require('http'); 7 | 8 | var Utils = require('./util/utils'); 9 | 10 | var VERSION = (function() { 11 | var data = fs.readFileSync(path.join(__dirname, '../package.json')).toString(); 12 | return JSON.parse(data).version; 13 | })(); 14 | 15 | var chkInitProcess = function(options) { 16 | 17 | var homePath = Utils.getHomePath(options); 18 | options.home = homePath; 19 | try { 20 | if (!fs.existsSync(homePath)) fs.mkdirSync(homePath, parseInt('0766', 8)); 21 | if (!fs.existsSync(homePath + '/' + (options.upload || 'upload'))) fs.mkdirSync(homePath + '/' + (options.upload || 'upload'), parseInt('0766', 8)); 22 | } catch (e) { 23 | console.log('Error creating xpush directory: ' + e); 24 | } 25 | 26 | require('../lib/util/logging')({ 27 | type: options.type, 28 | port: options.port, 29 | path: homePath 30 | }); 31 | }; 32 | 33 | var welcome = function(options) { 34 | 35 | if (options && options.logo) { 36 | return options.logo; 37 | } else { 38 | return [ 39 | "", 40 | " _ ", 41 | " | | ", 42 | " __ ___ __ _ _ ___| |__ ", 43 | " \\ \\/ / '_ \\| | | / __| '_ \\ ", 44 | " > <| |_) | |_| \\__ \\ | | |", 45 | " /_/\\_\\ .__/ \\__,_|___/_| |_|", 46 | " | | ", 47 | " |_| V " + VERSION, 48 | "" 49 | ].join('\n'); 50 | } 51 | 52 | }; 53 | 54 | var host = Utils.getIP(); 55 | var port = 8080; 56 | 57 | function onError(error) { 58 | if (error.syscall !== 'listen') { 59 | throw error; 60 | } 61 | var bind = typeof port === 'string' ? 62 | 'Pipe ' + port : 63 | 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | // SESSION SERVER 81 | function createSessionServer(options, cb, server) { 82 | 83 | 84 | var optionsServer = options.server || {}; 85 | if (options.httpsServerOptions) { 86 | optionsServer.httpsServerOptions = options.httpsServerOptions; 87 | options.protocol = 'https'; 88 | } else { 89 | options.protocol = 'http'; 90 | } 91 | 92 | host = options.host || host; 93 | port = options.port || port; 94 | options['type'] = 'SESSION'; 95 | 96 | chkInitProcess(options); 97 | 98 | // default RESTIFY server (http://restify.com/) 99 | if (server === undefined) { 100 | 101 | /** 102 | 103 | options.server = {} 104 | 105 | certificate [String] : If you want to create an HTTPS server, pass in the PEM-encoded certificate and key 106 | key [String] : If you want to create an HTTPS server, pass in the PEM-encoded certificate and key 107 | formatters [Object] : Custom response formatters for res.send() 108 | log [Object] : You can optionally pass in a bunyan instance; not required 109 | name [String] : By default, this will be set in the Server response header, default is restify 110 | spdy [Object] : Any options accepted by node-spdy 111 | version [String] : A default version to set for all routes 112 | handleUpgrades [Boolean] : Hook the upgrade event from the node HTTP server, pushing Connection: Upgrade requests through the regular request handling chain; defaults to false 113 | httpsServerOptions [Object] : Any options accepted by node-https Server. If provided the following restify server options will be ignored: spdy, ca, certificate, key, passphrase, rejectUnauthorized, requestCert and ciphers; however these can all be specified on httpsServerOptions. 114 | 115 | - http://restify.com/#creating-a-server 116 | - https://github.com/restify/node-restify/blob/master/lib/server.js 117 | 118 | **/ 119 | 120 | 121 | var restify = require('restify'); 122 | if (!optionsServer.name) optionsServer.name = 'xpush'; 123 | 124 | var server = restify.createServer(optionsServer); 125 | 126 | restify.CORS.ALLOW_HEADERS.push('authorization'); 127 | restify.CORS.ALLOW_HEADERS.push('accept'); 128 | restify.CORS.ALLOW_HEADERS.push('sid'); 129 | restify.CORS.ALLOW_HEADERS.push('lang'); 130 | restify.CORS.ALLOW_HEADERS.push('origin'); 131 | restify.CORS.ALLOW_HEADERS.push('withcredentials'); 132 | restify.CORS.ALLOW_HEADERS.push('x-requested-with'); 133 | 134 | server.use(restify.CORS({ 135 | origins: ["*"], 136 | headers: ["Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization"] 137 | })); 138 | 139 | server.opts(/.*/, function(req, res, next) { 140 | res.header("Access-Control-Allow-Origin", "*"); 141 | res.header("Access-Control-Allow-Methods", req.header("Access-Control-Request-Method")); 142 | res.header("Access-Control-Allow-Headers", req.header("Access-Control-Request-Headers")); 143 | res.send(200); 144 | return next(); 145 | }); 146 | 147 | var uploadPath = path.join( 148 | options.home, 149 | options.upload || 'upload' 150 | ); 151 | 152 | server.use(restify.fullResponse()) 153 | server.use(restify.jsonp()); 154 | server.use(restify.bodyParser({ 155 | mapParams: true, 156 | uploadDir: uploadPath, 157 | mapFiles: true, 158 | keepExtensions: true 159 | })); 160 | 161 | server.on('error', onError); 162 | console.log('default server type : RESTIFY '); 163 | 164 | } 165 | 166 | var sessionServer = require('./xpush-session-server.js'); 167 | sessionServer.init(server, options, function(err, result) { 168 | 169 | if (err) { 170 | console.error(err, result); 171 | if (cb) cb(err, result); 172 | } else { 173 | 174 | var callback = function(){ 175 | console.log(welcome(options), '"SESSION server" listening at ' + host + ":" + port); 176 | 177 | sessionServer.emit("started", host, port) 178 | 179 | if (cb) cb(err, { 180 | "host": host, 181 | "port": port 182 | }); 183 | }; 184 | 185 | if( server['server'] ){ 186 | //restiyfy 187 | server.listen(port, callback ); 188 | } else { 189 | //express 190 | var expressServer; 191 | if( options.protocol == 'https' && options.httpsServerOptions ){ 192 | expressServer = https.createServer(options.httpsServerOptions, server).listen(port, callback); 193 | } else { 194 | expressServer = http.createServer(server).listen(port, callback); 195 | } 196 | } 197 | } 198 | 199 | }); 200 | 201 | return (sessionServer); 202 | } 203 | 204 | // CHANNEL SERVER 205 | function createChannelServer(options, cb) { 206 | 207 | host = options.host = options.host || Utils.getIP(); 208 | port = options.port || 8080; 209 | options['type'] = 'CHANNEL'; 210 | 211 | chkInitProcess(options); 212 | 213 | var restify = require('restify'); 214 | var socketio = require('socket.io'); 215 | 216 | var serverOption = options.server || {}; 217 | 218 | if (!serverOption.name) serverOption.name = 'xpush'; 219 | 220 | if (options.httpsServerOptions) { 221 | serverOption.httpsServerOptions = options.httpsServerOptions; 222 | options.protocol = 'https'; 223 | } else { 224 | options.protocol = 'http'; 225 | } 226 | 227 | var server = restify.createServer(serverOption); 228 | 229 | var io = socketio.listen(server.server); 230 | 231 | server.use(restify.CORS()); 232 | server.on('error', onError); 233 | 234 | var channelServer = require('./xpush-channel-server.js'); 235 | channelServer.init(server, io, options, function(err, result) { 236 | 237 | if (err) { 238 | console.error(err, result); 239 | if (cb) cb(err, result); 240 | } else { 241 | server.listen(port, function() { 242 | 243 | console.log(welcome(options), '"CHANNEL server" listening at ' + host + ":" + port); 244 | 245 | channelServer.emit("started", host, port); 246 | channelServer._addNamespace(); 247 | 248 | if (cb) cb(err, { 249 | "host": host, 250 | "port": port 251 | }); 252 | 253 | }); 254 | } 255 | 256 | }); 257 | return (channelServer); 258 | } 259 | 260 | /** Main XPUSH Server **/ 261 | module.exports.createSessionServer = createSessionServer; 262 | module.exports.createChannelServer = createChannelServer; 263 | 264 | 265 | /** Additional Manager (optional, only for specific cases) **/ 266 | module.exports.createZookeeperClient = function(options) { 267 | 268 | var zookeeper = require('node-zookeeper-client'); 269 | var addr = 'localhost:2181'; 270 | if (options.zookeeper) { 271 | if (typeof options.zookeeper === 'string' || options.zookeeper instanceof String) { 272 | addr = options.zookeeper; 273 | } else { 274 | if (options.zookeeper.address) addr = options.zookeeper.address; 275 | } 276 | } 277 | var zkClient = zookeeper.createClient(addr, { 278 | retries: 2 279 | }); 280 | 281 | return zkClient; 282 | 283 | }; 284 | module.exports.createRedisManager = function(options) { 285 | 286 | var conf = { 287 | redis: options.redis 288 | }; 289 | 290 | var RedisManager = require('./session-manager/redis-manager'); 291 | return new RedisManager(conf); 292 | 293 | }; 294 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xpush/node-xpush/4e1ef2c94484a12bb82fdddf57645d9e56d7ad13/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xpush", 3 | "description": "Web based Realtime Communication Platform", 4 | "version": "0.1.6", 5 | "keywords": [ 6 | "push", 7 | "websocket", 8 | "gcm", 9 | "apn", 10 | "channel" 11 | ], 12 | "contributors": [ 13 | { 14 | "name": "John Kim", 15 | "email": "yohany@gmail.com" 16 | }, 17 | { 18 | "name": "Notdol", 19 | "email": "nixenic@gmail.com" 20 | }, 21 | { 22 | "name": "James Jung", 23 | "email": "0nlyoung7@gmail.com" 24 | }, 25 | { 26 | "name": "John Ko", 27 | "email": "eunsan.ko@gmail.com" 28 | } 29 | ], 30 | "main": "./lib/xpush.js", 31 | "dependencies": { 32 | "async": "^2.1.2", 33 | "node-zookeeper-client": "^0.2.2", 34 | "redis": "^2.6.2", 35 | "redis-shard": "github:johnkim/node-redis-shard", 36 | "restify": "^4.1.1", 37 | "shortid": "^2.2.6", 38 | "socket.io": "^1.5.1", 39 | "winston": "^2.2.0", 40 | "optimist": "^0.6.1" 41 | }, 42 | "devDependencies": { 43 | "faker": "^3.0.1", 44 | "mime": "^1.3.4", 45 | "socket.io-client": "^1.3.7" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "https://github.com/xpush/node-xpush" 50 | }, 51 | "license": "MIT", 52 | "scripts": { 53 | "test": "node test" 54 | }, 55 | "homepage": "http://xpush.github.io", 56 | "engines": { 57 | "node": ">= 4.0.0" 58 | } 59 | } 60 | --------------------------------------------------------------------------------