Tcl par l'exemple - le Client/Serveur

 

ulis, 2007-01-17. Un client/serveur simple pour comprendre les principes de base, les opérations de base et les commandes de base des sockets.

Exercices : copiez-collez les exemples, exécutez-les puis modifiez-les. Durée : 60 mn.

(attention : il y a deux scripts, un pour le serveur et un pour le client)


Introduction

Une architecture client/serveur implique un ensemble d'au moins deux processus qui dialoguent :

Le dialogue peut se faire de différentes façons (par des sockets, des pipes, à travers une liaison série...).

Tcl fournit des sockets sous TCP/IP.


Principes de base


Opérations de base

  1. Définition du protocole : qui comprend au moins l'adresse du serveur et le nom du service demandé, les règles régissant les messages échangés (demande du client, réponse du serveur...), la façon de se déconnecter.
  2. Attente du serveur : sur le port d'attente qui dans le cas de Tcl correspond à l'ouverture d'un socket server et la mise en attente du processus du serveur.
  3. Connexion du client : au serveur défini par son adresse réseau et le nom du service.
  4. Consommation du service : suivant les règles du protocole.
  5. Déconnexion du client : suivant les règles du protocole (ou anormale).

Commandes de base

Attente du serveur

  set channel [socket -server command ?options? port]
  set port [lindex [fconfigure $channel -sockname] end]

Connexion du client

  set channel [socket ?options? host port]

Lecture, écriture sur le socket, fermeture du socket

Le nom de canal fourni au call-back du serveur ou renvoyé par la commande socket du client permet toutes les opérations d'entrée-sortie standard (à l'exception de open). Il peut être utilisé avec read, gets, puts, flush et close.

Configuration du socket

Le même nom de canal permet de configurer le socket avec fconfigure pour le mettre en mode auto (conversion des fins de lignes) ou binary (sans conversion). Il permet aussi de mettre en place des call-back pour des opérations asynchrones avec fileevent.

Détection des exceptions

Le même nom de canal permet de détecter la clôture ou une erreur sur le canal avec les commandes eof et fconfigure -error.


Le script du serveur

(les explications sont après le code)

  # ###############
  #
  # server side
  #
  # ###############

  # client connection

  proc server {channel host port} \
  {
    # save client info
    set ::($channel:host) $host
    set ::($channel:port) $port
    # log
    log $channel <opened>
    set rc [catch \
    {
      # set call back on reading
      fileevent $channel readable [list input $channel]
    } msg]
    if {$rc == 1} \
    {
      # i/o error -> log
      log server ***$msg
    }
  }

  # client e/s

  proc input {channel} \
  {
    if {[eof $channel]} \
    {
      # client closed -> log & close
      log $channel <closed>
      catch { close $channel }
    } \
    else \
    {
      # receiving
      set rc [catch { set count [gets $channel data] } msg]
      if {$rc == 1} \
      {
        # i/o error -> log & close
        log $channel ***$msg
        catch { close $channel }
      } \
      elseif {$count == -1} \
      {
        # client closed -> log & close
        log $channel <closed>
        catch { close $channel }
      } \
      else \
      {
        # got data -> do some thing
        log $channel $data
      }
    }
  }

  # log

  proc log {channel msg} \
  { puts "$::($channel:host):$::($channel:port): $msg" }

  # ===================
  # start
  # ===================

  # open socket

  catch { console show }
  catch { wm protocol . WM_DELETE_WINDOW exit }
  set port 6000 ;# 0 if no known free port
  set rc [catch \
  {
    set channel [socket -server server $port]
    if {$port == 0} \
    {
      set port [lindex [fconfigure $channel -sockname] end]
      puts "--> server port: $port"
    }
  } msg]
  if {$rc == 1} \
  {
    log server <exiting>\n***$msg
    exit
  }
  set (server:host) server
  set (server:port) $port

  # enter event loop

  vwait forever

Les explications

  set port 6000 ;# 0 if no known free port
  set rc [catch \
  {
    set channel [socket -server server $port]
    if {$port == 0} \
    {
      set port [lindex [fconfigure $channel -sockname] end]
      puts "--> server port: $port"
    }
  } msg]
  if {$rc == 1} \
  {
    log server <exiting>\n***$msg
    exit
  }
  set (server:host) server
  set (server:port) $port

La commande socket avec son option -server ouvre un socket serveur et renvoie un nom de canal. Elle prend en argument le numéro du port d'attente du serveur. La valeur de l'option -server est le nom d'une procédure qui sera appelée lors d'une connexion d'un client avec trois arguments : le nom du nouveau canal ouvert pour le client, l'adresse réseau du client et le numéro de port du client.

Si le numéro de port est 0, le système va allouer dynamiquement un numéro de port. Pour le retrouver il faut utiliser la commande fconfigure avec son option -sockname (la description de l'option est dans la description de la commande socket). La commande renvoie une liste de trois valeurs : l'adresse du serveur, le nom du serveur et le numéro de port alloué. C'est ce numéro de port qui devra être utilisé par les clients désirant se connecter au serveur.

Les commandes d'entrée-sortie étant susceptibles de générer des exceptions, l'ensemble des deux commandes est dans un catch.

  vwait forever

La connexion d'un client à un socket serveur ne sera détectée que si le processus est entré dans la boucle (de traitement) des évènements.

Cette boucle est entréee automatiquement à la terminaison du script initial (le script que vous avez écrit) lorsqu'il est exécuté par wish. Sinon il faut utiliser la commande vwait.

Ici la commande vwait entre dans la boucle des évènements et attend une modification de la variable forever. Cette variable n'étant pas modifiée dans le script, cela peut durer longtemps.

  proc server {channel host port} \
  {
    # save client info
    set ::($channel:host) $host
    set ::($channel:port) $port
    # log
    log $channel <opened>
    set rc [catch \
    {
      # set call back on reading
      fileevent $channel readable [list input $channel]
    } msg]
    if {$rc == 1} \
    {
      # i/o error -> log
      log server ***$msg
    }
  }

La procédure est déclarée au moyen de la commande socket et définie par la valeur de l'option -server. Elle est appelée lors de la connexion d'un client et reçoit trois arguments : le nom du nouveau canal ouvert pour le client, l'adresse réseau du client et le numéro de port du client.

Ici la procédure est utilisée pour établir un call-back qui sera appelée à chaque fois que des informations seront disponibles en lecture sur le canal. Ce call-back est défini par la commande fileevent avec l'option readable : c'est un appel à la procédure input avec en argument le nom du canal alloué au client.

Les commandes d'entrée-sortie étant susceptibles de générer des exceptions, la commande fileevent est dans un catch.

  proc input {channel} \
  {
    if {[eof $channel]} \
    {
      # client closed -> log & close
      log $channel <closed>
      catch { close $channel }
    } \
    else \
    {
      # receiving
      set rc [catch { set count [gets $channel data] } msg]
      if {$rc == 1} \
      {
        # i/o error -> log & close
        log $channel ***$msg
        catch { close $channel }
      } \
      elseif {$count == -1} \
      {
        # client closed -> log & close
        log $channel <closed>
        catch { close $channel }
      } \
      else \
      {
        # got data -> do some thing
        log $channel $data
      }
    }
  }

La procédure est appelée dans différents cas :

Le cas fin de canal a été testé à part bien que, ici, ce ne soit pas utile. Mais cela sera indispensable dans le mode non bloquant. Alors, comme cela ne nuit pas, autant prendre de bonnes habitudes.

La fin de canal est testée par la commande eof avec en argument le nom du canal.

Les données sont lues avec la commande gets. Ce qui correspond au mode non bloquant et non binaire dans lequel s'exécute le script (pour plus d'info sur le sujet voir la commande fconfigure et ses options -blocking et -translation).

La commande gets prend en arguments le nom du canal et le nom d'une variable. Elle retourne le nombre d'octets lus sur le canal. Si le canal est vide et qu'une fin de canal est détectée par gets, le nombre d'octets retournés est -1.

La commande peut créer une erreur en cas d'anomalie sur le canal. Une fin de canal N'est PAS une anomalie.

Les commandes d'entrée-sortie étant susceptibles de générer des exceptions, chaque commande est dans un catch.

Ici, les données lues sont affichées par la procédure log. C'est ce traitement qui est à adapter à vos besoins.

Exercices

Voir ce qui se passe quand :

Modifier le code de la procédure input pour que le script devienne utile.

Mettre le serveur en mode synchrone (supprimer la commande fileevent) et observer le log. (en cas désespéré, voir le code du serveur en mode synchrone en fin de page)


Le script du client

(les explications sont après le code)

  # ###############
  #
  # client side
  #
  # ###############

  # sending proc

  proc send:data {channel data} \
  {
    set rc [catch \
    {
      puts $channel "[clock seconds] $data"
      flush $channel
    } msg]
    if {$rc == 1} { log $msg }
  }

  # closing proc

  proc close:exit {channel} \
  {
    close $channel
    exit
  }

  # log proc

  proc log {msg} { puts "$::host:$::port: ***$msg" }

  # ===================
  # start
  # ===================

  # open socket

  set host localhost
  set port 6000
  set rc [catch { set channel [socket $host $port] } msg]
  if {$rc == 1} { log $msg; exit }

  # send data

  set pid [pid]
  after 1000 send:data $channel $pid
  after 2000 send:data $channel $pid
  after 3000 send:data $channel $pid

  # close & exit

  after 4000 close:exit $channel

  # enter event loop

  vwait forever

Connexion au serveur

  set host localhost
  set port 6000
  set rc [catch { set channel [socket $host $port] } msg]
  if {$rc == 1} { log $msg; exit }

La connexion se fait par une commande socket ayant pour arguments l'adresse du serveur et le numéro du port demandé. La commande retourne le nom du canal.

Les commandes d'entrée-sortie étant susceptibles de générer des exceptions, la commande socket est dans un catch.

Dialogue

  set pid [pid]
  after 1000 send:data $channel $pid
  after 2000 send:data $channel $pid
  after 3000 send:data $channel $pid

Ici le dialogue est réduit à trois envois de données.

Entrée dans la boucle des évènements

  vwait forever

Les évènements socket ne sont gérés que si le script entre dans la boucle des évènements, sinon les évènements ne seront pris en compte qu'à la fin du script.

Cette boucle est entréee automatiquement à la terminaison du script initial (le script que vous avez écrit) lorsqu'il est exécuté par wish. Sinon il faut utiliser la commande vwait.

Ici la commande vwait entre dans la boucle des évènements et attend une modification de la variable forever. Cette variable n'étant pas modifiée dans le script, cela peut durer longtemps.

Ecriture des données sur le canal

  proc send:data {channel data} \
  {
    set rc [catch \
    {
      puts $channel "[clock seconds] $data"
      flush $channel
    } msg]
    if {$rc == 1} { log $msg }
  }

L'envoi des données sur le canal se fait au moyen de la commande puts (en corrélation avec la commande gets du serveur). La commande puts reçoit deux arguments : le nom du canal ouvert et les données à envoyer.

La commande flush assure que l'envoi est immédiat. Sans cette commande l'envoi se fait lorsque le système le décide (ici, ce serait à la terminaison du script initial).

Les commandes d'entrée-sortie étant susceptibles de générer des exceptions, l'ensemble des deux commandes est dans un catch.

Exercices

Voir ce qui se passe quand :

Mettre le client en mode asynchrone (utiliser fileevent) et vérifier que dans les cas simples ça ne sert à rien (le client ne fait qu'une chose à la fois).


Exemple de session

Voici ce que j'ai obtenu en lançant le serveur puis quatre clients :

  127.0.0.1:1428: <opened>
  127.0.0.1:1429: <opened>
  127.0.0.1:1428: 1169123015 708
  127.0.0.1:1429: 1169123016 636
  127.0.0.1:1428: 1169123016 708
  127.0.0.1:1429: 1169123017 636
  127.0.0.1:1428: 1169123017 708
  127.0.0.1:1430: <opened>
  127.0.0.1:1429: 1169123018 636
  127.0.0.1:1428: <closed>
  127.0.0.1:1431: <opened>
  127.0.0.1:1430: 1169123019 416
  127.0.0.1:1429: <closed>
  127.0.0.1:1431: 1169123020 992
  127.0.0.1:1430: 1169123020 416
  127.0.0.1:1431: 1169123021 992
  127.0.0.1:1430: 1169123021 416
  127.0.0.1:1431: 1169123022 992
  127.0.0.1:1430: <closed>
  127.0.0.1:1431: <closed>

Joli, non ?

Les nombres d'une ligne correspondent à :


Le script du serveur en mode synchrone

  # ###############
  #
  # server side (synchron mode)
  #
  # ###############

  # client connection

  proc server {channel host port} \
  {
    # save client info
    set ::($channel:host) $host
    set ::($channel:port) $port
    # log
    log $channel <opened>
    while {![eof $channel]} \
    {
      # receiving
      set rc [catch { set count [gets $channel data] } msg]
      if {$rc == 1} \
      {
        # i/o error -> log & close
        log $channel ***$msg
        break
      } \
      elseif {$count == -1} \
      {
        # client closed -> log & close
        log $channel <closed>
        break
      } \
      else \
      {
        # got data -> do some thing
        log $channel $data
      }
    }
    catch { close $channel }
  }

  # log

  proc log {channel msg} \
  { puts "$::($channel:host):$::($channel:port): $msg" }

  # ===================
  # start
  # ===================

  # open socket

  catch { console show }
  catch { wm protocol . WM_DELETE_WINDOW exit }
  set port 6000 ;# 0 if no known free port
  set rc [catch \
  {
    set channel [socket -server server $port]
    if {$port == 0} \
    {
      set port [lindex [fconfigure $channel -sockname] end]
      puts "--> server port: $port"
    }
  } msg]
  if {$rc == 1} \
  {
    log server <exiting>\n***$msg
    exit
  }
  set (server:host) server
  set (server:port) $port

  # enter event loop

  vwait forever

Exemple de session synchrone

Voici ce que j'ai obtenu en lançant le serveur puis quatre clients :

  127.0.0.1:1380: <opened>
  127.0.0.1:1380: 1169220120 1912
  127.0.0.1:1380: 1169220121 1912
  127.0.0.1:1380: 1169220122 1912
  127.0.0.1:1380: <closed>
  127.0.0.1:1381: <opened>
  127.0.0.1:1381: 1169220122 416
  127.0.0.1:1381: 1169220123 416
  127.0.0.1:1381: 1169220124 416
  127.0.0.1:1381: <closed>
  127.0.0.1:1382: <opened>
  127.0.0.1:1382: 1169220123 496
  127.0.0.1:1382: 1169220124 496
  127.0.0.1:1382: 1169220125 496
  127.0.0.1:1382: <closed>
  127.0.0.1:1383: <opened>
  127.0.0.1:1383: 1169220123 436
  127.0.0.1:1383: 1169220124 436
  127.0.0.1:1383: 1169220125 436
  127.0.0.1:1383: <closed>

Voir en particulier le troisième nombre (l'heure de lancement de la commande puts)


Voir Aussi


Discussion


Catégorie Exemple | Catégorie Tcl par l'exemple