Les concepts expliqués sont : 1. Introduction 2. Introduction aux classes et aux o
bjets 3. Héritage 4. Visibilité des variables 5. Surcharge de méthodes 6. Interfaces 7 . Les Applications Java 8. Les Applets Java 9. Evénements 10. Le fenêtrage avec AWT 11. Processus légers (threads) 12. Exclusion mutuelle 13. Synchronisation : les méth odes wait() et notify() 14. Les Exceptions 15. Type Wrappers 16. Incrémentation et décrémentation 17. Les fichiers 18. Les connexions réseau TCP/IP - la classe Socket 1 9. Les serveurs TCP/IP - SocketServer 20. Une Application client-serveur 1. Introduction Les pages qui constituent cette documentation hypertextuelle sur le langage Java ont été créées au cours du travail de diplôme d'Alexandre Maret, au printemps 1996. Sur l a page d'entrée, vous avez la possibilité d'accéder à deux index. Le premier est l'index des concepts, qui vous offre une liste de pages d'explications sur des points p récis de Java, ainsi que sur les classes standard qu'il intègre. Vous avez également a ccès à l'index des règles BNF définissant Java. Il peut être utile de garder une fenêtre ou erte sur la documentation de Sun sur les API Java. pendant la lecture des exempl es. Pour plus de clarté, les exemples présents dans ces pages ont été simplifiés. Ainsi, b ien qu'il soit conseillé de protéger au mieux les variables à l'aide de modificateurs, cel a n'est pas visible dans les exemples, afin d'éviter toute lourdeur. De même pour le s problèmes de concurrence et d'exclusion mutuelle. Les méthodes données en exemple da ns ces pages ne sont pas prévues pour être utilisées par plusieurs processus, car cert ains problèmes pourraient en découler. Les conventions typographiques qui sont utili sées dans cette documentation sont : Ecriture normale pour le texte Ecriture code pour les mots clés et les codes sources Italique pour les termes anglais Nous vous souhaitons une bonne lecture... 2. Introduction aux classes et aux objets Une classe est un type d'objet. On peut comparer la description d'une classe à cel le d'un article (record), contenant à la fois des données et des méthodes nécessaires au traitement de celles-ci. On peut par exemple imaginer une classe Voiture qui co ntiendrait des données - immatriculation, type - ainsi que des procédures localiser( ) . Ces procédures sont appelées méthodes en programmation orientée-objets, et elles opère nt sur des variables contenues dans l'objet. Nous l'avons dit précédemment, une clas se est en quelque sorte un type complexe. Lorsque l'on crée un objet d'une certain e classe, on parle d'une instance de cette classe. Pour créer un objet, on fait ap pel à un de ses constructeurs. Voyons comment créer une instance voiturePatron de la classe Voiture, et comment la "localiser". Voiture voiturePatron = new Voiture("patron","GE999",0,0); position = voiturePat ron.localiser(); La première ligne crée une nouvelle instance de la classe Voiture, en fournissant 4 paramètres au constructeur. En deuxième ligne, nous faisons un appel à la méthode locali ser() de l'instance voiturePatron. Le point marque la séparation entre le nom de l 'objet et le nom de la méthode de cet objet. Si l'on veut faire référence à une variable contenue dans cet objet, on utilisera la même notation. Dans cet exemple, localis er() ne prend pas de paramètre, mais il est tout de même requis de postfixer le nom de la méthode avec des parenthèses vides. Des exemples plus précis sont donnés à la page dé rivant la déclaration d'une classe. On peut aussi se reporter à la page héritage. 3. Héritage L'avantage d'un langage orienté-objets est que le code est réutilisable. Grâce à l'héritag e, on peut modifier une classe sans avoir à la réécrire complètement. On dit que la classe nou velle hérite de la classe ancienne . On dit aussi que nouvelle étend ancienne ou que nouvelle est dérivée de ancienne. nouvelle est alors une sous-classe de ancienne, e t ancienne une super-classe (surclasse) de nouvelle . Dans la pratique, cela signifie qu'une instance de nouvelle aura la possibilité d' utiliser des méthodes définies dans ancienne. Lorsque l'on code la classe nouvelle, on a la possibilité de remplacer des méthodes de la super-classe (classe parente), s implement en définissant une méthode avec le même nom et les mêmes paramètres. On remarque ra qu'il n'est pas possible de remplacer un constructeur, puisque celui-ci porte le nom de la classe dans laquelle il est déclaré, et que le nom des deux classes di ffère obligatoirement. Nous disions précédemment qu'il est possible de modifier une cl asse sans avoir à la réécrire complètement. Pratiquement, on déclare des méthodes supplémen res ou des méthodes portant le même nom que les méthodes de la super-classe. Dans ce d ernier cas, on masque l'ancienne méthode. Le mot clé super désigne la classe parente e t on peut alors faire appel à ses méthodes ou variables si celles-ci ont été masquées par des variables locales de même nom (voir visibilité des variables). Lorsque l'on écrit super.M(), on fait appel à la méthode M() de la super-classe. En Java, toutes les cl asses sont dérivées de la classe spéciale Object. C'est la racine de la hiérarchie des c lasses. Cela veut dire que toutes les classes Java possèdent un certain nombre de méthodes. La documentation des API disponible chez Sun les explique. Ci-dessous, v ous trouverez la classe Crayon, et la classe CrayonCouleur qui l'étend. Crayon class Crayon { protected int longueur = 100; protected int diametre = 5; // cree un crayon standard public Crayon() { } // cree un crayon de longueur l et de di ametre d public Crayon(int l,int d) { longueur = l; diametre = d; } public int q uelleLongueur() { return longueur; } public int quelDiametre() { return diametre ; } public void changeLongueur(int nouvelleLongueur) { longueur = nouvelleLongue ur; } } // en millimetres // en millimetres CrayonCouleur class CrayonCouleur extends Crayon { protected String couleur = "gris"; public C rayonCouleur() { super(); } public CrayonCouleur(int l,int d,String c) { super(l ,d); // initialise les variables longueur // et diametre en utilisant le // cons tructeur de la "superclass" couleur = c; } public CrayonCouleur(String c) { supe r(); couleur = c; } public String quelleCouleur() { return couleur; } public voi d changeCouleur(String nouvelleCouleur) { couleur = nouvelleCouleur; } } 4. Visibilité des variables En Java, une variable est visible dans le bloc où elle a été déclarée, ainsi que dans tous les sous-blocs de celui-ci. Exemples class scope { float nombre = 5f; int valeur = 12; int methode1() { boolean local e; // locale visible seulement ici // nombre et valeur sont visibles } int metho de2() { for (int i;i < n;i++) { // i visible seulement ici // nombre et valeur s ont visibles } { // debut d'un nouveau bloc int tresLocale; // tresLocale visibl e ici } // nombre et valeur sont visibles } // ni i ni tresLocale ne sont visible // nom bre et valeur sont visibles int methode3() { float nombre = 3f; /* la variable nombre declaree au debut de l a methode n'est plus visible, car elle est masquee par nombre de methode3 */ } i nt methode4(int valeur) { /* nombre est de nouveau visible ici. Par contre "vale ur" de la methode est cachee par le parametre valeur */ } } Prenons methode4 de l'exemple ci-dessus. Il peut tout de même être utile de pouvoir accéder à la variable globale valeur . Ceci peut être fait en préfixant le nom de la var iable par this., qui désigne l'objet courant. On pourra ainsi réaliser des opérations telles que this.valeur = valeur; pour copier le paramètre de méthode4 dans la variab le d'instance valeur. 5. Surcharge de méthodes La surcharge (overloading) permet au programmeur de déclarer plusieurs méthodes ou c onstructeurs ayant le même nom, mais ayant des paramètres différents. Au moment de la génération de code, le compilateur identifie tous les appels à ces méthodes. Il les comp are avec les déclarations des méthodes, et choisi la méthode à appeler en fonction de se s paramètres. ... int somme(int a,int b) { return (a+b); } int somme(int a,int b,int c) { retu rn (a+b+c); } float somme(float a,float b) { return (a+b); } ... ... int entA = 1,entB = 2,entC = 9; float fA = 1.3,fB = 2.6; int entSomme = somme(entA,entB,ent C); float fSomme = somme(fA,fB); ... println est une méthode de la classe PrintStream qui accepte plusieurs types de paramètres différents, grâce à la surcharge. Le paramètre de println est calculé avant l'ap el à la méthode. Ainsi, println(1+1) affichera 2 alors que println(1+" "+1) afficher a 1 1. Attention ! Il n'est pas possible d'avoir deux méthodes (de même nom) dont tous les paramètres son t identiques, et dont seul le type de méthode diffère. Le compilateur ne saurait pas quelle méthode employer. L'exemple ci-dessous n'est pas valide : int somme(float a,float b) { return ((int)(a+b)); } float somme(float a,float b) { return (a+b); } 6. Interfaces Une interface est un modèle d'implémentation. Tous les objets qui implémentent une int erface possèdent les méthodes déclarées dans celle-ci. On utilise souvent les interfaces dans les types de paramètres. En effet, dans certains cas, peu importe la classe d'un objet donné, car cela ne nous intéresse pas. Nous avons seulement besoin de sav oir que telle ou telle classe possède ces méthodes et se comporte de telle manière. On peut imaginer une interface appareilElectrique qui définit les méthodes estEnclen che() et alimente(boolean). Si une classe implémente l'interface appareilElectriqu e , on sait donc qu'elle possède au moins ces deux méthodes, qu'il s'agisse d'une ra dio, d'une lampe ou d'un autre appareil. Lorsque l'on désire indiquer à un appareil qu'il est alimenté, on exécute app.alimente(true), et pour lui indiquer qu'il a été débran ché, on exécute app.alimente(false). Nous allons donc créer deux types d'appareils élect riques, les radios et les lampes. Ces deux types implémentent l'interface appareil Electrique. Nous allons également créer une classe rallongeElectrique, qui est aussi un appareil électrique. La méthode la plus intéressante de cette classe est la méthode branche(appareilElectrique). Celle-ci enregistre l'objet fourni en paramètre comme étant l'objet alimenté à travers la rallonge. Cette rallonge possède un interrupteur, q ue l'on peut activer ou désactiver en appelant ses méthodes enclenche(), respectivem ent declenche(). Lorsque l'on change l'état de l'interrupteur, on indiquera à l'obje t qui en dépend qu'il est ou qu'il n'est plus alimenté. La page "déclaration d'interfa ce" peut également intéresser le lecteur. Interface - appareilElectrique public interface appareilElectrique { /** teste si l'appareil est enclenche */ public boolean estEnclenche(); /** on appelle cette methode lorsque l'on * branc he l'appareil dans une source de * courant active, avec true, ou false * si la s ource est inactive */ public void alimente(boolean alim); } Implémentation - radio // c'est une radio rudimentaire, qui se deregle quand on l'eteint ! class radio implements appareilElectrique { final static int freqMiseEnRoute = 1007; int fre q; boolean allumee = false; public boolean estEnclenche() { return allumee; } pu blic void alimente(boolean a) { allumee = a; if (allumee) freq = freqMiseEnRoute ; } /** on ne peut changer de frequence que si la * radio est en marche * retour ne true si le changement a ete effectue */ public boolean changeFreq(int freq) { if (!allumee) this.freq = freq; return allumee; } } // 100.7 MHz Implémentation - lampe class lampe implements appareilElectrique { boolean allumee = false; public bool ean estEnclenche() { return allumee; } public void alimente(boolean alim) { allu mee = alim; } } Implémentation - rallongeElectrique class rallongeElectrique implements appareilElectrique { appareilElectrique appa reilBranche = null; boolean conduit = false; // indique si l'interrupteur est fe rme (conduit=true) boolean estAlimente = false; public boolean estEnclenche() { return conduit; } public void alimente(boolean alim) { if (alim) { // on branche la rallonge dans une prise if (!estAlimente && conduit) on(); } else // on debr anche if (estAlimente && conduit) off(); estAlimente = alim; } /* gestion de l'i nterrupteur */ public void enclenche() { if (!conduit && estAlimente) on(); cond uit = true; } public void declenche() { if (conduit && estAlimente) off(); condu it = false; } /* gestion de l'appareil branche */ private void on() { if (appare ilBranche != null) appareilBranche.alimente(true); } private void off() { if (ap pareilBranche != null) appareilBranche.alimente(false); } // branche un nouvel a ppareil a la rallonge public boolean branche(appareilElectrique app) { if (appar eilBranche != null) // il y a deja un appareil branche ! return false; appareilB ranche = app; return true; } } Exemple d'utilisation - testElectrique class testElectrique { public static void main(String args[]) { rallongeElectriq ue rallonge1, rallonge2; rallonge1 = new rallongeElectrique(); rallonge2 = new r allongeElectrique(); radio radioSalon = new radio(); lampe lampeCuisine = new la mpe(); // on imagine que l'on branche les rallonges dans 2 prises rallonge1.alim ente(true); rallonge2.alimente(true); // on branche des appareils dans ces 2 ral longes rallonge1.branche(radioSalon); rallonge2.branche(lampeCuisine); System.ou t.println("la radio du salon est "+ (radioSalon.estEnclenche() ? "allumee" : "et einte")); System.out.println("on appuie sur l'interrupteur de la rallonge 1"); r allonge1.enclenche(); System.out.println("la radio du salon est maintenant "+ (r adioSalon.estEnclenche() ? "allumee" : "eteinte")); } } Exécution la radio du salon est eteinte on appuie sur l'interrupteur de la rallonge 1 la r adio du salon est maintenant allumee 7. Les Applications Java Une application Java est un programme qui réside sur la machine qui l'exécute. En Ja va, tout programme est une classe, et possède donc des méthodes. Lorsque la machine virtuelle Java veut démarrer le programme, elle appelle la méthode main(String args[ ]) de l'application chargée. Il faudra donc placer le code à exécuter dans cette méthode . Le tableau args contient les éventuels paramètres passés à l'application sur la ligne de commande. Pour plus d'explication sur la déclaration de la méthode main dans l'ex emple ci-dessous, veuillez consulter la page Les modificateurs. Exemple class exSimple1 { public static void main(String args[]) { int entA = 10; int en tB = 12; int entX; System.out.println("L'entier A vaut "+entA); System.out.print ln("L'entier B vaut "+entB); if (entA < entB) System.out.println("A est plus pet it que B"); else if (entA == entB) System.out.println("A est egal a B"); else Sy stem.out.println("A est plus grand que B"); System.out.println("comptons de 1 a "+entA); int somme = 0; int fact = 1; for (int i = 1;i <= entA;i++) { System.out .print(" "+i); somme += i; fact *= i; } System.out.println(); System.out.println ("la somme de tous les nombres de 1 a "+entA+" vaut "+somme); System.out.println ("la factorielle de "+entA+" vaut "+fact); } } Exécution L'entier A vaut 10 L'entier B vaut 12 A est plus petit que B comptons de 1 a 10 1 2 3 4 5 6 7 8 9 10 la somme de tous les nombres de 1 a 10 vaut 55 la factoriel le de 10 vaut 3628800 8. Les Applets Java Introduction Une applet Java est un programme qui est exécuté dans un browser tel que Netscape Na vigator, ou HotJava de Sun. Une applet est intégrée dans une page au format HTML et est automatiquement téléchargée sur le poste client. Elle est ensuite exécutée par celui-c i. Le téléchargement transparent et l'exécution automatique posent des problèmes de sécurité, et c'est pour cela que les applets Java sont limitées dans certains domaines . Typiquement, une applet ne peut pas gérer de fichiers, ni ouvrir des connections réseau arbitraires. Intégration de l'applet dans une page Web Il existe des balises (tags) HTML spécifiques pour indiquer au browser qu'une appl et est présente dans une page. Il s'agit de <APPLET>. Voici un exemple classique : <HTML> <HEAD> <TITLE>Exemple de page avec une applet</TITLE> </HEAD> <BODY> <H1> Exemple d'applet</H1> <applet code="appletExemple.class" width=100 height=50> Vo tre browser ne supporte pas les applets ! </applet> </BODY> </HTML> Mise en oeuvre Tout programme Java est une classe. Une applet n'échappe pas à cette règle. Si l'on ve ut créer une applet, on doit étendre la classe java.applet.Applet. Cette classe cont ient les méthodes nécessaires à la gestion de l'applet, et à l'interaction de celle-ci a vec son environnement (browser). Voyons les méthodes les plus importantes, que votre applet devra remplacer si nécess aire : public void init() Le browser fait appel à cette méthode lorsque l'applet est chargée (ou rechargée). Cette méthode devra charger les informations telles que images ou so ns, et récupérer les paramètres présents dans la page HTML. public void start() Après avoi r été initialisée, l'applet est démarrée, grâce à la méthode start() . L'applet est égaleme rrée après avoir été stoppée, lorsqu'elle est à nouveau visible. public void stop() Cette m ode permet à l'applet de s'arrêter lorsqu'elle n'est plus visible, parce que l'utili sateur a changé de page, par exemple. public void destroy() L'applet est détruite lo rsque le browser s'arrête, ou avant que l'applet soit rechargée. Cette méthode doit être remplacée s'il on veut stopper des threads créés par start() de l'applet. public void paint(Graphics g) Cette méthode est appelée chaque fois que l'on doit re dessiner l'applet. Le paramètre est de type Graphics et c'est la surface de dessin sur laquelle on doit travailler. Exemple import java.awt.*; import java.applet.*; public class applet1 extends Applet { F ont font; public void init() { font = new Font("TimesRoman",Font.PLAIN,20); } pu blic void paint(Graphics g) { g.setFont(font); g.setColor(Color.red); g.drawStri ng("Je suis une applet !",0,font.getSize()); } } Cette applet n'implémente pas la méthode start() , car aucune tâche n'est effectuée en p ermanence. La seule chose que l'applet fait est de se redessiner lorsque le brow ser le redemande (paint(Graphics g)). Que se passe-t-il si l'on veut que l'applet ait une activité permanente ? Toutes l es applets s'exécutant dans browser utilisent la même machine virtuelle Java. On ne peut donc pas créer une boucle dans la méthode start() pour effectuer une tâche, parce que l'on occuperait la MV (machine virtuelle). Il faut donc créer un thread afin d'autoriser l'exécution parallèle des multiples applets. Exemple import java.awt.*; public class applet2 extends java.applet.Applet implements Ru nnable { Thread tache; String lbl; Font font; boolean gris = false; public void init() { font = new java.awt.Font("TimesRoman", Font.PLAIN, 24); lbl = getParameter("lbl"); } public void paint(Graphics g) { g.setFont(font); if (g ris) g.setColor(Color.lightGray); else g.setColor(Color.black); g.drawString(lbl , 0, font.getSize()); } public void start() { tache = new Thread(this); tache.st art(); } public void stop() { tache.stop(); } public void run() { while (true) { try { Thread.sleep(1000); // pause de 1 seconde } catch (InterruptedException e ) {} gris = !gris; repaint(); // on force le réaffichage } } } La page suivante vous expliquera le fonctionnement des événements. 9. Evénements Le logiciel de navigation envoie des événements à l'applet par exemple lorsque l'utili sateur presse une touche du clavier ou utilise sa souris. Il envoie ces événements à l 'applet en appelant une des méthodes prévues à cet effet. On peut détecter ces événements e remplaçant les méthodes keyDown ou mouseDown. Cet exemple fait une utilisation très simple des événements qu'il reçoit. S'il s'agit d' un click de souris, on incrémente le compteur c. Si l'utilisateur a pressé une touch e flèchée vers le haut ou vers le bas, on augmente ou diminue le compteur. On peut l e remettre à zéro en pressant 0. Exemple import java.awt.*; import java.applet.*; public class applet3 extends Applet { i nt c = 0; Font font; // valeur du compteur // police de caracteres public void init() { // on choisit la plus grande police possible : la hauteur d e l'applet font = new Font("Helvetica",Font.BOLD,size().height); } public void p aint(Graphics g) { g.setFont(font); g.setColor(Color.blue); g.drawString("c = "+ c,0,font.getSize()); } public boolean mouseDown(Event evt,int x,int y) { c++; re paint(); return true; } public boolean keyDown(Event evt,int key) { if (key == E vent.UP) c++; else if (key == Event.DOWN) c--; else if ((char)key == '0') c = 0; repaint(); return true; } } Pour plus d'informations sur la structure du système de fenêtrage AWT (Abstract Wind ow Toolkit) ainsi que sur ses multiples composants, clickez ici (AWT) 10. Le fenêtrage avec AWT Introduction La librairie AWT (AWT Toolkit) fournit au programmeur toutes les primitives nécess aires à la gestion de l'interface graphique utilisateur. Cette librairie utilise l e système graphique sous-jacent pour afficher des objets graphiques tels que boutons, fenêtres, etc. M algré tout, toute applet ou application fonctionnera indépendamment du système d'explo itation. Pour les premiers exemples graphiques, vous pouvez consulter les pages sur les applets et sur les événements AWT. Les différents types d'éléments d'AWT Il existe principalement quatre types d'objets dans le monde AWT. Les conteneurs (container) Les conteneurs sont capables de contenir des canevas, des composant s graphiques, ou d'autres conteneurs. Le panneau, décrit plus loin, est un exemple typique de conteneur. Les canevas (canvas) Un canevas est un objet graphique très simple, sur lequel on ne peut que dessiner et afficher des images. On parle aus si de toile de fond, ou tout simplement de fond. Les composants graphiques (grap hic components) Les composants graphiques regroupent les objets traditionnels prés ents dans une interface graphique. On retrouve par exemple les boutons, les list es, les champs de saisie de texte, et bien d'autres objets encore. Les composant s de fenêtres (windowing components) Les composants de fenêtres permettent de créer de s fenêtres ou des boîtes de dialogue. La gestion des menus et de la barre de titre f ont également partie de ce type d'objets. Généralement, une applet ne fait pas usage d e ces objets, bien que cela soit possible. Les panneaux et les gestionnaires de mise en page Commençons par remarquer qu'une applet est un panneau, et que c'est grâce à cette prop riété que l'on peut insérer des boutons dans une applet. Cette insertion d'objets se f ait de manière dynamique, lors de l'exécution. Pour définir l'apparence de l'interface graphique, on utilise un gestionnaire de mise en page (layout manager). Celui-c i définit la taille et la position de chaque composant graphique lors de son inser tion. Il existe plusieurs gestionnaires de mise en page, chacun positionnant dif féremment les éléments graphiques, selon un algorithme qui leur est propre. L'exemple ci-dessous montre comment faire bon usage des gestionnaires de mise en page. Le code source de cette applet étant relativement long, vous pouvez y accéder sur cette page. S'il on regarde le code source, on voit que la première opération consiste à diviser l a surface de l'applet en une grille de 1 ligne de 3 colonnes. On utilise le gest ionnaire de mise en page par défaut pour la première case, à gauche. La case du milieu utilise BorderLayout, qui nous permet de placer des objets dans un bord de la p age, ou au milieu. Pour la troisième case, à droite, nous construisons une grille de 4 lignes de 3 colonnes, que l'on remplit de boutons. Nous avons créé une structure hiérarchique de panneaux, en les imbriquant. La gestion de événements et des actions Lorsque l'utilisateur agit sur un objet graphique tel que bouton ou champ de tex te, le logiciel de navigation envoie des actions et des événements au panneau conten ant l'objet. Les actions sont similaires aux événements, sauf qu'ils ne sont pas tra ités par la méthode handleEvent , mais action. Chaque panneau possède sa propre méthode action. Si l'on ne remplace pas cette méthode par une méthode spécifique, l'événement (act ion) se propagera automatiquement au panneau parent, et ainsi de suite. Dans l'applet ci-dessus, nous traitons tous les événements au niveau de l'applet. Ai nsi, lorsque l'on presse le bouton "3", par exemple, le browser fait appel à la méth ode panNumer.action. Comme nous n'avons pas remplacé cette méthode, l'action est pro pagée au panneau parent, c'est-à-dire l'applet layoutDemo. Lorsque la méthode action e st appelée, deux paramètres sont fournis. Il s'agit de l'événement, et d'un argument. Le s événements ont déja été décrits dans la page événements. L'élément evt.target est l'objet usé l'événement, et l'argument arg est un objet dont le type varie en fonction du type de l'événement. Si l'événement est produit pas une pression sur un bouton, l'argument s era une chaîne de caractères contenant le nom du bouton (label). Si l'action est due à un champ de texte, on sait que l'utilisateur a validé avec la touche entrée, car au cune action n'est générée lorsque l'on modifie ou que l'on sélectionne le champ. Les val eurs retournées par les méthodes d'événements (keyDown, mouseUp, etc) et la méthode action sont de type booléen. Si l'on souhaite propager l'événement au panneau parent, il fau dra retourner false, autrement on retournera true pour indiquer que l'événement a été tr aité. Il existe une grande quantité d'objets que nous n'avons pas décrit ici, pour plu s d'informations sur ce sujet, vous pouvez vous reporter à la spécification des API sur le serveur Web de Sun, à la page concernant AWT. 11. Processus légers (threads) Java permet l'exécution concurrente de plusieurs processus légers threads, c'est-à-dir e plusieurs tâches. On différencie généralement les processus légers des processus lourds. Les premiers sont internes à une application alors que les processus lourds sont vus par le système d'exploitation comme plusieurs processus. Les processus légers pa rtagent un espace d'adressage commun. Comme on le sait, programmation concurrent e sous-entend mécanismes de synchronisation et d'exclusion mutuelle. Le mécanisme d'exclusion mutuelle présent dans l'environnement Java est le moniteur, que l'on implémente grâce au modificateur synchronized. Le principe de base est qu' un seul processus ne peut entrer dans un moniteur si celui-ci est occupé. Mais voy ons tout d'abord comment créer plusieurs processus. Nous pouvons tout simplement éte ndre la classe java.lang.Thread et remplacer sa méthode run() par notre propre alg orithme. Lorsque l'on appelle la méthode start(), la méthode run() est déclenchée, et el le s'arrête lors d'un appel à stop(). Exemple class afficheur extends Thread { /** constructeur permettant de nommer le proces sus */ public afficheur(String s) { super(s); } /** affiche son nom puis passe l e controle au suivant */ public void run() { while (true) { System.out.println(" je suis le processus "+getName()); Thread.yield(); // passe le controle } } } cl ass thread12 { public static void main(String args[]) { afficheur thread1 = new afficheur("1"); afficheur thread2 = new afficheur("2"); thread1.start(); thread2 .start(); while (true) { System.out.println("je suis la tache principale !"); Th read.yield(); } } } Exécution je je je je je je je je suis suis suis suis suis suis suis suis la le le la le l e la le tache principale ! processus 1 processus 2 tache principale ! processus 1 processus 2 tache principale ! processus 1 je je je je je je je je je je je je je je je je processus 2 tache principale ! processus 1 processus 2 tache principale ! proces sus 1 processus 2 tache principale ! processus 1 processus 2 tache principale ! processus 1 processus 2 tache principale ! processus 1 processus 2 Le problème est que la méthode Thread.stop() est qualifiée de final. On ne peut donc suis suis suis suis suis suis suis suis suis suis suis suis suis suis suis suis le la le le la le le la le le la le le la le le pas la remplacer. Une autre solution est possible. L'interface Runnable permet de créer un objet, qu e l'on utilisera ensuite comme constructeur d'un Thread. C'est la méthode start() de l'objet qui sera responsable de la création et de l'activation du thread. Exemple class afficheurRunnable implements Runnable { boolean cont = true; String nomPro cessus; Thread th; /** constructeur permettant de nommer le processus */ public afficheurRunnable(String s) { nomProcessus = s; } public void start() { if (th = = null) { th = new Thread(this,nomProcessus); th.start(); } } public void stop() { if (th != null) { th.stop(); th = null; } } /** affiche son nom puis passe le controle au suivant */ public void run() { while (cont) { System.out.println("j e suis le processus "+th.getName()); Thread.yield(); // passe le controle } } } class runnable12 { public static void main(String args[]) { afficheurRunnable run1 = new afficheurRunnable("1"); afficheurRunnable run2 = new afficheurRunnabl e("2"); run1.start(); run2.start(); while (true) { System.out.println("je suis l a tache principale !"); try { Thread.sleep(20); } catch (InterruptedException e) {} } } } Exécution je je je je je je je je je je je je je je je suis suis suis suis suis suis suis suis suis suis suis suis suis suis suis la le le le le le le le le la le le le l e le tache principale ! processus 1 processus 2 processus 1 processus 2 processu s 1 processus 2 processus 1 processus 2 tache principale ! processus 1 processus 2 processus 1 processus 2 processus 1 Attention ! L'implémentation et le comportement de l'ordonnanceur de processus (scheduler) n'e st pas spécifié par Sun. Cela veut dire que les différentes machines virtuelles (MV) J ava n'auront pas forcément le même comportement. Une MV peut se comporter comme un n oyau temps réel (pas de timeslicing) ou comme un noyau préemptif. Comme nous l'avons vu dans les exemples précédents, on utilise Thread.yield() afin d e redonner le contrôle à l'ordonnanceur. De cette manière, quel que soit le comporteme nt de la machine virtuelle Java, les différents processus s'entrelaceront. Il est ég alement important de s'assurer que les accès concurrents aux variables et méthodes n e mettent pas en péril la validité des données. Pour ce faire, veuillez vous reporter à la page décrivant les mécanismes d'exclusion mutuelle de Java. Pour les mécanismes de synchronisation inter-processus, veuillez vous référer à la page décrivant les signaux. 12. Exclusion mutuelle Introduction Le mécanisme d'exclusion mutuelle présent dans Java est le moniteur. Si l'on désire défi nir une section critique, afin de s'assurer de la cohérence des données, nous devons utiliser le mot-clé synchronized . C'est de cette manière que l'on crée un moniteur. Sécurisation d'une méthode Lorsque l'on crée une instance d'une certaine classe, on crée également un moniteur qu i lui est associé. Lorsque l'on applique le modificateur synchronized, on place la méthode (le bloc de code) dans ce moniteur, ce qui assure l'exclusion mutuelle. Exemple class mutexAcc { int accumulateur = 0; public synchronized void stocke(int val) { accumulateur += val; } public int lit() { return accumulateur; } } L'opération d'incrémentation += prend plusieurs instructions, et un changement de co ntexte entre elles pourrait créer une incohérence des résultats. Le modificateur synch ronized indique que la méthode stocke fait partie du moniteur de l'instance, empêcha nt son exécution simultanée par plusieurs processus. Si le moniteur est déja occupé, les processus suivants seront mis en attente. L'ordre de réveil des processus n'est p as déterministe. Sécurisation de blocs de code L'utilisation de méthodes synchronisées trop longues peut créer une baisse d'efficacité. Avec Java, il est possible de placer n'importe quel bloc dans un moniteur, ce q ui permet ainsi de réduire la longueur des sections critiques. Dans l'exemple suiv ant, le code de methode1 et de methode2 est équivalent. synchronized void methode1() { // section critique... } void methode2() { synchr onized(this) { // section critique... } } Exemple public class Tortue { private Point pos; private int angle; public Tortue(int x, int y,int angle) { // ... } public synchronized void tourne(int degres) { angle += degres; } public synchronized void avance(int distance) { pos.x += (int) ((do uble)distance*Math.cos((double)angle)); pos.y += (int) ((double)distance*Math.si n((double)angle)); } public int angle() { return angle; } public synchronized Po int pos() { return new Point(pos.x,pos.y); } } public class mutexLogo { public / / // // // } int lectureEntier() { ... lecture d'un nombre entier pouvant prendr e du temps ... public static void carre(Tortue tortue) { int largeur = lectureEntier(); int lon gueur = lectureEntier(); synchronized (tortue) { tortue.tourne(90);tortue.avance (largeur); tortue.tourne(90);tortue.avance(longueur); tortue.tourne(90);tortue.a vance(largeur); tortue.tourne(90);tortue.avance(longueur); } // autres taches... } } Comme nous l'avons vu, quand le modificateur synchronized qualifie une méthode, on place cette méthode dans le moniteur de l'instance. Cela permet de sécuriser l'accès à une instance particulière d'un objet. En créant un bloc synchronized (tortue), on en tre dans le moniteur associé à l'instance tortue. Dans l'exemple ci-dessus, si un pr ocessus est en train d'afficher un carré en faisant appel à mutexLogo.carre(tortue) , un autre processus ne pourra pas déplacer la tortue. Malgré tout, pendant que l'on se trouve dans le méthode carre et que l'on est en train de lire les dimensions du carré, un autre pro cessus pourra utiliser la tortue. En effet, le moniteur n'est pas occupé, parce qu e la méthode n'est pas synchronized, seul un bloc l'est. Sécurisation des variable de classes Considérons maintenant le cas où il faut sécuriser l'accès à une variable de classe. La so lution est simple, il faut créer un moniteur qui est commun à toutes les instances d e la classe. La méthode getClass() retourne la classe de l'instance dans laquelle on l'appelle. Nous pouvons maintenant créer un moniteur qui utilise le résultat de g etClass() comme "verrou". class mutexStatic { private int accumulateur = 0; private static int acces = 0; public synchronized void stocke(int val) { accumulateur += val; synchronized (ge tClass()) { acces += 1; } } public int lit() { synchronized (getClass()) { acces += 1; } return accumulateur; } public int acces() { return acces; } } Cet exemple définit une classe d'accumulateur qui incrémente acces chaque fois que l 'on accède à stocke ou à lit . La variable acces est une variable de classe (déclarée publ ic), elle est donc partagée par les différentes instances de cette classe. La méthode getClass() retourne un objet de type Class avec lequel on crée un nouveau moniteur . 13. Synchronisation : les méthodes wait() et notify() Il peut être nécessaire de synchroniser des processus qui accèdent aux mêmes ressources. L'utilisation des moniteurs permet de garantir l'exclusion mutuelle, et pour la synchronisation, on utilisera des signaux, qui sont modélisés par les méthodes wait() et notify(). Un des exemples classiques de l'utilisation des signaux est celui des producteur s-consommateurs. Prenons un tampon borné de n objets, un processus producteur et un processus consommateur. Exemple - tamponCirc.java class tamponCirc { private Object tampon[]; private int taille; private int en, hors, nMess; /** constructeur. crée un tampon de taille éléments */ public tamponCirc (int taille) { tampon = new Object[taille]; this.taille = taille; en = 0; hors = 0; nMess = 0; } public synchronized void depose(Object obj) { while (nMess == t aille) { // si plein try { wait(); // attends non-plein } catch (InterruptedExce ption e) {} } tampon[en] = obj; nMess++; en = (en + 1) % taille; notify(); // en voie un signal non-vide } public synchronized Object preleve() { while (nMess == 0) { // si vide try { wait(); // attends non-vide } catch (InterruptedException e) {} } Object obj = tampon[hors]; tampon[hors] = null; // supprime la ref a l' objet nMess--; hors = (hors + 1) % taille; notify(); // envoie un signal non-ple in return obj; } } Exemple - utiliseTampon.java class producteur extends Thread { private tamponCirc tampon; private int val = 0 ; public producteur(tamponCirc tampon) { this.tampon = tampon; } public void run () { while (true) { System.out.println("je depose "+val); tampon.depose(new Integer(val++)); try { T hread.sleep((int)(Math.random()*100)); attend jusqu'a 100 ms } catch (Interrupte dException e) {} } } } class consommateur extends Thread { private tamponCirc ta mpon; public consommateur(tamponCirc tampon) { this.tampon = tampon; } public vo id run() { while (true) { System.out.println("je preleve "+((Integer)tampon.prel eve()).toString()); try { Thread.sleep((int)(Math.random()*200)); attends jusqu' a 200 ms } catch (InterruptedException e) {} } } } class utiliseTampon { public static void main(String args[]) { tamponCirc tampon = new tamponCirc(5); product eur prod = new producteur(tampon); consommateur cons = new consommateur(tampon); prod.start(); cons.start(); try { Thread.sleep(30000); // // secondes } } // s'execute pendant 30 } catch (InterruptedException e) {} Exécution ... je depose 165 je depose 166 je preleve 161 je depose 167 je preleve 162 je d epose 168 je preleve 163 je depose 169 je preleve 164 je depose 170 je preleve 165 je depose 171 je preleve 166 je preleve 167 ... Nous déclarons 2 processus, un producteur et un consommateur. Les données sont produ ites plus vite que le consommateur ne peux les prélever, à cause de la différence de d urée des délais (aléatoires) introduits dans les 2 processus. Il existe deux variantes de la méthode wait() qui permettent de spécifier un temps l imite après lequel le processus sera réveillé, sans avoir à être notifié par un processus c ncurrent. Il s'agit de wait(long milli), qui attend milli millisecondes, et de w ait(long milli,int nano) qui attend nano nanosecondes en plus du temps milli. Il n'est pas possible de savoir si wait() s'est terminé à cause d'un appel à notify() pa r un autre processus, ou de l'épuisement du temps. La méthode notifyAll() réveille tou s les processus, et dès que le moniteur sera libre, ils se réveilleront tour à tour. Attention ! Le noyau Java ne fait aucune garantie concernant l'élection des processus lors d'u n appel à notify(). En particulier, il ne garantit pas que les processus seront débl oqués dans l'ordre ou ils ont été bloqués. C'est à cause de cela que l'on a placé wait() da s une boucle dans l'exemple précédent, car un consommateur pourrait réveiller un autre consommateur alors que le tampon est vide. Remarquons aussi que les méthodes wait, notify() et notifyAll() ne peuvent être appe lées que dans des méthodes synchronisées (synchronized). 14. Les Exceptions Une exception survient lors d'un cas de figure anormal. Elle interrompt le flot normal d'exécution et exécute les éventuelles routines de traitement. Par exemple, s'i l on essaye d'ouvrir en lecture un fichier qui n'existe pas, une exception IOExc eption sera générée. En Java, il existe une classe Exception. Si l'on désire créer un nouveau "type" d'ex ception, il suffit d'étendre la classe, comme présenté ci-dessous : class divisionParZeroException extends Exception { } Remarquons que Exception et Error sont des sous-classes de Throwable . Les objets de la classe Error sont des erreurs internes à la machine virtuelle Java, c'est pourq uoi il faut être très vigilant si l'on désire les récupérer. En effet, il ne serait pas trè judicieux d'ignorer une exception de type "Plus de mémoire". Si dans un bloc (une méthode), on fait appel à une méthode qui peut potentiellement génére r une exception, on doit soit essayer de la récupérer avec try, soit ajouter le mot clé throws dans la déclaration du bloc. Si on ne le fait pas, il en résultera une erre ur à la compilation. Il n'est toutefois pas requis de traiter les exceptions et erreurs déclarées dans le paquetage java.lang, car celles-ci peuvent se déclencher partout. Po ur générer une exception, il suffit d'utiliser throw, suivi d'un objet dont la class e est dérivée de Throwable. Si l'on veut générer une exception dans une méthode, avec thro w, il faudra l'indiquer dans la déclaration de méthode, en utilisant le mot clé throws . throw new Exception(); throw new IOException("fichier ABSENT.TXT non trouve"); t hrow e; // si on ne fait que propager l'exception e Un exemple complet de l'utilisation des exceptions est donné à la page décrivant la cl ause try. Attention ! Attention à l'orthographe, lorsque l'on indique qu'une méthode peut générer une exceptio n, dans la déclaration de la méthode, on utilise le mot clé throws , qui prend un s. L orsque l'on génére l'exception, on utilise throw, qui ne prend pas de s. 15. Type Wrappers A chaque type standard est associée une classe que l'on appelle type wrapper. Il e xiste les classes Boolean, Integer, Long, Float, Double, Character. Ces classes peuvent stocker une valeur du type correspondant, mais surtout, elles offrent de s méthodes facilitant les tâches de conversion. Grâce à ces classes, on peut par exemple effectuer la conversion d'une chaîne de caractères en nombre entier. Ces méthodes de conversion opèrent sur des instances, mais il est également possible d'utiliser des méthodes statiques. Exemples Float objetPi = new Float("3.1415"); float r = 3.5f; float perimetre = 2 * objet Pi.floatValue() * r; int valeur = Integer.valueOf("555").intValue(); String peri metreStr = Float.toString(perimetre); Attention ! Ces classes ne sont pas interchangeables avec les types d'origine, car il s'agit d'objets. S'il on désire obtenir la valeur contenue dans un objet de ce type, on utilisera la méthode typeValue() où type est le nom du type standard. 16. Incrémentation et décrémentation Il existe deux opérateurs facilitant l'incrémentation et la décrémentation. Il s'agit re spectivement de ++ et --. Ils peuvent tous deux être utilisés en configuration postf ixe, ou préfixe. C'est-à-dire avant l'identificateur, ou après celui-ci. Si l'on place l'opérateur avant l' identificateur, on effectuera l'incrémentation avant l'exécution de la ligne en cours. La ligne System.out.println(x++); est équivalente à System.ou t.println(x); x = x+1; . De même, System.out.println(++x); équivaut à x = x+1;System.o ut.println(x);. Exemple class incrementation { public static void main(String args[]) { int x = 0; int y = 0; x++; ++y; // postfixe // prefixe System.out.println("x == "+x); System.out.println("y == "+y); System.out.println ("x++ == "+ x++); System.out.println("++y == "+ ++y); System.out.println("x == " +x); System.out.println("y == "+y); x = x++; y = --y; // inutile car equivalent a "x = x; x = x+1;" // inutile car equivalent a "y = y-1; y = y;" System.out.println("x == "+x); System.out.println("y == "+y); x = y++; // equiva lent a "x = y; y = y+1;" System.out.println("x == "+x); System.out.println("y == "+y); } } Exécution x == 1 y == 1 x++ == 1 ++y == 2 x == 2 y == 2 x == 2 y == 1 x == 1 y == 2 Notons que l'incrémentation et la décrémentation sont également possibles sur des variab le à virgule flottante. Cela a pour effet de leur ajouter, respectivement soustrai re, la quantité 1.0. 17. Les fichiers En Java, on peut facilement accéder à des fichiers à partir d'une application. On ne p eut pas le faire à partir d'une applet pour des questions de sécurité. On remarquera éga lement qu'à tout accès au système de fichiers, il peut se produire une exception java. io.IOException. L'exemple ci-dessous montre comment obtenir des informations sur un fichier. Not ons qu'il n'existe pas de classe spéciale pour traiter des répertoires, ceux-ci sont considérés comme des fichiers. Informations sur un fichier import java.io.*; class fileInfo { public static void main (String args[]) throw s java.io.IOException { System.out.println("Entrez le repertoire ou se trouve le fichier : "); char ch; // directory StringBuffer dirBuf = new StringBuffer(); w hile ((ch = (char)System.in.read()) != '\n') dirBuf.append(ch); File dir = new F ile(dirBuf.toString()); // file System.out.println("Entrez le nom du fichier : " ); StringBuffer fileBuf = new StringBuffer(); while ((ch = (char)System.in.read( )) != '\n') fileBuf.append(ch); File fil = new File(dir,fileBuf.toString()); if (fil.exists()) { System.out.println("Fichier trouve"); System.out.println("Nom d e fichier : "+fil.getName()); System.out.println("Chemin du fichier : "+fil.getP ath()); System.out.println("Chemin absolu : "+fil.getAbsolutePath()); System.out .println("Droit de lecture : "+fil.canRead()); System.out.println("Droit d'ecrit ure : "+fil.canWrite()); System.out.println("\nContenu du repertoire :"); String listing[] = dir.list(); for (int i = 0;i < listing.length;i++) System.out.print ln(listing[i]); } } } else { System.out.println("Fichier absent"); } La lecture et l'écriture se fait en utilisant les différentes classes de traitement des flux (streams). L'exemple ci-dessous copie un fichier source dans un fichier destination, après avoir effectué un certain nombre de tests pour vérifier que l'opérat ion de copie est possible. Les classes utilisées sont FileInputStream et FileOutpu tStream du paquetage java.io. Ces deux classes étendent java.io.InputStream. Copieur de fichiers import java.io.*; class fileCopy { public static void main (String args[]) throw s java.io.IOException { char ch; // repertoire source System.out.println("repert oire source : "); StringBuffer dirBuf = new StringBuffer(); while ((ch = (char)S ystem.in.read()) != '\n') dirBuf.append(ch); File srcDir = new File(dirBuf.toStr ing()); if (!srcDir.exists()) { System.out.println("repertoire absent"); System. exit(1); } else if (!srcDir.canRead()) { System.out.println("repertoire illisibl e"); System.exit(1); } // fichier source System.out.println("fichier source : ") ; StringBuffer fileBuf = new StringBuffer(); while ((ch = (char)System.in.read() ) != '\n') fileBuf.append(ch); File srcFile = new File(srcDir,fileBuf.toString() ); if (!srcFile.exists()) { System.out.println("fichier absent"); System.exit(1) ; } else if (!srcFile.canRead()) { System.out.println("fichier illisible"); Syst em.exit(1); } // repertoire destination System.out.println("repertoire destinati on : "); dirBuf = new StringBuffer(); while ((ch = (char)System.in.read()) != '\ n') dirBuf.append(ch); File dstDir = new File(dirBuf.toString()); if (!dstDir.exists()) { System.out.println("repertoire absent"); System.exit(1); } else if (!dstDir.canWrite()) { System.out.println("repertoire protege en ecri ture"); System.exit(1); } // fichier destination System.out.println("fichier des tination : "); fileBuf = new StringBuffer(); while ((ch = (char)System.in.read() ) != '\n') fileBuf.append(ch); File dstFile = new File(dstDir,fileBuf.toString() ); if (dstFile.exists()) { System.out.println("fichier existant"); System.exit(1 ); } // copie du fichier FileInputStream inStream = new FileInputStream(srcFile) ; FileOutputStream outStream = new FileOutputStream(dstFile); // tant que ce n'e st pas la fin du fichier while (inStream.available() > 0) outStream.write(inStre am.read()); // fermeture des fichiers inStream.close(); outStream.close(); } } 18. Les connexions réseau TCP/IP la classe Socket Avec Java, il est très facile d'ouvrir une connexion TCP/IP depuis une applet ou u ne application. La classe Socket fournit tous les outils nécessaires à cela. Une foi s qu'une connexion est réalisée avec une autre machine, on a accès à un flux d'entrée et u n flux de sortie, comme si l'on travaillait avec un fichier. Exemple import java.io.*; import java.net.*; public class jnetcat { public static void m ain(String args[]) { if (args.length != 2) System.out.println("usage : jnetcat h ote port"); else { Socket sk = null; try { sk = new Socket(args[0],Integer.valueOf(args[1]).intValu e()); DataInputStream is = new DataInputStream(sk.getInputStream()); String lign e; while ((ligne = is.readLine()) != null) System.out.println(ligne); } catch (U nknownHostException e) { System.out.println("hote inconnu : "+e.getMessage()); } catch (IOException e) { System.out.println("erreur entree/sortie : "+e.getMessa ge()); } } } } Cette application se connecte sur la machine et le port spécifiés sur la ligne de co mmande. Elle lit ensuite toutes les données, et les affiche à l'écran. On peut par exe mple exécuter : java jnetcat eig6.unige.ch 13 Exécution - port 13 Thu May 30 11:23:19 1996 Le port 13 donne l'heure, on parle du généralement du port daytime. Exécution - port 19 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abc defg !"#$%&' ()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcd efgh "#$%&'()*+,-. /0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcde fghi #$%&'()*+,-./012345 6789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdef ghij $%&'()*+,-./0123456789:;< =>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefg hijk %&'()*+,-./0123456789:;<=>?@ABC DEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefgh ijkl &'()*+,-./0123456789:;<=>?@ABCDEFGHIJ KLMNOPQRSTUVWXYZ[\]^_`abcdefghi jklm '()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQ RSTUVWXYZ[\]^_`abcdefghij klmn Le port 19 est le port générateur de caractères, chargen, il envoie des lignes de cara ctères tant que le client est connecté. Pour arrêter le client, on doit presser contrôle -C . 19. Les serveurs TCP/IP - SocketServer Nous allons voir comment réaliser une application serveur TCP/IP. La classe Socket Server permet de se mettre en attente d'un client sur un port. La méthode SocketSe rver.accept() retourne un objet de la classe Socket lorsqu'un client veut se con necter. C'est grâce à cet objet que l'on pourra établir une connexion avec le client. Serveur d'heure import java.net.*; import java.io.*; import java.util.Date; class serveurHeure { public static void main(String args[]) { try { ServerSocket sks = new ServerSoc ket(11111); Socket sk = sks.accept(); System.out.println(sk.toString()); DataOut putStream out = new DataOutputStream(sk.getOutputStream()); out.writeBytes("Bien venue "+sk.getInetAddress().getHostName()+ ". Il est actuellement "+new Date()+" \n"); sk.close(); sks.close(); } catch (IOException e) { System.out.println(e.ge tMessage()); } } } Le résultat que l'on peut voir apparaitre sur la console où l'on a lancé le serveur es t : Socket[addr=129.194.184.2,port=2569,localport=11111] Pour se connecter à ce serveu r, on peut utiliser telnet ou l'utilitaire jnetcat qui a été décrit dans les pages décrivant la classe Socket. Le client qui se connecte sur le p ort 11111 du serveur peut voir apparaitre un message du type : Bienvenue eig. Il est actuellement Thu May 30 14:12:35 MET DST 1996 Généralement, on désire pouvoir servir plusieurs applications clientes à partir du même se rveur, comme c'est le cas pour FTP et HTTP. Nous allons voir comment implémenter u n serveur multi-thread, qui crée un processus par client. Nous déclarons une classe connexion qui est capable de traiter les entrées/sorties d'un client que nous inst ancions chaque fois qu'un client se connecte sur le port serveur. Le programme c i-dessous est un exemple de serveur qui peut s'accomoder de plusieurs clients co ncurrement. Lorsqu'un client s'y connecte, le serveur initialise un compteur à 0, et chaque nombre que le client lui envoie est additionné à ce compteur. Serveur multi-clients /* * Serveur multi-clients * Additionne tous les nombres fournis par les clients */ import java.io.*; import java.net.*; public class serveurAdd { static int po rt; public static void main(String args[]) { if (args.length != 1) System.out.pr intln("usage : serveurAdd port"); else { ServerSocket serveur; try { port = Inte ger.parseInt(args[0]); } catch (NumberFormatException e) { System.out.println("l e port doit etre une valeur entre 1024 et 65535"); System.exit(1); } try { serve ur = new ServerSocket(port); for (;;) new connexion(serveur.accept()); } catch ( IOException e) { System.out.println("erreur a la creation d'un objet Socket : "+ e.getMessage()); System.exit(1); } } } } class connexion implements Runnable { S ocket client; DataInputStream depuisClient; PrintStream versClient; int somme = 0; public connexion(Socket client) { this.client = client; try { // creation des flux de/vers le client depuisClient = new DataInputStream(client.getInputStream ()); versClient = new PrintStream(client.getOutputStream()); // message d'accuei l versClient.println("Bienvenue sur le serveur additionneur"); versClient.printl n("Entrez un nombre suivi de entree"); } } catch (IOException e) { try { client.close(); } catch (IOException ee) {} } ne w Thread(this).start(); public void run() { try { String lue; while ((lue = depuisClient.readLine()) != null) { somme += Integer.parseInt(lue); versClient.println("merci. La somme est maintenant "+somme); } versClient.println("Au revoir !"); } catch (IOException e ) { System.out.println("Exception entree/sortie : "+e.getMessage()); } catch (Nu mberFormatException e) { versClient.println("Seul les nombres sont autorises. Au revoir !"); } stop(); } public void stop() { try { client.close(); } catch (IOE xception e) { System.out.println("exception a la fermeture d'une connexion : "+e ); } } } 20. Une Application client-serveur Il est conseillé de lire les pages concernant les classes Socket et SocketServer a vant de poursuivre la lecture de cette page. Nous allons créer un système qui permet à des applets d'afficher des messages sur un s ystème serveur. Lorsque le logiciel de navigation lance l'exécution de l'applet cli, celle-ci se connecte sur le serveur pour obtenir une liste de noms. Cette liste est traitée est insérée dans une objet graphique List (voir awt). Lorsque l'utilisate ur presse le bouton "envoi", l'applet transmet le message au serveur, dans le fo rmat : send:nom:message Lorsque le serveur recoit un message comme celui-ci, il l'affiche sur la console . La liste de noms que le serveur envoie à l'applet est contenue dans un fichier q ui est lu à l'initialisation. Le client - cli.java import import import import import java.awt.*; java.applet.*; java.io.*; java.ne t.*; java.util.StringTokenizer; public class cli extends Applet implements Runnable { final static int serverPor t = 11115; Thread kicker; List beepList; TextField messageArea; Button sendButto n; String serverHost; Socket sk; DataInputStream fromServer; PrintStream toServe r; public void init() { add(new Label("Client")); beepList = new List(5,false); add(beepList); add(new Label("Entrez votre message : ")); messageArea = new Text Field(30); add(messageArea); sendButton = new Button("Envoi"); add(sendButton); serverHost = getCodeBase().getHost(); } public void start() { if (kicker == null ) { kicker = new Thread(this); kicker.start(); } } public void stop() { if (kick er != null) { kicker.stop(); } } kicker = null; public void run() { try { connect(); String line; while ((line = fromServer.read Line()) != null) { StringTokenizer st = new StringTokenizer(line,":",false); Str ing token = st.nextToken(); if (token.equals("beep")) { beepList.addItem(st.next Token()); } else if (token.equals("message")) { System.out.println("message du s erveur : "+st.nextToken()); } else if (token.equals("hello")) { showStatus("conn ected to server"); toServer.println("list"); } else if (token.equals("end")) { s howStatus("Entrez votre message et pressez Envoi"); } else { toServer.println("m essage:Unknown command"); System.out.println("message inconnu du serveur : "+lin e); } } } catch (IOException e) { showStatus("Ne peut pas se connecter au serveu r"); } finally { disconnect(); } } public boolean action(Event evt,Object arg) { if (evt.target instanceof Button) { if (evt.target == sendButton) try { sendPag e(); } catch (IOException e) { System.out.println("IO : "+e.getMessage()); } } e lse return false; return true; } public void sendPage() throws IOException { if (beepList.getSelectedItem() == null) { showStatus("Vous devez selectionner un de stinataire"); return; } if (messageArea.getText().equals("")) { showStatus("Vous devez entrer un message"); return; } System.out.println("Envoi d'un message a " +beepList.getSelectedItem()+" !"); toServer.println("send:"+beepList.getSelectedItem()+":"+messageArea. getText()); } public void connect() throws IOException { sk = new Socket(serverHost,serverP ort); fromServer = new DataInputStream(sk.getInputStream()); toServer = new Prin tStream(new DataOutputStream(sk.getOutputStream())); } public void disconnect() { try { sk.close(); } catch (IOException e) { } } } Le serveur - ser.java /* * Serveur multi-clients */ import java.io.*; import java.net.*; import java.u til.*; public class ser { static int port; */ /** cree une Hashtable avec le nom des personnes atteignables public static Hashtable buildDataBase(String filename) { Hashtable db = new Hash table(); try { DataInputStream fileInput = new DataInputStream(new FileInputStre am(filename)); String line; while ((line = fileInput.readLine()) != null) { Stri ngTokenizer st = new StringTokenizer(line,":",false); db.put(st.nextToken(),st.n extToken()); } } catch (NoSuchElementException e) { System.out.println("Erreur d e format dans la base de donnees : "+e.getMessage()); System.exit(1); } catch (I OException e) { System.out.println("Erreur lors de la lecture de la base de donn ees : "+e.getMessage()); System.exit(1); } return db; } public static void main( String args[]) { if (args.length != 2) System.out.println("usage : ser bdonnee port"); else { ServerSocket server; try { port = Integer.parseInt(args[1]); } catch (NumberFormatException e) { System.o ut.println("le port doit etre une valeur entre 1024 et 65535"); System.exit(1); } try { server = new ServerSocket(port); Hashtable dataBase = buildDataBase(args [0]); for (;;) new connection(server.accept(),dataBase); } catch (IOException e) { System.out.println("erreur a la creation d'un objet Socket : "+e.getMessage() ); System.exit(1); } } } } class connection implements Runnable { Hashtable db; Socket client; DataInputStream fromClient; PrintStream toClient; int somme = 0; public connection(Socket client,Hashtable db) { this.client = client; this.db = db; try { // creation des flux de/to le client fromClient = new DataInputStream( client.getInputStream()); toClient = new PrintStream(client.getOutputStream()); System.out.println("Nouvelle connexion au serveur : "+client); } catch (IOExcept ion e) { try { client.close(); } catch (IOException ee) {} } new Thread(this).st art(); } public void run() { try { toClient.println("hello"); String line; while ((line = fromClient.readLine()) != null) { StringTokenizer st = new StringToken izer(line,":",false); String token = st.nextToken(); if (token.equals("list")) s endList(); else if (token.equals("send")) { sendPage(st.nextToken(),st.nextToken ()); toClient.println("message:Le serveur vous dit merci !"); } else if (token.equals("message")) System.out.println("message : "+st.nextToken ()); else if (token.equals("exit") || token.equals("quit")) break; else toClient .println("message:Unknown command"); } toClient.println("message:bye !"); } catc h (IOException e) { System.out.println("Exception entree/sortie : "+e.getMessage ()); } stop(); } public void stop() { try { client.close(); } catch (IOException e) { System.out.println("exception a la fermeture d'une connection : "+e); } } /** envoie la liste de noms au client */ public void sendList() { for (Enumerati on e = db.keys();e.hasMoreElements();) { toClient.println("beep:"+(String)e.next Element()); } toClient.println("end"); } /** cette methode est appelee lorsqu'un client demande l'envoi d'un message */ public void sendPage(String name,String msg) { System.out.println("Requete d'envoi du message \""+msg+"\" a "+name); } }