A WebSocket Server
ถึงเวลาเขียน server จริงสักตัว ใน Node ไลบรารีโดยพฤตินัยคือ ws — เล็ก, เร็ว, และใกล้ชิดกับโปรโตคอล บทเรียนนี้จะพาเดินผ่านสามชิ้นส่วนหลักของตัวเอง: การสร้าง server, connection event, และการเรียก message/send ต่อ socket
การสร้าง server
หัวข้อที่มีชื่อว่า “การสร้าง server”มีสองวิธีที่พบบ่อยในการตั้ง WebSocketServer ขึ้นมา วิธีแรกให้ ws เป็นเจ้าของ port โดยตรง:
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('listening', () => { console.log('listening on ws://localhost:8080');});นั่นเหมาะกับ WebSocket service แบบ standalone อย่างยิ่ง แต่แอปจริงส่วนใหญ่ให้บริการ HTTP อยู่แล้ว — REST API, health check, static files — และต้องการ WebSocket บน port เดียวกัน สำหรับกรณีนั้น คุณ attach ws เข้ากับ HTTP server ที่มีอยู่แล้วแทน:
import { createServer } from 'node:http';import { WebSocketServer } from 'ws';
const server = createServer((req, res) => { res.writeHead(200); res.end('ok'); // ordinary HTTP still works here});
// No port here — ws piggybacks on the HTTP server's upgrade events.const wss = new WebSocketServer({ server });
server.listen(8080);ความแตกต่างมีความสำคัญ: ด้วย { server }, HTTP server จะรับ Upgrade request แล้วส่งมอบ socket ให้ ws ดังนั้น HTTP และ WebSocket จึงใช้ listener ตัวเดียวร่วมกัน ใช้ { port } สำหรับ socket service เฉพาะทาง; ใช้ { server } (หรือ { noServer: true } เพื่อควบคุม upgrade ด้วยมือเต็มที่) เมื่อ WebSocket อยู่ร่วมกับแอป HTTP
flowchart TB
REQ["HTTP request<br/>Upgrade: websocket"] --> WSS["WebSocketServer"]
WSS -- "valid upgrade" --> CONN["connection event<br/>(ws, req)"]
CONN --> H1["ws.on('message', ...)"]
CONN --> H2["ws.send(...)"]
CONN --> H3["ws.on('close', ...)"]
H1 -- "frame arrives" --> APP["your routing logic"]
APP --> H2 connection event
หัวข้อที่มีชื่อว่า “connection event”ทุก upgrade ที่รับเข้ามาจะ fire connection event หนึ่งครั้ง callback ของตัวเองรับ socket ws ที่เพิ่งเชื่อมต่อมาสด ๆ และ upgrade request req ตัวต้นฉบับ:
wss.on('connection', (ws, req) => { // This runs ONCE per client. Everything here is scoped to that one socket. console.log('a client connected from', req.socket.remoteAddress); ws.send('welcome');});สองสิ่งที่ต้องซึมซับไว้ อย่างแรก handler นี้รัน หนึ่งครั้งต่อ client — ถ้ามีคนเชื่อมต่อร้อยคน handler ก็ fire ร้อยครั้ง แต่ละครั้งมี ws ต่างกัน อย่างที่สอง req คือ HTTP upgrade request นั่นคือที่ที่คุณอ่าน cookie, header, หรือ URL เพื่อหาว่า ใคร เพิ่งเชื่อมต่อเข้ามา (เราจะพึ่งสิ่งนี้ในบทเรียนถัดไป)
การรับและการส่ง
หัวข้อที่มีชื่อว่า “การรับและการส่ง”ภายใน connection handler, socket คือ event emitter ฟัง message เพื่อรับ frame และเรียก send เพื่อ push ข้อมูลกลับ:
wss.on('connection', (ws) => { ws.on('message', (data) => { // `data` is a Buffer (or array of Buffers). Convert it to text. const text = data.toString(); console.log('received:', text); ws.send(`you said: ${text}`); });
ws.on('close', (code) => console.log('client gone, code', code)); ws.on('error', (err) => console.error('socket error:', err.message));});จุดละเอียดอ่อนหนึ่งที่ทำให้มือใหม่สะดุด: ใน ws, payload ของ message มาถึงในรูปแบบ Buffer ไม่ใช่ string เพราะ frame สามารถพกข้อมูล binary ได้ เรียก .toString() เมื่อคุณคาดหวัง text (ถ้าคุณตั้ง { binary: false } หรือใช้ layer สำหรับ parse คุณสามารถเปลี่ยนพฤติกรรมนี้ได้ แต่ค่า default ดิบคือ byte)
รันของจริงสักตัว
หัวข้อที่มีชื่อว่า “รันของจริงสักตัว”เดโมด้านล่างคือ body ของ connection handler นั้น ที่รันบน Node ws server ของจริง เปิดใน StackBlitz จากนั้น npm run client เพื่อเชื่อมต่อ server จะทักทาย client แต่ละราย, echo ข้อความพร้อม counter, และรายงาน close code
Needs the Node.js runtime — open in StackBlitz to run.
ทุกข้อความที่คุณส่งกลับมาพร้อมหมายเลข — เป็นหลักฐานว่าตัวแปร count มีชีวิตอยู่ตลอดอายุของ connection หนึ่งนั้น ไม่ใช่ต่อข้อความ scope ต่อ socket นั้นแหละคือสิ่งที่บทเรียนถัดไปจะแปรเปลี่ยนเป็น state ต่อ connection ที่มีประโยชน์
| ข้อดี (WebSocket Server) | ข้อแลกเปลี่ยน |
|---|---|
| handle message ด้วย event loop — ไม่ block ต่อ connection | connection แต่ละตัวกิน memory — 10k connection = 10k state objects |
| event-driven — server ตอบสนองเฉพาะเมื่อมี event | long-lived connection ต้องมี health check mechanism |
| Node.js/ws ใช้ single thread handle concurrent connection ได้ | CPU-intensive work ต้องออก worker thread |
| integrate กับ HTTP server เดิมได้ — shared port | open file descriptor limit บน OS ต้องตั้งค่า |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”ทำ CPU-intensive Work ใน Message Handler อาการ:
- parse, validate, process message ซับซ้อนใน
ws.on('message', ...)โดยตรง - block event loop — connection อื่นรอด้วย
- ย้าย heavy work ไป worker thread หรือ job queue
ไม่ Set Max Connection Limit อาการ:
- server รับ connection ไม่จำกัด — memory หมดเมื่อ connection มาก
- OOM crash ทำให้ทุก user disconnect พร้อมกัน
- ตั้ง max connection และ reject ด้วย 503 เมื่อเกิน limit
💡 ตัวอย่างจากของจริง
Socket.IO:
- popular WebSocket library สำหรับ Node.js — abstracts server-side handling
- ใช้ใน Trello, Notion สำหรับ real-time collaboration
uWebSockets.js:
- high-performance WebSocket server ที่ Binance และ trading platform ใช้
- handle 1M+ concurrent connection บน hardware เดียว