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.
Notre connexion est en place, nous allons donc écouter son comportement :
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 :
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 :
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).
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("
\n
Requesting 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.
...
;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 :
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.
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 :
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
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 :
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:
400
px;
padding:
10
px;
background:
#ccc
;
margin:
20
px auto
;
}
.sii-chat-content
{
min-height:
400
px;
max-height:
400
px;
overflow:
hidden
;
overflow-y:
scroll
;
background:
#fff
;
box-shadow:
0
px 0
px 5
px 0
px #000
;
margin-bottom:
10
px;
}
.console
{
min-height:
50
px;
max-height:
100
px;
overflow:
hidden
;
overflow-y:
scroll
;
}
.sii-chat-form
input[
name=
"sii-chat-message"
]{
width:
300
px;
box-shadow:
inset
0
px 0
px 5
px 0
px #000
;
}
.sii-chat-form
button{
width:
80
px;
float:
right
;
color:
#ffffff
;
-moz-box-shadow:
0
px 0
px 5
px #343434
;
-webkit-box-shadow:
0
px 0
px 5
px #343434
;
-o-box-shadow:
0
px 0
px 5
px #343434
;
box-shadow:
0
px 0
px 5
px #343434
;
-moz-border-radius:
5
px;
-webkit-border-radius:
5
px;
border-radius:
5
px;
border:
1
px 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.