Aujourd’hui, nous allons poursuivre notre dossier consacré à HTML5 par une étude du standard qui a eu le plus de succès : les WebSockets. Cette spécification permet d’ouvrir une connexion bi-directionnelle permanente entre un client et un serveur, afin de résoudre certains problèmes posés par le caractère unidirectionnel et déconnecté du protocole HTTP.
Les WebSockets autorisent ainsi le développement de véritables applications temps-réel performantes telles que des sites d’informations ou de suivi des cours boursiers, ou des applications multi-utilisateurs (chat, jeux en ligne…).
La spécification permettant d’utiliser les WebSockets est développée par le W3C, tandis que le protocole de communication est standardisé par l’IETF.
Le protocole WebSocket
Pour établir une connexion WebSocket, une requête de type « upgrade » doit être envoyée par le client, afin de demander la mise à jour de la connexion TCP/HTTP actuelle vers le mode WebSocket. Cette requête peut se présenter de la manière suivante:
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 6
Et le serveur renvoie une réponse à la requête:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat
Cet échange initial est nommé « handshake ». A partir de là, la communication entre serveur et client se fait par le protocole WebSocket. Les détails du protocole se trouvent sur le site de l’IETF. De nouvelles versions sont publiées régulièrement, le protocol que j’ai présenté étant la dernière version en date (25/02/11). Je précise que ces publications sont en état de « brouillon » et changent constamment.
L’API WebSocket
L’API, développée par le W3C, introduit l’interface WebSocket suivante :
[Constructor(in DOMString url, in optional DOMString protocols)]
[Constructor(in DOMString url, in optional DOMString[] protocols)]
interface WebSocket {
readonly attribute DOMString url;
const unsigned short CONNECTING = 0;
const unsigned short OPEN = 1;
const unsigned short CLOSING = 2;
const unsigned short CLOSED = 3;
readonly attribute unsigned short readyState;
readonly attribute unsigned long bufferedAmount;
attribute Function onopen;
attribute Function onmessage;
attribute Function onerror;
attribute Function onclose;
readonly attribute DOMString protocol;
void send(in DOMString data);
void close();
};
WebSocket implements EventTarget;
Ainsi pour créer une instance de WebSocket, le seul argument qu’on doit fournir est l’URL du serveur (compatible Websocket) avec lequel on souhaite établir une liaison. Elle commence obligatoirement par ws:// (ou wss:// pour une connexion sécurisée). Ensuite, l’interface comporte les attributs fonctionnels permettant de gérer les évènements associés:
onopen : ouverture d'une WebSocket onmessage : réception d'un message onerror : erreur(s) survenue(s) onclose : fermeture de WebSocket
Support
Implémentation côté serveur
Il existe à ce jour plusieurs implémentations de WebSocket côté serveur:
- Kaazing WebSocket Gateway
- Socket.IO-Node (NodeJS)
- Jetty (Java)
- Netty (Framework Java client serveur)
- JWebSocket (Java)
- Web Socket Ruby (Ruby)
- mod_pyWebSocket (extension en Python pour le serveur Apache HTTP)
- Websocket (Python)
- …
De nombreux projets sont en cours pour créer d’autres serveurs et frameworks intégrant les WebSockets.
Implémentation côté client : navigateurs
- Chrome : supporté version 4+
- Firefox : supporté mais désactivé version 4+
- IE : non supporté
- Opéra : supporté mais désactivé version 11+
- Safari : supporté version 5+
Le manque de support des navigateurs est dû à des failles de sécurité dans la spécification du protocole. J’en parlerai plus en détail dans la section relative à la sécurité.
Application
A présent, nous allons voir comment on peut utiliser l’API WebSocket côté client avec JavaScript.
On commence d’abord par créer une instance de websocket avec une url valide:
var ws= new WebSocket("ws://websocketUrl");
Ensuite, on gère les événements survenus suite à la création:
ws.onopen = function(evt) { alert("Connection open ..."); };
ws.onmessage = function(evt) { alert( "Received Message: " + evt.data); };
ws.onclose = function(evt) { alert("Connection closed."); };
ws.onerror = function(evt) { alert("WebSocket error : " + e.data) };
Pour envoyer des messages ou données on utilise la fonction send :
myWebSocket.send(MyMessage);
Et enfin, on ferme la WebSocket avec la méthode close :
myWebSocket.close();
Démonstration avec Jetty 7 :
L’idée est de faire une application de type forum de discussion. J’ai choisi pour cela d’utiliser le serveur Jetty 7 qui gère les WebSockets avec une implémentation de servlets, et qui permet de comprendre simplement comment utiliser l’API. On commence ainsi par créer une classe représentant notre WebSocket et sous la forme suivante :
public class ExampleWebSocket extends WebSocketServlet {
private static final long serialVersionUID = 1L;
public ExampleWebSocket() {
super();
}
private final Set<InternalExampleWebSocket> members = new CopyOnWriteArraySet<InternalExampleWebSocket>();
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
getServletContext().getNamedDispatcher("default").forward(request, response);
}
protected WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
return new InternalExampleWebSocket();
}
class InternalExampleWebSocket implements WebSocket {
private Outbound outbound;
public void onConnect(Outbound outbound) {
this.outbound = outbound;
members.add(this);
}
public void onMessage(byte frame, byte[] data, int offset, int length) {}
public void onMessage(byte frame, String data) {
for (InternalExampleWebSocket member : members) {
try {
member.outbound.sendMessage(frame, data);
} catch (IOException e) {
// org.eclipse.jetty.util.log.Log.warn(e);
e.printStackTrace();
}
}
}
public void onDisconnect() {
members.remove(this);
}
public void onFragment(boolean more, byte opcode, byte[] data, int offset, int length) {}
}
}
Cette servlet hérite de WebSocketServlet, une classe de Jetty, et contient une classe interne qui implémente l’interface WebSocket. Lors d’une connexion, la méthode doWebSocketConnect est appelée et un objet de type WebSocket est créé. Dans la classe interne, des méthodes sont implémentée pour gérer les événements liés à la connexion WebSocket (onConnect, onMessage, onDisconnect). On peut générer par ailleurs cette classe en utilisant Eclipse et le plugin Jetty WTP Adaptor.
Ensuite on crée une page HTML qui contient le code client de notre servlet. Voici des extraits du code JavaScript :
var ws;
var username;
var msg;
function updateStatus(id, message) {
document.getElementById(id).innerHTML = message;
}
function loadWebSocket() {
if (window.WebSocket) {
var url = "ws://localhost:8080/Html5WebSocket/ExempleWebSocket/";
ws = new WebSocket(url);
updateStatus("wsStatus", "webSocket supported !");
ws.onopen = function() {
updateStatus("wsStatus", "Connected to WebSocket server!");
}
ws.onmessage = function(e) {
displayMessage(e.data);
}
ws.onclose = function() {
updateStatus("wsStatus", "WebSocket closed!");
}
ws.onerror = function(e) {
updateStatus("wsStatus", "WebSocket error : " + e.data);
}
} else {
updateStatus("wsStatus", "Your browser does NOT support webSocket.");
}
}
//fonction qui envoie le texte saisi et validé par l'utilisateur.
function sendMyPost(newPost) {
username = document.forms["myform"].name.value;
msg = document.forms["myform"].msg.value;
if (msg) {
var post = username + " : " + msg;
if (ws.readyState == 1) {
ws.send(post);
} else {
alert("The websocket is not open! try refreshing your browser");
}
}
}
function displayMessage(message) {
//afficher le message en dessous du formulaire
//...
}
Une petite explication en ce qui concerne l’initialisation de WebSocket : ici l’url est « ws://localhost:8080/Html5WebSocket/ExempleWebSocket/ » sachant que le serveur Jetty est situé à localhost:8080, le nom du projet
est Html5WebSocket, et le nom de la servlet est ExempleWebSocket.
On peut également mémoriser les messages envoyés en local en utilisant l’API LocalStorage (voir mon billet précédent sur le mode hors-ligne HTML5). A chaque message envoyé, on sauvegarde dans le navigateur le nom de l’utilisateur et le message envoyé, puis à l’ouverture de la page on récupère les données enregistrées.
function saveData() {
if (window.localStorage) {
window.localStorage.setItem(username, msg);
}
}
function getData() {
if (window.localStorage) {
for ( var i = 0; i < window.localStorage.length; i++) {
var key = window.localStorage.key(i);
var value = window.localStorage.getItem(key);
var data = key + " : " + value;
displayMessage(data);
}
}
}
Et voici en images le résultat obtenu :
Vous trouverez le code complet en pièce jointe de l’article.
Sécurité
Des études ont été menées par des experts de sécurité concernant l’utilisation des WebSockets et des failles importantes ont été découvertes, notamment la possibilité de cache-poisoning dans des cas de présence de proxys transparents. Suite à ces études, les navigateurs Opéra et Firefox ont décidé de désactiver leur support des WebSockets (qui peut être réactivé dans les options) jusqu’à la résolution de ces problèmes dans une future version.
Conclusion
Le protocole WebSocket représente une avancée très importante dans la communication client-serveur. Le protocole HTTP est un protocole non connecté et sans état, et n’a pas été prévu pour établir des connexions permanentes ni le push de données : en HTTP, il faut que le client demande explicitement les données au serveur. Bien-entendu, plusieurs techniques ont été developpées pour contourner cette barrière et permettre la réception des données mises à jour quasi-instantanément (polling, long-polling…) ; mais elles surchargent le réseau en headers HTTP inutiles. Les WebSocket permettront bientôt d’y remédier.
Ces problèmes et lacunes soulevés rappellent que la spécification n’est encore qu’à l’état de brouillon et qu’il nous faudra patienter encore avant de pouvoir utiliser plainement les WebSockets. En attendant, des frameworks comme Kaazing WebSocket Gateway nous permettent de développer des applications temps-réel performantes en intégrant de nombreuses fonctionnalités, et accessibles sur tous les navigateurs.

