# Membangun Real-Time Web Server Mandiri: Komparasi Performa Antara Go WebSockets dan Soketi (Pusher Open-Source Clone)

> Diterbitkan oleh Literally Web ID pada 21 Oct 2026
> Sumber: https://literally.web.id/articles/membangun-real-time-web-server-mandiri-komparasi-performa-antara-go-websockets-dan-soketi-pusher-open-source-clone

---

## 1. Menu Kita Hari Ini & Alat yang Perlu Disiapkan

Pernahkah Anda mendapati aplikasi web yang Anda kembangkan mendadak megap-megap saat trafik lonjakan terjadi? Kemarin malam, saya mendampingi sebuah tim e-commerce yang migrasi dari polling HTTP tradisional ke arsitektur real-time. Masalahnya klise: server mereka kehabisan memori bukan karena database-nya jebol, melainkan karena file descriptor Linux habis tergerus oleh ribuan koneksi HTTP/1.1 yang malas ditutup.

Dalam 15 menit ke depan, kita tidak akan membahas teori klise tentang apa itu jaringan. Kita akan merakit langsung dua pendekatan server real-time mandiri dari nol sampai berjalan mulus: **Go WebSockets** yang terkenal buas dan efisien secara memori, serta **Soketi** (clone open-source Pusher berbasis Node.js) yang sangat ramah bagi ekosistem Laravel Broadcasting. 

Sebelum kita mulai meracik kode di dapur terminal, pastikan meja kerja Anda sudah bersih dari kekacauan dengan menyiapkan:
- **VPS Linux (Ubuntu 22.04 LTS)** dengan akses root atau sudo yang bernapas lega.
- **Go (Golang) versi 1.21+** terinstal rapi.
- **Node.js versi 18+** bersama npm.
- **Nginx** sebagai reverse proxy dan penjaga pintu depan.
- Domain aktif yang sudah diarahkan via DNS Record A ke IP server Anda.

Mari kita bedah arsitektur konseptualnya sebelum tangan kita kotor oleh kode.

## 2. Peta Jalan Singkat: Apa yang Sedang Kita Rakit

Secara historis, HTTP/1.1 menciptakan koneksi baru untuk setiap siklus *request-response* kecuali *Keep-Alive* diaktifkan secara agresif. Namun, untuk komunikasi dua arah *real-time* seperti panel monitoring, *live chat*, atau notifikasi instan, HTTP/1.1 standar terasa seperti mengemudikan truk kontainer di jalan tikus pegunungan.

Di sinilah **Persistent Connection** via WebSocket mengambil alih setelah proses *handshake* HTTP awal selesai. Di satu sisi, kita punya **Go WebSockets**, sebuah keajaiban konkurensi berkat *Goroutines* yang hanya memakan beberapa kilobyte memori per koneksi dan *Channels* bawaan untuk *broadcasting*. Di sisi lain, kita memiliki **Soketi**, pahlawan tanpa tanda jasa bagi pengembang yang ingin protokol Pusher standar tanpa harus merogoh kocek langganan layanan berbayar pihak ketiga.

> [!IMPORTANT]
> Perhatikan batas *file descriptor* default di kernel Linux (`ulimit -n`). Secara *default*, Ubuntu sering kali membatasi hanya pada 1024 koneksi. Kita akan melipatgandakannya nanti agar server tidak *crash* di tengah jalan.

## 3. Langkah 1: Mempersiapkan Fondasi & Environment

Masuklah ke terminal Anda via SSH. Kita mulai dengan menyesuaikan parameter kernel Linux agar sanggup menahan gempuran ribuan koneksi persisten secara simultan.

```bash
# Periksa limit file descriptor saat ini
ulimit -n

# Tambahkan konfigurasi limit sistem ke /etc/security/limits.conf
echo "* soft nofile 65535" | sudo tee -a /etc/security/limits.conf
echo "* hard nofile 65535" | sudo tee -a /etc/security/limits.conf
```

Sekarang, buat direktori kerja baru untuk proyek kita kali ini:

```bash
mkdir -p ~/realtime-lab/go-server
mkdir -p ~/realtime-lab/soketi-server
cd ~/realtime-lab
```

> [!TIP]
> Jika Anda melihat respons perintah bernilai `65535` setelah sesi SSH direstart, itu adalah tanda pertama bahwa fondasi OS kita sudah siap memikul beban berat.

## 4. Langkah 2: Meracik Konfigurasi Inti Tanpa Basa-basi

Mari kita bangun fondasi pertama: **Native WebSocket Server di Golang**. Masuk ke direktori `go-server`,inisialisasi *module*, dan buat berkas `main.go`.

```bash
cd ~/realtime-lab/go-server
go mod init realtime-go
go get github.com/gorilla/websocket
```

Buka editor teks favorit Anda (nano/vim) dan tulis kode server Go berikut. Perhatikan bagaimana kita memanfaatkan *Goroutines* untuk menangani *read* dan *write* secara asinkron tanpa memblokir *event loop*.

```go
package main

import (
	"log"
	"net/http"

	"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		return true // Izinkan semua origin untuk keperluan testing
	},
}

// Hub client untuk manajemen broadcast sederhana
type ClientManager struct {
	clients    map[*websocket.Conn]bool
	broadcast  chan []byte
	register   chan *websocket.Conn
	unregister chan *websocket.Conn
}

var manager = ClientManager{
	broadcast:  make(chan []byte),
	register:   make(chan *websocket.Conn),
	unregister: make(chan *websocket.Conn),
	clients:    make(map[*websocket.Conn]bool),
}

func (manager *ClientManager) start() {
	for {
		select {
		case conn := <-manager.register:
			manager.clients[conn] = true
			log.Println("Klien baru terhubung")
		case conn := <-manager.unregister:
			if _, ok := manager.clients[conn]; ok {
				delete(manager.clients, conn)
				conn.Close()
				log.Println("Klien terputus")
			}
		case message := <-manager.broadcast:
			for conn := range manager.clients {
				err := conn.WriteMessage(websocket.TextMessage, message)
				if err != nil {
					conn.Close()
					delete(manager.clients, conn)
				}
			}
		}
	}
}

func wsHandler(w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println(err)
		return
	}
	manager.register <- conn

	go func() {
		defer func() {
			manager.unregister <- conn
		}()
		for {
			_, message, err := conn.ReadMessage()
			if err != nil {
				break
			}
			manager.broadcast <- message
		}
	}()
}

func main() {
	go manager.start()
	http.HandleFunc("/ws", wsHandler)
	log.Println("Go WebSocket server berjalan pada port 8080...")
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}
```

Jalankan server Go tersebut dengan perintah `go run main.go`. Biarkan ia berjalan di latar belakang atau tab terminal terpisah.

Sekarang, mari bergeser ke alternatif kedua: **Soketi Server di atas Node.js**. Masuk ke direktori `soketi-server`.

```bash
cd ~/realtime-lab/soketi-server
npm init -y
npm install @soketi/soketi
```

Buat file konfigurasi sederhana bernama `soketi.json`:

```json
{
  "apps": [
    {
      "id": "app-id-123",
      "key": "app-key-abc",
      "secret": "app-secret-xyz",
      "maxConnections": -1
    }
  ]
}
```

Jalankan Soketi dengan perintah:

```bash
npx @soketi/soketi start --config=soketi.json --port=6001
```

> [!NOTE]
> Perhatikan output terminal Soketi. Ia akan menampilkan status *booting* yang bersih, menandakan server kompatibel Pusher Anda siap menerima koneksi pada port 6001.

## 5. Langkah 3: Sentuhan Akhir Keamanan & Optimasi

Di dunia nyata, kita tidak pernah membiarkan klien mengakses port mentah seperti `8080` atau `6001` secara langsung. Kita membutuhkan Nginx sebagai penjaga pintu gerbang, menangani SSL Termination (HTTPS/WSS), serta meneruskan *upgrade header* WebSocket dengan benar.

Buat berkas konfigurasi Nginx baru, misal `/etc/nginx/sites-available/realtime.conf`:

```nginx
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name realtime.domainanda.com;

    # Redirect HTTP ke HTTPS secara otomatis
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name realtime.domainanda.com;

    # SSL Configuration (Gunakan Let's Encrypt Certbot)
    ssl_certificate /etc/letsencrypt/live/realtime.domainanda.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/realtime.domainanda.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    # Rute untuk Native Go WebSocket
    location /ws {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }

    # Rute untuk Soketi Server
    location /app/ {
        proxy_pass http://127.0.0.1:6001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_read_timeout 3600s;
    }
}
```

Aktifkan konfigurasi dan uji sintaksnya:

```bash
sudo ln -s /etc/nginx/sites-available/realtime.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```

## 6. Tes Rasa: Memastikan Semuanya Berjalan Sempurna

Bagaimana cara kita membuktikan bahwa arsitektur ini benar-benar tangguh? Kita gunakan **k6**, *load testing tool* modern berbasis JavaScript.

Buat berkas skrip *load test* bernama `loadtest.js`:

```javascript
import ws from 'k6/ws';
import { check } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 100 }, // Naikkan secara bertahap ke 100 koneksi
    { duration: '1m', target: 100 },  // Tahan selama 1 menit
    { duration: '10s', target: 0 },   // Turunkan kembali
  ],
};

export default function () {
  const url = 'ws://127.0.0.1:8080/ws';
  
  const res = ws.connect(url, {}, function (socket) {
    socket.on('open', function () {
      socket.send('Halo dari k6 load tester!');
    });

    socket.on('message', function (message) {
      // Memastikan pesan pantul diterima kembali
    });

    socket.setTimeout(function () {
      socket.close();
    }, 10000);
  });

  check(res, { 'status is 101': (r) => r && r.status === 101 });
}
```

Jalankan pengujian dengan perintah:

```bash
k6 run loadtest.js
```

Perhatikan metrik memori dan CPU. Anda akan melihat Go WebSockets mempertahankan *memory footprint* yang sangat minimal (hanya beberapa megabyte untuk ratusan koneksi), sementara Soketi memberikan kenyamanan pustaka manajemen *channel* tingkat tinggi dengan biaya sumber daya Node.js yang sangat masuk akal.

## 7. Pertolongan Pertama: Jika Terjadi Kendala Kecil

Di lapangan, tidak jarang kita menemui hambatan tak terduga. Berikut adalah beberapa jurus penyelamatan cepat:

- **Error 502 Bad Gateway di Nginx?** Periksa apakah aplikasi Go atau Soketi Anda benar-benar sedang berjalan di port yang sesuai (`8080` atau `6001`). Gunakan `netstat -tuln | grep -E '8080|6001'` untuk memastikannya.
- **Koneksi terputus tiba-tiba (*Timeout*)?** Pastikan direktif `proxy_read_timeout` dan `proxy_send_timeout` di Nginx sudah diset cukup panjang (misal 3600 detik) agar proxy tidak memutus koneksi *idle* terlalu cepat.
- **Masalah CORS atau Origin ditolak?** Pada implementasi Go, pastikan fungsi `CheckOrigin` mengembalikan nilai `true` untuk tahap pengembangan, atau sesuaikan domain valid pada lingkungan produksi.