Erlang: Servidores UDP

Una de las grandes potencias de Erlang es su capacidad para construir servidores. A través de OTP, esta tarea se convierte en algo tan sencillo, que asombra ver con qué pocas líneas de código se crea un servidor.

En este caso vamos a montar un servidor UDP. Esto lo revisé en su día porque estuve realizando un pequeño servidor para el protocolo syslog, de lo que resultó este servidor, que permite pasar a través de un manejador de eventos (gen_event) los mensajes de syslog recibidos a través del puerto 514 en formato UDP.

En principio, comentaré un poco la estructura del comportamiento (behaviour) de Erlang/OTP que emplearemos: gen_server.

Empleando los gen_server

El gen_server es el comportamiento más básico que se encuentra disponible en OTP, ya que sobre él se construyen otros como gen_fsm. Este comportamiento específico se fundamenta en la recepción de mensajes y su manejo a través de unos callbacks, de modo que tenemos tres tipos de mensajes que podemos procesar:

  • call: referidos a llamadas en las que se envía una petición y se espera por una respuesta del servidor. Siendo realizadas desde otros procesos de Erlang.
  • cast: estas son llamadas que se envían y no se espera por una respuesta. También son realizadas desde otros procesos de Erlang.
  • info: este podría considerarse un comodín, ya que en esta categoría entran todos los mensajes que no entran en las anteriores. Normalmente, aquí es donde recibiremos la información UDP y TCP de los servidores que creemos de cara a la red.

La estructura de un código con gen_server es la siguiente:

-module(server).
-author('bombadil@bosqueviejo.net').
 
-behaviour(gen_server).
 
-define(SERVER, ?MODULE).
 
-export([start_link/]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3]).
 
-record(state, {}).
 
start_link() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
 
init([]) ->
    {ok, #state{}}.
 
handle_call(_Request, _From, State) ->
    Reply = ok,
    {reply, Reply, State}.
 
handle_cast(_Msg, State) ->
    {noreply, State}.
 
handle_info(_Info, State) ->
    {noreply, State}.
 
terminate(_Reason, _State) ->
    ok.
 
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

Si guardamos esta estructura como server.erl (que es el nombre del módulo, tal y como se indica en la primera línea), el sistema compilará y lo podremos incluso lanzar con el comando:

server:start_link().

Convirtiendo nuestro gen_server en servidor UDP

Para convertir nuestro gen_server en un servidor UDP solo necesitamos abrir en escucha el puerto que queramos en modo UDP a través de gen_udp y el resto lo hará la estructura gen_server. Sería tan simple como modificar la función init para que albergase el siguiente código:

init([]) ->
    Port = 5000,
    {ok, Socket} = gen_udp:open(Port),
    error_logger:info_msg("Listen in port ~p~n", [Port]),
    {ok, #state{socket=Socket}}.

Y también la estructura del registro de estado, para albergar la información que necesitamos almacenar en el proceso, que será el recurso del puerto abierto:

-record(state, {socket}).

Es bueno que, además, agregemos en el terminate la cláusula que nos permita cerrar el servidor UDP, esto lo podemos agregar modificando la función termiante de la siguiente forma:

terminate(_Reason, State) ->
    gen_udp:close(State#state.socket),
    ok.

Por último, vamos a hacer que cada mensaje que nos llegue desde el servidor UDP se presente a modo de log en la consola. Esto lo haremos modificando la función handle_info, que como habíamos visto antes, es la que recibe los mensajes UDP:

handle_info(Info, State) ->
    error_logger:info_msg("Received via INFO: ~p~n", [Info]),
    {noreply, State}.

Probando el servidor

Para probarlo, solo tenemos que ejecutar en una consola:

$ erl 
Erlang R15B (erts-5.9) [source] [64-bit] [smp:4:4] [async-threads:0] [kernel-poll:false]
 
Eshell V5.9  (abort with ^G)
1> server:start_link().
 
=INFO REPORT==== 21-Mar-2012::10:52:44 ===
Listen in port 5000
{ok,<0.34.0>}

Abrimos otra consola, y ejecutamos el siguiente comando para generar un paquete UDP:

$ nc -u localhost 5000
hola

NOTA: tras escribir hola hay que presionar el retorno de carro (enter o intro) para que el paquete sea enviado.

Esto generará en la consola de Erlang el siguiente mensaje:

=INFO REPORT==== 21-Mar-2012::10:54:01 ===
Received via INFO: {udp,#Port<0.506>,{127,0,0,1},36772,"hola\n"}

Empleando el match en la función handle_info e incluso con expresiones regulares o lo que queramos, podremos tratar de forma más adecuada el mensaje.

Un ejemplo de aplicación de este servidor, como comenté al principio, se puede ver en este proyecto que comencé hace tiempo, un servidor de syslog, que se basa en la recepción de mensajes por el puerto UDP 514, y el envío, a través de gen_event de los mensajes recibidos.

Conclusiones

Con este ejemplo, Erlang demuestra que es muy fácil construir servidores de red, con muy pocas líneas de código. En este ejemplo, el hecho de ser un servidor UDP permite eliminar el payload de la escucha, aceptación y mantenimiento de conexión con el destinatario, simplemente se acepta el mensaje, se lee y procesa, muy simple y con un buen rendimiento del sistema.