Un chat en HTML5 avec les websockets

Voici un tutoriel permettant de créer un chat grâce à l'API websocket en HTML5.

30 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

1. Compatibilité

Tous les navigateurs modernes proposent un support du websocket de manière native ou via un plugin. 

Voici la liste des navigateurs :

  • Chrome : support natif ;
  • Safari : support natif ;
  • Firefox : support natif ;
  • Opéra : support natif mais nécessite de l'activer ;
  • Internet Explorer : utilisation d'un prototype des websockets :  websockets prototype pour IE 9.

Pour Firefox

Pour activer les websockets dans Firefox s'ils ne le sont pas (comme dans la version 4 par exemple), il suffit de se rendre dans la barre d'adresse et taper la commande suivante :

 
Sélectionnez

	about:config
    		

Une page de confirmation apparaît, continuez. Dans le champ filtre, tapez la recherche suivante :

 
Sélectionnez

	network.websocket
    		

Deux options apparaîtront, il faut alors changer leurs valeurs :

  • network.websocket.enabled : true
  • network.websocket.override-security-block : true

Pour Opera

Dans la barre d'adresse du navigateur, tapez la commande suivante :

 
Sélectionnez

	opera:config#Enable WebSockets
	

Cliquez sur la checkbox afin d'activer les websockets, puis cliquez sur "Save".

Dans les deux cas, il faudra redémarrer le navigateur.

2. l'API websocket HTML5

D'aucuns diront que le websocket a une faille de sécurité et qu'il ne faut pas l'utiliser en l'état actuel des choses. C'est vrai, mais il faut se projeter. Les websockets se positionnent comme les remplaçants de l'AJAX. Ils sont rapides, il n'y a que les données qui transitent (très peu de données dans le header de la réponse, voire pas du tout) et le fonctionnement est robuste en termes de charge.

Grâce à l'arrivée du HTML5, nous pouvons donc commencer à utiliser les websockets via l'API websockets.

Celle-ci nous fournit des fonctions nous permettant de mettre très facilement en place une communication via les websockets.

Comment utiliser l'API websockets ?

En JavaScript, il faut d'abord instancier un objet Websocket qui prend pour paramètre une URL vers un serveur websocket.

 
Sélectionnez
1.
var socket = new Websocket("ws://localhost:11345/serveur.php");

Notre connexion est en place, nous allons donc écouter son comportement :

 
Sélectionnez
1.
2.
3.
4.
socket.onopen = function(e){} /*on "écoute" pour savoir si la connexion vers le serveur websocket s'est bien faite */
socket.onmessage = function(e){} /*on récupère les messages provenant du serveur websocket */
socket.onclose = function(e){} /*on est informé lors de la fermeture de la connexion vers le serveur*/
socket.onerror = function(e){} /*on traite les cas d'erreur*/

Pour envoyer un message (des données) vers le serveur websocket, il faut utiliser l'instruction suivante :

 
Sélectionnez
1.
socket.send('mon message') /*'mon_message' peut être du JSON mais il conviendra de le stringifier via JSON.stringify('{"msg": "message type string"}')*/

Enfin, l'API propose de pouvoir clôturer une connexion vers le serveur websocket via l'instruction suivante :

 
Sélectionnez
1.
socket.close();

3. le serveur websocket

Il existe sur le Web de nombreuses solutions pour chaque type de langage :

Il existe également d'autres solutions émergentes telles que :

Dans notre cas, nous utiliserons phpwebsocket. Pour l'utiliser, téléchargez-le et dézippez-le dans le dossier de votre choix.

Ensuite nous allons modifier légèrement le fichier server.php (que j'ai renommé serveur.php  dans le package à télécharger).

Si toutefois vous rencontrez des problèmes lors de l'installation des fichiers de phpwebsocket chez vous, n'hésitez pas à télécharger le package du cours où vous trouverez un serveur websocket tout fait (celui utilisé dans cet article).

 
Sélectionnez
1.
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.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
#!/php -q
<?php  /*  >php -q server.php  */
ini_set('default_socket_timeout', 10);
include_once('WebSocketHandshake.class.php');
error_reporting(E_ALL);
set_time_limit(0);
ob_implicit_flush();
$master  = WebSocket('localhost',11345); /* le host et le port du serveur websocket */
$sockets = array($master);
$users   = array();
$debug   = true;
 
/*
 * serveur websocket qui tourne en continu
 */
while(true){
       $changed = $sockets;
       $expect = $sockets;
       socket_select($changed,$write=NULL,$expect,0,1000000);
       foreach($changed as $socket){
               if($socket==$master){
                       $client=socket_accept($master);
                       if($client<0){ 
                             console("socket_accept() failed"); continue; 
                       }else{
                               socket_set_option($client,SOL_SOCKET, SO_KEEPALIVE, 1) or die('Can not set keepalive');
                               connect($client);
                       }
               }else{
                       $bytes = @socket_recv($socket,$buffer,2048,0);
                       if($bytes==0){ disconnect($socket); }
                       else{
                               $user = getuserbysocket($socket);
                               if(!$user->handshake){ dohandshake($user,$buffer); }
                               else{ process($user,$buffer); }
                       }
               }
       }
}
 
/*
 * fonction pour savoir quel type d'action est demandé - dans notre cas, 
 * il n'y en a qu'un mais s"il y avait plusieurs actions sur ce serveur 
 * en plus du chat, nous aurions d'autres cas dans le switch
 */
function process($from,$msg){
       console("< ".$from->label." (".$from->userId.") : \n\t".$msg);
       $msg = unwrap($msg);
       $msg = json_decode($msg,true);
       switch($msg['action']){
               case "ctrl/chat/out"    : onCtrlChatOut($from,  $msg['msg']);  
               break;
               default : onActionError($from, $msg);
               break;
       }
}
/*
 * réécrit les données sur le socket ouvert
 */
function send($to,$msg){
       say('> '.$to->label.' ('.$to->userId.") :\n\t".$msg);
       $msg = wrap($msg);
       return socket_write($to->socket,$msg,strlen($msg));
}
/*
 * envoi du message (des données) à chaque utilisateur connecté 
 * sur le serveur de socket (et sur le même socket)
 */
function broadcast($msg, $excludeUser=''){
       say(">> ");
       global $users;
       foreach($users as $user){
               send($user, $msg);
       }
}
/*
 *initialisation du socket
 */
function WebSocket($address,$port){
       $master=socket_create(AF_INET, SOCK_STREAM, SOL_TCP)     or die("socket_create() failed");
       socket_set_option($master, SOL_SOCKET, SO_REUSEADDR, 1)  or die("socket_option() failed");
       socket_bind($master, $address, $port)                    or die("socket_bind() failed");
       socket_listen($master,20)                                or die("socket_listen() failed");
       echo "Server Started : ".date('Y-m-d H:i:s')."\n";
       echo "Master socket  : ".$master."\n";
       echo "Listening on   : ".$address." port ".$port."\n\n";
       return $master;
}
/*
 * lors de la connexion d'un utilisateur sur le serveur, 
 * il est enregistré pour le broadcast des données
 */
function connect($socket){
       global $sockets,$users;
       $user = new User();
       $user->id = uniqid();
       $user->socket = $socket;
       array_push($users,$user);
       array_push($sockets,$socket);
       console($socket." CONNECTED!");
}
/*
 * déconnexion de tous les utilisateurs
 */
function disconnect($socket){
       global $sockets,$users;
       $found=null;
       $n=count($users);
       for($i=0;$i<$n;$i++){
               if($users[$i]->socket==$socket){ $found=$i; break; }
       }
       if(!is_null($found)){ array_splice($users,$found,1); }
       $index = array_search($socket,$sockets);
       socket_close($socket);
       console($socket." DISCONNECTED!");
       if($index>=0){ array_splice($sockets,$index,1); }
}
/*
 * fonction de la persistance de la connexion websocket
 */
function dohandshake($user,$buffer){
       console("\nRequesting handshake...");
       console($buffer);
       list($resource,$host,$origin) = getheaders($buffer);
       console("Handshaking...");
       $handshake = WebSocketHandshake($buffer);
       socket_write($user->socket,$handshake,strlen($handshake));
       $user->handshake=true;
       console($handshake);
       console("Done handshaking...");
       return true;
}
function getheaders($req){
       $r=$h=$o=null;
       if(preg_match("/GET (.*) HTTP/"   ,$req,$match)){ $r=$match[1]; }
       if(preg_match("/Host: (.*)\r\n/"  ,$req,$match)){ $h=$match[1]; }
       if(preg_match("/Origin: (.*)\r\n/",$req,$match)){ $o=$match[1]; }
       return array($r,$h,$o);
}
/*
 * identification des utilisateurs dans le socket
 */
function getuserbysocket($socket){
       global $users;
       $found=null;
       foreach($users as $user){
               if($user->socket==$socket){ $found=$user; break; }
       }
       return $found;
}
/**
 *      fonction permet de formater le message de retour 
 * à envoyer vers les navigateurs (les utilisateurs)
 */
function onCtrlChatOut($from, $msg){
       $msg = json_decode($msg);
       if($msg->message==='demo.stop'){
               $msg->message='WebSockets server is going down...';
               broadcast('{"action": "ws/chat/in", "msg":'.json_encode($msg).'}');
               die();
       }
       broadcast('{"action": "ws/chat/in", "msg":'.json_encode($msg).'}');
}
/**
 *              Usefull functions
 */
function     say($msg=""){ echo $msg."\n"; }
function    wrap($msg=""){ return chr(0).$msg.chr(255); }
function  unwrap($msg=""){ return substr($msg,1,strlen($msg)-2); }
function console($msg=""){ global $debug; if($debug){ echo $msg."\n"; } }
 
class User{
       var $id;
       var $socket;
       var $handshake;
       var $userId=null;   // From GUI
}
?>

Le code peut paraître un peu obscur si l'on n'est pas familiarisé avec les sockets en PHP. Ce qu'il faut comprendre c'est que ce script permet d'instancier un serveur de socket et qu'il gère la connexion des utilisateurs ainsi que l'envoi des messages via les sockets.

Bon, nous avons notre serveur websocket mais comment le lancer ?

Il faudra s'assurer que l'extension php_sockets est bien chargée dans PHP. Pour ce faire, rendez-vous dans le php.ini du PHP que vous utilisez et décommentez la ligne nécessaire.

 
Sélectionnez
...
;extension=php_soap.dll
extension=php_sockets.dll
;extension=php_sqlite.dll
;extension=php_sqlite3.dll
...

Dans notre cas, nous n'avons pas besoin de serveur Web. En effet, nous allons utiliser le serveur websocket. J'entends par là que nous pouvons utiliser notre page de chat en local (sans serveur Web, donc pas de http) et c'est notre serveur websocket qui va s'occuper de faire transiter les données entre les différents clients (navigateurs) connectés au serveur websocket.

Pour cela, il suffit donc de lancer notre serveur websocket en accédant à une console de commande (cmd sous Windows par exemple) et accéder au répertoire où se situe notre serveur websocket.

Une fois arrivé au dossier nécessaire, on lance la commande suivante :

 
Sélectionnez
c:\websocket\serveur\>php -q serveur.php

Si le message suivant apparaît, vous avez un serveur de websocket qui fonctionne et qui n'attend plus que les données :

 
Sélectionnez
Server Started : 2011-06-30 13:59:03
Master socket  : Resource id #5
Listening on   : localhost port 11345

La console ainsi ouverte vous permettra de voir le comportement du serveur sur la connexion d'utilisateurs et de traitement de message (de données).

4. Le chat

Jusqu'ici, nous avons notre serveur qui fonctionne, nous allons maintenant créer l'interface permettant d'envoyer et de recevoir des messages depuis notre chat.

Commençons pour l'IHM que nous allons réaliser en HTML. Voici le code :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
<!DOCTYPE html>
       <html>
       <head>
       <meta charset="ISO-8859-1">
       <link href="style/style.css" type="text/css" rel="stylesheet"/>
       <script type="text/javascript" src="script/script.js"></script>
       <title>Insert title here</title>
       </head>
       <body>
               <div class="sii-chat"> <-- conteneur du chat -->
                       <div>Pseudo : <input type="text" name="sii-chat-name" /><button class="sii-chat-login">Valider</button></div> <-- pseudo à saisir pour le chat -->
                       <div class="sii-chat-content"> <-- les messages apparaitront ici -->
                       </div>
                       <div>
                               <form class="sii-chat-form" onsubmit="return false;">
                                       <input type="text" value="" name="sii-chat-message" disabled="disabled"/><-- saisie du message à saisir -->
                                       <button class="sii-chat-send" disabled="disabled">ok</button> <-- Bouton d'envoi du message saisi -->
                               </form>
                       </div>
                       <div class="console"></div>
               </div>
       <script type="text/javascript" src="script/websocket.js"></script>
       </body>
       </html>

La zone de saisie d'un message et le bouton d'envoi d'un message sont volontairement en "disabled", pour obliger l'utilisateur à renseigner un pseudo qui permettra d'identifier chaque intervenant sur le chat.

Notre IHM est en place, intéressons-nous à la partie JavaScript. Dans un premier temps nous allons mettre en place le websocket via un objet JavaScript. Je crée une classe WebsocketClass qui va initialiser la communication avec le serveur et gérer tous les évènements liés au websocket.

La variable host permet de donner l'URL du serveur de socket.

 
Sélectionnez
1.
2.
3.
4.
var WebsocketClass = function(host){
   this.socket = new WebSocket(host);
   this.console = document.getElementsByClassName('console')[0];
};

Dans cette classe, je crée une variable this.socket qui récupère l'instanciation de WebSocket()  ainsi qu'une variable this.console pour pouvoir retranscrire les actions du websocket dans ma console affichée sur ma page Web.

Une fois la classe créée, je vais l'étendre grâce au prototypage. Il y aura donc une initialisation du websocket, la gestion des évènements soulevés et une fonction permettant d'envoyer les messages.

Nous allons également placer les variables nécessaires au bon fonctionnement du chat.

Voici le code :

 
Sélectionnez
1.
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.
        var uId = ''; /* pseudo de l'utilisateur*/
       var button = document.getElementsByClassName('sii-chat-send')[0]; /* bouton d'envoi du message */ 
       var messageInput = document.getElementsByName('sii-chat-message')[0]; /* message à envoyer vers le serveur */
       var buttonUser = document.getElementsByClassName('sii-chat-login')[0]; /* bouton de soumission du pseudo */
       var contentMessage = document.getElementsByClassName('sii-chat-content')[0]; /* div contenant les messages reçus par le serveur*/
        var WebsocketClass = function(host){
               this.socket = new WebSocket(host);
               this.console = document.getElementsByClassName('console')[0];
       };
       WebsocketClass.prototype = {
               initWebsocket : function(){
                       var $this = this;
                       this.socket.onopen = function(){
                               $this.onOpenEvent(this);
                       };
                       this.socket.onmessage = function(e){
                               $this._onMessageEvent(e);
                       };
                       this.socket.onclose = function(){
                               $this._onCloseEvent();
                       };
                       this.socket.onerror = function(error){
                               $this._onErrorEvent(error);
                       };
                       this.console.innerHTML = this.console.innerHTML + 'websocket init <br />';
               },
               _onErrorEvent :function(err){
                       console.log(err);
                       this.console.innerHTML = this.console.innerHTML + 'websocket error <br />';
               },
               onOpenEvent : function(socket){
                       console.log('socket opened');
                       this.console.innerHTML = this.console.innerHTML + 'socket opened Welcome - status ' + socket.readyState + '<br />';
               },
               _onMessageEvent : function(e){
                       e = JSON.parse(e.data);
                      if(e.msg.length > 0) e.msg = JSON.parse(e.msg);
                      contentMessage.innerHTML = contentMessage.innerHTML 
                               + '><strong>' + e.msg.from + '</strong> : ' + e.msg.message + '<br />';
                      contentMessage.scrollTop = contentMessage.scrollHeight; /* permet de scroller automatiquement vers le bas dans la div contenant la réception des messages */
                      this.console.innerHTML = this.console.innerHTML + 'message event lanched <br />';
               },
               _onCloseEvent : function(){
                       console.log('connection closed');
                       this.console.innerHTML = this.console.innerHTML + 'websocket closed - server not running<br />';
                       uId = '';
                       document.getElementsByName('sii-chat-name')[0].value = '';
                       messageInput.disabled = 'disabled';
                       button.disabled = 'disabled';
               },
               sendMessage : function(){
                       var message = '{"from":"' + uId + '", "message":"' + messageInput.value + '"}';
                       this.socket.send('{"action":"ctrl/chat/out", "msg":' + JSON.stringify(message) + '}');
                       messageInput.value = '';
                       this.console.innerHTML = this.console.innerHTML + 'websocket message send <br />';
               }
       };

Notre classe est créée, il ne nous suffit plus que de l'utiliser. Nous en profiterons pour également rajouter des écouteurs d'évènements sur les boutons afin de mettre en place le mécanisme suivant :

l'utilisateur s'identifie et ensuite il aura accès au chat.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.

		var socket = new WebsocketClass('ws://localhost:11345/serveur.php'); /* on instancie un objet WebsocketClass avec l'URL en paramètre */
       if(button.addEventListener){
               buttonUser.addEventListener('click', function(e){ /* on écoute l'évènement 'click' sur le bouton permettant de valider son pseudo */
                       e.preventDefault(); /* on stoppe la propagation */
                       socket.initWebsocket(); /* initialisation de la connexion vers le serveur de socket */
                       uId = document.getElementsByName('sii-chat-name')[0].value; /* récupération de la valeur du pseudo de l'utilisateur */
                       messageInput.disabled = ''; /* on permet l'accès au chat (aux champs permettant d'envoyer des messages) */
                       button.disabled = '';
                       return false; /* on évite le rechargement de page */
               }, true);
               button.addEventListener('click',function(e){ /* on écoute l'évènement 'click' sur le bouton permettant d'envoyer le message */
                       e.preventDefault();
                       socket.sendMessage(); /* on envoie un message vers le serveur*/
                       return false;
               }, true);
       } else{
               console.log('votre navigateur n\'accepte pas le addevenlistener');
       }

Ajoutons à l'IHM un brin de CSS pour améliorer le visuel de celle-ci :

 
Sélectionnez
1.
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.
        .sii-chat{
               width:400px;
               padding:10px;
               background:#ccc;
               margin:20px auto;
       }
       .sii-chat-content{
               min-height:400px;
               max-height:400px;
               overflow:hidden;
               overflow-y:scroll;
               background:#fff;
               box-shadow:0px 0px 5px 0px #000;
               margin-bottom:10px;     
       }
       .console{       
               min-height:50px;
               max-height:100px;
               overflow:hidden;
               overflow-y:scroll;
       }
       .sii-chat-form input[name="sii-chat-message"]{
               width:300px;
               box-shadow:inset 0px 0px 5px 0px #000;
       }
       .sii-chat-form button{
               width:80px;
               float:right;
               color:#ffffff;
               -moz-box-shadow: 0px 0px 5px #343434;
               -webkit-box-shadow: 0px 0px 5px #343434;
               -o-box-shadow: 0px 0px 5px #343434;
               box-shadow: 0px 0px 5px #343434;
               -moz-border-radius: 5px;
               -webkit-border-radius: 5px;
               border-radius: 5px;
               border: 1px solid #656565;
               filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="34cdf9", endColorstr="3166ff"); /* Pour IE seulement et mode gradient à linear */
               background: -webkit-gradient(linear, left top, left bottom, from(#34cdf9), to(#3166ff));
               background: -moz-linear-gradient(top center, #34cdf9, #3166ff);
       }
       .sii-chat-form button:active{
               filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="3166ff", endColorstr="34cdf9"); /* Pour IE seulement et mode gradient à linear */
               background: -webkit-gradient(linear, left top, left bottom, from(#3166ff), to(#34cdf9));
               background: -moz-linear-gradient(top center, #3166ff, #34cdf9);
       }

Et voilà, le chat est terminé et fonctionnel. Pour bien tester la démonstration, ouvrez deux navigateurs avec la page de chat et conversez entre les deux. Vous verrez que l'un et l'autre se mettent à jour en même temps et rapidement.

5. Package

6. Remerciements

Merci à Djug et  ovh pour leurs précisions.
Merci à Bovino pour sa relecture et ses conseils.
Merci à ClaudeLELOUP pour sa relecture orthographique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

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 © ornitho13. 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.