Dans ce tutorial, nous allons créer chat dans lequel chaque client peut choisir son salon (room) et voir tous ceux qui s'y trouvent.
Vous pouvez directement observer le résultat de ce tutorial
en ligne.
Dans un premier temps, nous allons nous occuper de la partie serveur toujours basée sur le serveur de
ce tutorial. Nos explications seront en php puisque il s'agit du langage le plus rependu.
L'équivalent en perl est aussi disponible au bas de cette page. Globalement, le perl et le php sont assez proches pour pouvoir passer de l'un a l'autre facilement.
Voici donc la base de notre serveur :
<?php
$mainsock = socket_create(AF_INET, SOCK_STREAM, 0);
socket_set_option($mainsock, SOL_SOCKET,SO_REUSEADDR, 1);
socket_bind($mainsock, "127.0.0.1", 22222) or die('Could not bind to address');
socket_listen($mainsock);
///////////////////
$clients=Array();
$compteur=0;
echo("En attente d'un client !\n");
while(true){
$toread=Array();
array_push($toread,$mainsock);
for ($i=0;$i<count($clients);$i++){ // pour tous les clients
array_push($toread,$clients[$i]["SOCKET"]);
}
socket_select($toread,$a=null,$a=null,$a=null);
if(in_array($mainsock, $toread)){// le mainsock est dans le tableau $toread.
// c'est notre mainsock donc un nouveau client
$sock=socket_accept($mainsock);
echo("Arrivée d'un nouveau client !\n");
$compteur++;
$nb=count($clients);
$clients[$nb]["SOCKET"]=$sock;
$clients[$nb]["UID"]=$compteur;
}else{
// c'est un client qui dit quelque chose
for ($i=0;$i<count($clients);$i++){ // on cherche le client
if(in_array($clients[$i]["SOCKET"], $toread)) { // celui la est dans le tableau toread
$input = socket_read($clients[$i]["SOCKET"], 1024);
if($input==null){
/// deconnection du client !
for ($j=0;$j<count($clients);$j++){ // on le cherche dans le tableau
if($clients[$j]==$clients[$i]){ // trouvé
//////////////////////////////////////////////////Perte d'un client
//////////////////////////////////////////////////
echo("Deconnection du client ".$clients[$j]["UID"]."\n");
array_splice($clients,$j,1); // on le retire du tableau
$i--;
}
}
}else{
//////////////////////////////////////////////////Message d'un client
//////////////////////////////////////////////////
}
}
}
}
}
function sendToAll($sauf,$msg){
global $clients;
for($i=0;$i<count($clients);$i++){ // pour tous les clients sauf celui qui envoie
if($sauf!=$clients[$i]["UID"]){
socket_write($clients[$i]["SOCKET"],$msg.chr(0));
}
}
}
php?>
Dans le code ci-dessus nous avons mis en évidence (séparés par des ////) les deux zones dans lesquelles nous allons travailler.
Ces deux zones représentent : la gestion de déconnection d'un client et de réception de messages.
Il nous faut d'abord décider du format des messages envoyés par un client. Voici un exemple des messages que le serveur peut recevoir. Chaque message commence par une action suivis de paramètres séparés par un caractère spécial (nous choisissons le signe "="). Nous n'utilisons pas d'XML qui est trop gourmant en CPU et bande passante :
INIT=1256=totoLe client transmettra donc toutes ses informations quand il serra prêt à être affiché sur l'écran des autres. Ces informations seront enregistrées par le serveur sous forme de variables dans l'objet client afin que si un autre se connecte, on puisse lui transmettre toutes les informations des autres clients déjà en ligne. Le premier paramètre (1256) est donc la couleur de du client. Le second (toto) est son pseudo.
SETROOM=roomLe client change de room. Le paramètre est donc le nom de la room.
MSG=mon texteUn client viens de chatter on récupère le texte. Le serveur connaissant la room du client s'occupe de transmettre les informations aux autres.
Ensuite les messages que le serveur peut envoyer aux clients :
NEWONE=12556=totoOn informe le client qu'un nouveau viens d'arriver. Le premier paramètre est sa couleur. Le second est son pseudo.
MSG=le message=couleurUn client viens de chatter, le serveur en informe les autres de la même room.
USERLIST=pseudo1,couleur1=pseudo2,couleur2On informe les clients de ceux qui se trouve dans cette room. Chaque paramètre est un couple pseudo/couleur séparé par une ",".
ROOMLIST=nom1,nombre1=nom2,nombre2On informe les clients de la liste des room ouvertes avec le nombre de connectés.
A l'initialisation, le client se connecte, choisis (aléatoirement pour simplifier) la couleur de son texte, transmet au serveur le pseudo choisis par l'internaute ainsi que sa room. Dans notre cas, on ne fait rien quand le client se connecte. On va attendre qu'il soit "initialisé" avec son nom et qu'il est défini sa room pour en informer les autres clients. Nous utiliserons alors la fonction déclarée
sendToAll().
Pour gérer les room, nous allons mémoriser dans chaque objet client du serveur, la room dans laquelle il se trouve. Nous enverrons alors les messages aux clients se trouvant dans la même room. Nous allons donc commencer par modifier la fonction
sendToAll() afin que l'on puisse aussi lui indiquer la room dans laquelle seul les clients peuvent recevoir des messages. Notez aussi que nous devons envisager la possibilité d'envoyer des messages à tous les clients sans spécifier de room. D'autre part, nous n'envoyons des messages aux clients que s'ils sont "initialisés" :
function sendToAll($sauf,$msg,$room){
global $clients;
for($i=0;$i<count($clients);$i++){ // pour tous les clients sauf celui qui envoie
if($sauf!=$clients[$i]["UID"] && ($room==$clients[$i]["ROOM"] || $room==null) && $clients[$i]["INIT"]){
socket_write($clients[$i]["SOCKET"],$msg.chr(0));
}
}
}
Il nous faut également une fonction indiquant aux clients les rooms actuellements ouvertes avec le nombre d'internautes dedans :
function sendRoomList(){
global $clients;
$memoire=Array();
for($i=0;$i<count($clients);$i++){ // pour tous les clients sauf celui qui envoie
if($clients[$i]["INIT"]){
if($memoire[$clients[$i]["ROOM"]]){
$memoire[$clients[$i]["ROOM"]]++;
}else{
$memoire[$clients[$i]["ROOM"]]=1;
}
}
}
$message="ROOMLIST";
foreach($memoire as $nom => $nbc){
$message.="=".$nom.",".$nbc;
}
sendToAll(null,$message,null);
}
Une foie que l'on a construit notre message, nous l'envoyons à tous les clients de toutes les room. De même, il nous faut une
fonction indiquant aux clients la liste de ceux qui se trouve dans leur room :
function sendUserList($room){
global $clients;
$message="USERLIST";
for($i=0;$i<count($clients);$i++){ // pour tous les clients sauf celui qui envoie
if($clients[$i]["INIT"] && $clients[$i]["ROOM"]==$room){
$message.="=".$clients[$i]["PSEUDO"].",".$clients[$i]["COLOR"];
}
}
sendToAll(null,$message,$room);
}
Une fois que l'on a construit notre message, nous l'envoyons à tous les clients de cette room.
Nous allons donc traiter les messages des clients. Chaque client nous envoie un message quand il s'initialise, change de room ou quand il chat. Nous allons décomposer cette variable afin d'en connaître les composants et tester de quelle action il s'agit :
$comp=split("=", $input);
if($comp[0]=="INIT"){
}else if($comp[0]=="SETROOM"){
}else if($comp[0]=="MSG"){
}
Nous avons ici un léger problème. Comme expliqué dans
ce post , flash utilise un byte 0 pour terminer ses messages. Ce caractère de fin se retrouve donc dans notre variable
$input. D'une manière générale, nous allons retirer tous caractères de fin envoyé par les clients. Pour cela nous allons utiliser la fonction php
trim() :
$input=trim($input);
$comp=split("=", $input);
if($comp[0]=="INIT"){
}else if($comp[0]=="SETROOM"){
}else if($comp[0]=="MSG"){
}
Il ne nous reste plus qu'a traiter chaque action. L'action "INIT" doit mémoriser les données dans les variables du client. L'action "SETROOM" doit changer le nom de la room dans l'objet client, ré envoyer la liste des utilisateurs connectés à son ancienne room (seulement s'il était dans une room), transmettre la liste des clients connectés à tous ceux de cette nouvelle room, transmettre à tous les clients la liste des room ouvertes avec leur nombre de connectés. L'action "MSG" doit simplement envoyer le message aux clients de même room.
$input=trim($input);
$comp=split("=", $input);
if($comp[0]=="INIT"){
$clients[$i]["INIT"]=true; // on mémorise que ce client est initialisé
$clients[$i]["COLOR"]=$comp[1];
$clients[$i]["PSEUDO"]=$comp[2];
}else if($comp[0]=="SETROOM"){
//// on informe ceux de l'ancienne room
$ancienne=$clients[$i]["ROOM"];
$clients[$i]["ROOM"]=$comp[1];
if($ancienne){
sendUserList($ancienne);
}
//// on informe ceux de la nouvelle room
sendUserList($comp[1]);
//// on informe tous le monde des room ouvertes
sendRoomList();
}else if($comp[0]=="MSG"){
sendToAll($clients[$i]["UID"],"MSG=".$clients[$i]["PSEUDO"].">".$comp[1]."=".$clients[$i]["COLOR"],$clients[$i]["ROOM"]);
}
Nous devons aussi penser à informer tous les clients si l'un d'entre nous se déconnecte. Donc dans la zone "Perte d'un client" nous placerons le code qui permet d'envoyer l'information à ceux de la room du client que quelqu'un est partit. Nous mettons aussi à jour la liste des room ouvertes :
$clients[$i]["INIT"]=false; // pour ne plus lui envoyer de message
sendUserList($clients[$i]["ROOM"]);
sendRoomList();
Le code du serveur est donc :
<?php
$mainsock = socket_create(AF_INET, SOCK_STREAM, 0);
socket_set_option($mainsock, SOL_SOCKET,SO_REUSEADDR, 1);
socket_bind($mainsock, "127.0.0.1", 22222) or die('Could not bind to address');
socket_listen($mainsock);
///////////////////
$clients=Array();
$compteur=0;
echo("En attente d'un client !\n");
while(true){
$toread=Array();
array_push($toread,$mainsock);
for ($i=0;$i<count($clients);$i++){ // pour tous les clients
array_push($toread,$clients[$i]["SOCKET"]);
}
socket_select($toread,$a=null,$a=null,$a=null);
if(in_array($mainsock, $toread)){// le mainsock est dans le tableau $toread.
// c'est notre mainsock donc un nouveau client
$sock=socket_accept($mainsock);
echo("Arrivée d'un nouveau client !\n");
$compteur++;
$nb=count($clients);
$clients[$nb]["SOCKET"]=$sock;
$clients[$nb]["UID"]=$compteur;
}else{
// c'est un client qui dit quelque chose
for ($i=0;$i<count($clients);$i++){ // on cherche le client
if(in_array($clients[$i]["SOCKET"], $toread)) { // celui la est dans le tableau toread
$input = socket_read($clients[$i]["SOCKET"], 1024);
if($input==null){
/// deconnection du client !
for ($j=0;$j<count($clients);$j++){ // on le cherche dans le tableau
if($clients[$j]==$clients[$i]){ // trouvé
//////////////////////////////////////////////////Perte d'un client
$clients[$i]["INIT"]=false; // pour ne plus lui envoyer de message
sendUserList($clients[$i]["ROOM"]);
sendRoomList();
//////////////////////////////////////////////////
echo("Deconnection du client ".$clients[$j]["UID"]."\n");
array_splice($clients,$j,1); // on le retire du tableau
$i--;
}
}
}else{
//////////////////////////////////////////////////Message d'un client
$input=trim($input);
$comp=split("=", $input);
if($comp[0]=="INIT"){
$clients[$i]["INIT"]=true; // on mémorise que ce client est initialisé
$clients[$i]["COLOR"]=$comp[1];
$clients[$i]["PSEUDO"]=$comp[2];
}else if($comp[0]=="SETROOM"){
//// on informe ceux de l'ancienne room
$ancienne=$clients[$i]["ROOM"];
$clients[$i]["ROOM"]=$comp[1];
if($ancienne){
sendUserList($ancienne);
}
//// on informe ceux de la nouvelle room
sendUserList($comp[1]);
//// on informe tous le monde des room ouvertes
sendRoomList();
}else if($comp[0]=="MSG"){ sendToAll($clients[$i]["UID"],"MSG=".$clients[$i]["PSEUDO"].">".$comp[1]."=".$clients[$i]["COLOR"],$clients[$i]["ROOM"]);
}
//////////////////////////////////////////////////
}
}
}
}
}
function sendToAll($sauf,$msg,$room){
global $clients;
for($i=0;$i<count($clients);$i++){ // pour tous les clients sauf celui qui envoie
if($sauf!=$clients[$i]["UID"] && ($room==$clients[$i]["ROOM"] || $room==null) && $clients[$i]["INIT"]){
socket_write($clients[$i]["SOCKET"],$msg.chr(0));
}
}
}
function sendRoomList(){
global $clients;
$memoire=Array();
for($i=0;$i<count($clients);$i++){ // pour tous les clients sauf celui qui envoie
if($clients[$i]["INIT"]){
if($memoire[$clients[$i]["ROOM"]]){
$memoire[$clients[$i]["ROOM"]]++;
}else{
$memoire[$clients[$i]["ROOM"]]=1;
}
}
}
$message="ROOMLIST";
foreach($memoire as $nom => $nbc){
$message.="=".$nom.",".$nbc;
}
sendToAll(null,$message,null);
}
function sendUserList($room){
global $clients;
$message="USERLIST";
for($i=0;$i<count($clients);$i++){ // pour tous les clients sauf celui qui envoie
if($clients[$i]["INIT"] && $clients[$i]["ROOM"]==$room){
$message.="=".$clients[$i]["PSEUDO"].",".$clients[$i]["COLOR"];
}
}
sendToAll(null,$message,$room);
}
php?>
A ce stade le serveur est prêt. Nous allons maintenant nous occuper du client flash. Pour faire simple, nous utiliserons deux images clés. Le première contiendra le code de la gestion socket et l'interface de sélection du pseudo. La seconde, le code des éléments graphiques du chat (ceux-ci n'étant pas présents sur la première clé, on ne peut leur donner des actions) et tous ces champs dynamiques. Nous allons reprendre le code
de ce tutorial :
stop();
var socket = new XMLSocket()
socket.connect("127.0.0.1", 22222)
socket.onConnect = function(success){
}
socket.onData=function(msg){
}
Vous trouverez le fla (fla_etape_1.fla) à cette étape avec les éléments graphiques déjà nommés et installés en bas de la page.
Nous allons indiquer dans le
onConnect() si la connection s'est bien effectuée ou non :
socket.onConnect = function(success){
if(success){
txt_info.text="Connecté sur le port 22222 !"
}else{
txt_info.text="Impossible de se connecter au port 22222 !";
}
}
De la même façon, nous allons alerter l'internaute si la connection est perdue :
socket.onClose = function(){
txt_info.text="Connection perdue !"
}
Nous allons déterminer la couleur de texte de ce client. Nous utiliserons la fonction
random(). La couleur doit être au format hexadécimal, c'est pour cette raison que nous utilisons la fonction
toString(16) :
var selfColor=random(255).toString(16)+random(255).toString(16)+random(255).toString(16);
Dans un premier temps, l'internaute doit choisir son pseudo. Nous allons alors directement programmer l'évènement de validation. Quand l'internaute clique sur "Entrer", il faut tester q'il ait bien tapé quelque chose dans le champ de texte. Si c'est le cas, on demande au serveur de l'initialiser, on part à la clé suivante et on défini sa room (déjà définie dans le champ de texte) :
bt_valide.onRelease=function(){
if(txt_pseudo.text.length>0){
socket.send("INIT="+selfColor+"="+escape(txt_pseudo.text))
nextFrame();
socket.send("SETROOM="+escape(txt_salon.text))
}
}
Notez la présence de la fonction
escape(). Imaginez ce qui se passe si le client désire se nommer "=-moi-=". Le signe "=" du pseudo entrerait en conflit avec notre signe "=" choisis pour séparer les variables. Ainsi on est sur que le message est toujours bien formaté.
Maintenant que nous sommes sur la seconde clé du fla, nous allons donner la possibilité aux clients de chatter. Rappel : le serveur ne nous renvoie pas nos propres messages. Il faut donc penser à ajouter notre texte nous même. Seulement, le champ de texte est au format html. Il y'aura donc un bug si l'on envoie un caractère comme "<" ou "&". Nous allons donc écrire une fonction
parseHtml() sur la première image clé qui permettra de nettoyer le texte afin de bien l'afficher. Le plus simple est de couper le texte en tableau et de le reconstituer avec les bons caractères :
function parseHtml(msg){
var tab=msg.split("&");
msg=tab.join("&");
var tab=msg.split("<");
msg=tab.join("<");
return msg;
}
Notre action de boutons est donc :
bt_send.onRelease=function(){
if(txt_message.text.length){
socket.send("MSG="+escape(txt_message.htmlText));
txt_message.htmlText=""
txt_ecran.htmlText+="<font color='"+selfColor+"'>"+txt_message.htmlText+"</font>"
txt_ecran.scroll=txt_ecran.maxscroll;
}
}
Toujours sur la clé deux, l'internaute peut choisir de changer de room. Ce changement sera détecté grâce à la méthode
onChanged() du champs de texte
txt_salon. Quand on change de room, on en avertit le serveur qui nous renvoies toutes les nouvelles données :
txt_salon.onChanged=function(){
socket.send("SETROOM="+escape(this.text));
}
Nous allons maintenant nous occuper de ce que le serveur nous envoie. Comme pour le serveur, il nous faut d'abord décomposer le message afin d'en connaître l'action :
socket.onData=function(msg){
var comp=msg.split("=");
if(comp[0]=="ROOMLIST"){
}else if(comp[0]=="USERLIST"){
}else if(comp[0]=="MSG"){
}
}
Premièrement l'action "ROOMLIST". Le serveur nous donne donc la liste de toutes les room ouvertes avec le nombre de connectés. Nous allons faire en sorte que l'on puisse cliquer sur la room pour y entrer directement. Pour cela, ce champs de texte doit être au format html. Nous y placerons le tag "<A HREF='asfunction:func,param'>" pour appeler une fonction quand on y clique dessus. Nous allons donc écrire cette fonction :
function clickRoom(room){
txt_salon.text=unescape(nom)
socket.send("SETROOM="+this.text);
}
Ici, pas besoin de fonction
escape() puisque le paramètre sera déjà parsé dans le tag html. Ne reste plus qu'a remplir notre champ de texte
txt_room grâce aux informations du serveur dans l'action "ROOMLIST". Ces informations sont au format : "room,nombre". Nous devons donc décomposer chaque paramètres :
txt_room.htmlText="";
for(var i=1;i<comp.length;i++){
var detail=comp[i].split(",");
txt_room.htmlText+="<A HREF='asfunction:clickRoom,"+detail[0]+"'><U>"+parseHtml(unescape(detail[0]))+"
("+detail[1]+")</U></A><br>";
}
Remarquez ici la présence de la fonction
unescape(). Souvenez-vous que pour être sur que le choix hasardeux du pseudo ou de la room d'un client ne perturbe pas le format de nos messages, nous utilisions la fonction
escape(). Pour récupérer le texte au bon format, nous utilisons donc l'inverse qui est bien
unescape().
Nous allons faire pareil pour l'action "USERLIST" mais sans se préoccuper du tag "<A HREF>". En revanche, il faut le mettre à la bonne couleur :
txt_user.htmlText="";
for(var i=1;i<comp.length;i++){
var detail=comp[i].split(",");
txt_user.htmlText+="<FONT COLOR='#"+detail[1]+"'>"+parseHtml(unescape(detail[0]))+"</font><br>";
}
Ne reste plus qu'à traiter l'action "MSG" avec la bonne couleur :
txt_ecran+="<FONT COLOR='#"+comp[2]+"'>"+parseHtml(unescape(comp[1]))+"</FONT><br>";
txt_ecran.scroll=txt_ecran.maxscroll;
Notre gestion de réceptions de données deviens alors :
socket.onData=function(msg){
var comp=msg.split("=");
if(comp[0]=="ROOMLIST"){
txt_room.htmlText="";
for(var i=1;i<comp.length;i++){
var detail=comp[i].split(",");
txt_room.htmlText+="<A HREF='asfunction:clickRoom,"+detail[0]+"'><U>"+parseHtml(unescape(detail[0]))+" ("+detail[1]+")</U></A><BR>";
}
}else if(comp[0]=="USERLIST"){
txt_user.htmlText="";
for(var i=1;i<comp.length;i++){
var detail=comp[i].split(",");
txt_user.htmlText+="<FONT COLOR='#"+detail[1]+"'>"+parseHtml(unescape(detail[0]))+"</font><br>";
}
}else if(comp[0]=="MSG"){
txt_ecran.htmlText+="<FONT COLOR='#"+comp[2]+"'>"+parseHtml(unescape(comp[1]))+"</FONT><br>";
txt_ecran.scroll=txt_ecran.maxscroll;
}
}
Et voila :-)
Au final, le code sur la première clé ressemble à ceci :
stop();
var socket = new XMLSocket()
var selfColor=random(255).toString(16)+random(255).toString(16)+random(255).toString(16);
socket.connect("127.0.0.1", 22222)
socket.onConnect = function(success){
if(success){
txt_info.text="Connecté sur le port 12000 !"
}else{
txt_info.text="Impossible de se connecter au port 12000 !";
}
}
socket.onClose = function(){
txt_info.text="Connection perdue !"
}
bt_valide.onRelease=function(){
if(txt_pseudo.text.length>0){
socket.send("INIT="+selfColor+"="+escape(txt_pseudo.text))
nextFrame();
socket.send("SETROOM="+escape(txt_salon.text))
}
}
socket.onData=function(msg){
trace(msg)
var comp=msg.split("=");
if(comp[0]=="ROOMLIST"){
txt_room.htmlText="";
for(var i=1;i<comp.length;i++){
var detail=comp[i].split(",");
txt_room.htmlText+="<A HREF='asfunction:clickRoom,"+detail[0]+"'><U>"+parseHtml(unescape(detail[0]))+" ("+detail[1]+")</U></A><BR>";
}
}else if(comp[0]=="USERLIST"){
txt_user.htmlText="";
for(var i=1;i<comp.length;i++){
var detail=comp[i].split(",");
txt_user.htmlText+="<FONT COLOR='#"+detail[1]+"'>"+parseHtml(unescape(detail[0]))+"</font><br>";
}
}else if(comp[0]=="MSG"){
txt_ecran.htmlText+="<FONT COLOR='#"+comp[2]+"'>"+parseHtml(unescape(comp[1]))+"</FONT><br>";
txt_ecran.scroll=txt_ecran.maxscroll;
}
}
function parseHtml(msg){
var tab=msg.split("&");
msg=tab.join("&");
var tab=msg.split("<");
msg=tab.join("<");
return msg;
}
function clickRoom(nom){
txt_salon.text=unescape(nom)
socket.send("SETROOM="+nom);
}
Celui de la seconde :
bt_send.onRelease=function(){
if(txt_message.text.length){
socket.send("MSG="+escape(txt_message.htmlText));
txt_ecran.htmlText+="<font color='#"+selfColor+"'>"+txt_message.htmlText+"</font>"
txt_ecran.scroll=txt_ecran.maxscroll;
txt_message.htmlText=""
}
}
txt_salon.onChanged=function(){
socket.send("SETROOM="+escape(this.text));
}
Vous pouvez télécharger le fla (fla_etape_2.fla) ou directement observer le résultat de ce tutorial
en ligne.