กรณีศึกษา - การอัปเดตใน Stream Congress แบบเรียลไทม์

บทนำ

HTML5 ช่วยให้นักพัฒนาซอฟต์แวร์สร้างเว็บแอปที่สื่อสารกับเซิร์ฟเวอร์แบบเรียลไทม์ได้ผ่าน WebSockets และ EventSource สตรีมสภาครัฐ (มีให้บริการใน Chrome เว็บสโตร์) ให้ข้อมูลอัปเดตแบบเรียลไทม์เกี่ยวกับการทำงานของสภาครัฐสหรัฐอเมริกา โดยสตรีมข้อมูลอัปเดตจากทั้งสภาผู้แทนราษฎรและวุฒิสภา ข้อมูลอัปเดตข่าวที่เกี่ยวข้อง ทวีตจากสมาชิกสภาคองเกรส และข้อมูลอัปเดตอื่นๆ ในโซเชียลมีเดีย แอปนี้มีไว้เพื่อให้เปิดอยู่ตลอดทั้งวันเนื่องจากจะบันทึกกิจกรรมของสมาชิกสภาคองเกรส

เริ่มต้นด้วย WebSocket

ข้อกำหนด WebSockets ได้รับความสนใจอย่างมากเนื่องจากสิ่งที่ทำได้คือ ซ็อกเก็ต TCP แบบ 2 ทิศทางที่เสถียรระหว่างเบราว์เซอร์กับเซิร์ฟเวอร์ ไม่มีการกำหนดรูปแบบข้อมูลในซ็อกเก็ต 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 เป็นตัวเลือกที่เหมาะสำหรับนักพัฒนาเว็บ Ruby เนื่องจากเป็นภาษาเฉพาะโดเมน (DSL) สำหรับเว็บแอปแบบเรียลไทม์ ผู้ที่คุ้นเคยกับการเขียนตัวควบคุมใน 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
  • ต้องใช้เว็บเซิร์ฟเวอร์ที่ทำงานตามเหตุการณ์ รองรับบางและรุ้ง
  • แอป Cramp ต้องทำงานแยกจากแอป Rails หลักที่ขับเคลื่อน Stream Congress, รีสตาร์ท และตรวจสอบแยกกัน

ข้อจํากัดปัจจุบัน

WebSockets ประสบปัญหาเมื่อวันที่ 8 ธันวาคม 2010 เมื่อมีการเผยแพร่ช่องโหว่ด้านความปลอดภัย ทั้ง Firefox และ Opera ได้ยกเลิกการรองรับ WebSocket ในเบราว์เซอร์ แม้ว่าจะไม่มี polyfill ของ JavaScript ล้วนๆ แต่ก็มีเนื้อหาสำรองสำหรับ Flash ที่ใช้กันอย่างแพร่หลาย อย่างไรก็ตาม การพึ่งพา Flash นั้นไม่เหมาะอย่างยิ่ง แม้ว่า Chrome และ Safari จะยังคงรองรับ WebSocket ต่อไป แต่เราก็พบว่าจำเป็นต้องแทนที่ WebSocket เพื่อรองรับเบราว์เซอร์สมัยใหม่ทั้งหมดโดยไม่ต้องอาศัย Flash

การเปลี่ยนกลับไปใช้การสำรวจ AJAX

เราจึงตัดสินใจเลิกใช้ WebSockets และกลับไปใช้การโพล AJAX แบบ "เก่าๆ" แม้ว่าจะมีประสิทธิภาพน้อยกว่ามากจากมุมมอง I/O ของดิสก์และเครือข่าย แต่การโหวต AJAX ก็ทำให้การใช้งานทางเทคนิคของ Stream Congress ง่ายขึ้น ที่สำคัญที่สุดคือคุณไม่จำเป็นต้องใช้แอป Cramp อีกต่อไป แอป Rails จะเป็นผู้ระบุปลายทาง AJAX แทน โค้ดฝั่งไคลเอ็นต์ได้รับการแก้ไขให้รองรับการเรียกข้อมูล AJAX ของ jQuery ดังนี้

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 หลัก ซึ่งทำได้โดยใช้พร็อกซีที่เว็บเซิร์ฟเวอร์ สมมติว่าแอป 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 เหนือ WebSocket คือ Fallback นั้นใช้ JavaScript ทั้งหมดโดยที่ไม่จําเป็นต้องใช้ Flash Polyfill ของ Remy Sharp ทํางานนี้ได้โดยใช้การโพลลิงแบบนานในเบราว์เซอร์ที่ไม่รองรับ EventSource โดยค่าเริ่มต้น ดังนั้น EventSource จึงทํางานในเบราว์เซอร์สมัยใหม่ทั้งหมดที่เปิดใช้ JavaScript

บทสรุป

HTML5 เปิดโอกาสใหม่ๆ ที่น่าตื่นเต้นมากมายให้กับการพัฒนาเว็บ WebSockets และ EventSource ช่วยให้นักพัฒนาเว็บมีมาตรฐานที่ชัดเจนและเข้าใจง่ายเพื่อเปิดใช้เว็บแอปแบบเรียลไทม์ แต่ผู้ใช้บางรายอาจใช้เบราว์เซอร์ที่ทันสมัย คุณต้องพิจารณาการลดระดับอย่างราบรื่นเมื่อเลือกที่จะนำเทคโนโลยีเหล่านี้มาใช้ และเครื่องมือฝั่งเซิร์ฟเวอร์สําหรับ WebSocket และ EventSource ยังอยู่ในช่วงเริ่มต้น คุณควรคำนึงถึงปัจจัยเหล่านี้เมื่อพัฒนาแอป HTML5 แบบเรียลไทม์