Récemment j’ai pu tester un serveur push AJAX grâce à @LaFermeDuWeb qui a fait quelques essais sur son serveur. Il s’agissait là d’APE (Ajax Push Engine), un système push AJAX comprenant grossièrement un module Apache côté serveur et un bout de script JS côté client. Ce système permet de réaliser des applications temps réel, l’exemple classique est un tchat en ligne type IRC. J’ai toujours été intéressé par ces principes, me documentant autant que je le peux sur une application web mondialement connue : Facebook. J’ai donc fait de nouvelles recherches afin d’en savoir un peu plus.
Les suppositions et la théorie
J’ai voulu en savoir un peu plus sur ce système de push AJAX. Selon moi, c’est un serveur qui gère les données et un client qui reçoit ces données. A ma connaissance il n’est pas possible d’envoyer une donnée depuis le serveur directement vers le client au sens où le client deviendrait un écouteur du serveur.
Pour résoudre ce problème, une solution est de procéder à l’envers et simuler un écouteur client. En somme, le client doit requêter le serveur en permanence et le serveur doit fournir à chaque fois un nouveau contenu. Mais là, le client travaillerait sans interruption et occuperait une partie importante de la bande passante utilisateur. Il faut donc affiner ce procédé. Pour cela, admettons que ce soit le serveur qui fasse le requêtage permanent. Comment ? En temporisant nos requêtes clientes grâce à un blocage serveur.
Attention tout de même, ici on parlera de long polling et non de push ajax réel mais selon certains les deux technologies bien que différentes peuvent se confondre.
Le push AJAX utilise un autre principe qui est d’avoir un pseudo client sur le serveur, ainsi la communication va pouvoir s’établir du serveur vers le client. Ce pseudo client permet de conserver une liaison ouverte entre le serveur et le client, permettant ainsi l’envoi de données sans problème.
La vision technique
De manière plus concrète, j’envoie une requête cliente, le serveur va boucler tant qu’aucune nouvelle donnée n’est disponible. Le gain côté client est évident : économie de requêtes donc de bande passante, de charge processeur, de mémoire, etc… Une fois la donnée récupérée côté client, on va réinvoquer le serveur en lui demandant une nouvelle donnée.
Le serveur boucle sur la datasource, à la recherche d’une nouvelle donnée. On peut choisir de ne pas le faire boucler indéfiniement. Pour cela, on va fixer un timeout de 10 secondes mais également un pas de boucle d’une seconde (utilisation de la fonction usleep() par exemple). Ainsi, le serveur interroge la datasource toutes les secondes pendant dix secondes au maximum. Soit il y a une nouvelle donnée et on la renvoit, soit il n’y en a pas et alors on ne renvoit rien.
On a ainsi une bonne approche du temps réel mais il reste bien évidemment les soucis d’implémentation et de choix dans les technologies à notre disposition.
Implémentation
Côté serveur, on va utiliser JavaScript et plus particulièrement AJAX pour les requêtes. L’implémentation de cette partie est certainement la plus fastidieuse car c’est elle qui se charge du rendu (ce que voit l’utilisateur).
J’ai implémenté une solution avec JQuery, API JavaScript simplifiant grandement les choses et notamment la construction d’une requête AJAX. La requête AJAX invoque le serveur (en méthode POST) et récupère des données en JSON. Lorsque la page est chargée (évènement load), le script se lance. Il envoie une première requête au serveur (fonction luStart()). Lorsque le serveur retourne des données, on effectue le traitement (fonction luOnComplete()) puis on effectue une nouvelle demande au serveur.
// Fichier loop_updates.js
// William DURAND <william.durand1@gmail.com>
var luOnComplete = function(data, textStatus) {
var json = eval(data);
var room = document.getElementById('room');
for(var stuff in json)
{
room.innerHTML += '<p><strong>' + json[stuff].nickname + ':</strong> ' + json[stuff].message + '</p>';
room.scrollTop = room.scrollHeight;
if(document.activeElement.id != 'send' || document.activeElement.id != 'msg')
window.document.title = 'New message';
}
luStart();
};
var luStart = function() {
$.post('/mvc/assets/js/loop_updates.php', {}, luOnComplete);
};
addEvent('load', luStart);
// William DURAND <william.durand1@gmail.com>
var luOnComplete = function(data, textStatus) {
var json = eval(data);
var room = document.getElementById('room');
for(var stuff in json)
{
room.innerHTML += '<p><strong>' + json[stuff].nickname + ':</strong> ' + json[stuff].message + '</p>';
room.scrollTop = room.scrollHeight;
if(document.activeElement.id != 'send' || document.activeElement.id != 'msg')
window.document.title = 'New message';
}
luStart();
};
var luStart = function() {
$.post('/mvc/assets/js/loop_updates.php', {}, luOnComplete);
};
addEvent('load', luStart);
Du côté serveur, il faut réfléchir aux technologies. Par exemple, ce qui est important c’est une couche de persistence et qui plus est rapide. APE dispose d’un serveur en C. On peut penser à Java, en jouant sur le scope de l’application. Mais on peut utiliser PHP. Pour cela, nous devrons utiliser une datasource connectée à une couche de persistence. Plusieurs choix possibles, pour ma part j’ai testé une base de données MySQL et un serveur memcached.
Version base de données avec MySQL :
// Fichier loop_updates.php
// William DURAND <william.durand1@gmail.com>
$ressource = mysql_connect('localhost', 'root', '');
mysql_select_db('chat');
$time = time();
$query = 'SELECT * FROM entries WHERE time > "' . date('Y-m-d H:i:s', time()) . '";';
while((time() - $time) < 10)
{
$bool = false;
$data = array();
$response = mysql_query($query);
while($donnees = mysql_fetch_array($response))
{
array_push($data, $donnees);
$bool = true;
}
if($bool)
{
echo json_encode($data);
break;
}
usleep(1000);
flush();
}
mysql_close($ressource);
// William DURAND <william.durand1@gmail.com>
$ressource = mysql_connect('localhost', 'root', '');
mysql_select_db('chat');
$time = time();
$query = 'SELECT * FROM entries WHERE time > "' . date('Y-m-d H:i:s', time()) . '";';
while((time() - $time) < 10)
{
$bool = false;
$data = array();
$response = mysql_query($query);
while($donnees = mysql_fetch_array($response))
{
array_push($data, $donnees);
$bool = true;
}
if($bool)
{
echo json_encode($data);
break;
}
usleep(1000);
flush();
}
mysql_close($ressource);
Version complète avec memcached :
// Fichier loop_updates.php
// William DURAND <william.durand1@gmail.com>
$memcache = new Memcache();
$memcache->connect('localhost', 11211) or die ('impossible de se connecter');
if(!$memcache->get('counter'))
$memcache->set('counter', 0);
if(isset($_POST['nickname']) && isset($_POST['message']))
{
$nickname = htmlspecialchars($_POST['nickname']);
$message = htmlspecialchars($_POST['message']);
$memcache->set($memcache->get('counter'), array('nickname' => $nickname, 'message' => $message), false, 10);
$memcache->increment('counter');
}
else
{
$time = time();
$counter = $memcache->get('counter');
while((time() - $time) < 10)
{
$bool = false;
$data = array();
while($donnees = $memcache->get($counter))
{
array_push($data, $donnees);
$bool = true;
$counter++;
}
if($bool)
{
echo json_encode($data);
break;
}
usleep(1000);
flush();
}
}
$memcache->close();
// William DURAND <william.durand1@gmail.com>
$memcache = new Memcache();
$memcache->connect('localhost', 11211) or die ('impossible de se connecter');
if(!$memcache->get('counter'))
$memcache->set('counter', 0);
if(isset($_POST['nickname']) && isset($_POST['message']))
{
$nickname = htmlspecialchars($_POST['nickname']);
$message = htmlspecialchars($_POST['message']);
$memcache->set($memcache->get('counter'), array('nickname' => $nickname, 'message' => $message), false, 10);
$memcache->increment('counter');
}
else
{
$time = time();
$counter = $memcache->get('counter');
while((time() - $time) < 10)
{
$bool = false;
$data = array();
while($donnees = $memcache->get($counter))
{
array_push($data, $donnees);
$bool = true;
$counter++;
}
if($bool)
{
echo json_encode($data);
break;
}
usleep(1000);
flush();
}
}
$memcache->close();
Ces sources sont fonctionnelles mais très perfectible, ce sont des preuves de concept, pas des sources de production. Couplés à ces deux scripts, vous aurez un tchat JS/PHP complet (et fonctionnel) bien que très basique :
<!--
Fichier tchat.html
William DURAND <william.durand1@gmail.com>
-->
<html>
<head>
<style type="text/css">
body {
text-align:center;
margin:10px auto;
}
#global {
width:500px;
margin:auto;
text-align:center;
background-color:lightblue;
border:2px solid lightblue;
}
#global form {
width:500px;
margin:auto;
padding:2px;
}
#global form #nick {
text-align:center;
}
#global #room {
width:500px;
height:200px;
overflow:auto;
text-align:justify;
background-color:#fff;
}
#global #room p {
width:475px;
display:block;
font-size:0.8em;
margin:5px 0 0 5px;
}
</style>
<title>Tchat Room</title>
</head>
<body>
<div id="global">
<div id="room"></div>
<form onsubmit="return false;">
<input type="text" id="nick" name="nick" size="10" value="Anonymous" />
<input type="text" id="msg" name="msg" size="45" />
<input type="submit" value="Envoyer" id="send" name="send" />
</form>
</div>
<script type="text/javascript">
/*<![CDATA[*/
// Loading JavaScript
var dynamic_script_1 = document.createElement("script");
dynamic_script_1.src = "/mvc/assets/js/combo_js.php?files=jquery.1.2.6.min.js|events.min.js|loop_updates.js|tchat.js";
dynamic_script_1.type = "text/javascript";
document.getElementsByTagName("head")[0].appendChild(dynamic_script_1);
/*]]>*/
</script>
</body>
</html>
Fichier tchat.html
William DURAND <william.durand1@gmail.com>
-->
<html>
<head>
<style type="text/css">
body {
text-align:center;
margin:10px auto;
}
#global {
width:500px;
margin:auto;
text-align:center;
background-color:lightblue;
border:2px solid lightblue;
}
#global form {
width:500px;
margin:auto;
padding:2px;
}
#global form #nick {
text-align:center;
}
#global #room {
width:500px;
height:200px;
overflow:auto;
text-align:justify;
background-color:#fff;
}
#global #room p {
width:475px;
display:block;
font-size:0.8em;
margin:5px 0 0 5px;
}
</style>
<title>Tchat Room</title>
</head>
<body>
<div id="global">
<div id="room"></div>
<form onsubmit="return false;">
<input type="text" id="nick" name="nick" size="10" value="Anonymous" />
<input type="text" id="msg" name="msg" size="45" />
<input type="submit" value="Envoyer" id="send" name="send" />
</form>
</div>
<script type="text/javascript">
/*<![CDATA[*/
// Loading JavaScript
var dynamic_script_1 = document.createElement("script");
dynamic_script_1.src = "/mvc/assets/js/combo_js.php?files=jquery.1.2.6.min.js|events.min.js|loop_updates.js|tchat.js";
dynamic_script_1.type = "text/javascript";
document.getElementsByTagName("head")[0].appendChild(dynamic_script_1);
/*]]>*/
</script>
</body>
</html>
Et :
// Fichier tchat.js
// William DURAND <william.durand1@gmail.com>
var send = function() {
var nick = document.getElementById('nick');
var msg = document.getElementById('msg');
var m = msg.value;
var n = nick.value;
if(m == '')
return;
msg.value = '';
$.post('/mvc/assets/js/loop_updates.php', {message:m,nickname:n});
}
var title = function() {
window.document.title = 'Tchat Room';
}
function ready() {
$("#send").click(send);
$(document).focus(title);
}
addEvent('load', ready);
// William DURAND <william.durand1@gmail.com>
var send = function() {
var nick = document.getElementById('nick');
var msg = document.getElementById('msg');
var m = msg.value;
var n = nick.value;
if(m == '')
return;
msg.value = '';
$.post('/mvc/assets/js/loop_updates.php', {message:m,nickname:n});
}
var title = function() {
window.document.title = 'Tchat Room';
}
function ready() {
$("#send").click(send);
$(document).focus(title);
}
addEvent('load', ready);
La fonction addEvent() permet d’ajouter un écouteur sur l’élément window. Le combo JS a déjà été présenté dans l’article suivant http://www.willdurand.fr/articles/32/combo-handler-optimiser-le-nombre-de-requetes-http-pour-les-fichiers-css-et-javascript.html.
Debriefing
Au niveau des performances, peu de différences entre memcached et MySQL mais l’avantage non négligeable est la longévité des variables avec memcached. En effet ici, je définie une durée de vie de 10 secondes pour chaque message. Ils seront donc envoyés au client puis supprimés ensuite. Ceci permet de ne pas surcharger inutilement toute mémoire ou espace de stockage. Je me base sur le temps courant pour déterminer les nouveaux messages parmis tous les messages enregistrés.
Pour le reste, on a une bonne approche de l’instantané sans pour autant investir (je ne parle pas forcément d’argent) dans un serveur en plus et de nouveaux outils.


















3 commentaires
Merci
Je crois que c’est le seul mot qui convient.
Je cherchais un moyen simple et funky de diffuser en temps réel les annotations de mes documents.
Vlad
Bonjour,
Un seul mot grand merçi tu m’a sauvé la vie tanks.
Bonjour,
Merci encore j’essaye d’implémenter votre code mais le « addevent » au niveau du loop_updates.php ne semble pas fonctionner