Introduccion A Nodejs A Traves de Koans Ebook
Introduccion A Nodejs A Traves de Koans Ebook
Introduccion A Nodejs A Traves de Koans Ebook
JS A
TRAVÉS DE KOANS
http://nodejskoans.com
You should have received a copy of the GNU General Public License
along with this program. If not, see
<http://www.gnu.org/licenses/>.
El diseño de la portada ha corrido a cargo del autor del libro, empleando para ello
las tipografías: Bebas Neue, una versión modificada de la Aharoni Bold, Comfor-
taa, Tribal Animals Tatto Design (http://tattoowoo.com/) y Entypo pictograms
by Daniel Bruce (www.entypo.com).
2013-05-23
A mis padres, Arturo y Fabi, y a
mi hermano Edu, por su apoyo y
confianza sin límites.
Índice general I
Índice de Tablas V
1. Introducción 1
1.1. ¿Qué son los Koans? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2. Guía de lectura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.3. Conseguir el código fuente . . . . . . . . . . . . . . . . . . . . . . . . . 6
I
2.7. “Perfecto para aplicaciones en tiempo real data-intensive” . . . . . . . 36
2.7.1. Tiempo real y Node . . . . . . . . . . . . . . . . . . . . . . . . . . 36
2.7.2. ¿Para qué es útil Node entonces? . . . . . . . . . . . . . . . . . 37
5. Módulo Http 89
5.1. Aspectos de HTTP relevantes para Node . . . . . . . . . . . . . . . . . 89
5.1.1. La parte del Cliente . . . . . . . . . . . . . . . . . . . . . . . . . 91
5.1.2. La parte del Servidor . . . . . . . . . . . . . . . . . . . . . . . . . 96
5.2. HTTP en Node . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
5.2.1. ServerRequest . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
5.2.2. ServerResponse . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
5.2.3. Clientes HTTP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
5.2.4. ClientResponse . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
II
5.3. Aplicación con HTTP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
5.3.1. Descripción del problema . . . . . . . . . . . . . . . . . . . . . . 110
5.3.2. Diseño propuesto . . . . . . . . . . . . . . . . . . . . . . . . . . 112
5.4. Objetivos de los Koans . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
5.5. Preparación del entorno y ejecución de los Koans . . . . . . . . . . . . 119
5.6. Conclusión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
6. Express 121
6.1. Connect . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
6.2. Express . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
6.2.1. Request . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
6.2.2. Response . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
6.3. MongoDB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
6.4. Aplicación de ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
6.4.1. Clon de Twitter, objetivo 1: autenticación y control de sesiones 140
6.4.1.1. Descripción del Objetivo . . . . . . . . . . . . . . . . . . 140
6.4.1.2. Diseño propuesto al Objetivo implementado con Ex-
press . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
6.4.1.3. Objetivos de los Koans . . . . . . . . . . . . . . . . . . 149
6.4.1.4. Preparación del entorno y ejecución de los Koans . . . 150
6.4.2. Clon de Twitter, objetivo 2: publicación de whizs y follow y
unfollow de otros usuarios . . . . . . . . . . . . . . . . . . . . . 151
6.4.2.1. Descripción del Objetivo . . . . . . . . . . . . . . . . . . 151
6.4.2.2. Diseño propuesto al Objetivo implementado con Ex-
press . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
6.4.2.3. Objetivos de los Koans . . . . . . . . . . . . . . . . . . 154
6.5. Conclusión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
7. Socket.IO 157
7.1. ¿Qué es Socket.IO? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
7.1.1. “Socket.IO pretende hacer posible las aplicaciones en tiempo
real” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
7.1.2. “en cada navegador y dispositivo móvil” . . . . . . . . . . . . . 158
7.1.3. “difuminando las diferencias entre los diferentes mecanismos
de transporte” . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
7.2. Usando Socket.IO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
7.2.1. Servidor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
III
7.2.2. Cliente [2] . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
7.3. Aplicación con Socket.IO . . . . . . . . . . . . . . . . . . . . . . . . . . 176
7.3.1. Descripción del juego . . . . . . . . . . . . . . . . . . . . . . . . 176
7.3.2. Objetivos perseguidos . . . . . . . . . . . . . . . . . . . . . . . . 176
7.3.3. Diseño propuesto . . . . . . . . . . . . . . . . . . . . . . . . . . 177
7.3.4. Implementación con Socket.IO . . . . . . . . . . . . . . . . . . . 181
7.4. Objetivos de los Koans . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
7.5. Preparación del entorno y ejecución de los Koans . . . . . . . . . . . . 187
7.6. Conclusión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
A. Listados 191
A.1. Módulos dgram y Buffer . . . . . . . . . . . . . . . . . . . . . . . . . . 191
A.2. Módulos net y Stream . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
A.3. Módulo http . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
A.4. Módulo Express . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206
A.5. Módulo Socket.IO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
IV
Índice de tablas
V
VI
Índice de figuras
VII
VIII
Capítulo 1
Introducción
Cada uno de estos capítulos tratará uno o dos módulos muy relacionados entre
sí, con la intención de que se cubran tópicos que vayan de menor complejidad a
mayor. La estructura de dichos capítulos es siempre la misma:
un ligero repaso a los conceptos teóricos que hay detrás del módulo, general-
mente desgranando una RFC (Request For Comments), con especial énfasis
en aquellos que el API da opción a manejar.
1
impone.
2
camino hacia “el despertar” puesto que suponen para él realizar un esfuerzo para
entender la doctrina.
Los koans en el ámbito informático fueron idea de Joe O’Brien y Jim Weirich, de
EdgeCase, como forma de alcanzar “la iluminación” en el aprendizaje del lenguaje
Ruby. Manteniendo la analogía con la filosofía Zen, los koans de Ruby son peque-
ños trozos de código donde se sustituye alguna parte con unos guiones bajos ‘__’,
al estilo de los ejercicios de rellenar el hueco, que el alumno deberá cambiar por el
código que piense que es el correcto para que el ejercicio ejecute sin errores. Para
evaluar si lo que el alumno ha escrito en lugar de los ‘__’ es correcto se emplean
unos casos de prueba unitarios diseñados para cada koan.
La filosofía de los Koans que se han diseñado para Node es la misma que la que
se ha presentado para los lenguajes de programación: fragmentos de código eva-
luables mediante pruebas unitarias que marquen al alumno el camino a seguir
para la correcta comprensión y asimilación de los aspectos relevantes necesa-
rios para iniciarse en Node. Estos trozos de código se engloban en el marco de
una aplicación concreta, con lo que no son independientes entre sí, sino que su
resolución lleva al alumno a tener en sus manos un programa con completa fun-
cionalidad donde entender mejor cuáles son los puntos clave más básicos cuando
se programa con Node como tecnología de Servidor.
Con la intención con la que se crearon los Koans se crea esta obra, cuyos capítulos
van marcando el camino hacia el conocimiento y manejo de Node. El recorrido que
se marca para ello es el que sigue:
Capítulo 2
El punto de partida que se va a tomar para introducir la programación de
aplicaciones para Internet es uno de sus protocolos más sencillos: UDP. Con
él se presentará el módulo dgram, que implementa las características de
este protocolo, y Buffer, íntimente ligado a los protocolos de red porque es la
manera que tiene Node de manejar datos binarios sin un formato concreto,
en bruto.
3
Con dgram se tomará contacto con la creación y gestión de conexiones UDP
y, sobre todo, con todo lo referente a operaciones de Entrada/Salida asín-
crona en redes, como envío y recepción de datos.
Con Buffer, se conocerá una de las clases básicas de Node, siempre presente
en el espacio de ejecución de cualquier programa, por lo que resulta impres-
cindible conocer cómo se crean y se manejan los datos que contiene a través
de los múltiples métodos que Buffer ofrece para ello.
Capítulo 3
Siguiendo con los protocolos de red, se da un paso más con respecto al tema
anterior y se introduce el módulo net, que da soporte a la transmisión de
datos sobre el protocolo TCP, “hemano mayor” de UDP. Estos datos se ven
como un flujo continuo, o stream, con lo que la inclusión del módulo stream
en este capítulo cobra sentido.
Capítulo 4
Por encima del nivel de transporte, en el que se ubicarían los protocolos
vistos en los capítulos anteriores, se encuentra el nivel de aplicación para el
que Node también posee módulos en su API. En concreto, para el Protocolo
de Transferencia de Hipertexto, HTTP, en el que se basa toda la Web.
4
sigue el modelo Cliente-Servidor ya asimilado, pero se añaden más carac-
terísticas propias de la especificación HTTP. Por ello, junto al manejo de
conexiones HTTP se introducirán los mecanismos para gestionar peticiones
interpretando los métodos y cabeceras propias del protocolo para generar
las respuestas de la manera más adecuada.
Aunque HTTP se sitúe por encima de TCP, no implica que no pueda convivir
con más protocolos en una misma aplicación. En este caso, en la aplicación
de ejemplo, se utilizará junto con RTP para proporcionar una interfaz web
con la que controlar qué se emite por streaming, simulando un reproductor
de audio tradicional.
Capítulo 5
Una vez que se conocen las librerías fundamentales de Node es hora de
evolucionar y subir un peldaño en la arquitectura de los módulos de terceras
partes para la plataforma. Visto HTTP, se presentará la manera de tratar con
él desde otra perspectiva: la de la creación de Aplicaciones Web.
Junto con ellas, se hablará de Mongoose, un driver para Node para la base
de datos no relacional MongoDB. Este tipo de bases de datos están muy
presentes en el desarrollo de las aplicaciones web más modernas y, como
toda base de datos, son imprescindibles a la hora de desarrollar una solución
completa. Por ser un driver muy completo y complejo, se presentarán las
operaciones más elementales que se pueden realizar con él para emprender
un desarrollo básico pero perfectamente funcional.
Capítulo 6
El último peldaño antes de adquirir una base más o menos amplia sobre la
programación para Internet con Node, son las aplicaciones en tiempo real,
5
las cuales están adquiriendo, si es que no lo han hecho ya, una importancia
fundamental en cualquier desarrollo web.
Todo el código que se ha generado para este proyecto puede localizarse en Git-
hub1 , una comunidad de desarrolladores. El repositorio concreto donde se hallan
todos los archivos es uno en la cuenta personal del autor del libro, al que se ha lla-
mado nodejskoans: https://github.com/arturomtm/nodejskoans.git. Para
tener una copia con la que trabajar, es necesario clonarlo en un directorio de la
máquina de trabajo:
$ mkdir nodejskoans
$ cd nodejskoans
$ git clone https://github.com/arturomtm/nodejskoans.git
Una vez clonado, se puede trabajar con el código tal y como se explica en los
capítulos que siguen.
1
http://github.com
6
Capítulo 2
Node.js, de ahora en adelante Node, es un proyecto creado por Ryan Dahl a prin-
cipios de 2009. Se diseñó orientado a la creación de aplicaciones para Internet,
principalmente Web, porque la programación de software para servidores era el
tipo de desarrollo que hacía el autor en aquella fecha.
7
paradigma de plataforma de aplicaciones de tiempo real.
Node está apadrinado por la compañía Joyent6 , que contrató a Ryan Dahl cuando
comenzaba el proyecto. Joyent ofrece, conjuntamente con Nodejistsu7 , como IaaS,
un entorno en “la Nube” donde desplegar aplicaciones Node.
Pero no sólo Joyent ofrece alojamiento para esta plataforma. Heroku8 también
tiene soluciones cloud-computing personalizables, y, si se eligen opciones gratuitas
y open source, Nodester9 da espacio para aplicaciones como PasS.
Como colofón, Node fue galardonado en 2012 con el premio “Tecnología del Año”
por la revista InfoWorld, perteneciente a una división prestigioso grupo interna-
cional de prensa especializada IDG. Y, posiblemente, todo no haya hecho más que
empezar.
1
https://github.com
2
https://npmjs.org/
3
http://nodeknockout.com/
4
http://www.nodeconf.com
5
http://jsconf.com
6
http://joyent.com
7
https://www.nodejitsu.com/
8
http://www.heroku.com
9
http://nodester.com
8
2.1. ¿Qué es Node?
Esta visión global de Node se puede diseccionar en pequeñas partes que, una vez
analizadas separadamente, dan una visión mucho más precisa y detallada de las
características del proyecto.
9
demanden. Se puede echar un vistazo a este código en el fichero del código fuen-
te src/node.js. Ahí se ve que el encargado de realizar la carga es el objeto
NativeModule, que ofrece un minimalista sistema de gestión de módulos.
Una de las primeras acciones que se realizan es hacer disponibles las variables,
objetos y funciones globales. Por globales se entiende que están disponibles en el
scope donde corre Node, que se hace disponible al programa mediante, precisa-
mente, la variable global. Por defecto, disponemos de las funciones que ofrecen
los módulos console y timers, el objeto Buffer, del módulo buffer, y el objeto
nativo process.
Se hablará de process por ser quizás uno de los objetos más interesantes de la
plataforma desde el punto de vista del diseño. Presente en el espacio global de
ejecución del proceso principal de Node representa a ese mismo proceso. Como
se ha dicho, se hace disponible en él después de crearse en código nativo a tra-
vés de la función SetupProcessObject() en el proceso de arranque definido en
el fichero src/node.cc10 . Con esta función se crea un objeto al que se le aña-
den propiedades y métodos en código nativo que luego están disponibles para el
programador a través del API. Éstos permiten:
10
estadísticas de ejecución como el tiempo que lleva corriendo el proceso, con
process.uptime() y la memoria que está consumiendo con
process.memoryUsage()
Además, existen una serie de variables y métodos no documentados del todo que
son interesantes desde el punto de vista de funcionamiento interno de Node:
11
la arquitectura de Node.
Se definen los métodos relacionados con las señales13 del sistema operativo:
process.exit(), para terminar la ejecución del programa especificando un
código de salida, y process.kill(), para enviar señales a otros procesos,
tal y como se haría con la llamada al sistema operativo kill.
Script
se accede a este modo pasando por línea de comandos el nombre de un
fichero .js que contiene el código del programa. Node cargará ese fichero y
lo ejecutará. Es el modo más común de trabajar en la plataforma.
12
o –eval seguido del nombre del script.
REPL
son las siglas de Read Eval Print Loop. Es el modo interactivo en el que
Node presenta un prompt (’>’) donde manualmente se van introduciendo
expresiones y comandos que serán evaluados al presionar la tecla Enter.
Debug
se invoca incluyendo el modificador debug en la línea de comandos y deja
a Node en un modo interactivo de depuración de scripts donde se pueden
introducir mediante comandos las acciones típicas en este tipo de sesiones:
step in, step out, fijar y limpiar breakpoints...
Adicionalmente, los autores del proyecto permiten cargar código propio del pro-
gramador y arrancar con él en lugar de hacerlo con el arranque normal de Node.
Para ello se debe incluir en el directorio lib/ del código fuente del proyecto el
fichero _third_party_main.js que contendrá el código personalizado, y luego
compilarlo todo.
Las librerías del core son también archivos de extensión .js que se hallan en
el directorio lib/ del código fuente (pero empotrados en el ejecutable una vez
compilado, como se ha dicho). Cada uno de esos archivos es un módulo que sigue
el formato que define CommonJS.
15
module.js https://github.com/joyent/node/blob/v0.8.20-release/lib/module.js#
L377
13
2.2.2. El formato CommonJS
Node soporta e implementa el sistema que CommonJS define para esta gestión
de módulos, lo cual es importante no sólo a la hora de organizar el código sino
a la hora de asegurar que se ejecuta sin interferir en el código de los demás
módulos aislándolos unos de otros de tal manera que no haya conflicto entre, por
ejemplo, funciones o variables con el mismo nombre. A esto se le conoce como
scope isolation.
14
var PI = 3.14;
exports.area = function (r) {
return PI * r * r;
};
exports.circumference = function (r) {
return 2 * PI * r;
};
console
marcado en el API como STDIO, ofrece el objeto console para imprimir men-
sajes por la salida estándar: stdout y stderr. Los mensajes van desde los
habituales info o log hasta trazar la pila de errores con trace.
timers
ofrece las funciones globales para el manejo de contadores que realizarán
la acción especificada pasado el tiempo que se les programa. Debido a la
cómo está diseñado Node, relacionado con el bucle de eventos del que se
hablará en un futuro, no se puede garantizar que el tiempo de ejecución de
dicha acción sea exactamente el marcado, sino uno aproximado cercano a
él, cuando el bucle esté en disposición de hacerlo.
module
proporciona el sistema de módulos según impone CommonJS. Cada módulo
que se carga o el propio programa, está modelado según module, que se verá
como una variable, module, dentro del mismo módulo. Con ella se tienen dis-
15
ponibles tanto el mecanismo de carga require() como aquellas funciones y
variables que exporta, en module.exports, que destacan entre otras menos
corrientes que están a un nivel informativo: módulo que ha cargado el actual
(module.parent), módulos que carga el actual
(module.children)...
buffer
es el objeto por defecto en Node para el manejo de datos binarios. Sin embar-
go, la introducción en JavaScript de los typedArrays desplazará a los Buffers
como manera de tratar esta clase de datos [9].
Los módulos siguientes, listados por su identificador, también forman parte del
núcleo de Node, aunque no se cargan al inicio, pero se exponen a través del
API:
util
conjunto de utilidades principalmente para saber de si un objeto es de tipo
array, error, fecha, expresión regular...También ofrece un mecanismo para
extender clases de JavaScript a través de herencia:
inherits(constructor, superConstructor);
events
provee la fundamental clase EventEmitter de la que cualquier objeto que
emite eventos en Node hereda. Si alguna clase del código de un programa
debe emitir eventos, ésta tiene que heredar de EventEmitter.
stream
interfaz abstracta que representa los flujos de caracteres de Unix de la cual
muchas clases en Node heredan.
crypto
algoritmos y capacidades de cifrado para otros módulos y para el código de
programa en general.
tls
comunicaciones cifradas en la capa de transporte con el protocolo TLS/SSL,
que proporciona infraestructura de clave pública/privada.
string_decoder
proporciona una manera de, a partir de un Buffer, obtener cadenas de ca-
16
racteres codificados en utf-8.
fs
funciones para trabajar con el sistema de ficheros de la manera que es-
tablece el estándar POSIX. Todos los métodos permiten trabajar de forma
asíncrona (el programa sigue su curso y Node avisa cuando ha terminado
la operación con el fichero) o síncrona (la ejecución del programa se detiene
hasta que se haya completado la operación con el fichero).
path
operaciones de manejo y transformación de la ruta de archivos y directorios,
a nivel de nombre, sin consultar el sistema de ficheros.
net
creación y manejo asíncrono de servidores y clientes, que implementan la
interfaz Stream mencionada antes, sobre el protocolo de transporte TCP.
dgram
creación y manejo asíncrono de datagramas sobre el protocolo transporte
UDP.
dns
métodos para tratar con el protocolo DNS para la resolución de nombres de
dominio de Internet.
http
interfaz de bajo nivel, ya que sólo maneja los Streams y el paso de mensa-
jes, para la creación y uso de conexiones bajo el protocolo HTTP, tanto del
lado del cliente como del servidor. Diseñada para dar soporte hasta a las
características más complejas del protocolo como chunk-encoding.
https
versión del protocolo HTTP sobre conexiones seguras TLS/SSL.
url
formateo y análisis de los campos de las URL.
querystrings
utilidades para trabajar con las queries en el protocolo HTTP. Una query son
los parámetros que se envían al servidor en las peticiones HTTP. Dependien-
do del tipo de petición (GET o POST), pueden formar parte de la URL por lo
17
que deben codificarse o escaparse y concatenarse de una manera especial
para que sean interpretadas como tal.
readline
permite la lectura línea por línea de un Stream, especialmente indicado para
el de la entrada estándar (STDIN ).
repl
bucle de lectura y evaluación de la entrada estándar, para incluir en pro-
gramas que necesiten uno. Es exactamente el mismo módulo que usa Node
cuando se inicia sin argumentos, en el modo REPL comentado con anterio-
ridad.
vm
compilación y ejecución bajo demanda de código.
child_process
creación de procesos hijos y comunicación y manejo de su entrada, salida y
error estándar con ellos de una manera no bloqueante.
assert
funciones para la escritura de tests unitarios.
tty
permite ajustar el modo de trabajo de la entrada estándar si ésta es un
terminal.
zlib
compresión/descompresión de Streams con los algoritmos zlib y gzip. Es-
tos formatos se usan, por ejemplo, en el protocolo HTTP para comprimir los
datos provenientes del servidor. Es conveniente tener en cuenta que los pro-
cesos de compresión y descompresión pueden ser muy costosos en términos
de memoria y consumo de CPU.
os
acceso a información relativa al sistema operativo y recursos hardware sobre
los que corre Node.
_debugger
es el depurador de código que Node tiene incorporado, a través de la opción
debug de la línea de comandos. En realidad es un cliente que hace uso
18
de las facilidades de depuración que el intérprete de Javascript que utiliza
Node ofrece a través de una conexión TCP al puerto 5858. Por tanto, no es
un módulo que se importe a través de require() sino el modo de ejecución
Debug del que se ha hablado antes.
cluster
creación y gestión de grupos de procesos Node trabajando en red para dis-
tribuir la carga en arquitecturas con procesadores multi-core.
punycode
implementación del algoritmo Punycode, disponible a partir de la versión
0.6.2, para uso del módulo url. El algoritmo Punycode se emplea para con-
vertir de una manera unívoca y reversible cadenas de caracteres Unicode a
cadenas de caracteres ascii con caracteres compatibles en nombres de red.
El propósito es que los nombres de dominio internacionalizados (en inglés,
IDNA), aquellos con caracteres propios de un país, se transformen en cade-
nas soportadas globalmente. [10]
domain
módulo experimental en fase de desarrollo y, por tanto, no cargado por de-
fecto para evitar problemas, aunque los autores de la plataforma aseguran
un impacto mínimo. La idea detrás de este él es la de agrupar múltiples
acciones de Entrada/Salida diferentes de tal manera que se dotan de un
contexto definido para manejar los errores que puedan derivarse de ellas [9].
De esta manera el contexto no se pierde e incluso el programa continua su
ejecución.
_linklist
implementa una lista doblemente enlazada. Esta estructura de datos se em-
plea en timers.js, el módulo que provee funcionalidad de temporización.
Su función es encadenar temporizadores que tengan el mismo tiempo de
espera, timeout. Esta es una manera muy eficiente de manejar enormes can-
tidades de temporizadores que se activan por inactividad, como los timeouts
de los sockets, en los que se reinicia el contador si se detecta actividad en
19
él. Cuando esto ocurre, el temporizador, que está situado en la cabeza de la
lista, se pone a la cola y se recalcula el tiempo en que debe expirar el primero
[11].
buffer_ieee754
implementa la lectura y escritura de números en formato de coma flotante
según el estándar IEEE754 del IEEE16 que el módulo buffer emplea para las
operaciones con Doubles y Floats.
constants
todas las constantes posibles disponibles de la plataforma como, por ejem-
plo, las relacionadas con POSIX para señales del sistema operativo y modos
de manejo de ficheros. Sólo realiza un binding con node_constants.cc.
freelist
proporciona una sencilla estructura de pool o conjunto de objetos de la mis-
ma clase (de hecho, el constructor de los mismos es un argumento necesa-
rio). Su utilidad se pone de manifiesto en el módulo http, donde se mantiene
un conjunto de parsers HTTP reutilizables, que se encargan de procesar las
peticiones HTTP que recibe un Servidor.
sys
es un módulo deprecado, en su lugar se debe emplear el módulo utils.
Node, además de por las características que se están desgranando, es una gran
plataforma de desarrollo por el inmenso ecosistema que ha crecido en torno suyo.
Se pueden encontrar infinidad de módulos desarrollados por terceras partes para
usar en proyectos propios, o desarrollar librerías con un propósito específico,
tanto para uso propietario como para uso de la comunidad Node.
16
IEEE: Institute of Electrical and Electronics Engineers
20
A la hora de utilizar módulos de terceras partes, se debe ser capaz de especificar
su ubicación para cargarlos o saber dónde Node va a buscarlos para instalarlos.
Cuando se indica, a través de la función del estándar CommonJS, que un módu-
lo es necesario, el orden que sigue Node para resolver la ruta del fichero hasta
encontrarlo es:
21
Por último, existen unas variables de entorno desde donde Node buscará librerías
en caso de que todo lo anterior falle. Por ejemplo, desde la ubicación que indique
NODE_PATH. No obstante, este método se desaconseja totalmente por haber que-
dado obsoleto, con lo que no se le va a dar mayor importancia.
interpretado: está diseñado para que una capa intermedia de software, el in-
térprete, lo ejecute, en contraposición a los lenguajes compilados, que corren
directamente sobre la arquitectura y sistema operativo objetivo (por regla
general, ya que Java es compilado pero se ejecuta dentro de una máquina
virtual)
22
[15]. JavaScript posee características de la programación funcional como
son las Funciones de Orden Superior. Se denominan así a las funciones que
admiten por parámetro otras funciones o que como resultado devuelven una
función.
débilmente tipado: se realiza una conversión, o cast, del tipo de las variables
en tiempo de ejecución, según el uso que se hace de ellas.
funciones anónimas: son las funciones que se pasan como parámetro a las
funciones de orden superior. Son útiles para usarlas como callback, asig-
nándolas por parámetro a algún objeto mediante el método correspondiente.
Los callbacks son funciones que se ejecutan como respuesta a un evento.
23
2.3.2. El motor V8 de Google
La elección del motor V8 de Google para Node se debió a que ambos proyectos
comenzaron prácticamente a la vez [3] y, en palabras del autor de Node, “V8 es
una buena, organizada librería. Es compacta e independiente de Chrome. Se dis-
tribuye en su propio paquete, es fácil de compilar y tiene un buen archivo header
con una buena documentación. No tiene dependencia de más cosas. Parece más
moderna que la de Mozilla” [17]. Además este motor presenta unas características
revolucionarias a la hora de interpretar JavaScript.
Para realizar una comparativa entre los diferentes motores JavaScript se pueden
ejecutar los test de benchmarking [18] diseñados para probar V8. Estas pruebas
evalúan el rendimiento en operaciones matemáticas complejas, como criptografía,
o una simulación de kernel de sistema operativo entre otros muchos, y se ejecutan
sobre el motor presente en el navegador.
V8 es de código abierto, bajo licencia New BSD [19], y está escrito en C++. Im-
plementa la 5a edición del estándar ECMA-262 y es multiplataforma, lo que ha
permitido a Node estar presente en sistemas tipo Unix (estándar POSIX), Mac y,
a partir de la versión 0.6, en Windows [20].
Dos de las características principales de V8, que lo han hecho destacar sobre el
resto de motores, son:
V8 permite a cualquier aplicación C++ que lo use hacer disponibles sus objetos
y funciones al código JavaScript que ejecuta. Un claro ejemplo de esto es el ya
comentado objeto process disponible en el scope global de cualquier programa
para Node.
24
Todo el código JavaScript que corre en Node, tanto el de su núcleo como el de
programa, se ejecuta en un único contexto que se crea cuando se inicia la plata-
forma. Para V8, los Contextos son entornos de ejecución separados y sin relación
que permiten que se ejecuten varias aplicaciones JavaScript, una en cada Con-
texto, en una única instancia del intérprete [22]. En el caso de Node, como se ha
comentado, sólo hay uno y sólo una instancia de V8 por lo que únicamente se
ejecuta un programa a la vez. Si se desea ejecutar más de un script de Node, se
deben levantar más instancias.
Una ventaja de emplear JavaScript como lenguaje para las aplicaciones en Node
es que, al ser un lenguaje con una curva de aprendizaje pronunciada, es decir, que
sus fundamentos básicos se aprenden fácilmente, es posible empezar a desarro-
llar aplicaciones rápidamente con sólo tener unas nociones de las características
fundamentales del lenguaje y conocer el grueso de las librerías del núcleo de la
plataforma.
25
http.createServer(function (req, res) {
res.writeHead(200, {’Content-Type’: ’text/plain’});
res.end(’Hello World\n’);
}).listen(1337, "127.0.0.1");
console.log(’Server running at http://127.0.0.1:1337/’);
el uso de funciones de ese módulo para tratar con servidores web y peticiones
HTTP
Una de los aspectos con mayor impacto en la escalabilidad es el diseño del sis-
26
tema. Éste es uno de los puntos fuertes de Node. Su arquitectura y la forma en
que las aplicaciones deben programarse para correr en ella hacen que se cumplan
principios básicos del buen diseño para la escalabilidad [24], un par de ejemplos
de esto son:
Sin esperas: el tiempo que un proceso espera a que un recurso esté dispo-
nible es tiempo que otro proceso no emplea para ejecutarse. La naturaleza
asíncrona y monoproceso de Node aseguran que se optimizará el tiempo de
ejecución de ambas tareas.
Lucha por los recursos: Node gestiona internamente de manera eficiente los
recursos del sistema para que todas las operaciones sobre, por ejemplo, red
o ficheros que se demandan en el código estén satisfechas sin que se abuse
de ellas. Así se evita un uso descompensado de las mismas por las distintas
partes del código de la aplicación.
Uno de los puntos críticos, usual cuello de botella, que afecta en alto grado al
rendimiento de cualquier sistema, en especial a aquellos que hacen un uso in-
tensivo de datos, ya sea mediante fichero, bases de datos o conexiones a través
de una red, son las operaciones de Entrada/Salida con ficheros y dispositivos.
Habitualmente, en las presentaciones de Node que su autor emplea para dar a
conocer la plataforma, se justifica el diseño de la arquitectura con unas medidas
sobre el impacto que tiene la interacción de un sistema normal con las distintas
fuentes de datos en términos de latencia y ciclos de reloj de la CPU. En concreto,
se manejan las cifras siguientes [26]:
Las latencias correspondientes a las cachés del procesador y la memoria RAM son
muy bajas, pero se puede considerar que las de disco y red poseen un retardo
27
excepcionalmente alto que deja al procesador desocupado. La reacción habitual
a estos “ciclos muertos”, en el sentido de actividad de un programa típico, es,
simplemente, la espera a que la operación termine para continuar con la ejecución
de las siguientes instrucciones. Es lo que se conoce como “sistemas bloqueantes”:
durante los ciclos de Entrada/Salida el programa se queda bloqueado en esa
tarea.
28
un evento que desencadenará la ejecución del callback, cuyo código modela la
siguiente tarea a realizar una vez se dispone del resultado de las operaciones
Entrada/Salida.
Nuevamente, a nivel de aplicación, esta vez el código tendría el aspecto que si-
gue:
Implementando este modelo se cumple uno de los objetivos de diseño [26] de Node
que es que “ninguna función debe realizar Entrada/Salida directamente”. Node
delega todas estas operaciones a mecanismos del sistema operativo a través de
la librería libuv [27], desarrollada específicamente para la arquitectura de Node
[28]. Libuv surge cuando se porta la plataforma a Windows y el propósito es el
de abstraer todo el código que sea dependiente de una plataforma específica (*ni-
x/Windows), en concreto aquel referente a la gestión de eventos y operaciones
Entrada/Salida no bloqueantes.
En versiones de Node previas, hasta la 0.6, en los sistemas *nix aquella fun-
cionalidad residía en las librerías libev y libeio respectivamente, y en sistemas
Windows, en IOCP. Con la versión 0.6 se introdujo libuv que implementaba para
Windows la funcionalidad que antes correspondía a IOCP, aunque para *nix se-
guía teniendo debajo a libev y libeio. Con la versión 0.8 de la plataforma, libuv
asume las funciones de las dos librerías para todos los sistemas operativos.
Para entender qué funciones asume libuv se debe conocer qué papel desempeña-
ban las dos librerías que originariamente formaban parte de Node:
29
bucle gestionar y cómo debe reaccionar ante ellos, vía callback. Los eventos
son de muchos tipos, desde que un descriptor, como un fichero o un socket,
puede comenzar a leerse hasta que se ha producido un timeout. Node de
por sí, al arranque, registra muy pocos watchers17 , tan sólo los referentes
al recolector de basura y aquel que se encarga de la ejecución de funciones
postpuestas para la siguiente iteración del bucle [11].
30
Los bindings son la manera que tiene el código en JavaScript de los módulos del
core de hacer uso del código nativo [31]. Un binding es un módulo escrito en
C/C++ que es capaz de utilizar las librerías, por ejemplo libev y libeio, a través de
libuv, para interactuar con el bucle de eventos y la Entrada/Salida directamente,
que es algo que el código JavaScript no puede hacer. Se podría decir que es una
librería a bajo nivel. El binding a su vez proveerá una serie funciones al módulo
para que éste ofrezca en su exports al desarrollador de aplicaciones su funcio-
nalidad. Por ejemplo, los siguientes bindings se emplean en las librerías del core
de Node:
buffer
Soporte de codificación en diferentes sistemas y manejo de datos binarios a
bajo nivel.
cares_wrap
Consultas asíncronas a DNS a través de la librería cares de la que Node
depende.
constants
Exposición directa de las constantes posibles de la plataforma.
crypto
Capacidades criptográficas y de cifrado a través de la librería openSSL.
evals
Acceso a funcionalidad de máquina virtual V8 sobre la manipulación de Con-
textos para la evaluación de scripts con código JavaScript. De ahí el nombre
del módulo, node_script.cc.
fs
Operaciones propias del sistema de ficheros, no operaciones con ficheros
como el nombre del código fuente del módulo podría indicar (node_file.cc).
31
Entre otras: manejo de directorios o cambio de permisos.
fs_event_wrap
Modela la clase evento de sistema de archivos que se emplea para notificar
cambios o errores en el mismo.
http_parser
Usado en conjunto con la dependencia http_parser de Node, que extrae
del mensaje HTTP toda la información relevante. Sirve de capa intermedia
entre ésta y el código JavaScript realizando principalmente la gestión de los
callbacks.
natives
Contiene el código fuente de todas las librerías del core de Node. El código
fuente del módulo se genera en tiempo de compilación por lo que no se
hallará en disco.
os
Funciones de recogida de información sobre el sistema operativo directa-
mente expuestas a os.js.
pipe_wrap
Capacidades de comunicación con procesos y con streams de TCP.
process_wrap
Métodos spawn y kill para el manejo de procesos.
tcp_wrap
Manejo de conexiones TCP y su stream asociado.
32
Se usa en el módulo net.js
timer_wrap
Funciones básicas para el manejo de un timer: start, stop, again...
tty_wrap
Integración con el terminal de libuv.
udp_wrap
Manejo de UDP.
zlib
Integración del proceso de compresión y/o descompresión en el thread pool
de libuv.
33
Básicamente, al proporcionar a una clase métodos para la gestión de eventos, se
facilita que:
_events = {
"connect": [function(){}, function(){}],
"customEvent": [function(){}, function(){}],
//si solo hay un subscriptor no se mete en un array
"newListener": function(){}
}
34
process.nextTick() se define en el proceso de bootstrap de node.js 18 . A través
de ella se podrán encolar una o más funciones en nextTickQueue, un array no
accesible a través del scope porque está protegido19 mediante un cierre, o closure,
al ser una variable local. El contenido de esta cola se ejecuta en la siguiente
iteración del bucle de eventos proporcionado por libuv. El funcionamiento interno
de este proceso se describe a continuación:
Node es una fina capa de software entre el sistema operativo y la aplicación escrita
para la plataforma porque los objetivos que se han perseguido con su arquitectura
son la velocidad y la eficiencia.
35
plexar la mayor cantidad posible de operaciones de Entrada/Salida en un mismo
thread en lugar de una por thread. Como ejemplos de alternativas que cumplen
estas condiciones y pueden ser tomadas como referencia están los green threads
y los “procesos” del lenguaje Erlang.
El modelo de concurrencia de Node encaja con los requisitos que se exigen a las
aplicaciones en tiempo real flexible (soft real-time). Un sistema de tiempo real es
un sistema informático que interacciona repetidamente con su entorno físico y
que responde a los estímulos que recibe del mismo dentro de un plazo de tiempo
determinado [33]. Hay principalmente dos tipos de estos sistemas:
sistemas de tiempo real flexible, entendiéndose por éstos aquellos en los que
las restricciones/condiciones de latencia son flexibles: se pueden perder pla-
zos, es decir, la respuesta al estímulo no cumple las condiciones de tiempo
impuestas, y además el valor de la respuesta decrece con el tiempo pero no
acarrea un desenlace fatal
36
No obstante, un ciclo de recolección no comienza aleatoriamente: es Node quien
avisa a V8 de que puede entrar en acción.
Node es útil por tanto para aplicaciones no críticas que admitan un cierto retardo.
Además se debe tener en cuenta su capacidad de manejar un alto número de
conexiones y procesar un enorme número de operaciones de Entrada/Salida muy
rápidamente. Éste es un requisito especialmente importante para las aplicaciones
que hacen un uso intensivo de datos (data-intensive applications) porque emplean
la mayor parte del tiempo en realizar este tipo de operaciones. Se puede afirmar
por tanto que Node encaja de manera excelente si se quiere [35]:
37
entre el cliente y el servidor.
Datos por streaming: al tratar las conexiones HTTP como streams, se pueden
procesar ficheros al vuelo, conforme se envían o reciben.
38
Capítulo 3
Las siglas UDP (User Datagram Protocol) se refieren a un sencillo protocolo que
pertenece al conjunto de protocolos de Internet. Se sitúa en el nivel de transporte,
por encima del nivel de red empleándose por ello en comunicaciones punto a
punto en redes de conmutación de paquetes basadas en IP. En estas redes el
paquete se denomina datagrama y se encamina sin que exista un circuito virtual
establecido sino que se hace en base a las direcciones que dicho datagrama lleva
en su cabecera.
39
puerto de origen (opcional)
puerto de destino
El resto del datagrama lo compone el mensaje, en forma binaria, que, junto con el
puerto de destino, es la información indispensable que necesita este nivel.
40
Una de las maneras de enviar tráfico a los clientes es el denominado Broadcasting.
Con este tipo de direccionamiento, el servidor envía datos a todos los clientes de
la red y ellos son los que deciden si estos datos son de su interés o no. Como es
lógico no se envían cliente por cliente, ya que supone un desperdicio de ancho de
banda, sino a la dirección de broadcast de la red.
Como se sabe, en una red hay dos direcciones especiales: aquella en la que los
bits del sufijo son todo ceros, que identifica a la red, y aquella en la que los bits
del sufijo son todo unos. Ésta última es la dirección de broadcast y todos los
interfaces, aparte de escuchar la dirección IP que tienen configurada, escuchan
el tráfico dirigido a esta otra IP.
Una aplicación para Node puede hacer uso de UDP importando la librería dgram:
La manera de obtener un socket UDP es sencilla, tanto para cliente como para
servidor, es invocando el método:
41
para IPv6. Opcionalmente y para sockets servidor, la función de callback será
la que atienda los mensajes que lleguen de los clientes instalándose como
listener del evento ’message’, por lo que puede hacerse más adelante.
dgram.bind(puerto, [direccion])
Permite escuchar en un puerto la llegada de datagramas. En máquinas con
varios interfaces, se escuchará en todos excepto si se especifica uno concreto
pasando como argumento su direccion IP.
dgram.close()
Deja de escuchar en el socket y lo cierra, emitiendo el evento ’close’. A
partir de que se emita este evento no se generarán más eventos ’message’.
rinfo = {
address: ’192.168.21.7’,
family: ’IPv4’,
port: 50251,
42
size: 436 }
Junto con los tres eventos vistos (’listening’, ’message’, ’close’) se encuen-
tra ’error’, que se emite sólo cuando ocurre un error en el socket:
Aparte de los métodos anteriores está disponible una función auxiliar con la que
obtener información (dirección, puerto y protocolo) del socket que la máquina está
empleando en la comunicación:
dgram.address()
devuelve el objeto en notación JSON, por ejemplo:
{ address: ’0.0.0.0’,
family: ’IPv4’,
port: 23456 }
Más allá del nivel de transporte, Node también permite controlar ciertos paráme-
tros del nivel de red, justamente aquellos relacionados con la difusión de la que
antes se ha comentado.
El único parámetro más general que se puede ajustar es el TTL (Time To Live).
Este tiempo de vida es un entero entre 1 y 255 (8 bits) que marca el número má-
ximo de saltos que un paquete IP puede dar entre redes antes de ser descartado.
Típicamente es 64 y se fija con:
dgram.setTTL(saltos);
dgram.setBroadcast(flag);
dgram.addMembership(direccionMulticast, [interfaz])
es el mecanismo que el módulo ofrece para la subscripción de la interfaz
de la máquina a un grupo de multicast identificado con la dirección IP
direccionMulticast. Si no se especifica la interfaz, se añadirán todos
los posibles.
43
dgram.dropMembership(direccionMulticast, [interfaz])
realiza la acción opuesta al método anterior, sacando a la interfaz del gru-
po multicast de dirección IP direccionMulticast. Al igual que con
dgram.addMembership(), si no se especifica interfaz alguna, se sacan
del grupo todos los que pertenezcan a la máquina.
dgram.setMulticastLoopback(flag)
habilita o deshabilita a la interfaz local para que también reciba o deje de
recibir los paquetes multicast que se envían desde el mismo.
dgram.setMulticastTTL(saltos)
establece, de manera idéntica a dgram.setTTL(), el número de saltos que
un datagrama multicast puede dar en una red.
Cuando se trabaja con texto, o sea, cadenas de caracteres, debe existir una forma
de hacer “legible” el conjunto de bits sin formato que existe en memoria agru-
pándolos de manera que se transformen en texto reconocible, con o sin sentido,
por el ser humano. Este proceso se conoce como Codificación de caracteres. En
él se establece una correspondencia unívoca, llamada esquema de codificación,
entre secuencias de bits y caracteres correspondientes a algún alfabeto concreto
definido por su "juego de caracteres"(character set).
ASCII
[38] este esquema está diseñado para la representación de texto usando el
alfabeto inglés. Empleando 7 bits para ello, por lo que define 128 caracteres,
incluyendo alfanuméricos, símbolos y 33 caracteres de control, como por
44
ejemplo NULL o CR (retorno de carro). Hacer notar que un byte consta de 8
bits, de tal manera que si cada carácter se codifica con uno, todavía quedan
128 caracteres por definir. Esto se ha empleado habitualmente para definir
ampliaciones del código, existiendo multitud de ellas, incompatibles entre sí
puesto que sólo tienen en común los 128 primeros caracteres.
Unicode
[39] es la solución propuesta a los problemas aparecidos por el gran número
de codificaciones. Pretende emplear un sólo código para todos los alfabetos
posibles, siendo capaz de representar más de un millón de caracteres. In-
troduce los llamados “puntos de código” (codepoints) que son, por decirlo de
alguna manera, un identificador de carácter. Así, a una “A” le corresponde
un codepoint, a una “B” otro codepoint...Los codepoints se distinguen por ser
un número hexadecimal que comienza por U+. Para el caso anterior, a la
“A” le corresponde el codepoint U+0041, a la “B” el U+0042...JavaScript es
un lenguaje de programación pensado para un Entrada/Salida formateada
como texto. Y éste codificado en Unicode.
Son varias las maneras en que Unicode realiza las codificaciones de los code-
points. La primera de ellas es la más conocida y extendida en uso: UTF-8. Es
una codificación multibyte: los primeros 128 codepoints se almacenan con
un solo byte, y los siguientes se van almacenando con dos, tres o hasta seis
bytes.
45
de mensaje cuya codificación debe ser ASCII (según la RFC822) [40]. Si se adjunta
contenido multimedia o texto en otro lenguaje con símbolos que no pueden repre-
sentarse en esa codificación, como puede ser un vídeo, habría problemas para su
envío o recepción. En este caso concreto, se introdujo un mecanismo, MIME [41],
que utiliza Base64 para codificar el contenido de los mensajes de correo y hacer
posible su correcto intercambio.
Hay ocasiones en las que la entrada o salida del sistema no es texto sino datos
binarios como, por ejemplo, los que se reciben en las conexiones UDP o TCP si
se está trabajando con redes, o los que se reciben a raíz de la lectura de un
fichero, si se trabaja con el sistema de ficheros de la máquina donde se ejecuta
el programa. En estos casos se necesita tratar con los octetos en bruto, tal y
como están en memoria, accediendo a ellos y transformándolos o convirtiéndolos
en determinados tipos de datos según sea necesario durante la ejecución del
programa.
Con este propósito existe el objeto Buffer: tener una manera eficiente de crear,
manipular y consumir bloques de octetos. A bajo nivel, el API de Node habla de
que un objeto Buffer contiene una zona de memoria tal cual está alojada fuera
del heap del motor V8. Puede verse como un array de bytes con tamaño fijo que
no puede redimensionarse.
46
El módulo buffer, que es el que provee el objeto Buffer, está presente en el espacio
global de ejecución del script src/node.js, con lo cual no es necesario importarlo
explícitamente con require(). Directamente, se puede instanciar un Buffer de
tres maneras:
A partir de este punto tenemos un Buffer, buf, del que conocemos su tamaño
con buf.length, sobre el que se pueden realizar operaciones de escritura y lec-
tura.
una indexación directa, como con los arrays, buf[indice]. De esta manera
se accede a la posición indice cuyo octeto se puede fijar con un valor que,
al tener 8 bits, varía entre 0 y 255.
47
También se debe contemplar también el caso que no haya suficiente espacio
en el Buffer, con lo que sólo se escribirán los bytes que quepan, sin escribir
parcialmente un carácter. Esto es, si queda una posición libre de un byte y
se pretende introducir un carácter Unicode, que ocupa dos, no será posible.
• enteros de 8 bits con y sin signo, cuyos rangos válidos son, -128 a 127
y 0 a 255, respectivamente, que ocuparán una posición del Buffer
• enteros de 16 bits con y sin signo, cuyos rangos válidos son, -32768 a
32767 y 0 a 65535, respectivamente, que ocuparán dos posiciones del
Buffer
• enteros de 32 bits con y sin signo, cuyos rangos válidos son, -21474883648
a 21474883647 y 0 a 4294967295, respectivamente, que ocuparán cua-
tro posiciones del Buffer
• float de 32 bits
• double de 64 bits
Big Endian asigna los bytes más significativos a los índices más bajos del
Buffer y, por el contrario, Little Endian asigna el byte menos significativo a
los índices más bajos. Sirva de ejemplo, el entero de 32 bits 0xEF4A71B8 se
escribiría en el Buffer, representado aquí por [], como [EF, 4A, 71, B8]
en Big Endian y como [B8, 71, 4A, EF] en Little Endian.
48
Las combinaciones posibles deben ser lógicas y de acuerdo a lo explicado an-
tes, por ejemplo, son válidas buf.writeUInt16LE() o buf.writeFloatBE()
y no son válidas, por ejemplo, buf.writeFloat32LE(), porque Float siem-
pre es de 32 bits y por tanto no se especificaría, o buf.writeUDoubleBE(),
porque Double no es un tipo que se pueda escoger con signo o sin él. Los
correctos serían, buf.writeFloatLE() y buf.writeDoubleBE().
La clase Buffer además ofrece operaciones similares a las que se pueden realizar
con un array:
buf.slice([inicio], [final])
es una función que devuelve un Buffer cuyo contenido es el mismo que el
49
contenido del Buffer buf entre las posiciones inicio y final pero, muy
importante, porque hace referencia a él. Esto significa que si se modifica
cualquier octeto en alguna posición de ese rango en alguno de los dos Buf-
fers, el cambio se refleja en ambos.
Buffer.concat(lista, [longitudTotal])
concatena todos los Buffers contenidos en el array lista devolviendo un
nuevo Buffer, siempre que lista tenga más de uno o no esté vacía, porque
si no, devuelve el Buffer o no devuelve nada respectivamente.
Como ejemplo del tipo de aplicaciones que se pueden crear con UDP se va a imple-
mentar una versión muy sencilla del protocolo RTP. Se empleará para desarrollar
una aplicación que emitirá en tiempo real los archivos de audio MP3 alojados en
una máquina remota.
50
Se puede configurar de la siguiente manera para escuchar las canciones que
emite la aplicación:
51
Figura 3.3: Reproductor VLC, configuración de la IP
RTP son las siglas de Real Time Protocol, un protocolo definido para el envío de
contenido multimedia en tiempo real sobre UDP. La versión definitiva de la espe-
cificación completa se encuentra en la RFC3350 [42]. Sólo se implementará una
pequeña parte de ella, que proporcione la suficiente funcionalidad para el pro-
pósito de la aplicación. Esto incluye, básicamente, la generación correcta de la
cabecera RTP de los paquetes.
versión: 2 bits que almacenan el número de versión del protocolo, que va por
la segunda, por lo que el valor será siempre 2.
padding: flag que indica la presencia de relleno al final del paquete. Suele
ser común en aplicaciones que necesitan un tamaño fijo de paquete. No es
el caso de esta aplicación, por lo que su valor puede quedar a false.
extension: flag que indica que la cabecera se extiende con una cabecera de
ampliación que no se va a usar en esta aplicación.
52
contador CSRC: la cabecera RTP puede llevar a continuación una lista de
fuentes que contribuyen a la generación del contenido del paquete. Este
campo de 4 bits indica cuántas van en la lista anexa, pero sólo lo insertan
los mezcladores, por lo que carece de utilidad en una aplicación de este tipo.
marker bit: flag cuyo significado depende del contenido que se esté transmi-
tiendo. El propósito general es destacar puntos concretos en la transmisión,
como sucede cuando para transmitir un frame se emplean varios paquetes.
En este caso el marker bit activado indica que se ha transmitido el último
paquete de los que componen el frame.
timestamp: 32 bits cuyo valor indica el instante de muestreo del primer oc-
teto del contenido del paquete. El cálculo de este valor depende del tipo de
contenido. En el caso de audio MPEG, MPA, la RFC3351 marca que la tasa de
reloj sea siempre de 90000 Hz, independientemente de la tasa de muestreo
del audio (44100 Hz para esta aplicación).
SSRC: identifica con 32 bits la fuente, único para cada fuente distinta en
una sesión. Esta aplicación dispone de una sola fuente, por lo que su valor
puede ser establecido sin necesidad de tener en cuenta la probabilidad de
colisión con los valores de otras fuentes.
Además de esto, enviar audio en formato MP3 requiere una cabecera adicional
específica, tal y como la RFC2250 explica [44, Section 3.5], con dos campos:
MBZ: 16 bits que no tienen un uso definido y por tanto, no se van a emplear.
Fragment Offset: 16 bits que marcan el inicio del frame de audio dentro de
los datos del paquete.
53
Todo esto se implementa en el módulo RTPProtocol.js. Este módulo ofrece la clase
RTPProtocol cuya misión es simplemente encapsular un frame multimedia, en
este caso audio MP3 de unas características concretas, en un paquete RTP con la
cabecera que corresponda.
54
La clase MP3Source modela el comportamiento de una fuente de audio MP3 que
a partir de un archivo genera frames con una frecuencia tal que simula que se
están emitiendo en tiempo real. En la aplicación que se está desarrollando como
ejemplo, sólo se tratará con archivos .mp3 cuyas características son que están
muestreados a una frecuencia de 44100 Hz y codificados con una velocidad de
128 Kbps. Cualquier otro .mp3 con distintas características producirá un mal-
funcionamiento de la aplicación. Estos ficheros .mp3 se obtienen de la librería
Mp3Library:
128000 bytes
× 0,02612 segundos bytes
f rame = 417,959 f rame (3.2)
8 segundo
55
Los frames que la fuente MP3 emite deben ser encapsulados en un paquete RTP
con su cabecera perfectamente formada antes de ser enviados al grupo multicast.
Por este motivo se hará que el objeto RTPProtocol atienda los eventos ’frame’
que emite MP3Source.
mp3source.on(’frame’, function(frame){
rtpserver.pack(frame);
});
Cuando a través de estos eventos RTPProtocol recibe el frame MP3, le añade las
cabeceras, con RTPProtocol.pack(payload), y genera un evento ’packet’ para
que el objeto en la capa de red que atiende el evento pueda enviarlo. Junto al
evento ’packet’ se envía el paquete calculado.
RTPPacket.writeUInt8(128, 0);
Para esta aplicación, que el Marker Bit esté activo o no depende de un par de
criterios un tanto arbitrarios. El primero es si no hay payload por el motivo
que sea, por ejemplo que la fuente no haya sido capaz de generar uno en el
56
tiempo previsto. Se activará siempre después de que se hayan actualizado
los campos relativos al orden de los paquetes y al tiempo porque, a pesar de
ello, éstos siguen contando.
if (!payload) {
this.setMarker = true;
return;
}
mp3source.on(’track’, function(){
rtpserver.setMarker = true;
}); (Player.js)
++this.seqNum;
RTPPacket.writeUInt16BE(this.seqNum, 2);
57
/ SAMPLING_FREQUENCY);
this.timestamp += TIMESTAMP_DELTA;
RTPPacket.writeUInt32BE(this.timestamp, 4);
el identificador de fuente SSRC, que será único, puesto que sólo se emplea
una fuente en la aplicación, y aleatorio, porque se ha comentado que no
hay previsión de que pueda colisionar con más fuentes. Se calcula en el
constructor:
RTPPacket.writeUInt32BE(this.ssrc, 8);
añadir la cabecera específica para audio MPEG, donde ambos campos serán
cero. El primero, MBZ, porque no se usa, y el segundo, Fragment Offset,
porque el inicio del frame dentro de la carga de datos es inmediatamente al
principio, puesto que no lleva más que eso:
RTPPacket.writeUInt32BE(0, 12);
payload.copy(RTPPacket, 16);
rtpserver.on(’packet’, function(packet){
udpSender.broadcast(packet);
}); (Player.js)
58
stats = {
txPackets : 0,
txBytes : 0
};
Por defecto, no se responde a las peticiones de estadísticas con lo que habrá que
habilitarlo una vez se haya creado la clase:
Por esta doble función que desempeña entonces, esta clase consta de dos soc-
kets:txSocket, para emitir al grupo de multicast, y rxSocket, para recibir las
peticiones de los datos estadísticos:
59
});
El segundo socket, rxSocket, escucha los mensajes que piden que se envíen las
estadísticas al solicitante. En esta aplicación está asociado, arbitrariamente, al
puerto 5001:
this.rxSocket.bind(5001);
Una vez habilitado para recibir, recibirá atendiendo el evento para ello, ’message’
enviando los datos solicitados de inmediato. A diferencia de txSocket, que trans-
mite para una IP multicast, la respuesta debe ir dirigida al solicitante cuya IP no
se conoce a priori pero que puede consultarse en el paquete de la petición. Estará
contenida en la estructura rinfo, remote info, que el listener recibe junto con el
mensaje:
Toda la lógica que establece la relación entre estos módulos está en el script
app.js, y se ha ido introduciendo conforme se ha ido desgranando la interacción
de los mismos.
60
3.6. Objetivos de los Koans
RTPPacket.___(128, 0);
RTPPacket.___(this.seqNum, 2);
RTPPacket.___(this.timestamp, 4);
De dgram se pretende conocer el ciclo de vida de un socket UDP, con las primitivas
más comunes que soporta para una comunicación completa.
this.txSocket = dgram.___(’udp4’);
this.rxSocket = dgram.___(’udp4’);
this.rxSocket.___(7531);
this.txSocket.___(packet,
0,
packet.length,
this.port,
this.broadcastAddress,
61
function(err, bytes){
++self.stats.txPackets;
self.stats.txBytes += bytes;
});
4. Saber que hay que atender el evento ’message’ para recibir correctamente
datagramas de clientes:
function(msg, rinfo){
var stats = new Buffer(JSON.stringify(self.stats));
dgram.createSocket(’udp4’).send(stats,
0,
stats.length,
5002,
___.address);
};
Los koans están distribuidos en dos ficheros: dgram-koans.js, que contiene los
relacionados con el módulo dgram, y buffer-koans.js, que comprende aquellos
que tienen que ver con el módulo buffer. Ambos deben ser editados y completados,
sin que quede ningún hueco ___ (tres guiones bajos seguidos).
Verificar que se han realizado con éxito los koans se determina ejecutando los
casos de prueba con:
62
Como resultado de una correcta resolución de los koans, la práctica es completa-
mente funcional y se puede ejecutar con el comando:
$ node app.js
Para escuchar la música que se distribuye por RTP, hay que configurar un repro-
ductor tal y como se indica en el apartado “Descripción de la aplicación”.
2
http://netcat.sourceforge.net/
63
Figura 3.6: Respuesta a la petición con Netcat
3.8. Conclusión
64
Capítulo 4
TCP es un protocolo fiable: los datos enviados se entregarán en destino sin errores
y en el mismo orden en que se transmitieron. Para conseguir estas características,
TCP dispone de mecanismos como los números de secuencia y los asentimientos,
conocidos como ACK, a través de los cuales se indica qué octetos se han envia-
do y cuáles se han recibido. Hay algunos más, como el control de congestión,
65
que regula el flujo de datos entre emisor y receptor para evitar pérdidas, o el
establecimiento de conexión, que inicializa los estados del protocolo. Todos son
posibles gracias a los campos de la cabecera específicamente reservados con ese
objetivo.
Como es lógico, toda esta evolución con respecto al protocolo UDP se ve reflejada
en la cabecera de un segmento TCP. Aparte de los ya conocidos campos Puerto
Origen, Puerto Destino y Checksum, aparecen varios más relacionados con los
mecanismos anteriormente mencionados:
Flags
Para distintos estados y acciones del protocolo que activados indican que
ACK el segmento TCP está siendo utilizado para realizar el asentimiento del
correspondiente segmento recibido y por tanto el número de asentimien-
to que contiene es válido.
PSH los datos deben ser enviados al receptor inmediatamente. Esto es por-
que TCP, si lo estima necesario, puede almacenar los datos que la apli-
cación le entrega hasta que considere que se pueden enviarlo a través
del canal, es decir, el envío de datos puede no ser inmediato. Este flag,
66
push, activado en un segmento, hará que todos los datos se envíen.
Tamaño de ventana
como parte del mecanismo de control de congestión, este campo indica el
número de octetos que el receptor admite a partir del último que ha recibido
correctamente (indicado por el número de asentimiento). [47, Chapter 2.6].
67
3. Se envía otro segmento de asentimiento con, únicamente, el flag ACK
activado, con el que se acepta el número de secuencia recibido en el
paso anterior.
68
por la que no se han recibido datos o asentimientos en mucho tiempo es-
tá todavía activa. Para ello se envían periódicamente segmentos TCP sin
datos o con un octeto aleatorio como carga y se espera como respuesta
un ACK, siendo un RST si ha habido interrupción de la conexión. Estos
envíos se realizan después de que haya pasado un intervalo de tiempo
configurable, o por defecto no inferior a dos horas según la RFC, sin
actividad en el socket desde la última recepción de un segmento TCP.
Se recomienda, en caso de que se use, que sea por la parte servidora hacia el
cliente para detectar caídas o desconexiones accidentales por parte de éste
y liberar los recursos que se le habían asignado.
69
que quiera comportarse como un Stream a heredar de él y a ofrecer una serie de
métodos para el manejo de dicho Stream y, además, condiciona al programador a
implementar esos métodos de manera que sean capaces de realizar operaciones
sobre el flujo concreto que yace por debajo. Esto se debe a que cada flujo binario
puede tratarse de diferentes maneras y las acciones de lectura/escritura no tienen
porqué ser iguales a las de otro flujo que también las ofrezca.
Por ejemplo, una conexión TCP y un archivo del sistema de ficheros en Node se
tratan como Streams, sin embargo, la implementación de las operaciones sobre
ellos son distintas por la propia naturaleza de cada uno: datos sobre un protocolo
de red vs. datos almacenados en el sistema de ficheros del sistema operativo.
stream.pause()
detiene la entrega de datos, es decir, no se emitirán eventos ’data’ con
lo que la lectura quedará pausada. El API de Node advierte que pueden
seguir emitiéndose algunos eventos ’data’, cuya información se puede el
programa puede almacenar, después de invocar al método hasta que la parte
de Entrada/Salida, libuv, de Node procese la señal de parada.
stream.resume()
reanuda la entrega de datos pausada con el método anterior
stream.destroy()
anula toda posibilidad de operaciones Entrada/Salida sobre el socket. En
consecuencia, no se emitirán más eventos ’data’ o ’end’ aunque sí cabe
esperar un evento ’close’ cuando se liberen todos los recursos asociados
al Stream.
70
si son datos en el formato por defecto, esto es, los bytes en bruto de un
Buffer, se utiliza stream.write(buffer).
El ciclo de vida de este Stream writable se gestiona con un método que adopta
varias formas:
stream.end()
que finaliza el stream según corresponda a la naturaleza del Stream. Por
ejemplo, si se trata de un fichero, escribe el carácter de control EOF (End Of
File).
Tiene una variante, que permite escribir datos en el stream antes de cerrar
y luego, cerrarlo. Si los datos son bytes se emplea stream.end(buffer), y
si es texto, stream.end(string, [codificacion]).
stream.destroy()
tiene idéntico efecto al del caso del Stream readable: impide que se realicen
operaciones de Entrada/Salida, sin importar que queden datos por escribir.
Para que una clase creada por el programador se comporte como un Stream se
debe importar la clase base Stream contenida en el módulo stream, heredar de ella
e implementar los métodos para las operaciones de lectura/escritura y gestión del
Stream:
function myNewStream(){
Stream.call(this);
// codigo que implementa el constructor
}
utils.inherit(myNewStream, Stream);
71
myNewStream.prototype.write = function(datos, codificacion){
// implementacion de la funcion
}
Stream.pipe(destino, [opciones])
permite conectar un Stream readable, que será una fuente de datos binarios,
a un destino writable donde se escriben esos datos de manera sincronizada.
Esto lo consigue pausando y reanudando el Stream emisor según los datos
se van escribiendo o no en el Stream destino. Cuando el emisor finaliza
con el evento ’end’, invoca al método destino.end() a menos que en las
opciones se indique lo contrario:
Una aplicación Node puede hacer uso de TCP importando la librería net:
72
se instancia directamente: se debe hacer a través de métodos específicos para
ellos.
Desde la parte cliente, obtenemos un socket creando una conexión a una ubica-
ción concreta mediante los métodos net.connect() o
net.createConnection() indistintamente. Ambos métodos tienen varias cabe-
ceras que aceptan distintos argumentos. Las más generales son:
{ port: 5479,
host: "192.168.1.32",
localInterface: "192.168.1.26" }
Para ambos casos existe una opción común, allowHalfOpen, por defecto a
false pero que si se activa permite mantener la conexión “medio abierta”
cuando el otro extremo cierra su parte, dando al programador la posibili-
dad de gestionar él mismo qué hacer. A nivel de protocolo, esto significa que
cuando se recibe desde el otro extremo un segmento FIN, no se envía otro
FIN, como se ha visto que se comporta el protocolo en la fase de Fin de Co-
nexión, sino que el programador puede seguir enviando datos (recordar que
half-open implica que sí se puede recibir) o cerrar la conexión explícitamente
con socket.end().
73
son las cabeceras para emplear sockets de dominio UNIX.
Todos los métodos emiten el evento ’connect’ de tal manera que instalar un lis-
tener para ese evento con socket.on(’connect’, function() ) es equivalente
a invocar a los métodos pasándoles un callback.
socket.setNoDelay([sinRetardo])
activa o desactiva, dependiendo del valor del argumento booleano
sinRetardo, el algoritmo de Nagle. sinRetardo por defecto es true, con
lo que los datos se envían de inmediato por el socket y, por tanto, indica que
el algoritmo está desactivado.
socket.setKeepAlive([activo], [retardoInicial])
activa o desactiva, con el argumento booleano activo, el mecanismo de
Keep-Alive, por defecto desactivado. El retardoInicial indica en milise-
gundos el tiempo que debe estar el socket inactivo para que entre en acción.
socket.setEncoding()
independiente del protocolo TCP en sí (no se modifican parámetros relativos
74
a la conexión) y orientado a la codificación de los datos que transporta para
su consumo en el programa. Por defecto, la codificación es la estándar de
Node, utf8.
socket.address()
proporciona la dirección IP address de la interfaz donde está establecido el
socket, la familia family de dicha dirección (IPv4 o IPv6) y el puerto local
port al que está ligado, todo encapsulado en un objeto JSON, como por
ejemplo
{ address: "192.168.1.35",
port: 5556,
family: "IPv4" }
socket.remoteAddress
es la dirección IP del otro extremo del socket
socket.remotePort
es el puerto que emplea el otro extremo del socket
socket.on(’data’, function(data){
//codigo para procesar los datos
});
75
Se puede además gestionar su ciclo de vida con los métodos habituales para ello,
tanto los de los Streams writable como los de los readable:
socket.pause()
el API de Node lo recomienda junto con resume() para ajustar la tasa de
transferencia en la recepción de datos (por parte del servidor en los prove-
nientes del cliente) ya que, como se ha explicado, pause() detiene la recep-
ción de datos.
socket.resume()
reanuda la recepción de datos detenida con el método pause(). De esta
manera se vuelven a emitir eventos de tipo ’data’.
socket.end([datos], [codificacion])
si se invoca sin argumentos, comienza la fase de final de conexión enviando
un paquete FIN, quedando ésta “medio cerrada”: no se podrá escribir en el
socket pero sí se podrán recibir datos del otro extremo (si este extremo no
ha finalizado su conexión también con end()).
socket.destroy()
inhabilita completamente las operaciones de Entrada/Salida en el socket,
por lo que se usa en los casos en que se haya producido un error.
76
‘connection’ como con todos los eventos de las instancias de la clase
EventEmitter: server.on(’connection’, function(socket){ }).
server.address()
que funciona de manera idéntica a la función homónima de la clase Socket,
devolviendo un objeto JSON con tres propiedades: dirección IP address, tipo
de IP family (v4 o v6) y puerto de escucha port. Por ejemplo:
{ port: 6420,
family: ’IPv4’,
address: ’127.0.0.1’ }
server.connections
es la propiedad que lleva la cuenta del número de conexiones concurrentes
que maneja el servidor en ese momento
server.maxConnections
es la propiedad que fija el número máximo de conexiones concurrentes en
el servidor, es decir, es el límite superior de la anterior propiedad. Cuando
se alcanza este número, las conexiones se rechazan, abortando el intento de
conexión en la parte cliente.
77
callback recibe el socket correspondiente a la conexión, el cual, obviamente,
es una instancia de Socket y, por tanto, se maneja como tal.
Durante la ejecución del servidor pueden producirse errores que se procesan cap-
turando el evento ’error’:
server.on(’error’, function(error){ })
78
Puesto la aplicación se basa en un intérprete de comandos, seguirá siendo nece-
sario un reproductor de audio que soporte RTP que apunte a la dirección IP de la
máquina desde donde se realice la conexión a la aplicación.
Los requisitos de esta aplicación hacen necesaria una modificación del diagrama
de módulos respecto al que modelaba la aplicación UDP/RTP. En este escenario
se introduce la clase RemotePrompt en el módulo principal de la aplicación y será
donde se implementen las características que se le han pedido al servidor. El
diagrama que modela el escenario para esta práctica sería el siguiente:
79
this.server.on(’connection’, function(connection){
var remoteIP = connection.remoteAddress;
if (remoteIP in sessionsDB){
connection.end("Duplicated session, closing.");
return;
};
sessionsDB[remoteIP] = true;
Por cada una de las conexiones anteriores se creará una fuente de audio MP3,
MP3Source, que controle el estado de la reproducción. Esta clase proporcionará
para ese propósito unas funciones que se invocarán según demanden los co-
mandos. Además, informará a través de eventos sobre aspectos relevantes de la
reproducción que son útiles que el cliente conozca, notificándoselo automática-
mente. En concreto, mediante el evento ’track’, MP3Source avisará de cuándo
se ha realizado una operación sobre una canción, bien bajo demanda del cliente
o bien porque sea inherente al transcurso de la reproducción, como cuando se
pasa a la siguiente pista:
source.on(’track’, function(trackName){
connection.write("Now playing " + trackName + "\r\n# ");
});
source.on(’listEnd’, function(){
var seconds = 10;
connection.write("End of the list reached.Closing in "
+ seconds
80
+ " seconds\r\n# ");
connection.setTimeout(seconds * 1000, function(){
delete sessionsDB[this.remoteAddress];
connection.end("Your session has expired. Closing.");
});
});
rtpserver.on(’packet’, function(packet){
udpSocket.send(packet, 0, packet.length, 5002, remoteIP);
});
source.on(’frame’, function(frame){
rtpserver.pack(frame);
});
Los comandos se reciben a través del socket de la conexión. Se ha visto que para
recibirlos, y recibir cualquier tipo de datos en general, es necesario atender al
evento ’data’ para lo que habrá que instalar el correspondiente listener: será el
encargado de procesar los comandos y actuar en consecuencia sobre la fuente
MP3.
connection.on(’data’, function(data){
this.setTimeout(0);
// Logica del procesado de los comandos
});
81
posteriormente, proceder a identificarlos:
switch(command){
case "list":
var playlist = source.list();
this.write("\r\nSongs in the playlist");
this.write("\r\n---------------------");
for (var i=0; i < playlist.length; i++){
var song = playlist[i];
this.write("\r\n"
+ (source.currentTrack() == song? "> " : " ")
+ song);
}
this.write("\r\n# ");
break;
case "play":
source.play();
break;
case "pause":
source.pause();
break;
case "next":
source.next();
break;
case "prev":
source.prev();
break;
82
case "exit":
delete sessionsDB[this.remoteAddress];
this.end("Bye.");
break;
default:
this.write("Command " + command + " unknown\r\n# ");
}
Exceptuando “exit”, que no actúa sobre la fuente de audio, el resto de los coman-
dos provocarán que MP3Source emita el evento ’track’. Así, se ofrecerá infor-
mación al cliente sobre el resultado de su acción, como se ha comentado durante
el desarrollo de la solución propuesta.
Por último, sería necesario liberar los recursos ocupados por un cliente una vez
éste haya finalizado su sesión ya sea porque la cierra el cliente (con “exit”), el
servidor (por ’listEnd’) o se corta la conexión. La mejor manera de conocer esto
es atendiendo el evento ’close’ del socket:
connection.on(’close’, function(){
source.pause();
udpSocket.close();
rtpserver = source = null;
});
Toda la lógica anterior, menos la parte que pone a escuchar al servidor, está con-
tenida en el constructor del objeto RemotePrompt. Aparte del constructor, Remote-
Prompt también posee un método listen(), con el que comenzar la actividad de
la aplicación, puesto que se encarga de ordenar al servidor comenzar a escuchar
en un puerto:
this.listen = function(port){
this.server.listen(port);
}
83
de create(), consume un tiempo indeterminado que debe esperarse y durante
el cual, el objeto RemotePrompt está en un estado indefinido. Con create() se
eliminan los errores derivados de esta espera, pues gestiona ella misma el evento
’ready’ de la librería, y ofrece una instancia de RemotePrompt funcional desde
el primer momento. El siguiente fragmento de código explica lo anterior, aunque
no sería necesario conocerlo para el desarrollo de la práctica:
exports.create = function(){
var app;
var library = new nodeMp3.Mp3Library({ basedir: ’../data/songs/’ });
library.on(’ready’, function(){
> require(’./RemotePrompt’).create().listen(2323)
84
1. Emplear el método createServer() para disponer de un servidor para co-
nexiones TCP:
this.server = net.___();
this.server.___(port);
3. Atender cada una de las conexiones que acepta el servidor mediante la es-
cucha del evento ’connection’ que él mismo emite:
this.server.on(___, function(connection){});
85
El fichero que contiene los koans es net-koans.js, que es en realidad el módulo
RemotePrompt, puesto que es éste el que lleva todo el código asociado al módulo
net. Al igual que en la práctica anterior debe ser editado y completado, sin dejar
ningún hueco ___.
Para verificar que se han realizado con éxito los koans se ejecutan los casos de
prueba con:
$ node app.js
Para escuchar la música que se distribuye por RTP, hay que configurar un re-
productor tal y como se indicó en el apartado “Descripción de la aplicación” del
anterior capítulo. Esta vez, sin embargo, los paquetes se reciben en la dirección
IP de la interfaz de la máquina que se conecta al servidor. Cuando se realiza
una conexión al servidor, éste informa de la dirección donde hay que apuntar el
Reproductor. Para conectarse bastará un simple telnet:
86
Figura 4.3: Reproductor VLC, configuración
87
4.7. Conclusión
88
Capítulo 5
Módulo Http
<protocolo>://<maquinaremota>:<puerto>/<ruta>/<al>/<recurso>
[?<query>=<valor>]
Por el tema que nos ocupa, <protocolo> es obvio que es http, aunque podría
ser https si la conexión sobre la que se establece la comunicación fuese un
canal seguro. La <maquinaremota> es el nombre de DNS o la dirección IP de
la máquina donde corre el servidor HTTP, general, pero no necesariamente, es-
cuchando en el <puerto> 80 que es el asignado por la IANA1 para HTTP. La
<ruta>/<al>/<recurso> es el path, en general relativo en el sistema de ficheros,
del servidor donde se aloja el recurso. Por último, en el caso de que el recurso sea
1
Internet Assigned Numbers Authority es el organismo encargado de fijar de manera oficial los
códigos y números que se emplean en los protocolos estándar de Internet como rangos de IPs o
números de puerto
89
dinámico, es decir, sea la salida de un programa ejecutable localizado en la URL,
se le pueden pasar parámetros de entrada en la parte opcional de la misma, la
<query>. Una Query comienza en la URL con el signo de interrogación ‘?’ a partir
del cual se concatenan con el símblo & los parámetros de nombre <query> y su
<valor> asociado con un =. Ilustrando todo lo anterior:
http://www.google.com/search?q=nodejs+koans
El escenario de uso del protocolo implica, en su manera más básica, a dos actores
que interactúan mediante un modelo de intercambio de mensajes denominados
petición, para la parte del cliente, y respuesta, para la del servidor. Este inter-
cambio no tiene estado, es decir, los mensajes son independientes de anteriores
y posteriores.
Los actores implicados son principalmente el cliente, a través del Agente de Usua-
rio, que es aquel programa que genera las peticiones, y el Servidor, que las recibe
y es capaz de interpretar el protocolo en el extremo opuesto.
Un túnel se puede considerar, como explica la RFC [49, Section 1.3], como un
intermediario que actúa de enlace entre dos conexiones, sobre las que pueden
ir distintos protocolos. El mecanismo de tunelado permite a la máquina origen,
cliente, establecer una conexión punto a punto a través del túnel, que dejará de
ser parte de la comunicación HTTP y se convertirá en un elemento transparente,
dejando de existir cuando se cierre la comunicación en los extremos.
90
5.1.1. La parte del Cliente
Las peticiones HTTP están formadas por líneas de texto claro, terminadas con los
caracteres de control <CRLF> (Retorno de carro más salto de línea). La primera
línea, request-line, es siempre obligatoria, y el resto, las cabeceras y cuerpo, apor-
tan información adicional. El esquema que sigue la request-line son tres campos
separados entre ellos por un espacio:
GET
devuelve el recurso identificado por la <URI>. Puede ser estático o pueden
ser datos generados dinámicamente.
HEAD
devuelve información sobre el recurso solicitado siendo dicha información
la misma que proporcionaría GET pero sin incluir el recurso en sí en la
respuesta
Los dos métodos anteriores se denominan “Métodos seguros” [49, Section 9.1.1]
puesto que la acción que se solicita con ellos únicamente tiene como resultado
la obtención de un recurso del servidor y/o información sobre él. Hay otros mé-
todos que se pueden considerar inseguros porque pueden producirse resultados
potencialmente peligrosos:
POST
solicita al servidor el procesado del cuerpo de la petición con objeto introdu-
cir nueva información en el recurso identificado por la URI como, por ejemplo
y según la RFC2616, aportar datos desde un formulario o introducir infor-
mación en una base de datos.
PUT
solicita al servidor crear o modificar el recurso identificado por la URI.
DELETE
solicita al servidor la eliminación del recurso identificado por la URI.
91
De todos los anteriores métodos, GET, HEAD, PUT y DELETE se pueden con-
siderar “Métodos idempotentes”, es decir, que los efectos colaterales de varias
peticiones de uno de ellos son los mismos que si se invocase el método una sola
vez.
La versión 1.1 añade muchas mejoras respecto a la 1.0, entre las cuales se pueden
destacar:
Nuevos métodos: HTTP 1.0 contempla los métodos GET, HEAD y POST úni-
camente, el resto de métodos comentados anteriormente y alguno más, como
OPTIONS, pertenecen a HTTP 1.1.
Conexiones persistentes: con HTTP 1.0, cada petición se realizaba bajo una
conexión establecida con tal propósito. Una vez recibida la respuesta, la
conexión se cerraba. HTTP 1.1 permite conexiones persistentes: sobre una
misma conexión TCP se pueden realizar las peticiones HTTP necesarias, con
el consiguiente ahorro de recursos en las máquinas (cliente, servidores y
elementos de red) y la reducción de la latencia y la congestión de red, entre
otras cosas.
Nuevos códigos de respuesta: HTTP 1.0 tenía reservado el rango de los có-
92
digos 1xx, que devuelve el servidor como información al cliente para casos
que pudiesen surgir en un futuro, pero no tenía definido ninguno. HTTP 1.1
introduce dos: 100 Continue y 101 Switching Protocols.
93
Contenido Tipo MIME (tipo/subtipo) Descripción
text/plain Sólo texto
Texto
text/html Lenguaje de marcado hi-
pertexto para páginas web
image/gif Formato de intercambio de
Imagenes
gráficos gif
image/jpeg Imágenes codificadas se-
gún el estándar propues-
to por el Joint Photographic
Experts Groups
Audio audio/mpeg3 Audio en formato MP3 se-
gún el estándar MPEG-1
Vídeo video/mpeg Vídeo en el estándar de co-
dificación del Moving Pictu-
re Experts Group
Datos application/octet-stream Secuencia binaria genéri-
ca
la versión 1.1 del protocolo incluye un campo de cabecera que se puede em-
plear para solicitar al servidor un cambio de protocolo a nivel de aplicación
(nunca a nivel de transporte) en la conexión que se está empleando. Por tan-
to, no se asegura que el servidor acepte el cambio, ni el cambio se hará en
otra de las conexiones que pueda tener en paralelo el cliente con el servidor,
ni mucho menos será posible cambiar, por ejemplo, de TCP a UDP. Sí será
94
posible, y de hecho son las aplicaciones que tiene esta cabecera, securizar
conexiónes HTTP pasándolas a HTTPS (HTTP sobre TLS) o de HTTP a Web-
Socket (que se verá en profundidad en el último tema). La existencia de esta
cabecera obliga a que la cabecera Connection también esté presente y que
su valor sea Upgrade.
95
Sirva de ejemplo de petición la siguiente:
username=arturo&password=nodejskoans
En la parte del servidor, las respuestas que éste envía al cliente como resultado de
una petición, como sucede con las peticiones, también son líneas de texto claro
que tienen una estructura determinada. La primera línea, status-line, contiene el
código de respuesta, status-code, en una estructura como la que sigue:
100 Continue
sirve para informar al cliente de que el servidor acepta el tipo de peti-
ción en base a las cabeceras que le está enviando y puede continuar
transmitiendo el cuerpo del mensaje. Es útil, por ejemplo, en el caso
de que el cliente tenga que asegurarse de que el servidor va a aceptar
datos que suponen un uso importante de ancho de banda o consumen
96
mucho tiempo en su transmisión. En este caso, el cliente empleará una
cabecera Expect: 100-continue
2xx: éxito
La petición fue recibida y procesada. Todo ha ido como se esperaba.
El código más típico de esta categoría es 200 OK, que se obtiene cuando todo
ha sido satisfactorio y se devuelve el recurso resultante del método. Si es éste
es GET, se devuelve el recurso y, si es POST, el resultado de la acción.
3xx: redirección
La petición no se ha completado y se deben realizar más acciones para ha-
cerlo. Generalmente indican un cambio de localización del recurso. La nueva
localización se indica en una cabecera para ese propósito: Location.
Si por el contrario el cambio es temporal existen los códigos 302 Found, 303
See Other (sólo HTTP/1.1) y 307 Temporary Redirect (sólo HTTP/1.1),
con los que las siguientes peticiones deben seguir haciéndose a la URI origi-
nal de la petición o a la que indica Location.
97
Sin embargo, el motivo más genérico de error es el 400 Bad Request que
indica que la sintaxis de la petición HTTP es errónea. El Agente de Usuario
debería revisar que se hayan formado bien la request-line y las cabeceras, y
que no contengan errores en sus nombres o valores.
Otros motivos de error pueden estar relacionados con los permisos de acceso
al recurso. Si el recurso está protegido y el usuario no presenta credenciales
o éstas son erróneas, se obtendrá un error 401 Unauthorized y se pedi-
rán las credenciales incluyendo obligatoriamente en la respuesta la cabece-
ra WWW-Authenticate. Un Agente de Usuario se acredita mediante el campo
Authorization en las cabeceras de la petición. El proceso de Autenticación
y Autorización está descrito con detalle en la RFC2617: HTTP Authentication:
Basic and Digest Access Authentication.
98
obtener un 404 Not Found.
Una cabecera obligatoria, que el servidor debe incluir siempre (con la excepción
de los códigos 1xx principalmente), es Date y estampa la fecha de la creación de
la respuesta por parte del servidor. Su principal utilidad es la gestión por parte
de la chaché de las entidades que se transmiten en el mensaje.
Otra de las cabeceras es Set-Cookie. Con ella el servidor inicia el mecanismo que
tiene HTTP para almacenar en la parte cliente pequeñas piezas de información
referentes al estado de la comunicación HTTP. Estas piezas de información se
conocen con el nombre de cookies y contienen pares <clave>=<valor>;. Cada
cookie se fija con una cabecera Set-Cookie, que además de la propia cookie
incluye la fecha de expiración de la misma, por lo que puede haber varias de éstas
en una respuesta. Cuando un Agente de Usuario recibe esas cabeceras, almacena
las cookies y está obligado a incluirlas (si no han expirado) en las siguientes
peticiones al dominio, mediante la cabecera de petición Cookie.
Como parte del mecanismo de Upgrade, el servidor también puede incluir la cabe-
cera homónima Upgrade en la respuesta de información de cambio de protocolo
(101 Switching Protocol). Esta cabecera contendrá la pila de protocolos que se
va a empezar a usar separándolos por comas. El primero de ellos se corresponde
99
con el que se usará en el nivel más bajo de la pila.
Content-Encoding
indica, cuando aparece, la codificación adicional a la que se ha sometido el
contenido y por tanto, cómo debe el Agente de Usuario decodificarlo. Habi-
tualmente se trata de compresión con distintos algoritmos (zip, lempel-ziv-
welch...) que se indican con los valores gzip, compress, deflate...
Content-Length
el tamaño en octetos del recurso que se envía. Si se le han aplicado codi-
ficaciones adicionales (indicadas con la cabecera anterior), el tamaño debe
calcularse después de aplicarlas.
Content-Type
expresa el formato MIME de la entidad que se envía de la misma manera en
que MIME definió la cabecera del mismo nombre en la RFC1341.
HTTP/1.1 200 OK
Content-Encoding: deflate
Content-Type: application/octect-stream
Date: Mon, 04 Mar 2013 17:23:34 GMT
Se tienen disponibles las clases que modelan un escenario HTTP básico impor-
tando el módulo http de la manera habitual:
100
El módulo comprende ambos extremos de una comunicación HTTP además de
los mensajes que se intercambian las partes. El extremo servidor ofrece la clase
http.Server
El servidor HTTP que ofrece este módulo se puede considerar como un Servidor
TCP con nuevas funcionalidades o características ya que realmente hereda de
la clase net.Server 2 . Consecuentemente, se puede atender a los eventos de ésta
y utilizar sus métodos, incluido listen(), para empezar a escuchar conexiones
entrantes, y close() para dejar de escucharlas, que son los más relevantes. Todo
ello está documentado detalladamente en el capítulo anterior.
El resto de las novedades que introduce el Servidor HTTP son todo eventos, prin-
cipalmente referidos al ciclo de vida de las peticiones:
101
cuerpo del mensaje si procede. Ambos mensajes son instancias de las clases
ServerRequest y ServerResponse respectivamente, que se tratan más ade-
lante, y que básicamente son un modelo de las peticiones y la respuesta tal
y como las definen las RFCs relacionadas con HTTP.
El objeto response se genera una vez se han procesado las cabeceras HTTP
de la petición4 . Ambos, request y response, se entregan a la aplicación
mediante el evento ’request’ que se encarga de emitir el Servidor.
3
http.js: https://github.com/joyent/node/blob/v0.8.20-release/lib/http.js#L1748
4
http.js: https://github.com/joyent/node/blob/v0.8.20-release/lib/http.js#L1868
102
Como cuando el cliente pide Upgrade es para cambiar el protocolo de aplicación
que se está empleando en la conexión, Node pone a disposición del listener el
socket entre cliente y servidor, con el que se puede trabajar directamente, y un
Buffer, head, que puede contener el primer paquete que el cliente envía con el
nuevo protocolo. Este paquete encapsula cabeceras y datos y es deber del pro-
gramador interpretarlo correctamente para identificar su contenido. El resto de
paquetes se reciben a través del socket de la manera habitual en que se reciben
datos en un Stream: atendiendo al evento ’data’:
Se debe observar que si se desea envíar una respuesta HTTP, ésta no se recibe
como argumento en los listeners sino que debe ser escrita directamente en el
socket.
5.2.1. ServerRequest
El API de Node advierte que es una clase que no se puede instanciar directamente.
Y en efecto, no se puede instanciar con new ya que, en detalle, los objetos de esta
clase los crea node_http_parser bajo demanda5 .
103
request.connection, y siguiendo por la imprescindible request-line:
Si la petición contiene cuerpo de mensaje, éste será accesible gracias a que la pe-
tición es un Stream y los datos que se envían a través del socket pueden recibirse
atendiendo al evento ’data’ de la manera habitual:
5.2.2. ServerResponse
104
te y será puesta a disposición del programador a través de uno de los múltiples
eventos de un servidor http.Server. ServerResponse implementa un Stream de
sólo escritura, como es lógico para configurar una respuesta adecuada que enviar
al cliente.
Con este objetivo se ofrecen facilidades, para generar desde las cabeceras hasta el
cuerpo, con soporte para el mecanismo de Continue gracias al método
response.writeContinue(), que símplemente envía el mensaje HTTP/1.1 100
Continue al cliente.
105
serverResponse.write()
Con Node se pueden realizar peticiones HTTP a un servidor a través del método
http.request(opciones, function(){})
No es necesario atender a este evento para continuar la petición, sino que simple-
mente es una notificación más de las fases por las que va pasando la comunica-
ción.
106
Una vez preparado, no sería necesario escribir la cabecera completa en el socket,
ya que la request-line y algunas cabeceras de la petición se crean automáticamen-
te cuando se invoca a http.request(), empleando valores de las opciones que se le
pasan. En este caso, la request-line se configura: fijando el método de la petición
se a través de opciones.method, por defecto ’GET’, obteniendo la url del recur-
so, incluidas queries si procediese, de opciones.path, por defecto ‘/’ empleando
siempre la versión del protocolo 1.17 .
La cabecera completa se queda en cola de salida para ser enviada cuando co-
mience a generarse el cuerpo de la petición, si lo hubiera. Mientras ésta no
se haya enviado, las cabeceras pueden modificarse con los métodos accesores
getHeader(nombre), setHeader(nombre, valor) y removeHeader(nombre).
En caso de que haya una entidad que precise enviarse, se hará con el método que
el interfaz Stream define: clientRequest.write(datos, [codificacion]).
107
Continue, Upgrade o Connect, como se ha visto que suele hacerse a lo largo de
toda la librería.
108
cómo manejarlo. Aún así, este caso es extraño y tampoco debería producirse en
una comunicación normal.
5.2.4. ClientResponse
Si la respuesta incluye una entidad, ésta estará disponible conforme se vaya re-
cibiendo, de la manera en que se leen los datos en un Stream: con el evento
’data’.
109
5.3. Aplicación con HTTP
En esta práctica se dotará al servidor RTP de una interfaz web que controle la lis-
ta de reproducción de una IP de broadcast concreta, protegida por contraseña. Se
presentará en una página unos sencillos botones de reproducción, pausa y ade-
lante/atrás junto con una lista de las canciones disponibles. Los efectos de estas
acciones sobre la lista de canciones se notarán en todos los clientes que pertenez-
can al grupo de broadcast, con lo que puede usarse como panel de administración
de la difusión del contenido.
110
Figura 5.1: Interfaz de la Solución HTTP
111
5.3.2. Diseño propuesto
112
La interfaz del Reproductor es sencilla, posee un método por cada acción que se
quiera realizar sobre él (play, pause. . . ). Los métodos admiten una función de
callback que se ejecutará cuando se haya terminado de realizar la acción, de esta
forma, por ejemplo, se podrá enviar la respuesta al cliente con la seguridad de
que la acción que ha solicitado se ha llevado a cabo.
Como ListHttpServer sólo tiene como propósito procesar peticiones web con los
métodos habituales, GET y POST, se atenderá exclusivamente al evento
’request’:
El código asociado al evento será el que procese el tipo de petición. Los requisitos
que se exigen a esta aplicación implican solamente a dos de ellos:
var allowedList = {
"224.0.0.114": "password"
}
113
El tipo de autenticación a utilizar es la más sencilla: Basic. Cada vez que se re-
ciba una petición, se comprobará pues que existe la cabecera Authorization y
que las credenciales que se presentan en ella son las correctas. En caso de no ser-
lo se devolvería el código de estado adecuado para este caso: 401 Unauthorized.
if (!credentials
|| !(broadcastIp in allowedList
&& broadcastIp in this.broadcastList)
|| allowedList[broadcastIp] != pass) {
res.writeHead(401, "Unauthorized", {
"WWW-Authenticate": ’Basic realm="List Server Authentication"’
});
res.end();
return;
}
114
Esta página debe ser generada dinámicamente utilizando el estado del re-
productor (reproduciendo una pista concreta o pausado) en el momento de
la petición. Se empleará un método privado del módulo, writeDocument(),
creado para tal efecto.
algún fichero estático, como imágenes u hojas de estilo que apliquen un di-
seño sobre la página principal. Estas entidades se solicitan con una petición
de tipo GET y se diferencian de las anteriores peticiones en que el path es
distinto de ’/’. La ruta en el sistema de ficheros es relativa al directorio donde
reside el script ejecutable del servidor. Se debe tener muy en consideración
que no se realiza ningún tipo de control sobre la ruta que se recibe en las
peticiones por lo que la aplicación es totalmente vulnerable a ataques de
directorio transversal.
115
return;
}
var fileExtension = path.substr(path.lastIndexOf(".") + 1);
var mimeType = MIME_TYPES[fileExtension];
res.setHeader("Content-Type", mimeType);
if (mimeType.indexOf("text/") >= 0){
res.setHeader("Content-Encoding", "utf-8");
}
res.setHeader("Content-Length", data.length);
res.write(data, "binary");
res.end();
});
que se realice una acción sobre el reproductor. Proviene del formulario que
presenta la interfaz html:
así que se espera una petición de tipo POST cuyo contenido es un único cam-
po action. El valor de action, además de indicar qué acción tomar, coincide
con el nombre del método a invocar del reproductor que se controla. Con
esta convención, se pretende simplificar la manera de llamar al método, de
tal forma que sólo sea cuestión de:
if (action in player) {
player[action](function(){
writeDocument(res, {group:broadcastIp,
paused:player.paused,
tracks:player.list(),
currentTrack:player.currentTrack()});
});
}
116
en el servidor porque su tamaño es mínimo. Aún así, no debe darse la petición
por recibida hasta que el cliente no la finalice. Por tanto se deben atender dos
eventos:
req.on(’end’, function(){
var action = querystring.parse(body).action;
var player = self.broadcastList[broadcastIp];
if (action in player) {
player[action](function(){
writeDocument(res, player)
})
}
})
Como resultado se espera la misma página web que se genera cuando se accede
al reproductor, pero actualizada con el estado en que se encuentre el reproductor
(parado, en el siguiente track. . . ) después de la acción. A tal efecto será útil de
nuevo la función writeDocument() que se invocará en el callback que se pasa
como argumento al Reproductor, puesto que la página html se debe enviar una
vez ejecutada la acción.
117
características más avanzadas de HTTP. Alguno de ellos se tratará en el tema
siguiente. Por lo pronto, se analizarán los siete Koans del tema.
3. Conocer el módulo url y ser capaz de diseccionar las URLs que las peticiones
contienen en la request-line con parse()
switch(req.___){
case ’GET’:
break;
case ’POST’:
break;
}
res.___("Content-Type", "text/css");
118
5.5. Preparación del entorno y ejecución de los Koans
Para verificar que se han realizado con éxito los koans se ejecutan los casos de
prueba con:
Como resultado de una correcta resolución de los mismos, la práctica será com-
pletamente funcional y se podrá ejecutar con el comando:
$ node app.js
Como en todas las prácticas que incluyen al protocolo RTP para generar contenido
de audio en streaming, se debe configurar un Reproductor tal y como se indicó en
el apartado “Descripción de la aplicación” del capítulo “Módulos dgram y buffer”.
Puesto que el objetivo de la práctica es la gestión del audio que se emite en un
grupo concreto multicast, hay que volver a fijar la dirección IP a la del grupo que se
ha tomado como ejemplo en la práctica: el 224.0.0.114 con puerto 5002. Huelga
volver a describir los pasos a seguir para hacerlo.
5.6. Conclusión
En este capítulo se ha fijado el manejo a través del módulo http de las caracterís-
ticas de más bajo nivel del protocolo HTTP: recepción de peticiones atendiendo al
evento ’request’ identificando en ellas los parámetros imprescindibles como el
método empleado method o sus cabeceras headers. Con toda esta información se
119
ha podido ver cómo generar una respuesta adecuada, por ejemplo, estableciendo
también unas cabeceras concretas con setHeader().
120
Capítulo 6
Express
6.1. Connect
121
Hasta la fecha, en su versión 2.7, Connect posee más de 20 middlewares que
ofrecen una variada funcionalidad:
logger
Trazas de log, por defecto a la salida estándar pero redirigible a cualquier
Stream. La información a registrar se ofrece a través de tokens a los cuales
puede dárseles un formato de presentación. Tanto los tokens como el forma-
to, aparte de haber algunos predefinidos, pueden ser personalizados por el
desarrollador.
csrf
Prevención del ataque Cross-Site Request Forguery [51].
Mediante este ataque, una página maliciosa puede enviar una petición a un
servidor vulnerable si conoce la URL y los parámetros de la query con los que
realizar la petición. Normalmente la URL se inserta en la página maliciosa
como un recurso, de tal manera que será el navegado el que la solicite sin
intervención del usuario, por ejemplo:
<img src="http://bank.com/transfer?amount=1000&to=Malicious"/>
compress
122
Compresión gzip y deflate de la entidad que se transmite en la respues-
ta, a menos que se el campo Content-Encoding se haya definido como
identity (sin compresión). Modificará o añadirá la cabecera
Content-Encoding.
basicAuth
Soporte para la autenticación HTTP básica.
bodyParser
Procesa el cuerpo de las peticiones con soporte para los tipos MIME
application/json, application/x-www-form-urlencoded y
multipart/form-data.
json
Procesa el cuerpo de una petición web en formato JSON, definido por su
Content-Type: application/json, haciéndolo disponible como un objeto
en la propiedad req.body.
urlencoded
Procesa una petición del tipo application/x-www-form-urlencoded gene-
rando el objeto req.body en la petición.
Este es el tipo MIME que se aplica por defecto a los datos que se envían a
través de un formulario HTML, mediante peticiones tanto GET como POST.
A estos datos se les aplican unas reglas de codificación especificadas en el
estándar del W3C que son las que definen la estructura de una query, ya
conocidas, como que los espacios se sustituyen por ’+’, los caracteres no
alfanuméricos por su código ascii precedido de ’ %’ de tal manera que tienen
la forma %HH, o que los pares nombre/valor se separan con ’&’ de otros
pares y con ’=’ entre ellos [52, Section 17].
123
multipart
Procesa los cuerpos de las peticiones multiparte, de tipo MIME
multipart/form-data.
Este tipo se aplica a los datos que se envían cuando el tamaño de éstos es de-
masiado grande como para enviarlo como
application/x-www-form-urlencoded. Esto sucede, por ejemplo, con ar-
chivos binarios donde para codificar cada octeto, hacen falta tres caracteres
( %HH), lo cual triplica la cantidad de datos a enviar siendo altamente inefi-
ciente.
En los mensajes multiparte, como su nombre indica, hay varias partes se-
paradas con un limitador (una cadena de caracteres tal que no se encuen-
tre en el cuerpo de ninguna de las partes que se transmite). Cada una de
las partes tiene su juego de cabeceras, como su propio Content-Type o su
Content-Transfer-Encoding. Destaca sobre ellas la cabecera
Content-Disposition que, aparte de tener valor fijo (form-data) incluye
el atributo name que da nombre a la parte [53].
cookieParser
Procesa el campo Cookie de la cabecera HTTP. De esta manera, podemos ac-
ceder a las cookies a través de las propiedades req.cookies o
req.signedCookies que el middleware incluye en la petición req. Estas
propiedades son un objeto cuyas claves son el nombre de la cookie y sus
valores, el contenido de las mismas.
session
Implementa un completo sistema de gestión de sesiones las cuales residiran
en memoria, por tanto, como se advirte, no apto para usar en producción.
124
su interacción con ella. Ya que, como se recordará, HTTP es un protocolo sin
estado y, por tanto, incapaz de cumplir esta función.
cookieSession
Habilita el mecanismo de sesiones pero guardando la información en la mis-
ma cookie en lugar de la memoria del servidor [55].
methodOverride
Sobreescribe el método HTTP, accesible por req.method, de las peticio-
nes que tengan presente el parámetro _method en la query o la cabecera
X-Http-Method-Override. Esta cabecera es la manera que tienen los clien-
tes de un servicio REST de realizar peticiones a este servicio sin que sean
rechazadas por un proxy.
responseTime
Añade a la respuesta la cabecera X-Response-Time indicando el tiempo que
125
ha tardado la aplicación en procesar la petición.
static
Aporta la capacidad de servir de manera eficiente fichero estáticos, como
imágenes u hojas de estilo, por defecto contenidos en el directorio ./public.
Gestiona automáticamente los tipos MIME de los ficheros y los errores que
puedan surgir empleando los códigos de respuesta apropiados.
staticCache
Habilita una pequeña cache de archivos servidos por el middleware static.
En total, mantiene en memoria los 128 archivos menores de 256Kb más
solicitados al servidor, gestionando las cabeceras HTTP relacionadas con la
caché. Ambas cifras son por defecto y pueden ser cambiadas a través de las
opciones de staticCache.
directory
Genera un listado de los directorios y archivos contenidos en un directorio
raíz dado, y lo envía en html, JSON o texto plano, según acepte el agente de
usuario.
vhost
Facilita la creación de hosts virtuales basados en nombre [56].
En este caso, los hosts virtuales son aplicaciones web que corren en una
misma máquina con una sola dirección IP. Se accede a cada host virtual
creando un alias (CNAME) en el DNS apuntando a la máquina. El nombre
del host es una de las cabeceras HTTP (Host) y es en lo que se basa este
middleware para dirigir la petición.
favicon
El favicon es un icono que los navegadores solicitan al servidor de la página
o aplicación web para identificarla en la barra de direcciones, pestaña del
navegador y marcadores.
Este middleware ofrece el icono que está en el path que se pasa por paráme-
tro. Si no se especifica, muestra uno por defecto.
126
port/favicon.ico automáticamente. Sin el middleware la petición llega al
controlador de las rutas que puede direccionar mal la petición o no saber
cómo manejarla, produciendo con toda probabilidad un error.
limit
Establece un límite para el cuerpo de las peticiones. El límite se pasa por pa-
rámetro en cifra o como una cadena de caracteres, soportando expresiones
en kb, mb o gb, por ejemplo, ‘1.3mb’.
query
Únicamente hace disponible el objeto req.query como propiedad de la peti-
ción req. Hace uso de la librería url del core de Node y del módulo qs para
procesar la query.
errorHandler
Adapta las trazas generadas por un Error al tipo MIME que el cliente solicite:
html, json y text (por defecto).
6.2. Express
Construido encima de Connect, establece una fina capa de software que enrique-
ce de manera sencilla, elegante y transparente las características que exponía
el módulo HTTP, abstrayendo de él detalles muy concretos, simplificándolos y
ampliando el rango de características manejables del protocolo HTTP al desarro-
llador.
1
http://expressjs.com/
127
Para comenzar a usar Express, como todos los módulos de Node, debe solicitarse
con
que importa el objeto factoría para crear aplicaciones Express, cuyo uso difiere
ligeramente del que se ha visto anteriormente parar require (situar bajo un na-
mespace las clases y funciones del módulo). Ahora, invocando esta función se
genera directamente una aplicación web:
Las variables que pueden modificarse con estos métodos corresponden a distintas
características de la aplicación que se irán desgranando posteriormente. De una
manera rápida, son las siguientes:
128
Ámbito Variables
jsonp callback name
Generación de respuestas en notación JSON json replacer
json spaces
case sensitive routing
Rutas de las peticiones según su URL
strict routing
view cache
Sistema de generación de la parte de interfaz (vistas) view engine
views
app.use(express.favicon());
app.use(express.static(__dirname + ’/public’));
app.use(express.cookieParser());
• views es la ruta del directorio donde se alojan las plantillas que contie-
nen el código de la vista. Por defecto es ’/views’
129
se declaran las variables locales, que son aquellas comunes a todas las plan-
tillas de la aplicación
Por último, para tener la aplicación perfectamente funcional, deben definirse las
rutas a las que un cliente puede acceder, con qué métodos HTTP puede hacerlo
y el comportamiento que se espera de la aplicación. Para ello Express ofrece su
sólido sistema de enrutamiento en el que cada método HTTP es una función de la
aplicación cuyo primer argumento es la ruta sobre la que se realiza la petición (sin
tener en cuenta la parte de la query). Esto significa que en la práctica existirán
las funciones app.get(ruta, [callbacks...], callback), app.post(ruta,
[callbacks...], callback), app.put(ruta, [callbacks...], callback),
app.delete(ruta, [callbacks...], callback), etc...
Si una ruta admite todos los métodos entonces puede abreviarse con app.all().
La potencia de Express en este ámbito radica en que las rutas pueden ser ex-
presiones regulares. De hecho, una ruta identificada por una String se compila
a una expresión regular. Además, se pueden parametrizar mediante tokens. Los
2
http://jade-lang.com/
130
tokens representarán partes variables dentro de la estructura concreta de una
URL. Con esto se da flexibilidad a la definición de la ruta y además se hace dis-
ponible esa parte a la lógica de la aplicación a través de la propiedad params del
objeto Request que modela la petición. Un token se define con un identificador
que comienza con dos puntos ’:’. Cuando llega una petición, Express analiza
la URL y extrae de ella el valor del o los tokens. Ejemplo de ruta parametrizada
puede ser:
/users/:uid/friends/:friendname
/users/34123/friends/arturo o /users/87345/friends/irene.
Una vez definidas las rutas, éstas quedan reflejadas en el objeto app.routes
desde donde se pueden consultar.
La función de callback que atiende las peticiones es similar a los listeners del
módulo http: admite como argumentos la petición y la respuesta de la transacción.
Express modela ambas como los objetos Request y Response que son una versión
enriquecida de los objetos ServerRequest y ServerResponse del módulo http de
Node, es decir, las propiedades de ambos serán las que tienen ambas clases más
las que el distinto middleware que se ha configurado con Connect y el propio
Express hayan añadido.
6.2.1. Request
Request es el modelo que Express proporciona para las peticiones HTTP. A través
de él, el desarrollador tiene acceso de una manera sencilla a gran variedad de
información que el cliente envía en las cabeceras, sin tener que incluir lógica
adicional para procesarlas.Request proporciona soporte para las siguientes:
Accept
Express disecciona los tipos que el cliente acepta en la petición y construye
req.accepted, un array donde están todos ordenados según prioridad del
cliente. El desarrollador puede comprobar si el cliente acepta unos tipos de-
terminados (separados por comas) con req.accepts(tipos), que devuelve
el tipo mejor soportado de todos.
Accept-Charset
131
Express disecciona esta cabecera y genera el array req.acceptedCharset
con los juegos de caracteres que admite el cliente ordenados por preferencia.
También ofrece la facilidad req.acceptsCharset(charset) para determi-
nar si el cliente aceptaría un charset concreto.
Accept-Language
al igual que en los dos casos anteriores, Express disecciona esta cabece-
ra para obtener un array ordenado según las preferencias del cliente con los
idiomas preferidos de éste. De manera análoga, con
req.acceptsLanguage(idioma) se puede consultar si el cliente acepta un
determinado idioma.
Content-Type
de existir esta cabecera, con req.is(tipo) se puede comprobar si el cuerpo
de la petición es ese tipo.
Cookie
a través del middleware cookieParser() de Connect, Express disecciona
las cookies de la petición y las almacena en req.cookies y en
req.signedCookies en caso de que sean seguras.
Host
la información que Express extrae de esta cabecera es el nombre del host
req.host al que se dirige la petición, sin especificar el puerto en caso de ser
éste distinto de 80. Además, todos los subdominios que haya en el Host se
meten ordenados en req.subdomains
Last-Modified y ETag
el cliente emplea estas cabeceras en peticiones que impliquen entidades que
tenga cacheadas, llamadas cache-conditional requests. La RFC2616 [49, Sec-
tion 13.3.4] establece las pautas que un cliente debe seguir para incluirlas
en una petición. En base a éstas, Express determina si la entidad solici-
tada que el cliente posee es reciente, req.fresh es true, u obsoleta, con
req.stale.
6.2.2. Response
132
Como con el módulo http de Node, se puede establecer el status code de la res-
puesta, esta vez a través del método res.status() en lugar de modificar la pro-
piedad res.statusCode de la clase ServerResponse del módulo http. Hay que
recordar que modificar el status code sólo podía hacerse en los casos en que la
respuesta tenía cabeceras implícitas, es decir, se había comenzado a generar la
respuesta sin establecer explícitamente unas cabeceras. Además en ese caso y en
este, sólo podía modificarse el status code si la respuesta no se había comenzado
a enviar aún. Es por ello que el res.status() que ofrece Express es encadenable:
a continuación de él se pueden invocar el resto de métodos de res.
Content-Type
Express establece muchas veces el tipo MIME del contenido que se envía de
manera automática pero el criterio del desarrollador prevalece si se emplea
la función res.type(tipo). Además, si la clase del tipo es texto (text/*),
el juego de caracteres se puede especificar con la propiedad res.charset.
Cookie
se puede enviar una cookie al cliente con res.cookie(nombre, valor,
[opciones])
A la hora de enviar el cuerpo con la entidad solicitada, Express ofrece varias for-
mas de hacerlo dependiendo de cómo se esté generando la respuesta. La manera
más general es con res.send([statusCode], cuerpoRespuesta) que enviará
respuestas que no sean de tipo Stream, es decir, ya están calculadas y se pueden
enviar inmediatamente, por ejemplo, un objeto JSON o un Buffer. No entran en
esta categoría archivos o páginas dinámicas.
133
na un método específico para ello: res.sendfile(ruta, [opciones],
[function(error){}]). Este método envía el archivo ubicado en la ruta del
sistema de ficheros (o relativa a opciones.root, si se especifica) fijando automá-
ticamente el tipo según la extensión del mismo y su tiempo de caché si se indica
en opciones.maxAge.
Para el caso de archivos html dinámicos, que necesitan ser procesados por algún
motor de renderizado existe el método res.render(vista, [locales],
function(error, html){}), al que se le indica como primer argumento el nom-
bre de la vista.
6.3. MongoDB
Sin embargo, el modelo de datos de NoSQL es mucho más flexible, existiendo va-
rias formas de organizar la información: clave-valor, clave-documento o BigTable,
134
por citar algunas. Esta flexibilidad se ve reflejada también en la manera de reali-
zar las consultas: ahora, en lugar de usar un lenguaje declarativo, perfectamente
preciso, como SQL, se deja a la lógica de la aplicación realizar las operaciones
sobre los datos directamente, con la intención de tener un mayor control sobre el
rendimiento de la consulta.
Decantarse por el uso de NoSQL frente a SQL es una cuestión de análisis del pro-
blema: en general, se recomienda usar una base de datos no relacional cuando
se tienen grandes cantidades de datos y/o se encuentran dificultades para mode-
larlos según un esquema. De otra manera, SQL es una tecnología muy madura
con muchísimos años de investigación y desarrollo sobre ella [58].
Los autores del proyecto definen MongoDB como si fuera una base de datos re-
lacional a la que únicamente se le ha cambiado el modelo de datos manteniendo
otras características que tradicionalmente han funcionado muy bien, como índi-
ces o updates. Los puntos fuertes que destacan de MongoDB, según los autores
[60], son:
135
secundarios, consultas dinámicas, ordenación o operaciones “upsert” (upda-
te si el documento existe, insert si no)
escalabilidad horizontal, es decir, posee mecanismos para que sea muy fácil
y seguro, sin interrupciones de servicio, añadir más máquinas corriendo
MongoDB al cluster
Usar Mongoose en una aplicación web es bastante fácil y sólo requiere un par de
pasos:
mongoose.connect(IPServirdor, nombreBaseDeDatos);
mongoose.on(’error’, function(error){});
3
http://www.craiglist.org
4
https://foursquare.com
5
http://mongoosejs.com/
136
A partir de este punto, si no se ha obtenido ningún error, el desarrollador será
capaz de crear objetos a partir de un modelo. Este modelo sigue un esquema
con el que se guardarán sus instancias en la base de datos. Un esquema puede
verse como la definición de una colección ya que define la forma que tendrán los
documentos de dicha colección.
Por tanto, el último paso antes de realizar operaciones con la base de datos
es:
Por supuesto, este patrón de código se utilizará tantas veces como sea necesario
para hacer uso de todos los modelos que se hayan estimado necesarios en la
arquitectura de la aplicación.
La instancia creada posee métodos que interaccionan con MongoDB. Los más
comunes son:
miModelo.save([function(error, miModelo){}])
con el que se guardará el documento miModelo en la base de datos, tanto
como si es de reciente creación como si se está realizando una actualización
(update). Emplear este método es más usual si previamente se ha realizado
una búsqueda o se acaba de instaciar el objeto, ya que en caso de querer
actualizarlo se recomienda utilizar el método MiModelo.update() sobre el
modelo del documento, no sobre la instancia en sí.
137
miModelo.remove([function(error, miModelo){}])
con el que se borra el documento de la base de datos. Aún resultando con
éxito, el callback opcional recibe una copia del objeto recién eliminado como
argumento.
MiModelo.find(query,
[condiciones],
[opciones],
[function(err, resultSet){}])
MiModelo.findOne(query,
[campos],
[opciones],
[function(err, resultSet){}])
El argumento query contiene las condiciones que deben cumplir los documentos
para formar parte del resultado de la búsqueda. En concreto se refieren a valores
de uno o varios campos de éste.
El argumento campos es una String opcional con el nombre de los campos del
documento, separados por un espacio, que se quieren obtener si es lo que se
desea en lugar de obtener el documento completo.
UserModel.find(
{username : /^a\w+/, age: {$gt: 30}, accountType: ’premium’},
’username email’,
function(error, resultSet){}
);
138
que devolverá como resultado un conjunto resultSet de nombres de usuario y
su email asociado de cuentas ’premium’ de usuarios cuyo nombre comience por
la letra ’a’ y tengan edades superiores a 30 años.
UserModel.find(
{username : /^a\w+/, accountType: ’premium’},
’username email’)
.where(’age’)
.gt(30);
Como paso previo, debe tenerse una instancia de MongoDB corriendo en la má-
quina. En ella se creará la base de datos, a la que se llamará whizr, que empleará
la aplicación. Para generarla, basta con conectarse con el cliente mongo a Mon-
goDB e introducir a través del prompt el comando use whizr.
139
Aunque no sería necesario, ya que MongoDB lo haría implícitamente, se crearán
las collecciones que se usarán a lo largo del desarrollo. Una contendrá los usua-
rios, los whizrs, y otra las publicaciones, los whizs. Los dos comandos necesarios
para ello son:
db.createCollection("whizrs");
db.createCollection("whizs");
El primer objetivo que se debe alcanzar a la hora de crear una red social es aquel
que permite la propia existencia de la red: la posibilidad para un usuario de darse
de alta y acceder a ella.
Por tanto, lo que se pretende alcanzar con este caso de uso es que el usuario:
140
tenga la posibilidad de terminar su sesión, una vez haya accedido a su perfil
tres endpoints que atiendan las peticiones de crear una cuenta y hacer login
y logout. En este caso determinados por las acciones ’/register’, ’login’
y ’/logout’
Hay que tener en cuenta que el acceso a los endpoints, éstos descritos y los futu-
ros, debe ser restringidos en función de qué derechos posea el usuario sobre los
mismos, es decir, qué acciones puede o no realizar en función de si está logueado
o no o si es el propietario de la cuenta. Para ello se necesita hacer uso de un
mecanismo de control de sesiones (creación, mantenimiento y destrucción) y la
lógica necesaria para proteger dichos endpoints en base a ese mecanismo.
Por último, se debe articular un modelo de datos que permita manejar los usua-
rios en este sencillo caso de uso.
Las opciones del comando express son limitadas pero suficientes para conseguir
el máximo grado de funcionalidad en una aplicación web. Para esta práctica se-
rían útiles las opciones -e, que añade soporte para el motor de renderizado ejs, y
-s, que añade soporte para sesiones.
141
Figura 6.2: Scaffolding creado por Express
Como es habitual en las aplicaciones para Node, se empieza importando los mó-
dulos que se van a emplear y, como paso siguiente, se crea una instancia del
servidor Express:
142
a priori puede no ocurrírse la necesidad, pero los navegadores actuales so-
licitan el icono de favoritos, favicon.ico, a la aplicación lo que interferirá
sin duda al controlador de las rutas y con mucha probabilidad causar un
error, así que se incluirá esta capa en la pila middleware
app.configure(function(){
app.use(express.favicon());
app.use(express.static(__dirname + ’/public’));
app.use(express.cookieParser());
app.use(express.session({ secret: "Node.js koans" }));
app.use(express.bodyParser());
});
El siguiente paso ahora es definir las rutas y las acciones a realizar cuando se
invoquen pero antes, y puesto que las acciones involucran trabajar con el modelo
de datos de los usuarios de la aplicación, debe definirse este modelo. Se hará en
un módulo aparte que se importará en el código principal de la aplicación, sin
capas intermedias que abstraigan la base de datos por dos pequeños motivos:
uno, mantener el foco sobre Node y, dos, para no introducir más elementos en la
arquitectura de una aplicación que a priori se antoja muy sencilla.
Los campos que componen el modelo son los que se aceptan en el formulario
de registro más uno adicional que contiene el hash MD5 de la dirección de correo
normalizada en minúsculas. El motivo es meramente estético: ese hash sirve para
solicitar al API del servicio de fotos de perfil, o avatares, Gravatar6 una imagen
de perfil para el usuario en caso de tener cuenta en dicho servicio. Esta manera
de obtener automáticamente una imagen de perfil introduce una ruta adicional
auxiliar /username/profilepic que atenderá las peticiones de las imágenes,
6
http://www.gravatar.com
143
redirigiéndolas a la URL de Gravatar.
Estos formularios envían los datos con el método POST a los endpoints ’/register’
y ’/login’. Ambos aceptan parámetros y son rutas de acceso general (no restrin-
gido únicamente a usuarios logueados en el sistema) y se encargan de comprobar
que los campos cumplen las características que se les exigen redirigiendo al usua-
rio donde proceda: en caso de éxito, a la página del perfil de usuario, y en caso
de error, ’/register’ envía un código HTTP 400 Bad Request y ’/login’ un
código HTTP 404 Not Found.
144
if ( username.length <= 0 ||
username.length >= 10 ||
password.length <= 0 ||
password.length >= 10) {
res.redirect(400); // error
}
if (err) {
res.send(’Error’, 500);
}
if (doc != null) {
res.send(’Error’, 404);
}
whizr.save(function(err){
if (err) {
res.send(’Error’, 500);
}
res.redirect(’/’ + username);
});
});
});
145
app.post(’/login’, function(req, res){
// En caso de estar logueados
if (req.session.whizr != undefined) {
res.redirect(’/’ + req.session.whizr.username);
}
models.Whizr.findOne( { username: req.param(’username’) },
function(err, doc){
if (err) {
res.send(’Error’, 500);
}
if ( doc.password == req.param(’password’) ) {
req.session.whizr = doc;
res.redirect(’/’ + doc.username);
} else {
res.send(’Unauthorized’, 401);
};
})
});
Una vez establecida la sesión, se realiza una redirección 302 a una página diná-
mica, servida en la ruta ’/nombreDeUsuario’ y generada con el motor de rende-
rizado ejs. Esta página muestra la información básica de la cuenta del usuario y
ofrece la posibilidad de terminar la sesión con la acción logout.
146
req, y respuesta, res, y también el siguiente middleware o función a ejecutar.
Por tanto, se invocará antes de la función que procesa la petición.
app.register(’.html’, require(’ejs’));
app.set(’view engine’, ’ejs’);
app.set(’view options’, { layout: false });
Esta URL es una ruta parametrizada mediante el token :username que Express
resuelve dinámicamente aplicando la lógica que se ha programado para ella.
147
if (err || doc == null) {
res.send(’Not found’, 404);
};
res.render(’home.html’, { name: doc.name,
username: doc.username,
whizr: req.session.whizr });
});
});
Por último, se habilita la aplicación para que acepte peticiones en el puerto (arbi-
trario) 8080:
app.listen(8080);
148
6.4.1.3. Objetivos de los Koans
La primera parte de los Koans de este capitulo cubre aspectos relacionados con
la configuración y los fundamentos más básicos del desarrollo de una aplicación
con Express.
app.configure(function(){
app.___(express.favicon());
// Resto de configuracion
});
2. Habilitar a la aplicación app para que sea capaz de atender peticiones reali-
zadas con el método HTTP GET en una determinada ruta con app.get():
149
// Codigo con la logica del endpoint
});
El fichero que contiene los koans es express-koans.js, que lo único que expor-
ta es la propia aplicación Express, lista para ponerse a escuchar. De la misma
manera que en prácticas anteriores este fichero debe ser editado y completado,
sin que quede ningún hueco ___.
Para verificar que se han realizado con éxito los koans se ejecutan los casos de
prueba con:
$ node app.js
require(’./express-koans’).listen(8080);
Cabe indicar que es completamente necesario que exista una instancia de Mon-
goDB corriendo en el sistema. Por lo general, en entornos Unix, se puede instalar
como un daemon o servicio que arranca al inicio del sistema. De no ser así, la
base de datos siempre puede levantarse con el comando mongod, en principio sin
parámetros si no se ha tocado la configuración que por defecto ofrece:
$ mongod
150
En caso contrario, lo más común es que la instalación haya alterado la ruta al
directorio donde se almacenan los datos de la base de datos. Por defecto ésta es
/data/db pero podría localizarse en /var/lib/mongodb de tal manera que ahora
el comando sería:
El segundo objetivo que se debe alcanzar a la hora de crear una red social es,
precisamente, el que da sentido al concepto: otorgar a los usuarios la posibilidad
de crear relaciones entre ellos y de generar contenidos.
Esto, básicamente, introduce tres nuevos endpoints que respondan a las acciones
’/whiz’, ’/follow’ y ’/unfollow’. Es obvio que el acceso a estas rutas sólo
debería estar permitido a usuarios autenticados en el sistema, por lo que se hará
uso de los mecanismos ya implementados para protegerlas.
Un formulario sencillo que envíe los whizs al servidor. Bastará con un campo
de longitud de texto máxima de 140 caracteres y un botón para enviar.
151
Dos formularios que constan únicamente de un botón, que envíen al servidor
el nombre de usuario de la persona a la que se desea seguir o dejar de seguir.
Estos formularios deben ser excluyentes, es decir, si no se sigue al usuario,
en su perfil debe aparecer el formulario ’Follow’, y si se sigue, el formulario
’Unfollow’.
Como los whizs aparecen en la página de perfil del usuario, aquella definida por la
ruta ’/nombreDeUsuario’, no es necesario añadir nuevos elementos de presen-
tación html, ni estáticos ni dinámicos. Bastará con modificar los existentes.
En base a esto, los formularios se pueden incluir en la página del perfil que
es, de hecho, el lugar idóneo para hacerlo. Las restricciones que se encuentran
son:
Los datos que envían cada uno de ellos servirán para actualizar la base de datos,
con lo que el modelo de datos de la aplicación deberá ser extendido y actualiza-
do. Se puede introducir el esquema de una nueva entidad que represente a un
Whiz:
Así mismo, se debe reflejar que el usuario posee una lista de los otros usuarios
a los que sigue con lo que se hace necesaria la extensión del esquema Whizr
mediante un nuevo campo, following, donde se enumeren los nombres de usuario
de aquellos Whizrs a los que se sigue:
152
email : String,
emailHash : String,
following : [String]
});
Los nuevos endpoints serán los encargados de actualizar la base de datos según
su cometido. Los resultados de la acción que debe esperar el usuario son:
153
del método que la actualiza:
models.Whizr.update(
{username: whizr },
{ $pull: { following: unfollow } },
null,
function(err, numAffected){
if (!err) {
// actualiza la sesion evitando realizar
// una peticion a la base de datos
var following = req.session.whizr.following;
following.splice(following.indexOf(unfollow), 1);
res.redirect(’/’ + unfollow);
}
});
La segunda parte parte de los Koans de este capitulo está referida a conceptos de
nivel medio y más orientados a la parte de base de datos:
2. Aplicar el filtro middleware anterior a las rutas en que tenga sentido. En este
caso, sobre los endpoints que necesiten autorización para invocarse, como
logout, que se modifica respecto a su versión en el Objetivo#1:
154
3. Actualizar elementos de la base de datos con el método update() que ofre-
cen los modelos definidos con Mongoose:
6.5. Conclusión
155
156
Capítulo 7
Socket.IO
La librería más famosa, estándar de facto en Node, para este tipo de aplicaciones
es Socket.IO. En palabras de sus creadores, “Socket.IO pretende hacer posible las
aplicaciones en tiempo real en cada navegador y dispositivo móvil, difuminando las
diferencias entre los diferentes mecanismos de transporte” [61].
157
Estas tres características son parte del mecanismo del control del estado y re-
cuperación de la conexión para que, en caso de pérdida de la misma, se pueda
re-establecer. Una por una:
158
Chrome 4+, Firefox 3+ y Opera 10.61+ para escritorio, e iPhone Safari, iPad Safari,
Android Webkit y WebOS Webkit para móviles.
Websocket
Bajo el nombre de Websocket se engloba tanto el protocolo Websocket, defi-
nido en la RFC6455 [63], como el API que permite a las páginas web el uso
del mismo [64].
159
tado que permiten manejar un WebSocket desde un agente de usuario. En
concreto ofrece:
Adobe
R Flash
R Socket
La empresa Adobe
,
R con su tecnología Flash
,
R provee un entorno multipla-
160
AJAX Long Polling
Una técnica usada para simular una interacción con el servidor en tiempo
real es AJAX Polling, consistente en abrir una conexión AJAX cada cierto
intervalo de tiempo con objeto de recibir datos del servidor. Si el intervalo es
lo suficientemente pequeño, la sensación es la de comunicación en tiempo
real. Sin embargo, el coste que introduce reducir este intervalo es:
Sin embargo, los únicos navegadores que aceptan esta técnica son los que
están basados en Gecko, el motor de Mozilla, como, por ejemplo, Firefox [66].
Forever Iframe
Es otra de las técnicas que permiten mantener abierta una conexión perma-
nente con el servidor empleando una etiqueta html <iframe> de modo que
se permite embeber una página web dentro de otra. Esta página se envía del
servidor con dos cabeceras concretas:
161
Con esto se consigue que el navegador considere la página como un ente
infinito (de ahí el nombre Forever Iframe) y, como todos los navegadores mo-
dernos, la interpretará según la vaya recibiendo. Conociendo esto, conforme
haya datos disponibles, el servidor enviará etiquetas <script> en las que
se llame a una función del frame padre que será la que reciba y procese la
información del servidor.
JSONP Polling
Esta técnica de Polling utiliza JSONP en lugar de JSON. JSONP significa
JSON with Padding [68]. Se emplea para evitar la limitación impuesta por
los navegadores con su política de “mismo dominio de origen” por la cual
el navegador restringe el intercambio de datos entre una página web y un
servidor sólo si ambos pertenecen al mismo dominio, con objeto de evitar
problemas con código Javascript.
Se solicita al servidor que “recubra” los datos con una llamada a una fun-
ción presente en el espacio de ejecución del navegador capaz de procesarlos
correctamente como la siguiente:
http://dataserver.com/webservice?callback=handleData
Esta URL se emplea como fuente, atributo src, en una etiqueta script de
HTML [69] porque esta etiqueta no posee la restricción que se impone al
resto de peticiones cross-domain.
162
handleData( {datosJSON} )
7.2.1. Servidor
var io = require(’socket.io’).listen(8080);
163
Antes de ello, el cliente debe superar un proceso de autenticación, en la primera
fase del handshake. Este proceso es opcional y configurable por el programador
asignando una función al método authorization():
io.configure(function(){
io.set(’authorization’, function(handshakeData, callback){
// Aqui el algoritmo de autenticacion
callback(null, true);
});
});
io.sockets.on(’connection’, function(socket){})
Es la manera que existe de obtener la referencia a un socket la primera vez
que se conecta. Una vez se tenga, a través del callback, se pueden establecer
en él los eventos que se deseen.
socket.on(’message’, function(message){})
Se emite cuando se recibe un mensaje a través del método del socket cliente
socket.send(). El listener del evento recibe como argumento el mensaje
que envía el servidor.
164
socket.on(’disconnect’, function(){})
Se espera que se emita este evento cuando un socket se desconecta, en
principio porque el cliente desconecta aunque también es posible que se
emita cuando lo haga el servidor.
Esta función recibe como primer argumento los datos que envía el cliente, si
acaso envía, para procesarlos.
Estos métodos, y, por tanto, la información almacenada a través de ellos, sólo son
accesibles desde la parte del servidor, no desde el cliente.
En muchas ocasiones, será necesario o más útil y/o cómodo trabajar con colec-
ciones de sockets agrupados según algún criterio específico. Se ofrece para ello
dos maneras de particionar el conjunto de total: mediante namespaces o mediante
rooms (o salas).
Namespaces
se emplean cuando se quiere que los mensajes y eventos se emitan en un
endpoint en particular que se especifica como parte de la URI empleada por
165
el cliente cuando se conecta, por ejemplo, http://example.com/chat. El
beneficio que se obtiene es el de multiplexar una única conexión, es decir,
en lugar de usar varias conexiones Websocket, se usará la misma.
varnamespace1 = io
.of(’/endpoint_1’)
.on(’connection’, function(socket){
namespace1.emit(’event’, { newSocket: socket.id });
});
var namespace2 = io
.of(’/endpoint_2’)
.on(’connection’, function(socket){
socket.emit(’event’, { hello: socket.id });
});
var namespace = io
.of(’/endpoint’)
.authorization(function(handshakeData, callback){
handshakeData.miToken = ’autorizado’;
callback(null, true);
})
.on(’connection’, function(socket){
console.log(socket.handshake.miToken);
});
166
evento ’connect’.
Rooms
[74] básicamente son etiquetas que se asignan a los sockets para crear gru-
pos de ellos pudiendo un socket tener varias. Es una manera fácil de realizar
el particionado de los clientes. A diferencia de los namespaces, en los que el
cliente solicitaba formar parte, esta división se realiza en el servidor como
mecanismo disponible para el programador y transparente al cliente .
Otras diferencias con los namespaces son que no tiene soporte nativo para
la autenticación, y que las salas son parte de un namespace mientras que
los namespaces, sin embargo, son parte del scope global [75].
io.sockets.clients(’room’)
para obtener las conexiones a una room
io.sockets.manager.roomClients[socket.id]
para obtener la lista de salas a las que pertenece un cliente determi-
nado. La lista es un objeto cuyos atributos, todos de valor true, son el
nombre de cada una de las salas.
Hay que destacar que el nombre de las salas manejado de las maneras an-
teriores aparecerá precedido del carácter /.
Del lado del cliente, Socket.IO se importa como cualquier fichero .js:
167
<script src="/socket.io/socket.io.js"></script>
<script src="/socket.io/socket.io.v0.9.10.js"></script>
En caso contrario:
<script src="http://<uri:port>/socket.io/socket.io.js"></script>
Yendo más allá, se puede especificar otra ubicación para la librería y servirla por
cualquier otro medio, teniendo en cuenta que hay que:
168
A través del segundo argumento (opcional), options, se pueden modificar un
amplio rango de parámetros relativos a la conexión. Por defecto, el objeto que
contiene las opciones es:
{
"force new connection": false,
"resource": "socket.io",
"transports": ["websocket",
"flashsocket",
"htmlfile",
"xhr-multipart",
"xhr-polling",
"jsonp-polling"],
"connect timeout": 5000,
"try multiple transports": true,
"reconnect": true,
"reconnection delay": 500,
"reconnection limit": Infinity,
"max reconnections attempts": 10,
"sync disconnect on unload": false,
"auto connect": false,
"flash policy port": 10843,
"manualFlush": false
}
resource
identifica el endpoint donde realizar conexiones, por defecto es el espacio
global socket.io pero puede ser el de cualquier namespace.
transports
es la lista por orden de preferencia de transportes que se quiere que se
169
intente utilizar para realizar la conexión. Por defecto, están todos, pero puede
restringirse a unos cuantos.
connect timeout
son los milisegundos que tiene Socket.IO para conectarse al servidor me-
diante un transporte antes de reintentarlo. Hay que tener en cuenta que
algunos transportes necesitan más tiempo que otros para conectar por lo
que conviene no modificar este parámetro.
reconnect
flag que indica si se debe o no intentar la reconexión automática en caso de
desconexión.
reconnection delay
factor de espera en milisegundos antes de volver a intentar una reconexión
con el servidor. Forma parte de un algoritmo de tipo exponential back off que
fija los milisegundos totales del siguiente intento según la fórmula N × delay
con N=1, 2, 3. . . número de intento.
reconnection limit
como el anterior parámetro, reconnection delay crece según un algoritmo
exponencial a cada intento de reconexión, se introduce éste otro parámetro,
reconnection limit, para marcar un límite máximo, también en milise-
gundos, que marca hasta dónde puede aumentar.
auto connect
170
conecta automáticamente con el servidor cuando se crea el socket.
manualFlush
activando esta opción se deja a criterio del programador cuándo invocar el
método socket.flushBuffer() que envía los datos al servidor.
http://example.com/socket.io/1/
171
Una vez recibido la respuesta a la negociación el cliente selecciona el modo de
transporte intersecando la lista de los que puede usar él y la lista que recibe del
servidor e intentando conectar con ellos uno a uno hasta tener éxito. Normalmen-
te debería ser el primero, pero puede depender, por ejemplo, de si la conexión es
cross-domain.
Completado este proceso, la forma de las URL que utiliza Socket.IO es como si-
gue:
esquema
indica el protocolo que se usa, en principio, http o https, pero puede ac-
tualizarse a ws o wss
host
la máquina que ejecuta el servidor Socket.IO, por defecto será aquella que
haya servido la página cargada por el cliente
namespace
el namespace donde se conecta el cliente. Si éste no indica ninguno, por
defecto será socket.io
versión protocolo
hasta el momento, el protocolo Socket.IO está en la versión 1. No confundir
con la versión del módulo, que la que se emplea aquí es la 0.9.10
id transporte
el identificador del transporte que se haya acordado sobre el que realizar la
conexión: xhr-polling, xhr-multipart, htmlfile, websocket,
flashsocket, jsonp-polling. Cada uno, como es obvio, se corresponde
con uno de los métodos detallados al inicio.
id sesion
es el identificador de cliente que el servidor asigna al socket durante el pro-
ceso de handshake.
query
es un parámetro opcional a través del cual podemos pasar información adi-
cional al servidor respetando las reservadas por Socket.IO:
172
t
timestamp, usado para evitar la caché en viejos agentes de usuarios
disconnect
usado para la desconexión
En el cliente son conocidas las claves que se envían en la query y, para ac-
ceder a ellas desde el servidor, se pueden consultar como atributos del objeto
socket.handshake.query.
Pueden sin embargo recibirse otros dos códigos que indican que el proceso no se
ha terminado de realizar satisfactoriamente, son:
401 Unauthorized
Cuando el servidor rechaza la conexión en base a las credenciales (cabecera
Cookie o cualquier otro método) que presenta el cliente para ser autorizado.
options
el objeto que contiene todas las opciones fijadas para el socket
connected
booleano que indica si el socket está conectado o no
connecting
booleano que indica si el socket está conectando o no
reconnecting
booleano que indica si el socket está reconectando o no
transport
referencia a la instancia del transporte que se emplea en la comunicación
send(mensaje, function(){})
Envía la cadena de datos mensaje. Cuando el servidor la recibe, se ejecuta
173
automáticamente la función de asentimiento pasada como segundo argu-
mento.
disconnect()
Finaliza la conexión con el servidor.
socket.on(’connect’, function(){})
Emitido cuando el socket se conecta satisfactoriamente.
socket.on(’connecting’, function(transport_type){})
Cuando el socket está intentando conectar con el servidor, tanto la primera
vez como en una reconexión. Se facilita por parámetro el tipo de transporte
a través del cual se está intentando realizar la conexión. Si el socket está in-
tentando reconectarse, este evento se emite después del evento
’reconnecting’ y antes del evento ’connect’.
socket.on(’connect_failed’, function(){})
Se emite cuando expira el timeout después del último intento de conexión si
está activada la opción connect timeout. Si try multiple transports es
la opción activada sólo se emite una vez probados todos los posibles medios
de transporte.
socket.on(’message’, function(message){})
Emitido cuando se recibe un mensaje del servidor.
socket.on(’close’, function(){})
Se emite cuando se cierra la conexión. Los autores de la librería nos advier-
174
ten de que puede emitirse bajo desconexiones temporales conocidas, como
sucede con las técnicas de Polling, donde se realiza una desconexión cuando
se han recibido los datos del servidor para inmediatamente abrir otra que
espere los siguientes.
socket.on(’disconnect’, function(){})
Cuando el socket se desconecta.
socket.on(’reconnect’,function( transport_type,
reconnectionAttempts){})
Se emite una vez la conexión se haya reestablecido y sólo si está activada la
opción reconnect. Los argumentos que recibe el listener son
transport_type, que indica con qué modo de transporte se ha realizado
la reconexión, y reconnectionAttempts, que indica el número de intentos
realizados para reconectar.
socket.on(’reconnecting’, function(reconnectionDelay,
reconnectionAttempts){})
Se emite en cada uno de los intentos de reconexión, e indica el número
de intentos que lleva y el tiempo de espera hasta el siguiente.
socket.on(’reconnect_failed’, function(){})
Emitido cuando han fallado todos los intentos de reconexión y no ha podido
establecerse la reconexión con el servidor.
Pero no es on() el único método para trabajar con eventos. Socket tiene disponible
también:
once(’evento’, function(){})
Permite atender a un evento una sola vez después de lo cual se elimina el
listener.
removeListener(’evento’, functionName)
Elimina el manejador de eventos functionName del ’evento’. functionName
no puede ser, por tanto, una función anónima.
175
7.3. Aplicación con Socket.IO
El juego es un clásico de los juegos de mesa. Consiste en una serie de cartas con
una imagen, iguales dos a dos, que se colocan boca abajo sobre un tablero. Por
turnos, los participantes deben descubrir dos de ellas buscando que sean pareja.
Si las cartas descubiertas en el turno no son iguales, se vuelven a dar la vuelta y
se cede el turno al otro participante. Si son iguales, se dejan a la vista y el jugador
vuelve a descubrir otras dos. Los jugadores deben intentar memorizar la posición
de las cartas que se van descubriendo con objeto de conseguir descubrir el mayor
número de parejas posibles siendo el ganador el que más parejas averigüe.
176
7.3.3. Diseño propuesto
De acuerdo con las sencillas reglas explicadas anteriormente, una partida del
juego puede modelarse como un objeto Game que contiene el estado del juego
entre los dos participantes. Consecuentemente la información que una instancia
este objeto almacena es:
Este cliente genera un evento joins, que señala que quiere unirse a una partida
con su nombre de usuario:
joins = { username }
177
Ver “Implementación con Socket.IO. Punto 5”
if (!waitingRoom){
startingPlayer = username;
waitingRoom = uuid.v1();
room2game[waitingRoom] = new models.Game();
// TODO: Agregar el socket a una sala
}
room2game[waitingRoom].lastTurn = username;
// TODO: Agregar el cliente a la sala
// TODO: Generar el evento de inicio ’start’
waitingRoom = null;
startingPlayer = null;
start = { [players] }
178
El desarrollo del juego transcurre según las normas anteriormente descritas: por
turnos se van descubriendo cartas de dos en dos intentando formar parejas pa-
ra puntuar. Cada pareja que se descubra proporciona dos puntos al jugador y,
además, la oportunidad de descubrir otro par de cartas.
discover = { id }
El código de manejo del evento en el servidor contiene la lógica del desarrollo del
juego y gestiona discover recuperando, gracias al identificador del socket, en qué
room se ha generado
var id = card.id
var game = room2game[ roomId ];
if (game == undefined || !(id in game.cardsMap)) return;
Sólo se procesarán los eventos ’discover’ que provienen del jugador que tiene
el turno, ignorando el resto. Sabremos qué usuario es gracias al socket
179
discover = { id, src }
game.lastTurn = username;
game.lastCard = null;
que sean iguales: se eliminan del cadsMap del juego los id de las cartas,
por si por algún error se envía alguna al servidor, poder ignorar la petición
comprobando su presencia:
if (game.cardsMap[lastId] == game.cardsMap[id]){
delete game.cardsMap[lastId];
delete game.cardsMap[id];
game.lastCard = null;
// TODO: Generar evento de acierto
// TODO: Recalcular puntuacion y notificarla a los clientes
}
180
Se notifica a los clientes mediante el evento ’success’, que incluye las id
de la pareja. El cliente debe dejarlas descubiertas en la interfaz.
success = { [ids] }
if (game.isOver()) {
// TODO: Notify the end to clients
delete room2game[roomId];
}
finish = {}
181
io.sockets.on(’connection’, function(socket){
socket.on(...);
socket.on(...);
});
3. Errores
A lo largo de la aplicación, se pueden encontrar situaciones que no puedan
manejarse con el código y sea necesario notificar al cliente mediante el evento
’error’. Se han definido uno para el juego que se genera mediante emit() y
se produce cuando cliente no proporciona nombre de usuario o se conectan
dos clientes con el mismo nombre:
socket.emit(’error’,
{ code: 0, extendedInfo: ’No username provided’ }
);
socket.set(’username’, username);
socket.set(’score’, 0);
5. Callback de asentimiento
El segundo argumento de la función que maneja el evento ’joins’ es un
callback de asentimiento. Cuando el cliente genera un evento con
182
emit(’joins’, message, callback) da opción al servidor a invocar una
función callback que se ejecutará en el propio cliente.
socket.join(waitingRoom);
io.sockets.in(waitingRoom).emit(’start’,
{ players: [startingPlayer, username]}
);
183
el citado cliente.
Señalar que a cada una de esas etiquetas le precede el carácter ’/’ por lo
que, para hacer uso de ellas, se debe quitar.
var roomId;
for (roomId in io.sockets.manager.roomClients[socket.id]){
if (roomId != ’’) break;
};
roomId = roomId.substring(1);
var room = io.sockets.in(roomId);
socket.on(’discover’, function(card){
var id = card.id;
// TODO: Revisar el estado del turno del juego
});
184
es la ruta de la imagen en el servidor que deben usar para sustituir en el
navegador cuando se descubre una carta.
room.emit(’discover’,
{ id: id, src: game.cardsMap[id] }
);
room.emit(’fail’,
{ ids: [ lastId, id ], src: "images/back.png" }
);
room.emit(’finish’);
185
7.4. Objetivos de los Koans
Mediante los Koans asociados al tema de Socket.IO se persigue reforzar los puntos
comentados en la Implementación con Socket.IO de la práctica. Algunos de ellos
son redundantes por lo que se han seleccionado los más representativos, por
ejemplo la emisión de eventos es una acción recurrente cuyas apariciones en
el código son iguales, y que abarquen los aspectos principales con intención de
afianzarlos. Por objetivos, éstos son:
___(username);
});
io.sockets.___(waitingRoom).emit(’start’,
{ players: [startingPlayer, username] }
);
186
5. Conocer que con emit() se emiten eventos definidos por el programador
socket.___(’error’,
{ code: 0, extendedInfo: ’No username provided’ }
);
var roomId;
for (roomId in io.sockets.manager.___[socket.id]){
if (roomId != ’’) break;
};
Además, con objeto de realizar las pruebas, las dependencias que se necesita son
jasmine-node y socket.io-client.
Otro de los módulos que se emplean, pero que viene con la práctica, es koanizer,
187
que adapta el código para que puedan insertarse los huecos que caracterizan a
un koan.
Una vez resueltos, puede verificarse que la solución es correcta corriendo las
pruebas con:
$ node app.js
http://localhost:5555/
7.6. Conclusión
188
Capítulo 8
Llegados a este punto, se puede afirmar que se ha establecido una base bastante
sólida con la que comenzar a crear aplicaciones para redes en la plataforma Node,
si bien algunos tópicos avanzados han quedado fuera del libro. Sin embargo no es
la finalidad abarcar todos los aspectos de Node y sus módulos, inmenso de por sí,
sino fijar unos conocimientos con los que el seguir descubriendo la plataforma se
convierta en algo más sencillo y rápido. No sólo a nivel de librerías sino de deta-
lles de funcionamiento interno, indispensables para convertirse en un verdadero
experto sobre ella.
Queda así, el trabajo de seguir actualizando el libro, con las nuevas característi-
cas y modificaciones de las actuales que se puedan producir. También es deseable
introducir nuevos capítulos con módulos existentes que no se han tratado, como
tls/ssl o https, o módulos que vayan surgiendo (que seguro que los habrá y en
cantidad), como domain. Todo, por suspuesto, acompañado de su propia aplica-
ción y diseño sugerido para ella, que sirva de aprendizaje a través de, cómo no,
Koans.
189
Por último, se dejará todo el código generado en manos de la comunidad, que
será juez imparcial de la utilidad de todo lo aquí expuesto. Si resulta acepta-
do, indudablemente surgirán fallos en el código que deberán ser corregidos, y
sobre todo, lo más atractivo, surgirán propuestas que mejoren sustancialmente
el diseño propuesto a cada uno de los problemas, con el beneficio que conlleva:
aprendizaje.
190
Apéndice A
Listados
A continuación se incluyen los listados de todos los módulos ordenados por capí-
tulos y completos, sin los huecos de los Koans, como referencia.
koanize(this);
191
* REFERENCE_CLOCK_FREQUENCY
/ SAMPLING_FREQUENCY);
var SECONDS_PER_FRAME = SAMPLES_PER_FRAME / SAMPLING_FREQUENCY;
this.setMarker = false;
this.ssrc = Math.floor(Math.random() * 100000);
this.seqNum = Math.floor(Math.random() * 1000);
this.timestamp = Math.floor(Math.random() * 1000);
};
util.inherits(RTPServer, events.EventEmitter);
RTPProtocol.prototype.pack = function(payload){
++this.seqNum;
if (!payload) {
// Tried to send a packet, but packet was not ready.
// Timestamp and Sequence Number should be increased
// anyway ’cause interval callback was called and
// that’s like sending silence
this.setMarker = true;
return;
}
// version = 2: 10
// padding = 0: 0
192
// extension = 0: 0
// CRSCCount = 0: 0000
/*
KOAN #1
should write Version, Padding, Extension and Count
*/
RTPPacket.writeUInt8(128, 0);
// Marker = 0: 0
// RFC 1890: RTP Profile for Audio and Video
// Conferences with Minimal Control
// Payload = 14: (MPEG Audio Only) 0001110
RTPPacket.writeUInt8(this.setMarker? 142 : 14, 1);
this.setMarker = false;
// SequenceNumber
/*
KOAN #2
should write Sequence Number
*/
RTPPacket.writeUInt16BE(this.seqNum, 2);
// Timestamp
/*
KOAN #3
should write Timestamp...
*/
RTPPacket.writeUInt32BE(this.timestamp, 4);
// SSRC
/*
KOAN #3
...SSRC and...
*/
RTPPacket.writeUInt32BE(this.ssrc, 8);
193
// 3.5 MPEG Audio-specific header
/*
KOAN #3
...payload Format
*/
RTPPacket.writeUInt32BE(0, 12);
payload.copy(RTPPacket, 16);
this.emit(’packet’, RTPPacket);
};
koanize(this);
Sender.prototype.start = function(){
/*
194
KOAN #2
should make udp server listening sucessfully
*/
this.rxSocket.bind(5001);
};
Sender.prototype.broadcast = function(packet){
var self = this;
/*
KOAN #3
should send a message correctly
*/
this.txSocket.send(packet,
0,
packet.length,
this.port,
this.broadcastAddress,
function(err, bytes){
++self.stats.txPackets;
self.stats.txBytes += bytes;
});
};
Sender.prototype.enableStats = function(enable){
var self = this;
if (enable){
/*
KOAN #4
should attend incoming packets from clients
*/
this.rxSocket.on(’message’, function(msg, rinfo){
var stats = new Buffer(JSON.stringify(self.stats));
/*
KOAN #5
should response to clients with stats messages
*/
dgram.createSocket(’udp4’).send(stats,
195
0,
stats.length,
5002,
rinfo.address);
})
}else{
this.rxSocket.removeAllListeners();
}
};
Sender.prototype.end = function(){
this.rxSocket.close();
this.txSocket.close();
}
exports.Sender = Sender;
koanize(this);
196
*/
this.server = net.createServer();
this.listen = function(port){
/*
KOAN #2
should be able to listen to incoming connections
*/
this.server.listen(port);
};
this.close = function(){
this.server.close();
};
/*
KOAN #3
should attend incoming connections
*/
this.server.on(’connection’, function(connection){
var remoteIP = connection.remoteAddress;
/*
KOAN #4
should write in connection socket
*/
connection.write("Welcome to your command line playlist manager, "
+ remoteIP);
if (remoteIP in sessionsDB){
/*
KOAN #5
should be able to close connections
*/
connection.end("Duplicated session, closing.");
return;
};
sessionsDB[remoteIP] = true;
197
var source = new sources.Mp3Source(library);
var rtpserver = new RTPServer();
var udpSocket = udp.createSocket(’udp4’);
rtpserver.on(’packet’, function(packet){
udpSocket.send(packet, 0, packet.length, 5002, remoteIP);
});
source.on(’frame’, function(frame){
rtpserver.pack(frame);
});
source.on(’track’, function(trackName){
connection.write("Now playing " + trackName + "\r\n# ");
});
source.on(’listEnd’, function(){
var seconds = 10;
connection.write("End of the list reached.Closing in "
+ seconds
+ " seconds\r\n# ");
/*
KOAN #6
should trigger a unactivity timeout on the socket
*/
connection.setTimeout(seconds * 1000, function(){
delete sessionsDB[this.remoteAddress];
connection.end("Your session has expired. Closing.");
});
});
/*
KOAN #7
should receive incoming data from connections
*/
connection.on(’data’, function(data){
198
// disable timeout, in case it was set
this.setTimeout(0);
199
connection.on(’close’, function(){
source.pause();
udpSocket.close();
rtpserver = source = null;
});
exports.create = function(){
var actions = [];
var app;
var library = new mediaLibraries.Mp3Library;
library.on(’ready’, function(){
app = new RemotePrompt(this);
// TO COMPLETELY IGNORE:
// some kind of ugly, lame & messy code mixing promise pattern
// with proxy pattern.
//
// This offers an object with same listen method as RemotePrompt
// class invocable when app is not ready yet. It offers too an
// onListening method that will install a callback on ’listening’
// event on server property of RemotePrompt; this method should
// not be in RemotePrompt class itself but it’s useful for
// testing purposes. It’s also chainable.
200
return new function(){
var _defer = function(callback){
if (actions){
actions.push(callback);
} else {
callback.apply(app)
}
};
this.close = function(){
_defer(function(){
this.close();
})
};
this.onListening = function(callback){
_defer(function(){
this.server.on(’listening’, callback);
});
return self;
}
}
};
201
var http = require(’http’),
url = require(’url’),
fs = require(’fs’),
querystring = require(’querystring’),
koanize = require(’koanizer’);
koanize(this);
202
return;
}
/*
KOAN #3
should make the server to use url module
*/
var uri = url.parse(req.url);
/*
KOAN #4
should make the server to identify the request method
*/
switch(req.method){
case ’GET’:
var path = uri.pathname;
if ( path == ’/’ ){
var player = this.broadcastList[broadcastIp];
writeDocument(res, {group:broadcastIp,
paused:player.paused,
tracks:player.list(),
currentTrack:player.currentTrack()});
} else {
fs.readFile("." + path, function(error, data){
if (error){
res.writeHead(404);
res.end();
return;
}
203
res.setHeader("Content-Encoding", "utf-8");
}
res.setHeader("Content-Length", data.length);
res.write(data, "binary");
res.end();
});
}
break;
case ’POST’:
req.setEncoding(’utf8’);
req.on(’end’, function(){
/*
KOAN #6
should make the server to use querystring module
*/
var query = querystring.parse(body);
var action = query.action;
204
});
break;
default:
res.writeHead(501, "Not Implemented");
res.end();
break;
}
});
205
};
list += "</ul>";
var allowedList = {
"224.0.0.114": "password"
}
var MIME_TYPES = {
"png" : "image/png",
"css" : "text/css",
"js" : "text/javascript",
"html" : "text/html"
}
exports.create = function(db){
if (db == null) throw new Error(’Database cannot be empty’);
listServer.broadcastList = db;
return listServer;
}
206
koanize = require(’koanizer’);
koanize(this);
app.engine(’.html’, require(’ejs’).__express);
app.set(’view engine’, ’ejs’);
app.configure(function(){
/*
KOAN #1
The Application must be properly
configured to serve favicon
*/
app.use(express.favicon()); // avoid call twice the routes
app.use(express.static(__dirname + ’/public’));
app.use(express.cookieParser());
app.use(express.session({ secret: "Node.js koans" }));
app.use(express.bodyParser()); // to parse post params
});
/**********************
PROFILE & HOME *
***********************/
/*
KOAN #2
Application must handle index page
*/
app.get(’/’, function(req, res){
res.sendfile(’./views/index.html’);
});
207
var username = req.param(’username’);
models.Whizr.findOne({ username: username },
function(err, doc){
if (err || doc == null) {
/*
KOAN #3
Application must be able to generate simple responses
*/
res.send(404, ’Not found’);
} else {
/*
KOAN #4
Application must be able to produce dynamic responses
according to a view
*/
res.render(’home.html’, { name: doc.name,
username: doc.username,
whizr: req.session.whizr } )
}
});
});
/******************************
REGISTER & LOGIN & LOGOUT *
208
*******************************/
/*
KOAN #5
Application must handle action endpoints for forms
*/
app.post(’/login’, function(req, res){
if ( doc.password == req.param(’password’) ) {
req.session.whizr = doc;
res.redirect(’/’ + doc.username);
} else {
res.send(’Unauthorized’, 401);
};
} )
});
209
}
});
if ( username.length <= 4
|| username.length >= 10
|| password.length <= 4
|| password.length >= 10) {
res.redirect(400, ’/’); // error
return;
}
whizr.save(function(err){
if (err) {
res.send(’Error’, 500);
};
210
req.session.whizr = whizr;
res.redirect(’/’ + username);
});
});
});
koanize(this);
app.engine(’.html’, require(’ejs’).__express);
app.set(’view engine’, ’ejs’);
app.configure(function(){
app.use(express.favicon()); // avoid call twice the routes
app.use(express.static(__dirname + ’/public’));
app.use(express.cookieParser());
app.use(express.session({ secret: "Node.js koans" }));
app.use(express.bodyParser()); // to parse post params
});
/**********************
INDEX & PROFILE *
***********************/
211
app.get(’/:username’, function(req, res){
});
});
212
/******************************
REGISTER & LOGIN & LOGOUT *
*******************************/
if ( doc.password == req.param(’password’) ) {
req.session.whizr = doc;
res.redirect(’/’ + doc.username);
} else {
res.send(’Unauthorized’, 401);
};
})
});
/*
213
KOAN #2
Application must allow or deny access to certain endpoints
*/
app.post(’/logout’, checkAuth, function(req, res){
var redirect = req.session.whizr.username;
req.session.destroy();
res.redirect(’/’ + redirect);
});
if ( username.length <= 4
|| username.length >= 10
|| password.length <= 4
|| password.length >= 10) {
res.redirect(’/’); // error
return;
}
214
whizr.newMentions = 0;
whizr.save(function(err){
if (err) {
res.send(’Error’, 500);
return;
}
req.session.whizr = whizr;
res.redirect(’/’ + username);
});
});
});
/*************
WHIZEAR *
**************/
if ( text == null
|| text.length == 0
|| text.length >= 140) {
res.redirect(’Error’, 404);
return;
}
215
});
});
/**********************
FOLLOW & UNFOLLOW *
***********************/
if (followTo.length == 0
|| followTo == null
|| followTo == req.session.whizr.username){
res.send(’Error’, 500);
return;
}
/*
KOAN #3
Application must be able to update user’s profiles
*/
models.Whizr.update( { username: req.session.whizr.username },
{ $addToSet: { following: followTo } },
null,
function(err, numAffected){
if (!err) {
req.session.whizr.following.push(followTo);
console.log(req.session.whizr.following);
res.redirect(’/’ + followTo);
}
});
});
216
}
/*
KOAN #3
Application must be able to update user’s profiles
according to certain conditions
*/
models.Whizr.update( { username: req.session.whizr.username },
{ $pull: { following: unfollow } },
null,
function(err, numAffected){
if (!err) {
// update the session
var following = req.session.whizr.following;
following.splice(following.indexOf(unfollow), 1);
res.redirect(’/’ + unfollow);
}
});
});
217
var socketio = require(’socket.io’),
uuid = require(’node-uuid’),
Game = require(’./models/Game.js’),
util = require(’util’),
koanize = require(’koanizer’);
koanize(this);
exports.createGame = function(server){
io = socketio.listen(server);
io.set(’log level’, 1);
/*
KOAN #1
Server must be able to receive incoming connections
*/
io.sockets.on(’connection’, function(socket){
/*
KOAN #2
The server must be able to properly
act to joins messages from client
*/
socket.on(’joins’, function(message, callback){
var username = message.username;
if (username
&& username != ’’
&& startingPlayer != username){
socket.set(’username’, username);
socket.set(’score’, 0);
/*
KOAN #3
As result of the joins message, the Server must acknowledge
218
it sending the username back to the client
*/
callback(username);
if (!waitingRoom){
startingPlayer = username;
waitingRoom = uuid.v1();
room2game[waitingRoom] = new Game();
socket.join(waitingRoom);
} else {
room2game[waitingRoom].lastTurn = username;
socket.join(waitingRoom);
/*
KOAN #4
Having two players in a room, the server must be able to
notify both the start of the game
*/
io.sockets.in(waitingRoom)
.emit(’start’,
{players: [startingPlayer, username]});
waitingRoom = null;
startingPlayer = null;
};
} else {
/*
KOAN #5
The server must handle properly faulty inputs
*/
socket.emit(’error’,
{ code: 0,
msg: ’Invalid username’ });
}
});
socket.on(’discover’, function(card){
var socket = this;
var id = card.id;
219
var roomId = ’’;
/*
KOAN #6
The server must be able to know what room the client is in
*/
for (roomId in io.sockets.manager.roomClients[socket.id]){
if (roomId != ’’) break;
};
roomId = roomId.substring(1);
var room = io.sockets.in(roomId);
var game = room2game[ roomId ];
/*
KOAN #7 (I)
The socket must obtain any client info from the socket
*/
socket.get(’username’, function(err, username){
if (err) {
console.log("Discover: username error", err);
process.exit();
}
if (game.lastTurn != username){
220
if (game.cardsMap[lastId] == game.cardsMap[id]){
delete game.cardsMap[lastId];
delete game.cardsMap[id];
game.lastCard = null;
if (game.isOver()) {
room.emit(’finish’);
delete room2game[roomId];
}
}
});
});
221
}
exports.endGame = function(){
if (io) {
io.server.close();
}
}
222
Bibliografía
[5] POSIX Austin Joint Working Group. Portable operating system interface (po-
six(r)). http://standards.ieee.org/findstds/standard/1003.1-2008.
html.
[7] Kris Kowal. Commonjs effort sets javascript on path for world
domination. http://arstechnica.com/web/news/2009/12/
commonjs-effort-sets-javascript-on-path-for-world-domination.
ars.
223
[11] Marc Lehmann. libev - a high performance full-featured event loop written
in c. http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod.
[13] W3C DOM Working Group et al. Document object model, 2002.
[14] David Flanagan. JavaScript: the definitive guide. O’Reilly Media, Incorpora-
ted, 2006.
[17] Cade Metz. The node ahead: Javascript leaps from browser into futu-
re. http://www.theregister.co.uk/2011/03/01/the_rise_and_rise_
of_node_dot_js/.
224
[25] Alex Payne. Node and scaling in the small vs. scaling in the large, July 2010.
http://al3x.net/2010/07/27/node.html.
[32] Isaac Schlueter. Which callbacks are sent to the event loop? https:
//groups.google.com/d/topic/nodejs/qluCAFK5zp4/discussion.
[33] Juan Antonio de la Puente. Introducción a los sistemas de tiempo real, 2007.
http://laurel.datsi.fi.upm.es/~ssoo/STR/Introduccion.pdf.
225
[39] Joel Spolsky. The absolute minimum every software developer absolutely,
positively must know about unicode and character sets. http://www.
joelonsoftware.com/articles/Unicode.html.
[40] David H Crocker. Rfc 822, standard for the format of arpa internet text mes-
sages, august 1982. URL http://www. cis. ohio-state. edu/htbin/rfc/rfc822.
html, 23:38–41.
[41] Network Working Group et al. Mime (multipurpose internet mail extension).
Technical report, RFC 1341, avril, 1993.
[43] S Casner and H Schulzrinne. Rfc 3551: Rtp profile for audio and video con-
ferences with minimal control. Technical report, Technical report, Columbia
University, Packet Design, 2003.
[45] ISO Iso. IEC 11172-3 Information technology-coding of moving pictures and
associated audio for digital storage media at up to about 1.5 Mbit/s-Part3:
Audio. Motion Picture Experts Group, 1993.
[47] Jon Postel. Rfc 793: Transmission control protocol, september 1981. Status:
Standard, 2003.
226
[51] Cross-site request forgery (csrf). https://www.owasp.org/index.php/
Cross-Site_Request_Forgery_(CSRF).
[52] Dave Raggett, Arnaud Le Hors, Ian Jacobs, et al. Html 4.01 specification.
W3C recommendation, 24, 1999.
[58] Amy Brown and Greg Wilson. The architecture of open source applications.
Lulu. com, 2011.
[61] Guillermo Rauch. Socket.io: the crossbrowser websocket for real-time apps.
http://socket.io.
[63] I Fette and A Melnikov. Rfc 6455: The websocket protocol. Technical report,
Status: Internet Draft. Available at: http://tools. ietf. org/html/draft-ietf-
hybi-thewebsocketprotocol-17. Accessed 8-February-2012, 2011.
227
[64] Ian Hickson. The websocket api. W3C Working Draft WD-websockets-
20110929, September, 2011.
228