WebSocket

WebSocket #

WebSocket adalah protokol komunikasi dua arah berbasis TCP yang berjalan di atas HTTP. WebSocket dimulai sebagai HTTP request biasa, lalu di-upgrade menjadi koneksi persisten yang memungkinkan server mengirim data ke client kapan saja tanpa client harus request terlebih dahulu.

Kabar baiknya: Caddy mendukung WebSocket secara native tanpa konfigurasi tambahan. Saat Caddy mendeteksi request dengan header Upgrade: websocket, ia otomatis meneruskannya sebagai proxy WebSocket.

Cara Kerja WebSocket Upgrade #

1. Client kirim HTTP request dengan header Upgrade:
   GET /chat HTTP/1.1
   Host: example.com
   Upgrade: websocket
   Connection: Upgrade
   Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
   Sec-WebSocket-Version: 13

2. Caddy forward request ke backend

3. Backend merespons dengan:
   HTTP/1.1 101 Switching Protocols
   Upgrade: websocket
   Connection: Upgrade
   Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

4. Koneksi berubah menjadi WebSocket — bidirectional, persisten
   → Client dan server bisa kirim pesan kapan saja
   → Koneksi tetap terbuka sampai salah satu pihak menutup

Konfigurasi Dasar #

example.com {
    # Caddy otomatis handle WebSocket upgrade
    # Tidak ada konfigurasi khusus yang diperlukan!
    reverse_proxy localhost:3000
}

Caddy secara otomatis mendeteksi header Upgrade: websocket dan meneruskan koneksi WebSocket ke backend.


Node.js WebSocket Server (ws library) #

// server.js
const http = require('http');
const WebSocket = require('ws');

// HTTP server untuk health check
const server = http.createServer((req, res) => {
    if (req.url === '/health') {
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ status: 'ok', connections: wss.clients.size }));
    }
});

// WebSocket server pada HTTP server yang sama
const wss = new WebSocket.Server({ server, path: '/ws' });

wss.on('connection', (ws, req) => {
    const clientIp = req.headers['x-real-ip'] || req.socket.remoteAddress;
    console.log(`New connection from ${clientIp}`);
    
    ws.on('message', (message) => {
        console.log(`Received: ${message}`);
        
        // Broadcast ke semua client
        wss.clients.forEach((client) => {
            if (client.readyState === WebSocket.OPEN) {
                client.send(message.toString());
            }
        });
    });
    
    ws.on('close', () => {
        console.log(`Client disconnected`);
    });
    
    ws.send(JSON.stringify({ type: 'welcome', message: 'Connected!' }));
});

server.listen(3000, '127.0.0.1', () => {
    console.log('Server running on localhost:3000');
});

Socket.io dengan Caddy #

example.com {
    encode gzip zstd
    
    # Socket.io path — pastikan semua endpoint socket.io di-proxy
    handle /socket.io/* {
        reverse_proxy localhost:3000 {
            header_up X-Real-IP {remote_host}
            header_up X-Forwarded-Proto {scheme}
        }
    }
    
    # Regular HTTP requests
    handle /api/* {
        reverse_proxy localhost:3000
    }
    
    # Static files
    handle {
        root * /var/www/myapp/public
        file_server
    }
}
// Socket.io server configuration
const io = require('socket.io')(server, {
    // Jika Caddy memutus koneksi WebSocket terlalu cepat,
    // turunkan pingTimeout dan gunakan polling sebagai fallback
    transports: ['websocket', 'polling'],
    pingTimeout: 60000,
    pingInterval: 25000
});

WebSocket dengan Path-Based Routing #

example.com {
    # Chat WebSocket
    handle /ws/chat {
        reverse_proxy chat-service:3001
    }
    
    # Notifications WebSocket
    handle /ws/notifications {
        reverse_proxy notification-service:3002
    }
    
    # Live updates WebSocket
    handle /ws/updates {
        reverse_proxy update-service:3003
    }
    
    # Regular HTTP API
    handle /api/* {
        reverse_proxy api-backend:8080
    }
    
    # Frontend
    handle {
        root * /var/www/html
        file_server
    }
}

Autentikasi WebSocket #

WebSocket tidak mendukung header custom setelah initial handshake. Autentikasi dilakukan saat upgrade request:

// Server: validasi token dari query parameter atau cookie
wss.on('connection', (ws, req) => {
    // Opsi 1: Token dari query string
    // ws://example.com/ws?token=JWT_TOKEN
    const url = new URL(req.url, 'http://localhost');
    const token = url.searchParams.get('token');
    
    // Opsi 2: Token dari Cookie
    // const cookies = parseCookies(req.headers.cookie);
    // const token = cookies.auth_token;
    
    if (!validateToken(token)) {
        ws.close(1008, 'Unauthorized');
        return;
    }
    
    // Authenticated connection
    const user = decodeToken(token);
    ws.userId = user.id;
    
    ws.on('message', (message) => {
        // Handle message dengan user context
    });
});
example.com {
    # Validasi auth di level Caddy sebelum WebSocket upgrade
    # Memerlukan basicauth atau plugin auth
    
    @ws_path path /ws/*
    
    handle @ws_path {
        basicauth {
            alice $2a$14$aliceHashHere
        }
        reverse_proxy ws-backend:3000
    }
    
    handle {
        reverse_proxy backend:3000
    }
}

Load Balancing WebSocket dengan Sticky Sessions #

WebSocket connections bersifat stateful — satu client harus selalu terhubung ke server yang sama. Gunakan IP hash untuk sticky session:

example.com {
    reverse_proxy {
        to ws-server-1:3000 ws-server-2:3000 ws-server-3:3000
        
        # IP hash memastikan client yang sama selalu ke server yang sama
        lb_policy ip_hash
        
        # Health check
        health_uri /health
        health_interval 10s
        
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }
}

Alternatif yang lebih baik untuk production: gunakan Redis sebagai shared state agar WebSocket bisa di-load balance tanpa sticky session.


Timeout Konfigurasi untuk WebSocket #

{
    servers {
        # Timeout untuk idle connections
        # WebSocket yang aktif tidak akan terkena timeout ini
        timeouts {
            read_body   10s
            read_header 10s
            # Jangan set write timeout untuk WebSocket!
            # write       0  (default, tidak ada timeout)
        }
    }
}

example.com {
    reverse_proxy ws-backend:3000 {
        transport http {
            # Dial timeout (initial connection)
            dial_timeout 5s
            
            # Jangan set response_header_timeout untuk WebSocket
            # WebSocket connections bisa berlangsung berhari-hari
            # response_header_timeout tidak berlaku setelah upgrade
        }
    }
}

Debugging WebSocket #

# Test WebSocket dengan wscat
npm install -g wscat

# Koneksi ke WebSocket server via Caddy
wscat -c wss://example.com/ws

# Koneksi dengan header
wscat -c wss://example.com/ws \
    -H "Authorization: Bearer token" \
    -H "X-Custom: value"

# Test dengan curl (hanya untuk melihat response upgrade)
curl -iv \
    --http1.1 \
    -H "Connection: Upgrade" \
    -H "Upgrade: websocket" \
    -H "Sec-WebSocket-Version: 13" \
    -H "Sec-WebSocket-Key: $(openssl rand -base64 16)" \
    https://example.com/ws

# Cek apakah Caddy forward WebSocket headers dengan benar
# Lihat di access log: status 101 = WebSocket upgrade berhasil
cat /var/log/caddy/access.log | jq 'select(.status == 101)'

Ringkasan #

  • Tidak ada konfigurasi khusus yang diperlukan untuk WebSocket di Caddy — Caddy otomatis menangani Upgrade: websocket header saat menggunakan reverse_proxy.
  • Status 101 (Switching Protocols) di access log menandakan WebSocket upgrade berhasil — gunakan ini untuk debugging.
  • Untuk load balancing WebSocket, gunakan lb_policy ip_hash agar client yang sama selalu diarahkan ke server yang sama (sticky session).
  • Jangan set write_timeout untuk WebSocket — koneksi WebSocket bisa berlangsung berhari-hari dan timeout akan memutus koneksi aktif.
  • Autentikasi WebSocket dilakukan saat initial HTTP handshake — token bisa dikirim via query parameter atau cookie, bukan via header custom (tidak didukung setelah upgrade).
  • Gunakan Redis sebagai shared state untuk arsitektur multi-server WebSocket yang lebih scalable dibanding sticky session.

← Sebelumnya: Python WSGI   Berikutnya: SPA React →


WebSocket dengan Heartbeat dan Reconnect #

Koneksi WebSocket bisa terputus karena network issue atau idle timeout. Implementasikan heartbeat di client:

// Client-side WebSocket dengan auto-reconnect
class ReconnectingWebSocket {
    constructor(url) {
        this.url = url;
        this.ws = null;
        this.reconnectDelay = 1000;
        this.heartbeatInterval = null;
        this.connect();
    }
    
    connect() {
        this.ws = new WebSocket(this.url);
        
        this.ws.onopen = () => {
            console.log('WebSocket connected');
            this.reconnectDelay = 1000; // Reset delay
            this.startHeartbeat();
        };
        
        this.ws.onclose = () => {
            console.log(`Reconnecting in ${this.reconnectDelay}ms...`);
            this.stopHeartbeat();
            setTimeout(() => this.connect(), this.reconnectDelay);
            // Exponential backoff (maks 30 detik)
            this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
        };
        
        this.ws.onerror = (err) => console.error('WebSocket error:', err);
    }
    
    startHeartbeat() {
        this.heartbeatInterval = setInterval(() => {
            if (this.ws.readyState === WebSocket.OPEN) {
                this.ws.send(JSON.stringify({ type: 'ping' }));
            }
        }, 25000);  // Setiap 25 detik
    }
    
    stopHeartbeat() {
        clearInterval(this.heartbeatInterval);
    }
}

const ws = new ReconnectingWebSocket('wss://example.com/ws');

Implementasi heartbeat ini mencegah koneksi idle terputus oleh firewall atau proxy timeout, dan reconnect otomatis saat koneksi terputus karena network issue.

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