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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
import
java.net.*;
import
java.io.*;
import
java.util.*;
//** Classe principale du serveur, gère les infos globales **
public
class
BlablaServ
{
private
Vector _tabClients =
new
Vector
(
); // contiendra tous les flux de sortie vers les clients
private
int
_nbClients=
0
; // nombre total de clients connectés
//** Methode : la première méthode exécutée, elle attend les connexions **
public
static
void
main
(
String args[])
{
BlablaServ blablaServ =
new
BlablaServ
(
); // instance de la classe principale
try
{
Integer port;
if
(
args.length<=
0
) port=
new
Integer
(
"18000"
); // si pas d'argument : port 18000 par défaut
else
port =
new
Integer
(
args[0
]); // sinon il s'agit du numéro de port passé en argument
new
Commandes
(
blablaServ); // lance le thread de gestion des commandes
ServerSocket ss =
new
ServerSocket
(
port.intValue
(
)); // ouverture d'un socket serveur sur port
printWelcome
(
port);
while
(
true
) // attente en boucle de connexion (bloquant sur ss.accept)
{
new
BlablaThread
(
ss.accept
(
),blablaServ); // un client se connecte, un nouveau thread client est lancé
}
}
catch
(
Exception e) {
}
}
//** Methode : affiche le message d'accueil **
static
private
void
printWelcome
(
Integer port)
{
System.out.println
(
"--------"
);
System.out.println
(
"BlablaServeur : Par Minosis - Julien Defaut"
);
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
(
"--------"
);
}
//** Methode : envoie le message à tous les clients **
synchronized
public
void
sendAll
(
String message,String sLast)
{
PrintWriter out; // déclaration d'une variable permettant l'envoi de texte vers le client
for
(
int
i =
0
; i <
_tabClients.size
(
); i++
) // parcours de la table des connectés
{
out =
(
PrintWriter) _tabClients.elementAt
(
i); // extraction de l'élément courant (type PrintWriter)
if
(
out !=
null
) // sécurité, l'élément ne doit pas être vide
{
// écriture du texte passé en paramètre (et concaténation d'une string de fin de chaine si besoin)
out.print
(
message+
sLast);
out.flush
(
); // envoi dans le flux de sortie
}
}
}
//** Methode : détruit le client no i **
synchronized
public
void
delClient
(
int
i)
{
_nbClients--
; // un client en moins ! snif
if
(
_tabClients.elementAt
(
i) !=
null
) // l'élément existe ...
{
_tabClients.removeElementAt
(
i); // ... on le supprime
}
}
//** Methode : ajoute un nouveau client dans la liste **
synchronized
public
int
addClient
(
PrintWriter out)
{
_nbClients++
; // un client en plus ! ouaaaih
_tabClients.addElement
(
out); // on ajoute le nouveau flux de sortie au tableau
return
_tabClients.size
(
)-
1
; // on retourne le numéro du client ajouté (size-1)
}
//** Methode : retourne le nombre de clients connectés **
synchronized
public
int
getNbClients
(
)
{
return
_nbClients; // retourne le nombre de clients connectés
}
}
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
import
java.net.*;
import
java.io.*;
//** Classe associée à chaque client **
//** Il y aura autant d'instances de cette classe que de clients connectés **
//implémentation de l'interface Runnable (une des 2 méthodes pour créer un thread)
class
BlablaThread implements
Runnable
{
private
Thread _t; // contiendra le thread du client
private
Socket _s; // recevra le socket liant au client
private
PrintWriter _out; // pour gestion du flux de sortie
private
BufferedReader _in; // pour gestion du flux d'entrée
private
BlablaServ _blablaServ; // pour utilisation des méthodes de la classe principale
private
int
_numClient=
0
; // contiendra le numéro de client géré par ce thread
//** Constructeur : crée les éléments nécessaires au dialogue avec le client **
BlablaThread
(
Socket s, BlablaServ blablaServ) // le param s est donnée dans BlablaServ par ss.accept()
{
_blablaServ=
blablaServ; // passage de local en global (pour gestion dans les autres méthodes)
_s=
s; // passage de local en global
try
{
// fabrication d'une variable permettant l'utilisation du flux de sortie avec des string
_out =
new
PrintWriter
(
_s.getOutputStream
(
));
// fabrication d'une variable permettant l'utilisation du flux d'entrée avec des string
_in =
new
BufferedReader
(
new
InputStreamReader
(
_s.getInputStream
(
)));
// ajoute le flux de sortie dans la liste et récupération de son numéro
_numClient =
blablaServ.addClient
(
_out);
}
catch
(
IOException e){
}
_t =
new
Thread
(
this
); // instanciation du thread
_t.start
(
); // démarrage du thread, la fonction run() est ici lancée
}
//** Methode : exécutée au lancement du thread par t.start() **
//** Elle attend les messages en provenance du serveur et les redirige **
// cette méthode doit obligatoirement être implémentée à cause de l'interface Runnable
public
void
run
(
)
{
String message =
""
; // déclaration de la variable qui recevra les messages du client
// on indique dans la console la connexion d'un nouveau client
System.out.println
(
"Un nouveau client s'est connecte, no "
+
_numClient);
try
{
// la lecture des données entrantes se fait caractère par caractère ...
// ... jusqu'à trouver un caractère de fin de chaine
char
charCur[] =
new
char
[1
]; // déclaration d'un tableau de char d'1 élément, _in.read() y stockera le char lu
while
(
_in.read
(
charCur, 0
, 1
)!=-
1
) // attente en boucle des messages provenant du client (bloquant sur _in.read())
{
// on regarde si on arrive à la fin d'une chaine ...
if
(
charCur[0
] !=
'
\u0000
'
&&
charCur[0
] !=
'
\n
'
&&
charCur[0
] !=
'
\r
'
)
message +=
charCur[0
]; // ... si non, on concatène le caractère dans le message
else
if
(!
message.equalsIgnoreCase
(
""
)) // juste une vérification de principe
{
if
(
charCur[0
]==
'
\u0000
'
) // le dernier caractère était '\u0000' (char de terminaison nulle)
// on envoie le message en disant qu'il faudra concaténer '\u0000' lors de l'envoi au client
_blablaServ.sendAll
(
message,""
+
charCur[0
]);
else
_blablaServ.sendAll
(
message,""
); // sinon on envoie le message à tous
message =
""
; // on vide la chaine de message pour qu'elle soit réutilisée
}
}
}
catch
(
Exception e){
}
finally
// finally se produira le plus souvent lors de la déconnexion du client
{
try
{
// on indique à la console la déconnexion du client
System.out.println
(
"Le client no "
+
_numClient+
" s'est deconnecte"
);
_blablaServ.delClient
(
_numClient); // on supprime le client de la liste
_s.close
(
); // fermeture du socket s'il ne l'a pas déjà été (à cause de l'exception levée plus haut)
}
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é. 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
import
java.io.*;
//** Classe qui gère les commandes tapées dans la console **
// implémentation de l'interface Runnable (une des 2 méthodes pour créer un thread)
class
Commandes implements
Runnable
{
BlablaServ _blablaServ; // pour utilisation des méthodes de la classe principale
BufferedReader _in; // pour gestion du flux d'entrée (celui de la console)
String _strCommande=
""
; // contiendra la commande tapée
Thread _t; // contiendra le thread
//** Constructeur : initialise les variables nécessaires **
Commandes
(
BlablaServ blablaServ)
{
_blablaServ=
blablaServ; // passage de local en global
// le flux d'entrée de la console sera géré plus pratiquement dans un BufferedReader
_in =
new
BufferedReader
(
new
InputStreamReader
(
System.in));
_t =
new
Thread
(
this
); // instanciation du thread
_t.start
(
); // démarrage du thread, la fonction run() est ici lancée
}
//** Methode : attend les commandes dans la console et exécute l'action demandée **
public
void
run
(
) // cette méthode doit obligatoirement être implémentée à cause de l'interface Runnable
{
try
{
// si aucune commande n'est tapée, on ne fait rien (bloquant sur _in.readLine())
while
((
_strCommande=
_in.readLine
(
))!=
null
)
{
if
(
_strCommande.equalsIgnoreCase
(
"quit"
)) // commande "quit" detectée ...
System.exit
(
0
); // ... on ferme alors le serveur
else
if
(
_strCommande.equalsIgnoreCase
(
"total"
)) // commande "total" detectée ...
{
// ... on affiche le nombre de clients actuellement connectés
System.out.println
(
"Nombre de connectes : "
+
_blablaServ.getNbClients
(
));
System.out.println
(
"--------"
);
}
else
{
// si la commande n'est ni "total", ni "quit", on informe l'utilisateur et on lui donne une aide
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
(
); // on affiche tout ce qui est en attente dans le flux
}
}
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.
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).