การสร้าง real-time features ใน Rails กลายเป็นง่ายขึ้นมากด้วย libraries เช่น Action Cable ใน episode ของ AppSignal Academy นี้ เราจะดำดิ่งสู่ real-time updating และเล่นกับการสร้าง minimal WebSocket server เพื่อดูว่ามันทำงานภายใต้หน้าปากอย่างไร
เราจะสร้างแอปพลิเคชันที่ pushes data และใช้ Pub/Sub ผ่าน WebSocket ก่อนเริ่ม code มาใช้เวลานิดหน่อย cover ว่าสาม concepts นี้หมายถึงอะไร:
Push หมายถึงการ push data ไปยัง receiver แทนที่จะให้ receiver poll ข้อมูลนั้น ต้องมีใน realtime updates เช่น stock prices, chat applications, หรือ operations consoles
Pub/Sub หรือ publish and subscribe เป็น interaction model สำหรับ push data ที่ได้รับความนิยมจาก TIBCO บน Wall Street ในช่วง 1990s Receiver subscribe ไปที่ subject และรอ publisher push data ไปที่ subject นั้น มักมี wildcard pattern matching เพื่อ match published message ไปยัง listener แม้ simpler implementations บางอย่างใช้แค่ named channels แทน wildcards ใน subject ผมเริ่มต้นที่ TIBCO ในวันแรกๆ จึงชอบ flexibility ของ wildcard pattern matching
WebSocket เป็น protocol สำหรับ exchange data - ปกติระหว่าง web browser และ application HTTP connection ถูก upgrade เป็น WebSocket connection และ data สามารถส่งได้ทั้งสองทางระหว่าง endpoints WebSockets สามารถ push data จาก application ไปที่ browser ยังให้ mechanism อื่นนอกจาก POST หรือ PUT สำหรับส่ง data จาก JavaScript code ใน browser กลับไปที่ application ค่อนข้างสะดวกใช่ไหม?
Under the Hood
มาดูตัวอย่าง WebSocket server ทำงานอย่างไร จาก browser client พยายามทำ WebSocket connection ไปที่ server ด้วย JavaScript code
var sock = new WebSocket("ws://" + document.URL.split("/")[2] + "/upgrade");Server รับ HTTP request พร้อม indicator ว่ามีการ request upgrade โดยทั่วไป server ให้ application ตัดสินใจว่า upgrade หรือไม่ วิธีการทำขึ้นอยู่กับ API ที่ให้กับ app Server ที่ support Rack ให้ option hijack socket และให้ developer handle protocol details ทั้งหมด หรือตาม proposed PR response ที่ upgrade เพียงพอ
Upgrade เป็นชุด exchanges ระหว่าง server และ client ทุก browsers และบาง server gems ซ่อนรายละเอียดเหล่านี้ เมื่อ connection established messages สามารถ exchange ได้ตาม WebSocket protocol
Magic ภายใต้หน้าปาก handle encoding, decoding, และ message exchange protocol Messages เป็น binary, fixed-width structures พร้อม trailing payload encrypted ด้วย SHA1 WebSocket protocol รวม message types และ exchanges หลายอย่าง เช่น ping/pong heartbeats และ opening/closing message exchanges นั่นคือ magic ที่ servers ทำโดยไม่ใช้ connection-hijack approach
Diving In
เราจะใช้ตัวอย่าง clock thread ที่เริ่มต้นเพื่อ publish เวลาปัจจุบันไปยัง listening clients ทั้งหมด เราจะใช้ Agoo สร้าง server เพราะมันเร็วและทำให้ complexity ต่ำสุด
เราจะเริ่มด้วย JavaScript เป็น client โดยแสดงเวลาปัจจุบันบน HTML page หลังสร้าง WebSocket ใหม่ onopen callback ถูกตั้งค่าเปลี่ยน status HTML element onmessage callback update message HTML element Callbacks เป็น common design pattern เมื่อทำงานกับ asynchronous calls เช่น publish/subscribe exchanges
...
... waiting ...
เมื่อ client เสร็จ มา implement server ซึ่งเป็น Ruby application ใช้ Rack API Class Clock เองจะเป็น handler สำหรับ HTTP requests ทั้งหมดบน path /upgrade ถ้า request เป็น upgrade เราแค่คืน Success พร้อม HTTP status code 200 ไม่เช่นนั้นคืน 404 Page Not Found ขั้นตอนเดียวใน method #call คือ assignment ของ WebSocket handler
class Clock def self.call(env) unless env['rack.upgrade?'].nil? env['rack.upgrade'] = Clock [ 200, { }, [ ] ] else [ 404, { }, [ ] ] end end endAPI based บน callbacks Callback เดียวที่เรา care สำหรับ server คือ #on_open ซึ่งช่วยให้เราสร้าง subscription ไปที่ subject "time" Messages ถูก exchange ผ่าน channels ที่ identified ด้วย subjects หรือ topics #on_open ถูกเรียกเมื่อ web socket connection established
class Clock # ... def self.on_open(client) client.subscribe('time') end endตอนนี้มาเริ่ม publishing ด้วย thread ที่ publish เวลาหลังจากทุกวินาที การเรียก Agoo.publish ส่ง message บน subject "time" แล้ว subscribers ทั้งหมดรับ message Server track subscriptions และ connections และ deliver message ไปที่ JavaScript client ซึ่ง update HTML element
require 'agoo' Thread.new { loop do now = Time.now Agoo.publish('time', "%02d:%02d:%02d" % [now.hour, now.min, now.sec]) sleep(1) end }Code อื่นที่ต้องการคือ code ที่ initialize และ start server การเรียก Agoo::Server.handle(:GET, '/upgrade', Clock) บอก server ให้ listen HTTP GET requests บน URL path /upgrade และส่ง requests ไปที่ class Clock นี่ช่วยให้ routing เกิดขึ้นนอก Ruby เพื่อ performance และ flexibility ที่ดีขึ้น
Agoo::Server.init(6464, '.', thread_count: 0) Agoo::Server.handle(:GET, '/upgrade', Clock) Agoo::Server.startเราใกล้เสร็จ รัน server ด้วย command นี้
$ ruby pubsub.rb Log entry ควรปรากฏประมาณนี้ แสดงว่า server running และ listening on port 6464
I 2018/08/14 19:49:45.170618000 INFO: Agoo 2.5.0 with pid 40366 is listening on https://:6464. Time To See If It's Working
มาเปิด https://localhost:6464/websocket.html หลัง flicker เริ่มต้นระหว่าง connection established connection status และเวลาควรแสดง เวลาจะ increment ทุกวินาทีตาม clock ticks
connected 19:50:12 ยินดีด้วยที่ทำ publish/subscribe web application ได้ ;-)
ใน episode วันนี้ เราดูการใช้ WebSocket Server Side Events (SSE) เสนออีก option ทำสิ่งเดียวกัน และเรารวม SSE ใน full source code example ถ้าอยากรู้เพิ่มเติม ลองดู Agoo server ที่เราใช้ หรือ Iodine WebSocket Server
ถ้ามีคำถามหรือความคิดเห็น อย่าลังเล drop us a line @AppSignal