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

A WebSocket Server

ถึงเวลาเขียน server จริงสักตัว ใน Node ไลบรารีโดยพฤตินัยคือ ws — เล็ก, เร็ว, และใกล้ชิดกับโปรโตคอล บทเรียนนี้จะพาเดินผ่านสามชิ้นส่วนหลักของตัวเอง: การสร้าง server, connection event, และการเรียก message/send ต่อ socket

มีสองวิธีที่พบบ่อยในการตั้ง 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 จาก upgrade request ไปสู่การจัดการข้อความ

ทุก 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

Node.js

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

ทุกข้อความที่คุณส่งกลับมาพร้อมหมายเลข — เป็นหลักฐานว่าตัวแปร count มีชีวิตอยู่ตลอดอายุของ connection หนึ่งนั้น ไม่ใช่ต่อข้อความ scope ต่อ socket นั้นแหละคือสิ่งที่บทเรียนถัดไปจะแปรเปลี่ยนเป็น state ต่อ connection ที่มีประโยชน์

ข้อดี (WebSocket Server)ข้อแลกเปลี่ยน
handle message ด้วย event loop — ไม่ block ต่อ connectionconnection แต่ละตัวกิน memory — 10k connection = 10k state objects
event-driven — server ตอบสนองเฉพาะเมื่อมี eventlong-lived connection ต้องมี health check mechanism
Node.js/ws ใช้ single thread handle concurrent connection ได้CPU-intensive work ต้องออก worker thread
integrate กับ HTTP server เดิมได้ — shared portopen 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 เดียว
เมื่อใดที่คุณควรสร้าง server ด้วย `new WebSocketServer({ server })` แทนที่จะเป็น `{ port }`?
callback ของ `connection` event รันบ่อยแค่ไหน?
ในไลบรารี `ws`, argument `data` ของ `message` event เป็น type อะไรโดย default?