Fallstudie – Echtzeit-Updates in Stream Congress

Luigi
Luigi Montanez

Einleitung

Mithilfe von WebSockets und EventSource können Entwickler mit HTML5 Webanwendungen erstellen, die in Echtzeit mit einem Server kommunizieren. Stream Congress (im Chrome Web Store verfügbar) bietet Live-Updates zur Arbeit des US-amerikanischen Kongresses. Er streamt die neuesten Informationen aus dem Repräsentantenhaus und Senat, relevante Nachrichten, Tweets von Kongressmitgliedern und andere Beiträge aus den sozialen Medien. Die App sollte den ganzen Tag geöffnet bleiben, da sie das Geschäft des Kongresses einnimmt.

Mit WebSockets beginnen

Die WebSockets-Spezifikation hat bei ihrer Anwendung viel Aufmerksamkeit erhalten: einem stabilen, bidirektionalen TCP-Socket zwischen Browser und Server. Der TCP-Socket hat kein Datenformat. Der Entwickler kann ein Messaging-Protokoll definieren. In der Praxis ist es am praktischsten, JSON-Objekte als Strings zu übergeben. Der clientseitige JavaScript-Code zum Überwachen von Live-Updates ist übersichtlich und einfach:

var liveSocket = new WebSocket("ws://streamcongress.com:8080/live");

liveSocket.onmessage = function (payload) {
  addToStream(JSON.parse(payload.data).reverse());
};

Während die Browserunterstützung für WebSockets unkompliziert ist, befindet sich die serverseitige Unterstützung noch in der Gestaltungsphase. Socket.IO auf Node.js bietet eine der ausgereiftesten und robustesten serverseitigen Lösungen. Ein ereignisgesteuerter Server wie Node.js ist die richtige Lösung für WebSockets. Für alternative Implementierungen können Python-Entwickler Twisted und Tornado verwenden, während Ruby-Entwickler EventMachine nutzen.

Jetzt neu: Cramp

Cramp ist ein asynchrones Ruby-Web-Framework, das auf EventMachine ausgeführt wird. Der Entwurf stammt von Pratik Naik, einem Mitglied des Kernteams Ruby on Rails. Cramp stellt eine domainspezifische Sprache (DSL) für Echtzeit-Webanwendungen bereit und ist damit die ideale Wahl für Ruby-Webentwickler. Diejenigen, die mit dem Schreiben von Controllern in Ruby on Rails vertraut sind, werden den Cramp-Stil erkennen:

require "rubygems"
require "bundler"
Bundler.require
require 'cramp'
require 'http_router'
require 'active_support/json'
require 'thin'

Cramp::Websocket.backend = :thin

class LiveSocket < Cramp::Websocket
periodic_timer :check_activities, :every => 15

def check_activities
    @latest_activity ||= nil
    new_activities = find_activities_since(@latest_activity)
    @latest_activity = new_activities.first unless new_activities.empty?
    render new_activities.to_json
end
end

routes = HttpRouter.new do
add('/live').to(LiveSocket)
end
run routes

Da Cramp auf der nicht blockierenden EventMachine basiert, sind einige Aspekte zu beachten:

  • Es müssen nicht blockierende Datenbanktreiber wie MySQLPlus und em-mongo verwendet werden.
  • Ereignisgesteuerte Webserver müssen verwendet werden. Thin und Rainbows werden unterstützt.
  • Die Cramp-Anwendung muss getrennt von der Rails-App ausgeführt werden, die Stream Congress unterstützt, unabhängig neu gestartet und überwacht.

Aktuelle Beschränkungen

Als WebSockets am 8. Dezember 2010 eine Sicherheitslücke bekannt machte, kam es zu einem Rückschlag. Sowohl Firefox als auch Opera haben die Browserunterstützung für WebSockets entfernt. Es gibt zwar keinen reinen JavaScript-Polyfill, jedoch gibt es ein Flash-Fallback, das sich weithin durchgesetzt hat. Die Verwendung von Flash ist jedoch alles andere als ideal. Obwohl Chrome und Safari weiterhin WebSockets unterstützen, wurde klar, dass WebSockets ersetzt werden muss, um alle modernen Browser ohne Flash zu unterstützen.

Rollback auf AJAX-Abfrage durchführen

Die Entscheidung wurde getroffen, von WebSockets zurück zu den „alten Schule“-AJAX-Abfragen zu wechseln. Aus Sicht der Laufwerk- und Netzwerk-E/A ist die Effizienz zwar wesentlich weniger effizient, doch AJAX-Abfragen vereinfachten die technische Implementierung von Stream Congress. Am wichtigsten war, dass keine separate Cramp-App mehr erforderlich war. Der AJAX-Endpunkt wurde stattdessen von der Rails-App bereitgestellt. Der clientseitige Code wurde geändert, um jQuery AJAX-Abfragen zu unterstützen:

var fillStream = function(mostRecentActivity) {
  $.getJSON(requestURL, function(data) {
    addToStream(data.reverse());

    setTimeout(function() {
      fillStream(recentActivities.last());
    }, 15000);
  });
};

AJAX polling, though, is not without its downsides. Relying on the HTTP request/response cycle means that the server sees constant load even when there aren't any new updates. And of course, AJAX polling doesn't take advantage of what HTML5 has to offer.

## EventSource: The right tool for the job

Up to this point, a key factor was ignored about the nature of Stream Congress: the app only needs to stream updates one way, from server to client - downstream. It didn't need to be real-time, upstream client-to-server communication. 

In this sense, WebSockets is overkill for Stream Congress. Server-to-client communication is so common that it's been given a general term: push. In fact, many existing solutions for WebSockets, from the hosted [PusherApp](http://pusherapp.com) to the Rails library [Socky](https://github.com/socky), optimize for push and don't support client-to-server communication at all.

Enter EventSource, also called Server-Sent Events. The specification compares favorably to WebSockets in the context to server to client push:

- A similar, simple JavaScript API on the browser side.
- The open connection is HTTP-based, not dropping to the low level of TCP.
- Automatic reconnection when the connection is closed.

### Going Back to Cramp

In recent months, Cramp has added support for EventSource. The code is very similar to the WebSockets implementation:

```ruby
class LiveEvents < Cramp::Action
self.transport = :sse

periodic_timer :latest, :every => 15

def latest
@latest_activity ||= nil
new_activities = find_activities_since(@latest_activity)
@latest_activity = new_activities.first unless new_activities.empty?
render new_activities.to_json
end
end

routes = HttpRouter.new do
add('/').to(LiveEvents)
end
run routes

Ein wichtiges Problem bei EventSource ist, dass domainübergreifende Verbindungen nicht zulässig sind. Das bedeutet, dass die Cramp-Anwendung von derselben streamcongress.com-Domain wie die Rails-Hauptanwendung bereitgestellt werden muss. Dies kann über einen Proxy auf dem Webserver erreicht werden. Angenommen, die Cramp-Anwendung wird von Thin bereitgestellt und auf Port 8000 ausgeführt, sieht die Apache-Konfiguration so aus:

LoadModule  proxy_module             /usr/lib/apache2/modules/mod_proxy.so
LoadModule  proxy_http_module        /usr/lib/apache2/modules/mod_proxy_http.so
LoadModule  proxy_balancer_module    /usr/lib/apache2/modules/mod_proxy_balancer.so

<VirtualHost *:80>
  ServerName streamcongress.com
  DocumentRoot /projects/streamcongress/www/current/public
  RailsEnv production
  RackEnv production

  <Directory /projects/streamcongress/www/current/public>
    Order allow,deny
    Allow from all
    Options -MultiViews
  </Directory>

  <Proxy balancer://thin>
    BalancerMember http://localhost:8000
  </Proxy>

  ProxyPass /live balancer://thin/
  ProxyPassReverse /live balancer://thin/
  ProxyPreserveHost on
</VirtualHost>

Mit dieser Konfiguration wird ein EventSource-Endpunkt unter streamcongress.com/live festgelegt.

Stabiles Polyfill

Einer der bedeutendsten Vorteile von EventSource gegenüber WebSockets besteht darin, dass das Fallback vollständig auf JavaScript basiert und keine Abhängigkeit von Flash besteht. Mit polyfill von Remy Sharp wird dieses Ziel erreicht, indem Long Polling in Browsern implementiert wird, die EventSource nicht nativ unterstützen. Daher funktioniert EventSource heute in allen modernen Browsern mit aktiviertem JavaScript.

Fazit

HTML5 eröffnet viele neue und spannende Möglichkeiten der Webentwicklung. Mit WebSockets und EventSource verfügen Webentwickler nun über klare, klar definierte Standards, um Web-Apps in Echtzeit zu ermöglichen. Allerdings führen nicht alle Nutzer moderne Browser aus. Bei der Entscheidung für die Implementierung dieser Technologien muss Graceful Degradation berücksichtigt werden. Und die serverseitigen Tools für WebSockets und EventSource befinden sich noch in der Anfangsphase. Es ist wichtig, diese Faktoren bei der Entwicklung von HTML5-Echtzeit-Apps zu berücksichtigen.