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:
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:
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:
On a different terminal window you can then connect to our server using a command-line application like Telnet or Netcat:
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:
newclientconnectionfrom
127.0.0.1:57590
connectiondatafrom
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:
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:
You can now connect to that server — also using either Telnet or Netcat — type some text, getting back the uppercased version:
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:
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:
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:
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 Returnkey at the end of each one:
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.