Parte 1 - Sockets

por Ramiro Encinas


Diciembre 2015


¿Qué es un servidor web? ¿Qué es HTTP? ¿Qué relación tienen los servidores web con HTTP y la Web? ¿Cómo funciona un servidor web por dentro? Vamos a explorar una de las partes más interesantes e importantes de Internet con la ayuda del lenguaje Perl y GNU/Linux.

GNU/Linux es el sistema operativo más utilizado por un servidor web en Internet y Perl fue el primer lenguaje más utilizado para desarrollar páginas web dinámicas antes que cualquier otro. Perl suele venir instalado en prácticamente todas las distribuciones GNU/Linux debido a la dependencia que tienen de él para varias tareas del sistema y también es muy utilizado para desarrollar herramientas y aplicaciones que hacen un uso intensivo de redes e Internet. Además, Perl es uno de los lenguajes preferentes de los administradores de sistemas de GNU/Linux, de cualquier otro sistema tipo *NIX y es una referencia en el campo de la seguridad informática. La elección de GNU/Linux y Perl para desarrollar este documento no es trivial, pues ambos son ideales para mostrarnos de una forma simple el funcionamiento de un servidor web.

Apache, Internet Information Server, Varnish, etc., ya sabemos que son los servidores web más utilizados en Internet. Su función principal es proporcionar los recursos web solicitados por aplicaciones cliente como navegadores web y demás aplicaciones online. Un ejemplo de recurso web es este archivo html de esta página web que ahora estás leyendo.

Esta comunicación entre clientes web y servidores web es de tipo petición-respuesta de forma que el cliente web realiza la petición de un recurso web al servidor web y éste le responde incluyendo el recurso solicitado si todo es correcto. Lo que hace posible este intercambio de información es el protocolo HTTP (Hyper Text Transfer Protocol) y es el utilizado en la World Wide Web.

La World Wide Web utiliza el protocolo HTTP para hacer llegar a los navegadores web y otras aplicaciones online las páginas web y otros recursos web solicitados
Sockets de Internet

Para que un servidor web pueda recibir peticiones de clientes web, primero tiene que crear al menos un socket de Internet. Un socket sirve para comunicar entre sí a dos aplicaciones de red mediante una transmisión de paquetes. Una de las formas de crear un socket de Internet con Perl y configurarlo como un servidor web lo haría es la siguiente:

my $server = IO::Socket::INET->new(
    LocalPort => 8080,
    Type => SOCK_STREAM,
    Reuse => 1,
    Listen => 5) or die "$@\n";

En los ejemplos de código Perl que voy a utilizar en este documento, me he basado en el código publicado por littleman en este enlace. Este código es idóneo para ver de una forma simple el funcionamiento de un mini servidor web multihilo.


Si analizamos el código, en este caso utilizamos la forma orientada a objetos de Perl, invocando a IO::Socket::INET. Esto deja en la variable $server un objeto de tipo socket de Internet. INET indica que el socket utilizará la familia de direcciones AF_INET más conocida como la versión 4 del protocolo IP (son las direcciones comunes de Internet, tipo xx.xx.xx.xx de 32 bits). Con este protocolo, el socket puede localizar a los extremos implicados en la comunicación (la ip del servidor y la ip del cliente). Las propiedades de este objeto $server son:

  • LocalPort es el puerto que utilizará el socket. En este ejemplo, como estamos probando utilizamos el 8080, pero normalmente un servidor web utiliza el puerto 80. No hemos elegido el puerto 80 en este ejemplo pues es posible que ya esté asignado a algún servidor web activo en el sistema operativo, como Apache.
  • Type es el tipo de protocolo de transmisión de paquetes que utilizará el socket. SOCK_STREAM indica que utilizará el protocolo TCP (Transmission Control Protocol), aunque podría utilizar cualquier otro que logre el mismo propósito que es garantizar la entrega de paquetes.
  • Reuse tiene el valor 1 y significa que el socket podrá recibir y responder a distintos clientes (con ips distintas, claro), esto es, primero el servidor recibe una petición de una ip, la responde y después el servidor puede recibir otra petición de otra ip y la responde, todo esto utilizando el mismo socket.
  • Listen configura el socket para que permanezca a la espera de peticiones de algún cliente. En este caso el valor 5 indica el número máximo de peticiones que mantendrá en cola, esto es, el socket podrá retener hasta 5 peticiones recibidas y responderlas secuencialmente una por una hasta que termine con la última. Esta propiedad es fundamental en un servidor web, pues es una de sus funciones principales.
Perl utiliza IO::Socket::INET para comunicarse con otras aplicaciones de Internet utilizando direcciones IP versión 4

Ya tenemos el socket del servidor web preparado. Vamos a ponerlo a la escucha.

Primero necesitamos una variable donde ir dejando las conexiones que se produzcan, como por ejemplo $client. La escucha la realizaremos mediante un bucle while que aceptará peticiones cliente mediante el método $server->accept(). Al llegar una petición de algún cliente, se creará un "handler" o manipulador para esta petición y lo asignará en $client. El código relacionado es el siguiente:

my $client;

while ( $client = $server->accept()){

Dentro del while podemos acceder a la petición HTTP del cliente web mediante $client. Después, lo primero que tenemos que hacer es extraer la cabecera de petición HTTP contenida en $client, cosa que veremos en la siguiente Parte 2 - HTTP.


Parte 2 - HTTP

por Ramiro Encinas


Diciembre 2015


En la parte 1 vimos como crear un socket de Internet y configurarlo para esperar peticiones de clientes web, como lo haría un servidor web. En esta parte vamos a ver cómo el servidor web procesa las peticiones recibidas y las responde completando el recorrido petición-respuesta de una comunicación HTTP.

Como vimos antes, tenemos un while esperando las peticiones HTTP que llegan de clientes web. Con cada petición recibida se crea un "handler" o manipulador en $client y éste queda disponible dentro del while. Dentro del while vamos a extraer la cabecera de petición HTTP que está ubicada en $client. Esta cabecera de petición HTTP es fundamental para el servidor web pues indica el recurso que solicita el cliente web.

La cabecera de petición HTTP

Una cabecera de petición HTTP se compone de varias líneas de texto separadas entre sí por saltos de línea de tipo estandar (CRLF) y finaliza con una línea vacía.

Con Perl definiremos primero una nueva variable que contendrá la cabecera de la petición HTTP recibida y la llamaremos $client_headers. Después leeremos $client línea a línea mediante un while, agregando cada línea leída a $client_headers hasta llegar a una línea en blanco. Veamos el código:

my $client_headers;

while(<$client>){
    last if /^\r\n$/;
    $client_headers .= $_;
}

Con last if /^\r\n$/; indicamos que salga del while con last si encontramos una línea en blanco. La línea en blanco (CRLF) la representamos aquí mediante una expresión regular que dice: comienza(^) con un retorno de carro(\r), sigue con una nueva línea(\n) y finaliza($).

Cuando salgamos del while ya tenemos la cabecera de petición HTTP de $client en $client_headers.

La estructura de la cabecera HTTP se compone de varías líneas. La primera línea indica:

  • El método HTTP que normalmente suele ser GET o POST.
  • Un espacio en blanco.
  • La URI que es el Identificador Uniforme de Recurso que indica la ubicación del recurso solicitado en el servidor web.
  • Un espacio en blanco.
  • La versión del protocolo HTTP que soporta el cliente en la forma HTTP/versión, por ejemplo HTTP/1.1

Un ejemplo típico de esta primera línea de la cabecera HTTP puede ser:

GET /index.html HTTP/1.1

Esta línea viene a decir: dame el archivo index.html ubicado en el directorio raíz del servidor mediante la versión 1.1 del protocolo HTTP.

La primera línea de la cabecera HTTP del cliente indica qué recurso esta solicitando

El resto de líneas de la cabecera HTTP las vamos a ver al final de esta parte, en la respuesta al cliente web.


Respondiendo al cliente web

Un servidor web en producción debe analizar la cabecera de petición HTTP del cliente web y si la ha entendido, si ha localizado el recurso que indica y está disponible, debe responder al cliente web indicando que su petición ha sido aceptada y proporcionando el recurso solicitado.


Para agilizar esta parte, no vamos a analizar la cabecera de petición HTTP recibida, nos interesa verla, y vamos a verla como respuesta en el navegador web. Para ello vamos a responder directamente al cliente web (el navegador web) con la cabecera de la petición HTTP recibida.


Vamos a crear la respuesta HTTP. Esta respuesta se compone de una cabecera de respuesta HTTP, una línea en blanco de tipo estandar (CRLF) y el cuerpo de la respuesta donde irá la respuesta en sí. Como hemos dicho, en el cuerpo de la respuesta colocaremos la cabecera de la petición HTTP recibida del cliente web para verla en el navegador web.

Para crear una cabecera de respuesta HTTP mínima que pueda entender cualquier navegador web necesitamos tres líneas:

  • La primera línea se compone de la versión HTTP que utilizamos en la respuesta, un espacio, el código de respuesta que en este caso es 200 indicando que todo es correcto, un espacio y una descripción del código de respuesta: OK (no puede ser más explícito).
  • La segunda línea es de tipo Nombre: Valor e indica el tipo de contenido Content-type: que vamos a enviar en el cuerpo de la respuesta. En este caso, como el cuerpo de la respuesta es la cabecera de petición HTTP recibida y se trata de un texto, el valor adecuado para Content-type: será: text/html.
  • La tercera línea está en blanco e indica el fin de la cabecera de respuesta y el comienzo a partir de ella del cuerpo de la respuesta.

Con Perl podemos construir la cabecera de respuesta HTTP y enviarla directamente al cliente web en una sola línea de código:

print $client "HTTP/1.1 200 OK\r\nContent-type: text/html\r\n\r\n";

Como vemos, utilizamos print $client como si se tratara de escribir en un archivo. En este caso se trata de escribir en el "handler" o manipulador $client. También vemos que los saltos de línea son de tipo estandar (CRLF): \r\n, como ya vimos.

Ahora vamos a escribir en $client el cuerpo de la respuesta. Como hemos dicho, el cuerpo de la respuesta será la cabecera HTTP recibida del cliente alojada en $client_headers, y para verla mejor en el navegador web, vamos a sustituir en $client_headers los saltos de línea (\r\n) por la etiqueta de salto de línea html que es: <br>:

$client_headers =~ s/\r\n/<br>/g;

Como vemos, en la sustitución utilizamos el modificador g para que sustituya todas las ocurrencias (todos los saltos de línea). Ahora sí que podemos escribir el cuerpo de la respuesta HTTP en $client:

print $client $client_headers;

Y por último, enviamos la respuesta al navegador web cerrando el handler $client:

close($client);

Ahora ya tenemos un código completo y funcional de servidor web en Perl que realiza una comunicación HTTP completa:

#!/usr/bin/perl -w

use strict;
use IO::Socket;

my $server = IO::Socket::INET->new(
    LocalPort => 8080,
    Type => SOCK_STREAM,
    Reuse => 1,
    Listen => 5) or die "$@\n";

my $client;

while ( $client = $server->accept()){

    my $client_headers;

    while(<$client>){
        last if /^\r\n$/;
        $client_headers .= $_;
    }

    print $client "HTTP/1.1 200 OK\r\nContent-type: text/html\r\n\r\n";

    $client_headers =~ s/\r\n/<br>/g;

    print $client $client_headers;

    close($client);
}

Este código (server1.pl) está disponible aquí.

Para probarlo, guardamos este código, por ejemplo en server1.pl, lo iniciamos con perl server1.pl, vamos al navegador web e introducimos la siguiente URL:

http://localhost:8080

Si todo es correcto, en el navegador web veremos las cabeceras que envió el navegador a nuestro servidor web. Podemos parar la ejecución del servidor web con CTRL-C.

En la siguiente Parte 3 - Métodos GET y POST, veremos de forma general cómo procesar en el servidor web las peticiones GET y POST recibidas de los clientes.


Parte 3 - Métodos GET y POST

por Ramiro Encinas


Enero 2016


Estos dos métodos son los más utilizados por HTTP. GET es de propósito general y normalmente se utiliza para solicitar un recurso web y POST es el utilizado para enviar datos de un formulario a un servidor web y subir archivos.

Como hemos visto en la parte 2, la primera línea de la cabecera de una petición HTTP recibida por un servidor web indica el método, que puede ser, entre otros, GET o POST, un espacio en blanco y la URI indicando el recurso solicitado.

Adaptando el código anterior, vamos a guardar el método y el recurso solicitado de cada petición HTTP. Para ello, dentro del while que acepta las peticiones del cliente, declaramos un par de variables $method y $uri para alojar el método y el recurso (URI) de cada petición. Después leemos cada línea de la cabecera de la petición HTTP recibida. La primera línea de la cabecera de la petición debe comenzar con el método y el recurso solicitado que guardaremos respectivamente en $method y $uri:

while ( $client = $server->accept()){

    my $method = "";
    my $uri = "";

    while(<$client>){
        last if /^\r\n$/;

        if ( $_ =~ /^(GET|POST) (.*?) HTTP\// ){
            $method = $1;
            $uri = $2;
        }
    }

Como vemos, la expresión regular captura GET o POST (entre paréntesis) en $1 y el recurso que va a continuación .*? en $2.

Para ver estos valores en el navegador web, los incluiremos en una respuesta y también añadiremos un pequeño formulario POST para probar también las peticiones de este tipo:

my $resp_html =<<HTML;
HTTP/1.1 200 OK\r\nContent-type: text/html\r\n\r\n
Metodo: $method
<br>
URI: $uri
<hr>
<form action='/' method='POST'>
    <input type='text' name='val1'><br><br>
    <input type='text' name='val2'>
    <input type='submit' value='Enviar POST'>
</form>
HTML

Ya tenemos la respuesta en $html lista para enviar:

print $client $html;

close($client);

De forma que todo esto quedaría de la siguiente forma:

while ( $client = $server->accept()){

    my $method = "";
    my $uri = "";

    while(<$client>){
        last if /^\r\n$/;

        if ( $_ =~ /^(GET|POST) (.*?) HTTP\// ){
            $method = $1;
            $uri = $2;
        }
    }

    my $resp_html =<<HTML;
HTTP/1.1 200 OK\r\nContent-type: text/html\r\n\r\n
Metodo: $method
<br>
URI: $uri
<hr>
<form action='/' method='POST'>
    <input type='text' name='val1'><br><br>
    <input type='text' name='val2'>
    <input type='submit' value='Enviar POST'>
</form>
HTML

    print $client $resp_html;

    close($client);
}

El código completo (server2.pl) está disponible aquí.


Procesando peticiones POST

Ahora podemos ver el método y el recurso solicitado de cada petición HTTP, pero la situación con el método POST no ha terminado.

Si bien con el código anterior podemos ver el método y el recurso solicitado de cada GET, cuando enviamos un formulario mediante POST el recurso siempre será "/" porque así lo indica el formulario, pero no vemos los valores enviados del formulario. Esto es así debido a que ésta información no figura en la cabecera de la petición HTTP, sino en el cuerpo de la petición HTTP. El cuerpo de una petición HTTP se encuentra después de la cabecera y una línea en blanco, en un formato distinto al de la cabecera y se accede a él de forma distinta.

El cuerpo de la petición HTTP no finaliza con una línea en blanco ni con ningún otro marcador, y no tiene un límite de tamaño, quedando éste a criterio del servidor web. La forma de saber dónde finaliza el cuerpo de la petición HTTP es conociendo su tamaño en bytes.

El tamaño del cuerpo de una petición HTTP lo podemos encontrar en la línea de la cabecera de la petición HTTP que comienza con el texto Content-Length. Todas las peticiones POST deben incluir esta línea en su cabecera HTTP especificando el tamaño en bytes del cuerpo de la petición.

Veamos cómo averiguar el Content-Length: de una petición HTTP:

while ( $client = $server->accept()){

    my $method = "";
    my $uri = "";
    my $content_length = 0;

    while(<$client>){
        last if /^\r\n$/;

        if ( $_ =~ /^(GET|POST) (.*?) HTTP\// ){
            $method = $1;
            $uri = $2;
        }

        if ( $_ =~ /^Content-Length: ([0-9]+)\r\n$/ ){
            $content_length = $1;
        }
    }

Como vemos, bastaría con añadir otra variable llamada, por ejemplo $content_length y comprobar en cada línea de la cabecera HTTP si comienza con Content-Length:. Si es así, capturamos los bytes a continuación en $content_length.

Las peticiones POST deben especificar el tamaño en bytes del cuerpo de la petición mediante la línea Content-Length: de la cabecera de la petición HTTP

Ahora estamos preparados para leer el cuerpo de la petición HTTP:

my $post_body = "";
read $client, $post_body, $content_length;

Y lo hacemos mediante read. Esta instrucción lee de $client el número de bytes de $content_length y deja el resultado en $post_body.


Ahora sí podemos responder también con los valores del formulario POST:

    my $resp_html =<<HTML;
HTTP/1.1 200 OK\r\nContent-type: text/html\r\n\r\n
Metodo: $method
<br>
URI: $uri
<br>
Cuerpo POST: $post_body
<hr>
<form action='/' method='POST'>
    <input type='text' name='val1'><br><br>
    <input type='text' name='val2'>
    <input type='submit' value='Enviar POST'>
</form>
HTML
    print $client $resp_html;

    close($client);

El código completo (server3.pl) está disponible aquí.


Ya hemos visto el ciclo completo tanto de una petición GET como de una petición POST desde el punto de vista de un servidor web. En la siguiente y última Parte 4 - Multihilo veremos cómo atender cada una de estas peticiones en un proceso o hilo independiente con el objetivo de que el tiempo de respuesta sea mínimo, como lo haría cualquier servidor web en producción.


Parte 4 - Multihilo

por Ramiro Encinas


Febrero 2016


Un servidor web en producción suele utilizar un hilo o subproceso para gestionar cada petición web recibida. De esta forma, cada petición se procesa en paralelo disminuyendo el tiempo de respuesta al cliente web.

Multihilo es una característica multitarea ofrecida por los sistemas operativos a las aplicaciones para que éstas puedan crear subprocesos o procesos hijo, esto es, procesos que crean procesos para distribuir la carga de trabajo entre ellos. Los servidores y otras aplicaciones de alto rendimiento utilizan los procesos necesarios para lograr un tiempo de respuesta adecuado incluso cuando la carga de trabajo es elevada.

Cuando Perl ejecuta un proceso principal o padre puede crear otro proceso hijo mediante la función fork(). En el punto donde es llamado fork() se copia el proceso padre al nuevo proceso hijo y la ejecución continua en ambos procesos. Los manipuladores o "handlers" de ficheros o sockets abiertos en el proceso padre en el momento de la llamada también se copian en el proceso hijo.

Los servidores y otras aplicaciones de alto rendimiento utilizan los procesos necesarios para distribuir la carga entre ellos y obtener así un tiempo de respuesta adecuado

Cuando la función fork() es llamada, es necesario que la petición la atienda el proceso hijo y no el proceso padre, de forma que éste quede disponible para recibir y procesar la siguiente petición que pudiera llegar. El padre cerrará el "handler" de la petición, mientras que el proceso hijo atiende su copia de la petición y cuando finalice la cerrará también. Esto mismo puede hacerlo Perl de la siguiente forma:

while ( $client = $server->accept()){
    next if my $pid = fork(); # se duplica el proceso y bifurca padre e hijo
    # la copia del proceso en el hijo sigue por aquí
    # y procesa la petición hasta finalizar y cerrarla
    ...
    close($client);
}
continue{
    # la copia del proceso padre sigue por aquí,
    # cierra su copia de la petición
    close($client);
    # y vuelve a atender peticiones en el while
}

Lo primero que se ejecuta dentro del while es muy interesante: next if my $pid = fork();. Esto quiere decir que si llamamos a fork(), se realiza una copia del proceso actual (padre) en el proceso hijo, y éste sigue la ejecución en ese mismo punto, mientras que next hace que el proceso padre salga de este bucle concreto y se va a continue.

Como $client está tanto en el proceso padre como en el hijo, el padre cierra su copia en el continue y vuelve a esperar peticiones del while. Mientras, el proceso hijo sigue dentro del while con su copia de $client, procesa la petición, la responde y finalmente la cierra. La variable $pid tiene el retorno de fork(), que en el proceso padre es el número de proceso del proceso hijo, y en el proceso hijo tiene el valor 0.


El proceso hijo

En este momento tenemos dos procesos en memoria, el del padre y el del hijo. Vamos a ver el camino que tomará el proceso hijo.

Justo después de la llamada a fork() comienza el código que ejecutará el proceso hijo y es aconsejable comprobar si éste se ha creado correctamente. Esto lo podemos saber si $pid, que es el resultado del fork() está definido:

die "Error al crear proceso: $!\n" unless defined $pid;

Si todo es correcto, el código continúa de la misma forma que ya hemos visto, realizando las operaciones necesarias que requieran la petición, hasta que llega el momento de enviar la respuesta al cliente, donde justo antes y para evitar problemas de bloqueos y contenidos extraños con los "handlers" que pueden pasar por $client son necesarias un par de líneas:

select $client;
$| = 1;

select $client; indica que vamos a utilizar el "handler" actual de $client y no otro. Esto es necesario para evitar escribir en el "handler" equivocado, pues pueden existir varios "handlers" de $client abiertos al mismo tiempo en nuestro mismo socket, cada uno de ellos en procesos hijos distintos, con tiempos de respuesta distintos. De esta forma, se proteje al $client actual de ser modificado por otro proceso que también esté tratando su propio $client. La segunda línea $| = 1; indica que vacíe la salida de stdout que es la que utilizaremos después (con print) para escribir la respuesta. Esto es necesario pues es posible que en stdout existan contenidos de otros $client de otros procesos.

Ahora sí podemos escribir la respuesta, cerrar el "handler" $client y salir del proceso hijo:

print $client $resp_html;

close($client);

exit(fork);

Procesos zombies

Los sistemas operativos tipo UNIX tienen su propio Night of the Living Dead si un proceso crea procesos hijos y éstos completan su ejecución.

En nuestro caso, el proceso hijo finaliza con exit(fork). Al ocurrir esto, el sistema libera los recursos que tenía el proceso hijo dejándolos disponibles para otro proceso, pero no elimina la entrada del proceso hijo de la tabla de procesos, quedando el proceso en cuestión en modo "zombie". Esto es así porque el proceso padre necesita leer el estado de salida del proceso hijo. Después, el sistema elimina la entrada del proceso hijo "zombie" de la tabla de procesos.

El estado de salida del proceso hijo lo lee el padre mediante la llamada wait del sistema. En Perl podemos hacer esto añadiendo la siguiente línea de código al principio del script:

$SIG{CHLD} = sub {wait;};

Perl utiliza $SIG para gestionar señales del sistema, en este caso la señal CHLD trata los procesos hijos que pudieran crearse. Llamar a wait hace que los procesos hijos esperen a que el padre lea su estado de salida para que el sistema elimine después la entrada correspondiente del proceso hijo en la tabla de procesos.

Por último, dentro del continue después de cerrar $client en el proceso padre, necesitamos esta línea de código:

kill CHLD => -$$;

De esta forma, si por alguna situación especial el sistema no elimina la entrada del proceso hijo "zombie" de la tabla de procesos, la línea de código anterior se asegura de ello.

En el último ejemplo de código server4.pl utilizamos además sleep 10 para demorar la respuesta al cliente de forma que podamos ver varios procesos hijos a la vez y su posterior eliminación.


Finalizando

Hemos visto conceptos clave del funcionamiento básico de un servidor web: cómo crear un socket y ponerlo a la escucha, cómo hablar HTTP de forma básica y cómo utilizar un proceso por cada petición como lo haría un servidor web en producción. Si bien, un servidor web en producción tiene muchas más líneas de código, los conceptos fundamentales son los mismos y también nos pueden servir para aplicarlos en cualquier otro tipo de servidor.