Le push web avec Pusher

le 17/01/2012 par Eric Favre
Tags: Software Engineering

Introduction

Depuis que les sites web sont devenus des applications riches, le besoin de push s’est largement manifesté. Il est présent sur des sites de mails, de feeds d’information, de partage de documents, de réservation de billets avec choix des places… Le push web permet de notifier le client d’une certaine information directement depuis le serveur, sans nécessiter de recharger la page du client. C’est typiquement un paradigme qu’on peut utiliser sur un site de messagerie instantanée.

Plusieurs technologies permettent d’implémenter ce genre de comportement, les plus connues étant probablement les WebSockets, les server-sent events (tous deux inclus dans les spécifications HTML5), ou encore le long polling, du web pull simulant du web push, utile sur certains navigateurs des moins récents.

Pusher

Le produit

Un précédent article posté sur ce blog présentait le push web implémenté avec Diffusion (article en 2 parties). Nous allons présenter dans cet article une autre solution reposant sur la technologie WebSockets : Pusher.

Pusher se compose d’une API développée pour plusieurs langages (Ruby, Ruby on Rails, PHP, et ASP .Net) et d’une Infrastructure as a Service hébergée sur Amazon EC2, ce qui lui confère une scalabilité appréciable. Toute l'infrastructure gérant les connexions WebSockets et les canaux côté serveur est implémentée par la plateforme cloud Pusher. Il se distigue en cela d'autres frameworks tels socket.io qui implémentent uniquement la technologie WebSocket avec de nombreux fallbacks en cas d'incompatibilité, mais laissent la main à l'utilisateur sur la mise en place de l'infrastructure.

D’autre part, la PaaS Heroku ne supportant pas la technologie WebSocket, Pusher propose un add-on permettant son utilisation sur la plateforme de déploiement.

Mise en pratique

Canal Public

Sans plus attendre, passons à la pratique en mettant en place un site de messagerie instantanée, illustration typique de push web. Cet exemple permettra à la fois de transmettre des messages à tous les utilisateurs connectés, mais aussi d’afficher la liste de tous les utilisateurs présents. La compréhension de cette article implique des connaissances basiques en Ruby on Rails et en javascript, notamment jQuery. Vous pouvez d’ores et déjà trouver une démonstration de l’application finale ici (attention, rendu minimaliste !). Les credentials à utiliser sont test@pusher-chat-example.com/tester et test2@pusher-chat-example.com/tester. Tout le code est disponible sur github.

Au préalable, nous avons déjà mis en place un site développé en Ruby on Rails, déployé sur Heroku, et équipé de l’add-on Pusher. La librairie javascript jQuery est également ajoutée au projet. Enfin, le projet est configuré pour utiliser la gem pusher.

Le site dispose d’une page d’accueil et  d’une page d’authentification. Tout utilisateur accédant à la page d’accueil est déjà authentifié. La page d’accueil sera celle où nous créerons le service de messagerie instantanée.

Pour commencer, nous devons inclure dans notre page le fichier de l’API javascript de Pusher. On ajoute donc la ligne suivante dans le fichier html de la page d’accueil.

index.html.erb :

<%= javascript_include_tag "http://js.pusher.com/1.11/pusher.min.js" %>

Lors de l’ajout de l’add-on pusher depuis Heroku, un compte Pusher nous a été attribué.  Depuis ce compte, nous pouvons récupérer des informations d’identification pour le développement et pour la production. Ajoutons ces informations dans les fichiers appropriés :

config/environments/development.rb :

Pusher.app_id =  '13342'
Pusher.key    = 'd8d6a55bc5e7ffb81f0e'
Pusher.secret = 'my-development-secret-key'

config/environments/production.rb :

Pusher.app_id = '13341'
Pusher.key    = '6c786501a9b463e4465d'
Pusher.secret = 'my-production-secret-key'

Notre projet est à présent correctement paramétré pour utiliser Pusher. Nous pouvons désormais créer une fonction javascript d’initialisation qui s’abonne à un canal de push que l’on appelle « instant-messaging-channel » et ouvre une connexion sur ce canal vers la plateforme cloud de Pusher. L’application est identifiée auprès de Pusher par l’API key. La fonction d’initialisation est appelée au chargement de la page.

application.js :

function subscribeToPush() {
  var pusher = new Pusher('d8d6a55bc5e7ffb81f0e');
  var channel = pusher.subscribe('instant-messaging-channel');
}

Pour l’instant, la clé de l’API est écrite en dur dans le code.

D’autre part, côté html, nous ajoutons un formulaire depuis lequel nous pourrons poster un nouveau message aux destinataires, ainsi qu’un peu de code « cosmétique ».

index.html.erb :

<h1>Messagerie Instantanée</h1>
<div id="instant-messaging" data-pusher-key="<%=Pusher.key%>">
  <%= form_tag "home/send_message", :remote => true do %>
    <%= text_field_tag "message" %>
    <%= submit_tag "Envoyer" %>
  <% end %>
</div>

On constate qu’un attribut data de la balise div permet de communiquer la clé de l’API Pusher au javascript. Cela nous permet de récupérer la clé de développement ou de production via jQuery. On peut ainsi modifier la fonction javascript comme suit.

application.js :

function subscribeToPush() {
  var pusher = new Pusher(jQuery('#instant-messaging').attr('data-pusher-key'));
  var channel = pusher.subscribe('instant-messaging-channel');
}

Lorsqu’on poste un nouveau message par le formulaire, on souhaite maintenant qu’il soit diffusé sur  le canal et dispatché entre les différents abonnés du canal. L’action appelée par la soumission du formulaire est l’action send_message du contrôleur home. Dans cette action, on récupère le contenu du message posté par le formulaire, et on le diffuse sur le canal « instant-messaging-channel ». On définit l’évènement que l’on souhaite diffuser à travers ce canal comme du type « new-message ». Les données associées à l'évènement envoyé sont un hash qui contient ici le message posté avec :message comme clé. On répond ensuite par un header http de code 200.

home_controller.rb :

def send_message
  message = params[:message]
  Pusher['instant-messaging-channel'].trigger('new-message', {:message => message}) unless message.blank?
  head :ok
end

Ca n’est pas encore visualisable, mais croyez moi sur parole, nous diffusons à présent un évènement sur notre canal « instant-messaging-channel » chaque fois que nous cliquons sur le bouton d’envoi, et tous les abonnés de ce canal le reçoivent. L’évènement, comme l’indique le schéma présenté plus haut, est envoyé depuis le serveur de l’application (en l’occurrence, le serveur Heroku) vers la plateforme Pusher. Cette dernière le redistribue alors vers tous les clients ayant ouvert une connexion avec la même clé d’API Pusher et le même nom de canal.

Il nous reste maintenant à capter l’évènement poussé sur le client et à le traiter. Pour cela, nous allons ajouter un callback à notre canal. Celui-ci sera appelé chaque fois qu’un évènement de type « new-message » sera poussé sur le canal « instant-messaging-channel ». Nous utiliserons pour ça la fonction bind() de l’API javascript de Pusher. Dans notre application de messagerie instantanée simple, le traitement du callback sera de récupérer les données associées à l’événement, c’est-à-dire le hash envoyé depuis le contrôleur, et de les afficher les uns à la suite des autres. Donc, dans notre méthode d’abonnement au canal, nous ajoutons les lignes ci-dessous.

application.js :

function subscribeToPush() {
  var pusher = new Pusher(jQuery('#instant-messaging').attr('data-pusher-key'));
  var channel = pusher.subscribe('instant-messaging-channel');
  channel.bind('new-message', function(data) {
    jQuery('#instant-messaging').append(data.message + "<br />");
  });
}

Et voilà ! comme disent les américains. Vous avez à présent une application de messagerie instantanée, minimaliste certes, mais fonctionnelle. Pour la tester en local, il suffit de s’y connecter depuis plusieurs navigateurs différents. Nous pourrions à présent ajouter devant chaque message l’e-mail de son auteur très facilement. Il suffirait d’ajouter la paire clé/valeur e-mail dans les données associées à l’événement « new-message » lors de son envoi, et de récupérer ce nouveau paramètre à la réception de l’événement pour l’afficher à l’endroit qui convient. Le canal utilisé ici est un canal dit public, et son principe reste très simple.

Canal Présence

Nous allons à présent voir comment connaître en temps réel la liste des différents utilisateurs connectés au service de messagerie en utilisant un canal de présence. Le principe ici est globalement similaire à celui du canal public, mais il s’en distingue par quelques subtilités ayant leur importance.

L’idée est la suivante. Lorsqu’un client se connecte à Pusher, un token unique pour l’utilisateur et l’application est généré et lui est attribué. Lorsque ce même client souhaite s’abonner à un canal de présence (ou à un canal privé, dont le fonctionnement est sensiblement identique), l’API envoie d’abord une requête au serveur de l’application pour savoir si l’accès lui est autorisé. Le serveur peut déterminer l’autorisation d’accès grâce au nom du canal qui est transmis en paramètre. Le cas échéant, le service d’autorisation retourne avant tout une clé d’authentification. Cette clé permettra ensuite à Pusher de déterminer que l’utilisateur a bien accès au canal. D’autre part, le service retourne également certaines informations concernant l’utilisateur : un id obligatoire, d’une part, et toutes les informations que l’on souhaiterait voir transiter dans le canal de présence d’autre part (e-mail, statut, IP,…). Le message ainsi retourné ressemble à cela :

{"auth" => "d8d6a55bc5e7ffb81f0e:0d355103f73ba4de6a6f56975d3261bcea", "channel_data" => "{"user_id":4, "user_info":{"email":"mark.zuckerberg@gmail.com", "ip":"127.0.0.1"}}"}

Le schéma suivant illustre ce processus :

Maintenant que le principe est clair, passons à la pratique. On va donc commencer par définir auprès de l’API Pusher l’url sur laquelle il doit authentifier l’utilisateur. On ajoute dans la méthode subscribeToPush() :

application.js :

Pusher.channel_auth_endpoint <strong>=</strong> 'home/authenticate'

On demande ensuite un abonnement au canal présence (dont l’identifiant doit impérativement commencer par ‘presence’) :

applicaton.js :

var presence_channel <strong>=</strong> pusher.subscribe('presence-instant-messaging-channel');

Mais toute tentative d’abonnement à ce canal fera appel au service d’authentification que nous n’avons pas encore implémenté. Il faut donc ajouter l’action authenticate dans le contrôleur home.

def authenticate
  if user_signed_in?
    response = Pusher[params[:channel_name]].authenticate(params[:socket_id], {:user_id => current_user.id, :user_info => { :email => current_user.email}})
    render :json => response
  else
    render :text => "Not authorized", :status => '403'
  end
end

Ici, nous vérifions simplement que l’utilisateur s’est bien connecté au site, mais les contrôles pourraient être plus poussés et personnalisés, puisque l’identifiant du canal (« presence-instant-messaging-channel ») est passé dans les paramètres. La seule information pertinente que nous souhaitons envoyer à propos de l’utilisateur est son e-mail.

Les canaux de présence répondent par défaut à 4 évènements déclenchés par Pusher :

  • « pusher:subscription_succeeded » : envoyé quand l’abonnement a réussi. Une liste d’informations des membres connectés à ce même canal est envoyée conjointement à l’évènement.
  • « pusher:subscription_error » : envoyé si une erreur est survenue pendant l’abonnement. Le code http de l’erreur est associé à l’événement.
  • « pusher:member_added » : envoyé lorsqu’un nouveau membre s’abonne au canal. Les informations du membre sont envoyées conjointement à l’événement.
  • « pusher:member_removed » : envoyé lorsqu’un utilisateur s’est désabonné du canal. Les informations du membre sont envoyées conjointement à l’événement.

Dans les évènements décrits ci-dessus, les informations associées aux membres sont celles-là mêmes que l’on renvoie dans le service d’authentification.

Dans le cadre de notre application de messagerie instantanée, nous souhaitons afficher en temps réel la liste des utilisateurs présents sur le chat. Pour être à jour dans le liste, nous devons donc écouter les évènements « pusher:subscription_succeeded », « pusher:member_added » et « pusher:member_removed ».

Côté javascript, nous ajoutons donc les bind() suivants sur ces évènements :

application.js :

presence_channel.bind('pusher:subscription_succeeded', function(members){
  members.each(function(member) {
    jQuery('#members-list').append("<div id='member_" + member.id + "'>" + member.info.email + "</div><br />");
  });
});

presence_channel.bind('pusher:member_added', function(member) {
  jQuery('#members-list').append("<div id='member_" + member.id + "'>" + member.info.email + "</div><br />");
});

presence_channel.bind('pusher:member_removed', function(member) {
  jQuery('#member_' + member.id).remove();
});

Côté html, nous créons un div d’identifiant « members-list » destiné à accueillir les e-mails des utilisateurs connectés.

Ainsi, à l’établissement de l’abonnement au canal de présence, nous récupérons la liste des utilisateurs déjà présents, et affichons leurs e-mails. D’autre part, chaque fois qu’un utilisateur s’abonne ou se désabonne, nous ajoutons ou retirons son e-mail de la liste.

Limitations

Comme on vient de le voir le service de push web proposé par Pusher permet de mettre en place la technologie WebSocket au sein de son application de façon simple, rapide, et sécurisée si nécessaire. Cependant, Pusher souffre de quelques limitations.

Tout d’abord, si le protocole WebSocket est à présent largement supporté par les navigateurs récents, ce n’est pas le cas pour tous (décrite ici) antérieure à l’actuelle spécification du protocole. Heureusement, Pusher, dans son infinie sagesse, a automatiquement recours à un workaround dans le cas des navigateurs incompatibles. En effet, l’API fait alors appel à une émulation de WebSocket en Flash. Ici encore, cela impose qu’un plugin Flash soit installé sur le navigateur. Si ces limitations peuvent présenter un problème sur certaines infrastructures vieillissantes, elles sont toutefois vouées à disparaître.

De même, toujours en relation avec la technologie WebSocket, celle-ci peut connaître des dysfonctionnements si la connexion passe par un proxy. En effet, celui-ci, si il est configuré dans ce sens, peut parfois interrompre la connexion établie et empêcher le bon déroulement du push.

Ensuite, comme tous les services hébergés sur le cloud, tous vos messages passeront par Pusher, avec les craintes de confidentialité que ça peut susciter. Cette inquiétude est inhérente au cloud, et rien n’empêche de chiffrer ses messages avant de les transmettre. Il existe d'ailleurs une alternative à la plateforme Pusher avec le projet open source Slanger qui implémente le même protocole et permet d'internaliser la transmissions des messages WebSockets.

Enfin, Pusher est bien entendu payant si l’on va au-delà du simple test. Il propose plusieurs programmes allant de 19$/mois pour 100 connexions concurrentes et 200 000 messages/jour jusqu’à 199$/mois pour 5000 connexions concurrentes et 10 millions de messages/jour. La version de test permet toutefois de s’amuser suffisamment puisqu’elle permet 20 connexions concurrentes et 100 000 messages/jour.