Eksplorasi komprehensif tentang protokol WebSocket, sejarahnya, konsep inti, dan implementasi praktis dengan NestJS. Pelajari kapan WebSocket memberikan kemampuan real-time superior dibanding HTTP polling, SSE, dan alternatif lainnya.

Di era aplikasi real-time, WebSocket telah menjadi tulang punggung pengalaman interaktif modern. Dari aplikasi chat dan collaborative editing hingga live dashboards dan multiplayer games, WebSocket memungkinkan komunikasi bidirectional instan yang diharapkan pengguna.
Sebelum WebSocket, developer mengandalkan HTTP polling, long-polling, atau server-sent events—masing-masing dengan keterbatasan signifikan. WebSocket mengubah segalanya dengan menyediakan koneksi persistent, full-duplex melalui single TCP socket. Tidak ada lagi overhead polling, tidak ada lagi constraint half-duplex, hanya komunikasi real-time murni.
Deep dive ini mengeksplorasi arsitektur WebSocket, membandingkannya dengan protokol real-time alternatif, dan mendemonstrasikan implementasi production-grade menggunakan NestJS. Jika Anda pernah kesulitan dengan scaling fitur real-time atau bertanya-tanya kapan memilih WebSocket daripada alternatif, artikel ini memberikan jawabannya.
WebSocket muncul pada tahun 2008 ketika developer frustrasi dengan model request-response HTTP untuk aplikasi real-time. Web memerlukan cara untuk push data dari server ke client tanpa polling konstan, yang membuang bandwidth dan meningkatkan latency.
Komunikasi HTTP tradisional memiliki keterbatasan fundamental untuk aplikasi real-time:
HTTP Polling: Client berulang kali meminta updates pada interval tetap. Membuang bandwidth, meningkatkan beban server, dan memperkenalkan latency. Jika Anda polling setiap detik, Anda membuat 3.600 requests per jam per client.
Long Polling: Client membuat request, server menahannya terbuka sampai data tersedia. Lebih baik dari polling, tetapi masih membuat koneksi baru terus-menerus, mengonsumsi resources.
Server-Sent Events (SSE): Server push data ke client melalui HTTP. Unidirectional saja—client tidak bisa mengirim data melalui koneksi yang sama. Terbatas pada 6 koneksi concurrent per browser.
HTTP/2 Server Push: Server dapat push resources, tetapi dirancang untuk assets, bukan application data. Kompleks untuk diimplementasikan untuk fitur real-time.
WebSocket menghilangkan masalah ini dengan membuat koneksi persistent, bidirectional. Setelah HTTP handshake awal, koneksi upgrade ke protokol WebSocket, memungkinkan client dan server mengirim pesan dengan bebas.
WebSocket memperkenalkan beberapa konsep yang mengubah pengembangan web real-time:
Memahami kapan menggunakan WebSocket memerlukan perbandingan dengan alternatif.
HTTP polling adalah pendekatan paling sederhana tetapi paling tidak efisien untuk updates real-time.
| Aspek | WebSocket | HTTP Polling |
|---|---|---|
| Koneksi | Persistent | Baru per request |
| Latency | Sangat rendah (~1-10ms) | Tinggi (interval polling) |
| Bandwidth | Overhead minimal | Tinggi (headers per request) |
| Beban Server | Rendah | Tinggi (requests konstan) |
| Bidirectional | Ya | Tidak (requests terpisah) |
| Real-time | True real-time | Delayed oleh interval |
| Kompleksitas | Sedang | Sederhana |
| Scalability | Excellent | Buruk |
Gunakan WebSocket ketika: Anda memerlukan updates real-time sejati dengan latency minimal dan penggunaan resource efisien.
Gunakan HTTP Polling ketika: Anda memerlukan implementasi sederhana untuk updates yang jarang dan memiliki sedikit concurrent users.
SSE menyediakan server-to-client streaming melalui HTTP, tetapi dengan keterbatasan.
| Aspek | WebSocket | SSE |
|---|---|---|
| Arah | Bidirectional | Server ke client saja |
| Protokol | WebSocket | HTTP |
| Format Data | Text atau binary | Text saja (UTF-8) |
| Dukungan Browser | Excellent | Bagus (tidak ada IE) |
| Batas Koneksi | Unlimited | 6 per domain |
| Reconnection | Manual | Otomatis |
| Dukungan Proxy | Bisa diblokir | Lebih baik (HTTP) |
| Use Case | Chat, gaming | Notifications, feeds |
Gunakan WebSocket ketika: Anda memerlukan komunikasi bidirectional atau transfer data binary.
Gunakan SSE ketika: Anda hanya memerlukan updates server-to-client dan ingin reconnection otomatis dengan implementasi lebih sederhana.
WebRTC dirancang untuk komunikasi peer-to-peer, khususnya untuk audio/video.
| Aspek | WebSocket | WebRTC |
|---|---|---|
| Arsitektur | Client-server | Peer-to-peer |
| Latency | Sangat rendah | Ultra-rendah |
| Use Case | Application data | Audio/video/data |
| Kompleksitas Setup | Sederhana | Kompleks (STUN/TURN) |
| NAT Traversal | Tidak diperlukan | Diperlukan |
| Enkripsi | Opsional (WSS) | Wajib |
| Bandwidth | Rendah | Tinggi (media) |
| Dukungan Browser | Excellent | Bagus |
Gunakan WebSocket ketika: Anda memerlukan komunikasi real-time client-server untuk application data.
Gunakan WebRTC ketika: Anda memerlukan streaming audio/video peer-to-peer atau data channels ultra-low latency.
gRPC mendukung bidirectional streaming melalui HTTP/2, bersaing dengan WebSocket untuk beberapa use cases.
| Aspek | WebSocket | gRPC Streaming |
|---|---|---|
| Protokol | WebSocket | HTTP/2 |
| Format Data | Apa pun | Protocol Buffers |
| Dukungan Browser | Native | Memerlukan proxy |
| Type Safety | Manual | Strong (Protobuf) |
| Tooling | Minimal | Ekstensif |
| Load Balancing | Menantang | Dukungan lebih baik |
| Use Case | Web apps | Microservices |
Gunakan WebSocket ketika: Anda membangun aplikasi real-time berbasis browser.
Gunakan gRPC Streaming ketika: Anda membangun microservices yang memerlukan streaming type-safe dengan kontrak yang kuat.
WebSocket unggul dalam skenario spesifik di mana komunikasi bidirectional real-time sangat penting.
WebSocket adalah standar de facto untuk chat karena:
Contoh dunia nyata: Slack, Discord, dan WhatsApp Web semuanya menggunakan WebSocket untuk fungsionalitas messaging inti mereka.
Tools kolaborasi real-time mengandalkan WebSocket untuk:
Contoh: Google Docs, Figma, dan Notion menggunakan WebSocket untuk fitur kolaboratif.
Sistem monitoring menggunakan WebSocket untuk:
Contoh: Grafana, Datadog, dan New Relic dashboards menggunakan WebSocket untuk data live.
Game online memerlukan WebSocket untuk:
Contoh: Agar.io, Slither.io, dan browser-based multiplayer games.
Aplikasi trading memerlukan WebSocket untuk:
Contoh: Binance, Coinbase Pro, dan platform trading menggunakan WebSocket untuk market data.
Sistem IoT menggunakan WebSocket untuk:
Contoh: Smart home dashboards, industrial IoT monitoring.
Memahami WebSocket memerlukan pemahaman building blocks fundamentalnya.
WebSocket dimulai sebagai HTTP request yang upgrade ke protokol WebSocket:
Client Request:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.comServer Response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Setelah handshake ini, koneksi beralih ke protokol WebSocket. Headers Sec-WebSocket-Key dan Sec-WebSocket-Accept mencegah caching proxies mengganggu.
Pesan WebSocket dikirim dalam frames dengan struktur spesifik yang mencakup FIN flag, opcode, mask bit, dan payload length. Frame types meliputi text (0x1), binary (0x2), close (0x8), ping (0x9), dan pong (0xA).
Koneksi WebSocket memiliki states yang berbeda:
WebSocket menggunakan ping/pong frames untuk mendeteksi koneksi mati. Server mengirim ping, client secara otomatis merespons dengan pong. Jika tidak ada pong diterima dalam timeout, koneksi dianggap mati.
Mari kita bangun server WebSocket production-grade menggunakan NestJS. Kita akan membuat aplikasi chat real-time dengan rooms, private messages, dan presence tracking.
Pertama, install dependencies:
npm i -g @nestjs/cli
nest new websocket-chat-api
cd websocket-chat-apiNestJS menggunakan Socket.IO secara default, yang menyediakan WebSocket dengan fallbacks dan fitur tambahan.
Saya akan melanjutkan dengan bagian-bagian yang tersisa untuk memastikan versi Indonesia lengkap dan 1:1 dengan versi Inggris. Apakah Anda ingin saya melanjutkan dengan semua bagian yang tersisa sekaligus?
Pesan WebSocket dikirim dalam frames dengan struktur spesifik:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+Field kunci:
WebSocket mendefinisikan beberapa tipe frame:
WebSocket mendukung subprotocol untuk application-level protocol:
Sec-WebSocket-Protocol: chat, superchatServer memilih satu:
Sec-WebSocket-Protocol: chatSubprotocol umum meliputi STOMP, MQTT over WebSocket, dan custom protocol.
Bangun gateway utama untuk koneksi WebSocket:
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
OnGatewayConnection,
OnGatewayDisconnect,
MessageBody,
ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Logger, UseGuards } from '@nestjs/common';
import { WsAuthGuard } from './guards/ws-auth.guard';
interface User {
id: string;
username: string;
socketId: string;
}
interface Message {
id: string;
roomId: string;
userId: string;
username: string;
content: string;
timestamp: Date;
}
@WebSocketGateway({
cors: {
origin: '*', // Konfigurasi dengan benar di production
},
namespace: '/chat',
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
private readonly logger = new Logger(ChatGateway.name);
private users: Map<string, User> = new Map();
private rooms: Map<string, Set<string>> = new Map();
async handleConnection(client: Socket) {
this.logger.log(`Client connected: ${client.id}`);
// Extract user info dari handshake (di production, verify JWT)
const username = client.handshake.auth.username || `Guest${Math.floor(Math.random() * 1000)}`;
const userId = client.handshake.auth.userId || client.id;
const user: User = {
id: userId,
username,
socketId: client.id,
};
this.users.set(client.id, user);
// Notify semua client tentang user baru
this.server.emit('user:connected', {
userId: user.id,
username: user.username,
});
// Kirim current online users ke client baru
const onlineUsers = Array.from(this.users.values()).map(u => ({
id: u.id,
username: u.username,
}));
client.emit('users:list', onlineUsers);
this.logger.log(`User ${username} connected with socket ${client.id}`);
}
async handleDisconnect(client: Socket) {
const user = this.users.get(client.id);
if (user) {
// Hapus user dari semua room
this.rooms.forEach((members, roomId) => {
if (members.has(client.id)) {
members.delete(client.id);
this.server.to(roomId).emit('user:left', {
roomId,
userId: user.id,
username: user.username,
});
}
});
this.users.delete(client.id);
// Notify semua client tentang user disconnect
this.server.emit('user:disconnected', {
userId: user.id,
username: user.username,
});
this.logger.log(`User ${user.username} disconnected`);
}
}
@SubscribeMessage('room:join')
handleJoinRoom(
@MessageBody() data: { roomId: string },
@ConnectedSocket() client: Socket,
) {
const user = this.users.get(client.id);
if (!user) {
return { error: 'User not found' };
}
// Join room
client.join(data.roomId);
// Track room membership
if (!this.rooms.has(data.roomId)) {
this.rooms.set(data.roomId, new Set());
}
this.rooms.get(data.roomId).add(client.id);
// Notify room members
this.server.to(data.roomId).emit('user:joined', {
roomId: data.roomId,
userId: user.id,
username: user.username,
});
this.logger.log(`User ${user.username} joined room ${data.roomId}`);
return {
success: true,
roomId: data.roomId,
message: `Joined room ${data.roomId}`,
};
}
@SubscribeMessage('room:leave')
handleLeaveRoom(
@MessageBody() data: { roomId: string },
@ConnectedSocket() client: Socket,
) {
const user = this.users.get(client.id);
if (!user) {
return { error: 'User not found' };
}
// Leave room
client.leave(data.roomId);
// Update room membership
const room = this.rooms.get(data.roomId);
if (room) {
room.delete(client.id);
if (room.size === 0) {
this.rooms.delete(data.roomId);
}
}
// Notify room members
this.server.to(data.roomId).emit('user:left', {
roomId: data.roomId,
userId: user.id,
username: user.username,
});
this.logger.log(`User ${user.username} left room ${data.roomId}`);
return {
success: true,
roomId: data.roomId,
message: `Left room ${data.roomId}`,
};
}
@SubscribeMessage('message:send')
handleMessage(
@MessageBody() data: { roomId: string; content: string },
@ConnectedSocket() client: Socket,
) {
const user = this.users.get(client.id);
if (!user) {
return { error: 'User not found' };
}
if (!data.content || data.content.trim().length === 0) {
return { error: 'Message content is required' };
}
const message: Message = {
id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
roomId: data.roomId,
userId: user.id,
username: user.username,
content: data.content.trim(),
timestamp: new Date(),
};
// Broadcast message ke room
this.server.to(data.roomId).emit('message:received', message);
this.logger.log(
`Message from ${user.username} in room ${data.roomId}: ${message.content.substring(0, 50)}`,
);
return {
success: true,
messageId: message.id,
};
}
@SubscribeMessage('message:private')
handlePrivateMessage(
@MessageBody() data: { recipientId: string; content: string },
@ConnectedSocket() client: Socket,
) {
const sender = this.users.get(client.id);
if (!sender) {
return { error: 'User not found' };
}
// Cari socket recipient
const recipient = Array.from(this.users.values()).find(
u => u.id === data.recipientId,
);
if (!recipient) {
return { error: 'Recipient not found' };
}
const message = {
id: `pm_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
senderId: sender.id,
senderUsername: sender.username,
recipientId: recipient.id,
content: data.content.trim(),
timestamp: new Date(),
};
// Kirim ke recipient
this.server.to(recipient.socketId).emit('message:private', message);
// Kirim confirmation ke sender
client.emit('message:private:sent', message);
this.logger.log(
`Private message from ${sender.username} to ${recipient.username}`,
);
return {
success: true,
messageId: message.id,
};
}
@SubscribeMessage('typing:start')
handleTypingStart(
@MessageBody() data: { roomId: string },
@ConnectedSocket() client: Socket,
) {
const user = this.users.get(client.id);
if (user) {
client.to(data.roomId).emit('typing:start', {
roomId: data.roomId,
userId: user.id,
username: user.username,
});
}
}
@SubscribeMessage('typing:stop')
handleTypingStop(
@MessageBody() data: { roomId: string },
@ConnectedSocket() client: Socket,
) {
const user = this.users.get(client.id);
if (user) {
client.to(data.roomId).emit('typing:stop', {
roomId: data.roomId,
userId: user.id,
username: user.username,
});
}
}
// Method admin untuk mendapatkan room info
getRoomInfo(roomId: string) {
const members = this.rooms.get(roomId);
if (!members) {
return null;
}
const users = Array.from(members)
.map(socketId => this.users.get(socketId))
.filter(Boolean);
return {
roomId,
memberCount: members.size,
members: users.map(u => ({ id: u.id, username: u.username })),
};
}
// Broadcast ke semua connected client
broadcastToAll(event: string, data: any) {
this.server.emit(event, data);
}
// Broadcast ke specific room
broadcastToRoom(roomId: string, event: string, data: any) {
this.server.to(roomId).emit(event, data);
}
}Gateway ini mengimplementasikan:
Amankan koneksi WebSocket dengan authentication:
import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';
@Injectable()
export class WsAuthGuard implements CanActivate {
private readonly logger = new Logger(WsAuthGuard.name);
canActivate(context: ExecutionContext): boolean {
const client: Socket = context.switchToWs().getClient();
const token = client.handshake.auth.token;
if (!token) {
this.logger.warn(`Connection rejected: No token provided`);
throw new WsException('Unauthorized');
}
try {
// Verify JWT token (simplified untuk demo)
// Di production, gunakan proper JWT verification
const decoded = this.verifyToken(token);
// Attach user info ke socket
client.data.user = decoded;
return true;
} catch (error) {
this.logger.warn(`Connection rejected: Invalid token`);
throw new WsException('Unauthorized');
}
}
private verifyToken(token: string): any {
// Implementasi proper JWT verification
// Ini adalah contoh simplified
try {
const payload = JSON.parse(
Buffer.from(token.split('.')[1], 'base64').toString()
);
return payload;
} catch {
throw new Error('Invalid token');
}
}
}Terapkan guard ke specific message handler:
@UseGuards(WsAuthGuard)
@SubscribeMessage('message:send')
handleMessage(@MessageBody() data: any, @ConnectedSocket() client: Socket) {
// Handler code
}Pisahkan business logic dari gateway:
import { Injectable, Logger } from '@nestjs/common';
export interface ChatMessage {
id: string;
roomId: string;
userId: string;
username: string;
content: string;
timestamp: Date;
}
export interface ChatRoom {
id: string;
name: string;
createdAt: Date;
createdBy: string;
}
@Injectable()
export class ChatService {
private readonly logger = new Logger(ChatService.name);
// In-memory storage (gunakan Redis atau database di production)
private messages: Map<string, ChatMessage[]> = new Map();
private rooms: Map<string, ChatRoom> = new Map();
createRoom(id: string, name: string, createdBy: string): ChatRoom {
const room: ChatRoom = {
id,
name,
createdAt: new Date(),
createdBy,
};
this.rooms.set(id, room);
this.messages.set(id, []);
this.logger.log(`Room created: ${name} (${id})`);
return room;
}
getRoom(id: string): ChatRoom | undefined {
return this.rooms.get(id);
}
getAllRooms(): ChatRoom[] {
return Array.from(this.rooms.values());
}
deleteRoom(id: string): boolean {
const deleted = this.rooms.delete(id);
this.messages.delete(id);
if (deleted) {
this.logger.log(`Room deleted: ${id}`);
}
return deleted;
}
saveMessage(message: ChatMessage): void {
const roomMessages = this.messages.get(message.roomId) || [];
roomMessages.push(message);
this.messages.set(message.roomId, roomMessages);
// Simpan hanya 100 pesan terakhir per room
if (roomMessages.length > 100) {
this.messages.set(message.roomId, roomMessages.slice(-100));
}
}
getMessages(roomId: string, limit: number = 50): ChatMessage[] {
const messages = this.messages.get(roomId) || [];
return messages.slice(-limit);
}
getMessageById(roomId: string, messageId: string): ChatMessage | undefined {
const messages = this.messages.get(roomId) || [];
return messages.find(m => m.id === messageId);
}
deleteMessage(roomId: string, messageId: string): boolean {
const messages = this.messages.get(roomId);
if (!messages) {
return false;
}
const index = messages.findIndex(m => m.id === messageId);
if (index === -1) {
return false;
}
messages.splice(index, 1);
this.logger.log(`Message deleted: ${messageId} from room ${roomId}`);
return true;
}
searchMessages(roomId: string, query: string): ChatMessage[] {
const messages = this.messages.get(roomId) || [];
const lowerQuery = query.toLowerCase();
return messages.filter(m =>
m.content.toLowerCase().includes(lowerQuery) ||
m.username.toLowerCase().includes(lowerQuery)
);
}
getMessageStats(roomId: string): {
total: number;
byUser: Record<string, number>;
firstMessage: Date | null;
lastMessage: Date | null;
} {
const messages = this.messages.get(roomId) || [];
const byUser: Record<string, number> = {};
messages.forEach(m => {
byUser[m.username] = (byUser[m.username] || 0) + 1;
});
return {
total: messages.length,
byUser,
firstMessage: messages[0]?.timestamp || null,
lastMessage: messages[messages.length - 1]?.timestamp || null,
};
}
}Tambahkan HTTP endpoint untuk chat management:
import {
Controller,
Get,
Post,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ChatService } from './chat.service';
import { ChatGateway } from './chat.gateway';
@Controller('chat')
export class ChatController {
constructor(
private readonly chatService: ChatService,
private readonly chatGateway: ChatGateway,
) {}
@Get('rooms')
getRooms() {
return {
rooms: this.chatService.getAllRooms(),
};
}
@Get('rooms/:id')
getRoom(@Param('id') id: string) {
const room = this.chatService.getRoom(id);
if (!room) {
return { error: 'Room not found' };
}
const info = this.chatGateway.getRoomInfo(id);
return {
room,
...info,
};
}
@Post('rooms')
@HttpCode(HttpStatus.CREATED)
createRoom(
@Body() body: { id: string; name: string; createdBy: string },
) {
const room = this.chatService.createRoom(
body.id,
body.name,
body.createdBy,
);
// Notify semua connected client
this.chatGateway.broadcastToAll('room:created', room);
return { room };
}
@Delete('rooms/:id')
@HttpCode(HttpStatus.NO_CONTENT)
deleteRoom(@Param('id') id: string) {
const deleted = this.chatService.deleteRoom(id);
if (deleted) {
// Notify semua connected client
this.chatGateway.broadcastToAll('room:deleted', { roomId: id });
}
return { success: deleted };
}
@Get('rooms/:id/messages')
getMessages(
@Param('id') id: string,
@Query('limit') limit?: string,
) {
const messages = this.chatService.getMessages(
id,
limit ? parseInt(limit, 10) : 50,
);
return { messages };
}
@Get('rooms/:id/messages/search')
searchMessages(
@Param('id') id: string,
@Query('q') query: string,
) {
if (!query) {
return { error: 'Query parameter is required' };
}
const messages = this.chatService.searchMessages(id, query);
return { messages, count: messages.length };
}
@Get('rooms/:id/stats')
getRoomStats(@Param('id') id: string) {
const stats = this.chatService.getMessageStats(id);
return { stats };
}
@Post('rooms/:id/broadcast')
@HttpCode(HttpStatus.OK)
broadcastToRoom(
@Param('id') id: string,
@Body() body: { event: string; data: any },
) {
this.chatGateway.broadcastToRoom(id, body.event, body.data);
return {
success: true,
message: `Broadcast sent to room ${id}`,
};
}
}Gabungkan semuanya:
import { Module } from '@nestjs/common';
import { ChatGateway } from './chat.gateway';
import { ChatService } from './chat.service';
import { ChatController } from './chat.controller';
@Module({
controllers: [ChatController],
providers: [ChatGateway, ChatService],
exports: [ChatGateway, ChatService],
})
export class ChatModule {}import { Module } from '@nestjs/common';
import { ChatModule } from './chat/chat.module';
@Module({
imports: [ChatModule],
})
export class AppModule {}Konfigurasi CORS untuk WebSocket:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable CORS untuk HTTP
app.enableCors({
origin: '*', // Konfigurasi dengan benar di production
credentials: true,
});
await app.listen(3000);
console.log('WebSocket Chat Server running on http://localhost:3000');
console.log('WebSocket endpoint: ws://localhost:3000/chat');
}
bootstrap();Buat TypeScript client untuk aplikasi chat:
import { io, Socket } from 'socket.io-client';
interface ChatClientOptions {
url: string;
username: string;
token?: string;
}
export class ChatClient {
private socket: Socket;
private username: string;
constructor(options: ChatClientOptions) {
this.username = options.username;
this.socket = io(`${options.url}/chat`, {
auth: {
username: options.username,
token: options.token,
},
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5,
});
this.setupEventHandlers();
}
private setupEventHandlers() {
this.socket.on('connect', () => {
console.log('Connected to chat server');
});
this.socket.on('disconnect', (reason) => {
console.log('Disconnected:', reason);
});
this.socket.on('connect_error', (error) => {
console.error('Connection error:', error.message);
});
this.socket.on('reconnect', (attemptNumber) => {
console.log('Reconnected after', attemptNumber, 'attempts');
});
}
// Join room
joinRoom(roomId: string): Promise<any> {
return new Promise((resolve, reject) => {
this.socket.emit('room:join', { roomId }, (response: any) => {
if (response.error) {
reject(new Error(response.error));
} else {
resolve(response);
}
});
});
}
// Leave room
leaveRoom(roomId: string): Promise<any> {
return new Promise((resolve, reject) => {
this.socket.emit('room:leave', { roomId }, (response: any) => {
if (response.error) {
reject(new Error(response.error));
} else {
resolve(response);
}
});
});
}
// Kirim message ke room
sendMessage(roomId: string, content: string): Promise<any> {
return new Promise((resolve, reject) => {
this.socket.emit(
'message:send',
{ roomId, content },
(response: any) => {
if (response.error) {
reject(new Error(response.error));
} else {
resolve(response);
}
}
);
});
}
// Kirim private message
sendPrivateMessage(recipientId: string, content: string): Promise<any> {
return new Promise((resolve, reject) => {
this.socket.emit(
'message:private',
{ recipientId, content },
(response: any) => {
if (response.error) {
reject(new Error(response.error));
} else {
resolve(response);
}
}
);
});
}
// Typing indicator
startTyping(roomId: string) {
this.socket.emit('typing:start', { roomId });
}
stopTyping(roomId: string) {
this.socket.emit('typing:stop', { roomId });
}
// Event listener
onMessage(callback: (message: any) => void) {
this.socket.on('message:received', callback);
}
onPrivateMessage(callback: (message: any) => void) {
this.socket.on('message:private', callback);
}
onUserJoined(callback: (data: any) => void) {
this.socket.on('user:joined', callback);
}
onUserLeft(callback: (data: any) => void) {
this.socket.on('user:left', callback);
}
onUserConnected(callback: (data: any) => void) {
this.socket.on('user:connected', callback);
}
onUserDisconnected(callback: (data: any) => void) {
this.socket.on('user:disconnected', callback);
}
onTypingStart(callback: (data: any) => void) {
this.socket.on('typing:start', callback);
}
onTypingStop(callback: (data: any) => void) {
this.socket.on('typing:stop', callback);
}
onUsersList(callback: (users: any[]) => void) {
this.socket.on('users:list', callback);
}
// Disconnect
disconnect() {
this.socket.disconnect();
}
// Dapatkan connection status
isConnected(): boolean {
return this.socket.connected;
}
}
// Contoh penggunaan
async function example() {
const client = new ChatClient({
url: 'http://localhost:3000',
username: 'John Doe',
});
// Listen untuk message
client.onMessage((message) => {
console.log(`[${message.username}]: ${message.content}`);
});
// Listen untuk user event
client.onUserJoined((data) => {
console.log(`${data.username} joined ${data.roomId}`);
});
// Join room
await client.joinRoom('general');
// Kirim message
await client.sendMessage('general', 'Hello, everyone!');
// Typing indicator
client.startTyping('general');
setTimeout(() => client.stopTyping('general'), 2000);
}Bangun React chat component:
import { useEffect, useState, useRef } from 'react';
import { ChatClient } from '../chat-client';
interface Message {
id: string;
username: string;
content: string;
timestamp: Date;
}
export function ChatRoom({ roomId, username }: { roomId: string; username: string }) {
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState('');
const [typingUsers, setTypingUsers] = useState<Set<string>>(new Set());
const [onlineUsers, setOnlineUsers] = useState<string[]>([]);
const clientRef = useRef<ChatClient | null>(null);
const typingTimeoutRef = useRef<NodeJS.Timeout>();
useEffect(() => {
// Initialize client
const client = new ChatClient({
url: 'http://localhost:3000',
username,
});
clientRef.current = client;
// Setup event listener
client.onMessage((message) => {
setMessages((prev) => [...prev, message]);
});
client.onUserJoined((data) => {
console.log(`${data.username} joined`);
});
client.onUserLeft((data) => {
console.log(`${data.username} left`);
});
client.onTypingStart((data) => {
setTypingUsers((prev) => new Set(prev).add(data.username));
});
client.onTypingStop((data) => {
setTypingUsers((prev) => {
const next = new Set(prev);
next.delete(data.username);
return next;
});
});
client.onUsersList((users) => {
setOnlineUsers(users.map((u: any) => u.username));
});
// Join room
client.joinRoom(roomId);
// Cleanup
return () => {
client.leaveRoom(roomId);
client.disconnect();
};
}, [roomId, username]);
const handleSendMessage = async () => {
if (!inputValue.trim() || !clientRef.current) return;
await clientRef.current.sendMessage(roomId, inputValue);
setInputValue('');
// Stop typing indicator
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
clientRef.current.stopTyping(roomId);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
if (!clientRef.current) return;
// Start typing indicator
clientRef.current.startTyping(roomId);
// Clear existing timeout
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
// Stop typing setelah 2 detik inactivity
typingTimeoutRef.current = setTimeout(() => {
clientRef.current?.stopTyping(roomId);
}, 2000);
};
return (
<div className="chat-room">
<div className="sidebar">
<h3>Online Users ({onlineUsers.length})</h3>
<ul>
{onlineUsers.map((user) => (
<li key={user}>{user}</li>
))}
</ul>
</div>
<div className="main">
<div className="messages">
{messages.map((message) => (
<div key={message.id} className="message">
<strong>{message.username}:</strong> {message.content}
<span className="timestamp">
{new Date(message.timestamp).toLocaleTimeString()}
</span>
</div>
))}
</div>
{typingUsers.size > 0 && (
<div className="typing-indicator">
{Array.from(typingUsers).join(', ')} {typingUsers.size === 1 ? 'is' : 'are'} typing...
</div>
)}
<div className="input-area">
<input
type="text"
value={inputValue}
onChange={handleInputChange}
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
placeholder="Type a message..."
/>
<button onClick={handleSendMessage}>Send</button>
</div>
</div>
</div>
);
}Gunakan namespace untuk mengisolasi aplikasi berbeda:
@WebSocketGateway({ namespace: '/chat' })
export class ChatGateway {
// Fungsionalitas chat
}
@WebSocketGateway({ namespace: '/notifications' })
export class NotificationsGateway {
// Fungsionalitas notification
}
@WebSocketGateway({ namespace: '/admin' })
export class AdminGateway {
// Fungsionalitas admin
}Client connect ke specific namespace:
const chatSocket = io('http://localhost:3000/chat');
const notifSocket = io('http://localhost:3000/notifications');Gunakan room untuk broadcast ke specific group secara efisien:
// Join multiple room
client.join('room1');
client.join('room2');
client.join(`user:${userId}`); // User-specific room
// Broadcast ke specific room
this.server.to('room1').emit('event', data);
// Broadcast ke multiple room
this.server.to(['room1', 'room2']).emit('event', data);
// Broadcast ke semua kecuali sender
client.broadcast.emit('event', data);
// Broadcast ke room kecuali sender
client.to('room1').broadcast.emit('event', data);Pastikan message delivery dengan acknowledgment:
@SubscribeMessage('message:send')
handleMessage(
@MessageBody() data: any,
@ConnectedSocket() client: Socket,
) {
// Process message
const messageId = this.saveMessage(data);
// Return acknowledgment
return {
success: true,
messageId,
timestamp: new Date(),
};
}Client menerima acknowledgment:
socket.emit('message:send', { content: 'Hello' }, (ack) => {
console.log('Message delivered:', ack.messageId);
});Kirim binary data secara efisien:
@SubscribeMessage('file:upload')
handleFileUpload(
@MessageBody() data: { filename: string; buffer: Buffer },
@ConnectedSocket() client: Socket,
) {
// Process binary data
const fileId = this.saveFile(data.filename, data.buffer);
return {
success: true,
fileId,
size: data.buffer.length,
};
}Client mengirim binary:
const fileBuffer = await file.arrayBuffer();
socket.emit('file:upload', {
filename: file.name,
buffer: Buffer.from(fileBuffer),
});Tambahkan middleware untuk logging, authentication, dan rate limiting:
import { Socket } from 'socket.io';
import { Logger } from '@nestjs/common';
export const wsLogger = (socket: Socket, next: (err?: Error) => void) => {
const logger = new Logger('WebSocket');
logger.log(`Client connecting: ${socket.id} from ${socket.handshake.address}`);
// Log semua event
socket.onAny((event, ...args) => {
logger.debug(`Event: ${event} from ${socket.id}`);
});
next();
};
export const wsRateLimit = (socket: Socket, next: (err?: Error) => void) => {
const requests = new Map<string, number[]>();
const WINDOW_MS = 60000; // 1 menit
const MAX_REQUESTS = 100;
const clientId = socket.handshake.address;
const now = Date.now();
const clientRequests = requests.get(clientId) || [];
const recentRequests = clientRequests.filter(time => now - time < WINDOW_MS);
if (recentRequests.length >= MAX_REQUESTS) {
return next(new Error('Rate limit exceeded'));
}
recentRequests.push(now);
requests.set(clientId, recentRequests);
next();
};Terapkan middleware di gateway:
@WebSocketGateway({
namespace: '/chat',
middlewares: [wsLogger, wsRateLimit],
})
export class ChatGateway {
// Gateway code
}Salah: Mengasumsikan koneksi selalu stabil
socket.on('connect', () => {
// Hanya berjalan pada initial connect
loadInitialData();
});Benar: Tangani reconnection dengan benar
socket.on('connect', () => {
loadInitialData();
});
socket.on('reconnect', () => {
// Resync state setelah reconnection
resyncData();
rejoinRooms();
});Salah: Tidak membersihkan listener
useEffect(() => {
socket.on('message', handleMessage);
// Missing cleanup!
}, []);Benar: Selalu cleanup
useEffect(() => {
socket.on('message', handleMessage);
return () => {
socket.off('message', handleMessage);
};
}, []);Salah: Mengirim huge object
socket.emit('data', {
users: allUsers, // 10,000 users
messages: allMessages, // 100,000 messages
});Benar: Paginate dan compress
socket.emit('data', {
users: recentUsers.slice(0, 50),
hasMore: true,
});
// Atau gunakan compression
socket.emit('data', compressData(largeObject));Salah: Mempercayai client data
@SubscribeMessage('message:send')
handleMessage(@MessageBody() data: any) {
// Tidak ada validation!
this.saveMessage(data);
}Benar: Validasi semuanya
@SubscribeMessage('message:send')
handleMessage(@MessageBody() data: any) {
if (!data.content || typeof data.content !== 'string') {
throw new WsException('Invalid message content');
}
if (data.content.length > 1000) {
throw new WsException('Message too long');
}
this.saveMessage(data);
}Salah: Tidak ada error handling
@SubscribeMessage('message:send')
async handleMessage(@MessageBody() data: any) {
await this.database.save(data); // Bisa throw!
}Benar: Handle error dengan graceful
@SubscribeMessage('message:send')
async handleMessage(@MessageBody() data: any, @ConnectedSocket() client: Socket) {
try {
await this.database.save(data);
return { success: true };
} catch (error) {
this.logger.error('Failed to save message:', error);
client.emit('error', {
message: 'Failed to send message',
code: 'MESSAGE_SAVE_FAILED',
});
return { success: false, error: 'Internal error' };
}
}Deteksi dead connection:
@WebSocketGateway()
export class ChatGateway implements OnGatewayConnection {
private readonly pingInterval = 30000; // 30 detik
private readonly pongTimeout = 5000; // 5 detik
handleConnection(client: Socket) {
let isAlive = true;
const pingTimer = setInterval(() => {
if (!isAlive) {
clearInterval(pingTimer);
client.disconnect();
return;
}
isAlive = false;
client.emit('ping');
setTimeout(() => {
if (!isAlive) {
clearInterval(pingTimer);
client.disconnect();
}
}, this.pongTimeout);
}, this.pingInterval);
client.on('pong', () => {
isAlive = true;
});
client.on('disconnect', () => {
clearInterval(pingTimer);
});
}
}Scale di multiple server:
npm install @socket.io/redis-adapter redisimport { 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;
}
}
// Di main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const redisIoAdapter = new RedisIoAdapter(app);
await redisIoAdapter.connectToRedis();
app.useWebSocketAdapter(redisIoAdapter);
await app.listen(3000);
}Handle high message volume:
import { Injectable } from '@nestjs/common';
import { Queue } from 'bull';
import { InjectQueue } from '@nestjs/bull';
@Injectable()
export class MessageQueueService {
constructor(
@InjectQueue('messages') private messageQueue: Queue,
) {}
async queueMessage(message: any) {
await this.messageQueue.add('process', message, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000,
},
});
}
}
@Processor('messages')
export class MessageProcessor {
@Process('process')
async handleMessage(job: Job) {
const message = job.data;
// Process message (save ke DB, kirim notification, dll.)
await this.processMessage(message);
}
}Track WebSocket health:
import { Injectable } from '@nestjs/common';
import { Counter, Gauge, Histogram } from 'prom-client';
@Injectable()
export class WebSocketMetrics {
private readonly connectionsGauge = new Gauge({
name: 'websocket_connections_total',
help: 'Total number of WebSocket connections',
});
private readonly messagesCounter = new Counter({
name: 'websocket_messages_total',
help: 'Total number of messages',
labelNames: ['event', 'status'],
});
private readonly messageLatency = new Histogram({
name: 'websocket_message_duration_seconds',
help: 'Message processing duration',
labelNames: ['event'],
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
});
incrementConnections() {
this.connectionsGauge.inc();
}
decrementConnections() {
this.connectionsGauge.dec();
}
recordMessage(event: string, status: 'success' | 'error') {
this.messagesCounter.inc({ event, status });
}
recordLatency(event: string, duration: number) {
this.messageLatency.observe({ event }, duration);
}
}Handle server restart dengan benar:
import { Injectable, OnModuleDestroy } from '@nestjs/common';
@Injectable()
export class ChatGateway implements OnModuleDestroy {
async onModuleDestroy() {
// Notify semua client tentang shutdown
this.server.emit('server:shutdown', {
message: 'Server is restarting. Please reconnect in a moment.',
reconnectDelay: 5000,
});
// Tunggu message terkirim
await new Promise(resolve => setTimeout(resolve, 1000));
// Close semua connection dengan graceful
this.server.close();
}
}Gunakan WSS (WebSocket Secure) di production:
import { NestFactory } from '@nestjs/core';
import { readFileSync } from 'fs';
import * as https from 'https';
async function bootstrap() {
const httpsOptions = {
key: readFileSync('./secrets/private-key.pem'),
cert: readFileSync('./secrets/certificate.pem'),
};
const app = await NestFactory.create(AppModule, {
httpsOptions,
});
await app.listen(3000);
console.log('Secure WebSocket server running on wss://localhost:3000');
}Meskipun kuat, WebSocket adalah pilihan yang salah dalam banyak skenario:
Jika Anda membangun standard CRUD API, WebSocket adalah overkill:
Mengapa: REST API lebih sederhana, cacheable, dan lebih didukung oleh infrastructure (CDN, load balancer, proxy).
Alternatif: Gunakan REST dengan HTTP/2 untuk performance lebih baik.
Jika update terjadi jarang (setiap beberapa menit atau jam), WebSocket membuang resource:
Mengapa: Mempertahankan persistent connection untuk update yang jarang itu tidak efisien. Polling atau SSE lebih baik.
Alternatif: Gunakan HTTP polling dengan interval yang sesuai atau Server-Sent Events.
WebSocket tidak dirancang untuk large file transfer:
Mengapa: HTTP memiliki dukungan lebih baik untuk range request, resumable download, dan CDN caching.
Alternatif: Gunakan standard HTTP download dengan progress tracking.
Search engine tidak mengeksekusi koneksi WebSocket:
Mengapa: Konten yang dimuat via WebSocket tidak akan diindex oleh search engine.
Alternatif: Gunakan server-side rendering dengan HTTP untuk initial content, WebSocket untuk update.
Jika arsitektur Anda memerlukan stateless service, WebSocket menciptakan tantangan:
Mengapa: Koneksi WebSocket adalah stateful. Load balancing dan scaling menjadi kompleks.
Alternatif: Gunakan HTTP API atau message queue untuk service-to-service communication.
Koneksi WebSocket menguras battery di mobile device:
Mengapa: Persistent connection mencegah device masuk deep sleep.
Alternatif: Gunakan push notification (FCM/APNS) untuk mobile app, WebSocket hanya ketika app aktif.
Mari kita bangun comprehensive live trading dashboard yang mendemonstrasikan kekuatan WebSocket dalam skenario finansial realistis.
Anda membangun cryptocurrency trading platform dengan:
export interface PriceTick {
symbol: string;
price: number;
volume: number;
change24h: number;
timestamp: Date;
}
export interface OrderBookEntry {
price: number;
amount: number;
total: number;
}
export interface OrderBook {
symbol: string;
bids: OrderBookEntry[];
asks: OrderBookEntry[];
timestamp: Date;
}
export interface Trade {
id: string;
symbol: string;
side: 'buy' | 'sell';
price: number;
amount: number;
userId: string;
status: 'pending' | 'filled' | 'cancelled';
timestamp: Date;
}
export interface Portfolio {
userId: string;
balances: Record<string, number>;
totalValue: number;
change24h: number;
}
export interface Alert {
id: string;
userId: string;
symbol: string;
condition: 'above' | 'below';
targetPrice: number;
triggered: boolean;
}Contoh real-world ini mendemonstrasikan WebSocket handling high-frequency update, multiple data stream, dan user-specific notification—persis yang dibutuhkan platform finansial.
WebSocket telah merevolusi aplikasi web real-time dengan menyediakan komunikasi bidirectional yang efisien melalui single persistent connection. Dari aplikasi chat hingga live dashboard, collaborative tool hingga multiplayer game, WebSocket memungkinkan pengalaman instant dan interaktif yang diharapkan user.
Insight kunci:
WebSocket unggul ketika Anda memerlukan komunikasi bidirectional real-time sejati dengan latency minimal. Model persistent connection-nya menghilangkan polling overhead dan memungkinkan instant update di kedua arah.
WebSocket kesulitan dengan pattern request-response sederhana, update yang jarang, dan arsitektur stateless. Untuk skenario ini, REST, SSE, atau polling tetap menjadi pilihan lebih baik.
Implementasi NestJS mendemonstrasikan bahwa WebSocket dapat terintegrasi seamlessly dengan framework modern sambil mempertahankan scalability dan production-readiness. Dengan memahami kekuatan dan keterbatasan WebSocket, Anda dapat membuat keputusan informed tentang kapan ini adalah tool yang tepat untuk fitur real-time Anda.
Jika Anda membangun aplikasi yang memerlukan instant update, user presence, atau fitur kolaboratif, WebSocket layak dipertimbangkan serius. Kompleksitas managing persistent connection diimbangi oleh superior user experience dan efficient resource usage yang disediakannya.