MiniChat là nền tảng chat thời gian thực quy mô Enterprise được xây dựng theo mô hình pnpm Monorepo, kết hợp giữa NestJS Server (Port 4000) và Next.js App Router Client (Port 3000). Hệ thống kết hợp hài hòa giữa kênh truyền REST API cho dữ liệu phi trạng thái/truyền tải tệp lớn và kênh WebSocket song công (Full-duplex) qua Socket.IO cho tương tác tức thời.
- Kiến Trúc Client - Server Hiện Đại
- Giao Thức Socket.IO & WebSocket
- Các Sơ Đồ Luồng Tuần Tự Hệ Thống (Sequence Diagrams)
- Luồng 1: Thiết Lập Kết Nối & Gia Nhập Phòng (Handshake & Join Room)
- Luồng 2: Nhắn Tin Thời Gian Thực & Trạng Thái Gõ Phím (Chat & Typing)
- Luồng 3: Đính Kèm & Dán Ảnh Clipboard (Image Attachment & Preview)
- Luồng 4: Truyền Tải Tệp Phân Đoạn Stream, Backpressure & SHA-256
- Luồng 5: Rời Phòng & Ngắt Kết Nối (Leave Room & Disconnect Lifecycle)
- Framework NestJS - Phía Server (Port 4000)
- Framework Next.js - Phía Client (Port 3000)
- Ma Trận Sự Kiện WebSocket
- Thực Nghiệm Mã Nguồn
- Hướng Dẫn Cài Đặt & Khởi Chạy
flowchart TB
subgraph Client_Tier["CLIENT TIER: Next.js (App Router - Port 3000)"]
direction TB
RSC["React Server Components (RSC)<br/>• Layouts, Static UI Shell, Server Prefetch<br/>• Zero Client JS Bundle Size"]
CC["Client Components ('use client')<br/>• ChatBox, MessageList, TypingIndicator, FileUploader"]
Hook["Custom Hook: useMiniChatSocket / useChatSocket<br/>• Singleton socket instance qua useRef<br/>• Cleanup Phase qua useEffect (socket.off, disconnect)"]
RSC --> CC
CC --> Hook
end
subgraph Transport_Layer["DUAL-CHANNEL TRANSPORT & PROTOCOL LAYER"]
direction LR
REST_Chan["HTTP/1.1 REST Channel (Port 4000)<br/>• POST /upload/stream (Multipart Chunked Stream)<br/>• GET /api/server-status (Health check & Monitoring)"]
WS_Chan["Full-Duplex Socket.IO Channel (Port 4000)<br/>• Upgrade Handshake: HTTP 101 Switching Protocols<br/>• Persistent TCP Frame Engine"]
end
subgraph Server_Tier["SERVER TIER: NestJS Enterprise Architecture (Port 4000)"]
direction TB
AppModule["AppModule (Root IoC Container)"]
subgraph Chat_Module["ChatModule"]
ChatGateway["ChatGateway (@WebSocketGateway)<br/>• Decorators: @WebSocketServer, @SubscribeMessage, @ConnectedSocket, @MessageBody<br/>• Lifecycle: OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect"]
ChatService["ChatService (@Injectable)<br/>• Quản lý Room Memory & Connection Sessions<br/>• Broadcast & Direct Message Dispatcher"]
end
subgraph Media_Module["MediaStreamModule"]
UploadController["MediaStreamController (@Controller 'upload')<br/>• Endpoint: POST /upload/stream"]
StreamService["MediaStreamService (@Injectable)<br/>• pipeline(readable, writable) với highWaterMark<br/>• Backpressure Flow Control & SHA-256 Hashing"]
end
AppModule --> Chat_Module
AppModule --> Media_Module
ChatGateway --> ChatService
UploadController --> StreamService
StreamService -.->|Trigger Metadata & Message Event| ChatGateway
end
CC -->|1. Multipart Binary Stream| REST_Chan --> UploadController
Hook <==>|2. Bidirectional Events & Ack| WS_Chan <==> ChatGateway
- Kênh REST API: Đảm nhiệm các thao tác phi trạng thái (Stateless), yêu cầu xác thực độc lập, truy vấn dữ liệu ban đầu, và truyền tải tệp tin dung lượng lớn qua cơ chế Multipart / Binary Stream.
- Kênh WebSocket (Socket.IO): Thiết lập kênh kết nối 2 chiều liên tục (Full-duplex) với độ trễ cực thấp và chi phí overhead chỉ vài bytes/frame, phục vụ trao đổi tin nhắn tức thời, trạng thái gõ phím (typing indicator), cập nhật danh sách người dùng và phát tán thông báo metadata.
| Tiêu chí | HTTP Polling | HTTP Long-Polling | Server-Sent Events (SSE) | WebSocket / Socket.IO |
|---|---|---|---|---|
| Cơ chế hoạt động | Client gửi request định kỳ (ví dụ mỗi 2-5s) | Client gửi request, Server giữ connection đến khi có dữ liệu | Server mở stream 1 chiều liên tục đẩy dữ liệu về Client | Thiết lập kết nối TCP bền bỉ 2 chiều (Full-duplex) sau bắt tay HTTP 101 |
| Băng thông & Overhead | Rất lãng phí (~1KB header HTTP cho mỗi request rỗng) | Lãng phí (Tạo lại kết nối TCP và gửi lặp lại Header HTTP) | Tiết kiệm (Header chỉ gửi 1 lần khi thiết lập stream) | Tối ưu vượt trội (Frame nhị phân chỉ từ 2 đến 6 bytes) |
| Độ trễ (Latency) | Rất cao (Bằng polling interval) | Trung bình (Độ trễ tái thiết lập connection) | Rất thấp (Server gửi tức thì) | Cực thấp (Real-time: 15 - 25 ms) |
| Tính hai chiều | Bán song công (Half-duplex) | Bán song công (Half-duplex) | Đơn công (Chỉ Server |
Toàn song công (Full-duplex song song đồng thời) |
| Phù hợp nhất | Dashboard dữ liệu cập nhật theo phút | Ứng dụng chat đời cũ | Bảng tin tức, chứng khoán, thông báo một chiều | Chat thời gian thực, Game, Live Collaboration, Video/Audio Signaling |
1. Client gửi HTTP GET Request kèm Upgrade Headers:
GET /socket.io/?EIO=4&transport=polling HTTP/1.1
Host: localhost:4000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
2. Server NestJS xử lý SHA-1 Magic String và phản hồi:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Sau bước này, giao thức HTTP được nâng cấp thành kết nối WebSocket nhị phân hai chiều trực tiếp trên cổng 4000.
- Room Management: Không gian định tuyến logic ảo (ví dụ:
general,project-alpha). Socket instance có thể tham gia (client.join(roomId)) hoặc rời khỏi (client.leave(roomId)). - Cơ chế Phân Phối (Broadcast Scope):
- Broadcast To All:
this.server.emit('event', data)— Gửi tới toàn bộ client kết nối vào server. - Broadcast To Room:
this.server.to(roomId).emit('event', data)— Gửi tới toàn bộ client trong room chỉ định. - Broadcast To Room Except Sender:
client.to(roomId).emit('event', data)— Gửi tới mọi client trong room trừ client phát tín hiệu.
- Broadcast To All:
sequenceDiagram
autonumber
actor User as Người dùng (Browser)
participant Client as Next.js Client (Port 3000)
participant Gateway as ChatGateway (Port 4000)
participant Service as ChatService
participant Room as Thành viên trong Phòng
User->>Client: Mở giao diện chat và đăng nhập (Alice, phòng general)
Client->>Gateway: 1. HTTP GET /socket.io/?transport=websocket (Upgrade Handshake)
Gateway-->>Client: 2. HTTP 101 Switching Protocols (Nâng cấp thành công)
Note over Client,Gateway: Kết nối WebSocket hai chiều toàn song công (Full-duplex) được thiết lập
Gateway->>Service: handleConnection(client) -> Ghi nhận Socket ID
Client->>Gateway: 3. Emit "joinRoom" (room: general, username: Alice)
Gateway->>Service: handleStandardJoinRoom(client, payload)
Service->>Service: client.join("general") và Lưu Session
Service->>Room: 4. Emit "userJoined" và "userStatusChanged" (Alice JOINED)
Gateway-->>Client: 5. Ack Callback "joinRoomSuccess"
Client-->>User: Hiển thị giao diện phòng chat sẵn sàng
sequenceDiagram
autonumber
actor Alice as Alice (Người gửi)
participant ClientA as Next.js Client (Alice)
participant Gateway as ChatGateway (NestJS)
participant Service as ChatService & Store
participant ClientB as Next.js Client (Bob)
actor Bob as Bob (Người nhận)
Alice->>ClientA: Bắt đầu gõ phím vào khung soạn thảo
ClientA->>Gateway: 1. Emit "typingStatus" (isTyping: true)
Gateway->>ClientB: 2. Broadcast "typingStatus" (Alice isTyping: true)
ClientB-->>Bob: Hiển thị chỉ báo "Alice đang soạn tin nhắn..."
Alice->>ClientA: Nhấn nút "Gửi" (Nội dung: "Xin chào Bob!")
ClientA->>Gateway: 3. Emit "sendMessage" (room: general, sender: Alice)
Gateway->>Service: handleStandardSendMessage(client, payload)
Service->>Service: Đóng gói MessageDto, tăng totalMessages, ghi Activity Log
Service->>Gateway: Phát tán tin nhắn tới phòng general
par Phân phối tin nhắn đồng thời
Gateway->>ClientA: 4a. Ack Callback (status: success, messageId)
Gateway->>ClientB: 4b. Broadcast "receiveMessage" (Alice: "Xin chào Bob!")
end
ClientA->>Gateway: 5. Emit "typingStatus" (isTyping: false)
Gateway->>ClientB: 6. Broadcast "typingStatus" (Alice isTyping: false)
ClientB-->>Bob: Render tin nhắn mới vào MessageList và ẩn Typing Indicator
sequenceDiagram
autonumber
actor Alice as Alice (Người dùng)
participant UI as Giao diện Chat (Next.js)
participant SocketHook as useChatSocket / Socket.IO
participant Gateway as ChatGateway (NestJS)
participant Members as Thành viên nhận ảnh
alt Cách 1: Chụp màn hình và Dán trực tiếp
Alice->>UI: Nhấn Ctrl+V vào ô chat (Paste từ Clipboard)
else Cách 2: Chọn từ tệp
Alice->>UI: Click icon ảnh hoặc Kéo thả tệp vào ô chat
end
UI->>UI: 1. processImageFile (Kiểm tra PNG/JPEG/GIF/WEBP và dung lượng <= 5MB)
UI->>UI: 2. FileReader tạo DataURL và hiển thị Thẻ Preview
Note over Alice,UI: Ảnh CHƯA gửi ngay - người dùng có thể xem trước hoặc bấm xóa
Alice->>UI: 3. Nhập chú thích kèm theo và bấm nút Gửi
UI->>SocketHook: 4. emitImage (Sự kiện gửi ảnh kèm caption)
SocketHook->>Gateway: 5. Socket.IO Event "group-image-message"
Gateway->>Members: 6. Broadcast "receive-image-message" (Base64 Image + Caption)
Members-->>Members: Hiển thị hình ảnh và nội dung chú thích
UI->>UI: 7. Reset ô nhập liệu và đóng Thẻ Preview
sequenceDiagram
autonumber
actor Client as Next.js Client (Port 3000)
participant REST as MediaStreamController (POST /upload/stream)
participant StreamService as MediaStreamService
participant Disk as Storage Disk (fs.createWriteStream)
participant Gateway as ChatGateway (WebSocket)
participant Room as Room Members (Clients)
Client->>REST: 1. POST /upload/stream (Gửi Binary Stream)
REST->>StreamService: 2. handleStreamUpload(req, fileName, roomId, sender)
rect rgb(240, 248, 255)
Note over StreamService,Disk: Cơ chế Backpressure Flow Control và Tính toán Hash tức thì
loop Đọc và Ghi từng phân đoạn 64KB (Chunk)
StreamService->>StreamService: Tính toán mã băm SHA-256 (crypto.createHash)
StreamService->>Disk: pipeline(readableStream, writableStream)
Note over Disk: Nếu đĩa ghi chậm - Kích hoạt Backpressure tạm dừng đọc buffer
end
end
Disk-->>StreamService: 3. Ghi đĩa hoàn tất và tạo mã SHA-256
StreamService->>Gateway: 4. Kích hoạt sự kiện hoàn tất tệp
par Thông báo kép tới toàn bộ phòng
Gateway->>Room: 5a. WebSocket Emit "fileMetadataReady" (Metadata + SHA-256)
Gateway->>Room: 5b. WebSocket Emit "receiveMessage" (Thông báo tệp mới)
end
StreamService-->>REST: 6. Trả về FileUploadResult DTO
REST-->>Client: 7. HTTP 201 Created (Upload Response)
sequenceDiagram
autonumber
actor User as Người dùng
participant Client as Next.js Client (Port 3000)
participant Gateway as ChatGateway (Port 4000)
participant Service as ChatService & Store
participant Dashboard as Monitor Dashboard
alt Kịch bản A: Người dùng chủ động đổi phòng hoặc rời phòng
User->>Client: Chọn đổi sang phòng khác
Client->>Gateway: 1a. Emit "leaveRoom" (room: general, username: Alice)
Gateway->>Service: handleStandardLeaveRoom() -> client.leave("general")
Service->>Gateway: Broadcast "userLeft" và "userStatusChanged" (LEFT)
Client->>Gateway: 1b. Emit "joinRoom" (room: dev-team, username: Alice)
else Kịch bản B: Người dùng tắt trình duyệt hoặc mất mạng
User->>Client: Đóng Tab hoặc Tắt ứng dụng
Client-xGateway: 2a. TCP Connection Terminated
Gateway->>Gateway: 2b. Lifecycle Hook: handleDisconnect(client)
Gateway->>Service: cleanupUser(client, username)
Service->>Service: Cập nhật status offline và lastSeen
par Phát thông báo cập nhật toàn hệ thống
Service->>Gateway: Broadcast "userStatusChanged" (Alice OFFLINE)
Service->>Gateway: Broadcast "groups-updated" tới các client còn lại
Service->>Dashboard: Broadcast "stats-update" (Cập nhật số người online)
end
end
- Modules (
@Module): Đóng gói các ranh giới nghiệp vụ riêng biệt (ChatModule,MediaStreamModule,ApiModule,SharedModule). - Controllers (
@Controller): Tiếp nhận các REST HTTP requests (MediaStreamController,ApiController). - Services / Providers (
@Injectable): Xử lý logic cốt lõi. IoC Container của NestJS tự động quản lý vòng đời và tiêm phụ thuộc (Dependency Injection) qua constructor.
@WebSocketGateway(4000, { cors: ..., namespace: '/chat' }): Khởi tạo và liên kết WebSocket Server trên port 4000.@WebSocketServer(): Tiêm đối tượng Socket.IO Server vào Gateway.@SubscribeMessage('event'): Đăng ký listener lắng nghe sự kiện từ client.@ConnectedSocket()&@MessageBody(): Trích xuất socket của client và payload của thông điệp.- 3 Giao diện Vòng đời:
OnGatewayInit (afterInit): Chạy một lần khi gateway được khởi tạo thành công.OnGatewayConnection (handleConnection): Chạy khi có client mới kết nối.OnGatewayDisconnect (handleDisconnect): Chạy khi client ngắt kết nối.
Khi truyền tải tệp dung lượng lớn (hàng trăm MB - GB), đọc file toàn bộ vào Buffer sẽ gây Buffer Overflow / OOM.
- Cơ chế Backpressure: Khi
ReadableStreamđọc nhanh hơn tốc độ ghi củaWritableStream,WritableStreamđạt ngưỡnghighWaterMark(64KB), tự động phát tín hiệu tạm dừng (pause()) và kích hoạt sự kiệndrainđểresume(). - Sử dụng
pipeline():
import { pipeline } from 'node:stream/promises';
import { createWriteStream } from 'node:fs';
import { Readable } from 'node:stream';
// Tự động kiểm soát luồng dữ liệu, backpressure và an toàn giải phóng bộ nhớ
await pipeline(
readableStream,
createWriteStream(destinationPath, { highWaterMark: 64 * 1024 })
);- SSR (Server-Side Rendering): Render giao diện ban đầu thành HTML tĩnh trên Server giúp tối ưu SEO và thời gian First Contentful Paint (FCP).
- CSR (Client-Side Rendering): Xử lý các logic thay đổi trạng thái và tương tác real-time trên trình duyệt.
- Hydration: Quá trình React gắn các event listener và state vào HTML đã được render sẵn từ SSR.
| Tiêu chí So Sánh | React Server Components (RSC - Default) | Client Components ('use client') |
|---|---|---|
| Môi trường thực thi | Hoàn toàn trên Server (Node.js/Edge) | Server (Pre-render HTML) & Browser (Hydration) |
| Kích thước Bundle Client | 0 KB (Không đóng gói mã nguồn JS vào client bundle) | Tính vào dung lượng JavaScript bundle tải về browser |
| Khả năng dùng Hooks | Không (useState, useEffect, useRef...) |
Đầy đủ (useState, useEffect, useCallback...) |
| Quyền truy cập Web API | Không (Không có window, document, localStorage) |
Có (Truy cập window, WebSockets, DOM Events) |
| Phạm vi ứng dụng trong MiniChat | Root Layout, Sidebar tĩnh, Prefetch dữ liệu tĩnh | ChatBox, MessageList, FileUploader, WebSocket Hooks |
Để loại bỏ tình trạng Memory Leak và Duplicate Connection (đặc biệt trong chế độ React StrictMode):
- Singleton Instance: Sử dụng
useRef<Socket | null>lưu socket instance để không bị khởi tạo lại qua các lần re-render. - Quản lý Vòng đời: Thiết lập
useEffectvới dependency rõ ràng, tự động kết nối và join room khi mount. - Cleanup Phase: Khi component unmount, tự động gọi
socket.off()cho mọi listener vàsocket.disconnect()để giải phóng kết nối trên Server.
| Tên Sự kiện (Event) | Chiều (Direction) | Payload Schema (DTO) | Phạm vi Phát tán (Scope) | Mục đích Xử lý Nghiệp vụ |
|---|---|---|---|---|
joinRoom |
Client |
{ room: string, username: string } |
Direct to Server | Đăng ký client vào Socket.IO Room logic |
userJoined |
Server |
{ username: string, socketId: string, timestamp: string } |
client.broadcast.to(roomId) |
Thông báo thành viên mới tham gia phòng |
userStatusChanged |
Server |
{ username: string, status: string, room?: string } |
server.to(roomId).emit |
Cập nhật trạng thái JOINED / LEFT / OFFLINE |
sendMessage |
Client |
{ room: string, sender: string, message: string, type?: string } |
Direct to Server | Gửi tin nhắn mới có Ack callback |
receiveMessage |
Server |
ChatMessage (id, room, sender, message, timestamp) |
server.to(roomId).emit |
Phân phối tin nhắn mới đến toàn bộ phòng |
typingStatus |
Client |
{ room: string, username: string, isTyping: boolean } |
client.broadcast.to(roomId) |
Hiển thị thông báo đang soạn tin nhắn |
fileMetadataReady |
Server |
{ roomId: string, fileId: string, fileName: string, fileUrl: string, size: number, sha256: string } |
server.to(roomId).emit |
Phát thông báo khi tệp lớn stream xong lên server |
leaveRoom |
Client |
{ room: string, username: string } |
Direct to Server | Rời phòng chat và thông báo cho các thành viên |
@WebSocketGateway(4000, {
cors: { origin: process.env.CLIENT_ORIGIN || 'http://localhost:3000', credentials: true },
namespace: '/chat',
transports: ['websocket', 'polling'],
})
@Injectable()
export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer() server!: Server;
constructor(private readonly chatService: ChatService) {}
afterInit(server: Server) { /* Khởi tạo server */ }
handleConnection(client: Socket) { /* Kết nối client */ }
handleDisconnect(client: Socket) { /* Dọn dẹp ngắt kết nối */ }
@SubscribeMessage('joinRoom')
handleJoinRoom(@ConnectedSocket() client: Socket, @MessageBody() payload: JoinRoomDto) {
return this.chatService.handleStandardJoinRoom(client, payload);
}
@SubscribeMessage('sendMessage')
handleSendMessage(@ConnectedSocket() client: Socket, @MessageBody() payload: SendMessageDto) {
return this.chatService.handleStandardSendMessage(client, payload);
}
@SubscribeMessage('typingStatus')
handleTypingStatus(@ConnectedSocket() client: Socket, @MessageBody() payload: TypingStatusDto) {
return this.chatService.handleStandardTypingStatus(client, payload);
}
}'use client';
import { useEffect, useRef, useState, useCallback } from 'react';
import { io, Socket } from 'socket.io-client';
export function useChatSocket({ serverUrl = 'http://localhost:4000', roomId = 'general', username = '' }) {
const socketRef = useRef<Socket | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [messages, setMessages] = useState<StandardChatMessage[]>([]);
useEffect(() => {
if (!socketRef.current) {
socketRef.current = io(serverUrl, { transports: ['websocket', 'polling'], withCredentials: true });
}
const socket = socketRef.current;
socket.on('connect', () => {
setIsConnected(true);
socket.emit('joinRoom', { roomId, username });
});
socket.on('receiveMessage', (msg) => setMessages((prev) => [...prev, msg]));
return () => {
socket.off('connect');
socket.off('receiveMessage');
socket.disconnect();
socketRef.current = null;
};
}, [serverUrl, roomId, username]);
const sendMessage = useCallback((content: string) => {
socketRef.current?.emit('sendMessage', { roomId, sender: username, content });
}, [roomId, username]);
return { isConnected, messages, sendMessage };
}- Node.js: v18.0.0 trở lên
- pnpm: v9.0.0 trở lên
pnpm install# Khởi chạy đồng thời cả Server (Port 4000) và Client (Port 3000)
pnpm run dev
# Hoặc khởi chạy độc lập từng service:
pnpm run dev:server # NestJS Server: http://localhost:4000
pnpm run dev:client # Next.js Client: http://localhost:3000# 1. Build production bundles
pnpm run build
# 2. Khởi chạy thông qua PM2
pnpm start
# 3. Quản trị dịch vụ
pnpm run status # Xem trạng thái services
pnpm run logs # Xem streaming logs
pnpm run stop # Dừng hệ thống
pnpm run restart # Khởi động lại hệ thống