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

เกริ่นนำ

HTML5 ช่วยให้นักพัฒนาซอฟต์แวร์สร้างเว็บแอปที่สื่อสารกับเซิร์ฟเวอร์ได้ในแบบเรียลไทม์ผ่าน WebSockets และ EventSource Stream Congress (พร้อมใช้งานใน 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());
};

แม้ว่าการสนับสนุน WebSocket ของเบราว์เซอร์จะตรงไปตรงมา แต่การสนับสนุนฝั่งเซิร์ฟเวอร์ก็ยังอยู่ในขั้นพัฒนา Socket.IO บน Node.js เป็นหนึ่งในโซลูชันฝั่งเซิร์ฟเวอร์ที่มีประสิทธิภาพและสมบูรณ์ที่สุด เซิร์ฟเวอร์ที่ขับเคลื่อนด้วยเหตุการณ์อย่าง Node.js เหมาะสำหรับ WebSocket นักพัฒนาซอฟต์แวร์ 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
  • ต้องใช้เว็บเซิร์ฟเวอร์ที่ขับเคลื่อนเหตุการณ์ โดยจะมีการรองรับสำหรับ Thin และ Rainbows ในตัว
  • แอป Cramp ต้องทำงานแยกจากแอป Rails หลักที่ขับเคลื่อน Stream Congress รีสตาร์ทและตรวจสอบอย่างอิสระ

ข้อจำกัดปัจจุบัน

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

ย้อนกลับไปยังการสำรวจ AJAX

การตัดสินใจนี้ถูกยกเลิกจาก WebSockets และกลับไปใช้การสำรวจ AJAX "แบบเก่า" แม้ว่าจะมีประสิทธิภาพน้อยกว่ามากเมื่อมองจากดิสก์และ I/O ของเครือข่าย แต่การหยั่งสัญญาณ AJAX กลับทำให้การใช้งานทางเทคนิคของ Stream Congress ง่ายขึ้น ที่สำคัญที่สุดคือขจัดความจำเป็นในการใช้แอป Cramp แยกต่างหาก แอป Rails ให้ปลายทาง AJAX แทน โดยมีการแก้ไขโค้ดฝั่งไคลเอ็นต์เพื่อรองรับการหยั่งสัญญาณ 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 หลัก ซึ่งทำได้โดยใช้การพร็อกซีที่เว็บเซิร์ฟเวอร์ สมมติว่าแอป 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 เมื่อเทียบกับ WebSocket คือวิดีโอสำรองเป็นแบบ JavaScript ทั้งหมด โดยไม่ต้องอาศัย Flash โดย polyfill ของ Remy Sharp ดำเนินการดังกล่าวโดยใช้การสำรวจระยะยาวในเบราว์เซอร์ที่ไม่รองรับ EventSource ตั้งแต่ต้น ดังนั้น EventSource จะทำงานในเบราว์เซอร์สมัยใหม่ทั้งหมดที่เปิดใช้ JavaScript

บทสรุป

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