Développement d'un serveur Java multi-thread simple
Date de publication : 14/03/2005 ,
Date de mise a jour : 27/03/2005
Par
Minosis (Homepage)
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.
Téléchargez la version pdf. Et après l'article : détente avec du sudoku.
Introduction
1. Le fonctionnement
2. Le développement
3. La compilation
4. 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épendemment du type du client et du sytè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.
1. 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. A 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. A 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 ?
2. 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 3 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 textes 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 import java.net.*;
import java.io.*;
import java.util.*;
public class BlablaServ
{
private Vector _tabClients = new Vector();
private int _nbClients=0;
public static void main(String args[])
{
BlablaServ blablaServ = new BlablaServ();
try
{
Integer port;
if(args.length<=0) port=new Integer("18000");
else port = new Integer(args[0]);
new Commandes(blablaServ);
ServerSocket ss = new ServerSocket(port.intValue());
printWelcome(port);
while (true)
{
new BlablaThread(ss.accept(),blablaServ);
}
}
catch (Exception e) { }
}
static private void printWelcome(Integer port)
{
System.out.println("--------");
System.out.println("BlablaServeur : Par Minosis - Minosis");
System.out.println("Copyright : 2004 - Minosis.com");
System.out.println("Derniere version : 10/04/2004");
System.out.println("--------");
System.out.println("Demarre sur le port : "+port.toString());
System.out.println("--------");
System.out.println("Quitter : tapez \"quit\"");
System.out.println("Nombre de connectes : tapez \"total\"");
System.out.println("--------");
}
synchronized public void sendAll(String message,String sLast)
{
PrintWriter out;
for (int i = 0; i < _tabClients.size(); i++)
{
out = (PrintWriter) _tabClients.elementAt(i);
if (out != null)
{
out.print(message+sLast);
out.flush();
}
}
}
synchronized public void delClient(int i)
{
_nbClients--;
if (_tabClients.elementAt(i) != null)
{
_tabClients.removeElementAt(i);
}
}
synchronized public int addClient(PrintWriter out)
{
_nbClients++;
_tabClients.addElement(out);
return _tabClients.size()-1;
}
synchronized public int getNbClients()
{
return _nbClients;
}
}
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 import java.net.*;
import java.io.*;
class BlablaThread implements Runnable
{
private Thread _t;
private Socket _s;
private PrintWriter _out;
private BufferedReader _in;
private BlablaServ _blablaServ;
private int _numClient=0;
BlablaThread(Socket s, BlablaServ blablaServ)
{
_blablaServ=blablaServ;
_s=s;
try
{
_out = new PrintWriter(_s.getOutputStream());
_in = new BufferedReader(new InputStreamReader(_s.getInputStream()));
_numClient = blablaServ.addClient(_out);
}
catch (IOException e){ }
_t = new Thread(this);
_t.start();
}
public void run()
{
String message = "";
System.out.println("Un nouveau client s'est connecte, no "+_numClient);
try
{
char charCur[] = new char[1];
while(_in.read(charCur, 0, 1)!=-1)
{
if (charCur[0] != '\u0000' && charCur[0] != '\n' && charCur[0] != '\r')
message += charCur[0];
else if(!message.equalsIgnoreCase(""))
{
if(charCur[0]=='\u0000')
_blablaServ.sendAll(message,""+charCur[0]);
else _blablaServ.sendAll(message,"");
message = "";
}
}
}
catch (Exception e){ }
finally
{
try
{
System.out.println("Le client no "+_numClient+" s'est deconnecte");
_blablaServ.delClient(_numClient);
_s.close();
}
catch (IOException e){ }
}
}
}
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é. Si 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 import java.io.*;
class Commandes implements Runnable
{
BlablaServ _blablaServ;
BufferedReader _in;
String _strCommande="";
Thread _t;
Commandes(BlablaServ blablaServ)
{
_blablaServ=blablaServ;
_in = new BufferedReader(new InputStreamReader(System.in));
_t = new Thread(this);
_t.start();
}
public void run()
{
try
{
while ((_strCommande=_in.readLine())!=null)
{
if (_strCommande.equalsIgnoreCase("quit"))
System.exit(0);
else if(_strCommande.equalsIgnoreCase("total"))
{
System.out.println("Nombre de connectes : "+_blablaServ.getNbClients());
System.out.println("--------");
}
else
{
System.out.println("Cette commande n'est pas supportee");
System.out.println("Quitter : \"quit\"");
System.out.println("Nombre de connectes : \"total\"");
System.out.println("--------");
}
System.out.flush();
}
}
catch (IOException e) {}
}
}
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.
3. 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ème sur des jdk inférieurs. Je n'ai cependant pas testé.
Ouvrez une console (Dos, xTerm, ... selon votre OS). Placez vous dans le repertoire "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. 3 fichiers .class avec les même noms que les .java doivent être apparues dans le répertoire "blabla". Dans le cas contraire vérifiez que vous avez effectué toutes les tâches correctement ...
4. 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'appelerons "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 : tappez "sh BlablaServ.sh" ou "./BlablaServ.sh"
En cas de problème vérifiez que la version de votre JRE d'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 la version pdf de cet article.
Téléchargez les sources et packages du projet (inclus client Flash de test).
|