├── .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 |
--------------------------------------------------------------------------------