Mostrando entradas con la etiqueta programación. Mostrar todas las entradas
Mostrando entradas con la etiqueta programación. Mostrar todas las entradas

domingo, 14 de febrero de 2016

Introducción a POE para novatos


Sobre este texto

Este texto es una adaptación libre del texto original "A beginner's introduction to POE" de Dennis Taylor y Jeff Goff.

Qué es POE y porqué debería usarlo?

La mayoría de los programas que nosotros escribimos cada día tiene el mismo esqueleto básico: la inicialización, la realización de una serie de acciones y la finalización. Esto trabaja bien para programas que no necesitan mucha interacción con otros usuarios o datos, pero para tareas más complicadas, se necesita una estructura de programa más expresiva.
Ahí es donde POE (Perl Object Environment) viene a ayudar. POE es un framework para construir programas Perl que llevan en su naturaleza tareas que involucran reaccionar ante datos externos, tales como una red de comunicaciones o interfaces de usuario. Los programas escritos en POE son completamente no-lineales; se inicializa un conjunto de pequeñas subrutinas y se define cómo se llaman entre sí, y POE automáticamente va cambiando entre ellas mientras maneja la entrada y salida del programa. Puede ser confuso al principio, si estás acostumbrado a la programación procedural, pero con un poco de práctica se vuelve una segunda naturaleza.

Diseño POE

No es exagerado decir que POE es un pequeño sistema operativo escrito en Perl, con su propio kernel, procesos, comunicación entre procesos (IPC), drivers, etc. En la práctica, sin embargo, lo podemos describir como un simple sistema para ensamblar máquinas de estado. He aquí una breve descripción de cada una de las piezas que conforman un entorno POE:

Estados

El bloque de construcción básico de un programa POE es el estado, es la pieza de código a ser ejecutada cuando un evento ocurre, por ejemplo cuando arrivan datos o una sesión tiene cosas que hacer o cuando una sesión envía un mensaje a otra. Todo en POE está basado alrededor de recibir y manejar estos eventos.

El kernel

El kernel de POE es muy parecido al kernel de un sistema operativo: guarda la pista de cada proceso y dato que está detrás de escena y maneja cada pieza de tu código para que corra. Se puede usar el kernel para setear alarmas para los procesos POE, encolar estados que se quieran correr y realizar otros servicios varios de bajo nivel, pero casi nunca se interactúa con el kernel directamente.

Sesiones

Las sesiones son para POE el equivalente de los procesos en un sistema operativo real. Una sesión es sólo un programa POE con cambios de un estado a otro mientras ejecuta. Puede crear sesiones hijas, enviar eventos POE a otras sesiones, etc. Cada sesión puede almacenar datos específicos de la sesión en un hash llamado heap. La heap es accesible desde cada estado en a sesión. POE tiene un muy simple modelo multitasking cooperativo: cada sesión ejecuta en el mismo proceso SO sin threads ni forking. Por esta razón se debería cuidar de no usar system calls bloqueantes en los programas POE.
Estas son las piezas básicas de POE, sin embargo quedan unas pocas algo más avanzadas que explicaremos antes de ir a ver un código real.

Drivers

El más bajo nivel de la capa I/O de POE es Drivers. Actualmente hay sólo un driver incluído en la distribución POE (POE::Driver::SysRW lee y escribe datos desde un manejador de archivos) y no hay mucho para decir sobre él. Además, nunca se utiliza un driver directamente.

Filters

Por otro lado, Filters es extraordinariamente útil. Un filtro es una simple interface para convertir trozos de datos formateados hacia otro formato. Por ejemplo POE::Filter::HTTPD convierte requerimientos HTTP 1.0 en objetos HTTP::Request y viceversa, y POE::Filter::Line convierte un flujo de datos en una serie de líneas (parecido al operador <> de Perl).

Wheels

Los Wheels contienen piezas reutilizables de lógica de alto nivel para cumplir con tareas rutinarias. Ellos son la manera de encapsular código útil en POE. Las cosas comunes que se hacen con wheels en POE incluyen manejar entrada y salida conducida por eventos y crear conexiones de red de una manera fácil. A menudo se utilizan con Filters y Drivers para manejar y enviar los datos. Esta descripción es un tanto vaga, pero el código de más adelante provee ejemplos más concretos.

Componentes

Un componente es una sesión que está diseñada para ser controlada por otras sesiones. Las sesiones pueden enviar comandos al componente y recibir eventos de ellos, parecido a como un sistema operativo se comunica con sus procesos vía IPC. Algunos ejemplos de componentes son POE::Component::IRC, una interface para crear clientes IRC basados en POE, y POE::Component::Client::HTTP, un agente de usuario HTTP manejado por eventos. No usaremos Componentes en este tutorial, pero son una parte muy útil de POE.

Un ejemplo sencillo

En este ejemplo se hará un servidor demonio que acepta conexiones TCP e imprime la respuesta a un problema de aritmética simple que le envía un cliente. Cuando alguien se conecta al puerto 31008 mostrará en pantalla "Hola Cliente!". El cliente podrá enviar la expresión aritmética, terminada con un enter (por ejemplo '6 + 3 \n' o '50 / (7 - 2) \n', y el servidor retornará la respuesta. Sencillo, no?
Escribir tal programa en POE es tremendamente diferente al método tradicional de escribir demonios en Unix. Tendremos una sesión servidor escuchando por conexiones TCP entrantes en el puerto 31008. Cada vez que llegue una conexión, se creará una sesión hijo para manejar esa conexión. Cada sesión hijo interactuará con el usuario y en forma silenciosa muere cuando la conexión se cierra. La mejor parte es que solo toma 74 líneas de modular y simple Perl.
En forma inocente, el programa comienza así:

#!/usr/bin/perl -w
 use strict;
 use Socket qw(inet_ntoa);
 use POE qw( Wheel::SocketFactory  Wheel::ReadWrite
             Filter::Line          Driver::SysRW );
 use constant PORT => 31008;


Aquí importamos los módulos y las funciones que el programa usará. Definimos un valor constante para escuchar el puerto. Usamos qw() como un atajo para traer los módulos de POE que necesitamos en una sola línea. La manera equivalente de hacer esto mismo es:
use POE;
use POE::Wheel::SocketFactory;
use POE::Wheel:ReadWrite;
use POE::Filter::Line;
use POE::Driver::SysRW;

Ahora viene la parte buena:

POE::Session->create ( 
  inline_states => {
    _start => \&server_start,
    accept_new_client => \&accept_new_client,
    accept_failed => \&accept_failed,
    _stop  => \&server_stop,
  }
);
$poe_kernel->run();
exit;

Ese es el programa entero! Seteamos la sesión servidor principal, le decimos al kernel de POE que comience a procesar los eventos y entonces se hace un exit. El kernel considera que se acaba el programa cuando no quedan más sesiones que manejar, pero como pondremos un loop infinito dentro de la sesión, entonces nunca terminará de ejecutar. POE automáticamente exporta la variable $poe_kernel hacia el namespace del script cuando se ejecuta "use POE;".
La llamada a POE::Session->create necesita unas palabras de explicación. Cuando se crea la sesión se le da al kernel una lista de eventos que él aceptará. En el código de ejemplo le estamos diciendo que manejará los eventos start, accept_new_client, accept_failded y _stop llamando a las funciones server_start(), accept_new_client(), accept_failded() y server_stop() respectivamente. Cualquier otro evento que la sesión reciba será ignorado. Los eventos _start y _stop son especiales para POE: el estado _start es la primer cosa que ejecuta una sesión cuando es creada, y la sesión es pasada al estado _stop cuando el kernel está a punto de destruirla. Es decir, _start y _stop son básicamente un constructor y un destructor.
Ahora que hemos escrito el programa entero, tenemos que escribir código para los estados que nuestra sesión ejecutará mientras corre. Comenzaremos con server_start(), el cual se llama cuando la sesión servidor principal es creada al principio del programa:

sub server_start {
    $_[HEAP]->{listener} = new POE::Wheel::SocketFactory
      ( BindPort     => PORT,
        Reuse        => 'yes',
        SuccessEvent => 'accept_new_client',
        FailureEvent => 'accept_failed'
      );
    print "SERVER: Started listening on port ", PORT, ".\n";
}


Es un buen ejemplo de un estado en POE. Primero lo primero: notas la variable $_[HEAP]? POE tiene una manera especial de pasar argumentos. El array @_ es armado con montones de argumentos extra (una referencia al kernel actual y la sesión, el nombre del estado, una referencia a la heap y otras cosas). Para accederlas se indexa la variable @_ con varias constantes especiales que POE exporta, tales como HEAP, SESSION, KERNEL, STATE y ARG0 hasta ARG9 para acceder a los argumentos de estado del usuario. Como casi todas las decisiones de diseño de POE, el punto de este esquema es maximizar la compatibilidad hacia versiones anteriores sin perder en velocidad. El ejemplo de arriba almacena un wheel SocketFactory en la heap bajo la clave 'listener'.
El wheel POE::Wheel::SocketFactory es una de las cosas más sabrosas de POE. Se puede usar para crear cualquier tipo de socket (perdón, todavía no puede crear sockets UDP) sin preocuparse por los detalles. La declaración del ejemplo crea un SocketFactory que escucha sobre un puerto TCP específico (con la opción SO_REUSE seteada) para nuevas conexiones. Cuando una conexión es establecida éste llamará al estado accept_new_client() para pasar el nuevo cliente de socket; si algo va mal, llamará al estado accept_failed() para manejar el error. Eso es todo lo que hay de redes en POE!
Se almacena la wheel en la heap para salvarla de que accidentalmente sea recogida por el garbage-collector al finalizar el estado (de esta manera persiste entre todos los estados de la sesión). Ahora vamos por el estado server_stop():

sub server_stop {
    print "SERVER: Stopped.\n";
}


No hace mucho. Solamente ilustra el flujo del programa cuando corre. Podríamos no tener un estado _stop, pero es mucho más instructivo (y fácil de debuggear) de esta manera.
Ahora crearemos nuevas sesiones para las conexiones que llegan:


sub accept_new_client {
    my ($socket, $peeraddr, $peerport) = @_[ARG0 .. ARG2];
    $peeraddr = inet_ntoa($peeraddr);
    POE::Session->create(
      inline_states => {
        _start      => \&child_start,
        _stop       => \&child_stop,
        child_input => \&child_input,
        child_done  => \&child_done,
        child_error => \&child_error,
      },
      args => [ $socket, $peeraddr, $peerport ],
    );
    print "SERVER: Got connection from $peeraddr:$peerport.\n";
}


Nuestro POE::Wheel::SocketFactory llamará a esta subrutina si se establece una conexión satisfactoria con un cliente. La instrucción inet_ntoa se usa para convertir la dirección del socket en una dirección IP legible para humanos. Seteamos una nueva sesión para hablar con el cliente. Es similar al constructor anterior, pero tiene un par de cosas que explicaremos:
@_[ARG0 .. ARG2] es un atajo para ($_[ARG0],$_[ARG1],$_[ARG2]). Verás este tipo de slices de array muchas veces en los scripts POE.
Finalmente, la referencia al array al final de la lista de argumentos del constructor es la lista de argumentos que serán pasados manualmente al estado _start de la sesión.
Si hay problemas creando el socket de entrada o aceptando una conexión, el POE::Wheel::SocketFactory hará ésto:
sub accept_failed {
    my ($function, $error) = @_[ARG0, ARG2];
    delete $_[HEAP]->{listener};
    print "SERVER: call to $function() failed: $error.\n";
}


Imprimir un mensaje de error es lo normal, pero porqué borramos el wheel SocketFactory de la heap? La respuesta tiene que ver con la manera en que POE maneja los recursos sesión. Cada sesión es considerada "viva" mientras tenga manera de generar o recibir eventos. Si no tiene wheels o alias (una atractiva característica de POE que no veremos en este tutorial) el kernel POE se da cuenta que la sesión está muerta y el garbage-collector se la lleva. La única forma de que la sesión server pueda tener eventos es desde su wheel SocketFactory, si es destruída el kernel POE esperará hasta que todas sus sesiones hijas finalicen, entonces la sesión será recolectada. En ese momento no quedarán sesiones para ejecutar, entonces el kernel POE se queda sin trabajo y finaliza.
Así, básicamente, ésta es la manera normal de deshacerse de las sesiones POE no requeridas: se liberan los recursos de la sesión y se deja limpio el kernel. Ahora veremos detalles de las sesiones hijas:

sub child_start {
    my ($heap, $socket) = @_[HEAP, ARG0];
    $heap->{readwrite} = new POE::Wheel::ReadWrite
      ( Handle => $socket,
        Driver => new POE::Driver::SysRW (),
        Filter => new POE::Filter::Line (),
        InputEvent => 'child_input',
        ErrorEvent => 'child_error',
      );
    $heap->{readwrite}->put( "Hola cliente!" );
    $heap->{peername} = join ':', @_[ARG1, ARG2];
    print "CHILD: Connected to $heap->{peername}.\n";
}


Cada vez que una nueva sesión hija es creada se llama a child_start() para manejar una nueva conexión cliente. Se introduce un nuevo tipo de wheel aquí: el ReadWrite wheel, el cual maneja tareas de I/O. Se le pasa un filehandle, un driver que usará para llamadas de I/O, y un filtro que pasa los datos entrantes y salientes y los transforma (en este caso convierte un flujo crudo de datos del socket a lineas separadas, y vice versa). Entonces el wheel enviará a la sesión un evento child_input() cada vez que reciba datos del filehandle, y un evento child_error() si ocurre algún error.
De inmediato se usa el nuevo wheel para mandar el string "Hola cliente!" al socket (el filtro POE::Filter::Line agrega el terminador de línea por nosotros en el string). Finalmente, se almacena la dirección y puerto del cliente en la heap y se imprime un mensaje de éxito.
Ahora discutiremos el estado child_input():

sub child_input {
    my $data = $_[ARG0];
    $data =~ tr{0-9+*/()-}{}cd;
    return unless length $data;
    my $result = eval $data;
    chomp $@;
    $_[HEAP]->{readwrite}->put( $@ || $result );
    print "CHILD: Got input from peer: \"$data\" = $result.\n";
}


Cuando el cliente nos envía una linea de datos, la descomponemos en una simple expresión aritmética y la evaluamos, enviando el resultado o mensaje de error de regreso al cliente. Normalmente, pasar datos del usuario sin validar a la función eval() es una cosa horrible y peligrosa, entonces removemos cualquier carácter no aritmético antes de hacer la evaluación. La sesión hija aceptará cada nuevo dato del cliente hasta que éste cierre la conexión. Corre el código tú mismo para intentarlo!
El estado child_stop sólo tiene una línea de código. Los estados child_done() y child_error() son autoexplicativos, ellos borran el wheel ReadWrite de la sesión hija causando que sea colectada por el garbage-collector e imprimen una explicación de lo sucedido. Fácil.

sub child_stop {
  print "CHILD: Stopped.\n";
}
sub child_done {
  delete $_[HEAP]->{readwrite};
  print "CHILD: disconnected from ", $_[HEAP]->{peername}, ".\n";
}
sub child_error {
  my ($function, $error) = @_[ARG0, ARG2];
  delete $_[HEAP]->{readwrite};
  print "CHILD: call to $function() failed: $error.\n" if $error;
}


Eso es todo por hoy

Y eso es todo lo que hay. La subrutina más larga en el programa sólo tiene 12 líneas, y toda la parte complicada del servidor ha sido más llevadera gracias a POE. Pero podrías preguntarte si no sería más sencillo un estilo de programación procedural, como los ejemplos de "man perlipc". Para un programa como este simple ejemplo probablemente sí. Pero la belleza de POE es esa, a medida que tu programa escale, permanecerá fácil de modificar. Es más fácil organizar el script en elementos discretos, y POE provee todas las características que en otro caso hay que reinventar cada vez que se necesite.
Intenta usar POE en tu próximo proyecto. Cualquier cosa que necesite un event loop es un buen lugar para usar POE. A divertirse!

martes, 8 de diciembre de 2015

Largo y corto

100 Nombres

Daniel y Cristina van a tener un bebé. Todavía no saben si será nene o nena pero tienen una lista con los posibles nombres. Como les cuesta decidirse por alguno quieren divertirse diseñando un programa de computadora que les diga el nombre más corto y el más largo.

Objetivo

Desarrolle un programa que tenga la siguiente entrada:
  • primero, el usuario ingresa un número entero n, que indica cuántas palabras ingresará a continuación;
  • después el usuario ingresa n palabras.

La salida del programa deben ser la palabra más larga y la más corta que ingresó el usuario.

Por ejemplo, si el usuario ingresa:

5
Diego
Andrea
Marcelino
Azul
Ana

la salida debe ser:

Mas larga: Marcelino
Mas corta: Ana

Solución en Perl


#!/usr/bin/perl
use strict;
use warnings;

my $mas_corto= '';
my $mas_largo= '';
my $size_corto= 99;
my $size_largo= 0;
print "Por favor, ingrese la cantidad de nombres: ";
my $n = <stdin>;
chomp($n);
foreach my $i (1..$n){
  print "Nombre $i?: ";
  my $nombre = <stdin>;
  chomp($nombre);
  if (length($nombre) > $size_largo){ $mas_largo= $nombre; $size_largo= length($nombre); }
  if (length($nombre) < $size_corto){ $mas_corto= $nombre; $size_corto= length($nombre); }
}
print "Gracias por los valores ingresados \n";
print "El más corto: $mas_corto \n";
print "El más largo: $mas_largo \n";


Contador de signos

Signos del clima

Ramón está preocupado por el cambio climático y decide empezar a revisar cifras que tengan que ver con el clima y su evolución. Para empezar toma un listado de temperaturas alrededor del mundo y quiere contar las que están sobre cero y las que están bajo cero. Como el trabajo es muy repetitivo y propenso a errores se propone automatizarlo con un programa de computadora.

Objetivo

Desarrolle un programa que pida al usuario que ingrese varios números, uno por uno. El ingreso de números termina cuando el usuario ingresa el 0, entonces el programa debe entregar como salida la cantidad de números positivos y negativos que ingresó el usuario.

Por ejemplo, si el usuario ingresa los números 37, —4, —22, —7, 17, —1 y 0, el programa debe entregar la salida:

Positivos: 2
Negativos: 4


Solución en Perl

#!/usr/bin/perl
use strict;
use warnings;

my $positivos= 0;
my $negativos= 0;
print "Por favor, ingrese un valor: ";
my $temperatura = <stdin>;
chomp($temperatura);
while($temperatura){
  if ($temperatura > 0){ $positivos++; }
  if ($temperatura < 0){ $negativos++; }
  print "Por favor, ingrese un valor: ";
  $temperatura = <stdin>;
  chomp($temperatura);
}
print "Gracias por los valores ingresados \n";
print "Positivos: $positivos \n";
print "Negativos: $negativos \n";