Pourquoi tu push?

Comme je l’ai dit dans mon précédent post, je suis très attaché à cette notion de plateforme de développement web intégrée et au fait de pouvoir concevoir aujourd’hui des applications complètes sur une base de html. Parmis les points essentiels de cette manière de concevoir les choses, il y a la notion de temps réel. Cette notion est toute relative et subjective du point de vue utilisateur.

Par exemple, on considère habituellement que toute action de moins de 100ms peut être considérée comme instantanée d’un point de vue de l’interface utilisateur, ou encore que la seconde est la limite acceptable dans une application pour que l’utilisateur ait cette sensation de fluidité qui fait qu’une application est agréable à utiliser.

Dans les applications web, la communication a été globalement depuis le début à sens unique.

Le client effectue une requête > le serveur y répond > la communication est coupée.

Depuis quelques années maintenant, l’évolution de la technologie aidant ainsi que l’expérience que l’on a des applications web, nous avons eut l’envie et le besoin de pousser de l’information depuis le serveur vers le client. Des technologies et spécialistes sont arrivés sur ce segment, avec notamment Comet qui désigne globalement aujourd’hui la notion de push ajax. Néanmoins, les implémentations aussi diverses que variées ne sont pas toujours pratiques, les solutions custom pas forcément évidentes et surtout, elles fonctionnent en général très bien sur unix et pas sous Windows.

Et sur Asp.Net?

Comme je l’ai déjà mentionné, c’est certainement cette fonctionnalité qui a attiré en premier les développeurs sur la plateforme Node.js. Après avoir joué pas mal de temps avec, je me suis rendu que l’on avait au final souvent sous-estimé ce paradigme fonctionnel et appliqué une mauvaise implémentation. J’ai trouvé le fonctionnement et l’apport au sein d’une application vraiment top, mais qu’en est-il en Asp.Net?

J’ai par le passé, déjà implémenté cette pseudo notion de push temps réel dans des applications web mais toujours au travers d’un classique polling ajax (qui s’avère suffisamment efficace la plupart du temps). Néanmoins qu’est-ce qui nous empêche d’avoir de longues connexions persistantes et de faire du long polling ou du streaming en Asp.net?

Pas grand chose en fait, à par peut être une mauvaise interprétation des problématiques.

Faire du long polling et avoir donc quelque chose sur le serveur qui attend pour rien fait peur en général et ont se dit que l’on va rapidement atteindre les limites du raisonnable. D’où le simple polling ajax.

Il répond au même paradigme que les connexions aux bases données:

 

use it late, release it fast

Donc, en théorie je m’en sors mieux si je fais plein de petite requêtes que une seule longue…Oui, mais ce n’est pas fondamentalement comme cela que cela se passe.

Déjà, il faut voir l’ajout de traffic réseau, un echange de messages toutes les secondes (en partant sur un polling raisonnable à la seconde) n’a pas le même coût qu’un ping de présence toutes les 30 secondes.

Ensuite, il y a les problématiques liées en elles-même à ce choix d’infrastructure applicative. Même si votre polling effectue de requêtes approchées dans le temps, à la seconde, les évents sur le serveur sont eux instantanés! Ce qui implique donc de gérer des buffers, des timestamp qui vont vous permettre de stocker temporairement  les derniers événements et savoir quoi renvoyer à chaque requête utilisateur, ce qui n’est pas forcément trivial à intégrer.

Enfin il faut aussi comprendre le cycle de vie des requêtes http sur IIS et Asp.net. Nous avons à notre disposition depuis .net 2, des handler http asynchrones qui permettent justement un découplage entre les requêtes et les traitements. Je ne vais pas rentrer dans les détails, ce n’est pas le sujet ici et je ne suis pas non plus le mieux qualifié pour en parler, mais lors qu’une requête sur le serveur, celle-ci est prise en compte par un thread de communication. Il y a un pool commun de thread de communication et leur nombre est assez limité. Le traitement peut être réalisé directement de manière synchrone par celui-ci mais peut aussi, dans le cas d’un handler asynchrone, être délégué à un thread de traitement, rendant par la même occasion le thread de communication à nouveau disponible pour servir de nouvelles requêtes.

Cette manière de faire colle donc parfaitement de manière performante avec la notion de long polling!

  1. J’envoie une requête ajax sur le serveur
  2. un thread de communication prend en charge cette requete
  3. il la transfère à un thread de traitement
  4. le thread de communication est à nouveau immédiatement disponible pour servir d’autres clients
  5. le thread de traitement se met en attente d’un signal (une action ou un timer)
  6. quand il a fini il reprend un thread de communication dans le pool et lui donne la main
  7. le thread de communication répond au client
  8. celui-ci reçoit la réponse, la traite et renvoie à nouveau immédiatement une nouvelle requête au serveur.

Au final, toutes les techniques nécessaires à faire cela sont belles et bien disponibles depuis longtemps, c’est vraiment plus une question de pratiques et d’expériences qui fait que l’on s’y penche un peu plus aujourd’hui.

 

Ok, let’s do it!

Le principe de base est d’avoir un bus d’événements qui va recevoir les notifications et les repousser vers ses abonnés. Lorsque le handler asynchrone reçoit une requête il donne la main à un traitement annexe et repasse dans le thread pool de connexion. Ce traitement commence par s’abonner au bus d’événements puis s’abonne à un autre événement de type timer. Le premier des deux qui envoie un signal, provoque la poursuite de la séquence et génère la réponse, la main est ensuite rendue au thread de réponse.

J’ai regardé plusieurs types d’implémentations, principalement en passant par les nouvelles classes Task<> ou encore en passant par le framework Reactive Extensions. En parallèle, mon ami Zied a lui voulu faire un test simple sans aucun autre framework avec le bon vieux pattern utilisant le IAsyncResult. Sachant que l’intérêt principal était de faire un test basique et simple, je suis revenu moi aussi vers cette méthode en partant de sa solution. Comme, je voulais un exemple utilisable de bout en bout, j’ai démarré la construction d’un petit framework permettant de faire simplement du long polling.

Vous trouverez donc dans ce framework une manière de faire du long polling. Je n’ai pas la prétention de dire que c’est la meilleure, mais elle fonctionne. J’ai ajouté aussi tout une couche d’infrastructure de manière à rendre cela simple d’utilisation. Vous avez a votre disposition des bus d’événements (que vous mettez en place déclarativement par type et nom), puis des helpers pour accéder à aux send/push simplement. Je vais continuer à travailler sur ce petit framework car je pense qu’il y a un besoin d’avoir quelque chose de simple à comprendre et à mettre en place. Ceci étant, si vous voulez quelque chose d’un peu plus mature aujourd’hui, je vous invite vraiment à utiliser SignalR qui est vraiment très bien (et à regarder le source aussi).

 

Welcome to Mediator framwork

Ceci est un framwork permettant donc d’envoyer des messages par push dans une page web en asp.net. Les messages sont transportés par une enveloppe contenant un timestamp et l’objet du message en lui-même. Les bus de messages sont identifiées par un type (d’objet) et un nom. La création de ces bus se fait par configuration au démarrage de l’application. Un buffer est aussi intégré pour garder en mémoire les X derniers messages pour chaque bus (afin par exemple de les afficher au moment du chargement d’une page). Coté client, il n’y a rien de particulier, définissez vos requêtes ajax comme d’habitude, partez de l’exemple si vous avez des doutes.

NOTE :

Pour ceux qui n’ont pas la patience d’aller plus loin, vous pouvez aller directement à la page github du projet:

https://github.com/rhwy/Mediator

 

1. Configuration

1) Importez la librairie Mediator.dll dans votre projet/site

2) Adaptez votre web.config si besoin. Par défaut tous les modules managés sont chargés (ce qui sera donc le cas du notre):

<modules runAllManagedModulesForAllRequests="true"/> 

si ce n’est pas le cas, ajoutez notre module à l’intérieur de cette balise modules

3) Définissez la route pour l’envoi des messages:


routes.Add(
   "mediator",
   new Route(
      "mediator/{type}/{name}",
      new RouteValueDictionary(new { type = "string", name = "default" }),
      new EventBusRouteHandler()));

Pour l’intant, ce handler ne prend en charge que la propagation des messages aux abonnés, l’envoi des messages se faisant à votre convenance (pour moi préférablement dans un contrôler dédié, voir ci-dessous)

2. Server code

Dans notre exemple nous partirons donc sur un controller dédié dont on pourra par exemple déclarer la route comme ceci:


//Message send
routes.MapRoute(
   "Send",
   "MediatorNotifier/{action}",
   new { controller = "MediatorNotifier", action = "Index" } );

C’est la manière la plus simple de centraliser les éléments liés à la notification temps réel dans votre application si il n’y a pas beaucoup de types de messages différents (ce qui est vrai dans la plupart des cas). Si jamais vous aviez une architecture plus conséquente, il vaut mieux ajouter ces actions spécifiques à chaque controller avec un regroupement plutôt fonctionnel.

Pour ce qui est de l’action en charge de la notification, cela peut facilement être réalisé comme dans l’exemple ci-dessous:


[HttpPost]
public ActionResult NotifyChatMessage(string name, ChatMessage message)
{
   MediatorBus.Send<ChatMessage>(this, message);
   return Json(new { saved = "ok" });
}

L’intéret initial de passer par une action de controller standard au lieu d’intégrer la notification dans le framework avec la distribution des messages  est juste de pouvoir simplement profiter du DefaultModelBinder des actions d’Asp.Net Mvc…On peut donc passer en ajax des objets complexes et fortement typés, et les passer simplement au bus grâce à notre helper MediatorBus.

Le framework embarque aussi une petite mécanique pour avoir une sorte de tampon des derniers messages (pour chaque bus) afin de pouvoir imprimer simplement ce qui est en cours. Ceci est particulièrement utile lors du chargement d’une page pour afficher ce qu’il y a en cours. Il est évident que dans une application ayant un minimum de fonctionnalités, vous auriez tout intérêt à gérer vous même cette fonctionnalité avec une gestion propre et personnelle des évènements et de la persistance. Néanmoins, la mécanique en place vous permet d’avoir une solution simple de suite.

Pour utiliser cette mécanique, j’aurais tendance à passer par une action partielle (souvent les besoins de remontée d’infos de push dans une page, sont plutôt liées à l’application en elle-même et pas à une page en particulier, utiliser cette méthode en simplifie le découplage):


[ChildActionOnly]
public ActionResult BufferOfChatMessage()
{
   ViewBag.Message = "Welcome to long polling demo!";
   var messageBuffer = MediatorBus.BufferOf<ChatMessage>();
   return PartialView(messageBuffer);
}

Puis pour l’appeler dans la vue parente:


   <h2>Simple message exchange</h2>
   <p>
      @Html.Action("BufferOfChatMessage","MediatorNotifier")
   </p>

Voici un exemple de vue allant de pair avec ce modèle:


@model IEnumerable
@{
   Layout = null;
   var messages = Model.Cast<MessageOf<ChatMessage>>();
}
<ul id="messages">
@foreach (var item in messages) {
   <li>@item.MessageItem.User : @item.MessageItem.Message</li>
}
</ul>

3. Code client

Pour envoyer les notifications, il n’y a rien de particulier, c’est au choix de chacun. Pour la partie abonnement, voici un exemple de fonction javascript simple, utilisant jQuery:


function getMessages() {
   $.post("/mediator/string", null, function (data, s) {
      if (data.MessageItem != "" && data.MessageItem != undefined) {
         var $msg = $('<li/>');
         $msg.html(data.MessageItem);
         $msg.prependTo('#messages');
      } else {
         console.log(data.TimeStamp);
      } 

      setTimeout(function () { getMessages(); }, 10000);
   });
}

C’est une fonction récursive qui se rappelle elle-même au moment ou elle a fini.

4. Aller plus loin

Ce petit framework a principalement été initié pour montrer la mise en place du long polling en asp.net sans librairie externe autre. Il n’a pas la prétention de garantir un niveau de qualité de code pour des applications d’entreprise mais peu néanmoins assez sereinement être utilisé en production.

Je vous invite donc à aller voir la page github du projet pour récupérer les soures:

https://github.com/rhwy/Mediator

Je mettrai ça sur auget dès que possible!

Faites moi part de vos retours, forkez le code, remontez vos patches et suggestions! Faites des commentaires, tout cela est plus que bienvenu!

Learn, share, enjoy!

Bon push!