- Mastering Node.js
- Sandro Pasquali
- 2167字
- 2021-07-21 18:17:13
Listening for events
In the previous chapter we were introduced to the EventEmitter
interface. This is the primary event interface we will be encountering as we move chapter to chapter, as it provides the prototype class for the many Node objects exposing evented interfaces, such as file and network streams. Various close
, exit
, data
, and other events exposed by different module APIs signal the presence of an EventEmitter
interface, and we will be learning about these modules and use cases as we progress.
Instead, the primary purpose of this section is to discuss some lesser-known event sources—signals, child process communication, filesystem change events, and deferred execution.
Signals
In many ways, evented programming is like hardware interrupt programming. Interrupts do exactly what their name suggests. They use their ability to interrupt whatever a controller or the CPU or any other device is doing, demanding that their particular need be serviced immediately.
In fact, the Node process
object exposes standard Portable Operating System Interface (POSIX) signal names, such that a node process can subscribe to these system events.
A signal is a limited form of inter-process communication used in Unix, Unix-like, and other POSIX-compliant operating systems. It is an asynchronous notification sent to a process or to a specific thread within the same process in order to notify it of an event that occurred. |
||
--http://en.wikipedia.org/wiki/POSIX_signal |
This is a very elegant and natural way to expose a Node process to operating system (OS) signal events. One might configure listeners to catch signals instructing a Node process to restart or update some configuration files or simply clean up and shut down.
For example, the SIGINT signal is sent to a process when its controlling terminal detects a Ctrl-C (or equivalent) keystroke. This signal tells a process that an interrupt has been requested. If a Node process has bound a callback to this event, that function might log the request prior to terminating, do some other cleanup work, or even ignore the request:
setInterval(function() {}, 1e6); process.on('SIGINT', function() { console.log('SIGINT signal received'); process.exit(1); })
Here we have set up a far future interval such that the process does not immediately terminate, and a SIGINT listener. When a user sends a Ctrl-C interrupt to the terminal controlling this process, the message SIGINT signal received will be written on the terminal and the process will terminate.
Now consider a situation in which a Node process is doing some ongoing work, such as parsing logs. It might be useful to be able to send that process a signal, such as update your configuration files or restart the scan. You may want to send such signals from the command line. You might prefer to have another process do so—a practice known as Inter-Process Communication (IPC).
Create a file named ipc.js
containing the following code:
setInterval(function() {}, 1e6); process.on('SIGUSR1', function() { console.log('Got a signal!'); });
SIGUSR1
(and SIGUSR2
) are user-defined signals (they are triggered by no specific action). This makes them ideal signals for custom functionality.
To send a command to a process you must determine its process ID (PID). With a PID in hand, processes can be addressed, and therefore communicated with. If the PID assigned to ipc.js
after being run through Node is 123
, then we can send that process a SIGUSR1
signal using the following command line:
kill –s SIGUSR1 123
Note
A simple way to find the PID for a given Node process in UNIX is to search the system process list for the name of the program that says the process is running. If ipc.js
is currently executing, its PID is found by entering the following command line in the console/terminal:
ps aux | grep ipc.js
Try it.
Forks
A fundamental part of Node's design is to create or fork processes when parallelizing execution or scaling a system—as opposed to creating a thread pool, for instance. We will be using child processes in various ways throughout this book, and learn how to create and use them. Here the focus will be on understanding how communication events between child processes are to be handled.
To create a child process one need simply call the fork
method of the child_process
module, passing it the name of a program file to execute within the new process:
var cp = require('child_process'); var child = cp.fork(__dirname + '/lovechild.js');
In this way any number of subprocesses can be kept running. Additionally, on multicore machines, forked processes will be distributed (by the OS) to different cores. Spreading node processes across cores (even other machines) and managing IPC is (one) way to scale a Node application in a stable, understandable, and predictable way.
Extending the preceding, we can now have the forking process (parent) send, and listen for, messages from the forked process (child):
child.on('message', function(msg) { console.log('Child said: ', msg); }); child.send("I love you");
Similarly, the child process (its program is defined in lovechild.js
) can send and listen for messages:
// lovechild.js process.on('message', function(msg) { console.log('Parent said: ', msg); process.send("I love you too"); });
Running parent.js
should fork a child process and send that child a message. The child should respond in kind:
Parent said: I love you Child said: I love you too
Another very powerful idea is to pass a network server an object to a child. This technique allows multiple processes, including the parent, to share the responsibility for servicing connection requests, spreading load across cores.
For example, the following program will start a network server, fork a child process, and pass this child the server reference:
var child = require('child_process').fork('./child.js'); var server = require('net').createServer(); server.on('connection', function(socket) { socket.end('Parent handled connection'); }); server.listen(8080, function() { child.send("The parent message", server); });
In addition to passing a message to a child process as the first argument to send
, the preceding code also sends the server handle to itself as a second argument. Our child server can now help out with the family's service business:
// child.js process.on('message', function(msg, server) { console.log(msg); server.on('connection', function(socket) { socket.end('Child handled connection'); }); });
This child process should print out the sent message to your console, and begin listening for connections, sharing the sent server handle. Repeatedly connecting to this server at localhost:8080
will result in either Child handled connection or Parent handled connection being displayed; two separate processes are balancing the server load. It should be clear that this technique, when combined with the simple inter-process messaging protocol discussed previously, demonstrates how Ryan Dahl's creation succeeds in providing an easy way to build scalable network programs.
Note
We will discuss Node's new cluster
module, which expands (and simplifies) the previously discussed technique in later chapters. If you are interested in how server handles are shared, visit the cluster
documentation at the following link:
http://nodejs.org/api/cluster.html
For those who are truly curious, examine the cluster
code itself at:
https://github.com/joyent/node/blob/c668185adde3a474585a11f172b8387e270ec23b/lib/cluster.js#L523-558
File events
Most applications make some use of the filesystem, in particular those that function as web services. As well, a professional application will likely log information about usage, cache pre-rendered data views, or make other regular changes to files and directory structures.
Node allows developers to register for notifications on file events through the fs.watch
method. The watch
method will broadcast changed events on both files and directories.
watch
accepts three arguments, in order:
- The file or directory path being watched. If the file does not exist an ENOENT (no entity) error will be thrown, so using
fs.exists
at some prior useful point is encouraged. - An optional options object:
- persistent (Boolean): Node keeps processes alive as long as there is "something to do". An active file watcher will by default function as a persistence flag to Node. Setting this option to false flags not keeping the general process alive if the watcher is the only activity keeping it running.
- The listener function, which receives two arguments:
- The name of the change event (one of rename or change).
- The filename that was changed (important when watching directories).
This example will set up a watcher on itself, change its own filename, and exit:
var fs = require('fs'); fs.watch(__filename, { persistent: false }, function(event, filename) { console.log(event); console.log(filename); }) setImmediate(function() { fs.rename(__filename, __filename + '.new', function() {}); });
Two lines, rename
and the name of the original file, should have been printed to the console.
Watcher channels can be closed at any time using the following code snippet:
var w = fs.watch('file', function(){}) w.close();
It should be noted that fs.watch
depends a great deal on how the host OS handles file events, and according to the Node documentation:
"The
fs.watch
API is not 100% consistent across platforms, and is unavailable in some situations."
The author has had very good experiences with the module across many different systems, noting only that the filename argument is null
in callbacks on OS X implementations. Nevertheless, be sure to run tests on your specific architecture—trust, but verify.
Deferred execution
One occasionally needs to defer the execution of a function. Traditional JavaScript uses timers for this purpose, the well-known setTimeout
and setInterval
functions. Node introduces another perspective on defers, primarily as means of controlling the order in which a callback executes in relation to I/O events, as well as timer events properly.
We'll learn more about this ordering in the event loop discussion that follows. For now we will examine two types of deferred event sources that give a developer the ability to schedule callback executions to occur either before, or after, the processing of queued I/O events.
A method of the native Node process
module, process.nextTick
is similar to the familiar setTimeout
method in which it delays execution of its callback function until some point in the future. However, the comparison is not exact; a list of all requested nextTick
callbacks are placed at the head of the event queue and is processed, in its entirety and in order, before I/O or timer events and after execution of the current script (the JavaScript code executing synchronously on the V8 thread).
The primary use of nextTick
in a function is to postpone the broadcast of result events to listeners on the current execution stack until the caller has had an opportunity to register event listeners—to give the currently executing program a chance to bind callbacks to EventEmitter.emit
events. It may be thought of as a pattern used wherever asynchronous behavior should be emulated. For instance, imagine a lookup system that may either fetch from a cache or pull fresh data from a data store. The cache is fast and doesn't need callbacks, while the data I/O call would need them. The need for callbacks in the second case argues for emulation of the callback behavior with nextTick
in the first case. This allows a consistent API, improving clarity of implementation without burdening the developer with the responsibility of determining whether or not to use a callback.
The following code seems to set up a simple transaction; when an instance of EventEmitter
emits a start
event, log "Started" to the console:
var events = require('events'); function getEmitter() { var emitter = new events.EventEmitter(); emitter.emit('start'); return emitter; } var myEmitter = getEmitter(); myEmitter.on("start", function() { console.log("Started"); });
However, the expected result will not occur. The event emitter instantiated within getEmitter
emits "start"
previous to being returned, wrong-footing the subsequent assignment of a listener, which arrives a step late, missing the event notification.
To solve this race condition we can use process.nextTick
:
var events = require('events'); function getEmitter() { var emitter = new events.EventEmitter(); process.nextTick(function() { emitter.emit('start'); }); return emitter; } var myEmitter = getEmitter(); myEmitter.on('start', function() { console.log('Started'); })
Here the attachment of the on(start
handler is allowed to occur prior to the emission of the start event by the emitter instantiated in getEmitter
.
Because it is possible to recursively call nextTick
, which might lead to an infinite loop of recursive nextTick
calls (starving the event loop, preventing I/O), there exists a failsafe mechanism in Node which limits the number of recursive nextTick
calls evaluated prior to yielding the I/O: process.maxTickDepth
. Set this value (which defaults to 1000) if such a construct becomes necessary—although what you probably want to use in such a case is setImmediate
.
setImmediate
is technically a member of the class of timers (setInterval
, setTimeout
). However, there is no sense of time associated with it—there is no number of milliseconds to wait argument to be sent. This method is really more of a sister to process.nextTick
, differing in one very important way; while callbacks queued by nextTick
will execute before I/O and timer events, callbacks queued by setImmediate
will be called after I/O events.
This method does reflect the standard behavior of timers in that its invocation will return an object which can be passed to cancelImmediate
, cancelling setImmediate
in the same way cancelTimeout
cancels timers set with setTimeout
.