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

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 יעילים הרבה פחות מנקודת מבט של קלט/פלט (I/O) בדיסק וברשת, אבל הם פשוטים יותר להטמעה טכנית של 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 בזמן אמת.