(Petit) Clône de client telnet en Haskell


Bonjour,

L'autre nuit, muni de café, j'ai souhaité m'amuser avec Haskell. J'ai alors consulté le chapitre sur le réseau en Haskell de Real World Haskell et... j'ai écrit un (très petit) clône de client telnet... qui fait 41 lignes. Le seul soucis étant que dans toute application de ce genre, on doit partager habilement la lecture des entrées de l'utilisateur, et l'affichage de ce que l'on nous envoie. Ceci mis à part, tout cela fonctionne très bien !

Haskell

Regardons à quoi cela ressemble. Petit détail : à chaque tour, je lance la réception de données réseau dans un autre thread, pour ne pas bloquer l'écriture de l'utilisateur.

On importe d'abord les modules nécessaires.

?View Code HASKELL
import Control.Concurrent 
import Control.Monad 
import Network.Socket 
import Network.BSD 
import System.Environment 
import System.IO

Ensuite, j'ai écrit quelques fonctions pour rendre la connection, l'envoi etc plus faciles.

?View Code HASKELL
-- Ouverture d'une connexion TCP cliente 
-- Prend en argument l'host et le port/service (qui est une String également) 
-- Retourne le "Handle" vers le socket en question. 
openConnection :: String -> ServiceName -> IO Handle 
openConnection hostname port = 
  do 
  addrInfos <- getAddrInfo Nothing (Just hostname) (Just port) 
  let serveraddr = head addrInfos 
  sock <- socket (addrFamily serveraddr) Stream defaultProtocol 
  setSocketOption sock KeepAlive 1 
  connect sock (addrAddress serveraddr) 
  h <- socketToHandle sock ReadWriteMode 
  hSetBuffering h (BlockBuffering Nothing) 
  return h 
 
-- Ferme la connexion dont le Handle est donné 
closeConnection :: Handle -> IO () 
closeConnection h = hClose h 
 
-- Envoie la chaîne donnée sur le Handle donné, et ensuite fait un flush sur ce handle, ce qui force l'envoi immédiat et nettoie les états 
sendMsg :: Handle -> String -> IO () 
sendMsg h msg = hPutStrLn h msg >> hFlush h 
 
-- Lis sur le Handle donné une chaîne et la renvoie (dans la monade IO) 
recvMsg :: Handle -> IO String 
recvMsg h = do 
  msg <- hGetContents h  
  return msg

Voyons maintenant la fonction main.

?View Code HASKELL
main = do 
  args <- getArgs 
  let (host, port) = (args !! 0, args !! 1)

On récupère les arguments donnés au programme (s'il n'y en a pas assez, cela fera planter le logiciel -- je ne me suis pas vraiment préoccupé de tous les cas d'erreur, mais ce n'était pas ma priorité ici) via la fonction getArgs, et l'on en récupère l'host et le port donnés.

?View Code HASKELL
  putStrLn $ "Opening client on " ++ host ++ ":" ++ port ++ "\n" 
  hclient <- openConnection host port 
  putStrLn "Client connected"

Ca y est, on se connecte, en affichant des messages pour tenir l'utilisateur informé. Si la connection échoue, le programme s'arrêtera avec une erreur -- cf remarque ci-dessus.

Et maintenant, la boucle qui permet de lire depuis le réseau et lire depuis l'utilisateur, etc.

?View Code HASKELL
  forever $ do 
    getLine >>= sendMsg hclient 
    forkIO $ recvMsg hclient >>= putStrLn

Ici, on a une sorte de boucle "à la while(true)", dans laquelle on va tour à tour récupérer une entrée de l'utilisateur et l'envoyer au serveur, puis, dans un thread séparé, récupérer ce qu'a envoyé le serveur et l'afficher.
Enfin, on ferme la connection (ce code est inutile tant que l'on a pas de possibilité de sortir du forever... qui devra tôt ou tard se transformer en "until" ou autre).

?View Code HASKELL
  closeConnection hclient

Voilà le code complet :

?View Code HASKELL
import Control.Concurrent 
import Control.Monad 
import Network.Socket 
import Network.BSD 
import System.Environment 
import System.IO 
 
openConnection :: String -> ServiceName -> IO Handle 
openConnection hostname port = do 
  addrInfos <- getAddrInfo Nothing (Just hostname) (Just port) 
  let serveraddr = head addrInfos 
  sock <- socket (addrFamily serveraddr) Stream defaultProtocol 
  setSocketOption sock KeepAlive 1 
  connect sock (addrAddress serveraddr) 
  h <- socketToHandle sock ReadWriteMode 
  hSetBuffering h (BlockBuffering Nothing) 
  return h 
 
closeConnection :: Handle -> IO () 
closeConnection h = hClose h 
 
sendMsg :: Handle -> String -> IO () 
sendMsg h msg = hPutStrLn h msg >> hFlush h 
 
recvMsg :: Handle -> IO String 
recvMsg h = do 
  msg <- hGetContents h  
  return msg 
 
main = do 
  args <- getArgs 
  let (host, port) = (args !! 0, args !! 1) 
  putStrLn $ "Opening client on " ++ host ++ ":" ++ port ++ "\n" 
  hclient <- openConnection host port 
  putStrLn "Client connected" 
  forever $ do 
    getLine >>= sendMsg hclient 
    forkIO $ recvMsg hclient >>= putStrLn 
  closeConnection hclient

Compilation :

ghc --make -o net net.hs

Puis 2 exemples d'exécution...

$ ./net google.fr 80
Opening client on google.fr:80
Client connected
GET / HTTP/1.0
HTTP/1.0 302 Found
Location: http://www.google.fr/
Cache-Control: private
Content-Type: text/html; charset=UTF-8
Set-Cookie: PREF=ID=30b094ad566b5532:TM=1251456529:LM=1251456529:S=94IXp5-SGiaYADWc; expires=Sun, 28-Aug-2011 10:48:49 GMT; path=/; domain=.google.com
Date: Fri, 28 Aug 2009 10:48:49 GMT
Server: gws
Content-Length: 218 


302 Moved

302 Moved

The document has moved here. ^C


(c'est moi qui ai tapé "GET / HTTP/1.0")
( qui vous dira que la page a été déplacée :) )

Et enfin, la surprise...

$ ./net towel.blinkenlights.nl 23

Qui, une version simplifiée d'un film très connu, vous permettra de voir, Jedi.

Enjoy !

, ,

  1. #1 by nicolas66 - septembre 1st, 2009 at 21:37

    Belle illustration de la puissance d'Haskell. Mais j'avoue que la syntaxe me fait encore un brain flipper :/

  2. #2 by Alp Mestan - septembre 2nd, 2009 at 11:10

    Je dirais que c'est quand même pas mal une question d'habitude finalement. La syntaxe me faisait beaucoup flipper avant que je m'y mette aussi !

    Quels bouts de code te font particulièrement flipper ?

  3. #3 by Gollum - septembre 14th, 2009 at 16:52

    Ca à l'air pas mal comme langage mais je rejoins nicolas sur le fait que la syntaxe fais un peu peur. Après comme tu l'a dit c'est une bonne(ou pas) habitude à prendre.

    ++

  4. #4 by davidf - octobre 12th, 2009 at 10:48

    La syntaxe est plutôt simple car elle est homogène : tout est une expression, donc tout peut s'imbriquer dans tout (à quelques exceptions près certainement). Ensuite il faut faire attention à la priorité des opérateurs qui est assez différente (ex : les appels de fonctions prioritaires sur tout le reste). En revanche le fait de faire des blocks avec les espaces en début de ligne comme en Python est une top-idée à mon avis.

  5. #5 by davidf - octobre 12th, 2009 at 11:38

    Je suis tombé sur un guide condensé en 13 pages qu'on peut garder sous la main en codant en Haskell
    http://cheatsheet.codeslower.com/
    Certainement une bonne façon de n'avoir pas peur de la syntaxe est de garder le Cheat Sheet pas loin. :-)

(will not be published)

  1. No trackbacks yet.