Bạn vừa dựng xong một server chat bằng Socket.IO, rồi hăm hở viết client bằng new WebSocket("ws://localhost:3000") vì nghĩ "Socket.IO thì cũng là WebSocket thôi mà". Kết quả: client cứ treo ở trạng thái kết nối, hoặc nhận về mấy chuỗi khó hiểu kiểu 0{"sid":"..."} rồi bị ngắt. Lỗi này phổ biến đến mức gần như lập trình viên nào làm realtime cũng từng dính một lần.
Nguyên nhân nằm ở chỗ hai cái tên nghe giống nhau nhưng thuộc hai tầng khác hẳn: WebSocket là một giao thức chuẩn, còn Socket.IO là một thư viện xây bên trên. Bài này sẽ mổ xẻ từng lớp: chúng sinh ra để giải quyết chuyện gì, chạy thế nào, vì sao không nói chuyện được với nhau, và khi nào nên chọn cái nào.
Vì sao HTTP thôi là chưa đủ?
HTTP vận hành theo kiểu hỏi – đáp (request – response): trình duyệt gửi yêu cầu, máy chủ trả lời, xong là hết lượt. Máy chủ không có cách nào tự "gõ cửa" trình duyệt khi có tin nhắn mới, giá cổ phiếu vừa nhảy hay đơn hàng vừa đổi trạng thái.
Trước khi có WebSocket, người ta xoay xở bằng hai mẹo:
- Polling (hỏi định kỳ): cứ vài giây trình duyệt lại hỏi "có gì mới không?". Phần lớn câu trả lời là "không có gì", nhưng mỗi lần hỏi vẫn tốn nguyên một yêu cầu HTTP kèm hàng trăm byte header, cookie. Hỏi dày thì tốn băng thông và nặng máy chủ, hỏi thưa thì tin đến chậm.
- Long polling (hỏi rồi chờ): trình duyệt gửi yêu cầu, máy chủ giữ yêu cầu đó lại cho tới khi có dữ liệu mới mới trả lời, sau đó trình duyệt lập tức hỏi tiếp. Đỡ lãng phí hơn, nhưng vẫn là mỗi tin một vòng HTTP, và máy chủ phải giữ rất nhiều yêu cầu treo.
Hãy hình dung polling giống như cứ năm phút lại gọi điện hỏi shipper "tới chưa anh?", trong khi điều bạn thực sự cần là shipper tự gọi khi đến nơi.

WebSocket hoạt động thế nào?
WebSocket là giao thức được chuẩn hoá trong tài liệu RFC 6455 năm 2011. Nó mở một kết nối hai chiều, luôn mở (full-duplex) giữa trình duyệt và máy chủ trên một kết nối TCP duy nhất. Ai có dữ liệu thì gửi, không cần chờ bên kia hỏi.
Bắt tay bằng HTTP rồi "lột xác"
Kết nối bắt đầu bằng một yêu cầu HTTP bình thường, chỉ khác là có header Upgrade:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Nếu đồng ý, máy chủ trả mã 101 Switching Protocols kèm Sec-WebSocket-Accept (được tính từ key của client để chứng minh nó thực sự hiểu WebSocket). Từ giây phút đó, hai bên không nói HTTP nữa mà trao đổi bằng các khung dữ liệu (frame). Địa chỉ dùng tiền tố ws:// (cổng 80) hoặc bản mã hoá wss:// (cổng 443).
Vì sao WebSocket nhẹ đến vậy?
Mỗi frame chỉ có phần tiêu đề từ 2 đến 14 byte: 2 byte cơ bản, thêm vài byte độ dài nếu gói lớn, và 4 byte mã che (masking key) với frame đi từ client lên. So với hàng trăm byte header của mỗi yêu cầu HTTP, đây là lý do WebSocket gần như không tốn gì thêm cho mỗi tin và độ trễ rất thấp.

Phía trình duyệt, API có sẵn và rất gọn:
const ws = new WebSocket("wss://example.com/chat");
ws.onopen = () => ws.send("xin chào");
ws.onmessage = (e) => console.log("nhận:", e.data);
ws.onclose = () => console.log("mất kết nối");
Cái giá của sự tối giản
WebSocket thuần chỉ lo đúng một việc là chuyển byte qua lại. Mọi thứ còn lại bạn phải tự viết:
- Không tự nối lại: rớt Wi-Fi, đổi từ 4G sang Wi-Fi, server khởi động lại là kết nối chết hẳn.
- Không có sự kiện đặt tên: chỉ có một luồng
message, muốn phân biệt "tin chat" với "đang gõ phím" thì tự quy ước định dạng. - Không có phòng (room): muốn gửi tin cho đúng 20 người trong một nhóm thì tự quản lý danh sách kết nối.
- Không có xác nhận đã nhận (acknowledgement): gửi đi rồi không biết bên kia đã xử lý chưa.

Socket.IO thêm gì lên trên WebSocket?
Socket.IO là một thư viện gồm cả phần client và phần server (bản server chính thức viết cho Node.js, ngoài ra có các bản cộng đồng cho Python, Java, Go...). Nó dùng WebSocket làm đường truyền chính, nhưng bọc thêm một lớp giao thức riêng để lấp những chỗ trống kể trên.
Cơ chế dự phòng: không có đường cao tốc thì đi đường làng
Bên dưới Socket.IO là tầng Engine.IO, phụ trách việc giữ kết nối. Mặc định nó mở kết nối bằng HTTP long polling trước, rồi mới thử nâng cấp lên WebSocket. Nếu proxy công ty, tường lửa hay mạng khách sạn chặn WebSocket, kết nối vẫn chạy được bằng long polling. Người dùng chỉ thấy app chậm hơn chút, không thấy app chết.

Bộ đồ nghề có sẵn
- Tự động kết nối lại, có độ trễ tăng dần để không dội bom server.
- Sự kiện có tên:
socket.emit("chat", data)vàsocket.on("chat", ...). - Phòng:
socket.join("nhom-1")rồiio.to("nhom-1").emit(...)để gửi cho cả nhóm. - Namespace: tách kênh trên cùng một kết nối, ví dụ
/adminvà/chat. - Acknowledgement: truyền thêm một callback để biết chắc bên kia đã nhận.
- Heartbeat: gói ping/pong định kỳ (mặc định 25 giây một lần) để phát hiện kết nối "chết lâm sàng".
// server
io.on("connection", (socket) => {
socket.join("phong-1");
socket.on("chat", (msg, ack) => {
io.to("phong-1").emit("chat", msg);
ack("đã nhận");
});
});
Cái bẫy lớn nhất: hai bên không cùng "chuẩn phích cắm"
Đây là chỗ nhiều người vấp nhất. Socket.IO không phải là một bản cài đặt WebSocket. Mỗi gói tin của nó có định dạng riêng: mã loại gói của Engine.IO, mã loại gói của Socket.IO, rồi mới tới tên sự kiện và dữ liệu dạng JSON. Một tin chat trên dây trông như thế này:
42["chat","xin chào"]
Số 4 là "message" của Engine.IO, số 2 là "event" của Socket.IO. Client WebSocket thuần không biết những con số đó nghĩa là gì, cũng không biết phải trả lời ping của server. Kết quả:
- Client WebSocket thuần gọi server Socket.IO: không kết nối được, hoặc kết nối xong nhận gói lạ rồi bị ngắt vì không trả lời đúng giao thức.
- Client Socket.IO gọi server WebSocket thuần: client gửi yêu cầu tới đường dẫn
/socket.io/?EIO=4&transport=polling, server không hiểu, trả lỗi, client cứ thế thử lại mãi.

Giống như phích cắm chuẩn Mỹ và ổ cắm chuẩn châu Âu: cùng là điện, nhưng không cắm vừa nhau. Muốn nói chuyện thì hai đầu phải cùng dùng Socket.IO, hoặc cùng dùng WebSocket thuần.
Tab Network của trình duyệt liên tục có yêu cầu tới /socket.io/... trả về 404, hoặc kết nối WebSocket mở rồi đóng ngay với gói đầu tiên bắt đầu bằng số 0. Kiểm tra lại xem client và server có dùng cùng một thứ không.
Hiệu năng, dung lượng và khi mở rộng nhiều máy chủ
Overhead trên dây
Vì mỗi tin Socket.IO kèm thêm mã gói và tên sự kiện, cộng với nhịp ping/pong định kỳ, nên nó nặng hơn WebSocket thuần một chút. Với ứng dụng chat vài tin mỗi giây, khác biệt này không đáng kể. Với hệ thống đẩy hàng nghìn tin nhỏ mỗi giây, vài byte mỗi tin cộng dồn lại thành con số đáng để ý.
Dung lượng phía client
WebSocket có sẵn trong mọi trình duyệt hiện đại, không phải tải gì thêm. Thư viện client của Socket.IO nặng hơn 10 KB sau khi nén, đổi lại bạn có đủ tính năng ở trên mà không phải tự viết.
Khi chạy nhiều máy chủ
Một máy chủ chỉ biết các kết nối của chính nó. Khi mở rộng ra nhiều máy:
- Socket.IO cần một adapter, phổ biến nhất là Redis adapter, để một lệnh
io.to("phong-1").emit()đi tới được người dùng đang nối vào máy khác. - Nếu còn bật long polling, cần sticky session ở load balancer: các yêu cầu polling của cùng một client phải về đúng một máy, nếu không phiên sẽ bị lỗi.
- WebSocket thuần thì không có sẵn gì: bạn tự dựng lớp pub/sub (Redis, NATS...) để phát tin chéo giữa các máy.
Nếu bạn chắc chắn mạng của người dùng không chặn WebSocket, có thể cấu hình client Socket.IO với transports: ["websocket"] để bỏ qua long polling. Khi đó không cần sticky session nữa, nhưng bạn cũng mất luôn cơ chế dự phòng.
| Tiêu chí | WebSocket thuần | Socket.IO |
|---|---|---|
| Bản chất | Giao thức chuẩn (RFC 6455) | Thư viện client + server |
| Overhead mỗi tin | 2–14 byte tiêu đề frame | Thêm mã gói, tên sự kiện, ping/pong |
| Dung lượng client | 0 (có sẵn trong trình duyệt) | Hơn 10 KB sau nén |
| Tự nối lại | Không | Có |
| Phòng, namespace, ack | Tự viết | Có sẵn |
| Dự phòng khi bị chặn | Không | Lùi về HTTP long polling |
| Tương thích chéo ngôn ngữ | Mọi ngôn ngữ đều có thư viện chuẩn | Phải dùng bản Socket.IO tương ứng |
| Mở rộng nhiều máy | Tự dựng pub/sub | Adapter (Redis...) + sticky session |
Nên chọn WebSocket thuần hay Socket.IO?
Chọn WebSocket thuần khi
- Cần hiệu năng tối đa, độ trễ thấp nhất: sàn giao dịch, bảng giá realtime, game nhiều người chơi gửi vị trí liên tục.
- Kết nối với hệ thống viết bằng ngôn ngữ khác hoặc dịch vụ bên thứ ba: hầu hết API realtime công khai nói WebSocket chuẩn, không nói Socket.IO.
- Thiết bị IoT tài nguyên hạn chế, cần gửi dữ liệu cảm biến đều đặn với chi phí nhỏ nhất.


Chọn Socket.IO khi
- Làm ứng dụng chat, thông báo, bảng điều khiển cộng tác, nơi phòng, sự kiện và xác nhận dùng hằng ngày.
- Người dùng có thể ở mạng khó: mạng công ty, khách sạn, proxy chặn WebSocket.
- Cả client lẫn server đều dùng JavaScript và bạn cần ra sản phẩm nhanh, ổn định, không muốn tự viết lại phần nối lại hay quản lý phòng.

Trước khi chọn, tự hỏi: "Mình có định tự viết lại cơ chế nối lại, phòng và xác nhận không?". Nếu có và bạn cần kiểm soát từng byte, chọn WebSocket thuần. Nếu không, Socket.IO sẽ tiết kiệm cho bạn nhiều tuần công.
WebSocket là con đường, còn Socket.IO là chiếc xe chạy trên con đường đó, kèm sẵn bản đồ và phụ tùng.
Tóm tắt nhanh
- WebSocket là giao thức chuẩn (RFC 6455, 2011), mở kết nối hai chiều luôn mở, frame chỉ 2–14 byte tiêu đề.
- Socket.IO là thư viện dùng WebSocket làm đường truyền chính, thêm tự nối lại, sự kiện có tên, phòng, namespace, ack và dự phòng long polling.
- Hai thứ không tương thích trực tiếp: client WebSocket thuần không nói chuyện được với server Socket.IO và ngược lại.
- Socket.IO nặng hơn một chút trên dây và phía client, đổi lại đỡ rất nhiều công viết code.
- Hiệu năng tối đa, đa ngôn ngữ, IoT: WebSocket thuần. Chat, thông báo, cộng tác, mạng khó: Socket.IO.
Câu hỏi thường gặp
Socket.IO có phải là WebSocket không?
Không. Socket.IO dùng WebSocket làm đường truyền khi có thể, nhưng có giao thức riêng bọc bên trên, nên chỉ client Socket.IO mới nói chuyện được với server Socket.IO.
Trình duyệt bây giờ đều hỗ trợ WebSocket, vậy cơ chế dự phòng còn cần không?
Trình duyệt thì hỗ trợ, nhưng mạng ở giữa (proxy công ty, tường lửa, một số mạng công cộng) vẫn có thể chặn. Dự phòng long polling giúp app không chết hẳn trong các trường hợp đó.
Server viết bằng Python hay Go có dùng được Socket.IO không?
Được, có các bản server Socket.IO do cộng đồng phát triển cho nhiều ngôn ngữ. Tuy vậy, nếu hệ thống của bạn cần kết nối với nhiều dịch vụ khác nhau, WebSocket chuẩn vẫn là "ngôn ngữ chung" dễ tích hợp hơn.
WebSocket thuần có cơ chế ping/pong không?
Có, giao thức WebSocket có frame điều khiển ping và pong, nhưng API WebSocket trong trình duyệt không cho JavaScript tự gửi ping. Muốn phát hiện kết nối chết hay tự nối lại ở phía client, bạn vẫn phải tự viết logic.



