Introducción a



para

FLOQQ


https://slid.es/alexfernandez/node-js-floqq

Quién soy



Ingeniero de software con 15+ años de experiencia
Desarrollador senior en MediaSmart Mobile
Cacharreador impenitente desde siempre
@pinchito, alexfernandez, alejandrofer

Requisitos

Manejo de varios navegadores (Chrome, Firefox)


Conocimientos básicos de HTML


Conocimientos básicos de JavaScript


Familiaridad con algún tipo de servidor web

(Java, PHP, Apache)


Manejo básico de consola Unix / DOS

Sesiones


  1. Introducción a node.js
  2. Sesión práctica: hola, mundo (http)
  3. Introducción al manejo de eventos
  4. Sesión práctica: hola, mundo (sockets)
  5. Pruebas asíncronas con node.js
  6. Sesión práctica: pruebas de sockets
  7. Continuaciones y callbacks
  8. Sesión práctica: desmontando callbacks
  9. npm: Node Package Manager
  10. Sesión práctica: paquetes

Node.js



Una introducción amable

Breve historia de Node.js


Creado por Ryan Dahl (2009)


Usa el motor de Chrome JS: V8 (en cómic)
Software libre (código fuente)
Soporte de Joyent y StrongLoop

Tres generaciones


Procesos: CGI, PHP core


Hilos: Apache MTM, Java Servlets


Eventos: nginx, node.js


Modelo de ejecución

Asíncrono

No bloqueante

Dirigido por eventos


Servidores similares:

Python: Tornado, Twisted

Ruby: EventMachine

Java: Akka


Fuente: nodejs.org/about, Wikipedia

Node.js es rápido

¿Pero cómo de rápido?


No para correr código secuencial

... pero sí en ejecución concurrente


Sólo un hilo en ejecución (sin hilos)

Multi-proceso usando el módulo cluster


Node.js es muy escalable

Node.js es muy lineal


Fuente: MediaSmart Mobile

Historia de Éxito: PayPal

Página de Resumen de Cuentas

2 ingenieros durante 4 meses

Desarrollado dos veces más rápido que la versión Java

33% menos código, 40% menos ficheros

Dos veces más peticiones por segundo

Tiempos de respuesta un 35% más rápidos


Actualmente migrando toda su infraestructura


Fuente: Node.js at PayPal

Historia de Éxito: LinkedIn


Backend de apps móviles (2011)


Migración desde Ruby on Rails

De 15 instancias a 4 con node.js

El doble de carga que Ruby on Rails


Actualmente migrando toda su infraestructura


Fuente: VentureBeat

Historia de éxito: MediaSmart Mobile


Plataforma inicial: un ingeniero durante un año

Plataforma actual: tres ingenieros durante otro año

Sirve anuncios en móvil para campañas performance


Aguanta 20K peticiones/segundo (al menos)

Escala a 30 servidores (que sepamos)

Latencia < 80 ms


Fuente: MediaSmart Mobile

Sesión práctica 1: Hola, mundo


Bajar node.js e instalarlo


Crear proyecto node.js


Probar el código de muestra


¡Hola, mundo!

Hola, mundo (http)


var http = require('http');

http.createServer(function (request, response) { response.writeHead(200, {'Content-Type': 'text/plain'}); response.end('Hello World\n'); }).listen(1337, '127.0.0.1');

console.log('Server running at http://127.0.0.1:1337/');


Acceso


Fuente: node.js


Especificación técnica


Servidor http en el puerto 1337


Sirve siempre la misma página
en texto plano, que dice "Hello world"

Muestra una traza en consola al arrancar


Documentación: node.js http

Ejercicios


Cambiar la respuesta a "Hola, mundo" (en español)

Cambiar la respuesta a una página HTML
que diga "Hola, mundo"
(Pista: response.write())

Cambiar el servidor para que responda sólo en /,
en otra URL debe dar error 500
(Pista: request.url)


Eventos

Asincronía al poder

Emisor de eventos


Clase EventEmitter


Emite evento data:

emitter.emit('data', data);


Para recibir el evento data:

emitter.on('data', function(data) {
console.log('Data: %s', data);
});


API de recepción


emitter.on(event, function(data) {}); 

Registra un listener para un evento

emitter.once(event, function(data) {}); 

Registra un listener para una sola vez

emitter.removeListener(event, listener); 

Elimina un listener

emitter.removeAllListeners([event]); 
Elimina todos los listeners, opcionalmente para un evento

La temible fuga de memoria


(node) warning: possible EventEmitter memory leak detected. 11 listeners added. Use emitter.setMaxListeners() to increase limit.

Ocurre cuando se añaden listeners a un emisor

y no se eliminan


Ejemplo: reutilización de conexiones en un servidor


Solución: al terminar cada petición eliminar listener

emitter.removeListener('event', listener);

Emitiendo Eventos

Podemos crear nuestros propios EventEmitters:

var events = require('events');
var util = require('util');
function MyEmitter() {
    events.EventEmitter.call(this);
    http.createServer(function (request, response) {
        this.emit('request', request);
        res.end('Hello World\n');
    }).listen(1337, '127.0.0.1', function() {
        this.emit('init');
    });
}
util.inherits(LimitsUpdater, events.EventEmitter);
var emitter = new MyEmitter();
emitter.on('init', function() {
    console.log('Server started');
});

Esto nos permite gestionar eventos en lugar de callbacks

Sesión práctica 2: hola, mundo (again)


Servidor de sockets


Control a bajo nivel de la conexión


Recibe datos, envía respuesta


Manejado con eventos

Especificación técnica

Servidor de sockets, puerto 1702


Cuando se abre una conexión pregunta "Hello?"

Tras recibir "hello" responde "world" y termina

Cualquier otro mensaje resulta en "ERROR"


Nota técnica: ¡cuidado con los retornos de carro!
Siempre "\r\n".


Documentación: node.js net

Hola, mundo (sockets)


var net = require('net');
var port = 1702;
var server = net.createServer(function(connection) {
    console.log('Connection open');
    connection.write('Hello?\r\n');
    connection.on('data', function(data) {
        if (String(data).trim() != 'hello') {
            connection.write('ERROR\r\n');
        } else {
            connection.end('world\r\n');
            console.log('connection closed');
        }
    });
});
server.listen(port);

Contiene tres niveles de ejecución

Ejercicios


Admitir 'hello', 'Hello', 'HELLO, 'HeLlO'...

(Pista: String.toLowerCase())


Manejar los eventos error y end

Logar un error o un mensaje normal


Convertir a EventEmitter

Pruebas Asíncronas


¿Quién automatiza al automatizador?

Anónimo

Tipos de pruebas


Pruebas unitarias


Pruebas de integración /  de sistemas


Pruebas de carga



Pruebas automáticas: ¡las mejores!

Pruebas de código asíncrono


No basta con correr el código y ver el resultado


Requieren una librería especial


Veremos cómo usar testing

Librería especializada

Éxito y fracaso


testing.failure("Failed to do something", callback); 

Da la prueba por fallida con un mensaje


testing.success("Well done!", callback); 

Da la prueba por buena con un mensaje


Aserciones


Para comprobar que un valor contiene lo que se espera


testing.assert(value, "Error in value", callback); 

La variable value debe ser cierta (true o un objeto)


testing.assertEquals(sum(2, 2), 4, "Wrong sum", callback); 
La función sum(2,2) debe devolver 4

Ejecución asíncrona


testing sigue la convención habitual de node para callbacks:

doSomething(function(error, result) {
if (error) {
console.error('Could not do something: ' + error);
return;
}
....
});

El error contiene algo sólo si la función falló


testing.check(error, "There is an error", callback); 
Comprueba que no haya error; si lo hay, falla el test

Ejemplo de test


Comprobar que la función doSomethingAsync() no da error

y devuelve el valor 5


function testAsync(callback) {
doSomethingAsync(function(error, result) {
testing.check(error, 'Could not do something', callback);
testing.assertEquals(result, 5, 'Invalid result', callback);
testing.success(callback);
});
}
Casi todas las llamadas de testing terminan en una callback

Control


testing.run([testFirst, testSecond], timeout, callback); 

Corre las pruebas testFirst() y testSecond()

testing.show(results); 

Muestra el resultado de las pruebas


Uso habitual:

// run tests if invoked directly
if (__filename == process.argv[1])
{
testing.run([testFirst, testSecond], testing.show);
}

¡Automatiza!


Diseña un API de control

Arranca y para el sistema usando el API


Tests auto-contenidos: limpia al terminar


Tres reglas básicas:

  • Un único botón
  • Haz los fallos visibles
  • Sin intervención humana

Sesión práctica 3: pruebas


Diseñar las pruebas para el servidor de sockets

usando testing


Crear un API de control


Añadir a test.js

Diseño de la prueba


Arrancar el servidor de sockets en un puerto diferente (1705)


Conectar con un socket y enviar "hello"


Verificar que devuelve "world"


Cerrar el socket


Parar el servidor

API de control

Encapsular el arranque del servidor en una función

function start(port, callback) {
    var server = net.createServer(function(connection) {
        console.log('Conexión abierta');
        ...
    });
    server.listen(port, callback);
    return server;
}

Añadir la parada del servidor: server.close()

Llamar en secuencia:

function testServer(callback) {
var server = start(1705, function(error, result) {
server.stop(function(error, result) {
....
});
});
}

Prueba completa


function testServer(callback) {
var port = 1705;
var server = start(port, function(error) {
testing.check(error, 'Could not start server', callback);
console.log('started');
var socket = net.connect(port, 'localhost', function(error) {
testing.check(error, 'Could not connect', callback);
socket.on('data', function(data) {
console.log('Received ' + data);
var message = String(data).trim();
if (message == 'Hello?')
{
socket.write('hello');
return;
}
testing.assertEquals(message, 'world', 'Bad response', callback);
server.close(function(error) {
testing.check(error, 'Could not stop server', callback);
testing.success(callback);
});
});
});
});
}

Ejercicios


Eliminar todos los console.log()


Añadir otra prueba testError():

envía otra cadena, comprueba que obtiene 'ERROR'


Añadir una prueba que arranque dos servidores

en puertos distintos y los pare

Continuaciones


Introducción al "infierno" de las callbacks

Continuaciones


Concepto de programación funcional


CPS: Continuation-passing Style


¡También conocidas como callbacks!


El estado pasa como parámetros y clausuras


Ideal para llamadas asíncronas

Ejemplo: servidor sockets


var net = require('net');
var port = 1702;
var server = net.createServer(function(connection) {
    console.log('Connection to %s open', port);
    connection.write('Hello?\r\n');
    connection.on('data', function(data) {
        if (String(data).trim() != 'hello) {
            connection.write('ERROR\r\n');
        } else {
            connection.end('world\r\n');
            console.log('Connection to %s closed', port);
        }
    });
});
server.listen(port);
Continuaciones en amarillo
Parámetros en negrita
Clausuras en celeste

Infierno de las Callbacks


La temida pirámide de callbacks


Infinitas continuaciones anidadas


Abuso de lambdas (funciones anónimas)


Difícil de seguir, depurar y refactorizar


¿Son los GOTOs modernos?

Soluciones Sencillas


Usar funciones con nombre


Crear un objeto con funciones con nombre

Las clausuras se convierten en atributos


Usar async: async.waterfall()


Usar eventos: EventEmitter

Otros modelos


Promesas


Programación reactiva


Generadores: casi listos


Corutinas: falta bastante todavía

Sesión práctica 4: desmontando callbacks


Revisar la pirámide de callbacks


Código original: test de socket server


Convertir a objeto con atributos


No dejar más de dos callbacks anidadas

Ejercicios


Convertir la función start() también a un objeto


Convertir prueba a eventos
Evento error


Leer la spec de generadores
o ver este vídeo (en inglés)

NPM


Node Package Manager

Anatomía de un paquete (o módulo)


Fichero Readme: README.md


Definición: package.json


Punto de entrada: index.js


Código: lib/

Documentación: doc/

Binarios: bin/

Comandos NPM


  • npm install: instala uno o más paquetes
  • npm install -g: instalación global
  • npm update: actualiza uno o más paquetes
  • npm remove: elimina un paquete
  • npm test: corre las pruebas


Fuente: npm-index

Resolución de directorios

Local:

            node_modules
         ../node_modules
      ../../node_modules
   ...
$HOME/node_modules
          ./node_modules

Global:

{prefix}/lib/node_modules

donde {prefix} suele ser /usr/local


Fuente: npm-folders

aNATOMía de un package.json

{
"name": "simplecached",
"version": "0.0.1",
"description": "Simplified memcached server.",
"contributors": ["Alex Fernández <alexfernandeznpm@gmail.com>"],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alexfernandez/simplecached"
},
"dependencies": {
"testing": "1.2.x"
},
"keywords" : ["simplecached", "didactic", "memcached", "caching"],
"scripts": {
"test": "node test.js"
},
"private": "false"
}


Fuente: package.json

Sesión práctica 5: paquetes


Instalar una dependencia con npm install testing

Inspeccionar la estructura de directorios resultante


Listar los paquetes instalados con npm list


Eliminar una dependecia con npm remove testing



package.json


Crear un package.json siguiendo la especificación

partiendo del ejemplo anterior


Cambiar nombre, versión y descripción


Correr npm install

Crear tests


Exportar test() desde el servidor.


Importarlo desde test.js.


Correr npm test

Ejercicios


Paquetizar los ficheros existentes

Código en lib/


Crear index.js que exporte alguna función


Publicar con npm publish

¡Despublicarlo! con npm unpublish --force

¡Gracias!







Código: https://github.com/alexfernandez/floqq-node

Presentación: https://slid.es/alexfernandez/node-js-floqq

Curso de node.js Floqq

By Alex Fernández

Curso de node.js Floqq

Curso para Floqq sobre node.js. Si te gusta, ¡compra el vídeo completo y ayuda a que haga más presentaciones como ésta!

  • 4,756