Un chat en HTML5 avec les websockets

jerome debray

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

Commentez

- Titre : Un chat en HTML5 avec les websockets

- Auteur : jerome debray

- Parution : 25 octobre 2011

- Mise à jour : 3 novembre 2011

- 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 ®  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.

Sommaire

- 1. Compatibilité

- 2. l'API websocket HTML5

- 3. le serveur websocket

- 4. Le chat

- 5. Package

- 6. Remerciements

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 :

about:config

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

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 :

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.

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

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

1. socket.onopen = function(e){} /*on "écoute" pour savoir si la connexion vers le serveur websocket s'est bien faite */

2. socket.onmessage = function(e){} /*on récupère les messages provenant du serveur websocket */

3. socket.onclose = function(e){} /*on est informé lors de la fermeture de la connexion vers le serveur*/

4. 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 :

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 :

1. socket.close();

3 - le serveur websocket

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

- JAVA : jWebSocket ;

- Ruby : web-socket-ruby ;

- Node JS : Socket.IO-node ;

- PHP : phpwebsocket.

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

- kaazing kaazing ;

- wakanda wakanda server et studio .

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).

1. #!/php -q

2. <?php  /*  >php -q server.php  */

3. ini_set('default_socket_timeout', 10);

4. include_once('WebSocketHandshake.class.php');

5. error_reporting(E_ALL);

6. set_time_limit(0);

7. ob_implicit_flush();

8. $master  = WebSocket('localhost',11345); /* le host et le port du serveur websocket */

9. $sockets = array($master);

10. $users   = array();

11. $debug   = true;

13. /*

14. * serveur websocket qui tourne en continu

15. */

16. while(true){

17. $changed = $sockets;

18. $expect = $sockets;

19. socket_select($changed,$write=NULL,$expect,0,1000000);

20. foreach($changed as $socket){

21. if($socket==$master){

22. $client=socket_accept($master);

23. if($client<0){

24. console("socket_accept() failed"); continue;

25. }else{

26. socket_set_option($client,SOL_SOCKET, SO_KEEPALIVE, 1) or die('Can not set keepalive');

27. connect($client);

28. }

29. }else{

30. $bytes = @socket_recv($socket,$buffer,2048,0);

31. if($bytes==0){ disconnect($socket); }

32. else{

33. $user = getuserbysocket($socket);

34. if(!$user->handshake){ dohandshake($user,$buffer); }

35. else{ process($user,$buffer); }

36. }

37. }

38. }

39. }

41. /*

42. * fonction pour savoir quel type d'action est demandé - dans notre cas,

43. * il n'y en a qu'un mais s"il y avait plusieurs actions sur ce serveur

44. * en plus du chat, nous aurions d'autres cas dans le switch

45. */

46. function process($from,$msg){

47. console("< ".$from->label." (".$from->userId.") : \n\t".$msg);

48. $msg = unwrap($msg);

49. $msg = json_decode($msg,true);

50. switch($msg['action']){

51. case "ctrl/chat/out"    : onCtrlChatOut($from,  $msg['msg']);

52. break;

53. default : onActionError($from, $msg);

54. break;

55. }

56. }

57. /*

58. * réécrit les données sur le socket ouvert

59. */

60. function send($to,$msg){

61. say('> '.$to->label.' ('.$to->userId.") :\n\t".$msg);

62. $msg = wrap($msg);

63. return socket_write($to->socket,$msg,strlen($msg));

64. }

65. /*

66. * envoi du message (des données) à chaque utilisateur connecté

67. * sur le serveur de socket (et sur le même socket)

68. */

69. function broadcast($msg, $excludeUser=''){

70. say(">> ");

71. global $users;

72. foreach($users as $user){

73. send($user, $msg);

74. }

75. }

76. /*

77. *initialisation du socket

78. */

79. function WebSocket($address,$port){

80. $master=socket_create(AF_INET, SOCK_STREAM, SOL_TCP)     or die("socket_create() failed");

81. socket_set_option($master, SOL_SOCKET, SO_REUSEADDR, 1)  or die("socket_option() failed");

82. socket_bind($master, $address, $port)                    or die("socket_bind() failed");

83. socket_listen($master,20)                                or die("socket_listen() failed");

84. echo "Server Started : ".date('Y-m-d H:i:s')."\n";

85. echo "Master socket  : ".$master."\n";

86. echo "Listening on   : ".$address." port ".$port."\n\n";

87. return $master;

88. }

89. /*

90. * lors de la connexion d'un utilisateur sur le serveur,

91. * il est enregistré pour le broadcast des données

92. */

93. function connect($socket){

94. global $sockets,$users;

95. $user = new User();

96. $user->id = uniqid();

97. $user->socket = $socket;

98. array_push($users,$user);

99. array_push($sockets,$socket);

100. console($socket." CONNECTED!");

101. }

102. /*

103. * déconnexion de tous les utilisateurs

104. */

105. function disconnect($socket){

106. global $sockets,$users;

107. $found=null;

108. $n=count($users);

109. for($i=0;$i<$n;$i++){

110. if($users[$i]->socket==$socket){ $found=$i; break; }

111. }

112. if(!is_null($found)){ array_splice($users,$found,1); }

113. $index = array_search($socket,$sockets);

114. socket_close($socket);

115. console($socket." DISCONNECTED!");

116. if($index>=0){ array_splice($sockets,$index,1); }

117. }

118. /*

119. * fonction de la persistance de la connexion websocket

120. */

121. function dohandshake($user,$buffer){

122. console("\nRequesting handshake...");

123. console($buffer);

124. list($resource,$host,$origin) = getheaders($buffer);

125. console("Handshaking...");

126. $handshake = WebSocketHandshake($buffer);

127. socket_write($user->socket,$handshake,strlen($handshake));

128. $user->handshake=true;

129. console($handshake);

130. console("Done handshaking...");

131. return true;

132. }

133. function getheaders($req){

134. $r=$h=$o=null;

135. if(preg_match("/GET (.*) HTTP/"   ,$req,$match)){ $r=$match[1]; }

136. if(preg_match("/Host: (.*)\r\n/"  ,$req,$match)){ $h=$match[1]; }

137. if(preg_match("/Origin: (.*)\r\n/",$req,$match)){ $o=$match[1]; }

138. return array($r,$h,$o);

139. }

140. /*

141. * identification des utilisateurs dans le socket

142. */

143. function getuserbysocket($socket){

144. global $users;

145. $found=null;

146. foreach($users as $user){

147. if($user->socket==$socket){ $found=$user; break; }

148. }

149. return $found;

150. }

151. /**

152. *      fonction permet de formater le message de retour

153. * à envoyer vers les navigateurs (les utilisateurs)

154. */

155. function onCtrlChatOut($from, $msg){

156. $msg = json_decode($msg);

157. if($msg->message==='demo.stop'){

158. $msg->message='WebSockets server is going down...';

159. broadcast('{"action": "ws/chat/in", "msg":'.json_encode($msg).'}');

160. die();

161. }

162. broadcast('{"action": "ws/chat/in", "msg":'.json_encode($msg).'}');

163. }

164. /**

165. *              Usefull functions

166. */

167. function     say($msg=""){ echo $msg."\n"; }

168. function    wrap($msg=""){ return chr(0).$msg.chr(255); }

169. function  unwrap($msg=""){ return substr($msg,1,strlen($msg)-2); }

170. function console($msg=""){ global $debug; if($debug){ echo $msg."\n"; } }

172. class User{

173. var $id;

174. var $socket;

175. var $handshake;

176. var $userId=null;   // From GUI

177. }

178. ?>

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.

...
;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 :

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 :

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 :

1. <!DOCTYPE html>

2. <html>

3. <head>

4. <meta charset="ISO-8859-1">

5. <link href="style/style.css" type="text/css" rel="stylesheet"/>

6. <script type="text/javascript" src="script/script.js"></script>

7. <title>Insert title here</title>

8. </head>

9. <body>

10. <div class="sii-chat"> <-- conteneur du chat -->

11. <div>Pseudo : <input type="text" name="sii-chat-name" /><button class="sii-chat-login">Valider</button></div> <-- pseudo à saisir pour le chat -->

12. <div class="sii-chat-content"> <-- les messages apparaitront ici -->

13. </div>

14. <div>

15. <form class="sii-chat-form" onsubmit="return false;">

16. <input type="text" value="" name="sii-chat-message" disabled="disabled"/><-- saisie du message à saisir -->

17. <button class="sii-chat-send" disabled="disabled">ok</button> <-- Bouton d'envoi du message saisi -->

18. </form>

19. </div>

20. <div class="console"></div>

21. </div>

22. <script type="text/javascript" src="script/websocket.js"></script>

23. </body>

24. </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.

1. var WebsocketClass = function(host){

2. this.socket = new WebSocket(host);

3. this.console = document.getElementsByClassName('console')[0];

4. };

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 :

1. var uId = ''; /* pseudo de l'utilisateur*/

2. var button = document.getElementsByClassName('sii-chat-send')[0]; /* bouton d'envoi du message */

3. var messageInput = document.getElementsByName('sii-chat-message')[0]; /* message à envoyer vers le serveur */

4. var buttonUser = document.getElementsByClassName('sii-chat-login')[0]; /* bouton de soumission du pseudo */

5. var contentMessage = document.getElementsByClassName('sii-chat-content')[0]; /* div contenant les messages reçus par le serveur*/

6. var WebsocketClass = function(host){

7. this.socket = new WebSocket(host);

8. this.console = document.getElementsByClassName('console')[0];

9. };

10. WebsocketClass.prototype = {

11. initWebsocket : function(){

12. var $this = this;

13. this.socket.onopen = function(){

14. $this.onOpenEvent(this);

15. };

16. this.socket.onmessage = function(e){

17. $this._onMessageEvent(e);

18. };

19. this.socket.onclose = function(){

20. $this._onCloseEvent();

21. };

22. this.socket.onerror = function(error){

23. $this._onErrorEvent(error);

24. };

25. this.console.innerHTML = this.console.innerHTML + 'websocket init <br />';

26. },

27. _onErrorEvent :function(err){

28. console.log(err);

29. this.console.innerHTML = this.console.innerHTML + 'websocket error <br />';

30. },

31. onOpenEvent : function(socket){

32. console.log('socket opened');

33. this.console.innerHTML = this.console.innerHTML + 'socket opened Welcome - status ' + socket.readyState + '<br />';

34. },

35. _onMessageEvent : function(e){

36. e = JSON.parse(e.data);

37. if(e.msg.length > 0) e.msg = JSON.parse(e.msg);

38. contentMessage.innerHTML = contentMessage.innerHTML

39. + '><strong>' + e.msg.from + '</strong> : ' + e.msg.message + '<br />';

40. contentMessage.scrollTop = contentMessage.scrollHeight; /* permet de scroller automatiquement vers le bas dans la div contenant la réception des messages */

41. this.console.innerHTML = this.console.innerHTML + 'message event lanched <br />';

42. },

43. _onCloseEvent : function(){

44. console.log('connection closed');

45. this.console.innerHTML = this.console.innerHTML + 'websocket closed - server not running<br />';

46. uId = '';

47. document.getElementsByName('sii-chat-name')[0].value = '';

48. messageInput.disabled = 'disabled';

49. button.disabled = 'disabled';

50. },

51. sendMessage : function(){

52. var message = '{"from":"' + uId + '", "message":"' + messageInput.value + '"}';

53. this.socket.send('{"action":"ctrl/chat/out", "msg":' + JSON.stringify(message) + '}');

54. messageInput.value = '';

55. this.console.innerHTML = this.console.innerHTML + 'websocket message send <br />';

56. }

57. };

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.

1. var socket = new WebsocketClass('ws://localhost:11345/serveur.php'); /* on instancie un objet WebsocketClass avec l'URL en paramètre */

2. if(button.addEventListener){

3. buttonUser.addEventListener('click', function(e){ /* on écoute l'évènement 'click' sur le bouton permettant de valider son pseudo */

4. e.preventDefault(); /* on stoppe la propagation */

5. socket.initWebsocket(); /* initialisation de la connexion vers le serveur de socket */

6. uId = document.getElementsByName('sii-chat-name')[0].value; /* récupération de la valeur du pseudo de l'utilisateur */

7. messageInput.disabled = ''; /* on permet l'accès au chat (aux champs permettant d'envoyer des messages) */

8. button.disabled = '';

9. return false; /* on évite le rechargement de page */

10. }, true);

11. button.addEventListener('click',function(e){ /* on écoute l'évènement 'click' sur le bouton permettant d'envoyer le message */

12. e.preventDefault();

13. socket.sendMessage(); /* on envoie un message vers le serveur*/

14. return false;

15. }, true);

16. } else{

17. console.log('votre navigateur n\'accepte pas le addevenlistener');

18. }

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

1. .sii-chat{

2. width:400px;

3. padding:10px;

4. background:#ccc;

5. margin:20px auto;

6. }

7. .sii-chat-content{

8. min-height:400px;

9. max-height:400px;

10. overflow:hidden;

11. overflow-y:scroll;

12. background:#fff;

13. box-shadow:0px 0px 5px 0px #000;

14. margin-bottom:10px;

15. }

16. .console{

17. min-height:50px;

18. max-height:100px;

19. overflow:hidden;

20. overflow-y:scroll;

21. }

22. .sii-chat-form input[name="sii-chat-message"]{

23. width:300px;

24. box-shadow:inset 0px 0px 5px 0px #000;

25. }

26. .sii-chat-form button{

27. width:80px;

28. float:right;

29. color:#ffffff;

30. -moz-box-shadow: 0px 0px 5px #343434;

31. -webkit-box-shadow: 0px 0px 5px #343434;

32. -o-box-shadow: 0px 0px 5px #343434;

33. box-shadow: 0px 0px 5px #343434;

34. -moz-border-radius: 5px;

35. -webkit-border-radius: 5px;

36. border-radius: 5px;

37. border: 1px solid #656565;

38. filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="34cdf9", endColorstr="3166ff"); /* Pour IE seulement et mode gradient à linear */

39. background: -webkit-gradient(linear, left top, left bottom, from(#34cdf9), to(#3166ff));

40. background: -moz-linear-gradient(top center, #34cdf9, #3166ff);

41. }

42. .sii-chat-form button:active{

43. filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="3166ff", endColorstr="34cdf9"); /* Pour IE seulement et mode gradient à linear */

44. background: -webkit-gradient(linear, left top, left bottom, from(#3166ff), to(#34cdf9));

45. background: -moz-linear-gradient(top center, #3166ff, #34cdf9);

46. }

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

Télécharger le 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.