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:

  1. 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: 13
    
  2. Penerusan oleh Caddy: Caddy mendeteksi header Upgrade: websocket ini, lalu meneruskannya secara utuh ke server aplikasi backend kita.

  3. 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=
    
  4. 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)
    end

Ringkasan #

  • 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_hash saat melakukan load balancing multi-server WebSocket jika aplikasi tidak menggunakan shared Redis database.
  • Tuning Timeouts: Biarkan parameter global write_timeout bernilai default 0 (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.

← Sebelumnya: Python WSGI   Berikutnya: SPA React →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact