Push & communications asynchrones sur iOS (iPhone/iPad)

Par rapport aux classiques requêtes pull, le push ou l’envoi de données initiées par un serveur ouvre la voie à des services de veille ou de surveillance de l’information. Ce mode de communication rend possible la messagerie instantanée, les tableaux de bords temps réel ou la transmission d’alertes.  Les performances d’un service asynchrone peuvent s’avérer meilleure que de classique requêtes synchrones (comme les Web Services sur HTTP).  Il  peut aussi rendre plus fiable la délivrance des messages dans un environnement mobile.

Cet article se concentre sur deux grands principes de communications asynchrones sur iOS:

  • Les Push Notifications avec l’Apple Push Notification Service utilisable avec les serveurs d’Apple
  • Les messages envoyés et reçus par un serveur MQ

Le premier principe est déjà connu et documenté par Apple. Le deuxième principe est quand à lui, peu documenté et est en conséquent le principal objet de cet article.

Les Push notifications d’iOS

En 2009, avec la sortie d’iPhone OS 3.0 (ou iOS 3.0), Apple a mis en place l’Apple Push Notification Service (APNS). Ce service permet d’afficher, à distance, une notification sur un iPhone.

Caractéristiques d’une notification

Les notifications sont attachées à une application installée sur iOS. Si l’application n’est pas démarrée, la notification prend la forme d’au moins un de ces éléments:

  • un son émis à sa réception (parmi ceux packagés dans l’application)
  • d’une alerte, composée principalement d’un texte affiché à l’écran
  • d’un badge affiché sur l’icône de l’application

Le choix de ces paramètres est à la discrétion du développeur, lors de l’envoi d’une notification. L’utilisateur final peut inhiber certaines de ces notifications via le menu « Réglages/Notifications » d’iOS.

Exemple de Push Notification, géré par l’application Métro Paris


Exemple de badge, signalant deux notifications reçues et non consultées, pour l’application 
Métro Paris

Ne confondez pas les Local notifications avec les Push notifications
En 2010, iOS 4 a vu l’apparition des Local notifications, qui ne sont absolument pas envoyés par un serveur distant. Dans le cas d’une Local notification, iOS programme et affiche lui-même sa propre notification. Dans cet article, il est uniquement fait référence aux Push notifications.

Des échanges uniquement via les serveurs d’Apple

Apple a la réputation de tout contrôler, afin d’assurer une qualité de service qu’elle estime optimale. Le service de notifications ne déroge pas à la règle: tous les messages envoyés vers iOS, le seront uniquement à partir des serveurs d’Apple. Pour envoyer une notification, le développeur envoie un message à l’intention des serveurs d’Apple, qui eux se chargent de le délivrer à une unité iOS.

Ce message, envoyé par le développeur, respecte un certain format. Il contient notamment l’identifiant du destinataire, la date d’expiration et le payload.

Exemple d’une architecture d’échanges destinée à envoyer des notifications à des iPhones à partir d’un serveur d’applicationau travers du APNS,

Le payload (charge utile en Français) est une structure JSON de moins de 250 octets, contenant le descriptif de la notification.

{ "aps" : { "alert" : "Hello World" }, "myProperties" : [ "test1",  "test2" ] }

Exemple de payload simple, affichant l’alerte « Hello World », avec des paramètres personnalisés facultatifs destinés à être traités par l’application

Les messages poussés via les serveurs de l’APNS n’ont aucune garantie de remise. Mais, il reste possible de consulter le statut de remise d’un message, en utilisant le Feedback Servicede l’APNS.

La sécurité n’est pas en reste. Les notifications sont cryptées, via le protocole TLS,  et les acteurs authentifiés via des certificats.

La solution d’Apple est une solution simple à mettre en place, peu couteuse et sécurisée, mais limitée fonctionnellement et techniquement.

Le Push avec un serveur MQ

Une solution alternative à la solution d’Apple est d’utiliser les possibilités d’un serveur MQ.  Ce type de serveur permet de réceptionner et d’émettre des messages à destinations d’une ou plusieurs types d’unités.

Architecture générale

Dans le cas présenté, le serveur MQ joue le rôle de chef d’orchestre en passant des messages entre les différents acteurs de notre système d’information: notamment entre les unités iOS et notre serveur d’application.

A ce titre, le serveur MQ assure deux fonctions importantes:

  • Message Queuing: fournit des files d’attente de messages asynchrones entre les producteurs et les consommateurs. Les messages de ces files sont poussés aux unités qui s’y sont inscrites.
  • Message Broker: le serveur traduit un message d’un protocole à un autre (par exemple du JMS à Stomp)

Exemple d’une architecture d’échange de messages, basé sur un serveur MQ, des iPhones (avec un client Stomp) et un serveur d’application (avec un client JMS dans le cas typique d’une architecture Java)

Dans cet exemple d’architecture, le serveur d’application envoi un message (par exemple au format JMS)  au serveur MQ. Ce dernier reçoit le message, puis le transmet aux iPhones inscrits sur ce serveur, mais cette fois-ci en respectant le protocole Stomp.

Un autre cas d’utilisation possible est qu’un iPhone envoie, via le serveur MQ, un message à un autre iPhone ou au serveur d’application.

Qu’est ce que le protocole Stomp? Et pourquoi l’utiliser ?

Stomp (Streaming Text Oriented Message Protocol) est un protocole de message basé sur du texte et avec peu de commandes. Le fait d’être un protocole textuel le rend multiplateforme et simple d’implémentation. On trouve ainsi des clients Stomp en Java, C/C++, Objective-C, et dans bien d’autres langages.

Sa simplicité et son implémentation (sous licence MIT) de client pour iOS, en fait un protocole de choix pour l’envoi et la réception asynchrone de messages.

Et XMPP?
Le protocole XMPP est compatible avec de nombreux serveur MQ et il existe même des implémentations open source de ce protocole pour iOS (comme XMPPframework).

Toutefois, ce protocole (bien qu’extensible) est spécialisé dans la messagerie instantanée, à l’inverse de Stomp qui est généraliste. Nous ne l’aborderons donc pas dans cette étude.

Développons notre client Stomp

Un client Stomp a deux utilisations principales: recevoir des messages et envoyer des messages au serveur MQ.  Mais avant de développer un client, un serveur MQ doit être mis en place.

Mise en place d’un serveur MQ avec Active MQ

ActiveMQ est un des serveurs MQ les plus populaires du monde Java et supporte Stomp. Sa configuration de base est simple. Un travail supplémentaire sera demandé pour assurer sa sécurisation et tuner la gestion de sa mémoire.

Et les autres serveurs MQ open-source?
RabbitMQ fournit un connecteur Stomp au statut expérimental. C’est dommage, car il est de plus en préféré à ActiveMQ.
HornetQ (JBoss) possède, tout comme ActiveMQ, un connecteur Stomp en version stable.
OpenMQ a droit aussi à un support Stomp, depuis la version 4.4

Dans le fichier de configuration activemq.xml, un broker doit être défini en utilisant un connecteur de transports Stomp.

<!-- Exemple provenant du fichier par défaut activemq-stomp.xml --> <!-- Pour de meilleures performances avec Stomp: 1. donnez assez de mémoire au broker 2. désactivé les task runner dédiés Ex : ACTIVEMQ_OPTS="-Xmx1024M -Dorg.apache.activemq.UseDedicatedTaskRunner=false" --> <beans> <!-- ... --> <!-- L’élement <broker> est utilisé pour paramétrer le broker d’ActiveMQ. --> <broker xmlns="http://activemq.apache.org/schema/core" brokerName="Broker5freshminutes" dataDirectory="${activemq.base}/data"> <!-- ... --> <!-- Les connecteurs de transports <transportConnectors> expose ActiveMQ à un protocole donné pour des clients et d’autres brokers. Pour plus d’informations, consultez: http://activemq.apache.org/configuring-transports.html --> <transportConnectors> <transportConnector name="stomp" uri="stomp://0.0.0.0:61612?transport.closeAsync=false"/> <transportConnector name="stomp+nio" uri="stomp+nio://0.0.0.0:61613?transport.closeAsync=false"/> </transportConnectors> </broker> </beans>

Cette configuration présente relate deux types de connecteurs Stomp:

  • stomp+nio : utilise aussi le protocole stomp sur TCP, mais en utilisant New I/O, une API, permettant plusieurs connexions par thread. C’est un choix de connecteur suggéré.
  • stomp : pour utiliser le protocole stomp, avec un thread par connection.

Initialisons un client Stomp sur iOS

Dans un projet iOS, nous avons besoin de deux bibliothèques de fonctionnalités pour mettre en place le client Stomp:

  • les API open-source de Stomp, que vous pouvez récupérer sur CodeHaus.
  • le Framework CFNetwork (déjà présent dans le SDK de iOS)

Dans le code source, initialiser un client Stomp et le connecter à un serveur MQ se fait ainsi:

#import "CRVStompClient.h" ... //Login et password pour accéder au serveur MQ (nil => anonymous) #define kUsername	nil #define kPassword	nil //L’adresse de votre serveur MQ #define kQueueServer	@"mq.monnomdedomaine.com" //Port par défaut du serveur MQ #define kPort	61613 ... //1- Initialise le client Stomp CRVStompClient *service = [[CRVStompClient alloc] initWithHost:kQueueServer port:kPort login:kUsername passcode:kPassword delegate:self autoconnect:YES]; //2- le client se connecte au serveur MQ [service connect];

La classe CRVStompClient propose des fonctions déléguées (voir CRVStompClientDelegate) destinées à traiter les événements de connexion et de déconnexion du serveur :

/** * Méthode appelée au moment où le client se connecte */ - (void)stompClientDidConnect:(CRVStompClient *)stompService { NSLog(@"Le client est connecte au serveur"); } /** * Méthode appelée au moment où le client se deconnecte */ - (void)stompClientDidDisconnect:(CRVStompClient *)stompService { NSLog(@"Le client s’est deconnecte du serveur"); }

Afficher un texte poussé sur votre client

Votre client iOS doit s’inscrire à une queue sur le serveur MQ afin de pouvoir recevoir des messages. Une queue est une destination pour les messages. Une queue est définie par un nom préfixé par /queue/

Notez qu’un message posté dans une queue, est retiré dès qu’il a été consommé par un client (point-to-point).

//La queue auquelle votre client va s’incrire //Toutes les queues commencent pat /queue/ #define kSendQueueName	@"/queue/listen-5freshminutes" ... //1- définition des parametres d’inscription à une Queue NSDictionary *headers = [NSDictionary dictionaryWithObjectsAndKeys: @"client", @"ack", @"true", @"activemq.dispatchAsync", @"1", @"activemq.prefetchSize", nil]; //2- inscription à la queue [service subscribeToDestination:kListenQueueName withHeader: headers];

Une fois que vous êtes inscrits à la queue désirée, vous devez implémenter une méthode déléguée de CRVStompClientDelegate appelée à chaque message capturé par votre client iOS :

/** * Methode appelée, lorque iOS recoit un message pushé du serveur */ - (void)stompClient:(CRVStompClient *)stompService messageReceived:(NSString *) body_ withHeader:(NSDictionary *) messageHeader_ { // Le contenu du message est récupéré NSLog(@"Contenu: %@", body_); NSLog(@"Message ID: %@", [messageHeader_ valueForKey:@"message-id"]); // On envoit un accusé de réception au serveur MQ [stompService ack: [messageHeader_ valueForKey:@"message-id"]]; }

Pour vos tests, n’hésitez pas à utiliser l’interface web d’administration d’ActiveMQ afin d’envoyer un message sur une queue. Elle est généralement accessible à l’url de la forme: http://monserveuractivemq:8161

En suivant l’architecture et les snippets présentés, ce sera généralement à votre serveur d’application d’envoyer un message sur la queue listen-fresh.

Exemple d’envoi de message sur la queue listen5freshminutes, à partir de l’interface web d’administration du serveur ActiveMQ

Poster un texte dans la queue du serveur MQ avec le client Stomp

Il existe des cas fonctionnels, où un système veut informer un autre système, sans attendre de retours de ce dernier. Dans ce cas, un appel asynchrone (push) est plus pertinent qu’un appel synchrone (comme un Web Service) où l’on devrait attendre un traitement côté serveur et une réponse potentiellement inutile.

Sur iOS, le push de données est on ne peut plus simple :

//Queue où les messages sont envoyés #define kSendQueueName	@"/queue/send-5freshminutes" ... //Envoit le message Hello World a notre serveur [service sendMessage:@"Pushed Message by me" toDestination:kSendQueueName];

Encore une fois, votre serveur d’application a généralement à lire le message sur la queuesend-5freshminutes.

L’interface d’administration d’ActiveMQ, permet aussi de consulter les messages poussés sur une queue.

Exemple de message réceptionné sur la queue send-5freshminutes, à partir de l’interface web d’administration du serveur ActiveMQ

Sécuriser les échanges

Si des données sensibles sont échangées, il est possible de passer par un tunnel crypté de données SSL.

Si un tel protocole est utilisé,  celui-ci est assuré automatiquement par AsyncSocket, côté iOS. Côté ActiveMQ, il suffit de paramétrer un connecteur de transport SSL:

<transportConnector uri="stomp+ssl://0.0.0.0:61614"/>

Des mécanismes de sécurité supplémentaires sont offerts par ActiveMQ, notamment:

  • la gestion des authentifications et les droits d’accès avec JAAS (Java Authentification and Authorization Service) avec l’authorizationPlugin ou avec simpleAuthenticationPlugin
  • en implémentant vos propres règles de sécurités, grâce à une classe Java respectant l’interface MessageAuthorizationPolicy

Multicast de messages

Apple ne fournit pas de solutions du type « envoyer à toutes les unités iOS». Si un développeur cherche à envoyer une notification à tous les utilisateurs, il doit envoyer autant de messages à l’APNS qu’il y a d’unités iOS inscrites à son service.

Les serveurs MQ fournissent généralement des solutions de multicasting, facile à utiliser, grâce aux topics.

A la différence des queues, un message posté dans un topic n’est pas supprimé dès qu’il est consommé par un client. Il peut donc être reçu par plusieurs d’entre eux (publish/subscribe).

Dans le protocole Stomp, les topics ont une adresse de destination débutant par /topic/.

En utilisant des wildcards dans le chemin de destination (voir http://activemq.apache.org/wildcards.html), il est aussi possible de recevoir des messages provenant de plusieurs queues ou topics.

Et la réception de push en tache de fond avec iOS4?
iOS 4 a vu l’arrivée, tant attendu, du multitâches. Néanmoins, la bibliothèque Stomp proposée ne permet pas nativement d’utiliser la fonctionnalité d’écoute du serveur MQ en tache de fond. Ceci est du notamment aux limites actuelles de la bibliothèque AsyncSocket.

Robustesse et réactivité des échanges asynchrones

Les modes de communications asynchrones présentés sont plus complexes à mettre en œuvre qu’une simple requête HTTP.  Mais avec cette méthode, l’utilisateur final bénéficie d’un client rapide et plus fiable à délivrer l’information.

Les échanges sont beaucoup plus rapides avec un serveur MQ qu’avec un serveur HTTP synchrone.  Les requêtes HTTP perdent beaucoup de temps à demander une connexion au serveur, et en perdent d’autant plus si le cache DNS ne connait pas le nom de domaine du serveur. La connexion au serveur MQ est quand à elle déjà initialisée avant l’exécution de l’instruction d’envoi ou de réception.  Le message est donc transmis plus rapidement.

Le fait d’avoir des messages asynchrones permet d’avoir des transactions plus courtes. C’est un avantage pour les réseaux où la perte de connectivité est fréquente. L’exemple suivant permet de mieux comprendre l’intérêt de cette caractéristique.

Dans une application boursière iPhone, un utilisateur souhaite acheter des actions. Il  passe un ordre au serveur et souhaite avoir un retour sur cet achat. Dans ce retour l’utilisateur veut savoir si son ordre a été accepté, exécuté et si oui en connaitre les détails. Or juste après cet envoi, l’iPhone de l’utilisateur ne capte plus le réseau mobile et ne peut pas recevoir le retour attendu.

Si l’envoi d’ordre repose sur une requête HTTP synchrone, l’utilisateur ne pourra pas connaitre le statut de son ordre. Son ordre a peut-être été bien envoyé, mais le client HTTP de son iPhone rapportera certainement une erreur, car il n’a pas reçu de réponse. L’utilisateur pensera, à tort, que son ordre n’est pas passé!

Si la transmission du retour de l’ordre repose sur un serveur push,  dès que l’iPhone est en mesure de se reconnecter au réseau, il se remettra à écouter automatiquement le serveur.  Il recevra alors immédiatement le retour et les détails souhaités. L’utilisateur sera donc rassuré!

Résumons nos deux solutions

Comme vous avez pu le constater, l’utilisation de l’APNS ou d’un serveur MQ ne couvrent pas les mêmes besoins.

Serveurs MQ APNS
Positifs Envoi de messages longs possible Infrastructure sécurisée fournie par Apple et quasi gratuite  (juste le cout de la licence iPhone Developer)
Multicast Alerte émise si l’application n’est pas lancée
Bidirectionnel (envoi/réception) Relativement simple à mettre en place
Temps de latence potentiellement plus bas que l’APNS Temps de latence correct
Le message ne part pas de votre serveur, si l’iOS récepteur n’est pas connecté.
Indépendant d’Apple
Négatifs Infrastructure potentiellement plus couteuse, du à la gestion du serveur MQ Envoi de messages courts uniquement (< 250 octets)
Réception impossible si l’application n’est pas démarrée Multicast impossible
Cout réseau du au fait que des messages sont envoyés aux serveurs APNS, alors qu’ils peuvent ne pas être lu par iOS si ce dernier est éteint ou que votre application y est désinstallé.

Chaque solution n’est pas meilleure que l’autre dans l’absolu.

L’Apple Push Notification Service est adapté à:

  • l’envoi d’alertes exceptionnelles (nouveau mail/message, actualité importante, trafic routier…)
  • aussi bien à des petites infrastructures qu’à des grosses
  • La délivrance de messages personnels

Le Serveur MQ est adapté à :

  • la constitution de tableaux de bord avec des mises à jour fréquentes
  • la gestion des chatrooms et des services de messageries instantanées
  • envoyer des commandes demandant un  court temps de latence (envoi d’ordres boursiers, jeux vidéo en ligne…)
  • La délivrance de messages personnels et collectifs.

Il existe encore d’autres solutions pour mettre en place un mode de communication asynchrone. Les serveurs HTTP évoluent et permettent déjà, pour beaucoup d’entre entre eux, le push de données via des techniques comme COMET ou les WebSockets.

Si aucune solution de communication ne convient, il restera toujours la possibilité d’utiliser les API bas niveaux gérant les sockets et de définir son propre mode d’échanges.

Sources & Références iOS:

Sources & Références MQ :