Building a TCP service using Node.js


This is the eighth article from a new series about Node.js. In the previous article we talked about how we can use CouchDB to deliver configuration changes without needing to restart your processes. In this one we’ll build a simple TCP service.

Building a Web application using a framework like PHP, Django or Rails can quickly lead to monolithic applications in which you try to cram in every little bit of functionality.

This happens not because of the web framework itself, but because these frameworks are designed to build consumer-facing apps and make it more difficult to do networked apps.

Fortunately, Node.js makes it easy for us to build and consume networked apps. With little effort, you can spread the functionalities of the application that you’re building into a set of processes that communicate with each other. By avoiding the “large monolithic application” anti-pattern from the start you can:

  • improve code mantainability by having smaller versioned services that do just one thing and do it well;
  • have fine-grained control over the scalability model of your application: not all services need to share the resource requirements, allowing you to adjust them independently;
  • have a more controlled failure mode: one process in one service can fail without bringing the whole system down.

Having one code repository per service can also help with:

  • making the API boundaries more explicit: you have to document the API contract for every service;
  • improving the test coverage: since the code base for each service is small in comparison with the monolithic application, you can easily test whether the service is working or not before releasing a new version, by having automatic tests with significant code coverage.

This approach comes with a few attached prices, specifically, the need for thorough release management, (in which you must manage all these modules, their versions, and their dependencies on other modules and versions). Additionally, having a set of different services that talk to each other also makes the system more challenging to deploy and manage.

In this series of articles I’ll start the analysis of some networking patterns. In this particular article we’ll start by building a simple TCP service, which we’ll then make evolve throughout this and future articles.

Building a TCP Service

The simplest form of a networking service we can start off with is TCP service. A TCP service enables another process to connect to this service and, once connected, to have a raw bi-directional stream of data.

Let’s implement the first form of this service:

raw_server.js:

var net = require('net');
var server = net.createServer();    
server.on('connection', handleConnection);
server.listen(9000, function() {    
  console.log('server listening to %j', server.address());  
});
function handleConnection(conn) {    
  var remoteAddress = conn.remoteAddress + ':' + conn.remotePort;  
  console.log('new client connection from %s', remoteAddress);
  conn.on('data', onConnData);  
  conn.once('close', onConnClose);  
  conn.on('error', onConnError);
  function onConnData(d) {  
    console.log('connection data from %s: %j', remoteAddress, d);  
    conn.write(d);  
  }
  function onConnClose() {  
    console.log('connection from %s closed', remoteAddress);  
  }
  function onConnError(err) {  
    console.log('Connection %s error: %s', remoteAddress, err.message);  
  }  
}

Here we start by creating the server object by calling .createServer on the net module. This gives us a server object. After this we make it listen to port 9000, printing the server address once the server is listening.

We also bind the handleConnection function to the connection event. The server emits this event every time there is a new TCP connection made to the server, passing in the socket object.

This socket object can emit several events: it emits a data event every time data arrives from the connected peer; a close event once that connection closes; and an error event if an error happens on the socket.

When the server gets data from a connection, it logs it to the console and writes it back into the connection:

function onConnData(d) {  
    console.log('connection data from %s: %j', remoteAddress, d);  
    conn.write(d);  
  }

Copying the input into the output makes this server an “echo” server. This is not particularly useful at the moment, but we’ll make this evolve.

We also catch all the error events the socket emits. Typically you will get ECONNRESET errors here when the socket connection has been abruptly closed by the other end. All socket errors end the connection, which means that the close event will be emitted next.

You can test this service by first starting the server:

$ node raw\_server.js  
server listening to {"address":"0.0.0.0","family":"IPv4","port":9000}

On a different terminal window you can then connect to our server using a command-line application like Telnet or Netcat:

$ nc localhost 9000  
hey    
hey    
^C

You can now type and hit the ENTER key. You’ll see every line you enter echoed by the server.

Also, you’ll see on the server logs that the server gets raw buffer data, printing out each byte:

new client connection from 127.0.0.1:57590    
connection data from 127.0.0.1:57590: \[104,101,121,10\]

An Example of a Simple Service

For the sake of giving an example, let’s say that, instead of building an echo service, this service will expect UTF-8 strings and will uppercase all the characters.

capitalizing_server_01.js:

var net = require('net');
var server = net.createServer();    
server.on('connection', handleConnection);
server.listen(9000, function() {    
  console.log('server listening to %j', server.address());  
});
function handleConnection(conn) {    
  var remoteAddress = conn.remoteAddress + ':' + conn.remotePort;  
  console.log('new client connection from %s', remoteAddress);
  conn.setEncoding('utf8');
  conn.on('data', onConnData);  
  conn.once('close', onConnClose);  
  conn.on('error', onConnError);
  function onConnData(d) {  
    console.log('connection data from %s: %j', remoteAddress, d);  
    conn.write(d.toUpperCase());  
  }
  function onConnClose() {  
    console.log('connection from %s closed', remoteAddress);  
  }
  function onConnError(err) {  
    console.log('Connection %s error: %s', remoteAddress, err.message);  
  }  
}

This server code is a copy of the echo server with two small additions:

  • When the client connects, we set the encoding to utf8. This makes the connection pass strings when emitting data events instead of raw buffers, allowing us to have a JavaScript string we can then transform.
  • Before we write that string back to the connection, we transform it to uppercase.

Now, stop the previous server if you’re still running it, and start this new one:

$ node capitalizing\_server\_01.js  
server listening to {"address":"0.0.0.0","family":"IPv4","port":9000}

You can now connect to that server — also using either Telnet or Netcat — type some text, getting back the uppercased version:

$ nc localhost 9000  
hello    
HELLO    
please pass me the salt    
PLEASE PASS ME THE SALT

A JSON Stream

So far our service has only dealt with strings, but for some applications we may need some more structured data. JSON is a common representation for structured data, providing just a few basic types: Booleans, strings, numbers, arrays and objects. What we want most of the time is to pass in JavaScript objects that represent complex commands, queries or results, one at a time, throughout time; but it happens that JSON isn’t particularly stream-friendly: typically you want to transmit a complex structure to the other side — either an object or an array — and JSON is only valid once the whole object is transmitted.

A slight alteration of the JSON protocol enables us to use streaming: if none of the transmitted objects, when serialised, contain new-line characters, we can safely separate each one of the main objects with a new-line character. Fortunately, the default JSON encoding function available in Node.js doesn’t introduce any new-line character, and for those existing in strings, it encodes them with two characters: “\n”.

If you sent two objects down such a stream, they would be encoded for transmission like this:

{"a":1,"b":true,c:"this is a newline-terminated string\\n"}  
{"d":2,"e":false,f:"this is another newline-terminated string\\n"}

We can then easily create a service that accepts and replies with this encoding by using the json-duplex-stream module. Using this module we can let it create two streams for us:

  • one through stream that parses raw data and outputs the contained JavaScript objects;
  • another through stream that accepts JavaScript objects and outputs them JSON-encoded, with a new-line character at the end.

Example: An Events Gateway

Using this, we’re going to create a TCP server that accepts events from domotic devices (like thermometers, barometers, presence sensors, lights, etc.). Here are some requirements for this service:

  • Each device connects to this service and sends it events throughout time.
  • The gateway service saves each of these events into a persisted queue for further processing by other processes.
  • Each received object represents an event, and each event has a unique id field.
  • Once an event is saved into the queue, the gateway replies with a message confirming that the event with the given id was saved.

Let’s build this system.

First we have to install the json-duplex-stream package:

$ npm install json-duplex-stream

gateway_server.js:

var net = require('net');    
var server = net.createServer();    
var JSONDuplexStream = require('json-duplex-stream');
var Gateway = require('./gateway')
server.on('connection', handleConnection);    
server.listen(8000, function() {    
  console.log('server listening on %j', server.address());  
});
function handleConnection(conn) {    
  var s = JSONDuplexStream();  
  var gateway = Gateway();  
  conn.  
    pipe(s.in).  
    pipe(gateway).  
    pipe(s.out).  
    pipe(conn);
  s.in.on('error', onProtocolError);  
  s.out.on('error', onProtocolError);  
  conn.on('error', onConnError);
  function onProtocolError(err) {  
    conn.end('protocol error:' + err.message);  
  }  
}
function onConnError(err) {    
  console.error('connection error:', err.stack);  
}

As in the previous examples, we’re creating the server using net.createServer(). We’re then requiring the json-duplex-stream module to be used later.

Then we require the local gateway.js module, which implements our gateway logic.

After that we set a server connection handler named handleConnection, which gets called when a client connects to our server. Once that happens, we instantiate a JSON Duplex Stream and a gateway, and wire up the connection to the input side of the JSON Duplex Stream. When this JSON Duplex Stream instance gets valid JSON over the connection, it outputs the parsed JavaScript objects, which we then pipe into our gateway instance.

Our gateway instance is a transform stream, and every object it outputs is piped to the output side of the JSON Duplex Stream. This serialises each of the objects, outputting JSON strings that get piped back to the client. Here is a quick diagram of that flow:

connection (conn) => JSON Duplex Stream input handler (s.in) =>    
Gateway (gateway) => JSON Duplex Stream output handler (s.out) => connection (conn)

After we set up this pipeline, we need to handle protocol errors by listening to errors emitted by the streams s.in and s.out. The first one emits errors if the input data is not valid JSON; and the second one can emit errors if it’s unable to encode an object into JSON for some reason. We handle any of these errors by writing the error message into the connection and closing it.

Let’s then implement the core of our application, the gateway.js module:

gateway.js:

var extend = require('util').\_extend;    
var inherits = require('util').inherits;    
var Transform = require('stream').Transform;
module.exports = Gateway;
inherits(Gateway, Transform);
var defaultOptions = {    
  highWaterMark: 10,  
  objectMode: true  
};
function Gateway(options) {    
  if (! (this instanceof Gateway)) {  
    return new Gateway(options);  
  }
  options = extend({}, options || {});  
  options = extend(options, defaultOptions);
  Transform.call(this, options);  
}  

/// \_transform
Gateway.prototype.\_transform = \_transform;
function **\_transform**(event, encoding, callback) {    
  if (! event.id)  
    return handleError(new Error('event doesn\\'t have an \`id\` field'));
  pushToQueue(event, pushed);
  function pushed(err) {  
    if (err) {  
      handleError(err);  
    }  
    else {
      reply = {  
        id: event.id,  
        success: true  
      };
      callback(null, reply);  
    }  
  }
  function handleError(err) {  
    var reply = {  
      id: event.id,  
      success: false,  
      error: err.message  
    };
    callback(null, reply);  
  }  
};  

/// Fake push to queue
function pushToQueue(object, callback) {    
  setTimeout(callback, Math.floor(Math.random() \* 1000));  
}

Our gateway is a transform stream, inheriting from streams.Transform. As we saw, this stream is a duplex stream (accepts data and outputs data) and must implement the _transform method. In this method we get the event, which is a JavaScript object by now, and we take the chance to validate it and push it into the queue (using the fake pushToQueue function). If the push operation was successful we reply with an object that bears the same event id and a success attribute set to true, to indicate that the event was accepted. Otherwise, if there was an error, we write back a reply object containing the event id, the success attribute set to false, and an error message.

We can now test this service. Start by launching the server:

$ node gateway\_server  
server listening on {"address":"0.0.0.0","family":"IPv4","port":8000}

We can then use netcat or telnet to connect to the server using the command line:

$ nc localhost 8000

Once we are connected we’re acting like a device, and can start writing events as if one, hitting the _Return_key at the end of each one:

{ "when": "2014-08-06T13:36:31.735Z", "type": "temperature", "reading": 23.4, "units": "C", "id": "5f18453d-1907-48bc-abd2-ab6c24bc197d" }

For each correct one you enter you should get back a confirmation:

{"id":"5f18453d-1907-48bc-abd2-ab6c24bc197d","success":true}

Next article

In a previous article we covered the Event Emitter pattern, which allows you to somewhat detach the producer of events from the consumer. This pattern may also be useful for providing a means of communication between two processes: process A connects to process B, and they can both send events to each other. We’ll cover the remote emitter in the next article.

Written by Pedro Teixeira— published for YLD.

Interested in Node? Read more about it:


Written by YLDFebruary 2nd, 2016


Share this article