1. TD numéro 1 : Communication via des sockets

La section 1 positionne les questions étudiées tandis que la section 2 présente brièvement les classes qui seront utilisées. Leur lecture est optionnelle car ce sont potentiellement des rappels.

Les questions commencent en 1.4.

1.1. Prérequis, liens utiles

  • En cas de problème Instaler la JDK, à priori c’est déjà fait surtout si vous avez instalé Eclipse. Ou la trouve ici

  • Instaler telnet : Sous linux ou MacOSX ce petit programme est instalé par défaut. Sous windows entrer dans une invite de commande admin

    dism /online /Enable-Feature /FeatureName:TelnetClient
    
Voir aussi récupérer telnet pour d’autre méthodes. Telnet permet d’ouvrir manuellement des connections réseau (TCP) et donc de tester un serveur ou un protocole. Il est ainsi possible d’effectuer des requêtes HTTP élémentaires en utilisant telnet ou encore d´envoyer un email (vous trouverez de nombreux exemple sur internet, comme celui-ci envoyer un mail avec telnet.
  • Vérifier les chemins (en particulier sous windows) , diverses méthodes sont données Ici
Contact :
Stéphane Perennes stephane.perennes@cnrs.fr
Documentations :
Le site d’Oracle fournit des Tutoriaux ainsi qu’une documentation officielle. Les classes sont documentées Ici Par ailleurs les environements de devellopement intégrés fournissent une documentation immédiate.

1.2. Contexte et Objectif:

1.2.1. Importance de l’échange de messages réseau

L’élément de base de la programmation distribuée est la communication entre processus. Une des méthodes les plus générales et élémentaire pour faire communiquer deux processus consiste à simplement passer des messages sur le réseau. Notez bien que le réseau est souvent utilisé afin d’échanger des messages entre des processus situés sur la même machine physique. L’objectif de cette fiche est d’apprendre à faire communiquer des processus différents via des échanges de messages sur le réseau.

Des milliers de protocoles sont basés sur de simples échanges de messages, souvent même en mode texte et en clair, certains sont même lisible par un humain. Par exemple :

  • le protocole HTTP utilisé pour distribuer des pages web est principalement basé sur deux messages GET et POST.
  • le courrier electronique.
  • Certains player multimédia sont controlé via de échanges de message (VLC, mpd)
  • Il est courant de fournir une interface réseau permettant de télécommander et de contrôler un programme ou un service.

1.2.2. Les autres méthodes de communication

Par ailleurs c’est souvent par dessus ces méthodes de communication que sont construites des couches plus hautes, plus abstraites et plus complexes (appels de procédure distants, appels de méthodes rpc, rmi).

Il existe d’autres méthodes permettant de communiquer, en particulier dans le cas de processus s’exécutant sur une même machine, ceux ci partageant le système d’exploitation, la mémoire et le système, il leur est possible de communiquer de bien d’autres manières (thread, signaux unix, mémoire partagée, messages via le système de fichier, messages via des objets partagés).

Ceci dit l’utilisation de la couche réseau a l’avantage d’être universelle, deux processus distants hétérogènes (implémentés dans des langages différents, s’éxécutant sur des systèmes différents etc ..) peuvent toujours communiquer du moment qu’ils ont accès à la couche réseau.

1.2.3. UN réseau “orienté paquet”, Opération bloquantes, timeout

Les protocoles réseau courants sont orientés paquet et non pas session ou connexion. Ainsi quand on écoute un flux d’entrée il n’existe à priori aucun moyen de savoir si l’autre intervenant dans la communication est encore actif. Sa réponse peut être simplement tardive, mais il peut aussi être en panne (plantage), etc... La couche TCP/IP ne garantit que deux choses : les paquets arrivés sont valides, et les paquets arrivent dans le bon ordre.

En Java, par défaut un bon nombre de méthodes procurées par les objets réseau (accueil de client, lecture de message entrant) sont Bloquantes. Par exemple, par défaut, la lecture d’un message entrant est bloquante, ceci signifie que le flux d’exécution va s’interrompre jusqu’à ce qu’un message soit arrivé. Notez que les emissions de message sont non bloquantes, car envoyer un message consiste à le placer sur le réseau sans savoir si et quand il sera recu.

Les opérations bloquantes peuvent conduire à des pannes (i.e. atttentes infinie). Afin d’éviter cela on peut positionner en option un Timeout (délai maximum). Le cas typique est celui de deux processus échangeant des messages selon un protocole, si l’un deux vient à cesser de répondre le second peut être bloqué (si il attend une réponse). Le positionement d’un timeout permet donc à celui-ci de detecter le depassement du délai maximum et de gérer lanon réponse (plus exactement la non réponse à temps) du premier.

En Java les timeouts sont gérés via un mecanisme d’exception et de try/catch. Une attente top longue déclenche une exception particulière.

1.3. Librairies et Classes Utilisées

Les classes sont documentées sur le site d’Oracle :Javadoc

On va utilser les librairies standard réseau, entrées sortie et utilitaires.

1
2
3
  import java.net.*;
  import java.io.*;
  import java.util.*;

1.3.1. Classes Réseau

Principalement vous utiliserez les classes suivantes :

  • ServerSocket qui fournit une classe Serveur, cette classe implémente un objet qui va écouter sur un port donné et attendre une demande de connection. La classe Serveur fournit donc une salle d’attente où faire attendre d’éventuels clients. Quand un tel objet est créé sur un port $p$ tout client qui se connecte sur ce port est accueilli et placé en attente, les paquets qu’il envoie sont aussi acceptés et conservés par le SocketServer.
  • En dehors de la méthode création la méthode principale de cette classe est accept qui place le demandeur en attente et lui retourne un client dès qu’il y en a un. Le client est fourni via un objet Socket retourné par la méthode accept, ceci permet de communiquer avec avec le client. Notons enfin que la socket va alors contenir les paquets que le client a envoyé quand il était en attente.
  • Une autre méthode importante est close car qui termine (coté serveur) les connections des clients de la salle d’attente. Cela devrait donc aussi deconnecter les clients.
  • La classe dispose de bien d’autres méthodes, on peut en effet limiter la taille de la salle d’attente, spécifier un timeout. Pour plus voir les spécifications ServerSocker
  • Socket cette classe permet d’établir des connections, le système d’exploitation et la JVM tentent d’ouvrir la connection lors de la création de l’objet. Une socket est une connection bidirectionnelle, aussi la socket est associée à deux “flux de bits”, un flux entrant et un flux sortant.
    • On va écrire sur la sortie qui est un OutputStream (flux de bit sortant) on accède à ce flux via la méthode getOutputStream().
    • On va lire le données recues sur l’entrée qui est un InputStream (flux de bits entrant) on accède à ce flux via la méthode getInputStream().
    • Pour spécifier un timeout pour la lecture de données on utilise la méthode SetSosetSoTimeout

1.3.2. Classes Entrée Sortie

Les Flux (Streams) - En Java la notion de “flux” entrant (file, flot, stream) d’objets correspond à une séquence potentiellement infinie (dans une direction) d’objets. Un flux est un peu comme l’ensemble des entiers. Une chaine de caractère est un flux de caractères, un fichier est un flux de bits, et une connection réseau entrante est un flux.

  • De facon analogue, Java propose la notion un “flux” sortant (file, flot, stream) d’objets.
  • Étant donnée une socket, le flux de sortie retourné par getOutputStream() (respectivement le flux entrant retourné par InputStream) est un flux de bits. Le flux de sortie (resp. flux entrant) se manipule comme un fichier ouvert en écriture (resp lecture).
  • En général on encapsule ces flux dans des flux de caractères de plus haut niveau.

1.3.3. Bit, caractère, mise en tampon

  • Si on échange des informations en utilisant des sockets (via donc des streams), on a besoin de traduire les bits en caractères. Ceci pose le problème du codage des caractère en binaire (bits). Longtemps ce fut un sujet de maux de têtes car les caractères étaient codés sur 1 octet et il existait un codage par langue, même deux langues voisines avaient des codage différents (p.ex les lettre accentuées du Portugais diffèrent de celle du Francais). De plus chaque famille de système d’exploitation utilisait un codage qui lui était propre (ie Windows, Linux, Apple).
  • Comme en général le concepteur souhaite écrire/lire des chaines de caractères et non pas des bits sur un stream, Java procure un mécanisme d’encapsulation qui permet de manipuler des caractères, ce mecanisme converti les flux de bits en flux de caractère. L´ecapsulation permet aussi d’utiliser des opération d’entrée sortie de haut niveau (print(), format(), println(), readline()) alors que les flux de bas niveau ne proposent que les opération atomiques write et read.

Les 2 lignes de code ci dessous encapsulent les 2 flux de bit dans des flux de caractères.

ma_connection=Socket()
InputStreamReader isr = new InputStreamReader(ma_connection.getInputStream(), "UTF-8");
PrintWriter ma_sortie = new PrintWriter(ma_connection.getOutputStream() , true);

Javadoc about InputStreamReader

An InputStreamReader is a bridge from byte streams to character streams: It reads bytes and decodes them into characters using a specified charset. The charset that it uses may be specified by name or may be given explicitly, or the platform’s default charset may be accepted.

ma_connection = new Socket("127.0.0.1", 2017);
ma_connection.SetSotimeout(5000)

InputStreamReader isr = new InputStreamReader(ma_connection.getInputStream(), "UTF-8");
BufferedReader flux_entrant = new BufferedReader(isr) ;

nouveau_message=flux.entrant.readline()

1.4. Serveur Mono, ne traitant qu’un client

1.4.1. Le B.A.BA , un serveur jouet

Écrire une classe Serveur_Jouet qui :

  1. crée un gestionnaire de socket sur le port 12000 (ServerSocket).
  2. Attend une connections entrante et associe à celle-ci une socket (Socket) quand elle arrive.
  3. Lit de façons bloquante et ligne à ligne les données recues et les affiche à l’écran avec un compteur de lignes (on utilisera les classes InputStreamReader BufferedReader).
  4. On veillera à traiter au minimum les erreurs potentielles d’entrée sortie.

Afin de tester le fonctionnement on utilisera telnet (telnet 127.0.0.1 12000) pour se connecter au Serveur Jouet.

  1. Que ce passe t’il si on lance plusieurs sessions telnet ?
  2. Que deviennent les sessions telnet si on arrête le serveur ?
  3. Que se passe t-il si on lance une seconde instance du Serveur Jouet ?

1.4.2. Un Serveur Amélioré

On veut modifier le Serveur afin qu’il puisse traiter sequentiellement

plusieurs connections. Afin que cela soit possible il faut pouvoir terminer une connections. La terminaison va s’opérer à l’initiative du client, celui-ci terminera la connection en envoyant une séquence de caractère spéciale que nous notons FINISH.

On rendra compte du comportement du serveur (démarrage, prise en charge, terminaison etc ...) via des messages sur la sortie standard.

À partir de la classe Serveur_Jouet, définir la classe Serveur_multi qui obéit aux spécifications suivantes :

  1. Un des créateurs (à priori c’est le seul) associé à classe prend comme argument le numéro de port sur lequel le serveur va écouter, cet argument est fourni par l’utilisateur (ex: java Serveur_Multi 1234). Il sera aussi souhaitable de vérifier que l’argument correspond bel et bien à un numéro de port correctement formaté.
  2. La méthode run() Examine si des connections sont actives sur Server_Socket, si oui elle en prend une en charge et lui applique la méthode Service_Client.
  3. La méthode Service_Client lit puis affiche (sur la sortie standard) les données envoyées par le client (comme dans le cas du Serveur_Jouet. Cependant elle recherche dans chaque ligne envoyée par le client la séquence de terminaison FINISH, si cette séquence apparait la méthode Service_Client se termine, la socket est alors close et l’on retourne écouter le Serveur_Socket.
  4. Le choix de la séquence de terminaison FINISH vous est laissé, attention toutefois de ne pas utiliser quelque chose de trop commun (pex “\(\backslash\)n”, “ab”). Le caractère EOT, ctrl-D dont le code ASCII est 4 est un choix acceptable.

Tests: (telnet encore)

  1. Que se passe t’il si plusieurs sessions telnet sont lancées concurremment ?
  2. En quoi envoyer la séquence FINISH diffère de “tuer” le processus telnet (kill, ctrl-c) ?
  3. Faire divers tests avec des telnet concurrents que l’on terminera.
  4. Quelle entité gère les connections bloquées en attente de service ?

1.5. Un Client Basique

1.5.1. Un client Put

Ce client jouet sera implémenté via une classe Mini_Client, qui s’utilisera comme suit : java Mini_Client hote port NOM. On modifiera très légerement Serveur_Multi afin qu’il detecte le nom du client et accuse réception de la connection.

  • Le client cherchera à se connecter au serveur, ensuite une fois la connexion établie :
  • il annoncera son NOM , pour cela il utilisera une annonce formatée selon des règles de votre choix (par exemple “Hello je suis: NOM”).
  • Le serveur détectera cette annonce en utilisant un analyseur d’expressions régulières (classes Pattern, Matcher du paquet java.util.regex. Pour tester l’expression régulière on pourra utiliser http://www.regexplanet.com/advanced/java/index.html et determinera ainsi le NOM du client.
  • Le serveur retournera alors un accusé de réception (pex “Bonjour NOM”).
  • Le client transmettra alors des données (on pourra au choix transmettre une chaîne aléatoire (i.e n importe quoi) ou structurée (pex. l’alphabet répété 100 fois) de caractère, ou le contenu d’un fichier).
  • le client cloturera alors la session.

Option : Bien que la correction des erreurs soit assurée par la couche réseau tcp/ip , pour s’assurer de la bonne transmission on pourra :

  • Demander que le serveur retourne comme accusé de réception le hash des données reçues.
  • Le client controlera alors que ce hash correspond bien à ses données, si oui il cloture la session.
  • Sinon le client renverra les données, jusqu’à ce que la transmission soit correcte.

1.5.2. Pour aller plus loin : Put et Get

Section optionnelle à faire à la maison pour les motivés. Étoffer le client et le serveur afin qu’un client puisse effectuer les trois opérations suivantes :

  1. Consulter la liste des données stockées par le serveur.
  2. Poser un ensemble de données sur le serveur , cet ensemble sera identifié par les couple (nom du client, nom de la donnée).
  3. Récupérer un ensemble de données.

Note

Afin d’éviter des problèmes liés au passage de messages de contrôle et de données sur le même canal on pourra décider que toute ligne envoyée commençant par ctrl-D est une ligne de contrôle et supposer que les données ne contiennent pas telles lignes ou échapper de telles lignes.

Le serveur et le client étant assez symétrique on pourra essayer de structurer le programme afin de leur faire partager du code ou des objets.