Blog
WebSockets natives HTML5 avec AngularJS

Partager

Pourquoi les WebSockets ?

Avec le développement des RIA (Rich Internet Application), le besoin de développer des techniques de communication bidirectionnelle s’est fait ressentir.

Dans une architecture web classique, le client (navigateur) demande des informations au serveur. Mais il arrive parfois que le serveur ait besoin d’envoyer spontanément des informations au client, par exemple :

  • Pour être averti de certains événements (réception d'un message ou d'une notification).
  • Pour dessiner des graphiques en temps réel.

Bref historique des techniques de communication les plus populaires

Cela n'a pas toujours été si simple. On a d’abord eu Ajax et ses requêtes XHR qui permettait de recevoir des informations de manière asynchrone.
Certains développeurs utilisaient un setInterval sur une requête Ajax pour récupérer des informations de manière régulière. Cette technique est cependant déconseillée car très lourde (une nouvelle connexion http doit être ouverte à chaque fois, de la bande passante est consommée inutilement et ce n’est pas vraiment du temps réel puisqu’on n'est pas notifié dès que l’information est disponible)

Il y a aussi la technique du long-polling, qui consiste à envoyer une requête au serveur et si ce dernier n’a pas de résultat, la réponse n'est pas renvoyée. La requête reste donc en attente jusqu'à ce qu'un résultat soit disponible (possible grâce à un long TTL). La requête se termine lorsque le serveur renvoie les données. Une nouvelle connexion http est alors directement instanciée. Le principal avantage de cette technique est que la latence est faible puisque le serveur utilise déjà une connexion établie et prête à retourner l’information au client.
Le principal inconvénient du long-polling est que cela est assez complexe à mettre en place et demande beaucoup de ressources côté serveur.

WebSockets

C’est là qu’interviennent les websockets, un protocole standardisé fournissant une communication bidirectionnelle et livré avec une API.

Ce protocole est supporté par les principaux navigateurs : Google Chrome, Firefox, IE (à partir de la version 10), Safari et Opéra.

Une version sécurisée de ce protocole est également disponible (wss).

La librairie SocketIO est très populaire et fournit un wrapper très complet avec de nombreux fallbacks au cas où la technologie n’est pas supportée par le navigateur.

Cependant, elle est assez lourde (> 6000 lignes) et nécessite d’être implémentée côté client ET côté serveur.

Nous n’allons pas l’utiliser dans le cadre de cette démo car de nombreux tutoriaux traitent déjà de son utilisation. L’API est très simple à utiliser : il suffit d’instancier une WebSocket et de déclarer les principaux callbacks :

  • Connexion
  • Déconnexion
  • Erreur
  • Réception d’un message
var ws = new WebSocket("ws://127.0.0.1:8888/demo");
ws.onopen = function (event) {
	console.info('open');
	ws.send(JSON.stringify({event: "test", data: {"name": "BigInt"}})); 
};

ws.onclose = function (event) {
	console.info('close');
};

ws.onerror = function (event) {
	console.info('error');
};

ws.onmessage = function (event) {
	  console.log(event.data);
}

Il faut ensuite coder un serveur sur lequel se connecter afin de pouvoir échanger des messages par la suite. Nous avons choisi Tornado, un framework python scalable, utilisé pour faire du traitement réseau non bloquant :

class WebsocketHandler(tornado.websocket.WebSocketHandler):
    
    # Avoid a 403 error by accepting all cross-origin traffic for the purpose of this demo  
    def check_origin(self, origin):
        return True

    def open(self):
        print("WebSocket opened")
        self.write_message("ok")

    def on_message(self, message):
        try:
            print(self.request.headers)
            message = json.loads(message)
            if 'event' not in message:
                print('Missing event parameter')
                self.close()
                return
    
            print("event =", message['event'], ", data =", str(message['data']))
            self.write_message(u"" + json.dumps(message))
            self.write_message("message received")
        except ValueError:
           print("JSON expected.")
        except:
            print("Unexpected error:", sys.exc_info()[0])
 
    def on_close(self):
        print("WebSocket closed")

if __name__ == "__main__":
    application = tornado.web.Application([
        (r'/demo', WebsocketHandler)
    ])

    application.listen(8888)

    try:
        tornado.ioloop.IOLoop.instance().start()
    except KeyboardInterrupt:
        tornado.ioloop.IOLoop.instance().stop()

On a désormais la base pour que les WebSockets fonctionnent. On va essayer "d'angulariser" le code JS ci-dessous afin de créer une application permettant de récupérer l’état d’une WebSocket (en cours de connexion, connecté, déconnecté).

Cela est pratique si l’on décide d’activer ou de désactiver certaines fonctionnalités d’un site internet en fonction de l’état d’une WebSocket .

On crée un petit service utilisant la librairie angular-websocket. Il est très léger (< 400 lignes) et permet de gérer entre autres : la reconnexion automatique, des promises, une queue.

Ce service nous permet de wrapper la création de la WebSocket et de mapper les événements de connexion et de déconnexion :

angular.module('websocket').service('WebSocketWrapper', ['$log', '$websocket', '$rootScope', function($log, $websocket, $rootScope) {
	var ws = null;

	this.state = 'initializing';
	this.message = 'Websocket initializing';
	
	var self = this;
	
	this.init = function(){
		if (!ws) {
			ws = $websocket('ws://127.0.0.1:8888/demo', null, {reconnectIfNotNormalClose: true});
			
			ws.onOpen(function(){
				console.info('connected');
				$rootScope.$apply(function () {
					self.state = 'connected';
					self.message = 'Websocket connected';
				});
			});
			
			ws.onClose(function(){
				console.info('close');
				$rootScope.$apply(function () {
					self.state = 'disconnected';
					self.message = 'Websocket disconnected';
				});
			});
		}
	};
	
}]);

Notre contrôleur se charge d’appeler le service :

angular.module('websocket').controller('WebsocketStateCtrl', ['$scope', 'WebSocketWrapper', function($scope, WebSocketWrapper){
	$scope.websocket = WebSocketWrapper;
	$scope.websocket.init();
}]);
</div>

Nous pouvons finalement afficher l’état de la WebSocket dans notre page web en utilisant une pastille de couleur :

<div data-ng-controller="WebsocketStateCtrl">
	<span data-ng-bind="websocket.message"></span>
	<span class="circle" data-ng-class="{initializing: websocket.state === 'initializing', connected : websocket.state === 'connected', disconnected: websocket.state === 'disconnected', reconnecting: websocket.state === 'reconnecting'}"></span>
</div>

Conclusion

Nous venons de voir comment mettre en place les WebSockets rapidement sur un site. Tout le code est disponible sur notre GitHub.
Il est bien sûr possible de faire évoluer ce code en rajoutant le bind sur l’événement onMessage pour pouvoir réellement commencer à échanger de messages avec le serveur.

Enfin, il existe une alternative aux WebSockets peu connue : les Server Send Events. Il permet au serveur de communiquer un client (et uniquement dans ce sens) en utilisant la connexion http déjà établie.

comments powered by Disqus