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.
# 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:
mkdir -p ~/realtime-lab/go-server
mkdir -p ~/realtime-lab/soketi-server
cd ~/realtime-lab
[!TIP] Jika Anda melihat respons perintah bernilai
65535setelah 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.
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.
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.
cd ~/realtime-lab/soketi-server
npm init -y
npm install @soketi/soketi
Buat file konfigurasi sederhana bernama soketi.json:
{
"apps": [
{
"id": "app-id-123",
"key": "app-key-abc",
"secret": "app-secret-xyz",
"maxConnections": -1
}
]
}
Jalankan Soketi dengan perintah:
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:
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:
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:
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:
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 (
8080atau6001). Gunakannetstat -tuln | grep -E '8080|6001'untuk memastikannya. - Koneksi terputus tiba-tiba (Timeout)? Pastikan direktif
proxy_read_timeoutdanproxy_send_timeoutdi 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
CheckOriginmengembalikan nilaitrueuntuk tahap pengembangan, atau sesuaikan domain valid pada lingkungan produksi.