1. Apa Itu WebSocket?
WebSocket (RFC 6455) adalah protokol komunikasi full-duplex yang beroperasi di atas satu koneksi TCP. Berbeda dengan HTTP yang bersifat request/response, WebSocket memungkinkan client dan server mengirim data secara simultan dan asynchronous setelah koneksi terbentuk.
WebSocket vs HTTP Polling
| Aspek | HTTP Polling | Long Polling | Server-Sent Events | WebSocket |
|---|---|---|---|---|
| Direction | Client β Server | Client β Server | Server β Client | Bidirectional |
| Overhead | ~800 bytes per request | ~800 bytes per poll | HTTP header sekali | 2-6 bytes per frame |
| Latency | Interval-based | Rendah | Rendah | Sangat rendah |
| Real-time | Tidak | Mendekati | Ya (satu arah) | Ya (dua arah) |
| Protocol | HTTP/1.1+ | HTTP/1.1+ | HTTP/1.1+ | ws:// atau wss:// |
| Binary support | Base64 (boros) | Base64 | Base64 | Native binary |
| Use case | Data jarang berubah | Chat ringan | Notifikasi, feed | Chat, gaming, trading |
WebSocket ideal untuk: chat apps (WhatsApp Web), real-time dashboards (monitoring), multiplayer games, collaborative editing (Google Docs), live trading (crypto/forex), dan IoT telemetry. Jika Anda hanya butuh server-to-client, SSE lebih sederhana.
2. WebSocket Handshake
WebSocket menggunakan HTTP Upgrade mechanism untuk memulai koneksi. Client mengirim HTTP request dengan header Upgrade: websocket, server merespons 101 Switching Protocols, dan koneksi berubah dari HTTP ke WebSocket.
Upgrade Handshake Detail
# === 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
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
# === Server Response (101 Switching Protocols) ===
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
# === Penjelasan Header ===
# Sec-WebSocket-Key: random 16-byte nonce, base64-encoded
# β Digenerate client baru setiap koneksi
# β BUKAN security measure, hanya mencegah cache proxy
# Sec-WebSocket-Accept: server menghitung hash dari key client
# β SHA1(Key + "258EAFA5-E914-47DA-95CA-5AB40401B2E7")
# β "magic string" ini adalah GUID tetap dari RFC 6455
# β Client harus verifikasi hash ini!
# Sec-WebSocket-Extensions: permessage-deflate
# β Kompresi message (opsional)
# β Mengurangi bandwidth 50-80% untuk payload text
# Sec-WebSocket-Protocol: subprotocol negotiation
# β Client menawarkan beberapa, server memilih satu
Verifikasi Handshake di JavaScript
// Browser API β handshake dilakukan otomatis
const ws = new WebSocket('wss://server.example.com/chat', ['chat', 'superchat']);
ws.onopen = (event) => {
console.log('WebSocket connected!');
// Browser sudah melakukan:
// 1. Generate random Sec-WebSocket-Key
// 2. Kirim HTTP Upgrade request
// 3. Verifikasi Sec-WebSocket-AcAccept dari server
// 4. Switch ke WebSocket mode
ws.send(JSON.stringify({ type: 'join', room: 'general' }));
};
ws.onmessage = (event) => {
console.log('Received:', event.data);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = (event) => {
console.log(`Closed: code=${event.code}, reason=${event.reason}`);
};
// Server-side (Node.js dengan 'ws' library):
const WebSocket = require('ws');
const crypto = require('crypto');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws, req) => {
console.log('New connection from:', req.socket.remoteAddress);
// WebSocket library sudah handle handshake otomatis
// Tapi kita bisa akses raw headers:
console.log('Origin:', req.headers['origin']);
console.log('Protocol:', ws.protocol);
ws.on('message', (data) => {
console.log('Received:', data.toString());
ws.send(`Echo: ${data}`);
});
});
3. Frame Format & Data Types
WebSocket menggunakan binary framing yang sangat efisien. Setiap frame memiliki overhead hanya 2-6 bytes (tanpa masking key), jauh lebih kecil dari HTTP header.
WebSocket Frame Structure
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| | (jika 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) |
+---------------------------------------------------------------+
# Penjelasan Field:
# FIN (1 bit) : 1 = frame terakhir dari message
# RSV1-3 (3 bit) : Reserved (untuk extensions seperti permessage-deflate)
# OPCODE (4 bit) : Tipe frame
# MASK (1 bit) : 1 = payload di-mask (clientβserver WAJIB mask)
# Payload Length : 0-125 langsung, 126 = 2 byte extended, 127 = 8 byte extended
# Opcodes:
# 0x0 = Continuation frame
# 0x1 = Text frame (UTF-8)
# 0x2 = Binary frame
# 0x8 = Connection close
# 0x9 = Ping
# 0xA = Pong
Contoh Frame Encoding
import struct
import os
def encode_ws_frame(payload, opcode=0x1, mask=True):
"""Encode payload menjadi WebSocket frame"""
frame = bytearray()
# Byte pertama: FIN (1) + RSV (000) + Opcode
frame.append(0x80 | opcode) # 0x80 = FIN bit set
# Byte kedua: MASK bit + payload length
length = len(payload)
if length < 126:
frame.append((0x80 if mask else 0) | length)
elif length < 65536:
frame.append((0x80 if mask else 0) | 126)
frame.extend(struct.pack('>H', length))
else:
frame.append((0x80 if mask else 0) | 127)
frame.extend(struct.pack('>Q', length))
# Masking key (4 bytes random)
if mask:
masking_key = os.urandom(4)
frame.extend(masking_key)
# Mask payload
masked = bytearray(len(payload))
for i in range(len(payload)):
masked[i] = payload[i] ^ masking_key[i % 4]
frame.extend(masked)
else:
frame.extend(payload)
return bytes(frame)
# Contoh: Text frame "Hello"
frame = encode_ws_frame(b'Hello')
print(f"Frame size: {len(frame)} bytes") # 11 bytes (2 header + 4 mask + 5 payload)
print(f"Overhead: {len(frame) - len(b'Hello')} bytes") # 6 bytes overhead
# Contoh: Binary frame dengan data 1000 bytes
import json
data = json.dumps({"temp": 25.3, "humidity": 62}).encode()
frame = encode_ws_frame(data, opcode=0x2)
print(f"Binary frame: {len(frame)} bytes for {len(data)} bytes payload")
4. Ping/Pong & Heartbeat
WebSocket mendukung mekanisme Ping/Pong untuk mendeteksi koneksi yang mati (liveness check) dan menjaga koneksi tetap hidup melalui proxy/firewall yang mungkin akan menutup idle connections.
Mekanisme Ping/Pong
# Ping/Pong Flow:
# 1. Bisa dikirim oleh KEDUA belah pihak (client dan server)
# 2. Pong WAJIB dikirim sebagai respons terhadap Ping
# 3. Ping bisa membawa payload (application data)
# 4. Pong HARUS meng-copy payload dari Ping
# Contoh flow:
Client Server
| |
|--- Ping (frame 0x9) -------->|
| payload: "keepalive" |
| |
|<-- Pong (frame 0xA) ---------|
| payload: "keepalive" | β HARUS sama dengan Ping payload
| |
| ... connection healthy ...|
| |
|--- Ping ---------------------->|
| |
| (timeout, tidak ada Pong) |
| β Koneksi dianggap MATI |
| β Trigger close/onerror |
# Best Practice:
# - Ping interval: 30 detik (normal), 15 detik (high-reliability)
# - Pong timeout: 10 detik (jika tidak ada Pong dalam 10s β close)
# - Jangan terlalu sering (boros bandwidth) atau terlalu jarang (lambat deteksi)
Implementasi Heartbeat di Node.js
const WebSocket = require('ws');
class HeartbeatWebSocket {
constructor(url, options = {}) {
this.url = url;
this.pingInterval = options.pingInterval || 30000; // 30 detik
this.pongTimeout = options.pongTimeout || 10000; // 10 detik
this.isAlive = false;
this.pingTimer = null;
this.pongTimer = null;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.on('open', () => {
console.log('Connected');
this.isAlive = true;
this.startHeartbeat();
});
this.ws.on('pong', (data) => {
// Pong diterima β koneksi hidup
this.isAlive = true;
clearTimeout(this.pongTimer);
console.log(`Pong received: ${data.toString()}`);
});
this.ws.on('close', () => {
console.log('Disconnected');
this.stopHeartbeat();
// Reconnect setelah 3 detik
setTimeout(() => this.connect(), 3000);
});
}
startHeartbeat() {
this.pingTimer = setInterval(() => {
if (!this.isAlive) {
console.log('No pong received, terminating...');
return this.ws.terminate();
}
this.isAlive = false;
this.ws.ping('heartbeat');
// Set timeout untuk pong
this.pongTimer = setTimeout(() => {
if (!this.isAlive) {
console.log('Pong timeout, closing...');
this.ws.close(1000, 'Pong timeout');
}
}, this.pongTimeout);
}, this.pingInterval);
}
stopHeartbeat() {
clearInterval(this.pingTimer);
clearTimeout(this.pongTimer);
}
}
// Server-side heartbeat
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', () => {
ws.isAlive = true;
});
});
// Periodic check β setiap 30 detik
const heartbeatInterval = setInterval(() => {
wss.clients.forEach((ws) => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
wss.on('close', () => clearInterval(heartbeatInterval));
5. Close Handshake
WebSocket memiliki mekanisme graceful close yang memungkinkan kedua belah pihak menutup koneksi dengan terhormat, termasuk mengirim alasan penutupan.
Close Frame Format & Status Codes
# Close Frame Structure:
# OPCODE: 0x8
# Payload: 2-byte status code (big-endian) + optional UTF-8 reason string
# Close Handshake:
# 1. Peer A mengirim Close frame
# 2. Peer B menerima Close frame
# 3. Peer B mengirim Close frame balasan (echo status code)
# 4. TCP connection ditutup
Client Server
| |
|--- Close (1000, "Normal") -->|
| |
|<-- Close (1000) --------------|
| |
| TCP FIN βββββββββββββββββββ> |
| <βββββββββββββββββββββββββ FIN ACK
# Status Codes:
# 1000 β Normal closure
# 1001 β Going away (server shutdown, browser navigate away)
# 1002 β Protocol error
# 1003 β Unsupported data type (text vs binary mismatch)
# 1005 β No status received (internal, tidak dikirim di frame)
# 1006 β Abnormal closure (internal, tidak dikirim di frame)
# 1007 β Invalid frame payload data (misal: UTF-8 encoding error)
# 1008 β Policy violation
# 1009 β Message too big
# 1010 β Mandatory extension not negotiated
# 1011 β Internal server error
# 1012 β Service restart
# 1013 β Try again later
# 1014 β Bad gateway
# 3000-3999 β Registered extensions
# 4000-4999 β Application-defined
6. Scaling & Load Balancing
Scaling WebSocket server adalah tantangan tersendiri karena sifat stateful dari koneksi. Setiap client terikat ke satu server instance, berbeda dengan HTTP stateless yang bisa di-route ke mana saja.
Tantangan Scaling WebSocket
| Tantangan | Penjelasan | Solusi |
|---|---|---|
| Sticky Sessions | Client harus terhubung ke server yang sama | Sticky load balancing berdasarkan IP/cookie |
| State Management | State harus di-share antar server | Redis Pub/Sub sebagai message bus |
| Connection Memory | Setiap koneksi butuh RAM ~20-50KB | Optimasi buffer, horizontal scaling |
| Health Check | Load balancer perlu cek koneksi | Custom health endpoint, ping/pong |
| Graceful Shutdown | Server update tanpa memutus koneksi | Drain connections sebelum restart |
Architecture: Multi-Server dengan Redis Pub/Sub
# === Arsitektur Scaling WebSocket ===
# Client A (terhubung ke Server 1) mengirim message ke Client C (terhubung ke Server 2)
ββββββββββββββββ ββββββββββββββββ
β Client A β β Client B β
β (Server 1) β β (Server 1) β
ββββββββ¬ββββββββ ββββββββ¬ββββββββ
β β
βΌ βΌ
βββββββββββββββββββββββββββββββββββ
β Load Balancer β
β (Sticky by IP / Cookie) β
β Nginx / HAProxy / AWS ALB β
ββββββ¬βββββββββββββ¬ββββββββββββ¬ββββ
βΌ βΌ βΌ
βββββββββββ βββββββββββ βββββββββββ
β Server 1β β Server 2β β Server 3β
β Node.jsβ β Node.jsβ β Node.jsβ
ββββββ¬βββββ ββββββ¬βββββ ββββββ¬βββββ
β β β
βΌ βΌ βΌ
βββββββββββββββββββββββββββββββββββ
β Redis Pub/Sub β
β Channel: "chat:room-1" β
β Channel: "notifications" β
β Channel: "broadcast" β
βββββββββββββββββββββββββββββββββββ
# Flow:
# 1. Client A β Server 1: "Hello everyone!"
# 2. Server 1 β Redis PUBLISH "chat:room-1" {message}
# 3. Server 1, 2, 3 semua SUBSCRIBE "chat:room-1"
# 4. Server 1 β kirim ke Client A, B (yang terhubung ke Server 1)
# 5. Server 2 β kirim ke Client C (yang terhubung ke Server 2)
# 6. Server 3 β kirim ke Client D (yang terhubung ke Server 3)
Implementasi Multi-Server
const WebSocket = require('ws');
const Redis = require('ioredis');
const http = require('http');
// Setup Redis
const pub = new Redis({ host: 'redis-server', port: 6379 });
const sub = new Redis({ host: 'redis-server', port: 6379 });
// Setup HTTP + WebSocket server
const server = http.createServer();
const wss = new WebSocket.Server({ server });
// Track clients per room
const rooms = new Map();
wss.on('connection', (ws, req) => {
const clientId = generateId();
ws.clientId = clientId;
ws.rooms = new Set();
ws.on('message', (data) => {
const msg = JSON.parse(data);
switch (msg.type) {
case 'join':
joinRoom(ws, msg.room);
break;
case 'chat':
broadcastToRoom(msg.room, {
type: 'chat',
from: clientId,
message: msg.message,
timestamp: Date.now()
}, ws); // exclude sender
break;
}
});
ws.on('close', () => {
// Leave all rooms
ws.rooms.forEach(room => leaveRoom(ws, room));
});
});
function joinRoom(ws, room) {
ws.rooms.add(room);
if (!rooms.has(room)) {
rooms.set(room, new Set());
// Subscribe ke Redis channel untuk room ini
sub.subscribe(`room:${room}`);
}
rooms.get(room).add(ws);
// Notify semua di room
broadcastToRoom(room, {
type: 'join',
user: ws.clientId,
room: room
});
}
function broadcastToRoom(room, message, exclude = null) {
const localClients = rooms.get(room) || new Set();
// Kirim ke client lokal
const payload = JSON.stringify(message);
localClients.forEach(client => {
if (client !== exclude && client.readyState === WebSocket.OPEN) {
client.send(payload);
}
});
// Publish ke Redis agar server lain juga mengirim
pub.publish(`room:${room}`, JSON.stringify({
...message,
_fromServer: SERVER_ID
}));
}
// Terima message dari server lain via Redis
sub.on('message', (channel, data) => {
const message = JSON.parse(data);
const room = channel.replace('room:', '');
// Skip jika dari server ini sendiri
if (message._fromServer === SERVER_ID) return;
// Kirim ke client lokal
const clients = rooms.get(room) || new Set();
clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
});
const SERVER_ID = process.env.SERVER_ID || 'server-1';
server.listen(8080, () => console.log(`WS Server ${SERVER_ID} on :8080`));
Di Nginx, pastikan menambahkan header Upgrade dan Connection di proxy_pass. Tanpa ini, WebSocket handshake akan gagal. Tambahkan juga proxy_read_timeout yang cukup panjang agar Nginx tidak menutup koneksi idle.
# Nginx configuration untuk WebSocket load balancing
upstream websocket_backend {
ip_hash; # Sticky session berdasarkan IP
server 127.0.0.1:8081;
server 127.0.0.1:8082;
server 127.0.0.1:8083;
}
server {
listen 443 ssl;
server_name ws.example.com;
location /ws {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
# Header untuk WebSocket upgrade
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeout β 1 jam untuk WebSocket
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# Preserve client info
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Buffering off untuk real-time
proxy_buffering off;
}
}
7. Implementasi dengan Node.js & Socket.IO
Socket.IO adalah library populer yang built di atas WebSocket dengan fitur tambahan: automatic reconnection, rooms, namespaces, fallback ke HTTP polling, dan binary support.
// === Server (Express + Socket.IO) ===
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: { origin: '*' },
pingInterval: 25000,
pingTimeout: 20000,
maxHttpBufferSize: 1e6 // 1MB
});
// Namespace: /chat
const chatNs = io.of('/chat');
chatNs.use((socket, next) => {
// Middleware: authentication
const token = socket.handshake.auth.token;
if (verifyToken(token)) {
socket.userId = decodeToken(token).id;
next();
} else {
next(new Error('Authentication failed'));
}
});
chatNs.on('connection', (socket) => {
console.log(`User ${socket.userId} connected`);
// Join room
socket.on('join-room', (roomId) => {
socket.join(roomId);
socket.to(roomId).emit('user-joined', {
userId: socket.userId,
timestamp: Date.now()
});
});
// Chat message
socket.on('chat-message', (data) => {
const { room, message } = data;
chatNs.to(room).emit('new-message', {
from: socket.userId,
message: message,
timestamp: Date.now()
});
});
// Typing indicator
socket.on('typing', (room) => {
socket.to(room).emit('user-typing', { userId: socket.userId });
});
// Disconnect
socket.on('disconnect', (reason) => {
console.log(`User ${socket.userId} disconnected: ${reason}`);
});
});
httpServer.listen(3000);
// === Client (Browser) ===
import { io } from 'socket.io-client';
const socket = io('https://server.example.com/chat', {
auth: { token: 'jwt-token-here' },
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 10,
transports: ['websocket', 'polling']
});
socket.on('connect', () => console.log('Connected!'));
socket.on('new-message', (data) => console.log('Message:', data));
socket.emit('join-room', 'room-general');
socket.emit('chat-message', { room: 'room-general', message: 'Halo semua!' });