WebSocket, pipa dua arah yang bisa didorong server.
Penelusuran dari prinsip dasar tentang komunikasi backend real-time, dari kenapa bentuk request-response HTTP tidak bisa mendorong, lewat handshake upgrade WebSocket, frame, dan siklus hidup koneksi, sampai membangun server, mengelola ribuan koneksi, menangani backpressure, dan bagian yang sulit: menskalakan koneksi ber-state lintas banyak instance dengan backplane pub/sub. Ditulis bukan hanya menyebut apa fungsi tiap bagian tapi kenapa ia ada dan bagaimana ia bekerja di dalamnya. Kode server dan klien di Go dan Python.
Masalah yang Dipecahkan WebSocket
HTTP polos punya satu bentuk: klien bertanya, server menjawab (model request-response bab HTTP). Server tidak pernah bisa bicara duluan, ia tidak punya cara mendorong data ke klien yang belum baru saja meminta. Itu baik untuk mengambil halaman atau memanggil API, tapi hancur begitu Anda butuh server memberi tahu klien saat hal terjadi: pesan chat baru, tick harga langsung, notifikasi, kursor kolaborator bergerak, kecocokan ditemukan. Server punya pembaruannya; HTTP tidak memberinya kanal untuk mengirimkannya.
Sebelum WebSocket, Anda memalsukan server push dengan menyuruh klien bertanya berulang-polling. Setiap beberapa detik klien mengirim "ada yang baru?" dan biasanya mendengar "tidak." Ini boros di setiap sumbu: banjir request yang kebanyakan tidak mengembalikan apa pun, overhead penuh header HTTP dan setup koneksi di tiap request, dan latensi dibatasi interval polling Anda (poll tiap 5 detik dan event bisa duduk tak terkirim hampir 5 detik). WebSocket ada untuk mengganti hack itu dengan koneksi persisten dua arah yang nyata tempat kedua sisi bisa mengirim kapan saja.
HTTP tidak bisa membiarkan server memulai. WebSocket membangun satu koneksi full-duplex yang panjang umur sehingga entah sisi mana bisa mengirim pesan di waktu mana pun, tanpa overhead request per pesan dan tanpa jeda polling. Segala hal lain cara pipa itu dibuka, dibingkai, dikelola, dan di-scaling.
Spektrum Real-Time
WebSocket bukan satu-satunya cara mendapat pembaruan server-ke-klien, dan meraihnya secara refleks kesalahan umum. Ada spektrum teknik, masing-masing titik berbeda pada kompromi antara kesederhanaan dan kemampuan. Mengetahui keempatnya membuat Anda memilih hal paling sederhana yang memenuhi kebutuhan (panduan keputusan penuh di bagian 18).
| Teknik | Arah | Cara kerjanya | Terbaik saat |
|---|---|---|---|
| Short polling | klien menarik | Request berulang pada timer | Pembaruan jarang & toleransi latensi tinggi; Anda ingin nol infrastruktur |
| Long polling | klien menarik | Server menahan request terbuka sampai punya data, lalu klien bertanya lagi | Anda butuh perilaku mirip push tapi harus bekerja lewat proxy/klien lama mana pun |
| SSE | server -> klien | Satu respons HTTP panjang umur yang men-stream event teks | Push satu arah server (feed, notifikasi), lebih sederhana dari WS |
| WebSocket | dua arah | Koneksi full-duplex persisten yang di-upgrade dari HTTP | Interaksi dua arah sejati berlatensi rendah (chat, game, kolaborasi) |
WebSocket opsi paling mumpuni dan paling menuntut operasional, koneksi ber-state persisten yang memperumit scaling, load balancing, dan deployment (Bagian III-IV). Jika Anda hanya butuh server mendorong ke klien (feed live, notifikasi, progres), SSE jauh lebih sederhana: ia cuma HTTP, auto-reconnect, dan melewati proxy dengan mulus. Pakai full duplex WebSocket hanya saat klien benar-benar perlu sering mengirim juga (bagian 18).
Apa Sebenarnya WebSocket Itu
WebSocket kanal komunikasi persisten, dua arah, full-duplex di atas satu koneksi TCP, dibangun lewat request HTTP lalu dipegang terbuka. Urai tiap kata: persistent, dibuka sekali dan dipegang untuk sesi, bukan per pesan; bidirectional / full-duplex, kedua sisi bisa mengirim serentak dan independen, bukan bergiliran; single TCP connection, satu socket membawa segalanya, di port yang sama dengan HTTP (80/443), jadi ia melewati firewall yang mengizinkan lalu lintas web.
Pilihan desain yang cerdas: WebSocket memulai hidup sebagai request HTTP lalu meng-upgrade (bagian 4). Ini bukan kebetulan, inilah yang membuat WebSocket memakai ulang infrastruktur web yang ada (port, TLS, proxy, origin yang sama) alih-alih butuh port atau protokol baru yang akan diblokir firewall. Setelah di-upgrade, koneksi berhenti bicara HTTP dan mulai bicara protokol framing WebSocket (bagian 5). Skema URL mencerminkan ini: ws:// (seperti http://) dan wss:// (TLS, seperti https://).
WebSocket mendahului dan duduk di samping HTTP/2. HTTP/2 punya stream termultipleks sendiri (bab gRPC), dan bidirectional streaming gRPC mencakup medan serupa untuk pemakaian service-to-service. WebSocket tetap standar untuk real-time browser-ke-server, justru karena browser mengekspos API WebSocket yang bersih tapi bukan framing HTTP/2 mentah (alasan yang sama browser tidak bisa bicara gRPC natif, gRPC bagian 19). Perbandingan di semua ini bagian 18.
Handshake Upgrade
Setiap WebSocket dimulai dengan satu request HTTP khusus, upgrade handshake (bab HTTP bagian 19, lengkap). Klien mengirim GET yang tampak normal dengan header yang berbunyi "aku ingin mengalihkan koneksi ini ke protokol WebSocket." Jika server setuju, ia membalas dengan status 101 Switching Protocols: bukan 200, dan sejak instan itu koneksi bukan lagi HTTP; ia WebSocket mentah yang membawa frame (bagian 5).
GET /ws/chat HTTP/1.1
Host: api.example.com
Upgrade: websocket <- "switch this connection"
Connection: Upgrade <- the Upgrade header is meaningful
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== <- a random client nonce (base64)
Sec-WebSocket-Version: 13 <- the protocol version
Sec-WebSocket-Protocol: chat.v1 <- optional subprotocol(s) offered
Origin: https://app.example.com <- the browser sends this; SERVER must check (sec 14)
HTTP/1.1 101 Switching Protocols <- NOT 200, the upgrade succeeded
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= <- proves the server spoke WebSocket
Sec-WebSocket-Protocol: chat.v1 <- the subprotocol the server picked
# After 101, both sides stop speaking HTTP and start exchanging WebSocket frames.
Apa yang dibuktikan Sec-WebSocket-Accept
Server menghitung Accept dengan menggabungkan Sec-WebSocket-Key klien dengan GUID ajaib tetap yang didefinisikan spesifikasi, meng-hash SHA-1, dan meng-encode base64 hasilnya. Ini bukan keamanan, GUID-nya publik, ia bukti pemahaman protokol: mengonfirmasi penjawabnya server WebSocket asli dan bukan cache atau proxy yang membuta menggema 101. Klien memverifikasi nilainya cocok sebelum memperlakukan koneksi sebagai WebSocket yang bekerja. Library melakukan ini untuk Anda; Anda hampir tidak pernah menghitungnya manual.
Sec-WebSocket-Protocol membuat klien dan server menegosiasikan subprotokol level aplikasi (mis. chat.v1, atau standar seperti graphql-ws), seam versioning yang bersih. Perhatikan satu keterbatasan penting yang membentuk auth (bagian 13): API WebSocket browser tidak membiarkan Anda men-set header HTTP arbitrer pada handshake (Anda tidak bisa menambah Authorization: Bearer), jadi kredensial harus bepergian cara lain, parameter query, cookie, field subprotocol, atau pesan pertama setelah terhubung.
Frame: Format Wire
Setelah handshake, data tidak mengalir sebagai aliran byte mentah, ia dipotong menjadi frame, unit terstruktur kecil dengan header ringkas. Framing yang membuat satu koneksi bisa membawa pesan diskret (jadi penerima tahu di mana satu berakhir dan berikutnya mulai), membedakan teks dari biner, mengirim sinyal kontrol seperti close dan ping, dan memecah pesan besar lintas beberapa frame. Anda jarang menyentuh frame langsung, library mengekspos "kirim pesan / terima pesan", tapi memahami strukturnya menjelaskan masking, tipe pesan, dan frame kontrol.
Teks vs biner, dan kenapa frame klien di-mask
- Dua tipe data. Sebuah frame entah text (UTF-8, JSON biasanya menumpang di sini) atau binary (byte mentah, Protobuf, gambar, format kustom). Library menyerahkan string atau byte sesuai.
- Masking wajib klien->server, dilarang server->klien. Browser XOR-mask setiap frame yang mereka kirim dengan kunci acak. Ini bukan enkripsi (pakai
wss://untuk itu, bagian 14); itu mitigasi keamanan supaya halaman jahat tidak bisa meracik byte yang membingungkan proxy perantara lama menjadi salah mem-parse aliran sebagai hal lain (cache poisoning). Frame server tidak pernah di-mask. - Fragmentasi. Pesan besar bisa dipecah menjadi urutan frame (FIN=0 di semua kecuali yang terakhir, opcode
0x0continuation di yang tengah), membuat pengirim bisa men-stream tanpa mem-buffer semuanya dulu.
Kenyamanan kunci dibanding TCP mentah: WebSocket berorientasi pesan, bukan berorientasi aliran byte. Anda mengirim pesan dan sisi lain menerima pesan itu utuh, bukan potongan arbitrer aliran byte yang harus Anda batasi ulang sendiri. Framing yang melakukan pembatasan. (Di balik layar tetap TCP, jadi byte terurut dan andal, Anda hanya dapat batas pesan di atasnya.)
Siklus Hidup Koneksi
WebSocket punya hidup yang jelas: buka -> tukar pesan -> tutup, dengan event di tiap tahap yang di-hook kode Anda. Open selesai setelah handshake (bagian 4). Lalu pesan mengalir bebas. Menutup sendiri handshake kecil: satu sisi mengirim close frame (opcode 0x8) yang membawa status code dan alasan opsional, sisi lain menggema close frame, lalu keduanya mematikan koneksi TCP. Close yang bersih membuat tiap sisi tahu kenapa koneksi berakhir.
| Close code | Makna |
|---|---|
1000 | Penutupan normal, selesai sebagaimana dimaksud |
1001 | Pergi, server mematikan atau klien menavigasi pergi |
1006 | Abnormal, koneksi jatuh tanpa close frame (jaringan mati, crash). Anda tidak pernah mengirim ini; Anda mengamatinya. |
1011 | Internal server error |
1008 / 1009 | Pelanggaran kebijakan / pesan terlalu besar |
Handshake close yang bersih hanya terjadi saat kedua sisi bekerja sama. Kenyataannya koneksi jatuh tanpa anggun sepanjang waktu, laptop tidur, Wi-Fi berkedip, ponsel berganti jaringan, NAT timeout, dan Anda dapat tidak ada close frame, hanya socket mati (code 1006 atau read error, sering baru disadari jauh belakangan). Anda tidak bisa mengandalkan close yang bersih untuk mendeteksi klien yang pergi. Persis itulah kenapa heartbeat ada (bagian 7) dan kenapa klien harus terhubung ulang (bagian 17).
Ping/Pong & Heartbeat
Karena koneksi mati sering tampak identik dengan yang idle (tanpa byte di keduanya), Anda butuh cara aktif membedakan "masih hidup" dari "pergi diam-diam." Protokol menyediakan frame kontrol ping dan pong persis untuk ini: satu sisi mengirim ping, yang lain harus menjawab pong. Heartbeat pola mengirim ping pada timer dan memperlakukan pong yang terlewat (dalam batas waktu) sebagai koneksi mati yang harus ditutup dan dibersihkan. Ini masalah liveness yang sama dengan probe Kubernetes (containerization bagian 17) dan keepalive consumer Kafka, dipecahkan dengan gagasan yang sama: jangan asumsikan sehat, verifikasi.
Server real-time tanpa heartbeat perlahan terisi zombie connection: klien yang hilang tapi socket-nya tetap dipegang server, membocorkan memori dan goroutine/tugas (bagian 19). Set interval ping dan batas pong, jatuhkan koneksi yang melewatkannya, dan bebaskan resource-nya. Ia tidak opsional untuk produksi; begitulah Anda menjaga tabel koneksi tetap jujur.
Server WebSocket
Endpoint server melakukan tiga hal: menerima upgrade (bagian 4), lalu menjalankan loop membaca pesan dan cara menulisnya. Di Go library standar tidak mengirim implementasi WebSocket, jadi Anda memakai library, gorilla/websocket (standar lama) atau coder/websocket (nhooyr). Di Python, framework seperti FastAPI/Starlette mengekspos WebSocket langsung, dan library websockets standar mandirinya. Ini server echo minimal, "hello world"-nya WebSocket.
// github.com/gorilla/websocket
var upgrader = websocket.Upgrader{
// CheckOrigin guards against cross-site hijacking, DO NOT leave it open (sec 14).
CheckOrigin: func(r *http.Request) bool {
return r.Header.Get("Origin") == "https://app.example.com"
},
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil) // performs the 101 handshake (sec 4)
if err != nil {
return // upgrader already wrote an HTTP error
}
defer conn.Close() // always close -> frees the socket (sec 19)
for { // the READ LOOP, one per connection
mt, msg, err := conn.ReadMessage()
if err != nil {
break // client closed or connection died (sec 6) -> exit, cleanup via defer
}
// echo it straight back
if err := conn.WriteMessage(mt, msg); err != nil {
break
}
}
}
func main() {
http.HandleFunc("/ws", wsHandler) // a normal HTTP route that upgrades
http.ListenAndServe(":8080", nil)
}# FastAPI / Starlette expose WebSockets natively (the `websockets` lib is an alternative).
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
@app.websocket("/ws")
async def ws_handler(ws: WebSocket):
# Validate origin yourself before accepting (sec 14); then complete the handshake.
await ws.accept() # performs the 101 handshake (sec 4)
try:
while True: # the READ LOOP, one coroutine per connection
msg = await ws.receive_text() # awaits the next message
await ws.send_text(msg) # echo it straight back
except WebSocketDisconnect:
pass # client closed or connection died (sec 6)
# FastAPI cleans up the connection when the coroutine returns
# uvicorn app:app, uvicorn speaks the WebSocket protocol for youGo memberi tiap koneksi goroutine yang menjalankan loop baca blocking; Python memberi tiap koneksi coroutine async yang menunggu pesan. Struktur sama, terima, loop membaca, tulis, diekspresikan dalam model konkurensi tiap bahasa.
Aturan halus tapi kritis (terutama di Go dengan gorilla): tulis serentak ke satu koneksi tidak aman, dan Anda biasanya ingin satu goroutine membaca dan satu menulis. Pola standarnya goroutine writer khusus yang diberi makan channel, jadi semua tulis diseriakan. Jangan menulis ke koneksi yang sama dari banyak goroutine tanpa sinkronisasi, ia merusak aliran frame. Pola hub (bagian 10) membangun persis struktur ini.
Klien WebSocket
Backend sering juga klien WebSocket, mengonsumsi feed real-time dari layanan lain, menjembatani sistem, atau dalam tes. Sisi klien: dial URL ws:///wss:// (yang melakukan handshake), lalu baca dan tulis pesan. Bentuknya mencerminkan server.
// github.com/gorilla/websocket
func runClient() {
// Dial performs the upgrade handshake; header carries auth where allowed (sec 13).
h := http.Header{"Authorization": {"Bearer " + token}}
conn, _, err := websocket.DefaultDialer.Dial("wss://api.example.com/ws", h)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// reader goroutine
go func() {
for {
_, msg, err := conn.ReadMessage()
if err != nil {
return
}
log.Printf("recv: %s", msg)
}
}()
// send a message
conn.WriteMessage(websocket.TextMessage, []byte(`{"type":"hello"}`))
// graceful close: send a close frame, then the socket shuts (sec 6)
conn.WriteMessage(websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, "bye"))
}# `websockets` library, clean async client
import asyncio, websockets
async def run_client():
# extra_headers carries auth where the runtime allows it (sec 13);
# browsers can't set headers, but a backend client can.
async with websockets.connect(
"wss://api.example.com/ws",
extra_headers={"Authorization": f"Bearer {token}"},
) as ws: # context manager -> handshake + clean close
await ws.send('{"type":"hello"}') # send a message
async for msg in ws: # iterate incoming messages
print("recv:", msg)
# leaving the `async with` block sends a close frame and shuts down (sec 6)
asyncio.run(run_client())Saat service Anda jadi klien, perlakukan feed seperti upstream tak andal apa pun: koneksi akan jatuh (bagian 6), jadi bungkus hubungkan-dan-baca dalam loop reconnect dengan backoff (bagian 17), dan buat pemrosesan idempotent jika feed bisa mengirim ulang saat reconnect (disiplin at-least-once yang sama dengan bab Kafka). Klien WebSocket backend tanpa logika reconnection diam-diam berhenti menerima saat pertama kali jaringan berisik.
Manajemen Koneksi
Satu koneksi sepele; kerja nyata mengelola banyak. Server yang memegang ribuan koneksi live butuh tempat terpusat yang melacak siapa yang terhubung supaya bisa merutekan dan menyiarkan pesan. Solusi kanonis hub (atau "connection manager"): pemilik tunggal dari set koneksi aktif, dengan channel/lock untuk mendaftar, mencabut, dan mengirim, menghindari bahaya tulis-serentak dari bagian 8 dengan menyaring semua lewat satu koordinator.
type Hub struct {
clients map[*Client]bool
register chan *Client
unregister chan *Client
broadcast chan []byte
}
// One goroutine owns the map -> all mutation is serialized here (no locks needed).
func (h *Hub) Run() {
for {
select {
case c := <-h.register:
h.clients[c] = true
case c := <-h.unregister:
if _, ok := h.clients[c]; ok {
delete(h.clients, c)
close(c.send) // tell the client's writer goroutine to stop
}
case msg := <-h.broadcast:
for c := range h.clients {
select {
case c.send <- msg: // queue into the client's buffered channel
default: // buffer full -> slow client; drop it (sec 12)
delete(h.clients, c)
close(c.send)
}
}
}
}
}import asyncio
class Hub:
def __init__(self):
self.clients: set[WebSocket] = set() # the active connection set
async def register(self, ws: WebSocket):
await ws.accept()
self.clients.add(ws)
def unregister(self, ws: WebSocket):
self.clients.discard(ws) # idempotent removal
async def broadcast(self, message: str):
dead = []
for ws in self.clients:
try:
await ws.send_text(message) # fan out to everyone
except Exception:
dead.append(ws) # send failed -> connection is gone
for ws in dead:
self.unregister(ws) # clean up on the way out
hub = Hub()
# (asyncio is single-threaded, so the set needs no lock; just don't mutate
# it while iterating, collect dead ones, remove after.)Dua hal memisahkan mainan dari server nyata. Keamanan konkurensi: jangan pernah menyentuh set koneksi bersama dari banyak goroutine/tugas tanpa koordinasi (Go: satu goroutine pemilik + channel kirim per klien; Python asyncio: single-threaded, tapi jangan memutasi sambil iterasi). Pembersihan: setiap jalur yang mengakhiri koneksi harus menghapusnya dari registry dan membebaskan resource-nya, atau Anda bocor (bagian 19). Daftar saat connect, selalu cabut saat disconnect, termasuk jalur error dan panic.
Broadcast & Room
Aplikasi nyata jarang mengirim ke semua orang, mereka mengirim ke subset relevan: anggota satu ruang chat, subscriber satu simbol saham, pemain satu game. Polanya room (alias channel atau topic): koneksi berlangganan ke grup bernama, dan Anda broadcast ke grup alih-alih seluruh server. Secara konkret, hub memegang map dari nama room ke set koneksi di dalamnya.
Di satu server, room sederhananya map[namaRoom] -> set koneksi, dan broadcast mengiterasi set itu. Jebakannya muncul dengan banyak server (bagian 15): anggota room mungkin tersebar di instance berbeda, jadi pesan yang diproduksi di satu server harus mencapai anggota yang terhubung ke yang lain. Map room di memori saja tidak bisa melakukan itu, itulah seluruh alasan backplane ada. Bangun room sederhana dulu; raih backplane saat Anda melampaui satu instance.
Backpressure & Consumer Lambat
Mode kegagalan unik sistem push: apa yang terjadi saat Anda memproduksi pesan lebih cepat daripada yang bisa diterima klien? Ponsel di koneksi lemah, tab browser di-throttle di latar, klien menguras perlahan sementara server terus menghasilkan data untuknya. Tanpa rencana, data itu menumpuk di buffer per-klien yang tumbuh tanpa batas, dan satu klien lambat bisa menghabiskan memori server. Ini backpressure, dan Anda harus memutuskan kebijakan.
Implementasi standar (terlihat di hub bagian 10): beri tiap klien antrean kirim yang terbatas. Saat Anda mencoba enqueue dan penuh, Anda tidak memblokir broadcaster (itu akan membiarkan satu klien lambat menahan semua orang), gantinya Anda menerapkan kebijakan:
- Jatuhkan pesan: untuk data lossy yang hanya yang terbaru penting (posisi live, ticker), buang kelebihannya atau simpan hanya yang terbaru. Klien lambat melewatkan beberapa pembaruan tapi server tetap sehat.
- Putuskan klien: untuk data yang celahnya tak bisa diterima, tutup koneksi lambat dan biarkan ia terhubung ulang dan menyinkronkan ulang (bagian 17). Lebih baik menjatuhkan satu klien daripada menurunkan semua.
- Konflasi / batch: runtuhkan beberapa pembaruan tertunda menjadi satu (kirim snapshot terbaru alih-alih setiap langkah perantara).
Aturan kardinal sistem push: jangan pernah mem-buffer tanpa batas per koneksi. Setiap klien dapat antrean berukuran tetap dan kebijakan overflow eksplisit (drop, disconnect, atau conflate). Ini padanan real-time dari penanganan consumer lambat Kafka dan kekhawatiran backpressure di sistem streaming mana pun, satu consumer lambat tidak boleh pernah mampu merobohkan producer.
Autentikasi
Mengautentikasi WebSocket lebih rumit daripada request normal karena keterbatasan browser dari bagian 4: konstruktor WebSocket JavaScript tidak bisa men-set header kustom, jadi Authorization: Bearer biasa tidak tersedia dari browser. Anda mengautentikasi di handshake (ia tetap request HTTP, server bisa membaca cookie, URL, dan subprotokol) dan Anda melakukannya sebelum memanggil Upgrade/accept. Prinsip dari bab auth berlaku: verifikasi identitas di pintu; socket yang tidak terautentikasi tidak boleh pernah di-upgrade.
| Pendekatan | Cara | Catatan |
|---|---|---|
| Cookie / sesi | Browser mengirim cookie auth pada handshake otomatis | Alami untuk web app same-site; pasangkan dengan pemeriksaan origin ketat (bagian 14) untuk menahan CSWSH |
| Token di query string | wss://host/ws?token=JWT | Berfungsi di mana-mana, tapi token bisa mendarat di log/riwayat, pakai token berumur pendek |
| Field subprotocol | Selundupkan token di Sec-WebSocket-Protocol | Menghindari URL; trik JWT-over-WS umum |
| Pesan pertama | Hubungkan, lalu kirim pesan auth sebelum yang lain | Fleksibel; server harus menolak/menutup jika auth tidak tiba tepat waktu |
| Header (non-browser) | Klien backend bisa men-set Authorization (bagian 9) | Paling bersih, tapi hanya untuk klien non-browser |
func wsHandler(w http.ResponseWriter, r *http.Request) {
// AUTHENTICATE FIRST, before upgrading. Reject with a normal HTTP error.
token := r.URL.Query().Get("token") // or read a cookie / subprotocol
user, err := verifyJWT(token) // your auth logic (auth chapter)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized) // 401, no upgrade
return
}
// Only now perform the upgrade; attach the identity to the connection.
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
serve(conn, user) // every message from this conn is now tied to `user`
}@app.websocket("/ws")
async def ws_handler(ws: WebSocket):
# AUTHENTICATE FIRST, before accept(). Close with a policy code if it fails.
token = ws.query_params.get("token") # or a cookie / subprotocol
user = verify_jwt(token) # your auth logic (auth chapter)
if user is None:
await ws.close(code=1008) # 1008 = policy violation; no accept
return
await ws.accept() # only now complete the handshake
await serve(ws, user) # messages are tied to `user`Autentikasi di handshake membangun siapa pemilik koneksi. Tapi socket panjang umur bertahan melampaui satu aksi, jadi Anda tetap harus mengotorisasi setiap pesan terhadap apa yang diizinkan pengguna itu (bisakah mereka posting ke room ini? mengirim perintah ini?), aturan authz-pada-setiap-aksi yang sama dengan REST. Juga pertimbangkan token bisa kedaluwarsa di tengah koneksi: sesi panjang butuh strategi re-auth atau refresh token, atau Anda punya koneksi yang diautentikasi kredensial yang tidak lagi valid.
Keamanan
WebSocket mewarisi model ancaman web plus beberapa miliknya sendiri. Prinsip bab keamanan berlaku langsung; ini esensial khusus WebSocket.
| Perhatian | Apa & kenapa |
|---|---|
Pakai wss:// (TLS) | ws:// polos tidak terenkripsi, bisa dibaca dan dirusak di kabel. Selalu pakai wss:// di produksi, persis seperti HTTPS untuk HTTP (TLS, bab HTTP bagian 20). |
Validasi Origin (CSWSH) | Browser mengirim header Origin pada handshake; server harus memeriksanya. Melewatkannya memungkinkan Cross-Site WebSocket Hijacking: situs jahat membuka socket terautentikasi memakai cookie korban (sepupu WS dari CSRF). Allowlist origin Anda sendiri. |
| Validasi setiap pesan | Perlakukan frame masuk sebagai input tak tepercaya (bab validasi): periksa struktur, tipe, ukuran; tolak pesan salah bentuk. Koneksi yang terautentikasi tidak membuat payload-nya aman. |
| Batasi ukuran pesan | Set ukuran frame/pesan maksimum supaya klien tidak bisa mengirim payload raksasa untuk menghabiskan memori (tutup dengan 1009 jika dilampaui). |
| Rate-limit | Koneksi persisten bisa membanjiri Anda dengan pesan; rate-limit per koneksi/pengguna untuk mencegah penyalahgunaan dan loop tak sengaja. |
| Batasi koneksi per pengguna | Batasi koneksi serentak per identitas supaya satu klien tidak bisa membuka ribuan dan menghabiskan tabel koneksi (bagian 19). |
CheckOrigin is a trap
Lubang keamanan WebSocket paling umum: membiarkan pemeriksaan origin dinonaktifkan. CheckOrigin default Gorilla menolak request lintas-origin, tapi tak terhitung tutorial menyuruh Anda menimpanya dengan return true, yang membuka Cross-Site WebSocket Hijacking untuk endpoint terautentikasi-cookie mana pun. Selalu allowlist origin spesifik yang Anda percaya (seperti server bagian 8), jangan pernah izinkan menyeluruh. Jika Anda mengautentikasi dengan bearer token alih-alih cookie risikonya lebih rendah, tapi validasi origin tetap default yang benar.
Scaling: Backplane
Ini masalah sulit yang mendefinisikan WebSocket, dan alasan ia memperumit arsitektur jauh lebih banyak daripada HTTP stateless. Koneksi WebSocket ber-state dan ter-pin ke satu server: socket hidup di memori instance spesifik tempat klien terhubung. Begitu Anda menjalankan lebih dari satu instance, yang harus Anda lakukan demi kapasitas dan ketersediaan, muncul pertanyaan menyakitkan: jika user A terhubung ke server 1 dan user B (di room chat yang sama) terhubung ke server 2, bagaimana pesan A mencapai B? Server 1 tidak punya akses ke koneksi server 2.
Solusinya backplane (alias adapter pub/sub): sistem pesan eksternal yang dihubungi semua instance server. Saat server perlu mengirim pesan ke klien yang mungkin di instance mana pun, ia menerbitkan pesan ke backplane; setiap instance berlangganan, menerimanya, dan meneruskannya ke koneksi lokal mana pun yang seharusnya menerimanya. Instance tidak lagi perlu tahu koneksi satu sama lain, mereka berkoordinasi lewat bus bersama. Backplane umum:
- Redis Pub/Sub: pilihan paling umum: sederhana, cepat, latensi rendah, fire-and-forget. Pas saat Anda hanya butuh fan-out live dan tidak butuh riwayat (user yang terputus cukup melewatkan pesan).
- Kafka: saat Anda juga ingin daya tahan, replay, dan event-nya memberi makan sistem lain (bab Kafka): terbitkan event ke topic, tiap instance WS mengonsumsi dan meneruskan. Lebih berat, tapi pesannya jadi bagian stream event tahan lama Anda.
- NATS / pub/sub cloud, opsi messaging cepat lain dengan mekanik serupa.
Backplane mengubah "N server masing-masing dengan pulau koneksi sendiri" menjadi "N server yang secara kolektif berperilaku seperti satu." Ini gagasan decoupling yang sama dengan bab Kafka diterapkan ke pengiriman live: publisher tidak perlu tahu instance mana yang menampung penerima, ia menerbitkan, dan siapa pun yang memegang koneksi yang mengirimkan. Anda tidak bisa menskalakan WebSocket horizontal tanpa sesuatu yang memainkan peran ini; mendesainnya di awal jauh lebih mudah daripada meretrofit.
Load Balancing & Sticky Session
Menaruh load balancer di depan server WebSocket punya kerutannya sendiri, karena koneksinya panjang umur dan ber-state alih-alih request cepat. Tiga hal yang penting: LB harus mendukung WebSocket, koneksi biasanya butuh affinity, dan deploy harus menguras koneksi dengan anggun.
- LB harus bicara WebSocket. Ia harus meneruskan handshake
Upgradelalu menjaga koneksi tetap terbuka (proxy sadar L7/HTTP, atau pass-through TCP L4). Kebanyakan LB modern (Nginx, HAProxy, ALB cloud, controller Ingress Kubernetes bab 21) mendukung ini, tapi Anda mengonfigurasi idle timeout lebih panjang, idle timeout 60 detik default akan membunuh WebSocket yang sunyi, alasan lain untuk heartbeat (bagian 7). - Sticky session / affinity. Begitu klien terhubung ke instance 1, semua frame-nya harus terus pergi ke instance 1 (di sanalah socket-nya hidup). Dengan TCP mentah itu otomatis (satu koneksi = satu backend), tapi koneksi ulang dan routing per-pesan menginginkan session affinity supaya klien kembali ke instance yang konsisten. Dengan backplane (bagian 15) affinity kurang penting untuk pengiriman (instance mana pun bisa publish/menerima), tapi ia tetap memengaruhi state lokal.
- Connection draining saat deploy. Yang besar: deploy versi baru (rolling update bab 21 bagian 18) menghentikan instance, dan setiap WebSocket di instance yang dihentikan jatuh. Anda harus menguras dengan anggun, berhenti menerima koneksi baru, kirim close frame (code
1001"going away") supaya klien terhubung ulang bersih ke instance sehat, dan beri waktu sebelumSIGKILL(bab graceful shutdown). Meski begitu, deploy menyebabkan badai reconnection, itulah kenapa reconnection klien yang kokoh (bagian 17) wajib.
Tidak seperti HTTP stateless, tempat rolling deploy tak terlihat, menggulir armada WebSocket menjatuhkan setiap koneksi live saat instance lama pensiun. Ini fundamental koneksi ber-state, bukan bug. Rencanakan untuk itu: drain anggun dengan 1001 di server, reconnection exponential-backoff dengan jitter di klien (bagian 17, untuk menghindari reconnect thundering-herd), dan desain pesan yang mentoleransi celah singkat dan re-sync. Perlakukan reconnection sebagai kasus normal, bukan pengecualian.
Reconnection & Ketahanan
Mengingat semua di atas, koneksi jatuh diam-diam (bagian 6), deploy memutus semua orang (bagian 16), jaringan berisik, klien real-time yang tidak terhubung ulang otomatis rusak sejak desain. Ketahanan hidup kebanyakan di sisi klien, dengan dukungan sisi server untuk menyinkronkan ulang state yang terlewat. (Catatan: SSE memberi Anda reconnection gratis, alasan lain untuk memilihnya saat Anda hanya butuh server push, bagian 2.)
Checklist ketahanan
- Terhubung ulang dengan exponential backoff + jitter. Jangan menghantam server yang kesulitan dengan retry instan; kembangkan jeda (1s, 2s, 4s, ... dibatasi) dan tambah keacakan supaya ribuan klien yang terputus oleh deploy tidak terhubung ulang di instan yang sama (masalah thundering-herd / reconnect-storm dari bagian 16).
- Sinkronkan ulang setelah reconnect. Koneksi baru berarti kemungkinan celah di stream. Saat reconnect, klien harus mendamaikan: ambil state saat ini via panggilan HTTP normal, atau beri tahu server "event terakhir yang kulihat N, kirim aku semua sejak itu" (resume token / nomor urut). Di sinilah backplane tahan lama seperti Kafka (bagian 15) bersinar, pesan yang terlewat masih di log untuk diputar ulang.
- Putuskan jaminan pengiriman Anda. Seperti Kafka (bagian 9), pengiriman WebSocket tidak ajaib exactly-once. Untuk data kritis, beri pesan nomor urut/ID supaya klien bisa mendeteksi celah dan duplikat, dan buat penanganan idempotent. Untuk data lossy (posisi live), celah tidak masalah, pembaruan berikutnya mengoreksinya.
- Buffer keluar di klien saat terputus (dalam batas), dan flush saat reconnect, supaya aksi pengguna yang dilakukan selama blip tidak sekadar hilang.
Koneksi jatuh itu operasi normal, bukan kasus error, jadi logika reconnection bukan pelengkap, ia fungsionalitas inti yang Anda bangun dan uji sengaja. Keandalan sistem real-time kebanyakan ditentukan oleh seberapa anggun ia menangani gejolak konstan koneksi yang datang dan pergi. Desain untuk jatuhnya.
WebSocket vs SSE vs Streaming vs Polling
Keputusan yang harus datang sebelum Anda membangun apa pun real-time. WebSocket kuat tapi membawa beban operasional Bagian IV (koneksi ber-state, backplane, sticky session, reconnection), jadi memilih tool paling sederhana yang cocok penghematan nyata. Empat penantang, menyatukan spektrum (bagian 2) dan bab gRPC.
| Kebutuhan | Cocok terbaik | Kenapa |
|---|---|---|
| Pembaruan jarang, toleran latensi | Polling | Sepele, stateless, tanpa infra. Jangan over-engineer. |
| Push server -> browser, satu arah | SSE | Cuma HTTP, auto-reconnect, melewati proxy dengan mulus; jauh lebih sederhana dari WS. Feed, notifikasi, dashboard live. |
| Browser <-> server, sering dua arah | WebSocket | Satu-satunya opsi untuk dua arah latensi rendah sejati di browser. Chat, game, edit kolaboratif, UI trading. |
| Streaming service <-> service | gRPC streaming | Bertipe, efisien, natif HTTP/2 (bab gRPC). Untuk backend internal, bukan browser. |
Bergerak ke kiri sampai tidak bekerja lagi. Butuh server push ke browser tapi bukan celoteh klien->server? SSE, bukan WebSocket, Anda melewati seluruh beban backplane/sticky-session Bagian IV. Benar-benar butuh lalu lintas browser dua arah yang sering? WebSocket. Dua layanan backend? gRPC streaming. Meraih WebSocket saat SSE atau bahkan polling sudah cukup salah satu kesalahan over-engineering paling umum dan mahal dalam kerja real-time.
Jebakan Umum
Mode kegagalan yang menggigit server WebSocket di produksi, kebanyakan soal manajemen resource untuk koneksi panjang umur ber-state, disiplin yang tidak pernah dituntut HTTP stateless.
| Jebakan | Apa yang terjadi | Perbaikan |
|---|---|---|
| Bocor koneksi / goroutine | Jalur disconnect lupa mencabut & membebaskan; goroutine/tugas dan memori menumpuk sampai server mati | Selalu bersihkan di setiap jalur keluar (defer/finally); daftar saat connect, cabut saat disconnect (bagian 10) |
| Tanpa heartbeat -> zombie | Koneksi mati tampak idle (bagian 6); tabel terisi socket ke klien yang hilang | Ping pada timer; jatuhkan saat pong terlewat (bagian 7) |
| Buffer kirim tak terbatas | Antrean satu klien lambat tumbuh tanpa batas dan OOM server | Antrean per-klien terbatas + kebijakan drop/disconnect (bagian 12) |
| Tulis serentak ke satu koneksi | Banyak goroutine menulis socket yang sama merusak aliran frame | Satu goroutine writer diberi makan channel (bagian 8, bagian 10) |
| Memblokir loop baca | Melakukan kerja lambat inline di loop baca menahan koneksi itu (dan menyandung heartbeat) | Serahkan kerja ke worker; jaga loop baca cepat (gagasan antrean, bab 10) |
| Tanpa pemeriksaan origin | Cross-Site WebSocket Hijacking pada endpoint cookie-auth (bagian 14) | Allowlist origin; jangan pernah izinkan menyeluruh |
| Lupa backplane | Berfungsi di satu instance; pesan hilang antar pengguna begitu Anda scale out (bagian 15) | Rancang backplane pub/sub di awal |
| Tanpa logika reconnection | Klien diam-diam berhenti menerima setelah blip jaringan atau deploy pertama | Terhubung ulang dengan backoff + jitter, lalu sinkronkan ulang (bagian 17) |
Perhatikan polanya: hampir setiap jebakan resource yang tidak dibatasi atau tidak dibebaskan, koneksi bocor, buffer tak terbatas, socket zombie. HTTP stateless memaafkan kecerobohan di sini karena tiap request pendek umurnya dan membersihkan diri; WebSocket hidup berjam-jam dan memegang memori sepanjang waktu. Perlakukan setiap koneksi sebagai sesuatu yang harus Anda pertanggungjawabkan eksplisit dari buka sampai tutup, dan kebanyakan masalah produksi tidak pernah muncul.
Mendesain Sistem Real-Time
Menarik manual ke penilaian desain untuk apa pun real-time.
Prinsip desain
- Pilih transport paling ringan dulu. Polling -> SSE -> WebSocket -> gRPC streaming, paling kiri yang cocok (bagian 18). Jangan bayar kompleksitas WebSocket kecuali Anda butuh lalu lintas browser dua arah.
- Rancang backplane sejak hari pertama jika Anda akan pernah menjalankan lebih dari satu instance, yang selalu, di produksi (bagian 15). Meretrofit pengiriman lintas-instance ke desain satu-instance menyakitkan.
- Perlakukan disconnect sebagai normal. Heartbeat untuk mendeteksinya (bagian 7), drain anggun saat deploy (bagian 16), reconnection klien dengan backoff + jitter dan re-sync state (bagian 17). Koneksi akan bergejolak; bangun untuk itu.
- Batasi semuanya per koneksi. Ukuran buffer kirim, ukuran pesan, tingkat pesan, koneksi per pengguna (bagian 12, bagian 14, bagian 19). Apa pun tak terbatas pemadaman yang menunggu satu klien buruk.
- Jangan asumsikan pengiriman andal, terurut, exactly-once. Tambah nomor urut/ID untuk deteksi celah dan penanganan idempotent di tempat yang penting; terima kehilangan di tempat yang tidak (bagian 17, menggema Kafka bagian 9).
- Autentikasi di handshake, otorisasi setiap pesan, amankan kanalnya.
wss://, pemeriksaan origin, pesan masuk divalidasi (bagian 13-14). - Jaga loop baca cepat; geser kerja nyata ke antrean/worker (bab 10), dan seriakan tulis lewat satu writer per koneksi (bagian 8).
Rengkuh WebSocket untuk pengalaman browser dua arah berlatensi rendah yang benar-benar interaktif: chat, game multipemain, edit kolaboratif, antarmuka trading/taruhan live, dashboard real-time dengan interaksi klien. Jangan merengkuhnya untuk server push satu arah (pakai SSE), untuk pembaruan sesekali (polling), untuk request/response (HTTP/REST polos, bab HTTP), atau untuk streaming service-to-service (gRPC). Keterampilannya, seperti biasa, mencocokkan transport ke bentuk interaksi aktual alih-alih meraih tool paling kuat secara refleks.
Crib-Sheet
Seluruh manual dipadatkan menjadi yang Anda raih saat tertekan.
| Konsep | Satu baris |
|---|---|
| Kenapa WebSocket | HTTP tidak bisa mendorong; WS pipa full-duplex persisten yang bisa dikirim kedua sisi kapan saja. |
| Spektrum | Polling -> long-polling -> SSE (satu arah) -> WebSocket (dua arah). Pilih yang paling sederhana. |
| Apa itu | Satu koneksi TCP panjang umur, dimulai via HTTP, di port 80/443. ws:// / wss://. |
| Handshake | HTTP GET + Upgrade: websocket -> 101 Switching Protocols. Bukan 200. |
| Frame | FIN, opcode (text/binary/close/ping/pong), klien->server di-mask; berorientasi pesan, bukan aliran byte. |
| Siklus hidup | buka -> pesan -> tutup (dengan code). 1006 = mati tanpa close frame. |
| Heartbeat | Ping/pong pada timer; pong terlewat = mati -> tutup + bersihkan. Selalu jalankan. |
| Server | Terima upgrade, lalu loop baca; library yang melakukan framing (gorilla / FastAPI / websockets). |
| Konkurensi | Satu writer per koneksi; jangan pernah menulis socket yang sama dari banyak goroutine. |
| Hub | Registry terpusat memiliki koneksi; daftar/cabut/broadcast. Selalu bersihkan. |
| Room | Kelompokkan koneksi berdasarkan nama; broadcast ke grup, bukan semua orang. |
| Backpressure | Buffer per-klien terbatas; saat overflow drop / disconnect / conflate. Jangan pernah buffer tak terbatas. |
| Auth | Di handshake (cookie/query/subprotocol/pesan pertama), browser tidak bisa men-set header. Otorisasi setiap pesan. |
| Keamanan | wss:// + validasi Origin (CSWSH) + validasi pesan + batas ukuran/rate. |
| Backplane | Koneksi ter-pin per instance; pub/sub Redis/Kafka membuat server mengirim lintas instance. |
| Load balancing | LB harus bicara WS; naikkan idle timeout; affinity; drain (1001) saat deploy. |
| Deploy | Setiap rolling deploy memutus semua klien, reconnection wajib. |
| Reconnection | Backoff + jitter, lalu sinkronkan ulang state terlewat (resume token / fetch HTTP). |
| Pengiriman | Bukan exactly-once; tambah ID urut + idempotensi tempat celah penting. |
| vs yang lain | SSE untuk push satu arah; gRPC streaming untuk service<->service; polling untuk pembaruan jarang. |
| Jebakan | Bocor, zombie, buffer tak terbatas, tulis serentak, tanpa pemeriksaan origin, tanpa backplane. |
| Aturan desain | Transport paling ringan yang cocok; batasi semuanya; perlakukan disconnect sebagai normal. |
Seluruh topik dalam satu tarikan napas: WebSocket ada karena HTTP tidak bisa membiarkan server mendorong, dan polling boros (bagian 1), tapi mereka opsi terberat pada spektrum yang mencakup polling dan SSE satu arah (bagian 2). WebSocket satu koneksi TCP full-duplex persisten (bagian 3) yang dimulai sebagai handshake Upgrade HTTP yang dijawab 101 (bagian 4), lalu bertukar frame berorientasi pesan (bagian 5) melalui siklus hidup buka->pesan->tutup (bagian 6), dijaga jujur heartbeat ping/pong karena koneksi mati tampak idle (bagian 7). Anda membangun server sebagai terima-lalu-loop-baca (bagian 8), sering juga berperan klien (bagian 9), dan mengelola banyak koneksi lewat hub terpusat (bagian 10) dengan room untuk pengelompokan (bagian 11) dan buffer terbatas untuk backpressure supaya satu klien lambat tidak bisa menenggelamkan server (bagian 12). Di produksi Anda mengautentikasi di handshake dan mengotorisasi setiap pesan (bagian 13), mengamankan dengan wss:// dan pemeriksaan origin (bagian 14), dan, bagian yang sulit, menskalakan koneksi ber-state lintas instance dengan backplane pub/sub (bagian 15), di belakang load balancer sadar WebSocket yang menguras saat deploy (bagian 16), dengan klien yang terhubung ulang dengan backoff dan re-sync (bagian 17). Yang terpenting: pilih transport paling ringan yang cocok (bagian 18), hindari jebakan bocor resource (bagian 19), dan perlakukan disconnect sebagai kasus normal (bagian 20).
Berpijak pada MDN (developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) & RFC 6455 / gorilla/websocket & dokumentasi websockets/FastAPI / contoh Go 1.22+ / Python 3.11+.