cowboy: servidor pequeño, rápido y modular

Hace poco que le llevo siguiendo la pista a este framework para desarrollo en Erlang. cowboy se ha convertido, para mi, en una referencia a nivel de sistemas de inversión de control en Erlang, ya que son los únicos que he visto, hasta el momento (aparte de ciertas partes de código dentro de ejabberd) que usan los behaviours para extender funcionalidad.

La librería cowboy se define como un pool de servidores TCP. Por defecto viene con manejadores HTTP para: request, websocket, static y REST; pero también se pueden crear otros manejadores para otros servidor TCP como SMTP.

En esta entrada me basaré en la base de un manejador típico de HTTP, revisando lo que podemos encontrar a este nivel. Si deseas ver un ejemplo de websocket, en el Recetario de ErlAr puedes encontrar uno bastante completo.

Manejador HTTP

Vamos a comenzar por ver la plantilla que necesitamos implementar para hacer el manejador HTTP en cowboy. Tiene esta pinta:

-module(mi_modulo).
 
-behaviour(cowboy_http_handler).
 
-define(SERVER, ?MODULE).
 
%% behaviour callbacks
-export([init/3, handle/2, terminate/2]).
 
%%====================================================================
%% http_handler callbacks
%%====================================================================
 
init({tcp, http}, Req, _Opts) ->
    {ok, Req, undefined}.
 
handle(Req, State) ->
    {ok, Req2} = cowboy_http_req:reply(200, [], <<"Hello world!">>, Req),
    {ok, Req2, State}.
 
terminate(_Req, _State) ->
    ok.
 
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------

Al momento de iniciar el manejador se ejecuta init, a la salida o finalización del servicio, se ejecuta terminate. Para cada petición que llega al servicio se ejecuta handle.

Se pueden agregar tantos manejadores como se desee, por lo que no es necesario programar uno extenso que lo haga todo, sino que se puede hacer uno por servicio o URI que se desee programar y, desde el lanzador del servidor, programar las rutas que lleven a uno u otro manejador.

Para que esto sea posible, hay que lanzar el manejador HTTP de cowboy con su dispatch correspondiente. Esto se suele realizar desde la función start de una aplicación y suele tener la siguiente forma:

%% {Host, list({Path, Handler, Opts})}
Dispatch = [{'_', [
    {'_', mi_modulo, []}
]}],
%% Name, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts
cowboy:start_listener(mi_server, 100,
    cowboy_tcp_transport, [{port, 8080}],
    cowboy_http_protocol, [{dispatch, Dispatch}]
),

Rutas y dominios

Como podemos ver, el Dispatch es una entidad bastante completa que nos permite, en un principio, configurar las rutas por host incluso. Como se agrega el átomo ‘_’, esto hace de comodín para cualquier host. Como está dentro de una lista, se pueden agregar tantas líneas de host como se desee.

La segunda parte de la tupla del host son las rutas. El parámetro inicial de búsqueda es la ruta o URI, en este caso, igual que en el host, podemos usar el átomo ‘_’ como comodín, es decir, para cualquier URI, usamos esa línea (como en el ejemplo expuesto arriba). Igualmente está dentro de una lista, por lo que podríamos agregar más tuplas como esta.

La tupla de la URI se completa con manejador y opciones, como segundo y tercer elemento de la tupla.

El formato de las rutas y host se establece en listas de listas binarias, es decir, si queremos emplear una ruta que sea /mi/codigo, deberemos de escribirlo así:

[<<"mi">>, <<"codigo">>]

Igual pasa con los dominios, se parte por los puntos, de modo que si queremos usar el dominio www.google.es, debemos de escribirlo así:

[<<"www">>, <<"google">>, <<"es">>]

Podemos usar comodines como los átomos ‘_’ o ‘…’, que nos permitirán concordar con un elemento (el caso del ‘_’) o con tantos elementos como se encuentren antes, después o entre los datos indicados (el caso de ‘…’):

[ '_', <<"google">>, <<"es">> ]
[ <<"mi">>, <<"codigo">>, '...' ]

Veamos un ejemplo para el dispatch. En el caso de que tuviésemos al manejador my_images para la ruta /img, el manejador my_code para la ruta /code y el manejador about para la ruta /about, esto para el host bosqueviejo.com y bosqueviejo.org, mientras que bosqueviejo.net lo dejamos con un manejador code para todo:

Paths = [
    {[<<"img">>], my_images, []},
    {[<<"code">>], my_code, []},
    {[<<"about">>], about, []}
],
Dispatch = [
    {[<<"bosqueviejo">>, <<"org">>], Paths},
    {[<<"bosqueviejo">>, <<"com">>], Paths},
    {[<<"bosqueviejo">>, <<"net">>], [{'_', code, []}]}
],

Información de la petición

Dentro del manejador, obtenemos la información a través del parámetro Req que, aunque es un registro, en la web de cowboy se recomienda emplear el módulo cowboy_http_req para la extracción de los datos. Los datos que podemos obtener a través de las funciones y pasando como parámetro Req, son los siguientes:

method
Obtiene el método de la petición, pudiendo ser GET, PUT, POST, DELETE, … o cualquier otro que permita el estándar HTTP.
version
La versión de HTTP empleada para la comunicación.
peer
peer_addr
La dirección IP y puerto remoto. En caso de peer_addr este dato se obtiene de la cabecera X-Forwarded-For en caso de que esté presente.
host
host_info
raw_host
Retorna los tokens (lista de listas binarias, como en el dispatch) del host al que se realiza la petición. El caso de raw_host, da el host tal y como aparece en la petición.
port
El puerto del servidor en el que se recibió la solicitud.
path
path_info
raw_path
Siguiendo el RFC2396, esta función retorna los segmentos de la ruta (en lista de listas binarias, como en el dispatch). En caso de raw_path, se da la ruta tal y como se envió en la solicitud.
qs_val
qs_vals
raw_qs
Retorna un valor de la consulta (la parte de query de la URL) dada su clave para su búsqueda en caso de qs_val y todos los valores, en caso de qs_vals. La query tal cual se envió se obtiene con raw_qs.
body
body_qs
Permiten obtener el contenido (en caso de POST o PUT, por ejemplo), y la variante body_qs permite interpretar el contenido en caso de que sean datos de formulario.
cookie
Permite obtener los datos de las cookies.
header
headers
Permite obtener los datos de las cabeceras o una cabecera en concreto.

Hay muchas más funciones que nos pueden ayudar a obtener más información sobre la petición, no obstante, la documentación es escasa, por lo que es mejor acceder al código (que está bien documentado) y revisar las funciones, así como hacer pruebas para comprobar que los datos son los que queremos o hemos entendido de lo leído.

Respuestas

Para responder a una petición también emplearemos el módulo cowboy_http_req, a través de la función reply. Esta función acepta 2 o 3 parámetros. El primero es fijo, y se trata del estado, que normalmente será 200, a menos que queramos realizar un 302, 404 o 500, por ejemplo.

El segundo parámetro puede ser el cuerpo del mensaje de respuesta, en caso de que solo haya dos parámetro a indicar, o una lista con las cabeceras (deben de estar en una lista de tuplas, siendo los elementos que componen la tupla dos y de tipo lista binaria). El tercer parámetro sería el cuerpo de la respuesta.

Si lo que se quiere enviar es un fichero, se puede preparar el envío a través de la función set_resp_body, así como si queremos incluir alguna cabecera podemos emplear set_resp_header o para agregar alguna cookie usar set_resp_cookie.

Conclusiones

Con lo expuesto en la plantilla y el primer código del dispatch se puede programar sin mucho problema un Hola mundo! básico, por lo que, ya solo sería cuestión de trabajar con las posibilidades que nos brinda.

En sí, el manejador base HTTP es bastante simple y potente, nos permite un uso a bajo nivel del protocolo HTTP, pero de una forma muy simple a nivel de lenguaje. Si repasásemos el uso de REST, por ejemplo, veríamos que tener el background de un sistema como Rails, sería muy simple de obtener (lo que concierne a las rutas y el acceso a los controladores, que serían los manejadores base), con lo que, junto con ErlyDTL y Hiberl, por ejemplo, tenemos un framework web bastante simple de utilizar y muy potente.