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 Neuigkeiten aus den sozialen Medien gestreamt. Die App sollte den ganzen Tag geöffnet bleiben, da sie die Arbeit des Kongresses abbildet.
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. Dem TCP-Socket wird kein Datenformat auferlegt. 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 ist eine der ausgereiftesten und robustesten serverseitigen Lösungen. Ein ereignisgesteuerter Server wie Node.js eignet sich gut 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 basiert. Er wurde von Pratik Naik geschrieben, einem Mitglied des Ruby on Rails-Kernteams. Cramp bietet eine domänenspezifische Sprache (DSL) für Echtzeit-Web-Apps und ist daher 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 Haupt-Rails-App ausgeführt werden, die Stream Congress unterstützt, und 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 Verwendung von Flash ist jedoch alles andere als ideal. Auch wenn Chrome und Safari WebSockets weiterhin unterstützen, wurde klar, dass WebSockets ersetzt werden müssen, um alle modernen Browser ohne Flash zu unterstützen.
Rollback auf 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 keine separate Cramp-App mehr erforderlich. Der AJAX-Endpunkt wurde stattdessen von der Rails-Anwendung bereitgestellt. Der clientseitige Code wurde so geändert, dass er jQuery-AJAX-Polling unterstützt:
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.