WebSocket #
WebSocket adalah protokol komunikasi dua arah (full-duplex) berkinerja tinggi yang berjalan di atas satu koneksi TCP tunggal. Berbeda dari protokol HTTP konvensional yang bersifat stateless dan berbasis pola interaksi permintaan-respons (request-response model), WebSocket memungkinkan server mengirimkan data secara proaktif (push notifications) ke browser klien secara real-time tanpa menunggu adanya permintaan terlebih dahulu. Hubungan komunikasi ini dirintis menggunakan mekanisme jabat tangan HTTP (HTTP handshake upgrade) sebelum akhirnya koneksi diambil alih sepenuhnya untuk transmisi data biner atau teks tingkat rendah. Server web Caddy menawarkan dukungan bawaan yang luar biasa terhadap protokol ini. Secara default, Caddy menangani proses jabat tangan dan streaming data WebSocket secara otomatis tanpa memerlukan konfigurasi tambahan apa pun. Kita akan mengupas tuntas cara kerja jabat tangan WebSocket, memprogram server WebSocket Node.js, mengonfigurasi Socket.io, menerapkan perutean berbasis jalur (path-based routing), menyusun otentikasi saat proses jabat tangan, melakukan penyeimbangan beban (load balancing) dengan sticky sessions, mengonfigurasi batas waktu timeout sistem, hingga melakukan tuning batas kapasitas resource sistem operasi.
Cara Kerja WebSocket Upgrade #
Proses inisialisasi koneksi WebSocket tidak langsung menggunakan socket mentah, melainkan memanfaatkan infrastruktur port HTTP (port 80/443) yang telah ada untuk menjamin kompatibilitas melewati firewall jaringan.
Proses transisi ini terbagi menjadi empat langkah utama:
Permintaan Handshake Klien: Browser klien mengirimkan request HTTP GET reguler ke server web, namun menyertakan header penanda khusus yang meminta peningkatan protokol:
GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13Penerusan oleh Caddy: Caddy mendeteksi header
Upgrade: websocketini, lalu meneruskannya secara utuh ke server aplikasi backend kita.Persetujuan Upgrade oleh Backend: Aplikasi backend memvalidasi token keamanan key, lalu mengembalikan respons persetujuan berupa status HTTP khusus 101 Switching Protocols:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Pembajakan Socket (Hijacking): Segera setelah status 101 dikirimkan, koneksi HTTP terputus. Baik Caddy maupun backend membajak (hijack) socket TCP tersebut untuk tetap terbuka permanen. Data selanjutnya dialirkan menggunakan frame biner WebSocket yang sangat ringan tanpa beban header overhead HTTP.
Konfigurasi Dasar #
Karena Caddy dirancang dengan arsitektur modern yang memahami protokol web masa kini secara native, kita tidak perlu menambahkan parameter kustom apa pun pada konfigurasi reverse_proxy di Caddyfile:
# Konfigurasi dasar WebSocket Caddy (Bekerja secara otomatis)
example.com {
# Caddy mendeteksi header Upgrade secara otomatis
# dan mengubah mode routing menjadi terowongan biner WebSocket
reverse_proxy localhost:3000
}
Node.js WebSocket Server (ws library) #
Untuk melihat interaksi ini secara riil, mari kita buat server WebSocket sederhana di Node.js menggunakan pustaka ws yang berjalan secara internal di port 3000:
// server.js (Node.js WebSocket backend)
const http = require('http');
const WebSocket = require('ws');
// 1. Buat server HTTP dasar untuk menangani health check Caddy
const server = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'UP', activeConnections: wss.clients.size }));
} else {
res.writeHead(404);
res.end();
}
});
// 2. Buat server WebSocket terikat pada path /ws
const wss = new WebSocket.Server({ server, path: '/ws' });
wss.on('connection', (ws, req) => {
// Membaca header X-Real-IP yang diteruskan oleh Caddy
const clientIp = req.headers['x-real-ip'] || req.socket.remoteAddress;
console.log(`[WebSocket] Koneksi baru terjalin dari IP: ${clientIp}`);
ws.on('message', (message) => {
console.log(`[WebSocket] Menerima pesan: ${message}`);
// Kirimkan kembali (Broadcast) pesan ke seluruh klien yang terhubung
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(`Broadcast: ${message}`);
}
});
});
ws.on('close', () => {
console.log('[WebSocket] Koneksi ditutup oleh klien');
});
// Kirim pesan sambutan saat koneksi sukses terjalin
ws.send(JSON.stringify({ type: 'system', data: 'Sukses terhubung ke server!' }));
});
// Bind server ke localhost port 3000
server.listen(3000, '127.0.0.1', () => {
console.log('Server WebSocket berjalan internal pada http://127.0.0.1:3000');
});
Socket.io dengan Caddy #
Pustaka Socket.io menggunakan teknik polling HTTP panjang (long-polling) sebagai metode jabat tangan awal sebelum melakukan upgrade ke WebSocket sesungguhnya. Kita harus memastikan seluruh rute segmentasi URL Socket.io (/socket.io/*) diarahkan ke backend yang sama secara utuh:
# Konfigurasi Caddyfile untuk Socket.io
example.com {
encode zstd gzip
# Perutean khusus lalu lintas jabat tangan dan WebSocket Socket.io
handle /socket.io/* {
reverse_proxy localhost:3000 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
# Perutean file statis frontend web chat kita
handle {
root * /var/www/chat-app/public
file_server
}
}
Di sisi server aplikasi Node.js, kita juga harus mengonfigurasi batas waktu detak jantung (heartbeat) yang sesuai agar Caddy tidak menganggap koneksi terputus saat klien sedang idle:
// Inisialisasi Socket.io dengan penyesuaian interval ping
const io = require('socket.io')(server, {
transports: ['websocket', 'polling'],
pingTimeout: 60000, // Berikan batas waktu 60 detik sebelum dianggap terputus
pingInterval: 25000 // Kirimkan sinyal detak jantung ping setiap 25 detik
});
WebSocket dengan Path-Based Routing #
Kita dapat memanfaatkan arsitektur Caddy yang modular untuk membagi rute koneksi WebSocket ke berbagai microservices backend yang berbeda berdasarkan path URL. Sebagai contoh, kita memisahkan penanganan chat, notifikasi sistem, dan data live feed:
# Konfigurasi perutean WebSocket modular
example.com {
# 1. Rute Chat dialirkan ke port 3001
handle /ws/chat {
reverse_proxy localhost:3001
}
# 2. Rute Notifikasi dialirkan ke port 3002
handle /ws/notifications {
reverse_proxy localhost:3002
}
# 3. Rute Live Feed dialirkan ke port 3003
handle /ws/feed {
reverse_proxy localhost:3003
}
# 4. Lalu lintas web reguler dialirkan ke server utama
handle {
root * /var/www/html
file_server
}
}
Autentikasi WebSocket #
Protokol WebSocket standar di browser tidak membolehkan pengiriman header HTTP kustom tambahan (seperti header Authorization: Bearer <token>) saat memicu perintah inisialisasi new WebSocket().
Oleh karena itu, kita harus melakukan proses otentikasi menggunakan salah satu dari dua metode alternatif berikut selama fase jabat tangan awal:
1. Metode Menggunakan Query Parameter (Opsi Paling Populer) #
Browser mengirimkan token sebagai query parameter pada URL (misalnya wss://example.com/ws?token=JWT_TOKEN). Server backend membaca parameter ini saat koneksi masuk:
// Penanganan otentikasi di sisi backend Node.js
wss.on('connection', (ws, req) => {
const url = new URL(req.url, 'http://localhost');
const token = url.searchParams.get('token');
if (!validateJWT(token)) {
// Tutup koneksi dengan kode status Close: Policy Violation (1008)
ws.close(1008, 'Token tidak valid atau kedaluwarsa!');
return;
}
// Sukses terotentikasi
ws.username = getUsernameFromToken(token);
});
2. Metode Otentikasi Terpusat di Sisi Caddy Gateway #
Jika kita ingin Caddy menyaring keamanan sebelum request sempat diteruskan ke backend, kita dapat menerapkan direktif otentikasi dasar di level Caddyfile:
# Otentikasi dasar sebelum upgrade jabat tangan WebSocket
example.com {
@ws_route path /ws/*
handle @ws_route {
# Hanya izinkan browser yang menyertakan otentikasi dasar yang sah
basicauth {
admin $2a$14$kunci_hash_bcrypt_admin_kita
}
reverse_proxy localhost:3000
}
handle {
reverse_proxy localhost:3000
}
}
Load Balancing WebSocket dengan Sticky Sessions #
Koneksi WebSocket bersifat stateful (menyimpan data status koneksi aktif di memori RAM server). Sekali jabat tangan sukses terjalin di Server A, browser klien harus terus berkomunikasi dengan Server A yang sama sampai koneksi ditutup. Jika di tengah jalan paket data dialirkan ke Server B, Server B akan bingung karena tidak memiliki data memori status (session state) klien tersebut.
Jika kita men-deploy beberapa node server backend WebSocket di belakang load balancer Caddy, kita wajib mengonfigurasi kebijakan Sticky Sessions (persistensi sesi). Cara termudah di Caddy adalah menggunakan kebijakan IP Hash atau Cookie:
# Load balancing klaster WebSocket dengan Sticky Sessions
example.com {
reverse_proxy {
# Tentukan seluruh node server WebSocket produksi kita
to ws-node-1:3000 ws-node-2:3000 ws-node-3:3000
# IP Hash menjamin browser dengan alamat IP yang sama
# akan selalu diarahkan ke server node backend yang sama
lb_policy ip_hash
# Pengujian kesehatan instansi
health_uri /health
health_interval 10s
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
[!TIP] Gunakan Redis Pub/Sub untuk Skalabilitas Tanpa Batas. Pendekatan Sticky Sessions memiliki limitasi jika salah satu server backend mengalami crash atau dilakukan restart. Pengguna yang terikat pada server tersebut akan terputus dan dialihkan ke node lain yang kosong, sehingga memicu hilangnya riwayat chat yang belum disimpan. Solusi terbaik di skala produksi besar adalah membuat aplikasi WebSocket kita menjadi stateless dengan menggunakan Redis Pub/Sub sebagai Shared State Message Broker terpusat. Dengan demikian, Caddy bebas mengarahkan pengguna ke node mana pun secara acak (round-robin) tanpa takut kehilangan sinkronisasi data chat.
Timeout Konfigurasi untuk WebSocket #
Server web Caddy secara bawaan memiliki konfigurasi batas waktu penulisan data (write timeout) untuk memutus koneksi lambat yang tidak aktif guna menghemat resource. Kita harus berhati-hati saat menyusun konfigurasi timeouts global agar tidak memutus koneksi WebSocket aktif yang bisa bertahan berhari-hari:
# Konfigurasi timeout global yang aman untuk WebSocket
{
servers {
timeouts {
# Batasi waktu membaca body request awal
read_body 10s
# Batasi waktu membaca header request awal
read_header 10s
# PENTING: Jangan set write timeout (biarkan bernilai 0 / default).
# Menyetel write timeout ke nilai statis (misalnya 60s) akan secara paksa
# memutus seluruh koneksi WebSocket aktif setiap 60 detik sekali.
write 0
}
}
}
example.com {
reverse_proxy localhost:3000 {
transport http {
dial_timeout 5s
# Hindari pengaturan response_header_timeout untuk WebSocket
}
}
}
Tuning Batas Resource Sistem Operasi Linux (ulimit) #
Koneksi WebSocket menahan socket koneksi TCP agar tetap terbuka secara permanen di server Caddy kita. Di dalam sistem operasi Linux, setiap koneksi jaringan aktif direpresentasikan sebagai sebuah berkas (File Descriptor).
Secara bawaan, Linux membatasi jumlah berkas terbuka maksimal per proses hanya 1024 file (soft limit). Jika server kita melayani lebih dari 1000 pengguna WebSocket secara bersamaan, Caddy akan gagal melayani pengguna baru dan memicu log error too many open files.
Kita wajib meningkatkan batas File Descriptor limit pada sistem operasi produksi kita:
1. Modifikasi Konfigurasi Systemd Caddy #
Edit file override systemd service Caddy:
sudo systemctl edit caddy
Tambahkan baris berikut di dalam file konfigurasi editor yang muncul:
[Service]
# Tingkatkan batas maksimal file descriptor untuk proses Caddy (Soft & Hard Limit)
LimitNOFILE=65536
Simpan file, lalu lakukan reload systemd daemon:
sudo systemctl daemon-reload
sudo systemctl restart caddy
2. Verifikasi Batas Aktif Caddy #
Kita dapat memeriksa apakah batas resource proses Caddy sudah berhasil dinaikkan menggunakan perintah:
# Temukan PID proses Caddy
PID_CADDY=$(pgrep caddy)
# Baca berkas limits proses tersebut
cat /proc/$PID_CADDY/limits | grep "Max open files"
# Output harus menampilkan: Max open files 65536 65536
Client-Side WebSocket Heartbeat dan Reconnect #
Untuk memastikan koneksi tetap hidup melewati firewall router pengguna yang sering memutus koneksi idle secara sepihak, kita harus mengimplementasikan detak jantung (heartbeat) ping/pong dan rekoneksi otomatis (auto-reconnect) di sisi kode browser klien JavaScript kita:
// Browser Client JavaScript Utility
class SecureWebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectInterval = 1000; // Mulai jeda rekoneksi 1 detik
this.heartbeatTimer = null;
this.connect();
}
connect() {
console.log(`[WS Client] Mencoba koneksi ke ${this.url}...`);
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('[WS Client] Sukses terhubung ke WebSocket!');
this.reconnectInterval = 1000; // Reset kembali jeda ke 1 detik
this.startHeartbeat();
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('[WS Client] Menerima data:', data);
};
this.ws.onclose = () => {
console.log(`[WS Client] Koneksi terputus. Mencoba rekoneksi dalam ${this.reconnectInterval}ms...`);
this.stopHeartbeat();
// Rekoneksi otomatis menggunakan jeda waktu eksponensial (Exponential Backoff)
setTimeout(() => this.connect(), this.reconnectInterval);
this.reconnectInterval = Math.min(this.reconnectInterval * 2, 30000); // Maksimal jeda 30 detik
};
this.ws.onerror = (error) => {
console.error('[WS Client] Terjadi kesalahan:', error);
};
}
startHeartbeat() {
// Kirimkan sinyal ping setiap 25 detik ke server Caddy
this.heartbeatTimer = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping', data: 'heartbeat' }));
}
}, 25000);
}
stopHeartbeat() {
clearInterval(this.heartbeatTimer);
}
}
// Inisialisasi koneksi klien menggunakan protokol aman wss (WebSocket Secure)
const appSocket = new SecureWebSocketClient('wss://example.com/ws?token=JWT_TOKEN_KITA');
Diagram Alur Jabat Tangan Upgrade Protokol WebSocket #
Untuk memvisualisasikan bagaimana aliran proses transisi dari koneksi HTTP biasa menjadi jalur terowongan biner WebSocket dua arah di Caddy, perhatikan diagram sequence berikut:
sequenceDiagram
autonumber
participant Client as Browser Klien (JS)
participant Caddy as Server Caddy (Edge)
participant Backend as Node.js WebSocket App
Client->>Caddy: HTTP GET /ws (Upgrade Request + Sec-WebSocket-Key)
Note over Caddy: Deteksi header Upgrade: websocket
Caddy->>Backend: Forward HTTP GET /ws (Upgrade Request)
Note over Backend: Validasi Token Auth & Generate Accept Key
Backend-->>Caddy: HTTP 101 Switching Protocols (Upgrade Confirmed)
Caddy-->>Client: HTTP 101 Switching Protocols
Note over Caddy: Caddy & Backend membajak socket TCP di memori RAM
Note over Client: Koneksi berubah menjadi WebSocket Tunnel (Stateful)
par Transmisi Dua Arah (Full-Duplex)
Client->>Caddy: Kirim Data Frame (Biner / Teks)
Caddy->>Backend: Teruskan Data Frame
and
Backend->>Caddy: Kirim Data Frame (Proaktif)
Caddy->>Client: Teruskan Data Frame (Real-time Push)
endRingkasan #
- Dukungan Native: Caddy secara otomatis mendeteksi dan meneruskan lalu lintas jabat tangan WebSocket secara transparan tanpa memerlukan flag atau opsi tambahan pada Caddyfile.
- Status Validasi: Status respons HTTP 101 Switching Protocols pada berkas access log Caddy mengonfirmasi jabat tangan upgrade WebSocket berhasil dilaksanakan.
- Sticky Sessions: Gunakan kebijakan distribusi
lb_policy ip_hashsaat melakukan load balancing multi-server WebSocket jika aplikasi tidak menggunakan shared Redis database.- Tuning Timeouts: Biarkan parameter global
write_timeoutbernilai default0(tanpa batas waktu) agar koneksi WebSocket aktif tidak diputus paksa secara sepihak.- Otentikasi Aman: Terapkan proses validasi token otentikasi melalui query parameters atau cookies saat inisialisasi koneksi jabat tangan awal.
- File Descriptor OS: Tingkatkan batas kapasitas berkas terbuka maksimal sistem operasi (
LimitNOFILE=65536) pada systemd service Caddy untuk melayani ribuan koneksi konkuren.- Auto-Reconnect Klien: Implementasikan mekanisme detak jantung ping-pong dan rekoneksi eksponensial di sisi JavaScript browser untuk menghadapi gangguan ketidakstabilan jaringan.