Network Programming · Part 6 — WebSockets & Real-Time Communication
WebSockets for real-time push: HTTP limits, the upgrade handshake, ws server + client in Node.js + TypeScript, heartbeats, and when to use SSE/polling instead — bilingual with exercises.
Đây là Phần 6 của series 10 bài về lập trình mạng với Node.js + TypeScript. Phần 1–5 đã xây stack — TCP, UDP, DNS, và HTTP. Giờ ta xử lý real-time: server đẩy dữ liệu tới client mà không cần chờ request.
HTTP tuyệt cho tài liệu và API, nhưng nó là request/response và do client khởi tạo. App chat, dashboard live, hoặc game multiplayer cần server nói “có tin mới” bất cứ khi nào nó xảy ra. WebSocket giải quyết bằng một kết nối bền vững, full-duplex.
Vấn đề HTTP không giải quyết được
Ở Phần 5 bạn xây server HTTP: client gửi request, server trả lời, và (trừ keep-alive) trao đổi kết thúc. Để server push, dev từng vá tạm bằng:
- Polling — client hỏi “có gì mới?” mỗi N giây.
- Long-polling — client giữ request mở đến khi server có dữ liệu.
- SSE — một luồng HTTP, chỉ server → client.
Tất cả vẫn bắt đầu từ ngữ nghĩa HTTP. WebSocket nâng cấp cùng kết nối TCP thành giao thức frame nhị phân/text mà cả hai bên gửi bất cứ lúc nào.
Ý chính: một socket TCP, một bắt tay nâng cấp HTTP, rồi kênh message nhẹ — không phải request mới mỗi tin nhắn.
Bắt tay nâng cấp WebSocket
WebSocket không mở port ngẫu nhiên. Bắt đầu như HTTP trên cùng kết nối (thường 80/443 sau reverse proxy). Client gửi GET thường với header đặc biệt yêu cầu đổi giao thức. Server từ chối (giữ HTTP) hoặc trả 101 Switching Protocols và từ đó cả hai nói frame WebSocket — không còn HTTP.
Request client (HTTP đầu và cuối trên socket này):
GET /chat HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Response server (nếu chấp nhận nâng cấp):
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Key là nonce ngẫu nhiên; Sec-WebSocket-Accept là hash client xác minh để proxy HTTP ngẫu nhiên không “nâng cấp” nhầm. Sau 101, byte trên dây là frame (opcode, bit mask, độ dài payload) — thư viện như ws lo giúp bạn.
Bạn có thể tự implement bắt tay và parser frame trên node:http + node:net (học tốt, mệt). Trong production, ws là lựa chọn thực tế cho Node.
Server WebSocket với ws
Cài thư viện một lần:
npm install ws
npm install -D @types/ws
Server broadcast kiểu chat tối thiểu — mọi message tới mọi client đang kết nối:
import { WebSocketServer, WebSocket } from 'ws';
const PORT = 8080;
const wss = new WebSocketServer({ port: PORT });
const clients = new Set<WebSocket>();
wss.on('connection', (ws, req) => {
console.log('upgrade from', req.socket.remoteAddress);
clients.add(ws);
ws.on('message', (data) => {
const text = data.toString();
console.log('received:', text);
// Broadcast to every other open socket
for (const client of clients) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(text);
}
}
});
ws.on('close', () => {
clients.delete(ws);
console.log('client disconnected, remaining:', clients.size);
});
ws.send(JSON.stringify({ type: 'welcome', message: 'connected to chat' }));
});
console.log(`WebSocket server listening on ws://localhost:${PORT}`);
Chạy bằng npx tsx ws-server.ts (hoặc compile trước). Sự kiện chính:
- bắn sau khi nâng cấp HTTP thành công.
- payload frame WebSocket hoàn chỉnh (text hoặc binary).
- đẩy từ server tới client bất cứ lúc nào.
- chỉ gửi khi socket đang mở.
Client trình duyệt
Trình duyệt có WebSocket sẵn — không cần package ws. Lưu client.html và mở khi server đang chạy:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8" /><title>WS demo</title></head>
<body>
<input id="msg" placeholder="type a message" />
<button id="send">Send</button>
<ul id="log"></ul>
<script>
const log = document.getElementById('log');
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => append('connected');
ws.onmessage = (ev) => append('← ' + ev.data);
ws.onclose = () => append('disconnected');
document.getElementById('send').onclick = () => {
const input = document.getElementById('msg');
ws.send(input.value);
append('→ ' + input.value);
input.value = '';
};
function append(text) {
const li = document.createElement('li');
li.textContent = text;
log.appendChild(li);
}
</script>
</body>
</html>
Mở hai tab — tin gõ ở tab này hiện ở tab kia. Đó là server push full-duplex không cần polling.
Client Node.js (test & script)
Package ws cũng có client cho integration test và CLI:
import WebSocket from 'ws';
const ws = new WebSocket('ws://localhost:8080');
ws.on('open', () => {
console.log('connected');
ws.send('hello from node client');
});
ws.on('message', (data) => {
console.log('server said:', data.toString());
});
ws.on('close', (code, reason) => {
console.log('closed', code, reason.toString());
});
Dùng pattern này trong CI để assert server phát đúng sự kiện. Với WSS (TLS), chỉ truyền \{ rejectUnauthorized: false \} ở dev với cert tự ký — không bao giờ ở production.
Nhịp tim ping/pong
Kết nối TCP có thể chết im lặng — máy ngủ, NAT timeout, mất Wi-Fi — không có 'close' trong nhiều phút. WebSocket định nghĩa frame điều khiển ping/pong; ws có thể phát tự động:
import { WebSocketServer, WebSocket } from 'ws';
const wss = new WebSocketServer({
port: 8080,
perMessageDeflate: false,
});
const HEARTBEAT_MS = 30_000;
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', () => {
ws.isAlive = true;
});
ws.on('message', (data) => {
// handle app messages…
console.log('msg:', data.toString());
});
});
const interval = setInterval(() => {
for (const ws of wss.clients) {
if (!ws.isAlive) {
ws.terminate(); // dead connection — force close
continue;
}
ws.isAlive = false;
ws.ping();
}
}, HEARTBEAT_MS);
wss.on('close', () => clearInterval(interval));
Client dùng API WebSocket trình duyệt trả lời ping server tự động. Kết nối lại (backoff, khôi phục session, tin nhắn bỏ lỡ) là logic app — ta đi sâu ở Phần 10.
So sánh các lựa chọn real-time
| Approach | Direction | Connection | Complexity | Best when |
|---|---|---|---|---|
| Polling | Client pulls | New HTTP request each tick | Lowest | Rare updates, prototypes, legacy-only clients |
| Long-polling | Client pulls (held open) | HTTP request per event batch | Medium | Older browsers, simple “wait for update” |
| SSE | Server → client only | One long-lived HTTP stream | Medium | Live feeds, notifications, stock tickers (read-only push) |
| WebSocket | Full duplex | One upgraded TCP socket | Medium–high | Chat, games, collaborative editing, bidirectional RPC |
Quy tắc ngón tay cái: cần client → server và server → client độ trễ thấp? → WebSocket. chỉ cần server push qua HTTP thường? → SSE đơn giản hơn và chạy trên infra HTTP/2. Polling ổn khi cập nhật thưa và đơn giản quan trọng hơn độ trễ.
Lỗi người mới hay mắc
- Cố push từ server qua HTTP bằng
res.write()trên request thường — proxy và client mong response hoàn chỉnh; dùng WebSocket hoặc SSE. - Không heartbeat — kết nối TCP chết nằm trong
clientsđến khi thứ khác lỗi. - Không auth khi nâng cấp — ai hit
ws://host/chatđều vào; validate cookie/JWT lúc'connection'(hoặc từ chối nâng cấp trongverifyClient). - Tưởng giao hàng qua reconnect — WebSocket tin cậy khi đang kết nối, nhưng disconnect mất trạng thái đang bay trừ khi thêm ID, ack, hoặc replay buffer (Phần 10).
Bài tập
Thử từng bài trước khi mở lời giải.
- Mở rộng server broadcast để người gửi không nhận lại tin của mình (gợi ý: đã bỏ qua
client !== ws— kiểm tra với hai client Node). - Thêm endpoint HTTP
/statuscùng port dùngwss+node:http— trả JSON\{ connections: number \}(đọc docswsvề optionserver). - Log mã đóng khi client ngắt; tra nghĩa
1000vs1006trong RFC.
Lời giải
import { createServer } from 'node:http';
import { WebSocketServer, WebSocket } from 'ws';
const clients = new Set<WebSocket>();
const httpServer = createServer((req, res) => {
if (req.url === '/status') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ connections: clients.size }));
return;
}
res.writeHead(404).end();
});
const wss = new WebSocketServer({ server: httpServer });
wss.on('connection', (ws) => {
clients.add(ws);
ws.on('message', (data) => {
const text = data.toString();
for (const client of clients) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(text);
}
}
});
ws.on('close', (code, reason) => {
clients.delete(ws);
// 1000 = normal closure; 1006 = abnormal (no close frame — often network drop)
console.log('closed', code, reason.toString());
});
});
httpServer.listen(8080, () => {
console.log('HTTP + WS on http://localhost:8080');
});Bài 1: kiểm tra client !== ws loại socket gốc — chạy hai client ws; chỉ peer nhận broadcast. Bài 2: gắn WebSocketServer vào http.Server có sẵn là cách dùng chung port 8080 cho HTTP và nâng cấp WS. Bài 3: 1000 là đóng sạch hai bên đồng ý; 1006 là TCP mất mà không có bắt tay đóng WebSocket.
Điều cốt lõi
HTTP là kéo; app real-time cần đẩy. WebSocket nâng cấp một kết nối TCP từ HTTP thành kênh frame bền vững, full-duplex — client và server gửi bất cứ lúc nào. Dùng ws ở Node, WebSocket native ở trình duyệt, thêm ping/pong để dọn peer chết, và chọn SSE hoặc polling khi không cần hai chiều. Tiếp theo Phần 7: TLS — mã hóa mọi thứ ta đã xây.