Rooms and Broadcast
การ broadcast — หนึ่ง event ไปถึง client จำนวนมากพร้อมกัน — คือเหตุผลที่ WebSocket server มีอยู่ บทเรียนนี้ครอบคลุม broadcast สองรสชาติ (ไปยัง ทุกคน และไปยัง room) และโครงสร้างข้อมูลที่ทำให้ room ทำงานได้
การ broadcast ไปยังทุกคน
หัวข้อที่มีชื่อว่า “การ broadcast ไปยังทุกคน”ไลบรารี ws ให้ wss.clients มา ที่เป็น Set ของทุก socket ที่เปิดอยู่ในขณะนั้น เพื่อส่งอะไรบางอย่างไปยังพวกเขาทั้งหมด ให้วน loop แล้ว send — โดยตรวจก่อนว่าแต่ละ socket เปิดอยู่จริง:
import { WebSocket } from 'ws';
function broadcastAll(wss, message) { for (const client of wss.clients) { if (client.readyState === WebSocket.OPEN) { client.send(message); } }}การ์ด readyState === WebSocket.OPEN มีความสำคัญ: socket อาจอยู่ระหว่างปิดหรือกำลังจะปิดอยู่แล้ว และการเรียก send ตอนนั้นจะ throw หรือ buffer แบบเงียบ ๆ กรองให้เหลือเฉพาะ OPEN ก่อน send เสมอ นี่คือ broadcast ที่ง่ายที่สุดเท่าที่จะเป็นไปได้ — มีประโยชน์สำหรับประกาศแบบ global แต่เป็นค้อนปอนด์สำหรับอะไรที่ต้องเจาะจง
Rooms: การ broadcast ไปยังกลุ่มย่อย
หัวข้อที่มีชื่อว่า “Rooms: การ broadcast ไปยังกลุ่มย่อย”แอปส่วนใหญ่ไม่ต้องการให้ ทุกคน ได้ยิน ทุกอย่าง ข้อความแชทอยู่ใน channel หนึ่ง; การอัปเดตเกมอยู่ในแมตช์หนึ่ง pattern คือ room — กลุ่มของ socket ที่มีชื่อ โครงสร้างข้อมูลคลาสสิกคือ Map จากชื่อ room ไปยัง Set ของ socket:
const rooms = new Map(); // roomName -> Set<ws>
function join(room, ws) { if (!rooms.has(room)) rooms.set(room, new Set()); rooms.get(room).add(ws);}
function leave(room, ws) { rooms.get(room)?.delete(ws); if (rooms.get(room)?.size === 0) rooms.delete(room); // tidy up empty rooms}
function broadcastRoom(room, message) { for (const client of rooms.get(room) ?? []) { if (client.readyState === WebSocket.OPEN) client.send(message); }}ตอนนี้ข้อความ fan ออกไปเฉพาะ socket ที่ join room นั้น — ไม่ใช่ทั้ง server สังเกตความสมมาตรกับบทเรียนก่อนหน้า: set state.subscriptions ของแต่ละ socket คือมุมมองของ client ต่อ room ของตัวเอง ขณะที่ rooms map นี้คือ reverse index ของ server รักษาให้ทั้งสอง sync กันไว้
flowchart TB SENDER["client A<br/>(in room 'sports')"] -- "publish to 'sports'" --> S["server"] S --> ROOM["room 'sports'"] ROOM == "deliver" ==> B["client B ✓ in sports"] ROOM == "deliver" ==> C["client C ✓ in sports"] S -. "not delivered" .-> D["client D ✗ in 'news'"]
Client D ซึ่ง join room อื่น ไม่เคยได้ยินข้อความเลย — การเลือกเจาะจงนั้นคือประเด็นทั้งหมดของ room
อย่า echo กลับไปหาผู้ส่ง (โดยทั่วไป)
หัวข้อที่มีชื่อว่า “อย่า echo กลับไปหาผู้ส่ง (โดยทั่วไป)”การขัดเกลาที่พบบ่อย: เมื่อ broadcast บรรทัดแชท คุณมักจะข้ามผู้ส่ง เพราะ client ของพวกเขาเองได้แสดงข้อความแบบ optimistic ไปแล้ว แค่เปรียบเทียบกับ socket ที่ส่ง:
for (const client of rooms.get(room) ?? []) { if (client !== sender && client.readyState === WebSocket.OPEN) { client.send(message); }}จะรวมผู้ส่งหรือไม่นั้นเป็นทางเลือกของแอปพลิเคชัน — บางแอป echo กลับเพื่อให้ UI ของผู้ส่งยืนยันว่า server ได้รับแล้ว ไม่ว่าทางใด toggle client !== sender คือสวิตช์ที่คุณใช้ควบคุม
broadcast server ที่รันได้จริง
หัวข้อที่มีชื่อว่า “broadcast server ที่รันได้จริง”เดโมด้านล่าง implement room บน ws server จริง client ส่ง {"type":"join","room":"x"} เพื่อ join จากนั้น {"type":"say","text":"hi"} เพื่อ broadcast ไปยังทุกคนใน room ของตัวเอง การ cleanup ตอน close จะลบ socket ออกจาก room ของตัวเองเพื่อให้คุณไม่ broadcast ไปหา ghost เปิดใน StackBlitz แล้วเชื่อมต่อ client สองสามตัวเข้า room เดียวกัน
Needs the Node.js runtime — open in StackBlitz to run.
เมื่อมี client สองตัว join เข้า room เดียวกัน say จากตัวหนึ่งจะไปถึงอีกตัว แต่ไม่กลับไปหาผู้ส่ง (เราส่ง ws เป็น argument except) client ตัวที่สามใน room อื่นไม่ได้ยินอะไรเลย การ fan-out แบบเจาะจงนั้น — หนุนด้วย Map ของ Set และการ cleanup ที่มีวินัย — คือเครื่องยนต์แกนกลางของทุกฟีเจอร์แชท, presence, และ live-collaboration ที่คุณจะสร้าง
| Broadcast Pattern | ส่งถึง | Use Case |
|---|---|---|
| Global broadcast | ทุก connection | System announcement, maintenance notice |
| Room broadcast | connection ใน room เดียวกัน | Chat channel, game room, collaborative document |
| Direct message | connection เดียว | Private message, personal notification |
| Selective broadcast | connection ที่ match criteria | Geographic region, subscription tier |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”Broadcast ทุก Message ไปทุก Connection อาการ:
- server loop ผ่านทุก connection ทุกครั้งที่มี message
- 10,000 connection × 100 message/s = 1M iteration/s ที่ไม่จำเป็น
- ใช้ room/channel pattern — ส่งเฉพาะ connection ที่ subscribe
Room ที่ไม่ Cleanup เมื่อ User ออก อาการ:
- user disconnect แต่ยังอยู่ใน room set
- message broadcast ไปยัง connection ที่ dead — error ทุกครั้ง
- remove จาก room ใน
closeevent เสมอ
💡 ตัวอย่างจากของจริง
Discord:
- room = channel — message broadcast เฉพาะ member ที่ join channel
- voice room — audio mix เฉพาะ user ใน voice channel เดียวกัน
Figma:
- room = document — cursor, selection broadcast เฉพาะ user ที่เปิด document เดียวกัน
- user ออกจาก document → remove จาก room อัตโนมัติ