WebSocket Fundamentals — Realtime

WebSocket Fundamentals WebSocket membuat koneksi persistent, full-duplex antara client dan server. Setelah handshake HTTP awal, komunikasi dua arah via single…

WebSocket (RFC 6455) mengubah satu request HTTP jadi koneksi yang terus terbuka, tempat browser dan server bisa saling kirim pesan kapan saja. Bayangin beda telepon dan walkie-talkie. Di walkie-talkie cuma satu orang yang bisa bicara: tekan tombol, bicara, lepas, gantian. Itu half-duplex. Di telepon, Bu Rini dan Mas Eko bisa bicara di saat yang sama tanpa menunggu giliran. Itu full-duplex, dan begitulah WebSocket. HTTP biasa malah seperti surat: satu surat pergi, satu balasan datang.

🧪 Tebakan cepat: Browser membuka WebSocket dengan header Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==. Server harus membalas Sec-WebSocket-Accept berisi apa: kunci yang sama, kunci acak baru, atau nilai yang dihitung dari kunci itu? Jawabannya ada di bagian "Handshake: dari HTTP ke WebSocket".

Handshake: dari HTTP ke WebSocket

Koneksi dimulai sebagai request HTTP biasa yang minta "ganti protokol". Ini handshake papan Bu Rini ke server realtime, dengan kunci contoh dari RFC 6455:

GET /papan HTTP/1.1
Host: localhost:3000
Connection: Upgrade
Upgrade: websocket
Origin: http://localhost:8000
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Sec-WebSocket-Accept dihitung dari kuncinya: tempel kunci dengan GUID tetap dari RFC 6455, hitung SHA-1-nya (20 byte), lalu ubah ke base64 (28 karakter).

import { createHash } from 'node:crypto'

const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' // tetap, tertulis di RFC 6455
const key = 'dGhlIHNhbXBsZSBub25jZQ=='
console.log(createHash('sha1').update(key + GUID).digest('base64')) // s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Jadi jawabannya nilai yang dihitung dari kunci itu. Itu jawaban tebakannya. Browser menghitung hal yang sama, dan kalau beda, koneksinya gagal. Di percobaan dengan Chromium 147, kunci /YWRojGG13C3Iy7Ca8KYuQ== dijawab g87MRT1zr43h8qMK5LukGdNvXxM= dan diterima. Accept ini bukan password: dia cuma membuktikan server benar-benar mengerti WebSocket.

Setelah handshake, yang dipakai adalah protokol WebSocket, bukan HTTP lagi. Alamatnya ws:// (port 80) atau wss:// (lewat TLS, port 443). Halaman https:// wajib memakai wss://: Chromium dan Firefox melempar SecurityError untuk ws://, Safari menggagalkannya lewat event error.

Frame: amplop tiap pesan

Tiap pesan dikirim dalam frame. Header-nya kecil:

Bagian Ukuran Isinya
Byte 1 1 byte FIN (pesan terakhir?) dan opcode (jenis frame)
Byte 2 1 byte MASK (ditutupi atau gak) dan panjang payload 0 sampai 125
Panjang tambahan 0, 2, atau 8 byte nilai 126: panjang ada di 2 byte berikutnya (sampai 65.535). Nilai 127: di 8 byte berikutnya
Masking key 0 atau 4 byte cuma di frame dari browser

Opcode yang perlu kamu kenal: 1 teks, 2 biner, 0 lanjutan pesan yang dipecah, 8 close, 9 ping, A pong.

Server mengirim status ke papan, {"type":"status","id":12,"status":"Disetrika"}, 46 byte:

81 2E 7B 22 74 79 70 65 ...   (2 byte header + 46 byte payload = 48 byte)

Frame dari browser wajib ditutupi (masked): byte kedua jadi AE (MASK 1 + 46), diikuti 4 byte masking key acak, lalu tiap byte payload di-XOR dengan key itu. Totalnya 52 byte. Contoh di RFC 6455: "Hello" dari server 81 05 48 65 6c 6c 6f, dari browser dengan key 37 fa 21 3d jadi 81 85 37 fa 21 3d 7f 9f 4d 51 58 (48 XOR 37 = 7f). Masking bukan enkripsi, key-nya ada di situ. Gunanya supaya proxy lama gak bisa dikelabui menyimpan byte pilihan penyerang. Enkripsi datang dari wss://.

Diukur di Chromium 147, Firefox 146, dan WebKit 26.4: frame dari browser untuk pesan 46 byte punya header 6 byte, 200 byte jadi 8, 70.000 byte jadi 14. Karena tiap sisi mengirim frame-nya sendiri kapan saja, tanpa menunggu giliran, WebSocket disebut full-duplex.

Ping, pong, dan menutup koneksi

Kode Artinya Contoh di laundry
1000 ditutup normal, sengaja Bu Rini menekan "Keluar"
1001 pergi server realtime di-restart, atau tab pindah halaman (ketiga browser mengirim 1001)
1006 putus tanpa frame close Wi-Fi mati. Kode ini gak pernah dikirim, cuma dilaporkan browser, dengan wasClean false
1008 melanggar aturan token login salah
1011 server error server gagal menyimpan status

ws.close() di browser cuma menerima 1000 atau 3000 sampai 4999. ws.close(1001) melempar InvalidAccessError, dan alasan lebih dari 123 byte melempar SyntaxError.

WebSocket API di browser

const ws = new WebSocket('ws://localhost:3000/papan')
console.log(ws.readyState) // 0: CONNECTING

ws.onopen = () => {
  ws.send(JSON.stringify({ type: 'status', id: 12, status: 'Disetrika' }))
}
ws.onmessage = (event) => {
  const pesan = JSON.parse(event.data)
  console.log(pesan.id, pesan.status) // 12 Disetrika
}
ws.onclose = (event) => console.log('tutup', event.code, event.wasClean)
ws.onerror = () => console.log('error') // tanpa detail, close menyusul

Server-nya bisa memakai library ws di Node.js:

import { WebSocketServer } from 'ws'

const wss = new WebSocketServer({ port: 3000, path: '/papan' })
wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    for (const layar of wss.clients) layar.send(String(data)) // teruskan ke semua layar
  })
})

Reconnect: tulis sendiri, dengan backoff dan jitter

Browser gak menyambung ulang WebSocket. Diuji di ketiga browser: setelah koneksi diputus, readyState berhenti di 3 dan gak ada handshake baru. Kodenya kamu tulis sendiri:

function sambung(percobaan = 0) {
  const ws = new WebSocket('ws://localhost:3000/papan')
  ws.onopen = () => { percobaan = 0 }
  ws.onmessage = (event) => tampilkan(JSON.parse(event.data))
  ws.onclose = (event) => {
    if (event.code === 1000) return                      // ditutup sengaja
    const batas = Math.min(30000, 1000 * 2 ** percobaan) // 1, 2, 4, 8, 16, lalu 30 detik
    const jeda = Math.random() * batas                   // jitter: acak antara 0 dan batas
    setTimeout(() => sambung(percobaan + 1), jeda)
  }
}
sambung()

⚠️ Jebakan yang sering ketemu

TL;DR: WebSocket dimulai dengan handshake HTTP: Upgrade: websocket dan Sec-WebSocket-Key, dijawab 101 Switching Protocols dengan Sec-WebSocket-Accept = base64(SHA-1(key + GUID)), misalnya s3pPLMBiTxaQ9kYGzzhZRbK+xOo=. Sesudah itu koneksinya memakai protokol WebSocket (ws:// atau wss://), bukan HTTP. Pesan dikirim sebagai frame: opcode (1 teks, 2 biner, 8 close, 9 ping, A pong), panjang 7 bit atau 126 + 2 byte atau 127 + 8 byte, dan masking key 4 byte di frame dari browser. Full-duplex artinya kedua sisi bisa mengirim bersamaan tanpa menunggu giliran. Kode close: 1000 normal, 1001 pergi, 1006 putus tanpa close, 1008 melanggar aturan, 1011 server error. API browser: readyState 0 sampai 3, onopen, onmessage, onclose, onerror, send(), close(), bufferedAmount. Browser gak menyambung ulang: tulis reconnect dengan backoff dan jitter.

Yang akan kamu pelajari