1. Pengenalan WebSocket
WebSocket adalah protokol komunikasi yang menyediakan koneksi dua arah (full-duplex) antara client (browser) dan server melalui satu koneksi TCP yang persisten. Berbeda dari HTTP yang bersifat request-response, WebSocket memungkinkan server mengirim data ke client kapan saja tanpa diminta.
Mengapa WebSocket?
HTTP tradisional bekerja dengan model request-response: client mengirim request, server mengirim response, lalu koneksi ditutup. Untuk aplikasi real-time seperti chat, game, atau live dashboard, model ini sangat tidak efisien karena:
| Teknik | Cara Kerja | Kelebihan | Kekurangan |
|---|---|---|---|
| Polling | Client request berulang | Sederhana | Boros bandwidth, latency tinggi |
| Long Polling | Request ditahan sampai ada data | Lebih efisien | Overhead HTTP headers tiap request |
| Server-Sent Events | Server push via HTTP | Satu arah, mudah | Hanya server → client |
| WebSocket | Koneksi persisten dua arah | Low latency, real-time, efisien | Butuh setup lebih, scaling lebih kompleks |
Use Cases
- 💬 Chat & Messaging — WhatsApp Web, Telegram Web, Slack
- 📊 Live Dashboard — Grafik harga saham, monitoring server
- 🎮 Multiplayer Games — Game online, turn-based
- 📝 Collaborative Editing — Google Docs, Figma
- 🔔 Notifications — Push notification real-time
- 📈 Live Feeds — Berita, social media updates
- 📍 Location Tracking — GPS tracking, ride-hailing
- 🎓 Live Auction — Lelang online, bidding
WebSocket bukan selalu jawaban terbaik. Jika kamu hanya butuh server → client (satu arah), Server-Sent Events (SSE) lebih sederhana. Jika datanya jarang berubah, polling mungkin cukup.
2. WebSocket Protocol
WebSocket menggunakan protokol ws:// (atau wss:// untuk SSL/TLS) dan dimulai dengan HTTP Upgrade handshake. Setelah handshake berhasil, koneksi beralih dari HTTP ke WebSocket dan bisa mengirim data secara dua arah.
Handshake Process
# Client Request (HTTP Upgrade) GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 # Server Response (101 Switching Protocols) HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Frame Format
Setelah koneksi terbuka, data dikirim dalam bentuk WebSocket frames. Setiap frame memiliki header kecil yang berisi informasi seperti tipe data (text, binary, close, ping, pong) dan masking:
Frame Structure: ┌─────────┬─────────┬────────────┬────────────────┐ │ FIN(1) │ RSV(3) │ Opcode(4) │ MASK + LEN │ ├─────────┼─────────┼────────────┼────────────────┤ │ 1 bit │ 3 bit │ 0x1 = Text │ 1 bit mask │ │ │ │ 0x2 = Bin │ 7 bit length │ │ │ │ 0x8 = Close│ + extended len │ │ │ │ 0x9 = Ping │ │ │ │ │ 0xA = Pong │ │ ├─────────┴─────────┴────────────┴────────────────┤ │ Masking Key (32 bit, jika mask=1) │ ├─────────────────────────────────────────────────┤ │ Payload Data (Application Data) │ └─────────────────────────────────────────────────┘
WebSocket vs HTTP
| Aspek | HTTP | WebSocket |
|---|---|---|
| Koneksi | Baru setiap request | Persisten (satu koneksi) |
| Arah | Client → Server | Dua arah (full-duplex) |
| Overhead | Headers di setiap request | Headers hanya saat handshake |
| Latency | Tinggi | Sangat rendah |
| Data Format | Teks/binary (HTTP message) | Teks/binary (frames) |
| Scalability | Mudah (stateless) | Lebih kompleks (stateful) |
3. WebSocket Native API (Browser)
Browser modern sudah memiliki WebSocket API bawaan. Ini adalah API yang paling dasar — tanpa library tambahan. Mari kita pelajari cara menggunakannya.
Membuat Koneksi
// Membuat koneksi WebSocket baru
const ws = new WebSocket('ws://localhost:8080');
// Atau dengan secure connection (wss://)
const wsSecure = new WebSocket('wss://api.example.com/ws');
// Dengan protocols (opsional)
const wsWithProtocol = new WebSocket('ws://localhost:8080', ['chat', 'notifications']);
Event Handlers
const ws = new WebSocket('ws://localhost:8080');
// Event: koneksi berhasil dibuka
ws.onopen = (event) => {
console.log('✅ Terhubung ke WebSocket server');
console.log('URL:', ws.url);
console.log('Protocol:', ws.protocol);
// Kirim pesan pertama setelah terhubung
ws.send(JSON.stringify({
type: 'join',
username: 'Budi',
room: 'general',
}));
};
// Event: menerima pesan dari server
ws.onmessage = (event) => {
console.log('📩 Pesan diterima:', event.data);
// Parse jika data berupa JSON
try {
const data = JSON.parse(event.data);
switch (data.type) {
case 'chat':
displayMessage(data.username, data.message);
break;
case 'user_joined':
showNotification(`${data.username} bergabung`);
break;
case 'user_left':
showNotification(`${data.username} keluar`);
break;
}
} catch (e) {
console.log('Raw message:', event.data);
}
};
// Event: koneksi ditutup
ws.onclose = (event) => {
console.log('❌ Koneksi ditutup');
console.log('Code:', event.code);
console.log('Reason:', event.reason);
console.log('Clean close:', event.wasClean);
// Reconnect jika bukan close yang disengaja
if (!event.wasClean) {
setTimeout(() => reconnect(), 3000);
}
};
// Event: error
ws.onerror = (error) => {
console.error('🔴 WebSocket error:', error);
};
Mengirim Data
// Kirim teks
ws.send('Hello, Server!');
// Kirim JSON
ws.send(JSON.stringify({
type: 'chat',
message: 'Halo semua!',
timestamp: Date.now(),
}));
// Kirim binary data (Blob)
const blob = new Blob(['binary data'], { type: 'application/octet-stream' });
ws.send(blob);
// Kirim binary data (ArrayBuffer)
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setInt32(0, 42);
ws.send(buffer);
// Cek status koneksi sebelum mengirim
function safeSend(data) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(typeof data === 'string' ? data : JSON.stringify(data));
return true;
}
console.warn('WebSocket tidak terbuka, pesan tidak terkirim');
return false;
}
WebSocket States
| State | Constant | Penjelasan |
|---|---|---|
| CONNECTING | WebSocket.CONNECTING (0) |
Koneksi sedang dibuat |
| OPEN | WebSocket.OPEN (1) |
Koneksi terbuka, bisa mengirim/menerima |
| CLOSING | WebSocket.CLOSING (2) |
Koneksi sedang ditutup |
| CLOSED | WebSocket.CLOSED (3) |
Koneksi sudah ditutup |
4. WebSocket di Node.js
Untuk membuat WebSocket server di Node.js, kita bisa menggunakan library ws yang sangat ringan dan populer.
Instalasi
# Instal ws library npm install ws # Instal types untuk TypeScript npm install -D @types/ws
Basic WebSocket Server
import { WebSocketServer, WebSocket } from 'ws';
// Buat WebSocket server
const wss = new WebSocketServer({ port: 8080 });
// Set koneksi baru
wss.on('connection', (ws: WebSocket, req) => {
const clientIp = req.socket.remoteAddress;
console.log(`Client terhubung dari ${clientIp}`);
console.log(`Total klien: ${wss.clients.size}`);
// Kirim welcome message
ws.send(JSON.stringify({
type: 'welcome',
message: 'Selamat datang di WebSocket server!',
totalClients: wss.clients.size,
}));
// Handle pesan masuk
ws.on('message', (data) => {
const message = data.toString();
console.log('Pesan diterima:', message);
try {
const parsed = JSON.parse(message);
switch (parsed.type) {
case 'chat':
// Broadcast ke semua client lain
wss.clients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
type: 'chat',
username: parsed.username,
message: parsed.message,
timestamp: Date.now(),
}));
}
});
break;
case 'ping':
ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
break;
}
} catch (e) {
// Echo pesan kembali
ws.send(`Echo: ${message}`);
}
});
// Handle koneksi ditutup
ws.on('close', (code, reason) => {
console.log(`Client terputus: ${code} - ${reason}`);
});
// Handle error
ws.on('error', (error) => {
console.error('WebSocket error:', error.message);
});
});
console.log('WebSocket server berjalan di ws://localhost:8080');
WebSocket Server dengan Express + HTTP
import express from 'express';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
const app = express();
const server = createServer(app);
// Attach WebSocket ke HTTP server yang sama
const wss = new WebSocketServer({ server });
// REST API endpoint (biasa)
app.get('/health', (req, res) => {
res.json({
status: 'ok',
wsClients: wss.clients.size,
uptime: process.uptime(),
});
});
// WebSocket handler
wss.on('connection', (ws) => {
console.log('New WebSocket connection');
ws.on('message', (data) => {
// Broadcast ke semua client
wss.clients.forEach((client) => {
if (client.readyState === ws.OPEN) {
client.send(data.toString());
}
});
});
});
// Start server di port yang sama
server.listen(3000, () => {
console.log('Server berjalan di http://localhost:3000');
console.log('WebSocket tersedia di ws://localhost:3000');
});
5. Socket.IO Library
Socket.IO adalah library paling populer untuk real-time communication. Socket.IO dibangun di atas WebSocket tetapi menambahkan banyak fitur penting: auto-reconnection, rooms, namespaces, binary support, dan fallback ke polling jika WebSocket tidak tersedia.
Instalasi
# Server npm install socket.io # Client (untuk Node.js client) npm install socket.io-client
Socket.IO Server
import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';
const app = express();
const server = createServer(app);
const io = new Server(server, {
cors: {
origin: 'http://localhost:5173',
methods: ['GET', 'POST'],
},
pingTimeout: 60000,
pingInterval: 25000,
});
// Middleware: autentikasi
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (isValidToken(token)) {
socket.data.user = decodeToken(token);
next();
} else {
next(new Error('Authentication error'));
}
});
// Handle connection
io.on('connection', (socket) => {
console.log(`User terhubung: ${socket.id}`);
console.log(`Username: ${socket.data.user?.name}`);
// Custom events
socket.on('chat:message', (data) => {
console.log(`Pesan dari ${data.username}: ${data.message}`);
// Broadcast ke semua client kecuali pengirim
socket.broadcast.emit('chat:message', {
username: data.username,
message: data.message,
timestamp: Date.now(),
});
});
socket.on('chat:typing', (data) => {
socket.broadcast.emit('chat:typing', {
username: data.username,
isTyping: data.isTyping,
});
});
socket.on('disconnect', (reason) => {
console.log(`User ${socket.id} terputus: ${reason}`);
});
});
server.listen(3000, () => {
console.log('Socket.IO server running on port 3000');
});
Socket.IO Client (Browser)
<!-- Include Socket.IO client dari CDN -->
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<script>
// Koneksi ke server
const socket = io('http://localhost:3000', {
auth: { token: 'jwt-token-kamu' },
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
});
// Event: terhubung
socket.on('connect', () => {
console.log('✅ Terhubung:', socket.id);
});
// Event: menerima pesan
socket.on('chat:message', (data) => {
console.log(`${data.username}: ${data.message}`);
addMessageToUI(data);
});
// Event: user sedang mengetik
socket.on('chat:typing', (data) => {
if (data.isTyping) {
showTypingIndicator(data.username);
} else {
hideTypingIndicator(data.username);
}
});
// Event: disconnect
socket.on('disconnect', (reason) => {
console.log('❌ Terputus:', reason);
});
// Event: error
socket.on('connect_error', (error) => {
console.error('Koneksi gagal:', error.message);
});
// Kirim pesan chat
function sendMessage(message) {
socket.emit('chat:message', {
username: currentUser.name,
message: message,
});
}
// Kirim status typing
function setTyping(isTyping) {
socket.emit('chat:typing', {
username: currentUser.name,
isTyping: isTyping,
});
}
</script>
6. Rooms & Namespaces
Socket.IO menyediakan konsep Rooms dan Namespaces untuk mengorganisir koneksi. Rooms memungkinkan kamu mengirim pesan ke subset client tertentu, sementara Namespaces membagi logika ke beberapa endpoint.
Rooms
io.on('connection', (socket) => {
// Bergabung ke room
socket.on('join:room', (roomId) => {
socket.join(roomId);
console.log(`${socket.id} joined room ${roomId}`);
// Beri tahu anggota room
socket.to(roomId).emit('room:user_joined', {
userId: socket.id,
roomId,
});
});
// Keluar dari room
socket.on('leave:room', (roomId) => {
socket.leave(roomId);
socket.to(roomId).emit('room:user_left', {
userId: socket.id,
roomId,
});
});
// Kirim pesan ke room tertentu
socket.on('room:message', ({ roomId, message }) => {
io.to(roomId).emit('room:message', {
userId: socket.id,
username: socket.data.user?.name,
message,
roomId,
timestamp: Date.now(),
});
});
// Kirim ke semua room kecuali room tertentu
socket.on('broadcast:announcement', (data) => {
socket.broadcast.emit('announcement', data);
// Atau ke room spesifik:
// io.to('admin-room').emit('announcement', data);
});
});
// Dapatkan semua client di room
async function getRoomMembers(roomId: string) {
const sockets = await io.in(roomId).fetchSockets();
return sockets.map(s => ({
id: s.id,
username: s.data.user?.name,
}));
}
Namespaces
// Namespace untuk chat
const chatNs = io.of('/chat');
chatNs.on('connection', (socket) => {
console.log(`Chat namespace: ${socket.id}`);
socket.on('message', (data) => {
chatNs.emit('message', data);
});
});
// Namespace untuk notifications
const notifNs = io.of('/notifications');
notifNs.on('connection', (socket) => {
console.log(`Notification namespace: ${socket.id}`);
// Kirim notifikasi ke user tertentu
socket.emit('notif', {
title: 'Selamat datang!',
body: 'Kamu sudah terhubung ke notification service.',
});
});
// Namespace untuk admin
const adminNs = io.of('/admin');
adminNs.use((socket, next) => {
// Middleware khusus admin
if (socket.handshake.auth.role === 'admin') {
next();
} else {
next(new Error('Unauthorized'));
}
});
// Client: koneksi ke namespace spesifik
// const chatSocket = io('http://localhost:3000/chat');
// const notifSocket = io('http://localhost:3000/notifications');
7. Heartbeat & Reconnection
Dalam aplikasi production, koneksi WebSocket bisa terputus karena berbagai alasan: jaringan tidak stabil, server restart, timeout, atau NAT timeout. Implementasi heartbeat dan reconnection sangat penting untuk pengalaman user yang baik.
Custom Heartbeat
// Server-side heartbeat
const HEARTBEAT_INTERVAL = 30000; // 30 detik
const HEARTBEAT_TIMEOUT = 10000; // 10 detik timeout
wss.on('connection', (ws) => {
let isAlive = true;
let heartbeatTimer: NodeJS.Timeout;
// Fungsi heartbeat
function startHeartbeat() {
heartbeatTimer = setInterval(() => {
if (!isAlive) {
console.log('Client tidak merespons, memutus koneksi');
ws.terminate();
return;
}
isAlive = false;
ws.ping(); // Kirim ping
// Tunggu pong
setTimeout(() => {
if (!isAlive) {
ws.terminate();
}
}, HEARTBEAT_TIMEOUT);
}, HEARTBEAT_INTERVAL);
}
// Handle pong response
ws.on('pong', () => {
isAlive = true;
console.log(`Pong dari client ${ws._socket.remoteAddress}`);
});
ws.on('close', () => {
clearInterval(heartbeatTimer);
});
startHeartbeat();
});
Client-side Reconnection
class WebSocketClient {
private ws: WebSocket | null = null;
private url: string;
private reconnectAttempts = 0;
private maxReconnectAttempts = 10;
private reconnectDelay = 1000;
private heartbeatTimer: NodeJS.Timeout | null = null;
constructor(url: string) {
this.url = url;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('✅ Terhubung');
this.reconnectAttempts = 0;
this.startHeartbeat();
this.onConnect();
};
this.ws.onclose = (event) => {
console.log(`❌ Terputus: ${event.code}`);
this.stopHeartbeat();
if (!event.wasClean && this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnect();
}
};
this.ws.onerror = (error) => {
console.error('Error:', error);
};
this.ws.onmessage = (event) => {
this.handleMessage(event.data);
};
}
// Reconnection dengan exponential backoff
reconnect() {
const delay = Math.min(
this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
30000 // Max 30 detik
);
console.log(`Reconnecting dalam ${delay}ms (attempt ${this.reconnectAttempts + 1})`);
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
}
// Heartbeat: ping setiap 30 detik
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
}
send(data: unknown) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
onConnect() { /* Override */ }
handleMessage(data: string) { /* Override */ }
}
// Usage
const client = new WebSocketClient('ws://localhost:8080');
client.onConnect = () => console.log('Connected!');
client.handleMessage = (data) => console.log('Message:', data);
8. Real-time Patterns
Pattern 1: Broadcast
// Broadcast: kirim ke semua client
io.on('connection', (socket) => {
socket.on('broadcast', (message) => {
// Kirim ke semua termasuk pengirim
io.emit('broadcast', message);
// Atau kecuali pengirim
// socket.broadcast.emit('broadcast', message);
});
});
Pattern 2: Targeted Messaging
// Private messaging: kirim ke user tertentu
io.on('connection', (socket) => {
// Simpan mapping userId ke socketId
const userSockets = new Map<string, string>();
socket.on('register', (userId) => {
userSockets.set(userId, socket.id);
});
socket.on('private:message', ({ toUserId, message }) => {
const targetSocketId = userSockets.get(toUserId);
if (targetSocketId) {
io.to(targetSocketId).emit('private:message', {
from: socket.data.user?.name,
message,
timestamp: Date.now(),
});
}
});
});
Pattern 3: Presence System
// Presence system: siapa yang online
const onlineUsers = new Map<string, { id: string; name: string; status: string }>();
io.on('connection', (socket) => {
const user = socket.data.user;
// Tambahkan ke daftar online
onlineUsers.set(user.id, {
id: user.id,
name: user.name,
status: 'online',
});
// Broadcast status online ke semua
io.emit('presence:update', {
userId: user.id,
status: 'online',
users: Array.from(onlineUsers.values()),
});
// Handle status change
socket.on('presence:status', (status) => {
const userData = onlineUsers.get(user.id);
if (userData) {
userData.status = status; // 'online', 'away', 'busy'
io.emit('presence:update', {
userId: user.id,
status,
users: Array.from(onlineUsers.values()),
});
}
});
// Hapus dari daftar saat disconnect
socket.on('disconnect', () => {
onlineUsers.delete(user.id);
io.emit('presence:update', {
userId: user.id,
status: 'offline',
users: Array.from(onlineUsers.values()),
});
});
});
9. Scaling WebSocket
Scaling WebSocket jauh lebih kompleks dibanding scaling HTTP karena koneksi bersifat stateful. Ketika kamu menambah server, kamu perlu memastikan bahwa pesan yang dikirim dari satu server bisa diteruskan ke client yang terhubung di server lain.
Socket.IO Redis Adapter
import { Server } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
const io = new Server(3000);
// Setup Redis adapter untuk multi-server
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
io.adapter(createAdapter(pubClient, subClient));
console.log('Redis adapter connected - siap untuk scaling!');
});
// Sekarang io.emit() akan mengirim ke semua client
// di semua server yang terhubung ke Redis yang sama
Load Balancing dengan Nginx
# nginx.conf
upstream websocket_backend {
ip_hash; # Sticky sessions untuk WebSocket
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
}
server {
listen 80;
server_name api.example.com;
location /socket.io/ {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 86400; # 24 jam
proxy_send_timeout 86400;
}
}
Horizontal Scaling Architecture
┌─────────────────┐
│ Load Balancer │
│ (Nginx/HAProxy)│
└────────┬────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
│ WS Server │ │ WS Server │ │ WS Server │
│ (3001) │ │ (3002) │ │ (3003) │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└──────────────┼──────────────┘
│
┌────────┴────────┐
│ Redis Pub/Sub │
│ (Message Bus) │
└─────────────────┘
Pastikan menggunakan sticky sessions di load balancer. Tanpa sticky sessions, client bisa terhubung ke server berbeda setiap reconnect, yang menyebabkan masalah state. Socket.IO mendukung ip_hash atau cookie-based affinity.
Scaling Checklist
- ✅ Redis Adapter — Untuk broadcast lintas server
- ✅ Sticky Sessions — IP hash atau cookie affinity
- ✅ Connection Limits — Batasi koneksi per server
- ✅ Monitoring — Pantau jumlah koneksi, memory, CPU
- ✅ Graceful Shutdown — Kirim disconnect event sebelum server mati
- ✅ Rate Limiting — Cegah client spam
- ✅ Horizontal Scaling — Auto-scaling berdasarkan jumlah koneksi
- ✅ Connection Draining — Pindahkan koneksi saat scale down
10. Quiz Pemahaman
Uji pemahamanmu tentang WebSocket: