Skip to content
This repository has been archived by the owner on Apr 22, 2023. It is now read-only.

Commit

Permalink
Fix #3388 Support listening on file descriptors
Browse files Browse the repository at this point in the history
This implements server.listen({ fd: <filedescriptor> }).  The fd should
refer to an underlying resource that is already bound and listening, and
causes the new server to also accept connections on it.

Not supported on Windows.  Raises ENOTSUP.
  • Loading branch information
isaacs committed Jun 13, 2012
1 parent 0187b65 commit e733dc3
Show file tree
Hide file tree
Showing 10 changed files with 640 additions and 21 deletions.
46 changes: 46 additions & 0 deletions doc/api/cluster.markdown
Expand Up @@ -41,6 +41,52 @@ Running node will now share port 8000 between the workers:
This feature was introduced recently, and may change in future versions.
Please try it out and provide feedback.

## How It Works

<!--type=misc-->

The worker processes are spawned using the `child_process.fork` method,
so that they can communicate with the parent via IPC and pass server
handles back and forth.

When you call `server.listen(...)` in a worker, it serializes the
arguments and passes the request to the master process. If the master
process already has a listening server matching the worker's
requirements, then it passes the handle to the worker. If it does not
already have a listening server matching that requirement, then it will
create one, and pass the handle to the child.

This causes potentially surprising behavior in three edge cases:

1. `server.listen({fd: 7})` Because the message is passed to the worker,

This comment has been minimized.

Copy link
@koichik

koichik Jun 19, 2012

s/worker/master/ ?

This comment has been minimized.

Copy link
@isaacs

isaacs Jun 19, 2012

Author

Yes, that is correct. Good catch.

file descriptor 7 **in the parent** will be listened on, and the
handle passed to the worker, rather than listening to the worker's
idea of what the number 7 file descriptor references.
2. `server.listen(handle)` Listening on handles explicitly will cause
the worker to use the supplied handle, rather than talk to the master
process. If the worker already has the handle, then it's presumed
that you know what you are doing.
3. `server.listen(0)` Normally, this will case servers to listen on a
random port. However, in a cluster, each worker will receive the
same "random" port each time they do `listen(0)`. In essence, the
port is random the first time, but predictable thereafter. If you
want to listen on a unique port, generate a port number based on the
cluster worker ID.

When multiple processes are all `accept()`ing on the same underlying
resource, the operating system load-balances across them very
efficiently. There is no routing logic in Node.js, or in your program,
and no shared state between the workers. Therefore, it is important to
design your program such that it does not rely too heavily on in-memory
data objects for things like sessions and login.

Because workers are all separate processes, they can be killed or
re-spawned depending on your program's needs, without affecting other
workers. As long as there are some workers still alive, the server will
continue to accept connections. Node does not automatically manage the
number of workers for you, however. It is your responsibility to manage
the worker pool for your application's needs.

## cluster.settings

* {Object}
Expand Down
18 changes: 18 additions & 0 deletions doc/api/http.markdown
Expand Up @@ -158,6 +158,24 @@ a listener for the ['listening'](net.html#event_listening_) event.
See also [net.Server.listen()](net.html#server.listen).


### server.listen(handle, [listeningListener])

* `handle` {Object}
* `listeningListener` {Function}

The `handle` object can be set to either a server or socket (anything
with an underlying `_handle` member), or a `{fd: <n>}` object.

This will cause the server to accept connections on the specified
handle, but it is presumed that the file descriptor or handle has
already been bound to a port or domain socket.

Listening on a file descriptor is not supported on Windows.

This function is asynchronous. The last parameter `callback` will be added as
a listener for the ['listening'](net.html#event_listening_) event.
See also [net.Server.listen()](net.html#server.listen).

### server.close([cb])

Stops the server from accepting new connections.
Expand Down
19 changes: 19 additions & 0 deletions doc/api/net.markdown
Expand Up @@ -163,6 +163,25 @@ This function is asynchronous. When the server has been bound,
the last parameter `listeningListener` will be added as an listener for the
['listening'](#event_listening_) event.

### server.listen(handle, [listeningListener])

* `handle` {Object}
* `listeningListener` {Function}

The `handle` object can be set to either a server or socket (anything
with an underlying `_handle` member), or a `{fd: <n>}` object.

This will cause the server to accept connections on the specified
handle, but it is presumed that the file descriptor or handle has
already been bound to a port or domain socket.

Listening on a file descriptor is not supported on Windows.

This function is asynchronous. When the server has been bound,
['listening'](#event_listening_) event will be emitted.
the last parameter `listeningListener` will be added as an listener for the
['listening'](#event_listening_) event.

### server.close([cb])

Stops the server from accepting new connections and keeps existing
Expand Down
21 changes: 14 additions & 7 deletions lib/cluster.js
Expand Up @@ -193,7 +193,10 @@ if (cluster.isMaster) {

// This sequence of information is unique to the connection
// but not to the worker
var args = [message.address, message.port, message.addressType];
var args = [message.address,
message.port,
message.addressType,
message.fd];
var key = args.join(':');
var handler;

Expand All @@ -216,12 +219,14 @@ if (cluster.isMaster) {
worker.emit('listening', {
address: message.address,
port: message.port,
addressType: message.addressType
addressType: message.addressType,
fd: message.fd
});
cluster.emit('listening', worker, {
address: message.address,
port: message.port,
addressType: message.addressType
addressType: message.addressType,
fd: message.fd
});
};

Expand Down Expand Up @@ -508,12 +513,12 @@ cluster._setupWorker = function() {
};

// Internal function. Called by lib/net.js when attempting to bind a server.
cluster._getServer = function(tcpSelf, address, port, addressType, cb) {
cluster._getServer = function(tcpSelf, address, port, addressType, fd, cb) {
// This can only be called from a worker.
assert(cluster.isWorker);

// Store tcp instance for later use
var key = [address, port, addressType].join(':');
var key = [address, port, addressType, fd].join(':');
serverListeners[key] = tcpSelf;

// Send a listening message to the master
Expand All @@ -523,7 +528,8 @@ cluster._getServer = function(tcpSelf, address, port, addressType, cb) {
cmd: 'listening',
address: address,
port: port,
addressType: addressType
addressType: addressType,
fd: fd
});
});

Expand All @@ -532,7 +538,8 @@ cluster._getServer = function(tcpSelf, address, port, addressType, cb) {
cmd: 'queryServer',
address: address,
port: port,
addressType: addressType
addressType: addressType,
fd: fd
};

// The callback will be stored until the master has responded
Expand Down
60 changes: 47 additions & 13 deletions lib/net.js
Expand Up @@ -359,7 +359,9 @@ Socket.prototype._destroy = function(exception, cb) {

if (this.server) {
this.server._connections--;
this.server._emitCloseIfDrained();
if (this.server._emitCloseIfDrained) {
this.server._emitCloseIfDrained();
}
}
};

Expand Down Expand Up @@ -820,12 +822,33 @@ function toNumber(x) { return (x = Number(x)) >= 0 ? x : false; }


var createServerHandle = exports._createServerHandle =
function(address, port, addressType) {
function(address, port, addressType, fd) {
var r = 0;
// assign handle in listen, and clean up if bind or listen fails
var handle;

if (port == -1 && addressType == -1) {
if (typeof fd === 'number' && fd >= 0) {
var tty_wrap = process.binding('tty_wrap');
var type = tty_wrap.guessHandleType(fd);
switch (type) {
case 'PIPE':
debug('listen pipe fd=' + fd);
// create a PipeWrap
handle = createPipe();
handle.open(fd);
handle.readable = true;
handle.writable = true;
break;

default:
// Not a fd we can listen on. This will trigger an error.
debug('listen invalid fd=' + fd + ' type=' + type);
handle = null;
break;
}
return handle;

} else if (port == -1 && addressType == -1) {
handle = createPipe();
if (process.platform === 'win32') {
var instances = parseInt(process.env.NODE_PENDING_PIPE_INSTANCES);
Expand Down Expand Up @@ -855,14 +878,14 @@ var createServerHandle = exports._createServerHandle =
};


Server.prototype._listen2 = function(address, port, addressType, backlog) {
Server.prototype._listen2 = function(address, port, addressType, backlog, fd) {
var self = this;
var r = 0;

// If there is not yet a handle, we need to create one and bind.
// In the case of a server sent via IPC, we don't need to do this.
if (!self._handle) {
self._handle = createServerHandle(address, port, addressType);
self._handle = createServerHandle(address, port, addressType, fd);
if (!self._handle) {
process.nextTick(function() {
self.emit('error', errnoException(errno, 'listen'));
Expand Down Expand Up @@ -897,16 +920,16 @@ Server.prototype._listen2 = function(address, port, addressType, backlog) {
};


function listen(self, address, port, addressType, backlog) {
function listen(self, address, port, addressType, backlog, fd) {
if (!cluster) cluster = require('cluster');

if (cluster.isWorker) {
cluster._getServer(self, address, port, addressType, function(handle) {
cluster._getServer(self, address, port, addressType, fd, function(handle) {
self._handle = handle;
self._listen2(address, port, addressType, backlog);
self._listen2(address, port, addressType, backlog, fd);
});
} else {
self._listen2(address, port, addressType, backlog);
self._listen2(address, port, addressType, backlog, fd);
}
}

Expand All @@ -932,10 +955,21 @@ Server.prototype.listen = function() {
// The port can be found with server.address()
listen(self, null, null, backlog);

} else if (arguments[0] instanceof TCP) {
self._handle = arguments[0];
listen(self, null, -1, -1, backlog);

} else if (arguments[0] && typeof arguments[0] === 'object') {
var h = arguments[0];
if (h._handle) {
h = h._handle;
} else if (h.handle) {
h = h.handle;
}
if (h instanceof TCP) {
self._handle = h;
listen(self, null, -1, -1, backlog);
} else if (h.fd && typeof h.fd === 'number' && h.fd >= 0) {
listen(self, null, null, null, backlog, h.fd);
} else {
throw new Error('Invalid listen argument: ' + h);
}
} else if (isPipeName(arguments[0])) {
// UNIX socket or Windows pipe.
var pipeName = self._pipeName = arguments[0];
Expand Down
3 changes: 2 additions & 1 deletion test/simple/test-cluster-basic.js
Expand Up @@ -135,7 +135,8 @@ else if (cluster.isMaster) {
assert.equal(arguments.length, 1);
var expect = { address: '127.0.0.1',
port: common.PORT,
addressType: 4 };
addressType: 4,
fd: undefined };
assert.deepEqual(arguments[0], expect);
break;

Expand Down

0 comments on commit e733dc3

Please sign in to comment.