Einführung
Mit WebSockets und EventSource können Entwickler mit HTML5 Webanwendungen erstellen, die in Echtzeit mit einem Server kommunizieren. Stream Congress (verfügbar im Chrome Web Store) bietet Live-Updates zur Arbeit des US-Kongresses. Es werden aktuelle Informationen aus dem Repräsentantenhaus und dem Senat, relevante Nachrichten, Tweets von Kongressmitgliedern und andere Updates aus sozialen Medien gestreamt. Die App sollte den ganzen Tag geöffnet bleiben, da sie die Arbeit des Kongresses erfasst.
WebSockets als Ausgangspunkt
Die WebSockets-Spezifikation hat aufgrund ihrer Funktionen viel Aufmerksamkeit erhalten: Sie ermöglicht einen 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 einfachsten, JSON-Objekte als Strings zu übergeben. Der clientseitige JavaScript-Code zum Warten auf Live-Aktualisierungen ist übersichtlich und einfach:
var liveSocket = new WebSocket("ws://streamcongress.com:8080/live");
liveSocket.onmessage = function (payload) {
addToStream(JSON.parse(payload.data).reverse());
};
Die Browserunterstützung für WebSockets ist zwar unkompliziert, die serverseitige Unterstützung befindet sich jedoch noch in der Entwicklungsphase. Socket.IO auf Node.js bietet eine der ausgereiftesten und robustesten serverseitigen Lösungen. Ein ereignisgesteuerter Server wie Node.js eignet sich optimal für WebSockets. Als alternative Implementierungen können Python-Entwickler Twisted und Tornado verwenden, während Ruby-Entwickler EventMachine haben.
Jetzt neu: Cramp
Cramp ist ein asynchrones Ruby-Web-Framework, das auf EventMachine ausgeführt wird. Er wurde von Pratik Naik verfasst, einem Mitglied des Ruby on Rails-Kernteams. Mit einer domainspezifischen Sprache (DSL) für Echtzeit-Web-Apps ist Cramp die ideale Wahl für Ruby-Webentwickler. Wer mit dem Schreiben von Controllern in Ruby on Rails vertraut ist, erkennt Cramps Stil:
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 dem nicht blockierenden EventMachine basiert, sind einige Aspekte zu beachten:
- Es müssen nicht blockierende Datenbanktreiber wie MySQLPlus und em-mongo verwendet werden.
- Es müssen ereignisgesteuerte Webserver verwendet werden. Dünne und Regenbogen werden unterstützt.
- Die Cramp-App muss getrennt von der Rails-App ausgeführt werden, die Stream Congress nutzt. Sie muss unabhängig neu gestartet und überwacht werden.
Aktuelle Einschränkungen
Am 8. Dezember 2010 wurde eine Sicherheitslücke in WebSockets bekannt, was zu einem Rückschlag führte. Sowohl Firefox als auch Opera haben die Browserunterstützung für WebSockets entfernt. Es gibt zwar keine reine JavaScript-Polyfill, aber einen Flash-Fallback, der weithin verwendet wird. Die Nutzung von Flash ist jedoch alles andere als ideal. Obwohl Chrome und Safari weiterhin WebSockets unterstützen, wurde klar, dass WebSockets ersetzt werden mussten, um alle modernen Browser ohne Flash nutzen zu können.
Zurück zu AJAX-Polling
Es wurde beschlossen, WebSockets aufzugeben und zu „altmodischen“ AJAX-Polling zurückzukehren. AJAX-Polling ist zwar aus Sicht der Laufwerk- und Netzwerk-E/A-Vorgänge viel weniger effizient, hat aber die technische Implementierung von Stream Congress vereinfacht. Vor allem ist die Notwendigkeit einer separaten Cramp-App entfallen. Der AJAX-Endpunkt wurde stattdessen von der Rails-App bereitgestellt. Der clientseitige Code wurde geändert, um jQuery-AJAX-Abrufe 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-App von derselben streamcongress.com-Domain wie die Haupt-Rails-App bereitgestellt werden muss. Dies kann mit Proxying auf dem Webserver erfolgen. Angenommen, die Cramp-App wird von Thin unterstützt 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.
Stable Polyfill
Einer der wichtigsten Vorteile von EventSource gegenüber WebSockets ist, dass der Fallback vollständig JavaScript-basiert ist und nicht von Flash abhängig ist. Die Polyfill von Remy Sharp implementiert Long-Polling in Browsern, die EventSource nicht nativ unterstützen. EventSource funktioniert daher in allen modernen Browsern, in denen JavaScript aktiviert ist.
Fazit
HTML5 eröffnet viele neue und spannende Möglichkeiten für die Webentwicklung. Mit WebSockets und EventSource haben Webentwickler jetzt klare, gut definierte Standards, um Echtzeit-Webanwendungen zu ermöglichen. Aber nicht alle Nutzer verwenden moderne Browser. Bei der Implementierung dieser Technologien muss die Abwärtskompatibilität berücksichtigt werden. Die Tools für WebSockets und EventSource auf der Serverseite befinden sich noch in der Anfangsphase. Diese Faktoren sollten Sie bei der Entwicklung von Echtzeit-HTML5-Apps berücksichtigen.