Ajax (Fake) Push: Long Polling mit HTML 5 WebWorker

Push-Mechanismen im Web sind mittlerweile weit verbreitet – die Anforderungen an die Infrastruktur aber recht hoch. Nichts geht ohne Plugins (Flash, Applet, WebSocket) – dann braucht man mindestens einen zweiten Server, der via persistenter Verbindung Nachrichten verteilt.

Bedient man sich herkömmlicher JavaScript-Technik, muss man mit aynchronen Ajax-Requests herumkaspern, sich mit Timeouts, Memory-Leaks und Cross-Domain-Sicherheitspolicen herumschlagen. Und auch hier kommt man nicht herum, den Server zu tweaken oder gleich einen zweiten aufzusetzen, der ausschließlich auf Polls oder Long-Polls trainiert ist.

Gerade in der PHP-Welt ist es nicht einfach, einen üblichen LAMP-Stack soweit zu bringen, dass so etwas auch unter Last funktioniert – Java ist da wie immer weiter und bietet mit JMS ein einheitliches Interface – da gibt es Software für den gewöhnlichen Servletcontainer, im Anwendungsserver muss das laut Spezifikation sogar im Lieferumfang enthalten sein.

Möchte man trotzdem mit “Bordmitteln” mal schnell eine Chatbox aufsetzen, führt Longpolling + Ajax eigentlich schnell zu einem akzeptablen Ergebnis – nichts für Millionen konkurrierende Zugriffe, aber immerhin eine nette Spielerei für zwischendurch. Wäre da nicht ständiges Warten auf die Response, das sich nach einiger Zeit einerseits mit einer merklichen Ruckelorgie bemerkbar macht oder gar den Mauszeiger in eine ewiglich drehende Sanduhr verwandelt.

HTML 5 to the rescue…

HTML 5 kann sogenannte Dedicated Worker – “echte” Workerthreads auf Betriebssystemebene, die man mit einer Zeile spawnen kann.

new Worker('meinScript.js')

Innerhalb eines Threads kann man lang laufende Rechenoperationen im Hintergrund verstecken – warum also nicht auch einen Ajax-Request? Allerdings haben diese Threads einen Nachteil: Aus Sicherheitsgründen laufen sie in einem klar abgegrenzten Scope, in dem sozusagen nichts vorhanden ist. Auch keine Referenz auf das document-Object der Elternseite. Das stoppt die meisten Javascript-Bibliotheken, wie bspw. JQuery. Man muss sich also mit Bordmitteln begnügen.

Eine Methode, die einen Worker initialisiert, könnte so aussehen:

    _initializeLongPoll: function()
    {
      var messenger = this;

      var worker = new Worker('my-ajax-worker.js');
        worker.onmessage = function(event)
        {
          var json = jQuery.parseJSON(event.data);
            
          // NEUE CHAT-NACHRICHT ERZEUGEN
          messenger.$_list.append($('<li class="messenger-list-item">'
            + '<strong class="messenger-list-item-author">' + json.author + '</strong>'
            + '<span class="messenger-list-item-message">' + json.text + '</span></li>'));
        };
    }

“my-ajax-worker.js” ist ausschließlich für den Ajax-Request zuständig. Hier wird ein dynamisches Javascript in einem Symfony-Projekt generiert:

    
    var onLoad = function()
    {
      var output = httpRequest.responseText;
      if (output) {
       
        // DELEGIERT DIE RESPONSE ZURÜCK.
        postMessage(output.trim());
        
        // ERZEUGE NEUEN XmlHttpRequest
        httpRequest = initRequest();
      }
    };

    // WIR SPAREN UND DEN X-BROWSER XmlHttpRequest-KRAM
    var httpRequest = initRequest();

Details zum serverseitigen Script möchte ich an dieser Stelle vernachlässigen, im Grunde funktioniert es folgendermaßen: Es arbeitet so lange, bis eine Änderung festzustellen ist. Erst bei Änderung wird die Response an den Client ausgeliefert (darum heißt es “Long-Polling”, weil ein Request ganz schön lange dauern kann):

while(true)
{
  if(newData())
  {
    return getNewData()
  }
  sleep(5);
}

Der Kram funktioniert natürlich nur in HTML5-Browsern, die das Worker-Objekt unterstützen. Dazu zählen Firefox 3.6, Google Chrome und natürlich Safari. Ob Opera es beherrscht, weiß ich nicht, ob der IE 8 es beherrscht, bezweifle ich. Aber wie gesagt: Es ist nur eine Spielerei und keine produktiv geeignete Anwendung. Demnächst schau ich mir auch mal die WebSocket API an…

Known Issues

Vorsicht mit session_start() und konkurrierenden (asynchronen) HTTP-Zugriffen. Im User-Sessionscope wird der Apache immer auf das Schließen einer Session warten, bis er dem nächsten Request erlaubt, die Session wieder “aufzunehmen”. Und im Normalfall dauert eine User-Session genau so lange wie die Request-Lifetime. Das führt dazu, dass bei 4 gleichzeitig abgefeuerten XmlHttpRequests diese trotzdem als Stack nach dem FIFO-Prinzip abgehandelt werden. Man verliert somit den Vorteil der Asynchronität. Es ist also zwingend erforderlich, die Session bereits vor dem Long-Poll-Loop abzuschließen (durch den Aufruf von session_write_close();

2 Replies to “Ajax (Fake) Push: Long Polling mit HTML 5 WebWorker”

Comments are closed.