Module WebSockets độc lập với nền tảng (platform-agnostic), do đó bạn có thể mang theo thư viện riêng (hoặc thậm chí một implementation thuần) bằng cách sử dụng interface WebSocketAdapter. Interface này buộc bạn phải implement một vài phương thức được mô tả trong bảng sau:
Ghi chú người dịch Cơ chế này tương tự cách NestJS trừu tượng hóa tầng HTTP server qua HttpAdapter (cho phép bạn chạy cùng một app trên Express hoặc Fastify mà code hầu như không đổi). WebSocketAdapter làm điều tương tự cho tầng WebSocket, nhờ đó Nest có thể hỗ trợ cả socket.io lẫn ws (hoặc bất kỳ thư viện WebSocket nào khác) thông qua cùng một tập decorator và gateway.
create |
Tạo một socket instance dựa trên các tham số được truyền vào |
bindClientConnect |
Gắn sự kiện kết nối của client |
bindClientDisconnect |
Gắn sự kiện ngắt kết nối của client (tùy chọn*) |
bindMessageHandlers |
Gắn message đến với hàm xử lý message tương ứng |
close |
Kết thúc một server instance |
Mở rộng socket.io (Extend socket.io)
Package socket.io được bọc (wrap) trong một class IoAdapter. Điều gì sẽ xảy ra nếu bạn muốn mở rộng chức năng cơ bản của adapter? Ví dụ, yêu cầu kỹ thuật của bạn cần khả năng broadcast sự kiện qua nhiều instance load-balanced của web service. Để làm điều này, bạn có thể extend IoAdapter và override một phương thức duy nhất, có nhiệm vụ khởi tạo các socket.io server mới. Nhưng trước tiên, hãy cài đặt package cần thiết.
Cảnh báo Để dùng socket.io với nhiều instance load-balanced, bạn phải tắt polling bằng cách set transports: ['websocket'] trong cấu hình socket.io của client, hoặc bật cookie based routing trong load balancer. Chỉ dùng Redis thôi là chưa đủ. Xem tại đây để biết thêm thông tin.
$ npm i --save redis socket.io @socket.io/redis-adapter
Sau khi cài đặt package xong, ta có thể tạo class RedisIoAdapter.
import { IoAdapter } from '@nestjs/platform-socket.io';
import { ServerOptions } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
export class RedisIoAdapter extends IoAdapter {
private adapterConstructor: ReturnType<typeof createAdapter>;
async connectToRedis(): Promise<void> {
const pubClient = createClient({ url: `redis://localhost:6379` });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
this.adapterConstructor = createAdapter(pubClient, subClient);
}
createIOServer(port: number, options?: ServerOptions): any {
const server = super.createIOServer(port, options);
server.adapter(this.adapterConstructor);
return server;
}
}
Ghi chú người dịch Vấn đề ở đây giống hệt khi bạn scale horizontal một app Express/Socket.IO thông thường: mỗi instance server chỉ “biết” các client đang kết nối trực tiếp tới nó. Redis adapter dùng cơ chế pub/sub để mọi instance cùng “nghe” được sự kiện broadcast từ các instance khác, nhờ đó server.emit() vẫn tới đúng client dù client A và client B đang kết nối tới hai instance khác nhau sau load balancer.
Sau đó, chỉ cần chuyển sang dùng Redis adapter vừa tạo.
const app = await NestFactory.create(AppModule);
const redisIoAdapter = new RedisIoAdapter(app);
await redisIoAdapter.connectToRedis();
app.useWebSocketAdapter(redisIoAdapter);
Thư viện Ws (Ws library)
Một adapter khác có sẵn là WsAdapter, đóng vai trò như một proxy giữa framework và thư viện ws cực nhanh, được kiểm thử kỹ lưỡng. Adapter này tương thích hoàn toàn với WebSocket gốc của trình duyệt (native browser WebSockets) và nhanh hơn đáng kể so với package socket.io. Tuy nhiên, nó có ít tính năng có sẵn hơn nhiều. Trong một số trường hợp, bạn không thực sự cần đến những tính năng đó.
Ghi chú người dịch Khác biệt cốt lõi giữa ws và socket.io: ws chỉ implement đúng chuẩn WebSocket protocol (không tự động reconnect, không có room/namespace, không fallback sang long-polling), trong khi socket.io xây một custom protocol trên nền WebSocket nên nặng hơn nhưng nhiều tính năng hơn. Vì vậy client dùng thư viện socket.io-client sẽ không kết nối được tới server dùng WsAdapter — bạn cần dùng client WebSocket chuẩn (thư viện ws, hoặc WebSocket API có sẵn của trình duyệt).
Gợi ý Thư viện ws không hỗ trợ namespace (kênh giao tiếp được socket.io phổ biến hóa). Tuy nhiên, để mô phỏng phần nào tính năng này, bạn có thể mount nhiều ws server trên các path khác nhau (ví dụ: @WebSocketGateway({ path: '/users' })).
Để dùng ws, trước tiên ta cần cài đặt package cần thiết:
$ npm i --save @nestjs/platform-ws
Sau khi cài đặt package xong, ta có thể chuyển sang adapter khác:
const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new WsAdapter(app));
Gợi ý WsAdapter được import từ @nestjs/platform-ws.
wsAdapter được thiết kế để xử lý message ở định dạng { event: string, data: any }. Nếu bạn cần nhận và xử lý message ở định dạng khác, bạn sẽ cần cấu hình một message parser để chuyển đổi chúng về định dạng yêu cầu này.
const wsAdapter = new WsAdapter(app, {
// Để xử lý message ở định dạng [event, data]
messageParser: (data) => {
const [event, payload] = JSON.parse(data.toString());
return { event, data: payload };
},
});
Ngoài ra, bạn cũng có thể cấu hình message parser sau khi adapter đã được tạo, bằng phương thức setMessageParser.
Nâng cao (custom adapter) (Advanced (custom adapter))
Để minh họa, chúng ta sẽ tích hợp thư viện ws một cách thủ công. Như đã đề cập, adapter cho thư viện này đã được tạo sẵn và được export từ package @nestjs/platform-ws dưới dạng class WsAdapter. Dưới đây là cách mà một implementation đơn giản hóa có thể trông như thế nào:
import WebSocket from 'ws';
import { WebSocketAdapter, INestApplicationContext } from '@nestjs/common';
import { MessageMappingProperties } from '@nestjs/websockets';
import { Observable, fromEvent, EMPTY } from 'rxjs';
import { mergeMap, filter } from 'rxjs/operators';
export class WsAdapter implements WebSocketAdapter {
constructor(private app: INestApplicationContext) {}
create(port: number, options: any = {}): any {
return new WebSocket.Server({ port, ...options });
}
bindClientConnect(server, callback: Function) {
server.on('connection', callback);
}
bindMessageHandlers(
client: WebSocket,
handlers: MessageMappingProperties[],
process: (data: any) => Observable<any>,
) {
fromEvent(client, 'message')
.pipe(
mergeMap(data => this.bindMessageHandler(data, handlers, process)),
filter(result => result),
)
.subscribe(response => client.send(JSON.stringify(response)));
}
bindMessageHandler(
buffer,
handlers: MessageMappingProperties[],
process: (data: any) => Observable<any>,
): Observable<any> {
const message = JSON.parse(buffer.data);
const messageHandler = handlers.find(
handler => handler.message === message.event,
);
if (!messageHandler) {
return EMPTY;
}
return process(messageHandler.callback(message.data));
}
close(server) {
server.close();
}
}
Gợi ý Khi bạn muốn tận dụng thư viện ws, hãy dùng WsAdapter có sẵn thay vì tự tạo adapter riêng.
Sau đó, ta có thể thiết lập một custom adapter bằng phương thức useWebSocketAdapter():
const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new WsAdapter(app));
Ví dụ (Example)
Một ví dụ hoạt động sử dụng WsAdapter có sẵn tại đây.