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

Luigi Montanez
Luigi Montanez

מבוא

באמצעות WebSockets ו-EventSource, HTML5 מאפשר למפתחים ליצור אפליקציות אינטרנט שמתקשרות עם שרת בזמן אמת. Stream Congress (זמינה בחנות האינטרנט של 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 on 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

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

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

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

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

חזרה לבדיקות 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.

Polyfill יציב

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

סיכום

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