- WebSocket มีประโยชน์สำหรับการสื่อสารแบบเรียลไทม์ แต่ไม่ได้จำเป็นเสมอไป และทางเลือกที่อิงกับ HTTP อาจเรียบง่ายและเสถียรกว่า
- ในด้านการจัดการทรานแซกชัน การดูแลการเชื่อมต่อ และความซับซ้อนของเซิร์ฟเวอร์ WebSocket อาจก่อให้เกิด overhead มากเกินความจำเป็น
- การใช้ HTTP Streaming และไลบรารี
eventkit ทำให้สามารถซิงก์แบบเรียลไทม์และจัดการอีเวนต์ได้โดยไม่ต้องพึ่ง WebSocket
WebSocket คืออะไร
- WebSocket คือเทคโนโลยีที่เปิดช่องทางการสื่อสารแบบสองทางอย่างต่อเนื่องระหว่างไคลเอนต์และเซิร์ฟเวอร์
- แม้จะเริ่มต้นการเชื่อมต่อผ่าน HTTP แต่หลังจากนั้นการสื่อสารจะเกิดขึ้นผ่านโปรโตคอลอีกแบบหนึ่ง
- มักถูกใช้ในการสร้างแอปพลิเคชันแบบเรียลไทม์ และมีประโยชน์ตรงที่รองรับการสื่อสารสองทาง
ข้อความของ WebSocket ไม่ได้ทำงานแบบทรานแซกชัน
- WebSocket ไม่ได้การันตีความสัมพันธ์โดยตรงระหว่างคำขอกับคำตอบ
- คำสั่งเปลี่ยนสถานะและข้อความผลลัพธ์ของมันอาจถูกส่งมาปะปนกันในสตรีมเดียวกัน
- ตัวอย่างเช่น หากไคลเอนต์ตัวหนึ่งเปลี่ยนสถานะแล้วเกิดข้อผิดพลาด ก็อาจระบุได้ยากว่าข้อผิดพลาดนั้นเป็นของคำสั่งใด
- วิธีแก้คือใส่
requestId เพื่อเชื่อมคำสั่งกับคำตอบเข้าด้วยกัน แต่ก็ทำให้ความซับซ้อนและต้นทุนในการดูแลเพิ่มขึ้น
- แนวทางที่ง่ายกว่าคือใช้วิธีแบบทรานแซกชันผ่าน HTTP สำหรับส่งคำสั่ง และใช้ WebSocket เฉพาะการกระจายการเปลี่ยนแปลงสถานะ
- สามารถแยกฝั่งส่งเป็น HTTP request และฝั่งรับเป็น WebSocket หรือวิธีสตรีมมิงแบบอื่นได้
ความยากในการจัดการวงจรชีวิตการเชื่อมต่อของ WebSocket
- เมื่อใช้ WebSocket คุณต้องจัดการการเริ่มต้น การสิ้นสุด ข้อผิดพลาด และการเชื่อมต่อใหม่ด้วยตัวเอง
- ตัวอย่างการจัดการพื้นฐานในเบราว์เซอร์จะรวมถึงการเปิดการเชื่อมต่อ การรับข้อความ การเกิดข้อผิดพลาด และการจัดการอีเวนต์ตอนการเชื่อมต่อปิดลง
- ยังต้องมีลอจิกเพิ่มเติม เช่น การเชื่อมต่อใหม่ การบัฟเฟอร์ข้อความ และ exponential backoff
- ในทางกลับกัน HTTP มีจุดเริ่มต้นและจุดสิ้นสุดที่ชัดเจนในระดับคำขอ ทำให้พัฒนาได้ง่ายกว่า
- การจัดการวงจรชีวิตที่ซับซ้อนแบบนี้จะสมเหตุสมผลก็ต่อเมื่อมีเหตุผลชัดเจนพอที่จะใช้ WebSocket
ความซับซ้อนของโค้ดฝั่งเซิร์ฟเวอร์ที่เพิ่มขึ้น
- WebSocket ต้องรองรับคำขออัปเกรดจาก HTTP ซึ่งต้องมีลอจิก handshake เพิ่มเติม
- ต้องตรวจสอบ header พิเศษอย่าง
Sec-WebSocket-Key และส่ง response header กลับอย่างเหมาะสม
- หลังเชื่อมต่อ WebSocket แล้ว ยังต้องคงสถานะการรับส่งข้อความอย่างต่อเนื่อง และอาจเจอปัญหาอย่างการจัดการ partial frame
- เมื่อเทียบกับการใช้ HTTP อย่างเดียว การดีบักและการจัดการข้อผิดพลาดจะยากขึ้น
- แม้เฟรมเวิร์กจะช่วย abstract บางส่วนออกไปได้ แต่ความซับซ้อนพื้นฐานก็ยังคงอยู่
ทางเลือก: HTTP Streaming
- HTTP เป็นโปรโตคอลที่รองรับการสตรีมมิงมาแต่เดิม จึงส่งข้อมูลเป็นสตรีมแบบเรียลไทม์ได้โดยไม่ต้องรอทั้งไฟล์ครบก่อน
- สามารถแทนที่เฉพาะความสามารถฝั่งรับของ WebSocket เดิมด้วย HTTP streaming ได้
- ใช้ asynchronous generator เพื่อจัดการการอัปเดตสถานะในรูปแบบสตรีมได้
- โฟลว์ฝั่งเซิร์ฟเวอร์
- การอัปเดตสถานะจะเกิดขึ้นในฟังก์ชันประมวลผลคำสั่ง
- ไคลเอนต์ที่เชื่อมต่ออยู่จะได้รับค่าที่อัปเดตผ่าน generator ทุกครั้งที่มีค่าใหม่ออกมา
- คำสั่งเปลี่ยนสถานะจะถูกส่งผ่าน HTTP POST และสมัครรับสตรีมแบบเรียลไทม์ผ่านคำขอ GET
- โฟลว์ฝั่งไคลเอนต์
- รับข้อมูลแบบเรียลไทม์ผ่าน Fetch API และ Stream Reader
- ถอดรหัสข้อความแล้วอัปเดต UI
- โครงสร้างนี้ทำให้สามารถซิงก์สถานะแบบเรียลไทม์ได้โดยไม่ต้องใช้ WebSocket
โบนัส: แนะนำไลบรารี eventkit
eventkit เป็นไลบรารีที่ช่วยให้สร้างและสังเกต asynchronous stream ได้ง่าย
- คล้ายกับ RxJS แต่ปรับปรุงเรื่องการจัดการ side effect และออกแบบบนพื้นฐานของ generator
- เมื่อ push การอัปเดตสถานะเข้าไปในสตรีม ไคลเอนต์ก็จะรับข้อมูลนั้นแบบเรียลไทม์ได้
- สามารถใช้งานได้อย่างเรียบง่ายทั้งฝั่งเซิร์ฟเวอร์และไคลเอนต์ผ่าน
Stream และ AsyncObservable
- การใช้ eventkit ฝั่งเซิร์ฟเวอร์
- push การเปลี่ยนแปลงสถานะเข้าไปใน Stream แล้วให้ไคลเอนต์ subscribe สตรีมนั้น
- การใช้ eventkit ฝั่งไคลเอนต์
- รับข้อมูลจากสตรีม ถอดรหัส แล้วอัปเดต UI
- มีทั้งคลัง GitHub อย่างเป็นทางการและคู่มือ HTTP Streaming ให้ใช้งาน
GitHub: https://github.com/hntrl/eventkit
3 ความคิดเห็น
ความคิดเห็นบน Hacker News
คิดว่า HTTP streaming ไม่ได้ถูกออกแบบมาโดยคำนึงถึงแพตเทิร์นนี้ HTTP streaming มีไว้สำหรับแบ่งข้อมูลก้อนใหญ่ออกเป็นชิ้น ๆ หากใช้การสตรีมเหมือนกลไก pub/sub ก็อาจต้องมาเสียใจภายหลัง ตัวกลางของ HTTP ไม่ได้คาดการณ์แพตเทิร์นทราฟฟิกแบบนี้ไว้ (เช่น NGINX, CloudFlare) ทุกครั้งที่การเชื่อมต่อ WiFi หลุด
fetchAPI ก็น่าจะโยนข้อผิดพลาดว่า request ล้มเหลวการส่ง RequestID ไปยังเซิร์ฟเวอร์เพื่อให้ได้วงจร request/response ไม่ใช่เรื่องแปลกหรือเกินจำเป็น สำหรับแอปจริงจัง การมี API แบบ
send(message).then(res => ...)นั้นคุ้มค่าเสมอheaders['authorization']จากคำขอเว็บซ็อกเก็ตกลับมาใช้ซ้ำ กลับต้องไปเข้าถึงออบเจ็กต์connectionParamsที่ทำตัวเหมือนเป็น request headerการสตรีมวิดีโอคือไคลเอนต์ร้องขอชังก์ตามช่วงข้อมูล ไม่ใช่การเชื่อมต่อ HTTP เดียว
ควรใช้ SSE แทน EventKit
ใน POC จะใช้การส่งฟอร์ม HTTP แบบดั้งเดิม ไม่ต้องการอย่างอื่น
ปัญหาของ HTTP/2 คือ server push เป็นสิ่งที่ถูกเพิ่มทับลงบนโปรโตคอลเดิม HTTP เป็นโปรโตคอลสำหรับส่งทรัพยากร จึงเพิ่มโอเวอร์เฮดที่ไม่จำเป็น เป้าหมายหลักของ HTTP/2 คือให้เซิร์ฟเวอร์ push ไฟล์/ทรัพยากรไปยังไคลเอนต์ล่วงหน้าเพื่อลด round-trip latency
WebSockets ไม่ได้ส่งเป็นสตรีม แต่ส่งเป็น datagram (packet) WebSockets API ของไลบรารี JavaScript ไม่สามารถจัดการ backpressure ได้ และจัดการข้อผิดพลาดได้ไม่ครบทั้งหมด หากจะใช้เหมือนเป็น TCP stream ก็ต้องระวัง
รู้สึกเสียดายหลังจากนำ WebSockets ไปใช้ในโปรดักชัน มีปัญหาอย่าง NGINX ปิดการเชื่อมต่อหลัง 4/8 ชั่วโมง และเบราว์เซอร์ไม่เชื่อมต่อใหม่หลังจากเครื่องตื่นจาก sleep เป็นต้น ถ้าเป็นไปได้ควรหลีกเลี่ยง WebSockets และการเชื่อมต่อระยะยาว
มีภาพจำในอุดมคติเกี่ยวกับ WebSockets อยู่ หลายคนมักโน้มเอียงจะใช้ WebSockets กับกรณีใช้งานแบบสตรีมหรือเรียลไทม์ WebSockets ทำให้สูญเสียความเรียบง่ายและข้อดีของเครื่องมือในฝั่ง HTTP ทางออกสำหรับการเปลี่ยนแปลงฝั่งเซิร์ฟเวอร์แบบสตรีมคือ h2/h3 และ SSE หากสามารถทำแบตช์ได้ไม่เกิน 0.5 req/s ต่อไคลเอนต์ ก็ไม่จำเป็นต้องใช้ WebSockets
คนที่สนใจ HTTP streaming ควรไปดู Braid-HTTP ซึ่งขยาย HTTP สำหรับ event streaming ได้อย่างสวยงาม และให้โปรโตคอลซิงก์สถานะที่ทรงพลัง