Skip to content

S2: Node.js

Juan Gonzalez-Gomez edited this page Feb 7, 2023 · 4 revisions

Sesión 2: Node.js

  • Tiempo: 2h (50 + 50min)
  • Fecha: Martes, 7-Febrero-2023
  • Objetivos de la sesión:
    • Entender el funcionamiento de un servidor web
    • Repaso del protocolo HTTP
    • Repaso de Javascript
    • Mi primer servidor web con Node.js

Contenido

Introducción

En la asignatura de LTAW nos vamos a centrar en la programación del Back-end: estamos en el lado del Servidor. Los servidores de producción deben ser altamente escalabres: tienen que ser capaces de atender a cientos de miles de peticiones en el menor tiempo posible

Una forma de programar estas aplicaciones es con Nodejs. En esta sesión aprenderemos los conceptos básicos para programar nuestro primer servidor web usando Node. Para ello hay que entender (o refrescar) los conceptos involucrados en el camino: Funcionamiento del Protocolo HTTP, introducción a la programación asíncrona y repaso del lenguaje de programación Javascript, que es el que usa Node.js

Servidor Web

Un servidor web es una aplicación que procesa peticiones HTTP y genera sus correspondientes respuestas. Es decir, que los clientes le piden "cosas" al servidor, y este se las da (si las tiene). En caso de no poder atender a la solicitud se devuelve un código de error

Hay 3 operaciones que realiza el servidor:

  • Procesar la petición HTTP (¿Qué me piden?). Debe analizar el mensaje de solicitud recibido, y obtener el nombre del recurso que le están pidiendo
  • Obtener el recurso. A partir del nombre del recurso el servidor debe obtenerlo. Este recurso puede ser una página web contenida en un fichero, una imagen, un vídeo, etc. Típicamente los recursos se encontrarán almacenados en el disco duro. El servidor tendrá que acceder al sistema de ficheros de la máquina para obtenerlo
  • Generar el mensaje de respuesta y enviarlo de vuelta al cliente. Este mensaje se debe construir según la especificación indicada en HTTP. Habrá que incluir las cabeceras necesarias, el estado de la petición (ok, error, etc...) y la información pedida en el cuerpo del mensaje

Aunque para entender el funcionamiento estamos utilizando 1 cliente, en realidad un servidor real debe atender miles o decenas de miles de peticiones de clientes. Y esto es lo que hace que implementar un servidor sea complicado

Los clientes con.... ¡¡Un enjambre!! 😱️ 😱️

¡Node.js al rescate!

Para lograr que la programación de los servidores web sea más sencilla y sobre todo, escalable, nació Node.js. No sólo sirve para servidores web, sino para programas de red, que puedan ser altamente escalables. Actualmente lo están usando en producción empresas como Amazon, Netflix o Paypal

Características:

  • Software libre
  • Multiplataforma
  • Las aplicaciones se programan en Javascript
  • Paradigma de programación asíncrona: Permite tener una mejor respuesta en las aplicaciones y reduce el tiempo de espera del cliente
  • Arquitectura orientada a eventos: La programación asíncrona se implementa mediante funciones de callback()
  • Motor V8 de Google: Es el mismo motor javascript usado en Chrome. Permite que usemos el mismo código javascript tanto en el cliente como en el servidor

Programación síncrona vs asíncrona

La programación que utilizas normalmenete se denomina programación síncrona: Hasta que no termina de ejecutarse una instrucción no se pasa a la siguiente. Imaginemos que tu aplicación tiene que leer tres ficheros independientes que se encuentran en tu disco duro. Podrían ser, por ejemplo, las peticiones de 3 clientes.

Esta es la pinta que tendría un código síncrono, escrito en pseudocódigo:

[...]
contenido_1 = leer_fichero('fichero1.html');

contenido_2 = leer_fichero('fichero2.html');

contenido_3 = leer_fichero('fichero3.html');
[...]

El acceso al sistema de ficheros, para leer o escribir un fichero es una operación de entrada/salida, y es una operación lenta en comparación con la velocidad con la que el procesador ejecuta instrucciones. En este código primero se lee el fichero1.html, y se queda esperando hasta que la operación se termina. Luego se pasa a la siguiente línea: leer el fichero2.html, y se queda esperando a que termine. Por último se lee el último fichero: fichero3.html

Con este paradigma de programa síncrona tenemos garantizado el orden en el que se ejecutan las operaciones. Así, en este ejemplo, sabemos que los ficheros se leen SIEMPRE en este orden: fichero1.html, fichero2.html y fichero3.html

Para reducir los tiempos de espera, en los que NO se está ejecutando código de nuestra aplicación, se utiliza el paradigma asíncrono. En vez de esperar a que termine una tarea, se pasa como parámetro una función de retrollamada (callback). Esta función se invoca automáticamente en cuanto la tarea ha terminado. El ejemplo de la lectura de los 3 ficheros se haría de forma asíncrona de la siguiente forma:

[...]
Leer_fichero('fichero1.html', callback1)

Leer_fichero('fichero2.html', callback2)

Leer_fichero('fichero3.html', callback3)

imprimir_mensaje("Mensaje 1")
[...]

Al ejecutarse la primera instrucción se está diciendo: "Comienza la lectura del fichero fichero1.html. Cuando termines, me avisas, ejecutando la función callback1". Tras ello se pasa a ejecutar la siguiente línea, donde se hace lo mismo: se comienza la operación de lectura de fichero2.html y se indica que al terminar se ejecute la función callback2. Y lo mismo con el último fichero

La ventaja de esto es que mientras se están leyendo los ficheros en paralelo, se puede seguir ejecutando código de mi aplicación. El precio a pagar es que ahora NO sabemos en qué orden se van a leer los ficheros. Sí sabemos el orden en el que han comenzado las tareas de lectura, pero NO en qué orden terminan

Esta podría ser un posible orden de ejecución:

Mensaje 1
Lectura fichero2
Lectura fichero3
Lectura fichero1

Como las lecturas de los ficheros llevan tiempo, primero se vería en la consola el mensaje Mensaje 1. A continuación termina la lectura del fichero 2 (y se llama a callback2), luego la del fichero 3 y por último la del fichero 1. El orden podría haber sido así porque el fichero 2 fuese el más corto, y el fichero 1 el más largo. Pero en general, el orden podría cambiar (tal vez alguno de los ficheros esté cacheado, y aunque sea de mayor tamaño, tardará menos).

Con el paradigma de la programación asíncrona NO SABEMOS el orden de ejecución de las tareas. La único que tenemos garantizado es que se ejecutarán las funciones de callback cuando las tareas se completen

Protocolo HTTP

La comunicación entre el cliente y el servidor se realiza a través del Protocolo HTTP. Sólo hay dos tipos de mensajes:

  • Mensaje de solicitud (request): Lo envía el cliente al servidor
  • Mensaje de respuesta (response): Lo envía el servidor al cliente

La conversación la inicia siempre el cliente. El formato de ambos tipos de mensajes es el mismo. Son mensajes de texto organizados en líneas. Cada línea termina en el carácter de salto de línea '\n'. Las primeras líneas forman la cabeza, luego hay una línea en blanco y finalmente el cuerpo, que es opcional

La primera línea del mensaje de solicitud contiene el tipo de solicitud (método), el nombre del recurso solicitado y la versión del protocolo usado. Las siguientes líneas son las cabeceras: una lista de pares Nombre: valor, separados por dos puntos

Hay 3 métodos:

  • GET: Solicitar un recurso al servidor
  • POST: Envío de datos del cliente al servidor (en el cuerpo de la solicitud)
  • HEAD: Similar a GET, pero sólo se solicitan las cabeceras y no el cuerpo. Lo utiliza el cliente para saber si un determinado recurso se ha modificado desde la ultima vez que se pidió

Este es un ejemplo de un mensaje de solicitud en el que se pide el recurso raiz (/) usando el método GET. El mensaje no tiene cuerpo

Este es un ejemplo de un mensaje de respuesta. Es el mensaje generado por la solicitud anterior. La primera línea también es especial: contiene el resultado de la solitid: un código que indica qué ha pasado

Estos son algunos ejemplos de códigos:

  • 200: OK. La petición se ha podido realizar
  • 404: Not Found. El recurso pedido no está disponible en el servidor
  • 304: Not modified. El recurso pedido no ha cambiado desde la última vez

Tras la línea en blanco el mensaje de respuesta contiene el código html. El cliente sabe que esta información es un fichero HTML porque se indica en la cabecera Content-Type

Recordando javascript

Vamos a recordar algunos ejemplos básicos en Javascript, pero ahora ejecutándolos desde node.js en vez desde el navegador. En node.js se tiene acceso directo a muchos objetos, como por ejemplo los temporizadores o la consola

Probando ejemplos desde VSCode

Los ejemplos se pueden probar directamente desde la línea de comandos de nuestro sistema operativo. Pero, desde VSCode podemos abrir muy fácilmente un termimal de línea de comandos pinchando con el botón derecho sobre la carpeta dodne estén los ejemplos, y luego en la opción Open in integrated Terminal:

Desde ese terminal no hay más que escribir node seguido del nombre de nuestro programa

Ejemplo 1: Hola mundo

Este es el programa hola mundo, que imprime la cadena "Hola mundo" en la consola. Se han incluido los dos tipos de comentarios: de una línea y multilínea para recordar

//-- Programa Hola mundo en Node.js

/* Este es un ejemplo de comentario multilínea
   El objeto console está disponible directamente
   desde node.js, sin tener que importar nada */

//-- Imprimir un mensaje en la consola
console.log("¡Hola Mundo!");

Al ejecutarlo desde vSCode esto es lo que se obtiene:

Y este es el comando usado en la terminal

 $ node ej-01-hello.js 
¡Hola Mundo!
$

Ejemplo 2: Variables

Las variables se declaran con la palabra reservada let. En este ejemplo se define la variable n, que es un número entero, y se imprime su valor en la consola, usando diferentes formas

//-- Probando variables y cómo imprimirlas
//-- en la consola

//-- Variable numérica
let n = 3;

//-- Imprimir la variable directamente
console.log("Variable n: ", n);

//-- Valor de la variable dentro de una cadena
console.log(`Variable n: ${n} metros`);

//-- Concatenar la variable al mensaje
console.log("Variable n: " + n);

Esta es la ejecución desde VSCode:

Y este el comando en la terminal:

$ node ej-02-variables.js 
Variable n:  3
Variable n: 3 metros
Variable n: 3
$

Ejemplo 3: Bucles

Se utiliza un bucle para imprimir un mensaje 10 veces. El número de iteraciones se define mediante la constante N, que se crea con la palabra reservada const

//-- Ejemplo de bucles

//-- Definiendo una constante: Número de mensajes
const N = 10; 

//-- Bucle para imprimir N mensajes
for (i = 0; i < N; i++) {
    console.log("Mensaje " + i);
}

Esta es la ejecución desde VSCode:

Y este es el comando desde la terminal:

$ node ej-03-bucles.js 
Mensaje 0
Mensaje 1
Mensaje 2
Mensaje 3
Mensaje 4
Mensaje 5
Mensaje 6
Mensaje 7
Mensaje 8
Mensaje 9
$

Ejemplo 4: Objetos literales

En Javascript podemos crear objetos con estructura, en la que definimos propiedades que tienen un valor. Los objetos literales se crean con las llaves {}. En este ejemplo se define la variable objeto1 que tiene 3 propiedades, denominadas nombre, valor y test

//-- Ejemplo de definición y uso de objetos literales

//-- Definiendo un objeto con varias propiedades y valores
const objeto1 = {
    nombre: "Objeto-1",
    valor: 10,
    test: true
};

//-- Imprimiendo las propiedades del objeto
console.log("Nombre: " + objeto1.nombre);
console.log("Valor: " + objeto1.valor);
console.log("Test: " + objeto1.test);

//-- También te puedes referir a las propiedades
//-- usando su nombre entre comillas
console.log("");
console.log("Nombre: " + objeto1["nombre"]);
console.log("Valor: " + objeto1["valor"]);
console.log("Test: " + objeto1["test"]);

//-- Comprobar si un objeto tiene una propiedad
if ("test" in objeto1) {
    console.log("\nTiene propidad test");
}

//-- Recorrer todas las propiedades
console.log("");
for (prop in objeto1) {
    console.log(`Propiedad: ${prop} --> Valor: ${objeto1[prop]}`);
}

//-- Forma abreviada para obtener constantes
//-- con las propiedades del objeto
const { valor, nombre } = objeto1;

console.log("");
console.log("Nombre: " + nombre);
console.log("Valor: " + valor);

Todas las variables que se definen en VSCode se pueden ver en el menú lateral OUTLINE, junto a su estructura. Así, podemos ver cómo la variable objeto1 tiene efectivamente 3 propiedades. También vemos dos variables adicionales: valor y nombre

Esto es lo que obtenemos al ejecutarlo

Para obtener nuevas variables a partir de las propiedades se puede hacer de varias formas. En el ejemplo vemos esta línea:

const { valor, nombre } = objeto1;

Este código es la forma abreviada de este otro:

const valor = objeto1.valor;
const nombre = objeto1.nombre;

Ejemplo 5: Arrays literales

Los arrays (o listas) se crean encerrando entre corchetes sus elementos. En este ejemplo se crea el array a formado por los 4 primeros números impares

//-- Ejemplo de arrays literales

//-- Crear una lista (array) de 4 elementos
const a = [1,3,5,7];

//-- Mostrar el elemento 2
console.log("Elemento 2: " + a[2]);

//-- Recorrer todos los elementos
for (i in a) {
    console.log(`a[${i}] = ${a[i]}`);
}

//-- Imprimir el numero total de elementos
console.log("Cantidad de elementos: " + a.length);

La propiedad length de los arrays nos indica el número de elementos que totales que tiene

Aquí vemos el resultado de su ejecución

Ejemplo 6: Funciones

En Javascript las funciones se pueden declarar de muchas formas. En realidad son como cualquier otro objeto: se pueden pasar como parámetros, se pueden definir objetos que las contengan, etc.

En este ejemplo se definen 5 funciones de diferentes formas, y luego se llaman para ver el resultado:

//-- Ejemplo de definicion de funciones

//-- Se definen 4 funciones sin parámetros
//-- de diferentes formar

//-- Definición clásica
function mi_funcion1() {
    console.log("Mi funcion 1!!");
}

//-- Se define una función y se asigna a una variable
const mi_funcion2 = function() {
    console.log("Mi funcion2....");
}

//-- Otra forma de hacer lo anterior, pero con una
//-- notación abreviada
const mi_funcion3 = () => {
    console.log("Mi funcion3....")
}

//-- Definición de funciones dentro de un 
//-- objeto literal
const a = {
    x : 10,
    f4 : function() {
        console.log("Mi funcion4!!!");
    },
    f5: () => {
        console.log("Mi funcion 5!!!");
    }
}

//-- Llamando a las funciones
mi_funcion1()
mi_funcion2()
mi_funcion3()
a.f4()
a.f5()

En el menú OUTLINE vemos las 5 funciones definidas. Hay dos, mi_funcion2 y mi_funcion3, que en realidad son variables que referencian a una función. Pero a todos los efectos se comportan como una función:

Esto es lo que vemos al ejecutar el ejemplo:

Ejemplo 7: Paso de parámetros

Ejemplo de paso de parámetros a funciones. ¡Estos parámetros pueden a su vez ser también funciones!. La función sum(a,b) tiene dos parámetros de entrada y devuelve su suma. La función mensaje(msg) tiene un parámetro de entrada y no devuelve nada. La función call(func) tiene un parámetro de entrada que es una función, a la que llama y termina

//-- Ejemplo de paso de parametros a funciones

//-- Recibe dos parámetros y devuelve su suma
function suma(x,y) {
  //-- devolver la suma
  return x+y;
}

//-- Recibe un parámetro y lo imprime por la consola
function mensaje(msg) {
  console.log(msg);
}

//-- Funcion que no recibe parametros
function saluda() {
    mensaje("HOLA!!");
}

//-- Funcion que recibe una funcion como parametro
//-- y simplemente la llama 
function call(func) {
  console.log("--> Funcion recibida");

  //-- Llamar a la funcion pasada como argumento
  func();
}

//-- Llamar a suma
let a = suma(2,3);

//-- Probando la funcione mensaje
mensaje("Prueba")
mensaje(a);

//-- Probando la funcion call
call(saluda);

//-- Se le pasa como parametro una funcion
//-- que se define dentro de los parmatros, vez de 
//-- fuera
call( () => {
  mensaje("HOLI!!")
});

La última llamada se utiliza muchísimo. Se la pasa a call() una función, pero esta función se define dentro de los parámetros, en vez de hacerlo fuera y usar su nombre. Se hace así para implementar de forma abreviada las funciones de callback

Este es el resultado de la ejecución:

Temporizadores

Los temporizadores son elementos a los que tenemos acceso directo, sin tener que incluir ningún módulo. Hay 3 funciones para manejar temporizadores:

  • setTimeout(func, ms): Se llama a la función func() transcurridos ms milisegundos. La llamada se hace sólo una vez
  • setInterval(func, ms): Se llama de forma periódica a la función func(), cada ms milisegundos. Para eliminar el temporizador hay que invocar a clearInterval(id) pasando como argumento el identificador del temporizador
  • clearInterval(id): Eliminar el temporizador que tiene el identificador id

En este ejemplo se llama a 4 funciones de retrollamada. Una es tarea1(), definida como una función clásica, para que se ejecute al cabo de 1 segundo. La otra está definida dentro de los parámetros de setTimeout(), e imprime un mensaje en la consola transcurridos 2 segundos desde el comienzo. La tercera es una llamada periódica, a través de setInterval. Se imprimen el mensaje "Tic..." cada 200ms. Y por último hay una cuarta llamada que se encarga de eliminar el temporizador periódico para que el programa termine

//-- Ejemplo de uso de un temporizador

//-- Función a ejecutar tras un tiempo
//-- Función de retrollamada del temporizador
function tarea1() {
    console.log("Tarea 1 completada!");
}


//-- Llamada retardada mediante temporizador
//-- Cuando transcurran 1000 ms se llama a la función tarea 1
setTimeout(tarea1, 1000);

//-- Esta estructura también es muy típica: incluir la función 
//-- de retrollamada directamente en el parémtro, en vez de definirla
//-- fuera
setTimeout( () => {
    console.log("Tarea 2 completada!");
}, 2000);

console.log("Esperando a que terminen las tareas");

//-- Esta función de retrollamada se invoca cada 200ms
//-- Se guarda su identificador en la variable id par
//-- poder quitar el temporizador con ClearInterval 
let id = setInterval( () => {
    console.log("Tic...");
}, 200 );

//-- Al cabo de 3 segundos se desactiva el temporizador
setTimeout( ()=> {
  clearInterval(id)
  console.log("Stop!");
}, 3000);

Este es el resultado al ejecutarlo:

Módulos de node.js

Node.js viene con una serie de módulos del sistema listos para usarse. No hay que instalar nada adicional. Sólo hay que utilizar la palabra reservada require para importarlos. La sintaxis es esta:

//-- Importar el módulo 'module_name'
const mod = require('module_name')

Una vez importado se dispondrá del objeto definido (en este ejemplo mod) sobre el que se podrán acceder a todas sus propiedades y objetos adicionales

Estos son algunos de los módulos disponibles:

  • http: Módulo principal para crear servidores web. Nos permitir inicializar y lanzar el servidor. Contiene las clases http.IncomingMessage y http.ServerResponse para gestionar los mesajes de solicitud y de respuesta del protocolo HTTP
  • fs: Módulo para acceder al sistema de ficheros
  • path: Funcionalidad para trabajar con los nombres de los ficheros
  • url: Analizar y procesar URLs
  • os: Métodos y propiedades relativas al sistema operativo usado

La documentación de todos los módulos está disponible en esta página de documentación de nodejs

Módulo http

Es el módulo principal para trabajar con servidores web. Para acceder a él tenemos que poner la siguiente línea:

const http = require('http');

Si importamos el módulo desde VSCode, al escribir http y poner un punto (.), nos aparecerán todos los elementos disponibles en el módulo http

Creando el servidor: http.createServer()

El servidor es un objeto del tipo http.server que se crea llamando al método createServer();

//-- Crear el servidor
const server = http.createServer();

Función de retrollamada para atender peticiones

Cada vez que un cliente nos haga una solicitud, el servidor llamará a una función de retrollamada que definimos de la siguiente forma. En nuestro caso la vamos llamar atender()

//-- Función de retrollamada de petición recibida
//-- Cada vez que un cliente realiza una petición
//-- Se llama a esta función
function atender(req, res) {
    //-- req: http.IncomingMessage: Mensaje de solicitud
    //-- res: http.ServerResponse: Mensaje de respuesta (vacío)

    //-- Indicamos que se ha recibido una petición
    console.log("Petición recibida!");

    //-- pero no enviamos respusta (todavía)
}

Esta función recibe el mensaje de solicitud, req, que es del tipo http.IncommingMessage y también un objeto del tipo http.ServerResponse, res, para generar la respuesta (este objeto está inicialmente vacío)

En la versión más básica de atender() simplemente escribimos un mensaje en la consola. No devolvemos el mensaje de respuesta

Configurar la función de retrollamada

Cuando se produce una petición del cliente se emite el evento 'request'. Mediante esta línea se configura el servidor para que llame a la función atender() cada vez que se produce este evento

//-- Activar la función de retrollamada del servidor
server.on('request', atender);

Activar el servidor: Listen

El último paso es activar el servidor para que quede a la eschuca en el puerto indicado. Esto se hace con el método listen()

//-- Activar el servidor. A la escucha de peitciones
//-- en el puerto 8080
server.listen(8080);

Servidores web hola mundo

Ya tenemos todas las herramientas para construir nuestros primeros servidores, empezando por el más básico. Comenzaremos por uno que simplemente escucha peticiones y lo notifica en la consola, pero todavía no responde al cliente. Este servidor lo iremos mejorando prograsivamente en las siguiente versiones

Servidor 1: Detección de peticiones

El servidor más básico lo denominamos servidor Nulo. No atiende a clientes, porque no devuelve mensajes de respuesta, pero nos notifica en la consola que los clientes se han conectado

const http = require('http');

//-- Crear el servidor
const server = http.createServer();

//-- Función de retrollamada de petición recibida
//-- Cada vez que un cliente realiza una petición
//-- Se llama a esta función
function atender(req, res) {
    //-- req: http.IncomingMessage: Mensaje de solicitud
    //-- res: http.SercerResponse: Mensaje de respuesta (vacío)

    //-- Indicamos que se ha recibido una petición
    console.log("Petición recibida!");

    //-- pero no enviamos respusta (todavía)
}

//-- Activar la función de retrollamada del servidor
server.on('request', atender);

//-- Activar el servidor. A la escucha de peitciones
//-- en el puerto 8080
server.listen(8080);

En esta animación se muestra su funcionamiento:

Una vez arrancado el servidor, no ocurre nada. Se queda a la espera de conexiones. Desde el navegador lanzamos una petición. Nos conectamos la URL: http://127.0.0.1:8080/, que significa: "Conéctate al puerto 8080 de tu propia máquina"

Al hacerlo vemos la notificación en la consola del servidor: Petición recibida!. Sin embargo en el navegador no aparece nada. Se queda esperando a que llegue una respuesta. Transcurrido un tiempo aparecerá un mensaje indicando que NO se ha recibido respuesta del servidor

El servidor se detiene pulsando Ctrl-c

Enviando peticiones con curl

Las peticiones las estamos haciendo con el navegador, pero se pueden hacer directamente desde la línea de comandos usando la herramienta curl. En la web de Julia Evans se pueden encontrar ejemplos muy sencillos del uso de curl

Curl es especialmente útil para automatizar la prueba de servidores

El comando que se está ejecutando es este:

curl -m 1 127.0.0.1:8080

El parámetro -m 1 se usa para establecer un tiemout de 1 segundo. Si transcurrido un segundo no se obtiene respuesta, curl termina

Servidor 2: Haciendo el código más compacto

Esta segundo versión sigue siendo un servidor nulo, que no responde, pero el código se ha mejorado. Se define la constante PUERTO al comienzo, para indicar el puerto usado para la escucha. Se añade un mensaje al final indicando que el servidor está activo y en qué puerto está escuchando

El método http.createServer() permite pasar como parámetro la función de retrollamada de atención de las peticiones. La configuración del servidor para llamar a esta función al recibir el evento 'request' se hace internamente, por lo que lo podemos quitar del código

La nueva versión del servidor queda así:

const http = require('http');

//-- Definir el puerto a utilizar
const PUERTO = 8080;

//-- Función de retrollamada de petición recibida
function atender(req, res) {

    //-- Indicamos que se ha recibido una petición
    console.log("Petición recibida!");
}

//-- Crear el servidor. Se pasa como argumento la 
//-- función de retrollamada. La función createServer()
//-- la conecta con el evento 'request'
const server = http.createServer(atender);

//-- Activar el servidor: ¡Que empiece la fiesta!
server.listen(PUERTO);

console.log("Servidor activado. Escuchando en puerto: " + PUERTO);

Y en esta animación se muestra en funcionamiento. Usamos curl para lanzar las peticiones

Servidor 3: Definiendo el callback en createServer

El código se puede hacer todavía más compacto. La función atender() se puede definir directamente como primer parámetro en la llamada a http.createServer(). Esta forma de definir las funciones de callback es la que típicamente se usa en Javascript

const http = require('http');

//-- Definir el puerto a utilizar
const PUERTO = 8080;

//-- Crear el servidor. La función de retrollamada de
//-- atención a las peticiones se define detnro de los
//-- argumentos
const server = http.createServer((req, res) => {
    
  //-- Indicamos que se ha recibido una petición
  console.log("Petición recibida!");
});

//-- Activar el servidor: ¡Que empiece la fiesta!
server.listen(PUERTO);

console.log("Servidor activado. Escuchando en puerto: " + PUERTO);

El funcionamiento es exactamente igual que en la versión anterior. A partir de ahora siempre definiremos las funciones de retrollamada de esta forma

Servidor 4: Happy server: Enviando respuesta

En esta versión del servidor incluiremos un mensaje de respuesta. Este servidor lo denominamos Happy server porque siempre devuelve una respuesta OK, a todo el mundo. Para el Happy server siempre todo está perfecto y su vida es maravillosa

const http = require('http');

//-- Definir el puerto a utilizar
const PUERTO = 8080;

//-- Crear el servidor
const server = http.createServer((req, res) => {
    
  //-- Indicamos que se ha recibido una petición
  console.log("Petición recibida!");

  //-- Enviar una respuesta:. Siempre es la misma respuesta
  //-- Con el método res.write() se escribe el mensaje en el 
  //-- cuerpo de la respuesta
  res.write("Soy el Happy server!!\n");

  //-- Terminar la respuesta y enviarla
  res.end();
});

//-- Activar el servidor: ¡Que empiece la fiesta!
server.listen(PUERTO);

console.log("Happy server activado!. Escuchando en puerto: " + PUERTO);

En la función de retrollamada, que ahora la definimos en los parámetros de createServer(), recibimos como segundo parámetro el objeto res, que es un mensaje de respuesta vacío. Usando el método res.write() añadimos un texto al cuerpo de la respuesta. Y con el método res.end() indicamos que ya hemos terminado el mensaje y queremos que se envíe

El mensaje de respuesta de este servidor en realidad es incompleto: no hemos añadido cabeceras ni indicado el código de estado de la respuesta. Al invocar res.end() se completa la información por nosotros. Si no indicamos nada se asume que la respuesta es correcta

En esta animación se muestra su funcionamiento. Ahora ya sí funciona correctamente la petición que realiza el navegador, y nos muestra el mensaje recibido en la pantalla

Cuando el servidor está lanzado y realizamos la petición, aparece el mensaje que devuelve el Happy server. Cada vez que se recarga la página observamos que aparece una petición nueva. Si paramos el servidor (Ctrl-C) y recargamos la página, vemos que nos sale un mensaje de error

Servidor 5: Happy server: Añadiendo cabeceras

El servidor anterior funciona correctamente porque por defecto el tipo de datos esperado en el cuerpo, si no se indica otra cosa es texto plano (text/plain)

Mediante la cabecera Content-Type indicamos el tipo del cuerpo. Los valores típicos son los siguientes:

  • text/plain: Texto plano. Valor por defecto
  • text/html: Texto HTML
  • text/css: Hoja de estilo
  • image/jpeg: Imagen en formato jpeg
  • image/png: Imagen en formato png

Se trata de valores que están estandarizados. Se conocen como Tipos MIME

Para establecer el valor de una cabecera usamos el método setHeader(). En nuestro Happy server, como estamos devolviendo texto plano, le daremos el valor text/plain

Así es como nos queda el servidor

const http = require('http');

//-- Definir el puerto a utilizar
const PUERTO = 8080;

//-- Crear el servidor
const server = http.createServer((req, res) => {
    
  //-- Indicamos que se ha recibido una petición
  console.log("Petición recibida!");

  //-- Cabecera que indica el tipo de datos del
  //-- cuerpo de la respuesta: Texto plano
  res.setHeader('Content-Type', 'text/plain');

  //-- Mensaje del cuerpo
  res.write("Soy el Happy server!!\n");

  //-- Terminar la respuesta y enviarla
  res.end();
});

//-- Activar el servidor: ¡Que empiece la fiesta!
server.listen(PUERTO);

console.log("Happy server activado!. Escuchando en puerto: " + PUERTO);

El funcionamiento es exactamente igual que el anterior, pero ahora estamos enviando la cabecera Content-Type.

También podemos hacer pruebas accediendo al servidor web desde otro dispositivo, como por ejemplo nuestro teléfono móvil. Para ello necesitamos conocer la dirección IP de nuestro ordenador, y asegurarnos que el móvil está conectado por wifi a nuestra misma red de casa

Desde ubuntu puedes ver tu IP desde el menú de configuración general (Settings), en la opción de Network. Pincha en el icono del engranaje situado en la parte derecha de tu interfaz de red (en mi caso es PC Ethernet porque estoy conectado por cable)

También lo puedes ver desde la línea de comandos usando ifconfig o ip address

Desde el navegador del teléfono móvil debes poner tu dirección IP y el puerto al que conectarse. En mi caso es: 192.168.1.64:8080

Analizando el protocolo http

Ahora que ya tenemos un (cutre) servidor web básico funcionando, vamos a utilizar diferentes herramientas para analizar los mensajes de petición y respuesta generados. Lo podemos hacer de dos formas: por un lado usando las herramientas del desarrollador web del navegador, y por otro usando la herramienta curl en la consola

Herramientas del navegador para desarrolladores web

Todos los navegadores tienen herramientas para desarrollar aplicaciones web y encontrar los errores. Para analizar los mensajes HTTP intercambiados entre el cliente y el servidor vamos a utilizar la herramienta Network de Fireforx

Partimos del servidor web lanzado* (servidor 5). Desde el navegador nos conectamos a la URL: http://127.0.0.1:8080/. Accdedemos a la herramienta Network a través del menú de la esquina superior derecha, y luego vamos a Web developer

Ahí pinchamos en Network:

Nos aparece la herramienta de Network, que inicialmente no tiene información. Activamos la casilla Disable Cache para deshabilitar la caché y que todas las peticiones se realicen al servidor (de lo contrario no las hace porque ya tiene la información previamente almacenada)

Iniciamos la captura recargando la página. Nos aparece la información sobre las peticiones. Se han realizado en total 2 peticiones. El número lo vemos en la parte inferior izquieda

Las peticiones nos aparecen ordenadas por filas. En la primera fila está la petición que hemos hecho nosotros. Se está solicitando el recurso principal / al servidor web situado en 127.0.0.1:8080. En la parte izquierda vemos que el resultado de esta petición es correcta (OK, código 200). También vemos que el tipo de datos de la respuesta es plain (que significa texto plano)

Si pinchamos en la primera fila obtenemos más información sobre los mensajes:

Por defecto se abre en la pestaña Headers. Vemos que se trata del método GET. También vemos las cabeceras tanto de la respuesta como de la solicitud. Pinchando en GET podemos desplegarlo y obtener más información. Y pichando en RAW vemos las cabeceras tal cual son, sin procesar. Esto es muy útil para entender el protocolo HTTP y ver exactametne qué cabeceras envía el navegador y cuáles nuestro servidor

En las cabeceras del mensaje de respuesta vemos la que hemos activamo: Content-Type, y comprobamos que efectivamente tiene el tipo text/plain

Usando la herramienta curl en la línea de comandos

Con la herramienta Curl también podemos ver las cabeceras de los mensajes. No hay más que pasar el parámetro -v:

$ curl -vv  'http://localhost:8080/' 
*   Trying 127.0.0.1:8080...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Date: Mon, 22 Feb 2021 06:04:59 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
< 
Soy el Happy server!!
* Connection #0 to host localhost left intact
obijuan@corellia:~$

Los textos que comienzan por * son información general. Con el símbolo > se indica que se trata del mensaje de solicitud y con < es el mensaje de respuesta. En la parte final aparece el contenido que hay en el cuerpo del mensaje de respuesta

Autor

Licencia

Enlaces

TEORIA

Soluciones

LABORATORIO

Prácticas y sesiones de laboratorio

Práctica 0: Herramientas

Práctica 1: Node.js: Tienda Básica

Práctica 2: Interacción cliente-servidor. Tienda mejorada

Práctica 3: Websockets: Chat

Practica 4: Electron: Home Chat

  • L11: Home chat (25-Abril-2023)
  • L12: Laboratorio puro. NO hay contenido nuevo (8-Mayo-2023)

Cursos anteriores

Clone this wiki locally