Los servicios de red tienen dos objetivos: conectar dos puntos y transmitir informacion entre ellos.
No importa que tipo de información estemos transmitiendo, siempre deberemos hacer una conexión primero.
La conexión de un Socket TCP
consiste en dos puntos finales. Un punto final se une a un puerto numerado mientras que el otro se conecta a un puerto.
Esto funciona como en los sistemas telefónicos.
En Node las operaciones de enlace y conexión son proporcionadas por el módulo net
.
'use strict';
const
net = require('net'),
server= net.createServer(connection => {
});
server.listen(60300);
El método net.createServer
coge una callback
y devuelve un objeto servidor. La callback
se invoca cuando se conecta otro punto. El parámetro de conexión es un objecto Socket
que se puede usar para enviar y recibir datos.
El método server.listen
nos permite escuchar por el puerto indicado.
'use strict'
const fs = require('fs');
const net = require('net');
const filename = process.argv[2];
/* Comprobamos que se introdujo nombre de fichero*/
if(!filename) {
throw Error('Error: No filename specified.');
}
/* Establecemos la conexion y escuchamos por el puerto indicado*/
net.createServer( connection => {
//Reporting.
console.log('Subscriber connected.');
connection.write(`Now watching "${filename}" for changes...\n`);
//watcher setup.
const watcher =
fs.watch(filename, () => connection.write(`File changed: ${new Date()}\n`));
//Cleanup.
connection.on('close', () => {
console.log('Subscriber disconnected.');
watcher.close();
});
}).listen(60300, () => console.log('Listening for subscribers...'));
Ecribimos este código con el nombre net-watcher.js
. Como podemos ver le pasamos el nombre del fichero a observar como argumento. En caso de que el usuario no proporcione ninguno devolveremos un error personalizado.
Con el método connection.write
podemos escribir en el cliente y con watcher.close
cerramos la conexión.
Invocamos server.listen
cuando se enlaza correctamente con el puerto especificado y esta listo para recibir conexiones.
Para probar el fichero net-watcher.js
necesitamos 3 terminales:
- Una de servidor.
- Una de cliente.
- Una para los cambios en el fichero.
En una terminal usaremos el comando watch -n 1 touch target.txt
para modificar el fichero cada segundo.
En la segunda terminal ejecutaremos el servidor node net-watcher.js target.txt
.
Por último, en una tercera terminal, usaremos Netcat para conectarnos. Usaremos nc localhost 60300
. Netcat es un programa de utilidad de Socket.
Un esquema detallado de lo que sucede sería el siguiente.
Para ver como funciona net
en los sockets Unix modificamos el programa anterior .
Cambiamos el listen final por listen(‘/tmp/watcher.sock’, () => console.log(‘Listening for subscribers...’));
.
Este archivo lo guardamos como net-watcher-unix.js
.
NOTA: si se recibe un error EADDRINUSE debe eliminar el watcher.sock antes de ejecutar el programa de nuevo.
La ejecución será igual que la anterior pero al nc
le añadiremos la opción -U
seguido de /tmp/watcher.sock
.
Los sockets Unix pueden ser más rápidos que los sockets TCP porque no requieren invocar al hardware de red.
Un protocolo es un conjunto de reglas que definen como se comunican los puntos en un sistema. En Node
estaremos trabajando con uno o más protocolos. Vamos a crear un protocolo basado en pasar mensajes JSON
a través de TCP
.
Cada mensaje será un objeto serializado JSON. Basicamente es un hash clave - valor.
{“key: “value”, “anotherKey”: “anotherValue”}
El servicio net-watcher
que hemos creado envía dos tipos de mensajes que necesitamos convertir a JSON:
- Cuando la conexión se establece por primera vez.
- Cuando el fichero se modifica.
Podemos codificar el primer tipo de la siguiente manera:
{“type”: “watching”, “file”: “target.txt”}
Y el segundo de la siguiente:
{“type”: “changed”, “timestamp”: 1358175733785}
El campo timestamp
contiene un valor entero que representa el número de milisegundos desde la medianoche del 1 de enero de 1970. Podemos obtener la fecha actual con Date.now
.
Destacar que no usamos saltos de linea en nuestro mensaje JSON. Utilizamos en este caso los saltos de linea para separar los mensajes. Esto sería JSON delimitado por lineas (LDJ).
A continuación modificamos el servicio net-watcher
para emplear el protocolo que hemos definido.
Nuestra tarea es usar JSON.stringify
para codificar objectos de mensaje y enviarlos mediante connection.write
. Con JSON.stringify
lo que hacemos es coger un objeto JavaScript y devolver un string en forma de JSON.
Lo que haremos es modificar el connection.write
.
'use strict'
const fs = require('fs');
const net = require('net');
const filename = process.argv[2];
/* Comprobamos que se introdujo nombre de fichero*/
if(!filename) {
throw Error('Error: No filename specified.');
}
/*Establecemos la conexion y escuchamos por el puerto indicado*/
net.createServer( connection => {
//Reporting.
console.log('Subscriber connected.');
connection.write(JSON.stringify({type: 'watching', file: filename}) + '\n');
//watcher setup.
const watcher = fs.watch(filename, () => connection.write(
JSON.stringify({type: 'changed', timestamp: Date.now()}) + '\n'));
//Cleanup.
connection.on('close', () => {
console.log('Subscriber disconnected.');
watcher.close();
});
}).listen(60300, () => console.log('Listening for subscribers...'))
Ahora ejecutamos el nuevo archivo guardado como net-watcher-json-service.js
'use strict'
const net = require('net');
const client = net.connect({port: 60300});
/*Conectamos por el puerto indicado y en caso de recibir datos los procesamos*/
client.on('data', data => {
const message = JSON.parse(data);
if(message.type === 'watching') {
console.log(`Now watching: ${message.file}`);
} else if (message.type === 'changed') {
const date = new Date(message.timestamp);
console.log(`File changed: ${date}`);
} else {
console.log(`Unrecognized message type: ${message.type}`);
}
});
Este programa es un pequeño cliente que utiliza net.connect
para crear una conexión cliente en el puerto especificado del localhost. Cuando llega algún dato es analizado y se muestra adecuadamente por consola, hasta ahra no hemos tenido en cuenta el manejo de errores.
En el mejor de los casos los mensajes llegarán a la vez. El problema es que a veces llegaran en diferentes pedazos de datos. Necesitamos lidiar con este problema cuando ocurra.
El protocolo LDJ que desarrollamos anteriormente separa los mensajes con nuevas lineas.
Si llegara un mensaje separado llegaria como dos datos. Quedaría algo así:
Implementaremos un servicio de pruebas que divide a propósito un mensaje en múltiples partes.
'use strict';
const server = require('net').createServer(connection => {
console.log('Subscriber connected.');
//Two message chunks that together make a whole message.
const firstChunk = '{"type": "changed", "timesta';
const secondChunk = 'mp": 1450694370094}\n';
//Send the first chunk inmeditely.
connection.write(firstChunk);
//After a short delay, send the other chunk.
const timer = setTimeout(() => {
connection.write(secondChunk);
connection.end();
}, 100);
//Clear timer when the connection ends.
connection.on('end', () => {
clearTimeout(timer);
console.log('Subscriber disconnected.');
});
});
server.listen(60300, function() {
console.log('Test server listening for subscribers...');
});
Una vez guardado ejecutamos y comprobamos el error de que solo recibe en primer lugar un fragmento del mensaje. Como ya comentamos lo que hace es parsear el mensaje que le llega y como solo coge lo primero que le llegó salta el error al estar incompleto.
El programa cliente tiene dos tareas que hacer. Una es almacenar los datos entrantes en mensajes. La otra es manejar cada mensaje cuando llega.
En lugar de agrupar estos dos trabajos en un solo programa, lo correcto es convertir al menos uno de ellos en un módulo.
Para liberar al programa cliente del peligro de dividir los mensajes JSON, implementaremos un módulo de cliente de buffer LDJ.
Este código configura LDJClient para heredar de EventEmitter.
const EventEmitter = require ('events').EventEmitter;
class LDJClient extends EventEmitter {
constructor(stream){
super();
let buffer = '';
stream.on('data', data => {
buffer += data;
let boundary = buffer.indexOf('\n');
while(boundary !== -1) {
const input = buffer.substring(0, boundary);
buffer = buffer.substring(boundary + 1);
this.emit('message', JSON.parse(input));
boundary = buffer.indexOf('\n');
}
});
}
}
const EventEmitter = require ('events').EventEmitter;
class LDJClient extends EventEmitter {
constructor(stream){
super();
let buffer = '';
stream.on('data', data => {
buffer += data;
let boundary = buffer.indexOf('\n');
while(boundary !== -1) {
const input = buffer.substring(0, boundary);
buffer = buffer.substring(boundary + 1);
this.emit('message', JSON.parse(input));
boundary = buffer.indexOf('\n');
}
});
}
static connect(stream) {
return new LDJClient(stream);
}
}
/* Exporta la clase LDJClient*/
module.exports = LDJClient;
Dentro de la definición de clase, después del constructor, estamos agregando un método estático llamado connect
.
El código para usar el módulo sería algo como esto
O podríamos usar el método connect
.
'use strict';
/*Empleamos la clase LDJClient que hemos creado para conectarnos*/
const netClient = require('net').connect({port: 60300});
const ldjClient = require('./lib/ldj-client.js').connect(netClient);
/*Escuchamos si recibimos un mensaje y lo procesamos**/
ldjClient.on('message', message => {
if(message.type === 'watching') {
console.log(`Now watching: ${message.file}`);
} else if (message.type === 'changed') {
console.log(`File changed: ${new Date(message.timestamp)}`);
} else {
throw Error(`Unrecognized message type: ${message.type}`);
}
});
La principal diferencia respecto a lo anterior es que, en lugar de enviar buffers de datos directamente a JSON.parse , este programa se basa en el módulo ldj-client para producir eventos de mensajes .
Ejecutamos el servidor de pruebas y el nuevo cliente.
Mocha es un marco de pruebas para Node
. Lo instalamos con npm
y desarrollamos diferentes pruebas para LDJClient
.
- En la carpeta
networking
generamos un JSON connpm init -y
. - Posteriormente instalamos mocha con
npm install --save-dev --save-exact mocha@3.4.2
.
Se habrá creado un directorio llamado node_modules
que contiene mocha
y sus dependencias.
Además, el fichero package.json
contiene ahora una linea de dependencia de mocha
.
Creamos un subdirectorio llamado test
que es donde por defecto Mocha
buscará.
Desarrollamos un fichero de pruebas
Para poder ejecutar tenemos que añadir en el package.json
lo siguiente en la sección test
{
"name": "networking",
"version": "1.0.0",
"description": "",
"main": "net-watcher-json-client.js",
"directories": {
"lib": "lib"
},
"scripts": {
"test": "Mocha"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"mocha": "3.4.2"
}
}
A continuación, ejecutamos con npm test
.
Modificamos el describe
de la siguiente manera
'use strict'
const assert = require('assert');
const EventEmitter = require('events').EventEmitter;
const LDJClient = require('../lib/ldj-client.js');
/** Método en el que se realiza un test unitario que envia un mensaje*/
describe('LDJClient', () => {
let stream = null;
let client = null;
beforeEach(() => {
stream = new EventEmitter();
client = new LDJClient(stream);
});
it('should emit a message event from a single data event', done => {
client.on('message', message => {
assert.deepEqual(message, {foo: 'bar'});
done();
});
stream.emit('data', '{"foo": "bar"}\n');
process.nextTick(() => stream.emit('data','"bar}\n"'));
});
});
Esta prueba divide el mensaje en dos partes para ser emitidas por el stream
uno después del otro.
Si se quiere especificar un tiempo para un test puede usar el timeout
- Test que divide un mensaje en dos o más pedazos.
'use strict';
const assert = require('assert');
const EventEmitter = require('events').EventEmitter;
const LDJClient = require('../lib/ldj-client.js');
describe('LDJClient', () => {
let stream = null;
let client = null;
beforeEach(() => {
stream = new EventEmitter();
client = new LDJClient(stream);
});
it('should emit a message event from split data events', done => {
client.on('message', message => {
assert.deepEqual(message, {foo: 'bar'});
done();
});
stream.emit('data', '{"foo":"bar"}\n');
process.nextTick(() => stream.emit('data', '"bar"}\n'));
});
});
- Test que pasa un objecto nulo y detecta el error.
'use strict';
const assert = require('assert');
const EventEmitter = require('events').EventEmitter;
const LDJClient = require('../lib/ldj-client.js');
describe('LDJClient', () => {
let stream = null;
let client = null;
beforeEach(() => {
stream = new EventEmitter();
client = new LDJClient(stream);
});
it('Error creating an object with null parameter', done => {
assert.throws( () => {new LDJClient(null);});
done();
});
});
-
En caso de que el formato JSON que se reciba no sea adecuado el servidor se cerrará y el cliente verá la notificación del error.
-
Aquí tenemos un test para enviar y detectar el error de pasar un mensaje que no es JSON.
const assert = require('assert');
const EventEmitter = require('events').EventEmitter;
const LDJClient = require('../lib/ldj-client.js');
/** Test unitario que envia un mensaje no JSON*/
describe('LDJClient', () => {
let stream = null;
let client = null;
beforeEach(() => {
stream = new EventEmitter();
client = new LDJClient(stream);
});
it('should throw a error message because its not a JSON text.', done => { assert.throws( () => {
stream.emit('data', 'Not JSON message\n');
});
done();
});
});
- Si falta el último salto de linea lo que pasa es que se quedará esperando y nunca emitirá el mensaje. Para poder manejar esta situación implementamos un evento
close
que comprobará si existe o no un\n
al final del JSON. En caso de no existir lanza el error o, en caso contrario, emite el mensaje.
const assert = require('assert');
const EventEmitter = require('events').EventEmitter;
const LDJClient = require('../lib/ldj-client.js');
/** Test unitario que envia un mensaje sin salto de linea seguido de un evento close*/
describe('LDJClient', () => {
let stream = null;
let client = null;
beforeEach(() => {
stream = new EventEmitter();
client = new LDJClient(stream);
});
it('Without last new line and with close event', done => {
client.on('message', message => {
assert.deepEqual(message, {foo: 'bar'});
done();
});
stream.emit('data', '{"foo": "bar"}\n');
stream.emit('close');
});
});