GHC6-Network

De Wikihaskell
Saltar a: navegación, buscar
Funciones de red
Biblioteca de red para la construcción de programas distribuidos
Lenguaje Haskell
Biblioteca ghc6-network
Autores Abrahán Fernández Nieto
Juan Carlos Gonzalez Cerezo
Adrián Brenes Ureba

En esta página abordaremos cómo utilizar Haskell en el ámbito de la programación distribuida a bajo nivel.

Contenido

Introducción

Aquí mostraremos cómo usar Haskell aplicando los conocimientos de red de bajo nivel que ya tenemos. Las funciones de red que posee Haskell, casi siempre, se escriben como las llamadas a las funciones en C. Por lo tanto, al igual que con otros lenguajes basados en C, nos encontraremos con una interfaz muy familiar.

Comunicándonos con UDP

Como sabemos, los UDP dividen los datos o información en paquetes. No se está seguro de que los datos alcancen su destino, o lo alcance sólo una vez. Se hace uso de un resumen de comprobación para asegurar que los paquetes que lleguen no hayan sido degradados. Los UDP tienden a ser usados en aplicaciones que son sensibles al rendimiento, en las cuales cada paquete individual de datos es menos importante que el resto del rendimiento del sistema. Además debe ser usado donde el comportamiento del TCP no es el más eficiente, es decir, en aquellos que envían mensajes cortos.

Ejemplos de sistemas que tienden a usar UDP incluído audio y video, sincronización del tiempo, redes basadas en sistemas de ficheros, y sistemas de autentificación.

Ejemplo de cliente UDP: syslog

El servicio syslog tradicional de Unix permite enviar mensajes de inicio de sesión sobre una red a un servidor central que los registra. Algunos programas son un poco sensibles y deben generar un gran volumen de mensajes. Por esta razón, UDP es uno de los protocolos soportados por syslog para la transmisión de mensajes de autentificación. El protocolo es simple. A continuación se muestra una implementación del cliente:

import Data.Bits
import Network.Socket
import Network.BSD
import Data.List
import SyslogTypes

data SyslogHandle = 
    SyslogHandle {slSocket :: Socket,
                  slProgram :: String,
                  slAddress :: SockAddr}
openlog :: HostName             -- ^ Remote hostname, or localhost
        -> String               -- ^ Port number or name; 514 is default
        -> String               -- ^ Name to log under
        -> IO SyslogHandle      -- ^ Handle to use for logging
openlog hostname port progname =
    do -- Look up the hostname and port.  Either raises an exception
       -- or returns a nonempty list.  First element in that list
       -- is supposed to be the best option.

       addrinfos <- getAddrInfo Nothing (Just hostname) (Just port)
       let serveraddr = head addrinfos

       -- Establish a socket for communication
       sock <- socket (addrFamily serveraddr) Datagram defaultProtocol

       -- Save off the socket, program name, and server address in a handle
       return $ SyslogHandle sock progname (addrAddress serveraddr)

syslog :: SyslogHandle -> Facility -> Priority -> String -> IO ()
syslog syslogh fac pri msg =
    sendstr sendmsg
    where code = makeCode fac pri
          sendmsg = "<" ++ show code ++ ">" ++ (slProgram syslogh) ++
                    ": " ++ msg

          -- Send until everything is done
          sendstr :: String -> IO ()
          sendstr [] = return ()
          sendstr omsg = do sent <- sendTo (slSocket syslogh) omsg
                                    (slAddress syslogh)
                            sendstr (genericDrop sent omsg)

closelog :: SyslogHandle -> IO ()
closelog syslogh = sClose (slSocket syslogh)

{- | Convert a facility and a priority into a syslog code -}
makeCode :: Facility -> Priority -> Int
makeCode fac pri =
    let faccode = codeOfFac fac
        pricode = fromEnum pri 
        in
          (faccode `shiftL` 3) .|. pricode

Este fichero necesita otro fichero, SyslogTypes.hs, el cual se muestra a continuación:

-- file: SyslogTypes.hs
module SyslogTypes where
{- | Priorities define how important a log message is. -}

data Priority = 
            DEBUG                   -- ^ Debug messages
          | INFO                    -- ^ Information
          | NOTICE                  -- ^ Normal runtime conditions
          | WARNING                 -- ^ General Warnings
          | ERROR                   -- ^ General Errors
          | CRITICAL                -- ^ Severe situations
          | ALERT                   -- ^ Take immediate action
          | EMERGENCY               -- ^ System is unusable
                    deriving (Eq, Ord, Show, Read, Enum)

{- | Facilities are used by the system to determine where messages
are sent. -}

data Facility = 
              KERN                      -- ^ Kernel messages
              | USER                    -- ^ General userland messages
              | MAIL                    -- ^ E-Mail system
              | DAEMON                  -- ^ Daemon (server process) messages
              | AUTH                    -- ^ Authentication or security messages
              | SYSLOG                  -- ^ Internal syslog messages
              | LPR                     -- ^ Printer messages
              | NEWS                    -- ^ Usenet news
              | UUCP                    -- ^ UUCP messages
              | CRON                    -- ^ Cron messages
              | AUTHPRIV                -- ^ Private authentication messages
              | FTP                     -- ^ FTP messages
              | LOCAL0                  
              | LOCAL1
              | LOCAL2
              | LOCAL3
              | LOCAL4
              | LOCAL5
              | LOCAL6
              | LOCAL7
                deriving (Eq, Show, Read)

facToCode = [ 
                       (KERN, 0),
                       (USER, 1),
                       (MAIL, 2),
                       (DAEMON, 3),
                       (AUTH, 4),
                       (SYSLOG, 5),
                       (LPR, 6),
                       (NEWS, 7),
                       (UUCP, 8),
                       (CRON, 9),
                       (AUTHPRIV, 10),
                       (FTP, 11),
                       (LOCAL0, 16),
                       (LOCAL1, 17),
                       (LOCAL2, 18),
                       (LOCAL3, 19),
                       (LOCAL4, 20),
                       (LOCAL5, 21),
                       (LOCAL6, 22),
                       (LOCAL7, 23)
           ] 

codeToFac = map (\(x, y) -> (y, x)) facToCode

{- | We can't use enum here because the numbering is discontiguous -}
codeOfFac :: Facility -> Int
codeOfFac f = case lookup f facToCode of
                Just x -> x
                _ -> error $ "Internal error in codeOfFac"

facOfCode :: Int -> Facility
facOfCode f = case lookup f codeToFac of
                Just x -> x
                _ -> error $ "Invalid code in facOfCode"

Con GHCI, podemos enviar mensajes a un servidor cliente/servidor local. Pueden usarse los dos ejemplos mostrados en esta sección, o un servidor cliente/servidor como encontrarías en Linux u otros sistemas POSIX.

Si deseas enviar mensajes a un servidor cliente/servidor en un sistema local, debemos usar un comando, tal y como se muestra a continuación:

ghci> :load syslogclient.hs
[1 of 2] Compiling SyslogTypes      ( SyslogTypes.hs, interpreted )
[2 of 2] Compiling Main             ( syslogclient.hs, interpreted )
Ok, modules loaded: SyslogTypes, Main.
ghci> h <- openlog "localhost" "514" "testprog"
Loading package parsec-2.1.0.0 ... linking ... done.
Loading package network-2.1.0.0 ... linking ... done.
ghci> syslog h USER INFO "This is my message"
ghci> closelog h

Servidor Cliente/Servidor UDP

Los servidores UDP obligan a especificar el puerto de la máquina que utiliza. Éstos aceptarán los paquetes dirigidos a este puerto y los procesarán.

Para un protocolo orientado a paquetes, los programadores normalmente usan una llamada, como el recvFrom, para recibir la información y datos sobre la máquina que envía, la cuál es usada para devolver una respuesta.

-- file: syslogserver.hs
import Data.Bits
import Network.Socket
import Network.BSD
import Data.List

type HandlerFunc = SockAddr -> String -> IO ()

serveLog :: String              -- ^ Port number or name; 514 is default
         -> HandlerFunc         -- ^ Function to handle incoming messages
         -> IO ()
serveLog port handlerfunc = withSocketsDo $
    do -- Look up the port.  Either raises an exception or returns
       -- a nonempty list.  
       addrinfos <- getAddrInfo 
                    (Just (defaultHints {addrFlags = [AI_PASSIVE]}))
                    Nothing (Just port)
       let serveraddr = head addrinfos

       -- Create a socket
       sock <- socket (addrFamily serveraddr) Datagram defaultProtocol

       -- Bind it to the address we're listening to
       bindSocket sock (addrAddress serveraddr)

       -- Loop forever processing incoming data.  Ctrl-C to abort.
       procMessages sock
    where procMessages sock =
              do -- Receive one UDP packet, maximum length 1024 bytes,
                 -- and save its content into msg and its source
                 -- IP and port into addr
                 (msg, _, addr) <- recvFrom sock 1024
                 -- Handle it
                 handlerfunc addr msg
                 -- And process more messages
                 procMessages sock

-- A simple handler that prints incoming packets
plainHandler :: HandlerFunc
plainHandler addr msg = 
    putStrLn $ "From " ++ show addr ++ ": " ++ msg

Podemos probar a ejecutar esto en GHCI. Una llamada a serveLog 1514 plainHandler establecerá un servidor UDP en el puerto 1514 que usará plainHandler para imprimir y mostrar todo paquete UDP recibido a través de este puerto. Con Ctrl-C interrumpimos la ejecución del programa.

En caso de problemas:

bind: permission denied

Cuando aparezca este mensaje, aseguresé de que está usando un número de puerto mayor a 1024. Algunos sistemas operativos sólo permiten al usuario raíz poder asignar el puerto a menos de 1024.

Ejemplos

Primer ejemplo en red



Comunicándonos con TCP

Como sabemos, TCP está diseñado para poder llevar a cabo transferencias de datos a través de Internet de manera tan segura como sea posible. El tráfico TCP no es más que un flujo de datos. Aunque este flujo es dividido en paquetes individuales por el sistema operativo, los límites del paquete no son ni conocidos ni relevantes para las aplicaciones. TCP garantiza que, si el tráfico es distribuido para toda la aplicaión, significa que ha llegado intacto, sin sufrir cambios, sólo uno, y en orden. Obviamente, cosas como la rotura de un cable puede causar que el tráfico no sea posible, y el protocolo no puede superar esas limitaciones.

Esto trae consigo algunas contradicciones comparadas con UDP. Ante todo, hay algunos paquetes que deben ser enviados al principio de la conversión TCP para establecer el enlace. Para conversaciones cortas, entonces, UDP tendría un mejor rendimiento. Además, TCP intenta duramente la tranferencia de datos. Si un extremo de la conversación intenta enviar datos, pero no recibe un acuse de recibo, periódicamente retransmitirá el dato un tiempo antes de renunciar.

Esto hace que TCP sea robusto en vista de los paquetes caídos. Sin embargo, también significa que TCP no es la mejor opción para protocolos en tiempo real que implican cosas como audio o video en directo.

Flujo Multihilo en TCP

Con TCP, las conexiones son estables. Esto significa que existe un canal lógico dedicado entre un cliente y un servidor, mejor que sólo un paquete con UDP. Esto permite hace fácil las cosas al desarrollador de clientes. Las aplicaciones del servidor casi siempre querrán ser capaces de encargarse de más de una conexión TCP en cada momento. ¿Cómo lo consiguen?

En el lado del servidor, primero crearás un puerto, al igual que en la conexión UDP. En lugar de andar repetidamente escuchando a los datos de cualquier fuente o localización, el principal bucle se producirá en torno a la llamada de aceptación o adminisión. Cada vez que un cliente se conecta, el sistema operativo del servidor asigna un nuevo puerto para ello. Entonces, tendremos el puerto maestro, usado sólo para escuchar las conexiones entrantes, y nunca la transmisión de datos. Además, tenemos la posibilidad de usar múltiples puertos clientes a la vez, correspondiéndose cada uno con una conversación TCP lógica.

En Haskell, usarás normalmente forkIO para crear un hilo ligero separado para encargarse de cada conversación con un puerto cliente. Haskell tiene una implementación interna sobre esto que permite llevarlo a cabo bastante bien.

Conexión Cliente/Servidor TCP

Vamos a decir que queríamos volver a implementar la conexión Cliente/Servidor usando TCP en lugar de UDP. Podríamos decir que un simple mensaje está definido no por un simple paquete, pero está acabado por un carácter de nueva línea. Un cliente determinado podría enviar 0 o más mensajes al servidor usando a conexión TCP determinada. A continuación se muestra cómo debemos escribir esto:

-- file: syslogtcpserver.hs
import Data.Bits
import Network.Socket
import Network.BSD
import Data.List
import Control.Concurrent
import Control.Concurrent.MVar
import System.IO

type HandlerFunc = SockAddr -> String ->  IO()
serveLog :: String              -- ^ Port number or name; 514 is default
        -> HandlerFunc         -- ^ Function to handle incoming messages
        -> IO ()
serveLog port handlerfunc = withSocketsDo $
    do -- Look up the port.  Either raises an exception or returns
       -- a nonempty list.  
       addrinfos <- getAddrInfo 
                    (Just (defaultHints {addrFlags = [AI_PASSIVE]}))
                    Nothing (Just port)
       let serveraddr = head addrinfos
       -- Create a socket
       sock <- socket (addrFamily serveraddr) Stream  defaultProtocol                                                                                                                                                                                                                                                                -- Bind it to the address we're listening to
      bindSocket sock (addrAddress serveraddr)
      -- Start listening for connection requests.  Maximum queue size
      -- of 5 connection requests waiting to be accepted.
      listen sock 5
      -- Create a lock to use for synchronizing access to the handler
      lock <- newMVar ()
      -- Loop forever waiting for connections.  Ctrl-C to abort.
      procRequests lock sock
   where
         -- | Process incoming connection requests
         procRequests :: MVar () -> Socket -> IO ()
         procRequests lock mastersock = 
             do (connsock, clientaddr) <- accept mastersock
                handle lock clientaddr
                   "syslogtcpserver.hs: client connnected"
                forkIO $ procMessages lock connsock clientaddr
                procRequests lock mastersock
         -- | Process incoming messages
         procMessages :: MVar () -> Socket -> SockAddr -> IO ()
         procMessages lock connsock clientaddr =
             do connhdl <- socketToHandle connsock ReadMode
                hSetBuffering connhdl LineBuffering
                messages <- hGetContents connhdl
                mapM_ (handle lock clientaddr) (lines messages)
                hClose connhdl
                handle lock clientaddr 
                   "syslogtcpserver.hs: client disconnected"
         -- Lock the handler before passing data to it.
         handle :: MVar () -> HandlerFunc
         -- This type is the same as
         -- handle :: MVar () -> SockAddr -> String -> IO ()
         handle lock clientaddr msg =
             withMVar lock 
                (\a -> handlerfunc clientaddr msg >> return a)
-- A simple handler that prints incoming packets
plainHandler :: HandlerFunc
plainHandler addr msg = 
   putStrLn $ "From " ++ show addr ++ ": " ++ msg

Para la implementación de SyslogTypes, mirar la sección mostrada más arriba de este documento llamado “Ejemplo de Cliente UDP: syslog”.

Miremos este código. Nuestro bucle principal está en procRequests, donde se repite mientras se espera la conexión de los clientes. La llamada a accept agrupa hasta que un cliente conecta. Cuando un cliente conecta, tomamos un nuevo puerto y la dirección del cliente. Pasamos un mensaje al encargado de esto, entonces usamos forkOI para crear un hilo que maneje los datos de este cliente. Este hilo se ejecuta procMessages. Cuando se llevan a cabo conexiones TCP, es a menudo conveniente convertir un puerto en un Handle de Haskell. Esto se hace aquí, y poniendo explicitamente el buffering (un punto importante para la comunicación TCP). Después, establecemos la lectura perezosa del Handle del puerto. Para cada línea de entrada, se pasa al Handle. Una vez que no hay más datos, se cierra el puerto y se informa sobre ello (mensaje).

Cuando estamos tratando muchos mensajes de entrada a la vez, necesitamos asegurarnos de que no estamos escribiendo muchos mensajes en el Handler. Esto puede proporcionar resultados ambiguos. Usamos un simple bloqueo para llevar a cabo la administración de los accesos en el Handler, escribiendo para ello una función handle.

Podemos comprobar esto con el cliente que mostramos a continuación, o incluso usar el programa telnet para conectar con el servidor. Cada línea del texto que le enviamos será imprimida en el lado del servidor. ghci> :load syslogtcpserver.hs [1 of 1] Compiling Main ( syslogtcpserver.hs, interpreted ) Ok, modules loaded: Main. ghci> serveLog "10514" plainHandler Loading package parsec-2.1.0.0 ... linking ... done. Loading package network-2.1.0.0 ... linking ... done.

En este punto, el servidor comenzará escuchando la conexión a través del puerto 10514. No aparecerá nada hasta que el cliente no conecte. Podríamos usar telnet para conectar con el servidor.

~$ telnet localhost 10514 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Test message ^] telnet> quit Connection closed. Mientras tanto, en el otro servidor se encuentra ejecutando el servidor TCP, en el que se podrá verse algo como lo siguiente: From 127.0.0.1:38790: syslogtcpserver.hs: client connnected From 127.0.0.1:38790: Test message From 127.0.0.1:38790: syslogtcpserver.hs: client disconnected

Esto nos informa sobre la existencia de un cliente conectado desde el puerto 38790 en la máquina local (127.0.0.0.1). Tras la conexión, se envía un mensaje y se desconecta. Cuando actuamos como cliente TCP, el sistema operativo nos asigna un puerto libre. El número de este puerto será diferente cada vez que ejecutemos el programa.

Cliente TCP

A continuación, vamos a implementar un cliente para nuestro protocolo cliente/servidor TCP. Este cliente será similar al cliente UDP, excepto un par de cambios: En primer lugar, puesto que TCP es un protocolo de flujo, podemos enviar datos usando un Handle en lugar de usar operaciones del puerto de bajo nivel. En segundo lugar, ya no necesitamos almacenar la dirección de destino en el SyslogHandle ya que estaremos usando connect para establecer la conexión TCP. Finalmente, necesitaremos una manera de conocer dónde acaba un mensaje y comienza el siguiente. Con UDP, ésto era fácil porque cada mensaje era un paquete lógico. Con TCP, únicamente usaremos el carácter de nueva línea '\n' para marcar el final del mensaje.

Así pues, el código sería el siguiente:

-- file: syslogtcpclient.hs
import Data.Bits
import Network.Socket
import Network.BSD
import Data.List
import SyslogTypes
import System.IO

data SyslogHandle = 
    SyslogHandle {slHandle :: Handle,
                  slProgram :: String}

openlog :: HostName             -- ^ Remote hostname, or localhost
        -> String               -- ^ Port number or name; 514 is default
        -> String               -- ^ Name to log under
        -> IO SyslogHandle      -- ^ Handle to use for logging
openlog hostname port progname =
    do -- Look up the hostname and port.  Either raises an exception
       -- or returns a nonempty list.  First element in that list
       -- is supposed to be the best option.
       addrinfos <- getAddrInfo Nothing (Just hostname) (Just port)
       let serveraddr = head addrinfos 

       -- Establish a socket for communication
       sock <- socket (addrFamily serveraddr) Stream defaultProtocol 

       -- Mark the socket for keep-alive handling since it may be idle
       -- for long periods of time
       setSocketOption sock KeepAlive 1 

       -- Connect to server
       connect sock (addrAddress serveraddr)

       -- Make a Handle out of it for convenience
       h <- socketToHandle sock WriteMode

       -- We're going to set buffering to BlockBuffering and then
       -- explicitly call hFlush after each message, below, so that
       -- messages get logged immediately
       hSetBuffering h (BlockBuffering Nothing)
       
       -- Save off the socket, program name, and server address in a handle
       return $ SyslogHandle h progname

syslog :: SyslogHandle -> Facility -> Priority -> String -> IO ()
syslog syslogh fac pri msg =
    do hPutStrLn (slHandle syslogh) sendmsg
       -- Make sure that we send data immediately
       hFlush (slHandle syslogh)
    where code = makeCode fac pri
          sendmsg = "<" ++ show code ++ ">" ++ (slProgram syslogh) ++
                    ": " ++ msg

closelog :: SyslogHandle -> IO ()
closelog syslogh = hClose (slHandle syslogh)

{- | Convert a facility and a priority into a syslog code -}
makeCode :: Facility -> Priority -> Int
makeCode fac pri =
    let faccode = codeOfFac fac
        pricode = fromEnum pri 
        in
          (faccode `shiftL` 3) .|. pricode

Podemos intentar bajo el ghci. Si todavía tiene el servidor TCP ejecutándose, su sesión debe ser similar a lo siguiente:

ghci> :load syslogtcpclient.hs
Loading package base ... linking ... done.
[1 of 2] Compiling SyslogTypes      ( SyslogTypes.hs, interpreted )
[2 of 2] Compiling Main             ( syslogtcpclient.hs, interpreted )
Ok, modules loaded: Main, SyslogTypes.
ghci> openlog "localhost" "10514" "tcptest"
Loading package parsec-2.1.0.0 ... linking ... done.
Loading package network-2.1.0.0 ... linking ... done.
ghci> sl <- openlog "localhost" "10514" "tcptest"
ghci> syslog sl USER INFO "This is my TCP message"
ghci> syslog sl USER INFO "This is my TCP message again"
ghci> closelog sl
      

En el lado del servidor, podrá ver algo como esto:

From 127.0.0.1:46319: syslogtcpserver.hs: client connnected
From 127.0.0.1:46319: <9>tcptest: This is my TCP message
From 127.0.0.1:46319: <9>tcptest: This is my TCP message again
From 127.0.0.1:46319: syslogtcpserver.hs: client disconnected 

Ejemplos

Ejemplo de comunicación TCP con buffering

Referencias

  • Bryan O'Sullivan, John Goerzen & Don Stewart. Real World Haskell. Ed. O'Reilly. ISBN=978-0-596-51498-3
Herramientas personales