aprendiendo ( Erlang ).

domingo, 3 de junio de 2012

Gen_tcp I. Comunicándonos con el exterior.

| 0 comentarios |

Ya vimos, en su momento como montar un servidor genérico (gen_server). Esta librería nos dotaba, como podíamos comprobar, de unas serie de características muy deseables en un servidor. Pero … como seguro que adivinaste en su momento, sólo puede comunicarse con el mundo Erlang. Se trata de un servidor interno que no puede comunicarse con el exterior de forma natural. Para estos menesteres, Erlang nos provee de librerías como gen_tcp o gen_udp entre otras. Si, es esa la pregunta … ¿podemos combinar los dos comportamientos? No sólo se pueden combinar sino que en determinados ambientes será lo deseable.

Esta combinación la dejaremos para más adelante, ahora nos vamos a centrar en la comunicación con el exterior y más concretamente en la librería gen_tcp. Para ilustrar el comportamiento y pormenores de esta librería vamos a implementar un servidor de hora y su correspondiente cliente.

El cliente tcp

Lo primero que tenemos que pensar es el comportamiento que deseamos en nuestro cliente. Yo, por mi parte he decidido implementar tres funciones:

  • start/2: encargada de conectarse con el servidor y devolvernos el socket con el que nos vamos a comunicar con él.
  • get_time/1: que pide muy amablemente la hora al servidor para devolvérnosla.
  • stop/1: encargada de cerrar el socket del cliente cuando estemos en disposición de hacerlo.

Un poco de código para ir abriendo los pulmones:

-module(time_client).
-export([start/2, get_time/1, stop/1]).

% Opciones para el servidor tcp
-define(TCP_OPTIONS, [binary, {packet, 4}]).


start(Host, Port) ->
    {ok,Socket} = gen_tcp:connect ( Host,Port, ?TCP_OPTIONS ),
    io:format("Cliente conectado al ~p~n", [Socket]),
    Socket.

get_time(Socket) ->
    ok = gen_tcp:send ( Socket, list_to_binary("Por poner algo") ),
    io:format("Cliente envio realizado~n", []),
    receive
        {tcp,Socket,Bin} ->
            io:format("Cliente-receive: ~n", []),
            Time = binary_to_term(Bin),
            io:format("Cliente: ~p~n" ,[Time]);
        Otro ->
            io:format("Otro: ~p~n", [Otro])
    end.

stop(Socket) ->
    gen_tcp:close(Socket),
    io:format("Cliente cerrado~n" ).

Habrás apreciado que el código es simple y se ve claramente su funcionamiento. Si vamos por orden, lo primero que te salta a la vista es la macro TCPOPTIONS que he definido. Esta macro define una lista con los parámetros de configuración para la comunicación y comportamiento que deseamos de la librería gen_tcp. Podrás encontrar más información en la documentación oficial de Erlang sobre el TCP options.

Sigamos avanzando y veremos la llamada de conexión gen_tcp:connect/3, que dado un servidor, un puerto de comunicaciones y una lista de opciones, nos permite abrir la conexión con el servidor TCP. En caso de fallo, la función gen_tcp:connect/3, devolverá el típico {error, Motivo}.

Una vez que tenemos un socket en nuestro poder ya podemos utilizar a nuestro antojo. En la función, get_time/1, realizamos el envío de la información con la función gen_tcp:send/2, que espera el socket y los datos a enviar, y esperamos la respuesta con una sentencia receive. El mensaje de recepción, como te habrás dado cuenta enseguida, tiene el formato de {tcp, Socket, Datos}.

Por último, el cierre del socket se realiza con la función gen_tcp:close/1. Esta función recibe el socket que pretendemos cerrar.

El servidor arias "el monolítico".

Y para comenzar, con el lado del servidor, vamos a implementar un servidor sencillo y sobre él, iremos realizando pequeñas modificaciones para llegar a una versión correcta y deseable. Veamos nuestra primera implementación de un servidor tcp:

-module(time_server).
-export([start/1]).

% Opciones para el servidor tcp
-define(TCP_OPTIONS, [binary, {packet, 4}, {active, true}, {reuseaddr, true}]).

% Función de arranque del servidor
start(Port) ->
    {ok, ServerSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS),

    io:format("Servidor Acceptando~n"),
    {ok, ClientSocket} = gen_tcp:accept(ServerSocket),
    io:format("Aceptado socket: ~p~n", [ClientSocket]),

    io:format("Cerrando socket servidor: ~p~n", [ServerSocket]),
    gen_tcp:close(ServerSocket),

    io:format("Servidor reciviendo~n"),
    receive
        {tcp, ClientSocket, Bin} ->
            io:format("Servidor: ~p~n" ,[binary_to_list(Bin)]),
            gen_tcp:send(ClientSocket, term_to_binary(time()) );
        {tcp_closed, ClientSocket} ->
            io:format("Servidor cerrado~n");
        Otro ->
            io:format("Otro: ~p~n", [Otro])
    end.

Como ya habrás visto, se trata de un servidor monolítico que haremos evolucionar hasta tener un servidor con funcionamiento paralelo y totalmente operativo. Quizás este ejemplo, que siempre acaba borrado, es el más simple para ilustrar el comportamiento de un servidor TCP. Si lo vemos, desde un punto de vista lineal sería algo así…

Visión líneal de un servidor TCP

Con la función gen_tcp:listen/2 abrimos nuestro puerto para escuchar y aceptar conexiones. Una vez que hemos aceptado la conexión del cliente, con la función gen_tcp:acept/1, pasamos a esperar a que lleguen las peticiones de los clientes para despacharlas.

Veamos como se comporta nuestro servidor y nuestro cliente. Para ello, abriremos dos terminales. Uno para el cliente y otro para el servidor de forma que veamos como se comportan cada uno de ellos.

(servidor@maquina_verdi)1> c(time_server).
{ok,time_server}
(servidor@maquina_verdi)2> time_server:start(9999).
Servidor Acceptando
Aceptado socket: #Port<0.2081>
Cerrando socket servidor: #Port<0.2080>
Servidor reciviendo
Servidor: "Por poner algo"
ok

(cliente@maquina_verdi)1> c(time_client).
{ok,time_client}
(cliente@maquina_verdi)2> S = time_client:start("localhost", 9999).
Cliente conectado al #Port<0.2081>
#Port<0.2081>
(cliente@maquina_verdi)3> time_client:get_time(S).                 
Cliente envio realizado
Cliente-receive: 
Cliente: {21,12,12}
ok
(cliente@maquina_verdi)4> time_client:stop(S).
Cliente cerrado
ok

Sí, cierto, el servidor es bloqueante, y por ello, una vez que has entendido el comportamiento básico de un servidor TCP pasaremos a hacerlo de verdad. Vamos a parelizarlo.

Paralelizando el servidor.

Lo primero que comprobamos con el servidor monolítico es que sólo, y exclusivamente, aceptará una única conexión y recibirá de ella, un único mensaje. Sin lugar a dudas, inoperativo. Pues viendo esto, sólo podemos hacer una cosa, reescribirlo. Para empezar, vamos a retomar nuestro esquema y le vamos a añadir alguna cosilla.

Visión líneal de un servidor TCP

Aunque he intentado que el esquema sea lo suficientemente aclaratorio no hay nada como un poco de código clarificador.

-module(time_server2).
-export([start/1]).

% Opciones para el servidor tcp
-define(TCP_OPTIONS, [binary, {packet, 4}, {active, true}, {reuseaddr, true}]).

% Función de arranque del servidor
start(Port) ->
    {ok, ServerSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
    spawn (fun() -> accept(ServerSocket) end).

accept(ServerSocket) ->
    io:format("Servidor Aceptando~n"),
    {ok, ClientSocket} = gen_tcp:accept(ServerSocket),
    io:format("Aceptado socket: ~p~n", [ClientSocket]),
    loop(ClientSocket),
    accept(ServerSocket).

loop (ClientSocket) ->
    io:format("Servidor recibiendo de ~p~n", [ClientSocket]),
    receive
        {tcp, ClientSocket, Bin} ->
            io:format("Servidor: ~p~n" ,[binary_to_list(Bin)]),
            gen_tcp:send(ClientSocket, term_to_binary(time()) ),
            loop(ClientSocket);
        {tcp_closed, ClientSocket} ->
            io:format("Servidor cerrado cliente ~p~n", [ClientSocket]);
        Otro ->
            io:format("Otro: ~p~n", [Otro])
    end.

A poco que te fijes, te darás cuenta del problema que presenta este modelo. Efectivamente, sigue siendo inoperativo. Y esto es así por que el servidor acepta una conexión o cliente, atiende todas sus peticiones hasta que este cierre, pero sólo a un cliente a la vez. Veamos el funcionamiento del servidor en consola:

(servidor@maquina_verdi)2> time_server2:start(9999).
Servidor Aceptando
<0.45.0>
Aceptado socket: #Port<0.2076>
Servidor recibiendo de #Port<0.2076>
Servidor: "Por poner algo" 
Servidor recibiendo de #Port<0.2076>
Servidor: "Por poner algo" 
Servidor recibiendo de #Port<0.2076>
Servidor cerrado cliente #Port<0.2076>
Servidor Aceptando         
Aceptado socket: #Port<0.2077>
Servidor recibiendo de #Port<0.2077>
Servidor: "Por poner algo" 
Servidor recibiendo de #Port<0.2077>
Servidor cerrado cliente #Port<0.2077>
Servidor Aceptando 

Y ahora, el salto definitivo. Hasta ahora nuestro servidor sólo atiende a un cliente a la vez, ahora vamos modificar un poco el código:

-module(time_server3). 
-export([start/1]). 

% Opciones para el servidor tcp
-define(TCP_OPTIONS, [binary, {packet, 4}, {active, true}, {reuseaddr, true}]).

% Función de arranque del servidor
start(Port) ->
    {ok, ServerSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
    spawn (fun() -> accept(ServerSocket) end).

accept(ServerSocket) ->
    io:format("Servidor Aceptando~n"),
    {ok, ClientSocket} = gen_tcp:accept(ServerSocket),
    io:format("Aceptado socket: ~p~n", [ClientSocket]),
    spawn (fun() -> accept(ServerSocket) end),
    loop(ClientSocket).

loop (ClientSocket) ->
    io:format("Servidor recibiendo de ~p~n", [ClientSocket]),
    receive
        {tcp, ClientSocket, Bin} ->
            io:format("Servidor: ~p~n" ,[binary_to_list(Bin)]),
            gen_tcp:send(ClientSocket, term_to_binary(time()) ),
            loop(ClientSocket);
        {tcp_closed, ClientSocket} ->
            io:format("Servidor cerrado cliente ~p~n", [ClientSocket]);
        Otro ->  
            io:format("Otro: ~p~n", [Otro])                            
    end. 

No, no me estoy quedando con vosotros. Sólo hay que cambiar dos líneas para que un servidor poco o casi nada eficiente pase a ser un servidor paralelo. Hemos con vertido un servidor monolítico y sin futuro en un servidor eficiente capaz de servir la hora de tu ordenador a cualquier parte del mundo.

Publicar un comentario en la entrada

0 comentarios:

 
Licencia Creative Commons
Aprendiendo Erlang por Verdi se encuentra bajo una Licencia Creative Commons Atribución-NoComercial-CompartirIgual 3.0 Unported.