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 ล้มเหลว)
wire protocol
หัวข้อที่มีชื่อว่า “wire protocol”ออกแบบให้ชัดเจนอย่างน่าเบื่อ — แท็ก type และ field ที่แต่ละ type ต้องมี:
// client -> servertype ClientMessage = | { type: 'join'; room: string } | { type: 'leave'; room: string } | { type: 'chat'; room: string; text: string };
// server -> clienttype ServerMessage = | { type: 'ready'; userId: string } | { type: 'joined'; room: string } | { type: 'chat'; room: string; from: string; text: string } | { type: 'error'; message: string };รัน server จริง
หัวข้อที่มีชื่อว่า “รัน server จริง”ด้านล่างคือ เนื้อตัวของ 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
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 ที่คุณเจอมาแล้ว ที่เหลือเป็นเรื่องของตัวผลิตภัณฑ์
| Layer | Technology | ทำอะไร |
|---|---|---|
| Transport | WebSocket / SSE | ส่งข้อมูล real-time ระหว่าง client และ server |
| Protocol | JSON-RPC, custom envelope | structure ของ message |
| State Sync | CRDT, OT, event sourcing | sync state ข้ามหลาย client |
| Persistence | PostgreSQL, Redis | เก็บ message, session, history |
| Scale | Redis 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