You want to build a networked application in Node.js and utilize sockets to communicate between instances.
Solution
Sockets are native to the Node.js net module. This means that if you wish to utilize sockets, you need to require the net module in your script. You will then create a new instance of a socket by calling the Socket() constructor. Then to connect a socket, you simply create a connection with the socket.connect() method, directing the socket to which port and host you wish to connect (see Listing 2-25).
Listing 2-25. Creating a Socket Connection var net = require('net');
var socket = new net.Socket();
socket.connect(/* port */ 8181, /*host*/ '127.0.0.1' /, *callback*/ );
Assuming there is a connection that can be made on port 8181 of the localhost, you now have a socket connected to this server. At this point, there is nothing but a connected stream via this socket. Any data transmitted will be lost. Now let us take a closer look at a socket connection to a simple server to share messages between each other. To do this you can create a simple server (Listing 2-26) that will listen for the socket and its data as well as send a response back to the sockets.
Listing 2-26. Server to Communicate to Sockets var net = require('net');
var server = net.createServer(connectionListener); server.listen(8181, '127.0.0.1');
function connectionListener(conn) {
console.log('new client connected'); //greet the client
conn.write('hello');
// read what the client has to say and respond conn.on('readable', function() {
var data = JSON.parse(this.read()); if (data.name) { this.write('hello ' + data.name); } }); //handle errors conn.on('error', function(e) { console.log('' + e);
Chapter 2 ■ NetworkiNg with Node.js
This server will listen for a connection, and then greet the new connection with a “hello” via the socket stream. It will also listen for data from the socket, which is expected to be a JSON object in this case. You can then parse the data from the “readable” stream and return a response with the data parsed.
The socket connection, shown in Listing 2-27, shows how you can create this socket that will connect to the server from Listing 2-26 and send communication between the two.
Listing 2-27. Socket Connection var net = require('net');
var socket = new net.Socket(/* fd: null, type: null, allowHalfOpen: false */); socket.connect(8181, '127.0.0.1' /*, connectListener replaces on('connect') */); socket.on('connect', function() {
console.log('connected to: ' + this.remoteAddress); var obj = { name: 'Frodo', occupation: 'adventurer' }; this.write(JSON.stringify(obj));
});
socket.on('error', function(error) { console.log('' + error);
// Don't persist this socket if there is a connection error socket.destroy();
});
socket.on('data', function(data) {
console.log('from server: ' + data); });
socket.setEncoding('utf-8'); /* utf8, utf16le, ucs2, ascii, hex */ socket.setTimeout(2e3 /* milliseconds */ , function() {
console.log('timeout completed');
var obj = { name: 'timeout', message: 'I came from a timeout'}; this.write(JSON.stringify(obj));
});
Putting the server and the client server together—running the server first, so that your socket has an endpoint to connect to—you are able to successfully communicate with the socket connections. The initiating server console will look like Listing 2-28 while the client server output will look like Listing 2-29.
Listing 2-28. Server Output $ node server.js
new client connected
Listing 2-29. Connected Socket Output $ node socket.js
Connected to: 127.0.0.1 From server: hellohello Frodo Timeout completed
Chapter 2 ■ NetworkiNg with Node.js
How It Works
A net.Socket connection, as you have seen, is a Node.js object that represents a TCP or UNIX socket. In Node.js this means that it implements a duplex stream interface. A duplex stream in node represents two event emitters, objects that publish events in Node.js. The two event emitters that make up duplex streams are readable streams and writable streams, which you can see from the Node.js duplex stream source in Listing 2-30.
Listing 2-30. Duplex Stream Calls Readable and Writable Streams function Duplex(options) {
if (!(this instanceof Duplex)) return new Duplex(options); Readable.call(this, options); Writable.call(this, options);
if (options && options.readable === false) this.readable = false;
if (options && options.writable === false) this.writable = false;
this.allowHalfOpen = true;
if (options && options.allowHalfOpen === false) this.allowHalfOpen = false;
this.once('end', onend); }
The readable streams interface will take in data from a stream buffer and emit it as a data event on the socket. The writable streams, on the other hand, will emit data in the form of a write, or an end, event. Together these make up a socket. The socket has a few interesting properties and methods, some of which you utilized in Listings 2-26 and 2-27 to create your socket communication servers.
In the first server instance, within the connectionListener callback, the conn argument was passed. Because a net.Server object is really a socket that will listen for connections, this conn argument represents the socket you want to work with. The very first thing this server does is emit a greeting to the connection. This happens with
conn.write('hello'); which is a socket.write() method.
The socket.write() method takes one required argument, the data that are to be written, and two optional arguments. These optional arguments are encoding, which can be used to set the encoding type of the socket. The encoding defaults to utf8 but other valid values are utf-8, utf16le, ucs2, ascii, and hex.
Next, in the server’s connectionListener the socket was bound to the readable event. This readable event is from the stream module. This event is triggered each time that the stream sends data that are ready to be read. The way to retrieve the data that are sent via the readable event is to call the read() event to read the data. In the example from Listing 2-26, you expect the data to be a JSON string, which you can then parse to reveal the JSON object. This is then used to emit another message back to the connection via the write() method.
The final event binding in place on the server is a handling of errors by binding to the error event on the connection. Without this, the server will crash anytime an error occurs on the connection. This could be a killed connection, or any other error, but regardless of the type of error, there is nothing better to place on your Node.js code (or any code for that matter) than great error-handling capabilities.
Now look at the socket connections you made in Listing 2-27. This shows the other side of our communication story. It starts with the instantiation of a new net.Socket(). In the example, there are no arguments passed to the
Chapter 2 ■ NetworkiNg with Node.js
fd is the file descriptor, or what the socket handle should be; this defaults to null. The type key also defaults to a null value but can take the values tcp4, tcp6, or unix to determine the type of the socket you wish to instantiate. Again, as you have seen in previous sections, the allowHalfOpen option can be set to allow the socket to remain open once the initial FIN packet is transferred.
In order to connect the socket, you call the connect() event on the socket, with the host and port specified. This initializes the TCP or UNIX socket handle, beginning the connection. The host argument is optional, as is a callback function that was omitted in the example. The callback in the example was replaced, because the callback function on the connect function is the same as the socket.on('connect', ...) event listener, which is bound to the socket in our example listening for the connection to be established.
Inside of the connect event callback, the first thing your solution does is to gain a little knowledge about the connection by logging the remoteAddress() of the socket. You will see more about getting information about connected servers in the following section of this chapter. After this information, you create an object with some information, make it a string using the JSON.stringify method, then send it along the socket with the write() method. The object must be encoded as a string; if not, the write method will fail, as shown in Listing 2-31. Listing 2-31. socket.write from Node.js net Module
Socket.prototype.write = function(chunk, encoding, cb) { if (typeof chunk !== 'string' && !Buffer.isBuffer(chunk)) throw new TypeError('invalid data');
return stream.Duplex.prototype.write.apply(this, arguments); };
The socket is then bound to the error event. This event will handle all errors from the socket, but one thing that is noteworthy here is that once the error is handled, via the callback provided to the on('error') listener, the socket. destroy(); method is invoked. The destroy method provides a useful and graceful way to prevent any further I/O activity from happening and to close a socket. It goes through the closing of the socket handle, emitting any error callbacks as necessary during the destroy process. Finally, the close event is emitted after the handle is closed, as shown in Listing 2-32.
Listing 2-32. Closing a Socket within socket.destroy( ) Socket.prototype._destroy = function(exception, cb) { debug('destroy');
var self = this;
function fireErrorCallbacks() { if (cb) cb(exception);
if (exception && !self.errorEmitted) { process.nextTick(function() { self.emit('error', exception); }); self.errorEmitted = true; } }; if (this.destroyed) {
debug('already destroyed, fire error callbacks'); fireErrorCallbacks();
return; }
Chapter 2 ■ NetworkiNg with Node.js
self._connecting = false;
this.readable = this.writable = false; timers.unenroll(this);
debug('close'); if (this._handle) {
if (this !== process.stderr) debug('close handle');
var isException = exception ? true : false; this._handle.close(function() { debug('emit close'); self.emit('close', isException); }); this._handle.onread = noop; this._handle = null; } fireErrorCallbacks(); this.destroyed = true; if (this.server) { COUNTER_NET_SERVER_CONNECTION_CLOSE(this); debug('has server'); this.server._connections--; if (this.server._emitCloseIfDrained) { this.server._emitCloseIfDrained(); } } };
After the error handler for your socket, the socket is bound to the data event. This event will produce the data from a readable stream sent from the connection, essentially calling the stream.read( ) method and emitting that as the data event. This provides a useful place to parse and process the information sent from the connection.
As you saw above, for the write( ) method on the socket, there is the option to set the encoding for the data that are sent via the socket buffer. This can be configured for the entire socket by setting the setEncoding(<type>) parameter on the socket. In the example above, it was set to the default for strings of utf-8 but could be changed to any of the valid types of encoding. Changing this setting to each of the valid types causes different output, as shown in Listing 2-33. Listing 2-33. Variations on Encoding
# utf8
connected to: 127.0.0.1 from server: hello from server: hello Frodo timeout completed
from server: hello timeout # hex
Chapter 2 ■ NetworkiNg with Node.js from server: 68656c6c6f2046726f646f timeout completed from server: 68656c6c6f2074696d656f7574 # ucs2 connected to: 127.0.0.1 from server: 敨汬 from server: 桯汥潬䘠潲潤 timeout completed from server: 敨汬楴敭畯 # ascii connected to: 127.0.0.1 from server: hello from server: hello Frodo timeout completed
from server: hello timeout # utf16le
connected to: 127.0.0.1 from server: hello from server: hello Frodo timeout completed
from server: hello timeout
Finally, you saw that the socket can “wait” by using the setTimeout function. The setTimeout takes a parameter that dictates the number of milliseconds you choose to wait, and a callback. In the example application, this was used to send a message to the connection, from the socket on a two-second delay. For the callback to work (shown in Listing 2-34), the number of milliseconds must be greater than zero, finite, and not a number (NaN). If this is the case, then Node.js will add this callback to the list of timers and emit the timeout event once the timeout has occurred. Listing 2-34. socket.setTimeout
Socket.prototype.setTimeout = function(msecs, callback) { if (msecs > 0 && !isNaN(msecs) && isFinite(msecs)) { timers.enroll(this, msecs); timers.active(this); if (callback) { this.once('timeout', callback); } } else if (msecs === 0) { timers.unenroll(this); if (callback) { this.removeListener('timeout', callback); } } };
There are other events and parameters on a net.Socket that were not covered in the socket example from Listing 2-27. These are outlined in Table 2-1 below.
Chapter 2 ■ NetworkiNg with Node.js
These properties and events will be covered in more detail in the final two sections of this chapter in which you will discover how to retrieve details about connected servers and how to control these properties and details within the sockets themselves.