├── .gitignore ├── README.md ├── package.json ├── proxy.js ├── replace-console.js ├── sticky-socket-cluster.js └── test ├── package.json ├── public └── test.htm └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sticky sessions with proxy 2 | 3 | Simple and customizable way to use multicore features with [socket.io](http://socket.io/) and [express](https://github.com/strongloop/express). 4 | 5 | ## How it works 6 | 7 | Launches multiple worker processes through [cluster](http://nodejs.org/docs/latest/api/cluster.html), using bunch of ports. 8 | One worker process becomes also 'http-proxy', serving as sticky session balancer. 9 | 10 | Establishes sticky round-robin balancer for any kind of http frameworks. Not only, but including socket.io and express. 11 | Client will always connect to same worker process sticked with customizable hash function. 12 | For example, socket.io multi-stage authorization will work as expected. 13 | 14 | ## Installation 15 | 16 | ```bash 17 | npm install sticky-socket-cluster 18 | ``` 19 | 20 | 21 | ## Basic Usage 22 | 23 | ```javascript 24 | require('sticky-socket-cluster')(start); 25 | 26 | function start(port) { 27 | require('http').createServer(function(req, res) { 28 | res.end('worker: ' + port); 29 | }).listen(port, function() { 30 | console.log('server started on ' + port + ' port'); 31 | }); 32 | } 33 | ``` 34 | 35 | ## Customized Usage 36 | 37 | (Complete demo in [test](test) folder) 38 | 39 | ```javascript 40 | require('sticky-socket-cluster/replace-console')(); 41 | // prefixes console output with worker ids. 42 | 43 | var options = { 44 | workers: 2, // total workers (default: cpu cores count). 45 | first_port: 8000, // 8000, 8001 are worker's ports (default: 8000). 46 | proxy_port: 5000, // default (5000). 47 | session_hash: function (req, res) { return req.connection.remoteAddress; }, 48 | // can use cookie-based session ids and etc. (default: int31 hash). 49 | 50 | no_sockets: false // allow socket.io proxy (default: false). 51 | }; 52 | 53 | require('sticky-socket-cluster')(options, start); 54 | 55 | function start(port) { 56 | var express = require('express'); 57 | var http = require('http'); 58 | var app = express(); 59 | var server = http.Server(app); 60 | var io = require('socket.io')(server); 61 | 62 | io.on('connection', function(socket) 63 | { 64 | console.log("socket.io connection handler..."); 65 | //... 66 | }); 67 | 68 | server.listen(port, function() { 69 | console.log('Express and socket.io listening on port ' + port); 70 | }); 71 | } 72 | ``` 73 | 74 | 75 | #### LICENSE 76 | 77 | This software is licensed under the MIT License. 78 | 79 | Copyright Vladimir E. Koltunov and contributors, 2014-2015. 80 | 81 | Permission is hereby granted, free of charge, to any person obtaining a 82 | copy of this software and associated documentation files (the 83 | "Software"), to deal in the Software without restriction, including 84 | without limitation the rights to use, copy, modify, merge, publish, 85 | distribute, sublicense, and/or sell copies of the Software, and to permit 86 | persons to whom the Software is furnished to do so, subject to the 87 | following conditions: 88 | 89 | The above copyright notice and this permission notice shall be included 90 | in all copies or substantial portions of the Software. 91 | 92 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 93 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 94 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 95 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 96 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 97 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 98 | USE OR OTHER DEALINGS IN THE SOFTWARE. 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sticky-socket-cluster", 3 | "version": "0.0.11", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/PROGrand/sticky-socket-cluster" 7 | }, 8 | "description": "Sticky session balancer based on a `cluster` and `http-proxy` module with express and socket.io support.", 9 | "author": { 10 | "name": "Vladimir Koltunov", 11 | "email": "progrand@gmail.com" 12 | }, 13 | "maintainers": [ 14 | { 15 | "name": "progrand", 16 | "email": "progrand@gmail.com" 17 | } 18 | ], 19 | "main": "sticky-socket-cluster.js", 20 | "dependencies": { 21 | "http-proxy" : "*", 22 | "debug" : "*" 23 | }, 24 | "keywords": [ 25 | "express", 26 | "socket.io", 27 | "cluster", 28 | "http-proxy" 29 | ], 30 | "devDependencies": { 31 | }, 32 | "engines": { 33 | "node": ">=0.10.0" 34 | }, 35 | "license": "MIT" 36 | } 37 | -------------------------------------------------------------------------------- /proxy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var debug_log = require('debug')('scluster:log'); 4 | debug_log.log = console.log.bind(console); 5 | var debug_error = require('debug')('scluster:error'); 6 | 7 | 8 | var httpProxy = require('http-proxy'); 9 | var http = require('http'); 10 | 11 | var current_proxy = 0; 12 | var total_workers = 0; 13 | 14 | function next_proxy() { 15 | var proxy = proxies[current_proxy]; 16 | 17 | require('util').inspect(proxies, false, null); 18 | 19 | current_proxy = (current_proxy + 1) % total_workers; 20 | return proxy; 21 | } 22 | 23 | 24 | var stickers = {}; 25 | 26 | var proxies = {}; 27 | 28 | 29 | exports.init = function(workers, first_port, proxy_port, session_hash, no_sockets) { 30 | 31 | total_workers = workers; 32 | 33 | for (var n = 0; n < total_workers; n++) { 34 | proxies[n] = new httpProxy.createProxyServer({ 35 | target : { 36 | host : '127.0.0.1', 37 | port : first_port + n 38 | } 39 | }); 40 | 41 | proxies[n].on('error', function(error, req, res) { 42 | 43 | debug_log('proxy error: ' + error); 44 | if (null != res) 45 | { 46 | try 47 | { 48 | var json; 49 | 50 | if (!res.headersSent) { 51 | res.writeHead(500, {'content-type': 'application/json'}); 52 | } 53 | 54 | json = { error: 'proxy_error', reason: error.message }; 55 | res.end(JSON.stringify(json)) 56 | } 57 | catch (e) 58 | { 59 | } 60 | } 61 | }); 62 | } 63 | 64 | var server = http.createServer(function(req, res) { 65 | get_proxy(session_hash, req, res).web(req, res); 66 | }); 67 | 68 | if (!no_sockets) 69 | { 70 | server.on('upgrade', function(req, socket, head) { 71 | get_proxy(session_hash, req).ws(req, socket, head); 72 | 73 | }); 74 | } 75 | 76 | debug_log("main proxy listen on port: " + proxy_port); 77 | 78 | server.listen(proxy_port); 79 | } 80 | 81 | 82 | function get_proxy(session_hash, req, res) 83 | { 84 | var hash = session_hash(req, res); 85 | 86 | debug_log('hash: ' + hash); 87 | 88 | var proxy = undefined; 89 | 90 | if (hash !== undefined) { 91 | 92 | if (stickers[hash] !== undefined) { 93 | 94 | debug_log('restored proxy.'); 95 | 96 | proxy = stickers[hash].proxy; 97 | } else { 98 | 99 | debug_log('assigned proxy.'); 100 | 101 | proxy = next_proxy(); 102 | 103 | stickers[hash] = { 104 | proxy : proxy, 105 | } 106 | } 107 | 108 | } else { 109 | 110 | debug_log('random proxy.'); 111 | 112 | proxy = next_proxy(); 113 | } 114 | 115 | return proxy; 116 | } 117 | -------------------------------------------------------------------------------- /replace-console.js: -------------------------------------------------------------------------------- 1 | var cluster = require('cluster'); 2 | 3 | 4 | module.exports = function replaceConsole() 5 | { 6 | ['log','debug','info','warn','error'].forEach(function (item) 7 | { 8 | var old = console[item]; 9 | console[item] = function() 10 | { 11 | var prefix = '[' + ((cluster && cluster.worker && cluster.worker.id) ? cluster.worker.id : "0") + ']: '; 12 | old(prefix + Array.prototype.slice.call(arguments)); 13 | } 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /sticky-socket-cluster.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var cluster = require('cluster'); 4 | 5 | var debug_log = require('debug')('scluster:log'); 6 | debug_log.log = console.log.bind(console); 7 | var debug_error = require('debug')('scluster:error'); 8 | 9 | 10 | module.exports = function pool(options, callback) 11 | { 12 | if (typeof options === 'function') 13 | { 14 | callback = options; 15 | options = {}; 16 | } 17 | 18 | var seed = ~~(0.5 /*Math.random()*/ * 1e9); 19 | 20 | var session_hash_ip = function (req, res) { 21 | 22 | var ip = (req.connection.remoteAddress || '').split(/\./g); 23 | 24 | var hash = ip.reduce(function(r, num) { 25 | r += parseInt(num, 10); 26 | r %= 2147483648; 27 | r += (r << 10) 28 | r %= 2147483648; 29 | r ^= r >> 6; 30 | return r; 31 | }, seed); 32 | hash += hash << 3; 33 | hash %= 2147483648; 34 | hash ^= hash >> 11; 35 | hash += hash << 15; 36 | hash %= 2147483648; 37 | return hash >>> 0; 38 | } 39 | 40 | 41 | options.workers = options.workers || process.env.WORKERS || require('os').cpus().length; 42 | options.first_port = options.first_port || process.env.FIRST_PORT || 8000; 43 | options.proxy_port = options.proxy_port || process.env.PROXY_PORT || 5000; 44 | options.session_hash = options.session_hash || session_hash_ip; 45 | options.no_sockets = options.no_sockets || false; 46 | options.start_timeout = options.start_timeout || 3000; 47 | 48 | if (cluster.isMaster) { 49 | 50 | debug_log('*************** MASTER: ' + require('util').inspect(options, false, null)); 51 | 52 | var fork_worker = function(port) 53 | { 54 | var worker = cluster.fork({worker_port: port}); 55 | }; 56 | 57 | for (var n = 0; n < options.workers; n++) { 58 | fork_worker(options.first_port + n); 59 | } 60 | 61 | cluster.on('exit', function(worker, code, signal) { 62 | debug_error('Worker died (ID: ' + worker.id + ', PID: ' 63 | + worker.process.pid + '), port: ' + worker.port); 64 | 65 | fork_worker(worker.port); 66 | }); 67 | 68 | debug_log('init proxy...'); 69 | 70 | setTimeout(function (){ 71 | require('./proxy').init(options.workers, options.first_port, options.proxy_port, options.session_hash, options.no_sockets) 72 | }, options.start_timeout); 73 | 74 | } else if (cluster.isWorker) { 75 | 76 | debug_log('*************** WORKER: ' + process.env.worker_port); 77 | 78 | callback(process.env.worker_port); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sticky-socket-cluster-test", 3 | "version": "0.0.1", 4 | "main": "test.js", 5 | "dependencies": { 6 | "socket.io" : "*", 7 | "express" : "*", 8 | "cookie-parser" : "*", 9 | "body-parser" : "*", 10 | "sticky-socket-cluster" : "*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/public/test.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | require('sticky-socket-cluster/replace-console')(); 2 | // prefixes console output with worker ids. 3 | 4 | var options = { 5 | workers: 2, // total workers (default: cpu cores count). 6 | first_port: 8000, // 8000, 8001 are worker's ports (default: 8000). 7 | proxy_port: 5000, // default (5000). 8 | session_hash: function (req, res) { return req.connection.remoteAddress; }, 9 | // can use cookie-based session ids and etc. (default: int31 hash). 10 | 11 | no_sockets: false // allow socket.io proxy (default: false). 12 | }; 13 | 14 | require('sticky-socket-cluster')(options, start); 15 | 16 | function start(port) { 17 | const path = require('path'); 18 | const cookieParser = require('cookie-parser'); 19 | const bodyParser = require('body-parser'); 20 | var express = require('express'); 21 | var http = require('http'); 22 | var app = express(); 23 | app.use(bodyParser.json()); 24 | app.use(bodyParser.urlencoded({extended: false})); 25 | app.use(cookieParser()); 26 | app.use(express.static(path.join(__dirname, 'public'))); 27 | 28 | var server = http.Server(app); 29 | var io = require('socket.io')(server); 30 | 31 | io.on('connection', function(socket) 32 | { 33 | console.log("socket.io connection handler..."); 34 | 35 | socket.on('fromclient', function(msg) 36 | { 37 | console.log("message from client received: " + msg.text); 38 | socket.emit('message2', { text: 'stage 3'}); 39 | }); 40 | 41 | socket.emit('message', { text: 'stage 1'}); 42 | }); 43 | 44 | server.listen(port, function() { 45 | console.log('Express and socket.io listening on port ' + port); 46 | }); 47 | } 48 | --------------------------------------------------------------------------------