WebSocket adalah koneksi yang tetap terbuka antara browser dan server, sehingga dua-duanya bisa mengirim pesan kapan saja (full-duplex). HTTP biasa seperti berkirim surat: tiap pertanyaan satu surat, tiap jawaban satu surat balasan, dan server gak pernah mengirim surat duluan. WebSocket seperti telepon: sekali tersambung, dua pihak bisa bicara kapan pun sampai salah satu menutup. Di catatan-api, ini yang bikin catatan yang diubah Budi langsung muncul di layar Siti tanpa refresh.
🧪 Tebakan cepat: koneksi WebSocket dimulai dengan request HTTP biasa. Kalau server setuju pindah ke WebSocket, dia menjawab dengan status code berapa? Tebak dulu, jawabannya ada di bagian "Jabat tangan: dari HTTP jadi WebSocket".
Polling, SSE, atau WebSocket
Ada tiga cara supaya tab Siti tahu ada catatan berubah:
| Cara | Cara kerjanya | Ongkosnya |
|---|---|---|
| Polling | Browser bertanya GET /catatan tiap 2 detik |
30 request per menit, hampir semua jawabannya "gak ada yang baru" |
| SSE (Server-Sent Events) | Satu response HTTP yang gak pernah selesai, server menulis data: ... tiap ada kabar |
Satu arah saja: server ke browser. Browser nyambung lagi sendiri |
| WebSocket | Satu koneksi yang tetap terbuka, pesan bolak-balik | Dua arah, paling hemat untuk chat, kolaborasi, dan game |
Kabar satu arah (notifikasi, harga saham) cukup SSE. Kalau browser juga sering mengirim (chat, mengetik bersama), pakai WebSocket.
Jabat tangan: dari HTTP jadi WebSocket
Browser membuka koneksi dengan GET biasa yang membawa header Upgrade: websocket dan kunci acak Sec-WebSocket-Key. Server yang setuju menjawab 101 Switching Protocols. Itu jawaban tebakannya. Setelah itu koneksi TCP yang sama dipakai untuk pesan WebSocket, bukan HTTP lagi.
GET /live HTTP/1.1
Host: localhost:3000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Accept dihitung dari kuncinya: kunci + teks tetap 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, di-hash dengan SHA-1, lalu ditulis dalam base64. Browser menghitung hal yang sama. Kalau hasilnya beda, koneksi ditolak. Itu bukti server benar-benar paham WebSocket, bukan server HTTP yang kebetulan menjawab.
Server dengan ws
Library ws (npm install ws) menempel di server HTTP yang sama dengan Express:
// server.js
const http = require("node:http");
const { WebSocketServer, WebSocket } = require("ws");
const { app } = require("./app");
const server = http.createServer(app);
const wss = new WebSocketServer({ server, path: "/live" });
wss.on("connection", (ws) => {
console.log("Klien terhubung, total " + wss.clients.size);
ws.on("message", (data) => {
console.log("Diterima: " + data.toString());
});
ws.on("close", (code) => console.log("Klien terputus, kode " + code));
});
function siarkan(pesan) {
for (const klien of wss.clients) {
if (klien.readyState === WebSocket.OPEN) klien.send(JSON.stringify(pesan));
}
}
server.listen(3000);
wss.on("connection", ...)jalan sekali tiap ada klien baru.wsdi dalamnya adalah koneksi klien itu.ws.on("message", ...)menerima pesan dari klien itu. Isinya Buffer, jadidata.toString()dulu.wss.clientsberisi semua koneksi yang terbuka.siarkan(...)mengirim ke semuanya, misalnya dipanggil dari routePOST /catatan:siarkan({ type: "catatan:baru", catatan: baru }).
Di browser, cukup objek bawaan WebSocket:
const ws = new WebSocket("ws://localhost:3000/live"); // wss:// untuk https
ws.onmessage = (event) => {
const pesan = JSON.parse(event.data);
console.log(pesan.type, pesan.catatan.judul);
};
Socket.IO: event, broadcast, dan room
Socket.IO adalah library yang menambah fitur di atas WebSocket: pesan bernama (event), room, dan nyambung ulang otomatis. Kliennya harus library socket.io-client juga, karena new WebSocket(...) biasa gak bisa bicara dengan server Socket.IO.
const { Server } = require("socket.io");
const io = new Server(server);
io.on("connection", (socket) => {
socket.on("buka catatan", (id) => {
socket.join("catatan:" + id); // masuk room catatan itu
});
});
io.emit("catatan:baru", baru); // ke SEMUA klien
io.to("catatan:1").emit("catatan:diubah", catatan); // cuma klien di room catatan:1
socket.on(nama, ...)danio.emit(nama, data): kirim dan dengarkan event dengan nama yang kamu tentukan sendiri.socket.join(room)memasukkan klien ke room, yaitu grup bernama.io.to(room).emit(...)mengirim ke anggota room saja, dansocket.to(room).emit(...)ke anggota room kecuali pengirimnya.- Budi dan Siti membuka catatan 1, Andi membuka catatan 2. Budi mengubah catatan 1:
io.to("catatan:1")sampai ke Budi dan Siti, Andi gak terganggu.
Putus dan nyambung lagi
Wi-Fi putus, laptop tidur, server di-restart: koneksi tertutup, biasanya dengan kode 1006 (putus tanpa pamit). WebSocket bawaan browser gak nyambung sendiri, jadi kamu yang menulisnya, dengan jeda yang makin panjang (exponential backoff) supaya ribuan klien gak menyerbu server yang baru bangun bersamaan:
let percobaan = 0;
function sambung() {
const ws = new WebSocket("ws://localhost:3000/live");
ws.onopen = () => { percobaan = 0; };
ws.onclose = () => {
const jeda = Math.min(1000 * 2 ** percobaan, 30000); // 1, 2, 4, 8 ... maks 30 detik
percobaan++;
setTimeout(sambung, jeda + Math.random() * 500); // + acak sedikit
};
}
sambung();
Koneksi baru itu klien baru: room dan pesan yang lewat selama putus gak ikut kembali. Kirim lagi "buka catatan" setelah tersambung, dan ambil ulang datanya. Socket.IO sudah mengurus nyambung ulang, tapi room tetap harus di-join lagi.
Di latihan: pola on dan emit
ws.on("message"), socket.on("buka catatan"), io.emit(...): semuanya pola EventEmitter bawaan Node. on(nama, fungsi) memasang pendengar, emit(nama, data) memicu event dan memanggil semua pendengarnya. Di latihan, ChatRoom mewarisi EventEmitter: sendMessage menyimpan pesan ke this.messages lalu this.emit("message", ...), dan pendengar yang dipasang di constructor mencetaknya:
Budi bergabung ke chat
Siti bergabung ke chat
[Budi]: Halo semua!
[Siti]: Halo Budi!
Total pesan: 2
⚠️ Jebakan yang sering ketemu
- Mengirim object langsung. Di server,
ws.send({ a: 1 })melemparTypeError. Di browser, yang terkirim teks[object Object]. KirimJSON.stringify(...), terima denganJSON.parse(...). - Mengirim ke koneksi yang sudah tertutup. Cek
readyState === WebSocket.OPENdulu. - Mengira WebSocket nyambung sendiri. Yang bawaan browser enggak. Tulis backoff-nya, atau pakai Socket.IO.
- Klien
WebSocketbiasa ke server Socket.IO. Gak akan tersambung: Socket.IO punya protokolnya sendiri di atas WebSocket.
TL;DR: WebSocket adalah koneksi dua arah (full-duplex) yang tetap terbuka. Dimulai dengan GET ber-header Upgrade: websocket, dijawab 101 Switching Protocols dengan Sec-WebSocket-Accept = base64(SHA-1(kunci + GUID)). Di Node pakai ws: wss.on("connection"), ws.on("message"), ws.send(JSON.stringify(...)). Socket.IO menambah event, room, dan nyambung ulang: io.emit(...) ke semua, socket.join(room) masuk room, io.to(room).emit(...) ke anggota room saja. Polling boros request, SSE cukup untuk kabar satu arah. Koneksi putus (kode 1006) disambung lagi dengan backoff 1, 2, 4 detik, lalu join room lagi. Semua ini pola on/emit dari EventEmitter.