Client / Serveur
La curiosité n’est pas un vilain défaut mais une qualité fondamentale.
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 client qui demande un service,
- le serveur qui fournit le service.
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
- le serveur attend la connexion d'un client pour lui fournir un service.
- un client se connecte à un serveur, reçoit un service, puis se déconnecte du serveur.
- le serveur peut servir simultanément (ou séquentiellement) plusieurs clients.
- la connexion établit un canal (d'échange) entre les deux processus.
- la déconnexion supprime ce canal.
- les échanges entre les deux processus respectent des règles qui forment un protocole.
- ce protocole définit une session entre deux processus.
- un processus a une adresse (réseau) (adresse IP sous TCP/IP).
- un service a un nom (numéro de port sous TCP/IP).
Opérations de base
- 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.
- 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.
- Connexion du client : au serveur défini par son adresse réseau et le nom du service.
- Consommation du service : suivant les règles du protocole.
- Déconnexion du client : suivant les règles du protocole (ou anormale).
- Spécificité des sockets sous TCP/IP : lorsque le serveur reçoit une demande de connexion sur le port d'attente, il ouvre un canal sur un autre port et envoie le nouveau numéro de port au client qui va dialoguer avec le serveur à travers ce nouveau canal. Le canal d'attente redevient disponible pour la connexion d'un autre client.
Commandes de base
Attente du serveur
- La commande socket avec l'option -server permet d'ouvrir un socket serveur. Elle retourne le nom de canal du socket serveur.
set channel [socket -server command ?options? port]
- command est le nom d'une commande de call-back qui recevra en arguments, lors de la connexion d'un client, le nouveau numéro de port, l'adresse et le port du client. Elle retourne le nom du canal serveur.
- port est le numéro de port sur lequel devront se connecter les clients. Si ce numéro est 0, un numéro sera choisi par le système et pourra être connu au moyen de la commande :
set port [lindex [fconfigure $channel -sockname] end]
Connexion du client
- La commande socket sans l'option -server permet d'ouvrir un socket client vers un port serveur. Elle retourne le nouveau nom de canal choisi par le serveur.
set channel [socket ?options? host port]
- host e l'adresse IP du serveur (localhost s'il est sur la même machine que le client). st
- port est le numéro du port sur lequel le serveur est en attente.
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
- Ouverture du socket
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.
L'entrée dans la boucle des évènements
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ée 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.
La procédure de connexion
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.
Le call-back de lecture sur le canal
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 :
- si des données sont disponibles,
- en fin de canal (clôture du canal normale ou anormale),
- si une erreur est intervenue.
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 :
- on supprime la commande vwait
- on n'utilise pas la commande eof
- on ne teste pas si la commande gets retourne -1
- on utilise le port 0
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ée 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 :
- on supprime la commande vwait
- on supprime la commande flush
- on change l'adresse du serveur ou le numéro de port
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 à :
- l'adresse IP du serveur
- le numéro de port utilisé pour le client
- l'heure (en secondes) du lancement de la commande puts
- le process id du client
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)
La suite est placée pour info, mais les liens ne sont plus fonctionnel
Voir Aussi
- Les autres exemples : Tcl par l'exemple
- -
- A(Ready-to-use)Server (un serveur tout prêt) : http://wfr.tcl.tk/fichiers/ulis/
- The simplest possible socket demonstration : http://wiki.tcl.tk/15315
- A little client-server example : http://wiki.tcl.tk/15539
- A Server Template : http://wiki.tcl.tk/9414
- client/server with fileevent : http://wiki.tcl.tk/1757
- Network server application template : http://wiki.tcl.tk/8301
- Simple Server/Client Sockets : http://wiki.tcl.tk/5947
Mis à jour le 28 juin 2010 à 11:05 CEST par Kroc.