HTML5 et les WebSockets


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écedent 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:

DemoWebSocketForum.JPG

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.


Annexes

Commentaires

1. Le jeudi 10 mars 2011, 14:18 par Alexis Moussine-Pouchkine

WebSockets dans GlassFish (Grizzly):
http://antwerkz.com/glassfish-web-s...
http://www.youtube.com/watch?v=wcg2...

2. Le lundi 28 mars 2011, 23:15 par zepouet

Bonjour,

Je souhaiterais savoir s'il existe des api java websocket côté client. Mon problème est de faire communiquer une appli swing sur websocket (pour bypasser les firewalls) avec un serveur tel grizzly ou jetty. Evidemment je veux que ce soit le serveur qui notifie le ou les applis clientes qui soient notifiées (un vrai push ;)

Merci d'avance et surtout merci pour l'article.

Cordialement

3. Le lundi 6 juin 2011, 09:22 par lildadou

@zepouet: xLightWeb est une petite API sympa pour faire des serveurs/client HTTP supportant les WebSockets.
@author: Je ne l'ai est pas toutes essayé mais un bon nombres d'implémentation dite WebSocket ne le sont en réalité pas. Je pense en particulier à Kaazing...

4. Le vendredi 22 juillet 2011, 17:05 par Angelo

Bonjour,

Tout d'abord merci pour cet article qui donne une bonne idée de comment implémenter WebSocket coté client mais surtout côté serveur. Et c'est la ou je trouve que c'est le plus compliqué car il n'y a pas vraiment encore de spécification bien suivie. J'ai tenté plusieurs implémentation et la seule ou j'ai réussi a faire marcher une application de chat est celle de Jetty.

Le plugin WTP Jetty que j'avais développé intègre le wizard de génération du code Jetty de WebSocket mais il n'est plus d'actualité. En effet je viens de tester avec Jetty 7.4 et l'API a changé. Si j'ai le temps j'essaierais de corriger le wizard.

Je suis actuellement en train d'étudier une solution qui permette de faire tourner les WebSockets sur n'importe quel serveur (je ne veux pas utiliser Atmosphere qui apparemment se base sur les API WebSocket natives des Serveurs) . On n'a pas la chance de pouvoir utiliser Jetty dans un cadre professionnel et avec Embedded Jetty (on peut créer un serveur Jetty via un main) ca marche très bien. Il suffira ensuite d'embarquer ce code dans n'importe quel serveur et ca devrait fonctionner. Je vais faire une série d'articles sur ce sujet (je pourrais vous prévenir si cela vous intéresse).

@zepouet : j'ai vu dans le code de jetty-websocket qu'il faisait des JUnit qui appelait en Java (coté client) des WebSocket déployé coté serveur http://dev.eclipse.org/svnroot/rt/o...

Merci encore pour cet article.

Angelo

5. Le mardi 26 juillet 2011, 02:08 par Angelo

Bonsoir,

Pour information si cela vous intéresse, je viens de commencer une série d'articles intitulés "WebSockets with Embedding Jetty" sur http://angelozerr.wordpress.com/abo... ou j'explique pas a pas comment mettre en place l'application de chat fournit dans les examples de Jetty. Le serveur Jetty sera lancé via un main puis embarqué dans un autre serveur JEE pour montrer la possibilité de gerer WebSocket avec n'importe quel serveur Java (Tomcat, WebSphere...).

Angelo

Fil des commentaires de ce billet

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.