ข้ามไปยังเนื้อหา

Building a Real-Time App

นี่คือบทสรุปปิดท้าย ทุกโมดูลก่อนหน้าเพิ่มมาโมดูลละหนึ่งความสามารถ ตรงนี้เราจะเอาทุกชิ้นมาประกอบเป็น service real-time เล็ก ๆ ตัวเดียว — แชตแบบอิงห้องพร้อมการแจ้งเตือน — แล้วรันบน server Node ws ของจริง เป้าหมายไม่ใช่ทฤษฎีใหม่ แต่คือการได้ดูชิ้นส่วนที่คุณรู้จักอยู่แล้วประกอบเข้าด้วยกันเป็นสิ่งที่ยืนหยัดได้จริง: auth ตอน connect, rooms สำหรับ routing, heartbeat เพื่อตรวจจับลิงก์ที่ตายแล้ว และรูปร่างบนสายที่อยู่รอด reconnect

service real-time คือ pipeline ที่แต่ละการเชื่อมต่อไหลผ่าน พอ socket เข้ามา เรา authenticate ก่อน ผูก state ต่อการเชื่อมต่อ พาเข้า rooms route message และคอย heartbeat ให้มีชีวิตอยู่จนกว่าจะจากไป:

flowchart TD
  open["Client connects<br/>(token in query / first message)"] --> auth{"Token valid?"}
  auth -- "No" --> reject["close(4401)<br/>unauthorized"]
  auth -- "Yes" --> attach["Attach state:<br/>userId, rooms, isAlive"]
  attach --> ready["Send 'ready'<br/>client can subscribe"]
  ready --> route["Route messages:<br/>join / leave / chat"]
  route --> beat["Heartbeat:<br/>ping every 30s, expect pong"]
  beat -- "no pong" --> dead["terminate dead socket"]
  route -- "client gone" --> clean["close -> leave all rooms"]
วงจรชีวิตของการเชื่อมต่อหนึ่งในบริการ

แต่ละกล่อง map ไปยังโมดูลที่คุณทำมาแล้ว: auth คือ security, state ต่อการเชื่อมต่อและ rooms คือ server design, heartbeat คือ connection lifecycle และ message envelope คือ message design บทสรุปปิดท้ายก็แค่การต่อสายเข้าด้วยกัน

  • Auth ตอน connect. ปฏิเสธ socket ที่ไม่ได้ authenticate ทันทีด้วย custom close code เพื่อให้ client รู้ว่าไม่ควร retry แบบหลับหูหลับตา ตรงนี้เราอ่าน token ง่าย ๆ จาก URL ของ upgrade request ส่วนใน production ควรเป็น session ที่ verify แล้วหรือ JWT (ดู โมดูล security)
  • Rooms. แต่ละการเชื่อมต่อพก Set ของ rooms ที่เข้าร่วมไว้ message แชตจะส่งเฉพาะให้ socket อื่นใน room เดียวกัน — pattern แบบ pub/sub จากก่อนหน้านี้ในโมดูลนี้ ที่กำหนดขอบเขตอยู่กับกลุ่ม
  • Heartbeat. TCP อาจเงียบไปได้โดยไม่มี error event — แล็ปท็อปหลับ โทรศัพท์สัญญาณหาย ping เป็นช่วง ๆ แล้วรอ pong คือวิธีที่ server รู้ว่าการเชื่อมต่อตายไปแล้วจริง ๆ และจะได้คืนทรัพยากร แทนที่จะ broadcast เข้าสู่ความว่างเปล่าตลอดไป
  • ความเป็นมิตรต่อ reconnect. server reconnect แทน client ไม่ได้ แต่ทำให้การ reconnect ถูกลงได้: join ที่สะอาดและ idempotent เพื่อให้ client ที่กลับมาเพียงแค่ประกาศ rooms ของตัวเองใหม่ และ close code ที่เสถียรเพื่อให้ client รู้ว่าควร retry (ชั่วคราว) หรือหยุด (auth ล้มเหลว)

ออกแบบให้ชัดเจนอย่างน่าเบื่อ — แท็ก type และ field ที่แต่ละ type ต้องมี:

// client -> server
type ClientMessage =
| { type: 'join'; room: string }
| { type: 'leave'; room: string }
| { type: 'chat'; room: string; text: string };
// server -> client
type ServerMessage =
| { type: 'ready'; userId: string }
| { type: 'joined'; room: string }
| { type: 'chat'; room: string; from: string; text: string }
| { type: 'error'; message: string };

ด้านล่างคือ เนื้อตัวของ connection handler สำหรับ server ws จริง — ws (socket นี้), wss (server สำหรับ broadcast เข้า room) และ req (upgrade request) อยู่ใน scope แล้ว handler จะ authenticate จาก query parameter ?token= ผูก state ต่อการเชื่อมต่อ จัดการ join/leave/chat ด้วย broadcast ที่ขอบเขตอยู่กับ room และรัน heartbeat ต่อ socket ลองเปิดใน StackBlitz รัน npm start แล้วเชื่อม client (เพิ่ม ?token=alice ลงใน URL) จากนั้นดู join, chat ที่ขอบเขตอยู่กับ room และ heartbeat ใน log ของ server

Node.js

Needs the Node.js runtime — open in StackBlitz to run.

เพื่อทดสอบ rooms อย่างเหมาะสม ให้เชื่อม client สองตัวด้วย token ที่ต่างกัน (เช่น ?token=alice และ ?token=bob) ให้ทั้งคู่ join room เดียวกัน แล้วดู chat จากตัวหนึ่งปรากฏบนอีกตัว แต่ไม่เคยสะท้อนกลับไปยังผู้ส่ง ตัดการเชื่อมต่อตัวหนึ่งทิ้ง แล้ว close handler ของ server จะเคลียร์ rooms ของ client ตัวนั้นให้ — ไม่มีสมาชิกภาพผีหลงเหลืออยู่

ถอยออกมาดูว่าไฟล์เล็ก ๆ นี้คืออะไร: service real-time ที่สมบูรณ์และป้องกันตัวเองได้ ปฏิเสธ socket นิรนาม route ตาม room แทนที่จะกระหน่ำใส่ทุกคน อยู่รอดจาก client ที่หายวับไปโดยไม่มี close และพูด protocol ที่มี type ซึ่ง client ที่ reconnect ต่อยอดได้ทันที คุณสมบัติทุกข้อมาจากคนละโมดูล — และการเอามาประกอบกันนี่แหละคือทักษะที่คอร์สนี้ตั้งใจสอน จากนี้ไป การสเกลข้าม instance ก็คือ pub/sub fan-out ที่คุณเจอมาแล้ว ที่เหลือเป็นเรื่องของตัวผลิตภัณฑ์

LayerTechnologyทำอะไร
TransportWebSocket / SSEส่งข้อมูล real-time ระหว่าง client และ server
ProtocolJSON-RPC, custom envelopestructure ของ message
State SyncCRDT, OT, event sourcingsync state ข้ามหลาย client
PersistencePostgreSQL, Redisเก็บ message, session, history
ScaleRedis pub/sub, Kafkaกระจาย message ข้าม server

สร้าง Real-time Feature ก่อนมี Core Feature อาการ:

  • ใช้เวลากับ WebSocket infrastructure ก่อน feature หลักทำงานได้
  • product ยังไม่พิสูจน์ว่า user ต้องการ real-time
  • สร้าง feature ด้วย HTTP polling ก่อน ถ้า user ต้องการ real-time ค่อย upgrade

ไม่มี Offline State Handling อาการ:

  • user ออฟไลน์ชั่วคราว กลับมา online — miss event ระหว่างนั้น
  • UI แสดงข้อมูลเก่าโดยไม่รู้ว่า miss อะไร
  • เมื่อ reconnect ให้ request event ที่ miss ผ่าน REST API หรือ replay mechanism

💡 ตัวอย่างจากของจริง

Figma:

  • multiplayer collaborative editor บน WebSocket
  • central server แบบ authoritative resolve การแก้พร้อมกันด้วย last-writer-wins (ไม่ได้ใช้ CRDT หรือ OT) — client ส่ง change ไป server เป็นคนตัดสินค่าสุดท้าย

Linear:

  • real-time issue tracking ผ่าน WebSocket
  • optimistic update บน client, sync กับ server ผ่าน event — UX รู้สึก instant
ทำไม server ถึงปฏิเสธ socket ที่ไม่ได้ authenticate ด้วย close code เฉพาะอย่าง 4401?
heartbeat แก้ปัญหาอะไรในบริการนี้?
message แชตถูก route อย่างไรใน server ของ demo?