Le langage Java permet une communication entre machines
qui s'appuie sur le protocole IP (Internet Protocol), protocole de base du réseau
Internet. Il y a plusieurs façons de faire communiquer des machines.
Pour en savoir plus sur les Applets
Avant d'écrire notre premier programme sur les réseaux, nous allons utiliser le client Telnet pour établir des connexions à certaines machines qui proposent des services sur le réseau.
Rappelons qu'un service est un programme qui fonctionne en permanence (démon sous UNIX) dès que l'ordinateur est sous tension. Il est également à l'écoute du réseau pour capturer la moindre requête venant de l'extérieur, et dès lors, il va rendre le service demandé. Il est possible d'avoir plusieurs services sur une même machine, et pour les différencier, on utilise un numéro qui est appelé numéro de port. Ces numéros sont standardisés avec le 21 pour le service FTP, le 23 pour le service Telnet, le 25 pour le service SMTP, le 80 pour le service HTTP, etc. Pour établir la connexion avec la machine distante sur le réseau (tube de communication), il suffit de connaître le nom de la machine (nom de l'hôte) et le numéro du service (numéro de port). Ces tubes de communication sont appelés des Sockets.

Nous allons nous connecter au service HTTP proposé par le serveur Tomcat. Rappelez-vous, que par défaut, ce serveur est réglé sur le port 8080. Si cela n'est pas déjà fait, démarrer le serveur, et lancez ensuite le logiciel client Telnet.

Etablissez ensuite la connexion au serveur Tomcat à l'aide du client Telnet. pour cela on utilise la commande open.

Rien n'apparaît, mais vous êtes alors connecté au service http de Tomcat. Saisissez ensuite la ligne de commande suivante, exactement telle qu'elle apparaît, sans appuyer sur une touche de correction (Il est possible qu'il n'y ai pas d'écho à l'écran) : get / http/1.0 (attention aux espaces), appuyer ensuite deux fois sur la touche "Entrée".

Nous obtenons alors toutes les caractéristiques du serveur Tomcat avec entre autre, la localisation de la page d'accueil du site, et cela doit vous sembler familier, sur la partie basse nous découvrons du texte formaté en HTML. Cette technique correspond exactement à celle utilisée par votre navigateur pour charger une page Web.

Le programme du serveur fonctionne en permanence sur la machine distante, attendant un paquet du réseau qui essaierait de communiquer avec le port 8080. Lorsque le système d'exploitation de l'ordinateur distant reçoit le paquet contenant une requête de connexion sur le port 8080, le processus d'écoute du serveur est activé et la connexion est établie. Cette connexion demeure jusqu'à ce qu'elle soit arrêtée par l'une des deux parties.
Lorsque vous avez ouvert une session Telnet sur le port 8080 de l'ordinateur gravure, une partie indépendante du logiciel réseau à converti la chaîne de texte "gravure" en une adresse IP associée, c'est à dire 172.16.40.202. Puis le logiciel a envoyé une requête de connexion à cet ordinateur, en spécifiant le port 8080.
Une fois que cette connexion a été établie, le programme de l'ordinateur distant a renvoyé un ensemble de données, puis il a terminé la connexion. Bien sûr, dans le cas général, les clients et les serveurs entament un dialogue plus poussé avant que la connexion ne soit interrompue.
Notre premier exemple de programme réseau effectue la même chose que ce que nous venons de faire avec Telnet, c'est à dire se connecter au port 8080 du service http de Tomcat sur le poste gravure, et ensuite envoyer la commande get et recevoir les informations du serveur.

La première ligne du programme ouvre une socket, qui est en fait une abstraction du programme de réseau qui permet d'établir une communication en entrée et en sortie avec ce programme. L'adresse de l'ordinateur distant est passée au constructeur de la socket avec le numéro du service désiré. Si la connexion ne peut pas être ouverte, une UnknowHostException est déclenchée. Si une autre erreur survient, une IOException apparaît. Comme UnknowHostException est dérivée de IOException et qu'il s'agit d'un programme simple, nous pouvons nous contenter de gérer la classe de base.
Une fois que la socket est ouverte, les méthodes getOutputStream et getInputStream de java.net.Socket renvoient respectivement les objets OutputStream et InputStream que vous pouvez utiliser comme n'importe quel autre fichier. Au travers de ces deux objets, respectivement :

Pour en savoir plus sur les flux et les fichiers
| java.net.Socket | |
|---|---|
| Socket(String hôte, int port); | Crée une socket et la connecte à un port de l'ordinateur distant. |
| void close(); | Ferme la socket. |
|
InputStream getInputStream(); |
Récupère le flot de données à lire sur la socket. |
| OutputStream getOutputStream(); | Récupère le flot de données à écrire sur la socket. |
| void setSoTimeout(int délai); | Définit la valeur de temps d'attente lors de l'ouverture d'une socket. Si ce délai est écoulé, cela veut dire que la connexion n'a pas pu s'établir et une InterruptedIOException est déclenchée. Ce système permet d'éviter d'attendre indéfiniment. |
Depuis la version Tiger 5.0 de Java, une nouvelle classe a été rajoutée qui permet notamment de récupérer simplement la saisie issue du clavier. Il s'agit de la classe Scanner. Cette classe est toutefois beaucoup plus polyvalente. D'une façon générale, elle gére les flux d'octets pour délivrer ensuite du texte formatté. Elle est en fait symétrique par rapport à la classe PrintWriter. Ainsi, elle permet de récupérer soit des lignes de texte, soit des mots isolés entre eux, soit des entiers, soit des réels, etc.

En revenant sur le sujet du réseau, il souvent préférable maintenant de passer par la classe Scanner plutôt que par la classe BufferedReader. En effet, cette dernière attend des flots de caractères, et comme les sockets délivrent plutôt des flots d'octets, vous êtes obligés de passer par la classe intermédiaire InputStreamReader.

Par ailleurs, la classe Scanner dispose de méthodes qui permettent de contrôler si une donnée une présente dans le flux, et si c'est le cas, vous pouvez ensuite la récupérer par les méthodes de votre choix. Bref, cette classe est à la fois beaucoup simple et beaucoup plus souple d'emploi. En reprenant l'exemple de ce chapitre, voici ce que nous pouvons obtenir en utilisant la classe Scanner en lieu et place de la classe BufferedReader :
import java.io.*; import java.net.*; import java.util.Scanner; public class SocketTest { public static void main(String[] args) throws IOException { Socket connexion = new Socket("gravure", 8080); PrintWriter requête = new PrintWriter(connexion.getOutputStream(), true); requête.println("GET / HTTP/1.0\n"); Scanner réponse = new Scanner(connexion.getInputStream()); while (réponse.hasNextLine()) System.out.println(réponse.nextLine()); } }
Le code est beaucoup plus concis et agréable à lire. Dans la suite de cette étude je conserverais la classe BufferedReader (j'avoue que je n'ai pas envie de tout reconstruire). Toutefois, penser bien que généralement il sera préférable d'utiliser plutôt la classe Scanner.
Pour en savoir plus sur la classe Scanner
Maintenant que nous avons implémenté un client simple qui reçoit des données d'un serveur sur Internet (ou sur le réseau local), intéressons-nous à la fabrication d'un serveur simple qui devra envoyer des informations sur Internet (ou sur le réseau local). Une fois que ce serveur (service) sera lancé, il devra attendre qu'un client se connecte à l'un de ces port. Nous choisissons le port 8189, qui n'est utilisé par aucun service standard.

La classe java.net.ServerSocket permet de monter un service. Cette classe est ensuite utilisée pour établir une socket. Dans notre cas, le service est rattaché au port 8189. La méthode accept demande au programme d'attendre indéfiniment jusqu'à ce qu'un client se connecte sur ce port. Une fois qu'un ordinateur s'y est connecté en envoyant une requête adéquate sur le réseau, cette méthode renvoie un objet Socket qui représente la connexion établie. Vous pouvez vous servir de cet objet pour lire et pour écrire au travers de cette socket, comme nous l'avons déjà fait dans le chapitre précédent.
Tout ce que le serveur envoie à son flux de sortie devient un flux d'entrée pour le programme client, et toutes les sorties du client deviennent les entrées du serveur. Pour l'instant, dans ces exemples nous transmettons du texte. Nous pourrions bien sûr transmettre des données binaires, il faudrait alors utiliser respectivement les classes DataInputStream et DataOutputStream. Pour transmettre des objets en série, il faudrait utiliser ObjectInputStream et ObjectOutputStream.

Vous pouvez tester ce programme en utilisant Telnet. Une fois que le serveur est lancé, établissez la connexion au port 8189 sur l'ordinateur qui propose le service.

Lorsque vous vous connectez, vous obtenez le message d'invite "Bonjour, tapez OK pour sortir". Saisissez n'importe quoi, le serveur vous répond en vous renvoyant votre message précédé de "Echo : ". Pour vous déconnecter, il suffit de taper "ok" ou "OK". Le programme du serveur sera alors également arrêté.

| java.net.ServerSocket | |
|---|---|
| ServerSocket(int port) throws IOException; | Crée une socket de serveur qui examine un port. |
| Socket accept() throws IOException; | Attend une connexion. Cette méthode bloque le thread courant jusqu'à ce que la connexion soit établie. Cette méthode renvoie un objet Socket grâce auquel le programme peut communiquer avec le client. |
| void close() throws IOException; | Ferme la socket du serveur. |
Le serveur de l'exemple précédent possède un inconvénient. Supposons que nous voulions permettre à plusieurs clients de se connecter en même temps à notre serveur. Typiquement, un serveur est exécuté en permanence sur un ordinateur dédié à cette tâche, et plusieurs clients sur Internet peuvent se connecter à ce serveur simultanément. Si le serveur ne peut pas gérer plusieurs connexions en même temps, un utilisateur pourra le monopoliser en restant connecté longtemps. Mais nous pouvons faire beaucoup mieux grâce à la magie des threads.
Pour en savoir plus sur les threads
Chaque fois que le programme a établi une nouvelle connexion, c'est à dire qu'il a accepté une requête, nous allons créer un nouveau thread qui sera chargé de la gestion de la connexion entre le serveur et ce client. Le programme reviendra alors en arrière et attendra la prochaine connexion. Pour que cela puisse se produire, la boucle principale doit ressembler à ceci :

La classe ThreadConnexion dérive de Thread et contient la boucle de communication avec le client dans la méthode run.

Comme chaque nouvelle connexion lance un nouveau thread, plusieurs clients peuvent se connecter au serveur en même temps. Effectuez le test en lançant dans un premier temps le service ServeurThread, et ensuite, à partir de plusieurs ordinateurs, exécutez le programme Telnet en établissant la connexion vers l'ordinateur hôte au port 8189. Cette fois-ci, le numéro de la connexion a été rajouté sur le message de retour.

Pour faire la synthèse de ce que nous venons de voir, nous allons utiliser le serveur que nous venons de construire en rajoutant toutefois un petit programme Client qui remplace Telnet et qui offre les mêmes fonctionnalités en appelant le bon service 8189.


En général, vous n'avez pas besoin de vous préoccuper des adresses Internet. Cependant, vous pouvez avoir recours à la classe InetAddress si vous souhaitez traduire un nom d'ordinateur en adresse Internet et inversement. Les classes ServerSocket et Socket dispose d'une méthode getInetAddress() qui renvoie un objet InetAddress.
Pour en savoir plus sur la classe InetAddress
A titre d'exemple, pour valider à la fois le fonctionnement du réseau, mais égalemenent la bonne gestion des flux, je vous propose de visualiser automatiquement une photo numérique sur le serveur qui est envoyé par un client sur le réseau local. Nous devons donc élaborer deux applications :
Application cliente qui envoi les photos ............................................. Serveur qui reçoit les photos


| photos.PanneauImage.java |
|---|
package photos; import javax.swing.JComponent; import java.awt.Graphics; import java.awt.image.BufferedImage; class PanneauImage extends JComponent { private BufferedImage image; private double ratio; public void change(BufferedImage image) { if (image!=null) { this.image = image; ratio = (double)image.getWidth()/image.getHeight(); repaint(); } } @Override protected void paintComponent(Graphics surface) { if (image!=null) surface.drawImage(image, 0, 0, this.getWidth(), (int)(this.getWidth()/ratio), null); } } |
| photos.ClientPhotos.java |
|---|
package photos; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.awt.image.BufferedImage; import java.io.*; import java.net.*; import javax.imageio.*; public class ClientPhotos extends JFrame implements ActionListener { private String répertoire = "J:/Stockage/"; private String[] liste; private PanneauImage panneau = new PanneauImage(); private JComboBox choix; private JButton envoyer = new JButton("Envoyer la photo"); public ClientPhotos() { liste = new File(répertoire).list(); choix = new JComboBox(liste); panneau.change(récupérer()); choix.addActionListener(this); envoyer.addActionListener(this); setSize(500, 400); setTitle("Envoi de photos"); add(choix, BorderLayout.NORTH); add(envoyer, BorderLayout.SOUTH); add(panneau); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } private BufferedImage récupérer() { try { BufferedImage photo = ImageIO.read(new File(répertoire+choix.getSelectedItem())); return photo; } catch (Exception ex) { setTitle("Problème de localisation des photos"); return null; } } public static void main(String[] args) { new ClientPhotos(); } public void actionPerformed(ActionEvent e) { if (e.getSource()==choix) { panneau.change(récupérer()); } else if (e.getSource()==envoyer) { try { File fichier = new File(répertoire+choix.getSelectedItem()); byte[] octets = new byte[(int)fichier.length()]; FileInputStream photo = new FileInputStream(fichier); photo.read(octets); envoyer(octets); } catch (IOException ex) { setTitle("Problème avec le fichier"); } } } private void envoyer(byte[] octets) { try { Socket connexion = new Socket("localhost", 7777); ObjectOutputStream fluxRéseau = new ObjectOutputStream(connexion.getOutputStream()); fluxRéseau.writeObject(octets); connexion.close(); } catch (IOException ex) { setTitle("Problème avec le serveur"); } } } |
| photos.ServeurPhotos.java |
|---|
package photos; import java.io.*; import java.net.*; import javax.swing.JFrame; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; public class ServeurPhotos extends JFrame { private static PanneauImage panneau = new PanneauImage(); public ServeurPhotos() { setSize(500, 400); setTitle("Visionneuse"); add(panneau); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } public static void main(String[] args) throws Exception { new ServeurPhotos(); activerService(); } public static void activerService() throws Exception { ServerSocket service = new ServerSocket(7777); while (true) { Socket client = service.accept(); ObjectInputStream fluxRéseau = new ObjectInputStream(client.getInputStream()); byte[] octets = (byte[]) fluxRéseau.readObject(); ByteArrayInputStream fluxImage = new ByteArrayInputStream(octets); BufferedImage photo = ImageIO.read(fluxImage); panneau.change(photo); } } } |
![]()
Nous
disposons maintenant de tous les outils nécessaire pour mettre en oeuvre
une petite application "Chat". Nous en limiterons les performances
avec la possibilité de se connecter à un instant donné
à un seul interlocuteur.
Je rappelle qu'un Chat est la fois client et serveur. Au départ, lorsque le logiciel est lancé, le service est mise en route et attend une connexion éventuelle venant de l'extérieur. Lorsque le contact s'établie, vous pouvez envoyer vos messages dans la zone de saisie. Votre interlocuteur le reçoit alors dans la zone principale de la fenêtre. Vous pouvez demander vous-même à vous connecter avec votre interlocuteur, il suffit de placer alors le nom de l'hôte dans la zone prévue à cet effet.