12/04/2026 18:16น.

EP.120 Whiteboard & Real-time Drawing Synchronization ด้วย WebSocket
#WebSocket
#Golang The Series
#Golang
#Go
#whiteboard
หลังจากเราเรียนรู้การทำ Collaborative Document Editing (แบบตัวอักษร) มาแล้ว ตอนนี้ถึงเวลาขยับไปสู่สิ่งที่ "ท้าทายกว่า" การวาดรูปแบบเรียลไทม์ (Real-time Drawing)
Whiteboard App อย่าง Miro, FigJam หรือ Excalidraw จำเป็นต้องรองรับการทำงานที่ซับซ้อน เช่น
- ผู้ใช้หลายคนวาดพร้อมกัน
- การเคลื่อนไหวของเส้น รูปร่าง และ cursor แบบเรียลไทม์
- Canvas ที่ต้อง sync เหมือนกันทุกคน
- ระบบ Undo / Redo ที่ทำงานทันที
- ความหน่วง (Latency) ต้องต่ำมาก
บทความนี้จะพาคุณออกแบบ Real-time Whiteboard System โดยใช้ Go + WebSocket เป็นแกนกลาง พร้อมแนวคิดที่สามารถนำไปใช้ได้จริงใน Production
🧠 ภาพรวม Architecture
Client (Canvas)
↕ WebSocket
Whiteboard Server (Go)
↕ Broadcast
Clients ทุกคนใน board เดียวกัน
องค์ประกอบหลัก
- Canvas State – สถานะปัจจุบันของกระดาน
- Draw Events – การวาดเส้นหรือรูปร่าง
- Cursor Position – ตำแหน่งเมาส์ของแต่ละคน
- History Stack – สำหรับ Undo / Redo
- Room (Board) – แยกการวาดตาม
board_id
✏️ 1. รูปแบบข้อมูลสำหรับ Drawing Event
เราไม่ส่งทั้ง Canvas ทุกครั้ง แต่ส่งเฉพาะ Draw Event
{
"board_id": "board-001",
"user_id": "user-a",
"type": "draw",
"tool": "pen",
"points": [
{ "x": 120, "y": 240 },
{ "x": 122, "y": 245 }
],
"color": "#FF0000",
"stroke": 2
}
ประเภท Event ที่รองรับ:
draw(pen/brush)shape(rectangle, circle, arrow)cursorundo,redoclear
⚙️ 2. โครงสร้างข้อมูลฝั่ง Server (Go)
type DrawEvent struct {
BoardID string `json:"board_id"`
UserID string `json:"user_id"`
Type string `json:"type"`
Tool string `json:"tool"`
Points []Point `json:"points,omitempty"`
Color string `json:"color,omitempty"`
Stroke int `json:"stroke,omitempty"`
}
type Point struct {
X float64 `json:"x"`
Y float64 `json:"y"`
}
จัดการ Room แยกตาม board_id เพื่อให้ส่งเฉพาะกลุ่มที่ใช้งานร่วมกัน
🔄 3. Real-time Sync ด้วย WebSocket
var boards = make(map[string]map[*websocket.Conn]bool)
func handleWhiteboard(conn *websocket.Conn, boardID string) {
if boards[boardID] == nil {
boards[boardID] = make(map[*websocket.Conn]bool)
}
boards[boardID][conn] = true
defer func() {
delete(boards[boardID], conn)
conn.Close()
}()
for {
var event DrawEvent
if err := conn.ReadJSON(&event); err != nil {
return
}
broadcast(boardID, event)
}
}
func broadcast(boardID string, event DrawEvent) {
for c := range boards[boardID] {
c.WriteJSON(event)
}
}
ผลลัพธ์: ผู้ใช้ A วาด → ทุกคนเห็นทันที
Latency ต่ำ (ระดับ millisecond)
🖱️ 4. Mouse / Cursor Sync
เพื่อแสดงว่า “ใครกำลังทำอะไรอยู่บนกระดาน”
{
"type": "cursor",
"user_id": "user-b",
"x": 450,
"y": 300
}
- Client ส่งตำแหน่งเมาส์ทุก ~50ms
- Server broadcast → ทุก client แสดงตำแหน่ง cursor
↩️ 5. Undo / Redo แบบเรียลไทม์
แนวคิด:
- ทุก
drawevent = 1 action - Stack per board (
Undo,Redo)
type BoardHistory struct {
Undo []DrawEvent
Redo []DrawEvent
}
เมื่อ Undo:
- pop จาก undo
- push เข้า redo
- broadcast event undo → ทุก client ลบ action ล่าสุด
⚡ 6. Performance Optimization
สิ่งที่ต้องคำนึงถึงเมื่อระบบมีผู้ใช้งานจำนวนมาก:
- ส่งเฉพาะ delta (ไม่ส่งทั้งเส้น)
- Throttle mouse event (30–60 fps)
- Batch event ที่เกิดถี่ ๆ
- ใช้ Binary Protocol สำหรับ board ขนาดใหญ่
- แยก layer canvas (background / drawing / cursor)
🔐 7. Security & Access Control
ระบบควรมี:
- Board Permission (read / write)
- Rate Limit event ต่อวินาที
- Validate Shape Data (กัน payload แปลก)
- ป้องกัน flood draw / spam
🚀 ท้าให้ลอง!
Mini Project:
- วาดพร้อมกัน 2–3 คน
- Sync cursor แบบ real-time
- Undo / Redo ทันที
- เปิดหลาย tab → ต้อง sync กันเป๊ะ!
✅ ถ้าทำได้ครบ = คุณเข้าใจระบบ Real-time Drawing อย่างแท้จริง 🎯
🔮 EP ถัดไป: EP.121 Deploy WebSocket Server บน Kubernetes
ตอนหน้า เราจะนำ WebSocket Server ของคุณเข้าสู่โลก Production จริงจัง
คุณจะได้เรียนรู้:
- การ Deploy ด้วย Kubernetes
- Load Balancer และ Sticky Session
- การรองรับ Auto Scaling สำหรับ WebSocket