מקרה לדוגמה – עדכונים בזמן אמת בשידור הקונגרס

לואיג'י מונטנז
לואיג'י מונטנז

מבוא

באמצעות WebSockets ו-EventSource, HTML5 מאפשר למפתחים לבנות אפליקציות אינטרנט שמתחברות בזמן אמת עם שרת. הקונגרס סטרימינג (זמין בחנות האינטרנט של Chrome) מספק עדכונים בזמן אמת לגבי פעילות הקונגרס של ארצות הברית. הוא משדר עדכוני קומה מבית הנבחרים והסנאט, עדכוני חדשות רלוונטיים, ציוצים מחברי הקונגרס ועדכונים אחרים לגבי המדיה החברתית. האפליקציה אמורה להישאר פתוחה כל היום, בזמן שהיא מתעדת את פעילות הקונגרס.

התחלה עם WebSockets

המפרט של WebSockets קיבל הרבה תשומת לב מה הוא מאפשר: שקע TCP יציב ודו-כיווני בין הדפדפן לשרת. לא נקבע פורמט נתונים על שקע ה-TCP, המפתח רשאי להגדיר פרוטוקול העברת הודעות. בפועל, הכי נוח להעביר אובייקטים של JSON כמחרוזות. קוד ה-JavaScript בצד הלקוח, שמשמש להאזנה לעדכונים בזמן אמת, הוא נקי ופשוט:

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

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

למרות שהתמיכה בדפדפן ב-WebSockets פשוטה, התמיכה בצד השרת עדיין בשלב העיצוב. Socket.IO ב-Node.js מספק את אחד מהפתרונות המתקדמים ביותר בצד השרת, שהם הכי בוגרים ויציבים. שרת מבוסס-אירועים כמו Node.js מתאים במיוחד ל-WebSockets. בהטמעות חלופיות, מפתחי Python יכולים להשתמש ב-Twisted וב-Tornado, ובמפתחים של Ruby משתמשים ב-EventMachine.

חדש: Cramp

Cramp היא מסגרת אינטרנט אסינכרונית של Ruby שפועלת על גבי EventMachine. המאמר נכתב על ידי Pratik Naik, חבר בצוות הליבה של Ruby on Rails. Cramp, שמספק שפה ספציפית לדומיין (DSL) לאפליקציות אינטרנט בזמן אמת, הוא הבחירה האידיאלית למפתחי אתרים של Ruby. מי שמכיר את השלטים הכתיבה ב-Ruby ב-Rails יזהו את הסגנון של Cramp:

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

מכיוון ש-Camp נמצא מעל EventMachine שלא חוסם, יש כמה שיקולים שכדאי לזכור:

  • יש להשתמש במנהלי התקנים של מסד נתונים שאינם חוסמים, כגון MySQLPlus ו-em-mongo.
  • חובה להשתמש בשרתי אינטרנט מבוססי-אירועים. יש תמיכה מובנית ברקות ובקשתות בענן.
  • צריך להפעיל את אפליקציית Cramp בנפרד מאפליקציית Rails הראשית שמפעילה את Stream Congress, ושמופעלת מחדש ומנוטרת באופן עצמאי.

מגבלות נוכחיות

ב-8 בדצמבר 2010 אירעה תקלה ב-WebSockets, כאשר פורסמה פרצת אבטחה. Firefox ו-Opera הסירו את התמיכה בדפדפן ל-WebSockets. אמנם לא קיים polyfill טהור של JavaScript, אך קיימת החלופה ל-Flash המקובלת באופן נרחב. עם זאת, ההסתמכות על Flash אינה אידיאלית. למרות ש-Chrome ו-Safari ממשיכים לתמוך ב-WebSockets, התברר שכדי לתמוך בכל הדפדפנים המודרניים בלי להסתמך על Flash, צריך להחליף את WebSockets.

חזרה לתשאול של AJAX

קיבלנו את ההחלטה להפסיק להשתמש ב-WebSockets ולהתעדכן בתשאול AJAX. למרות שהיא יעילה בהרבה מנקודת מבט של קלט/פלט בדיסק וברשת, הסקרים של AJAX הפכו את היישום הטכני של Stream Congress. החשיבות הגבוהה ביותר היא ביטול הצורך באפליקציית Cramp נפרדת. נקודת הקצה של AJAX סופקה במקום זאת על ידי אפליקציית Rails. הקוד בצד הלקוח השתנה כדי לתמוך בתשאול jQuery AJAX:

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

בעיה משמעותית שכדאי לזכור לגבי EventSource היא שאסור להשתמש בחיבורים בין דומיינים. כלומר, צריך להציג את אפליקציית Cramp מאותו דומיין של streamcongress.com כמו אפליקציית Rails הראשית. ניתן לעשות זאת באמצעות שרת proxy בשרת האינטרנט. בהנחה שאפליקציית Cramp מופעלת על ידי Thin ופועלת ביציאה 8000, תצורת Apache נראית כך:

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>

ההגדרה הזו מגדירה נקודת קצה של EventSource ב-streamcongress.com/live.

פוליפיל יציב

אחד היתרונות המשמעותיים ביותר של EventSource מעל WebSockets הוא שהגיבוי מבוסס לגמרי על JavaScript, ללא תלות ב-Flash. polyfill של Remy Sharp משיג זאת על ידי הטמעת סקרים ארוכים בדפדפנים שאינם תומכים ב-EventSource באופן מובנה. לכן, EventSource פועל היום בכל הדפדפנים המודרניים שבהם JavaScript מופעל.

סיכום

HTML5 פותח את הדלת למספר רב של אפשרויות חדשות ומלהיבות לפיתוח אתרים. בעזרת WebSockets ו-EventSource, מפתחי אתרים קבעו סטנדרטים נקיים ומוגדרים היטב כדי לאפשר אפליקציות אינטרנט בזמן אמת. אך לא כל המשתמשים מריצים דפדפנים מודרניים. יש להביא בחשבון ירידה חיננית כשבוחרים להטמיע את הטכנולוגיות האלה. השימוש בכלים בצד השרת עבור WebSockets ו-EventSource עדיין נמצא בשלבי פיתוח מוקדמים. חשוב לזכור את הגורמים האלה במהלך פיתוח אפליקציות HTML5 בזמן אמת.