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 membalasSec-WebSocket-Acceptberisi 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=
Upgrade: websocketdanConnection: Upgrade: browser minta koneksi ini diganti protokolnya.Sec-WebSocket-Key: 16 byte acak dalam base64 (24 karakter), baru di tiap koneksi.101 Switching Protocols: server setuju. Sesudah baris kosong itu, koneksi TCP yang sama gak bicara HTTP lagi.
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)
81: FIN 1, opcode 1 (teks).2E: MASK 0, panjang 46.
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
- Ping (opcode 9) bertanya "masih di sana?", dijawab pong (opcode A) berisi payload yang sama. Browser menjawab ping dari server secara otomatis (diuji: ping
p1dijawab pongp1), tapi JavaScript di browser gak bisa mengirim ping: gak adaws.ping(). Di server, librarywspunyaws.ping(). - Close (opcode 8) membawa kode 2 byte dan alasan singkat:
| 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
readyState: 0 CONNECTING, 1 OPEN, 2 CLOSING, 3 CLOSED.send()sebelumopenmelemparInvalidStateError. Sesudah CLOSED,send()diam saja dan pesannya dibuang.bufferedAmount: byte yang sudah di-send()tapi belum terkirim. Diukur: tepat setelah mengirim sekitar 1 MB, nilainya 1.070.246, dan setengah detik kemudian 0 di localhost. Cek sebelum mengirim banyak di jaringan lambat.- Pesan teks datang sebagai string. Pesan biner datang sebagai
Blob, atauArrayBufferkalauws.binaryType = 'arraybuffer'.
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()
- Backoff: batas tunggunya berlipat dua tiap gagal, sampai 30 detik, supaya server yang mati gak dibanjiri.
- Jitter: jedanya diacak. Tanpa acak, saat server di-restart semua papan menyambung di detik yang persis sama (1, lalu 2, lalu 4) dan menyerbu server bersamaan.
onopenmengembalikan hitungan ke 0, jadi putus berikutnya mulai dari jeda pendek lagi.
⚠️ Jebakan yang sering ketemu
send()sebelumopen. LangsungInvalidStateError. Kirim di dalamonopen, atau cekws.readyState === WebSocket.OPEN.- Mengira browser menyambung ulang sendiri. Papan diam selamanya setelah Wi-Fi mati sebentar.
ws://di halamanhttps://. Diblokir. Pakaiwss://.- Reconnect tanpa jitter. Ratusan layar menyerbu server yang baru hidup di detik yang sama.
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.