TP 5 : Programmation réseau en Java
Transcription
TP 5 : Programmation réseau en Java
TP 5 : Programmation réseau en Java [email protected] Les TPs sont individuels. A la demande de l’enseignant vous envoyez (par mail) vos fichier *.java ainsi qu’un petit compte rendu expliquant ce que vous avez fait. Lorsqu’un TP est composé de plusieurs exercices, l’enseignant vous demandera de changer d’exercice au fur et à mesure de la séance même si vous n’avez pas fini votre exercice. Ainsi vous pourrez aborder toutes les notions présentées dans le sujet. Pendant les séances de TP, placez Eclipse sur l’un de vos bureau, OpenOffice sur un autre et Firefox sur un troisième. Firefox vous permet de consulter (exclusivement !) les pages de documentations de Sun : http ://java.sun.com/j2se/1.5.0/docs/api/ But du TP Dans ce TP vous allez travailler avec les composants réseaux de Java. Dans un premier temps, vous allez construire un serveur et un client élémentaires puis vous allez les modifiez pour utiliser le protocole HTTP. 1 Bases théoriques 1.1 Les flux et les fichiers en Java Dans les lignes qui suivent les mécanismes de base des fichiers et des flux vont être présentés. Cette présentation n’est absolument pas exhaustive, elle permet juste de pouvoir aborder la suite du TP sereinement... 1.1.1 Les fichiers Les fichiers sont supportés dans Java par la classe File. Pour créer un objet File, le plus simple et de passer le nom du fichier en paramètre du constructeur. On dispose ensuite de nombreuses méthodes de manipulation de fichier : lecture des bits d’états (isHidden, canRead, canWrite), manipulation (create, mkdir, ...)... 1.1.2 Les flux “élémentaires” L’accès aux donnés extérieures est assuré par des flux. Les flux matérialisent des données sortantes ou entrantes, cette représentation est basée sur deux classes abstraites : InputStream pour les données entrantes et OutputStream pour les données sortantes. Les descendants de ces classes permettent d’accéder à des données depuis différentes sources. Par exemple, FileInputStream et FileOutputStream permettent de construire des flux entrants et sortants à partir du objet File. Pour 1 lire des données à partir d’un FileInputStream on utilise la méthode read qui renvoie un octet, pour écrire un octet on utilise la méthode write de la classe FileOutputStream. 1.1.3 Les flux “enveloppants” Souvent, les données à lire ou à écrire sont stockées sous un format plus complexe que les octets, ce peut être des caractères, des chaînes de caractères, des nombres... Java propose des classes qui convertissent des données complexes (chaînes de caractères, chiffres...) pour les envoyer dans un flux élémentaire. Pour stocker les types élémentaires (entiers, flottants, booléen,...) les méthodes DataInputStream et DataOutputStream proposent des méthodes de conversion. Les deux classes les plus utilisées sont InputStreamReader et OutputStreamWriter. InputStreamReader convertit les octets du flux entrant en caractères 1 accessibles via les méthodes read. Réciproquement, OutputStreamWriter permet de convertir des caractères en octets à destination d’un flux sortant. Pour manipuler des objets de type String, Java propose deux classes de conversion : BufferedReader qui convertit les données entrantes d’un InputStreamReader en String et BufferedWriter qui convertit les objets String pour les envoyer dans un OutputStreamWriter. Les codes sources 1 et 2 présentent l’utilisation de ces classes pour manipuler des fichiers textes. Code source 1 Lecture de chaînes de caractères File fichier = new File("Document.txt"); FileInputStream fi = new FileInputStream(fichier); InputStreamReader isr = new InputStreamReader(fi); BufferedReader br = new BufferedReader(isr); String chaineLue = br.readLine(); System.out.println(chaineLue); br.close(); isr.close(); fi.close(); L’écriture de chaînes de caractères peut encore être simplifiée en utilisant la classe PrintWriter. Le constructeur admet soit un File, un nom de fichier ou un OutputStream. On dispose ensuite de méthodes comme print et println pour écrire des chaînes de caractères. 1.2 Le réseau TCP en Java La gestion des communications réseaux en Java est assurée par des flux. Une fois la connexion établie, l’envoie et la réception de données utilisent les mêmes 1 La conversion n’est pas aussi triviale qu’elle n’y parait. Selon, l’encodage choisi, un caractère peut prendre plusieurs octets. 2 Code source 2 Écriture de chaînes de caractères File fichier = new File("Sortie.txt"); FileOutputStream fo = new FileOutputStream(fichier); OutputStreamWriter osr = new OutputStreamWriter(fo); BufferedWriter bw = new BufferedWriter(osr); String maChaine; maChaine = "Bonjour"; bw.write(maChaine); bw.flush(); bw.close(); osr.close(); fo.close(); méthodes que les fichiers. Dans une connexion réseau on distingue deux entités différentes : le (ou les) client(s) et le serveur. 1.2.1 Les clients La classe Socket du package java.net permet d’établir une connexion entre deux ordinateurs. L’ordinateur attendant une connexion est le serveur, celui demandant une connexion est le client. Un des constructeurs permet de spécifier le nom de l’hôte et le port de connexion lors de la création de l’objet. Une fois la connexion établie, les données envoyées et reçues sont accessibles via les flux entrant et sortant de ce Socket. Les méthodes getInputStream et getOuputStream permettent d’accéder à ces flux pour envoyer ou recevoir des données. Elles renvoient des objets descendants de InputStream et OutputStream. Pour fermer un Socket et donc terminer la connexion, on utilise la méthode close. 1.2.2 Les serveurs Une application serveur attend un client pour établir une connexion sur un port donné. Pour construire un serveur sur un port on utilise la classe ServerSocket. Le constructeur n’admet qu’un paramètre, le numéro du port. Ensuite, on utilise la méthode accept qui bloque l’application tant qu’un client n’a pas demandé de connexion. Cette méthode renvoie un objet socket représentant la connexion sur le client une fois la connexion établie. Ensuite, le programme se comporte de la même manière que pour le client, les méthodes getInputStream et getOutputStream étant utilisées pour accéder aux flux. La méthode getInetAdress permet de connaître l’adresse du client. A la fin des transactions, les deux sockets (le ServerSocket et le socket client doivent être fermés en utilisant la méthode close). 3 1.2.3 Le protocole UDP Java permet aussi de communiquer à l’aide du protocole UDP. Pour cela, on utilise la classe DatagramSocket pour créer le socket de connexion et la classe DatagramPacket pour créer le paquet de données à envoyer. Nous n’irons pas plus loin dans le cadre de ce TP concernant le protocole UDP. 1.3 Introduction au protocole HTTP 1.3.1 Formatage des adresses Les adresses internet sont de la forme http ://serveur/document. Par exemple les pages de documentation de Java se situent sur http ://java.sun.com/j2se/1.5.0/docs/a L’entête http représente le protocole utilisé (HTTP, HTTPS, FTP, ... selon les sites). Le nom du serveur commence souvent par www mais ce n’est pas obligatoire (voir l’exemple proposé où c’est java.sun.com) enfin, le reste de l’adresse correspond au chemin pour atteindre le document. 1.3.2 Le protocole HTTP Le protocole HTTP (Hyper Text Transfert Protocole) est le protocole majoritairement utilisé par les serveurs Web. Il est souvent basé sur une connexion TCP sur le port 80. Le serveur et le client s’échange les données sous forme de chaînes de caractères non cryptées. Les commandes sont peu nombreuses et simples d’utilisation. Les commandes peuvent être suivies de paramètres (nommés champs) sous la forme Champ : valeur à raison d’un champ par ligne. Après chaque commande envoyée par le client le serveur envoie une réponse (composée d’un code, de champs et du corps de la réponse chaque partie étant séparée par une ligne vide) et clôt la connexion. Un échange est donc de la forme : Client : Serveur : Serveur : COMMANDE Champ : Valeur Champ : Valeur HTTP :/x.x Code Explication Champ : Valeur Champ : Valeur Corps de la réponse Fermeture de la connexion La commande GET C’est la commande la plus utilisée. Elle permet au client de demander un document (ou une ressource) situé à l’URL passée en paramètre suivie du protocole choisi (HTTP/1.0 ou HTTP/1.1). La commande GET ne nécessite pas de paramètre. Pour passer des variables au serveur on les encode à la suite de l’URL à l’aide du caractère ?. Par exemple pour ajouter la variable a=10 et b=12 à l’url http ://monsite/mapage.php on écrira http ://monsite/mapage.php?a=10&b=12. 4 Exemple d’utilisation : Après s’être connecté au site java.sun.com, on demande la page d’accueil2 en envoyant la chaîne de caractères : GET / HTTP/1.0 suivie d’une ligne vide (retour chariot). Le serveur renvoie alors diverses information 3 : HTTP/1.1 200 OK Server : Sun-Java-System-Web-Server/6.1 Date : Mon, 12 Dec 2005 09 :47 :12 GMT ... Connection : close < !DOCTYPE HTML PUBLIC ...> <html> ... </html> La première ligne renvoyée est une ligne représentant le statut de la demande. Elle commence par HTTP suivie de la version du protocole supportée puis des codes représentant l’état du traitement de la requête. Le code retour le plus courant est 200 OK qui signifie que la transaction s’est bien déroulée. Quelques codes sont bien connus des internautes comme 404 Not found qui est envoyé lorsque l’on demande un document qui n’existe pas ou 403 Forbidden qui est renvoyé lorsque le client demande une ressource à laquelle il n’a pas accès. Les lignes suivantes sont de la forme Champ : Valeur. On retrouve le nom du serveur, la date de la demande, ... Plusieurs champs sont souvent utilisés par le client : – Content-Length : C’est la taille en octets du corps de la réponse. – Content-Type : Le type MIME de la réponse est renvoyé (par exemple : text/html, image/png, ...). Le client utilise cette information pour pouvoir interpréter correctement les données de la réponse. – Last-Modified : Certains serveurs renvoient la date de la dernière modification du document, cette information est utilisée pour gérer les caches sur les navigateur en complément de la commande HEAD (voir ci-dessous). Une ligne blanche est insérée entre les champs et le corps du message. Ensuite, le document demandé est envoyé, dans l’exemple ci dessus, c’est une page HTML. La commande HEAD La commande HEAD est identique à la commande GET sauf que le serveur ne renvoie que l’entête du message (donc seulement le statut de la demande et les champs). Elle est utilisée pour vérifier qu’une page a été modifiée par rapport à celle présente dans le cache du navigateur avant de la télécharger. La commande POST POST a un fonctionnement proche de GET mais permet d’envoyer des données dans le corps de la demande. Cette méthode est souvent utilisée pour envoyer des données sans modifier l’adresse ou pour envoyer des données trop 2 3 La page d’accueil est généralement référencée par /. Seules quelques informations importantes ont été recopiées, le reste a été remplacé par des ... 5 longues pour être passées dans l’adresse. Les champs envoyés comprennent (au minimum) la taille du corps ainsi que l’encodage utilisé au format MIME. Par exemple, pour envoyer les données a=10 et b=12 à l’url http ://monsite/mapage.php on écrira : POST /mapage.php HTTP/1.0 Content-Type : application/x-www-form-urlencoded Content-Length : 9 suivie d’une ligne vide a=10&b=12 suivie d’une ligne vide Pour envoyer une image, on utilisera la même approche POST /saveimage.php HTTP/1.0 Content-Type : image/jpeg Content-Length : 32768 suivie d’une ligne vide Les octets composant l’image suivie d’une ligne vide Les deux méthodes (POST et GET) sont assez proche l’une de l’autre. Elles renvoient toutes les deux le même type de résultat. Les différences sont les suivantes : – La méthode GET ne permet d’envoyer qu’une faible quantité de données car elles sont ajoutées à l’URL (qui a une taille limité). – Généralement, les résultats de la méthode GET sont mis en cache par les navigateurs ce qui n’est pas le cas des résultats de la méthode POST (d’où les messages du type “La page que vous tentez... données POSTDATA...” de Firefox). 1.3.3 Test du protocole avec telnet Pour comprendre le protocole HTTP nous allons faire quelques essais avec le logiciel telnet. telnet est un protocole et un logiciel utilisant ce protocole pour se connecter à une machine distante. Pour cet exercice, nous allons utiliser telnet pour se connecter à une machine distante herbergeant un site internet. Connectez vous à un serveur web (java.sun.com par exemple) en utilisant telnet dans un terminal : telnet serveur 80 Si la connexion s’est bien passée, vous obtenez un message du type : Trying XX.XX.XX.XX Connected to serveur (XX.XX.XX.XX) Escape character is ’^]’ A partir de maintenant, toutes les chaînes que vous entrez sont envoyées au serveur après l’appui sur entrée (ou un caractère retour chariot). Utilisez la commande GET pour obtenir une page web. 6 2 Travail à réaliser Pour ce TP, créez un nouveau projet sous Eclipse. Ce TP est composé de trois parties, la création d’un serveur simple, d’un client et d’un serveur Web simple. 2.1 Un serveur simple Nous allons construire un serveur simple local (sur 127.0.0.1) et nous allons utiliser le programme telnet comme client. 2.1.1 Création des sockets Créez une nouvelle classe (par exemple Serveur) qui contient une méthode main. Dans cette classe ajoutez un constructeur. La première chose à faire lors de la construction de l’objet et de créer un objet ServerSocket sur un port libre (par exemple 2500) : ServerSocket sock = new ServerSocket(2500); Dès que vous ajoutez ces lignes, Eclipse signale la présence d’une éventuelle exception. Choisissez l’option qui permet de gérer cette exception (Surround with...) et ajoutez un message d’erreur pertinent dans la partie catch. A partir de maintenant tout votre code sera situé entre les accolades qui suivent try. Votre programme doit attendre qu’un client se connecte et ensuite il doit récupérer le socket sur ce client. Tout ceci se fait à l’aide de la méthode accept de l’objet ServerSocket. Cette méthode (bloquante) attend un client et renvoie un objet Socket sur le premier client connecté. Attendez un client et affichez son adresse IP puis fermez les Socket grâce à la méthode close. Pour tester cette partie, ajoutez un objet Serveur dans le main. Lancez votre programme puis dans une console, lancez telnet 127.0.0.1 2500. Cette instruction exécute le programme telnet en client sur l’adresse local sur le port 2500. Normalement, votre application doit vous afficher l’adresse IP du client (ici 127.0.0.1). Tuez votre application pour continuer le travail. 2.1.2 Ouverture du flux sortant Nous allons créer le flux sortant qui envoie des données du serveur vers le client. Un objet PrintWriter construit4 à partir du flux sortant permet d’écrire dans le flux en utilisant les méthodes print et println. : PrintWriter pwSock = new PrintWriter(socketClient.getOutputStream(), true) ; Dès que le client se connecte, le serveur doit lui envoyer la chaîne “Bonjour” : pwSock.println(“Bonjour”); 4 Le second paramètre du constructeur est un booléen que l’on place à true pour forcer la purge du buffer à chaque ajout de ligne. 7 2.1.3 Ouverture du flux entrant La classe Socket renvoie un InputStream représentant le flux entrant à l’aide de la méthode getInputStream. Un InputStream ne peut pas être directement utilisé pour accéder aux données. On doit utiliser un objet de la classe InputStreamReader construit à partir de ce flux pour pouvoir obtenir des caractères (la classe InputStreamReader convertit les octets en caractères) : InputStreamReader isrSock = new InputStreamReader(client.getInputStream()); Créez un tableau de caractères (char) de 10 éléments. Passez ce tableau en paramètre de la méthode read de l’objet isrSock. Ajoutez une ligne pour afficher le contenu du tableau. Testez de nouveau votre programme avec telnet. Une fois telnet lancé, tapez quelques caractères, ils doivent s’afficher dans la console d’Eclipse. Que se passet-il si vous tapez 5 caractères, 10 caractères ? Cette approche ne permet de lire que quelques caractères. L’utilisation d’un BufferedReader construit à partir du InputStreamReader permet de lire des lignes de caractères sous la forme de String. BufferedReader brSock = new BufferedReader(isrSock); La méthode readLine de l’objet brSock renvoie la chaîne lue. Affichez la et testez de nouveau votre programme. Construisez une boucle qui lit une chaîne et l’affiche tant que la chaîne n’est pas Bye. 2.2 Un client simple Nous allons créer un client simple sur le port 80 (port HTTP) d’un serveur web quelconque. 2.2.1 Création du socket et des flux Reprenez le travail précédent (création d’un socket, création du flux entrant, création du flux sortant) pour vous connecter à un site web quelconque. 2.2.2 Construction d’une requête HTTP Modifiez votre programme pour envoyer une requête HTTP sur une page de ce site web, affichez le résultat de la requête. 2.3 Un serveur web simple Pour cet exercice, reprenez le premier exercice du TP (serveur simple). 2.3.1 Recherche de la chaîne GET La méthode startsWith de la classe String permet de vérifier le début d’une chaîne. Utilisez la pour retrouver GET dans la chaîne reçue par le serveur. Quand cette chaîne est reçue, envoyez le statut de la demande (200 OK), puis les champs : Content-Type : text/html 8 Connection : close Respectez bien les sauts de ligne nécessaires. Puis envoyez une page web simple composée de la chaîne : “<html>Bonjour</html>” Pour tester votre programme utilisez Firefox sur le site http ://localhost :2500. Modifiez votre programme pour afficher toutes les chaînes envoyées par le client. 2.3.2 Analyse de la requête En utilisant les méthodes startsWith et contains de la classe String, modifiez votre serveur web pour pouvoir afficher trois pages différentes (les pages peuvent être, par exemple, page1, page2 et page3). Affichez un message différent dans chaque page pour vérifiez votre programme. 2.3.3 Utilisation de fichier Stockez vos pages HTML sous la forme de fichiers. Modifiez votre programme pour charger le bon fichier lors d’une requête, gérez l’erreur 404 (fichier non trouvé). Avant de modifier votre serveur, manipulez des fichiers dans une petite classe temporaire. 9