Activité pratique 2
Durée1h15Programmation d’une application communicante
Introduction
La programmation d’une application communicante suit le principe de découpage en couches protocolaires. L’application opère dans les couches hautes, en s’appuyant sur les services offerts par les couches basses. Les couches basses sont implémentées généralement au sein du système d’exploitation de l’ordinateur.
Pour accéder aux services réseaux implémentés dans le système d’exploitation, celui-ci offre une interface de programmation (API). Dans l’histoire de l’informatique, plusieurs ont vu le jour. L’une d’entre elle a connu un succès particulier: les Socket (historiquement les sockets BSD).
Cette session aborde la programmation Socket en Java au travers d’un exemple proposé dans sa documentation officielle.
Compiler et essayer
Prenez quelques minutes pour lire la page du tutoriel Java qui propose un exemple minimaliste de client-serveur.
Cette page introduit plusieurs concepts:
- socket: cet objet particulier qui sert de point de communication pour l’application, mais qui utilise des ressources offertes en fait par le système d’exploitation;
- try: le mécanisme d’exception de Java qui est (notamment) quasi-indispensable lorsqu’une application utilise des ressources système, et qui offre un moyen de gérer les cas d’erreurs;
- PrintWriter & BufferedReader: deux objets Java pour écrire et lire au travers du socket.
Cette page explique également le code proposé en exemple. (On va le télécharger juste après.)
Relisez la page du tutoriel, vous êtes allé trop vite, je le sais. 😉
Lecture de code
Fonctionnement nominal
-
Préparez-vous un répertoire de travail pour cette séance.
-
Récupérez le code du client et du serveur. Placez-les dans votre répertoire de travail.
-
Compilez ces deux codes
javac EchoClient.java
javac EchoServer.java
-
Ouvrez deux (ou plus) consoles terminal et essayez ce code:
java EchoServer 5050
java EchoClient localhost 5050
Tapez quelques caractères au clavier dans la console du client. Regardez le résultat. Est-ce que ça illustre bien la description donnée par la page du tutoriel ?
Fonctionnement aux limites
-
En gardant vos client-serveur actifs, ouvrez une troisième console et lancez un autre client.
java EchoClient localhost 5050
Tapez quelques caractères au clavier ici et là.
Que se passe-t-il ? et pourquoi selon vous ?
(Rappel: pour arrêter le programme, tapez Ctrl-C.)
-
Dans cette troisième console, toujours en gardant vos précédant client-serveur actifs, essayez maintenant de lancer un autre serveur.
java EchoServer 5050
Lisez le message d’erreur généré (merci la gestion des exceptions).
Cela vous semble cohérent avec la notion de port TCP vu en cours réseau ?
(Faire la correspondance entre les paquets qui arrivent et l’application concernée… mais s’il y a deux applis sur le même port…) -
Dans cette troisième console, essayez de lancer un autre serveur, mais sur un port inférieur à 1024.
java EchoServer 50
Que se passe-t-il ?
Remarquez que le message d’erreur n’est pas le même que précédemment. (Toujours lire avec attention les messages d’erreur.)
Les ports dans l’intervalle 0-1023 sont généralement considérés comme des ports réservés au système (pour des “services”). Normalement, une application d’un utilisateur non-privilégié ne peut ouvrir de socket sur ces ports.
Interopérabilité
Le comportement de cette démo Echo ressemble un peu à ce que fait ncat utilisé dans l’exercice précédent, mais pas complètement. Essayons donc pour voir…
-
Gardez le EchoServer, mais remplacez le EchoClient par ncat.
Tapez quelques caractères dans la console pour voir ce qu’il se passe.
-
Inversement, remplacez le EchoServer par ncat en mode serveur (rappelez-vous de l’option “-l”, listen = serveur), et lacez le EchoClient.
Tapez quelques caractères dans la console de l’un, de l’autre…
Que se passe-t-il lorsque l’on tape du texte dans la console de ncat ?
Que se passe-t-il si l’on tape plusieurs fois du texte (plusieurs lignes) dans la console de ncat, avant de taper du texte dans la console de EchoClient ?
Petites remarques concernant l’interprétation des résultats de nos expériences:
-
Un serveur qui écoute plusieurs clients, c’est compliqué, il faudrait probablement du code en plus.
-
La boucle lecture-écriture est figée, et est naturellement inversée entre le client et le serveur (quand l’un envoie, l’autre doit être prêt à recevoir, et réciproquement).
Si on veut une logique de communication plus souple (on envoie quand on veut, on écoute tout le temps), il faudrait du code en plus.
Compréhension de code
Vous avez fait des expériences sur ce code (connaissances pratiques), vous avez lu les explications de la page du tutoriel (connaissances théoriques). Maintenant que vous le connaissez bien, essayez de commenter ce code. (Il faut toujours mettre des commentaires dans son code, il n’y en a jamais trop.)
Ouvrez ces deux fichiers “EchoClient.java” et “EchoServer.java” dans votre éditeur favori.
Les deux codes se ressemblent furieusement, mais quelles sont les différences ?
-
Il y a deux types d’objet “Socket” et “ServerSocket”
-
Quels sont les paramètres du constructeur de chacun ? et pourquoi à votre avis ?
-
Repérer le “accept()”. Cherchez des explications (ce qu’il prend en paramètre, ce qu’il produit en sortie, et pourquoi tout cela).
-
Notez que l’API socket de java présuppose que “Socket” ce n’est rien d’autre que du TCP; pour de l’UDP c’est un objet DatagramSocket. Et d’autres protocoles (MPTCP, SCTP, DCCP, socket Unix, etc.) sont implémentés par des objets spécifiques.
-
Associé à ce socket, sont créés deux objets pour les lectures et pour les écritures sur ce canal de communication. Et ceci, aussi bien côté client que serveur (c’est symétrique).
-
Côté client, il y a en plus la lecture au clavier. Repérez à quel moment.
-
Repérez la grande boucle. Repérez dans quel ordre chacun fait la lecture puis l’écriture.
-
Repérez également l’utilisation des exceptions: la communication peut ne pas marcher! Quelles sont les cas d’échecs identifiés ?
-
Modification de code
On vous demande quelques modifications à ce code, histoire de se faire la main. À chaque étape, prenez l’habitude de tester: lancez le serveur, le client, faites des essais avec ncat, etc.
-
Côté serveur, affichez en plus sur la console le message reçu.
-
Côté serveur, ne renvoyez plus au client le message reçu; et dans le même temps côté client ne plus attendre la réception d’un message.
-
Côté client, fabriquez un message ad-hoc dans un format à-la-json, plutôt que d’envoyer ce que l’utilisateur a tapé au clavier. (Par contre, dans votre boucle, attendez que l’utilisateur ait tapé Entrée, avant d’envoyer votre message.)
Bien, on est pas très loin d’un scénario de capteur qui envoie ses mesures à un serveur là…
Commentaire de code
Récupérez les fichiers Java: Sensor.java SensorClient.java SensorServer.java.
-
Compilez, essayez.
-
Ouvrez ces fichiers Java dans votre éditeur préféré. Prenez un papier et un crayon et faites-en le diagramme de classes.
-
Ces codes manquent cruellement de commentaire, n’est-ce pas ? Il faut y Remédiez !
Serveur multiclients
Encore une fois, notre serveur n’accepte que un seul client à la fois. Avouez que ce n’est pas bien pratique…
Pour y remédier, on empoie principalement deux stratégies possibles:
- Un serveur concurrent: chaque fois que le serveur reçoit une demande de connexion, il va créer un nouveau sous-processus ou un thread pour gérer la communication avec ce client-là;
- Un serveur synchrone: le serveur a une grande boucle événementielle, il gère une liste des communications en cours, et à chaque tour de boucle il regarde s’il s’est passé quelque chose sur l’une de ses communications, et si oui il la traite (typiquement en appelant des fonctions de callbacks que le programmeur a associé à chaque événement ou flux).
Pour notre exercice, on vous propose donc un code de serveur multiclients. Récupérez les fichiers SensorServerMulti.java et ServerThread.java.
-
Compilez, essayez.
-
Devinez à quelle stratégie il fait référence.
-
Comme précédemment, faites le diagramme de classes.
-
Puis commentez le code.
IPv6 vs IPv4
Vos programmes, ils font de l’IPv4 ou de l’IPv6 ?
(N’hésitez pas à lancer Wireshark pour vérifier.)
-
Notez que le code ne fait pas de différence. Il fonctionne aussi bien en IPv4 qu’en IPv6. Si l’utilisateur donne des adresses IPv4, ce code fait de l’IPv4, et si l’utilisateur lui donne des adresses IPv6, ce code fait de l’IPv6.
-
Oui mais là, dans notre manière de les utiliser, on n’a pas précisé d’adresse IP. On a utilisé un nom de serveur.
Un nom, ça peut se traduire aussi bien par des adresses IPv4 que IPv6, ou les deux. C’est bien normal. Et c’est le cas ici: localhost se traduit en IPv4 par l’adresse 127.0.0.1 et en IPv6 par l’adresse ::1.
Lorsqu’il y a les deux, qui a choisi ?
Réponse: la JVM, suivant la manière dont elle a été configurée. Vous pouvez donc la forcer dans un sens ou dans l’autre en positionnement vos préférences. -
Pour pouvoir fonctionner aussi bien en IPv4 qu’en IPv6, notre Socket utilise en fait des adresses mappées.
En gros, ce sont des sockets IPv6 normales, sauf que si on leur donne une adresse IPv4, cette adresse va être traduite en IPv6 en utilisant un préfixe particulier (’::ffff:’) et la pile protocolaire du système d’exploitation va faire de l’IPv4. Bref, le programmeur n’a que des sockets IPv6 à gérer, et c’est le système d’exploitation qui se débrouille.
-
Une autre stratégie de transition IPv4 → IPv6 qui a été employée à une époque consistait pour le programmeur à gérer systématiquement deux sockets distincts, l’un pour faire de l’IPv4, l’autre pour faire de l’IPv6.
Bien compliqué (double de code), et risqué si l’on mélange avec des adresses mappées (car alors l’IPv4 va être pris en charge à la fois par le socket IPv4, et le socket IPv6 mappée, et donc créer un conflit…)
La suite ?
Bravo si vous êtes arrivé jusque-là pendant la séance.
Si ce n’est pas le cas, terminez à la maison.
S’il vous reste un peu de temps, jetez un œil sur l’exercice optionnel qui parle du chiffrement des communications.