Développement d'un serveur Java multithread simple

Julien DEFAUT

Cet article explique la réalisation pas à pas d'un serveur multithread en Java utilisant les sockets. Ce serveur se chargera de broadcaster les messages lui arrivant à tous les clients qui y sont connectés. ♪

- Titre : Développement d'un serveur Java multithread simple

- Auteur : Julien DEFAUT

- Parution : 14 mars 2005

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2005 Julien DEFAUT. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Introduction

- I. Le fonctionnement

- II. Le développement

- III. La compilation

- IV. Premier démarrage

- Téléchargements

Introduction

Nous allons, dans cet article, tenter de comprendre les mécanismes de base de la communication par sockets via la construction d'un serveur Java multiclient. Ce serveur s'attachera à simplement rediriger les flux arrivant d'un client connecté vers tous les autres et ceci indépendamment du type du client et du système l'hébergeant. En effet les sockets sont une couche suffisamment bas niveau pour être inclus dans la plupart des systèmes et pour être supportés par une majorité des langages de programmation. Voici les fonctionnalités du serveur Java que nous allons programmer, baptisé Blablaserveur : - serveur en Java, donc multiplateforme, - multithread, avec donc la possibilité d'accepter plusieurs clients simultanément, - action du serveur : reçoit un message du client, l'envoie alors à tous les autres connectés, - possibilité d'interroger le serveur à travers des commandes serveurs. Tout ceci peut sembler peu, mais ça implique une universalité du serveur. En plus d'être multiplateforme, il va s'avérer très simple de mettre en relation des clients de différents langages. Ainsi, il sera possible de faire communiquer des clients de tous types entre eux (flash, Java, c#, c++, delphi… ). L'inconvénient sera la sécurité puisque n'importe qui pourra développer un client pour « gêner » votre application. Mais il ne s'agit pas non plus de faire un serveur IRC, et BlablaServeur pourra convenir pour de petites applications.

I - Le fonctionnement

Voici un aperçu de l'architecture de l'application que nous allons développer :

D'abord, définition : qu'est-ce qu'un Thread ? Il s'agit d'un ensemble d'instructions qui va se partager le processeur avec d'autres « morceaux » du programme. On a ainsi l'impression que plusieurs actions sont effectuées en parallèle. Ici, nous aurons trois types de thread :

- le thread principal : il va attendre les connexions en écoutant le port ouvert pour le serveur. À chaque connexion il va lancer un nouveau thread de type client ;

- le thread client : il y en a donc un associé à chaque client connecté, il est chargé d'attendre l'arrivée d'un message. À chaque fois, il enverra le message reçu à tous les clients connectés ;

- le thread des commandes : il attend que l'admin du serveur tape des commandes dans la console pour faire une action (quitter le serveur par exemple).

Et en pratique, on fait ça comment ?

II - Le développement

Le langage choisi ici est le Java, simplement parce qu'on pourra lancer le serveur aussi facilement sous Unix, que sous Windows ou que sous Mac (et aussi parce que les sockets sont assez simples à utiliser en Java).

Nous allons créer trois classes :

BlablaServ, correspond au thread principal. Contient les méthodes suivantes :

- public static void main(String args[])

- static private void printWelcome(Integer port)

- synchronized public void sendAll(String message,String sLast)

- synchronized public void delClient(int i)

- synchronized public int addClient(PrintWriter out)

- synchronized public int getNbClients()

BlablaThread, correspond au thread client. Contient les méthodes suivantes :

- BlablaThread(Socket s, BlablaServ blablaServ)

- public void run()

Commandes, correspond au thread des commandes. Contient les méthodes suivantes :

- Commandes(BlablaServ blablaServ)

- public void run()

Créez un répertoire « blabla » et placez-y trois fichiers de type texte vides : BlablaServ.java, BlablaThread.java, Commandes.java.

Dans les explications nous ne détaillerons pas trop le code, il semble suffisamment commenté pour comprendre chaque ligne.

La classe BlablaServ

Mettez dans le fichier BlablaServ.java, le code suivant :

BlablaServ.java

1. import java.net.*;

2. import java.io.*;

3. import java.util.*;

5. //** Classe principale du serveur, gère les infos globales **

6. public class BlablaServ

7. {

8. private Vector _tabClients = new Vector(); // contiendra tous les flux de sortie vers les clients

9. private int _nbClients=0; // nombre total de clients connectés

11. //** Methode : la première méthode exécutée, elle attend les connexions **

12. public static void main(String args[])

13. {

14. BlablaServ blablaServ = new BlablaServ(); // instance de la classe principale

15. try

16. {

17. Integer port;

18. if(args.length<=0) port=new Integer("18000"); // si pas d'argument : port 18000 par défaut

19. else port = new Integer(args[0]); // sinon il s'agit du numéro de port passé en argument

21. new Commandes(blablaServ); // lance le thread de gestion des commandes

23. ServerSocket ss = new ServerSocket(port.intValue()); // ouverture d'un socket serveur sur port

24. printWelcome(port);

25. while (true) // attente en boucle de connexion (bloquant sur ss.accept)

26. {

27. new BlablaThread(ss.accept(),blablaServ); // un client se connecte, un nouveau thread client est lancé

28. }

29. }

30. catch (Exception e) { }

31. }

33. //** Methode : affiche le message d'accueil **

34. static private void printWelcome(Integer port)

35. {

36. System.out.println("--------");

37. System.out.println("BlablaServeur : Par Minosis - Julien Defaut");

38. System.out.println("Copyright : 2004 - Minosis.com");

39. System.out.println("Derniere version : 10/04/2004");

40. System.out.println("--------");

41. System.out.println("Demarre sur le port : "+port.toString());

42. System.out.println("--------");

43. System.out.println("Quitter : tapez \"quit\"");

44. System.out.println("Nombre de connectes : tapez \"total\"");

45. System.out.println("--------");

46. }

49. //** Methode : envoie le message à tous les clients **

50. synchronized public void sendAll(String message,String sLast)

51. {

52. PrintWriter out; // déclaration d'une variable permettant l'envoi de texte vers le client

53. for (int i = 0; i < _tabClients.size(); i++) // parcours de la table des connectés

54. {

55. out = (PrintWriter) _tabClients.elementAt(i); // extraction de l'élément courant (type PrintWriter)

56. if (out != null) // sécurité, l'élément ne doit pas être vide

57. {

58. // écriture du texte passé en paramètre (et concaténation d'une string de fin de chaine si besoin)

59. out.print(message+sLast);

60. out.flush(); // envoi dans le flux de sortie

61. }

62. }

63. }

65. //** Methode : détruit le client no i **

66. synchronized public void delClient(int i)

67. {

68. _nbClients--; // un client en moins ! snif

69. if (_tabClients.elementAt(i) != null) // l'élément existe ...

70. {

71. _tabClients.removeElementAt(i); // ... on le supprime

72. }

73. }

75. //** Methode : ajoute un nouveau client dans la liste **

76. synchronized public int addClient(PrintWriter out)

77. {

78. _nbClients++; // un client en plus ! ouaaaih

79. _tabClients.addElement(out); // on ajoute le nouveau flux de sortie au tableau

80. return _tabClients.size()-1; // on retourne le numéro du client ajouté (size-1)

81. }

83. //** Methode : retourne le nombre de clients connectés **

84. synchronized public int getNbClients()

85. {

86. return _nbClients; // retourne le nombre de clients connectés

87. }

89. }

La fonction statique main() est exécutée en premier au lancement de l'application. Les paramètres passés au démarrage de l'application se trouvent stockés dans le tableau de strings args[]. Le seul argument que nous gérons est le numéro de port. Si l'argument est absent, alors le port sera 18000. Si le port est déjà pris par une autre application, ou un problème survient lors de la création du ServerSoket, une exception est levée et un message d'erreur est affiché.

Juste après l'instanciation du ServerSocket, un message d'accueil est lancé ainsi que le thread d'attente des commandes.

Puis une boucle infinie attend les connexions des clients. Dès qu'une requête arrive, ss.accept() est débloqué et une nouvelle instance de BlablaThread est créée. L'objet BlablaServer est passé en paramètre pour pouvoir utiliser, dans les threads clients, ses méthodes de gestion du vecteur _tabClients. On observe que ces méthodes sont « synchronised », cela signifie que chaque thread, voulant en exécuter une, devra attendre qu'un thread ait fini de l'utiliser. Ceci évite ainsi des accès simultanés aux variables communes (_tabClients et _nbClients).

La classe BlablaThread

Mettez dans le fichier BlablaThread.java, le code suivant :

BlablaThread.java

1. import java.net.*;

2. import java.io.*;

4. //** Classe associée à chaque client **

5. //** Il y aura autant d'instances de cette classe que de clients connectés **

6. //implémentation de l'interface Runnable (une des 2 méthodes pour créer un thread)

7. class BlablaThread implements Runnable

8. {

9. private Thread _t; // contiendra le thread du client

10. private Socket _s; // recevra le socket liant au client

11. private PrintWriter _out; // pour gestion du flux de sortie

12. private BufferedReader _in; // pour gestion du flux d'entrée

13. private BlablaServ _blablaServ; // pour utilisation des méthodes de la classe principale

14. private int _numClient=0; // contiendra le numéro de client géré par ce thread

16. //** Constructeur : crée les éléments nécessaires au dialogue avec le client **

17. BlablaThread(Socket s, BlablaServ blablaServ) // le param s est donnée dans BlablaServ par ss.accept()

18. {

19. _blablaServ=blablaServ; // passage de local en global (pour gestion dans les autres méthodes)

20. _s=s; // passage de local en global

21. try

22. {

23. // fabrication d'une variable permettant l'utilisation du flux de sortie avec des string

24. _out = new PrintWriter(_s.getOutputStream());

25. // fabrication d'une variable permettant l'utilisation du flux d'entrée avec des string

26. _in = new BufferedReader(new InputStreamReader(_s.getInputStream()));

27. // ajoute le flux de sortie dans la liste et récupération de son numéro

28. _numClient = blablaServ.addClient(_out);

29. }

30. catch (IOException e){ }

32. _t = new Thread(this); // instanciation du thread

33. _t.start(); // démarrage du thread, la fonction run() est ici lancée

34. }

36. //** Methode :  exécutée au lancement du thread par t.start() **

37. //** Elle attend les messages en provenance du serveur et les redirige **

38. // cette méthode doit obligatoirement être implémentée à cause de l'interface Runnable

39. public void run()

40. {

41. String message = ""; // déclaration de la variable qui recevra les messages du client

42. // on indique dans la console la connexion d'un nouveau client

43. System.out.println("Un nouveau client s'est connecte, no "+_numClient);

44. try

45. {

46. // la lecture des données entrantes se fait caractère par caractère ...

47. // ... jusqu'à trouver un caractère de fin de chaine

48. char charCur[] = new char[1]; // déclaration d'un tableau de char d'1 élément, _in.read() y stockera le char lu

49. while(_in.read(charCur, 0, 1)!=-1) // attente en boucle des messages provenant du client (bloquant sur _in.read())

50. {

51. // on regarde si on arrive à la fin d'une chaine ...

52. if (charCur[0] != '\u0000' && charCur[0] != '\n' && charCur[0] != '\r')

53. message += charCur[0]; // ... si non, on concatène le caractère dans le message

54. else if(!message.equalsIgnoreCase("")) // juste une vérification de principe

55. {

56. if(charCur[0]=='\u0000') // le dernier caractère était '\u0000' (char de terminaison nulle)

57. // on envoie le message en disant qu'il faudra concaténer '\u0000' lors de l'envoi au client

58. _blablaServ.sendAll(message,""+charCur[0]);

59. else _blablaServ.sendAll(message,""); // sinon on envoie le message à tous

60. message = ""; // on vide la chaine de message pour qu'elle soit réutilisée

61. }

62. }

63. }

64. catch (Exception e){ }

65. finally // finally se produira le plus souvent lors de la déconnexion du client

66. {

67. try

68. {

69. // on indique à la console la déconnexion du client

70. System.out.println("Le client no "+_numClient+" s'est deconnecte");

71. _blablaServ.delClient(_numClient); // on supprime le client de la liste

72. _s.close(); // fermeture du socket s'il ne l'a pas déjà été (à cause de l'exception levée plus haut)

73. }

74. catch (IOException e){ }

75. }

76. }

77. }

Le socket créé par ss.accept() est ici reçu en paramètre, à partir de _s, on crée ainsi les buffers gérant le flux d'entrée et le flux de sortie.

Le thread est ensuite lancé (méthode run()). Une attente en boucle des données a lieu. Dès qu'un caractère arrive en entrée, il est concaténé à la chaine _message. Le message est envoyé dès qu'un caractère de fin de chaine est détecté. S’il s'agit de '\u0000', celui-ci sera concaténé au message lors de l'envoi (méthode sendAll()). Ceci est nécessaire, essentiellement à cause des clients Flash pour lesquels le message reçu doit se finir par le zéro terminal (cf. doc Flash partie onXML). Les autres types de clients sont, en général, plus tolérants. Au moins, BlablaServeur fonctionnera indépendamment du type de clients.

Enfin, après tout événement entraînant la fin de la boucle, le bloc finally{} sera exécuté pour mettre fin proprement à l'existence du thread client.

La classe Commandes

Mettez dans le fichier Commandes.java, le code suivant :

Commandes.java

1. import java.io.*;

3. //** Classe qui gère les commandes tapées dans la console **

4. // implémentation de l'interface Runnable (une des 2 méthodes pour créer un thread)

5. class Commandes implements Runnable

6. {

7. BlablaServ _blablaServ; // pour utilisation des méthodes de la classe principale

8. BufferedReader _in; // pour gestion du flux d'entrée (celui de la console)

9. String _strCommande=""; // contiendra la commande tapée

10. Thread _t; // contiendra le thread

12. //** Constructeur : initialise les variables nécessaires **

13. Commandes(BlablaServ blablaServ)

14. {

15. _blablaServ=blablaServ; // passage de local en global

16. // le flux d'entrée de la console sera géré plus pratiquement dans un BufferedReader

17. _in = new BufferedReader(new InputStreamReader(System.in));

18. _t = new Thread(this); // instanciation du thread

19. _t.start(); // démarrage du thread, la fonction run() est ici lancée

20. }

22. //** Methode : attend les commandes dans la console et exécute l'action demandée **

23. public void run() // cette méthode doit obligatoirement être implémentée à cause de l'interface Runnable

24. {

25. try

26. {

27. // si aucune commande n'est tapée, on ne fait rien (bloquant sur _in.readLine())

28. while ((_strCommande=_in.readLine())!=null)

29. {

30. if (_strCommande.equalsIgnoreCase("quit")) // commande "quit" detectée ...

31. System.exit(0); // ... on ferme alors le serveur

32. else if(_strCommande.equalsIgnoreCase("total")) // commande "total" detectée ...

33. {

34. // ... on affiche le nombre de clients actuellement connectés

35. System.out.println("Nombre de connectes : "+_blablaServ.getNbClients());

36. System.out.println("--------");

37. }

38. else

39. {

40. // si la commande n'est ni "total", ni "quit", on informe l'utilisateur et on lui donne une aide

41. System.out.println("Cette commande n'est pas supportee");

42. System.out.println("Quitter : \"quit\"");

43. System.out.println("Nombre de connectes : \"total\"");

44. System.out.println("--------");

45. }

46. System.out.flush(); // on affiche tout ce qui est en attente dans le flux

47. }

48. }

49. catch (IOException e) {}

50. }

51. }

Cette classe n'est pas indispensable dans le fonctionnement du serveur, mais apporte la possibilité d'une administration propre même si minimaliste.

La classe est instanciée dès le chargement du serveur (méthode main() de BlablaServ). Elle crée dès le départ un buffer d'entrée à partir du flux provenant du système (texte tapé dans la console en général, dans une fenêtre Dos sous Windows ou le terminal sous Unix/Linux). L'activité principale (dans run()) est d'attendre les instructions et de les exécuter. La méthode readLine() est ici plus adaptée et plus simple que le read() utilisé dans le thread client. Bien sûr la méthode readLine() n'était pas intéressante dans le thread client puisque le zéro terminal devait être géré.

Deux commandes sont implémentées : le « quit » qui appelle un simple System.exit(0) et le « total » qui informe sur la quantité de clients connectés.

III - La compilation

Le code est écrit, il reste maintenant à compiler nos .java et exécuter notre serveur.

Nous partons du principe que le jdk version 1.4.1 ou plus est installé sur votre machine. Comme il y a peu de packages évolués d'utilisés dans notre code, il ne devrait pas y avoir trop de problèmes sur des jdk inférieurs. Je n'ai cependant pas testé.

Ouvrez une console (Dos, xTerm… selon votre OS). Placez-vous dans le répertoire blabla et faites javac *.java. Normalement, un message indique que l'on utilise un élément déprécié de l'API. Il s'agit de fermeture sauvage du thread client _t.stop(). Nous ne tiendrons pas compte de ce message donc son apparition indique que la compilation s'est bien déroulée. Trois fichiers .class avec les mêmes noms que les .java doivent être apparus dans le répertoire « blabla ». Dans le cas contraire, vérifiez que vous avez effectué toutes les tâches correctement…

IV - Premier démarrage

Après avoir téléchargé le serveur (ou suivi les chapitres précédents), décompressez le zip dans le répertoire de votre choix (nous l'appellerons « blabla/ »).

Pour lancer le serveur sous n'importe quel système, tapez en ligne de commande dans le répertoire blabla : java -jar BlablaServ.jar 18000. Sous Windows, la commande java BlablaServ peut suffire, mais je n'ai pas réussi à lancer le serveur de cette manière sous Unix. Un fichier jar exécutable est donc contenu dans le zip et permet d'avoir un serveur qui fonctionne partout. Un jar exécutable est une simple archive zip renommée qui contient les classes ainsi qu'un manifest identifiant la classe principale. Décompressez le jar si vous souhaitez en comprendre la structure.

Encore plus simplement, dans le zip vous trouverez deux fichiers pour lancer le serveur sur le port 18000, éditez-les pour changer le port :

Windows : double-cliquez sur BlablaServ.bat

Unix/Linux : tapez sh BlablaServ.sh ou ./BlablaServ.sh

En cas de problème, vérifiez que la version de votre JRE installée est la 1.4.1 minimum pour être sûr du fonctionnement du serveur. Avec des versions antérieures, le serveur n'a pas été testé, mais peut sûrement fonctionner. Pour consulter la version, lancez une fenêtre console (Invite msdos par exemple) et tapez java -version. Si vous voulez installer la version la plus récente du JRE, rendez-vous sur le site de Sun. Vérifiez aussi que le port 18000 n'est pas pris.

Logiquement à ce stade un message apparaît en indiquant que le serveur est démarré sur le port 18000 :

On voit qu'en tapant le mot « total » (puis Entrer), le nombre de connectés apparaît. Logiquement, pour l'instant le nombre est 0.

Pour quitter proprement le serveur, veillez à taper « quit ».

Pour tester le serveur, l'archive propose un programme en Flash, il suffit d'ouvrir le fichier blablachat.swf de l'archive. Vous pouvez choisir l'adresse du serveur. Mais pour modifier le port de connexion 18000 par défaut, éditez blablachat.fla et changez la ligne socket.connect(_root.ipserveur, 18000); avec le port qui vous convient.

Téléchargements

Téléchargez les sources et packages du projet (inclus client Flash de test).