├── package.json ├── README.md └── multi-node.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multi-node", 3 | "version": "0.2.3", 4 | "author": "Kris Zyp", 5 | "description": "Multiple process HTTP serving for NodeJS", 6 | "keywords": [ 7 | "multiple processes", 8 | "http" 9 | ], 10 | "licenses": [ 11 | { 12 | "type": "AFLv2.1", 13 | "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L43" 14 | }, 15 | { 16 | "type": "BSD", 17 | "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L13" 18 | } 19 | ], 20 | "maintainers": [ 21 | { 22 | "name": "Kris Zyp", 23 | "email": "kriszyp@gmail.com" 24 | } 25 | ], 26 | "repository": { 27 | "type":"git", 28 | "url":"http://github.com/kriszyp/multi-node" 29 | }, 30 | "main": "./multi-node", 31 | "directories": { "lib": "." } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Multi-node is a deprecated package that provided functionality similar to Node's modern cluster functionality. Consequently this package is no longer needed and exists only for historical preservation. 2 | 3 | 4 | 5 | 6 | Multi-node provides launching of multiple NodeJS processes for TCP/HTTP serving. 7 | With multi-node it is very simple to add utilize multiple processes to concurrently 8 | serve HTTP requests, simply pass an http.Server object to the listen function: 9 | 10 | var server = require("http").createServer(function(request, response){ 11 | ... standard node request handler ... 12 | }); 13 | var nodes = require("multi-node").listen({ 14 | port: 80, 15 | nodes: 4 16 | }, server); 17 | 18 | The listen function takes two arguments, the first is the options, the second is the 19 | server. The options argument may have the following properties: 20 | 21 | * port - specifying the port number to listen on (defaults to 80) 22 | * nodes - specifying the number of node processes (defaults to 1) 23 | * host - address to listen on (defaults to 0.0.0.0) 24 | * masterListen - Indicate whether the master process should listen and handle 25 | requests as well (on by default, but you may want to turn this off if you processes are 26 | prone to dying and you want to reliably utilize auto-restart of processes), defaults to true 27 | * restartChildren - Automatically restart child process when they die (defaults to true) 28 | 29 | The object returned from the listen function also provides some useful capabilities. 30 | The return object has an "isMaster" property indicating if the current process is the 31 | original initiating master process. This can be used like: 32 | 33 | var nodes = require("multi-node").listen(...); 34 | if(nodes.isMaster){ 35 | // start a repl on just one process 36 | require("repl").start(); 37 | } 38 | 39 | The returned object also provides an "id" property with an id for the current 40 | process (each node/process has a unique id). 41 | 42 | Inter-process Communication 43 | ======================= 44 | 45 | Multi-node also provides critical inter-process communication facilities. For any web 46 | application that requires processes to be able to communicate with each other 47 | (for sending messages like in chat applications, or for doing in-memory sessions, etc.), 48 | it is necessary for each process to be able to communicate with other processes. 49 | The returned object is also an event emitter, and the "node" event is fired for each 50 | other node process that is created. The event handler is a passed a readable and 51 | writable stream that can be used to communicate with the other process. For example: 52 | 53 | var nodes = require("multi-node").listen(...); 54 | var allStreams = []; 55 | nodes.addListener("node", function(stream){ 56 | stream.addListener("data", function(data){ 57 | ... receiving data from this other node process ... 58 | }); 59 | allStreams.push(stream); 60 | }); 61 | 62 | function notifyOtherProcesses(message){ 63 | allStreams.forEach(function(stream){ 64 | stream.write(message); 65 | }); 66 | } 67 | 68 | Framing 69 | -------- 70 | 71 | The stream object returned from the "node" event for cross-process communication 72 | can be a bit unwieldy to work with by itself, since the stream events can break data 73 | up in non-deterministic fashion, and works at the binary level. You can use 74 | multi-node's framing mechanism to simplify this. Use the frameStream() function to 75 | transform a raw stream into a framed stream that follows the WebSocket API. With 76 | this API you can send strings, objects, and other values with the send(value) function 77 | and receive these values by listening for the "message" event: 78 | 79 | nodes.addListener("node", function(stream){ 80 | stream = require("multi-node").frameStream(stream); 81 | stream.addListener("message", function(data){ 82 | ... receiving string, object, or other value from the other node process ... 83 | }); 84 | stream.send({foo:"bar"}); 85 | }); 86 | 87 | 88 | Notes 89 | ---- 90 | 91 | Node doesn't support fd passing windows yet, so mult-process delegation doesn't work on windows. 92 | 93 | Licensing 94 | -------- 95 | 96 | Multi-node is part of the Persevere project, and therefore is licensed under the 97 | AFL or BSD license. The Persevere project is administered under the Dojo foundation, 98 | and all contributions require a Dojo CLA. 99 | 100 | -------------------------------------------------------------------------------- /multi-node.js: -------------------------------------------------------------------------------- 1 | var net = require("net"), 2 | childProcess = require("child_process"), 3 | Buffer = require("buffer").Buffer, 4 | lastStdout, 5 | netBinding = process.binding("net"); 6 | 7 | exports.ignoreReloadMessages = true; 8 | exports.listen = function(options, server){ 9 | var isMaster; 10 | var emitter = new process.EventEmitter(); 11 | if(process.env._CHILD_ID_){ 12 | emitter.id = process.env._CHILD_ID_; 13 | var stdin = new net.Stream(0, 'unix'); 14 | var descriptorType; 15 | stdin.addListener('data', function(message){ 16 | descriptorType = message; 17 | }); 18 | var siblingIn; 19 | stdin.addListener('fd', function(fd){ 20 | if(descriptorType == "tcp"){ 21 | server.listenFD(fd, 'tcp4'); 22 | } 23 | else if(descriptorType == "sibling"){ 24 | var stream = new net.Stream(fd, "unix"); 25 | emitter.emit("node", stream); 26 | stream.resume(); 27 | } 28 | else{ 29 | throw new Error("Unknown file descriptor " + descriptorType); 30 | } 31 | }); 32 | stdin.resume(); 33 | }else{ 34 | isMaster = true; 35 | emitter.id = "master"; 36 | var children = [], 37 | childId = 1, 38 | tcpDescriptor = netBinding.socket("tcp4"); 39 | netBinding.bind(tcpDescriptor, options.port || 80, options.host || '0.0.0.0'); 40 | netBinding.listen(tcpDescriptor, 128); 41 | var masterListen = options.masterListen !== false; 42 | var numChildren = (options.nodes || 1) - (masterListen ? 1 : 0); 43 | if(masterListen){ 44 | server.listenFD(tcpDescriptor, 'tcp4'); 45 | } 46 | var priorArgs = process.argv; 47 | if(process.platform == "cygwin" && priorArgs){ 48 | priorArgs = ["/usr/bin/bash","--login","-c", "cd " + process.cwd() + " && " + priorArgs.join(" ")]; 49 | } 50 | var env = {}; 51 | for(var i in process.env){ 52 | env[i] = process.env[i]; 53 | } 54 | var createChild = function(i){ 55 | var childConnection = netBinding.socketpair(); 56 | env._CHILD_ID_ = "child-" + i; 57 | // spawn the child process 58 | var child = children[i] = childProcess.spawn( 59 | priorArgs[0], 60 | priorArgs.slice(1), 61 | env, 62 | [childConnection[1], 1, 2] 63 | ); 64 | child.master = new net.Stream(childConnection[0], 'unix'); 65 | 66 | child.master.write("tcp", "ascii", tcpDescriptor); 67 | (function(child){ 68 | for(var j = 0; j < i; j++){ 69 | if(children[j]){ 70 | var siblingConnection = netBinding.socketpair(); 71 | child.master.write("sibling", "ascii", siblingConnection[1]); 72 | children[j].master.write("sibling", "ascii", siblingConnection[0]); 73 | } 74 | } 75 | var masterChildConnection = netBinding.socketpair(); 76 | process.nextTick(function(){ 77 | var stream = new net.Stream(masterChildConnection[0], "unix"); 78 | emitter.emit("node", stream); 79 | stream.resume(); 80 | child.master.write("sibling", "ascii", masterChildConnection[1]); 81 | }); 82 | })(child); 83 | child.addListener("exit", function(){ 84 | // remove the dead one 85 | delete children[i]; 86 | // make a new process to replace the dead one 87 | if(options.restartChildren !== false){ 88 | createChild(childId++); 89 | } 90 | }); 91 | } 92 | for(var i = 0; i < numChildren; i++){ 93 | createChild(childId++); 94 | } 95 | ["SIGINT", "SIGTERM", "SIGKILL", "SIGQUIT", "SIGHUP", "exit"].forEach(function(signal){ 96 | process.addListener(signal, function(){ 97 | children.forEach(function(child){ 98 | try{ 99 | child.kill(); 100 | }catch(e){ 101 | 102 | } 103 | }); 104 | // we use SIGHUP to restart the children 105 | if(signal !== 'exit' && signal !== 'SIGHUP'){ 106 | options.restartChildren = false; 107 | process.exit(); 108 | } 109 | }); 110 | }); 111 | 112 | } 113 | emitter.isMaster = isMaster; 114 | return emitter; 115 | } 116 | 117 | // pass in a raw unframed binary stream, and returns a framed stream for sending and 118 | // receving strings or other JSON data 119 | exports.frameStream = function(stream){ 120 | var parse = JSON.parse; 121 | var emitter = new process.EventEmitter(); 122 | var buffered = []; 123 | var start; 124 | stream.addListener("data", function(data){ 125 | start = 0; 126 | for(var i = 0, l = data.length; i < l; i++){ 127 | var b = data[i]; 128 | if(b === 0){ 129 | start = i + 1; 130 | } 131 | if(b === 255){ 132 | var buffer = data.slice(start, i); 133 | if(buffered.length){ 134 | buffered.push(buffer); 135 | var totalSize = 0; 136 | buffered.forEach(function(part){ 137 | totalSize += part.length; 138 | }); 139 | var buffer = new Buffer(totalSize); 140 | var index = 0; 141 | buffered.forEach(function(part){ 142 | part.copy(buffer, index, 0, part.length); 143 | index += part.length; 144 | }); 145 | } 146 | emitter.emit("message", parse(buffer.toString("utf8", 0, buffer.length))); 147 | start = i + 1; 148 | buffered = []; 149 | } 150 | } 151 | if(start < l){ 152 | buffered.push(data.slice(start, data.length)); 153 | } 154 | }); 155 | emitter.send = function(message){ 156 | var buffer = new Buffer(JSON.stringify(message), "utf8"); 157 | var framedBuffer = new Buffer(buffer.length + 2); 158 | framedBuffer[0] = 0; 159 | buffer.copy(framedBuffer, 1, 0, buffer.length); 160 | framedBuffer[framedBuffer.length - 1] = 255; 161 | stream.write(framedBuffer); 162 | }; 163 | emitter.on = emitter.addListener; 164 | return emitter; 165 | }; 166 | 167 | exports.frameStreamLengthEncoded = function(stream){ 168 | var emitter = new process.EventEmitter(); 169 | var buffer, bufferIndex; 170 | var remainingFrameSize = 0; 171 | stream.addListener("data", function(data){ 172 | while(data.length){ 173 | if(buffer && (buffer.length - bufferIndex > data.length)){ 174 | data.copy(buffer, bufferIndex, 0, data.length); 175 | bufferIndex += data.length; 176 | }else{ 177 | if(buffer){ 178 | data.copy(buffer, bufferIndex, 0, buffer.length - bufferIndex); 179 | emitter.emit("message", buffer.toString("utf8", 0, buffer.length)); 180 | data = data.slice(buffer.length - bufferIndex, data.length); 181 | } 182 | if(data.length){ 183 | buffer = new Buffer((data[index] << 24) + (data[index + 1] << 16) + (data[index + 2] << 8) + (data[index + 3])); 184 | bufferIndex = 0; 185 | data = data.slice(4, data.length); 186 | }else{ 187 | buffer = null; 188 | } 189 | } 190 | } 191 | }); 192 | emitter.send = function(message){ 193 | var buffer = new Buffer(message, "utf8"); 194 | stream.write(new Buffer([buffer.length >> 24, buffer.length >> 16 & 255, buffer.length >> 8 & 255, buffer.length & 255])); 195 | }; 196 | return emitter; 197 | }; 198 | --------------------------------------------------------------------------------