Trong bài trước, chúng ta đã được tiếp cận giao thức WebSocket và hiểu cách thức hoạt động cũng như những hạn chế mà các lập trình viên cần phải đối mặt. Vì thế thư viện Socket.IO được ra đời để hỗ trợ mạnh mẽ giao thức WebSocket thuận tiện hơn cho lập trình viên trong việc xây dựng các ứng dụng thời gian thực.
1. SocketIO là gì?
Socket.IO
là một thư viện cho phép kết nối với độ trễ thấp, hai chiều và hoạt động trên hướng sự kiện giữa client và server.
Socket.IO is a library that enables low-latency, bidirectional and event-based communication between a client and a server.
Cre: https://socket.io/docs/v4/
Kết nối Socket.IO
có thể được thiết lập với các phương thức vận chuyển cấp thấp khác nhau:
- HTTP long-polling
- WebSocket
- WebTransport
Socket.IO
chọn loại vận chuyển có sẵn hợp lý nhất dựa trên:
- Hỗ trợ của trình duyệt (xem tại đây)
- Mạng (một số mạng chặn kết nối
WebSocket
hoặc WebTransport)
2. Những sai lầm về Socket.IO
Socket.IO không phải là một ứng dụng hoàn toàn của WebSocket.
Thực tế thì Socket.IO
vẫn sử dụng giao thức WebSocket
để kết nối khi có thể, tuy nhiên có bổ sung thêm metadata vào mỗi gói tin. Đó là lý do mà một Socket.IO client không thể kết nối đến WebSocket server và tương tự với trường hợp WebSocket client kết nối đến Socket.IO server.
// WARNING: the client will NOT be able to connect!
const socket = io("ws://echo.websocket.org");
Lưu ý
Socket.IO
không khuyến khích chạybackground
trên ứng dụng điện thoại.- Thư viện
Socket.IO
giữ cho kết nối TCP luôn mở đến server, điều này có thể tiêu thụ nhiều năng lượng pin trên di động người dùng. - Có thể sử dụng nền tảng nhắn tin chuyên dụng như FCM cho trường hợp sử dụng trên ứng dụng di động.
3. Tính năng của Socket.IO
Socket.IO hỗ trợ một bộ tính năng phong phú khiến nó trở thành lựa chọn phù hợp cho các nhà phát triển muốn xây dựng các ứng dụng yêu cầu cập nhật dữ liệu tức thời. Hãy đi sâu vào một số tính năng quan trọng mà Socket.IO
cung cấp.
3.3.1 Dự phòng HTTP long-polling
Một trong những tính năng nổi bật của Socket.IO là cơ chế dự phòng tuyệt vời của nó. Trong các trường hợp không hỗ trợ kết nối WebSocket, chẳng hạn như trong các trình duyệt cũ hơn hoặc một số môi trường mạng nhất định, Socket.IO tự động trở về sử dụng các kỹ thuật như HTTP long-polling.
Điều này liên quan đến việc thực hiện các yêu cầu HTTP lặp đi lặp lại tới máy chủ, giữ kết nối mở cho đến khi có dữ liệu mới. Việc này đảm bảo rằng chức năng thời gian thực vẫn còn nguyên vẹn ngay cả trong điều kiện kém thuận lợi hơn khi mà trước đây chưa có nhiều trình duyệt hỗ trợ cho WebSocket
.
Ngay cả khi hầu hết các trình duyệt hiện nay đều hỗ trợ WebSocket
(hơn 97%), đây vẫn là một tính năng tuyệt vời vì chúng tôi vẫn nhận được báo cáo từ người dùng rằng không thể thiết lập kết nối WebSocket
vì họ sử dụng một số proxy bị định cấu hình sai.
3.3.2 Tự động kết nối lại
Mạng vốn không đáng tin cậy và kết nối có thể bị mất vì nhiều lý do - có thể là trục trặc nhất thời hoặc mất kết nối Internet tạm thời. Socket.IO nổi bật lên trong những tình huống như vậy bằng cách cung cấp tính năng tự động kết nối lại.
Trong một số điều kiện cụ thể, kết nối WebSocket
giữa server và client có thể bị gián đoạn mà cả hai bên đều không biết về trạng thái liên kết bị hỏng. Khi mất kết nối, Socket.IO sẽ liên tục cố gắng thiết lập lại kết nối, đảm bảo rằng người dùng gặp phải sự gián đoạn tối thiểu.
Đó là lý do tại sao Socket.IO
bao gồm cơ chế heartbeat
, kiểm tra định kỳ trạng thái kết nối. Và khi client cuối cùng bị ngắt kết nối, nó sẽ tự động kết nối lại với độ trễ chờ tăng dần theo theo cấp số nhân, để không làm server bị quá tải.
3.3.3 Packet buffering
Socket.IO được trang bị cơ chế tích hợp để lưu vào bộ đệm các tin nhắn. Điều này có nghĩa là ngay cả khi máy khách tạm thời mất kết nối, Các gói được tự động lưu vào bộ đệm khi client bị ngắt kết nối và sẽ được gửi tự động khi kết nối được thiết lập lại.
Tính năng này đảm bảo rằng không có dữ liệu nào bị mất do sự cố mạng tạm thời.Mặc dù hữu ích trong hầu hết các trường hợp (khi độ trễ kết nối lại ngắn), nhưng nó có thể dẫn đến một lượng lớn các sự kiện khi kết nối được khôi phục.
Có một số giải pháp để ngăn chặn hành vi ngoài ý muốn này, tùy thuộc vào trường hợp sử dụng của bạn:
- Sử dụng thuộc tính
connected
của phiên bản Socket - Sử dụng
volatile events
if (socket.connected) {
socket.emit( /* ... */ );
} else {
// ...
}
socket.volatile.emit( /* ... */ );
3.3.4 Acknowledgements
Tín hiệu xác nhận là một khía cạnh thiết yếu của giao tiếp thời gian thực. Chúng cho phép máy chủ xác nhận rằng tin nhắn đã được khách hàng nhận hoặc ngược lại.
Socket.IO tạo điều kiện thuận lợi cho việc này bằng cách cho phép các nhà phát triển gắn các hàm gọi lại vào các sự kiện được phát ra. Điều này trao quyền cho các nhà phát triển xây dựng các hệ thống mạnh mẽ trong đó tính toàn vẹn và độ tin cậy của dữ liệu là tối quan trọng.
Socket.IO
cung cấp một phương thức thuận tiện để gửi sự kiện và nhận phản hồi:
Sender
socket.emit("hello", "world", (response) => { console.log(response); // "got it"});
Receiver
Ngoài ra còn có thể hỗ trợ thêm độ trễ Timeout
socket.timeout(5000).emit("hello", "world", (err, response) => { if (err) { // the other side did not acknowledge the event in the given delay } else { console.log(response); // "got it" }});
3.3.5 Broadcasting
Socket.IO giúp dễ dàng truyền phát tin nhắn tới nhiều khách hàng cùng một lúc. Điều này rất có giá trị trong các tình huống mà nhiều người dùng cần được cập nhật theo thời gian thực, chẳng hạn như trong ứng dụng trò chuyện hoặc bảng điều khiển trực tiếp.
Việc phát sóng đảm bảo rằng các thông điệp được gửi đến tất cả các bên liên quan một cách hiệu quả. Từ phía server, bạn có thể gửi tin nhắn / sự kiện đến tất cả các client hoặc một nhóm client được chỉ định. Ví dụ:
// to all connected clients
io.emit("hello");
// to all connected clients in the "news" room
io.to("news").emit("hello");
3.3.6 Multiplexing
Trong các ứng dụng phức tạp, khi có nhiều kênh liên lạc giữa máy khách và máy chủ Socket.IO hỗ trợ khái niệm ghép kênh. Cho phép tạo nhiều không gian tên độc lập trên một kết nối. Điều này cho phép các nhà phát triển tổ chức và quản lý các loại giao tiếp khác nhau trong một ứng dụng cách liền mạch.
Khái niệm namespace
cho phép bạn phân chia logic của ứng dụng chỉ trong 1 kết nối. Điều này hữu ích cho trường hợp bạn phân quyền admin cho những người dùng được uỷ quyền.
io.on("connection", (socket) => {
// classic users
});
io.of("/admin").on("connection", (socket) => {
// admin users
});
4. Top 10 lý do nên sử dụng Socket.IO
Sau khi đã tìm hiểu cơ bản về Socket.IO
thì sau đây là những lý do bạn cân nhắc sử dụng thư viện Socket.IO
4.1 Giao tiếp thời gian thực
Socket.IO
cho phép giao tiếp thời gian thực giữa client và server, giúp các ứng dụng cập nhật dữ liệu ngay khi có sự thay đổi mà không cần phải tải lại trang.
4.2 Hỗ trợ cho nhiều trình duyệt
Socket.IO
cung cấp một lớp trừu tượng để xử lý các sự kiện và gửi dữ liệu, giúp đảm bảo tương thích với nhiều trình duyệt khác nhau.
4.3 Thiết lập kết nối dễ dàng
Socket.IO
tự động xử lý việc thiết lập kết nối giữa client và server, giúp giảm đi sự phức tạp trong việc cấu hình và quản lý kết nối.
const { Server } = require("socket.io");
const io = new Server(3000, { /* options */ });
io.on("connection", (socket) => {
// ...
});
4.4 Fallback options
Socket.IO
có khả năng tự động chuyển đổi về các phương thức truyền thông thay thế (polling, long-polling) nếu không được hỗ trợ WebSocket
, đảm bảo tính ổn định trên môi trường không hỗ trợ WebSocket
. Khắc phục được hạn chế của WebSocket
.
4.5 Giao tiếp event-based
Socket.IO
sử dụng mô hình gửi và nhận các sự kiện (event-based communication), giúp tổ chức và xử lý dữ liệu dễ dàng.
4.6 Room
Nếu bạn muốn phát triển một ứng dụng chat hoặc tương tự, hãy cân nhắc về Room
của Socket.IO
.
Room
là một kênh bất kì mà socket
có thể join và leave. Được dùng để phát sóng các sự kiện tới tập hợp client chỉ định.
Chú ý: Room
là khái niệm chỉ ở phía server. Ví dụ client không thể truy cập vào danh sách room
đã tham gia.
- Joining và leaving
Bạn có thể gọi join
để đăng ký socket tới một kênh nhất định:
io.on("connection", (socket) => {
socket.join("some room");
});
Sau đó bạn sử dụng to
hoặc in
(cả 2 như nhau) để phát sóng hoặc phát động sự kiện:
io.to("some room").emit("some event");
Bạn cũng có thể phát sóng theo kiểu loại trừ một room
:
io.except("some room").emit("some event");
Bạn cũng có thể phát động nhiều sự kiệu tới nhiều room
cùng một lúc:
io.to("room1").to("room2").to("room3").emit("some event");
Trong trường hợp này sẽ không có sự trùng lặp từ phía nhận, nghĩa là một hội (union) sẽ được thực hiện: với mỗi socket ở trong một room
được gửi hoặc nhiều hơn một room
thì cũng sẽ chỉ nhận được sự kiện này một lần.
Bạn cũng có thể phát sóng tới một room
bằng một socket chỉ định:
io.on("connection", (socket) => {
socket.to("some room").emit("some event");
});
Để rời khỏi một kênh, bạn có thể leave
theo cùng một cách của join
.
- Áp dụng thực tế
Phát dữ liệu tới thiết bị/tab của người được chỉ định:
io.on("connection", async (socket) => {
const userId = await fetchUserId(socket);
socket.join(userId);
// and then later
io.to(userId).emit("hi");
});
Gửi thông báo tới một thực thể được chỉ định:
io.on("connection", async (socket) => {
const projects = await fetchProjects(socket);
projects.forEach(project => socket.join("project:" + project.id));
// and then later
io.to("project:4321").emit("project updated");
});
- Ngắt kết nối
Khi bị ngắt kết nối, socket tự động leave
tất cả kênh mà nó đã tham gia, không cần hành động gỡ bỏ đặc biệt nào từ bạn.
Bạn có thể nạp lại những room
mà socket từng tham gia bằng cách lắng nghe sự kiện disconnecting
:
io.on("connection", socket => {
socket.on("disconnecting", () => {
console.log(socket.rooms); // the Set contains at least the socket ID
});
socket.on("disconnect", () => {
// socket.rooms.size === 0
});
});
4.7 Hỗ trợ đa luồng
Để phân tách ứng dụng (dựa theo module hay quyền hạn người dùng), Socket.IO
cho phép bạn tạo nhiều namespace
, những namespace
này sẽ hoạt động như các kênh truyền thông riêng biệt nhưng chia sẻ cùng một kết nối cơ bản.
Mỗi namespace
sẽ có các thành sau của riêng nó:
- event handlers
io.of("/orders").on("connection", (socket) => {
socket.on("order:list", () => {});
socket.on("order:create", () => {});
});
io.of("/users").on("connection", (socket) => {
socket.on("user:list", () => {});
});
room
const orderNamespace = io.of("/orders");
orderNamespace.on("connection", (socket) => {
socket.join("room1");
orderNamespace.to("room1").emit("hello");
});
const userNamespace = io.of("/users");
userNamespace.on("connection", (socket) => {
socket.join("room1"); // distinct from the room in the "orders" namespace
userNamespace.to("room1").emit("holà");
});
- middlewares
const orderNamespace = io.of("/orders");
orderNamespace.use((socket, next) => {
// ensure the socket has access to the "orders" namespace, and then
next();
});
const userNamespace = io.of("/users");
userNamespace.use((socket, next) => {
// ensure the socket has access to the "users" namespace, and then
next();
});
Áp dụng thực tế:
- Bạn muốn tạo một
namespace
đặc biệt mà chỉ có người dùng được ủy quyền mới có thể truy cập, vì vậy logic liên quan đến nhóm người dùng này được phân tách ra khỏi phần còn lại của ứng dụng. Ví dụ cho phép admin xoá người dùng
const adminNamespace = io.of("/admin");
adminNamespace.use((socket, next) => {
// ensure the user has sufficient rights
next();
});
adminNamespace.on("connection", socket => {
socket.on("delete user", () => {
// ...
});
});
- Ứng dụng của bạn có nhiều khách hàng và bạn có thể linh hoạt tạo
namespace
cho mỗi khách hàng như sau
const workspaces = io.of(/^\/\w+$/);
workspaces.on("connection", socket => {
const workspace = socket.nsp;
workspace.emit("hello");
});
4.7.1 Namespace chính
Những tính năng được demo ở mục 3 thật ra là bạn đang tương tác với namesapce
chính, gọi là /
. Đối tượng io
kế thừa tất cả phương thức của namespace
này
io.on("connection", (socket) => {});
io.use((socket, next) => { next() });
io.emit("hello");
// are actually equivalent to
io.of("/").on("connection", (socket) => {});
io.of("/").use((socket, next) => { next() });
io.of("/").emit("hello");
Trong những bài hướng dẫn bạn đã xem qua có thể đề cập tới io.sockets
, nhưng nó chỉ đơn giản là tên gọi khác của io.of("/")
io.sockets === io.of("/")
4.7.2 Namespace tuỳ chỉnh
Bạn có thể cài đặt tuỳ chỉnh namespace
, bằng cách gọi hàm of
bên phía server.
const nsp = io.of("/my-namespace");
nsp.on("connection", socket => {
console.log("someone connected");
});
nsp.emit("hi", "everyone!");
4.7.3 Khởi tạo phía client
const socket = io(); // or io("/"), the main namespace
const orderSocket = io("/orders"); // the "orders" namespace
const userSocket = io("/users"); // the "users" namespace
Hãy cùng xem xét qua ví dụ trên, chỉ có kết nối WebSocket
được thiết lập, còn những gói dữ liệu sẽ tự động được điều hướng tới đúng namespace
.
Lưu ý: Những trường hợp sau đây sẽ không áp dụng được đa luồng
- Nhiều khởi tạo cho cùng một namespace
const socket1 = io();
const socket2 = io(); // no multiplexing, two distinct WebSocket connections
- Tên miền khác nhau
const socket1 = io("https://first.example.com");
const socket2 = io("https://second.example.com"); // no multiplexing, two distinct WebSocket connections
- Sử dụng tuỳ chọn forceNew
const socket1 = io();
const socket2 = io("/admin", { forceNew: true }); // no multiplexing, two distinct WebSocket connections
4.8 Custom event
Lấy cảm hứng từ Nodejs EventEmitter, Socket.IO
cho phép bạn có thể định nghĩa tuỳ ý các sự kiện dựa trên yêu cầu của ứng dụng. Nghĩa là bạn có thể phát động sự kiện từ một phía và đăng kí listener từ phía còn lại.
// server-side
io.on("connection", (socket) => {
socket.emit("hello", "world");
});
// client-side
socket.on("hello", (arg) => {
console.log(arg); // world
});
4.9 Cộng đồng hỗ trợ
Socket.IO
có một cộng đồng phát triển lớn, có nhiều tài liệu, hướng dẫn và ví dụ mẫu dễ tìm kiếm.
4.10 Performance tốt
Mặc dù Socket.IO
cung cấp các tùy chọn giảm thiểu việc truyền thông (minification) và tối ưu hóa kết nối, nhưng cũng cần phải lưu ý rằng hiệu suất của ứng dụng sẽ phụ thuộc vào cách triển khai và môi trường sử dụng.
5. Tổng kết
Như chúng ta đã biết, việc sử dụng WebSocket
cơ bản có thể phức tạp và đầy thách thức. Đó là lý do tại sao thư viện Socket.IO
được tạo ra - để giúp đơn giản hóa việc triển khai và quản lý kết nối WebSocket
với nhiều tính năng nổi bật được hỗ trợ tự động.
Socket.IO
không chỉ hỗ trợ WebSocket
mà còn giả lập các sự kiện và nhiều tính năng hữu ích khác như dự phòng kết nối HTTP long polling khi có sự cố với WebSocket
và hỗ trợ kết nối lại cách tự động. Ngoài ra, các tính năng nổi bật hỗ trợ mạnh mẽ mà Socket.IO xây dựng hỗ trợ đa luồng với Namespace và Room.
Vì thế ngày nay Socket.IO
là một lựa chọn tối ưu trong việc phát triển ứng dụng với WebSocket
trở nên dễ dàng hơn. Với tính năng được hỗ trợ mạnh mẽ giúp công việc lập trình WebSocket trở nên an toàn và thuận tiện hơn bao giờ hết, tiết kiệm thời gian cho lập trình viên trong các công việc lặp đi lặp lại.
Bài viết liên quan
Giới thiệu Kiến trúc Backend for Frontend (BFF)
Nov 16, 2024 • 10 min read
Flask là gì? Hướng dẫn tạo Ứng dụng Web với Flask
Nov 15, 2024 • 7 min read
Webhook là gì? So sánh Webhook và API
Nov 15, 2024 • 8 min read
Spring Boot là gì? Hướng dẫn Khởi tạo Project Spring Boot với Docker
Nov 14, 2024 • 6 min read
Two-Factor Authentication (2FA) là gì? Vì sao chỉ Mật khẩu thôi là chưa đủ?
Nov 13, 2024 • 7 min read
Test-Driven Development (TDD) là gì? Hướng dẫn thực hành TDD
Nov 13, 2024 • 6 min read