Web Socket #
WebSocket adalah protokol komunikasi yang memungkinkan koneksi persisten dan dua arah antara browser (atau klien lain) dengan server, melalui satu koneksi TCP. Berbeda dari HTTP yang bersifat request-response — klien meminta, server menjawab, selesai — WebSocket membiarkan koneksi tetap terbuka sehingga server bisa kapan saja mendorong data ke klien tanpa perlu diminta terlebih dahulu. Ini adalah fondasi dari fitur real-time seperti chat, notifikasi langsung, dashboard live, dan kolaborasi dokumen. Kotlin memiliki dukungan WebSocket yang sangat baik melalui Ktor, Spring Boot, dan Vert.x. Artikel ini membahas cara WebSocket bekerja, implementasi lengkap dengan Ktor sebagai fokus utama, manajemen sesi, pola broadcast, dan kapan sebaiknya menggunakan WebSocket dibanding alternatif lain.
Cara Kerja WebSocket #
WebSocket dimulai sebagai koneksi HTTP biasa, lalu di-upgrade ke protokol WebSocket melalui handshake:
sequenceDiagram
participant K as Klien (Browser)
participant S as Server
K->>S: HTTP GET /ws\nUpgrade: websocket\nConnection: Upgrade
S->>K: HTTP 101 Switching Protocols\nUpgrade: websocket
Note over K,S: Koneksi TCP tetap terbuka — full duplex
K->>S: Frame: "Halo server!"
S->>K: Frame: "Halo klien!"
S->>K: Frame: "Notifikasi: ada pesan baru"
K->>S: Frame: PING
S->>K: Frame: PONG
K->>S: Frame: CLOSE
S->>K: Frame: CLOSESetelah handshake, komunikasi terjadi dalam frame bukan HTTP request. Baik klien maupun server bisa mengirim frame kapan saja — inilah yang membuat WebSocket full-duplex.
WebSocket vs HTTP Biasa vs Server-Sent Events #
Sebelum memilih WebSocket, pahami kapan ia tepat digunakan:
| Aspek | HTTP Polling | Server-Sent Events | WebSocket |
|---|---|---|---|
| Arah | Client → Server | Server → Client saja | Dua arah (full-duplex) |
| Koneksi | Dibuka-tutup tiap request | Persisten, satu arah | Persisten, dua arah |
| Overhead | Tinggi (header HTTP tiap request) | Rendah | Minimal |
| Cocok untuk | Data yang jarang berubah | Notifikasi, feed berita | Chat, game, kolaborasi |
| Dukungan browser | Universal | Baik (kecuali IE) | Sangat baik |
| Proxy friendly | Sangat baik | Baik | Kadang bermasalah |
GUNAKAN WebSocket jika:
✓ Butuh komunikasi dua arah real-time (chat, kolaborasi)
✓ Server perlu push data tanpa diminta klien
✓ Latensi sangat penting (gaming, trading)
✓ Banyak update kecil yang sering
JANGAN pakai WebSocket jika:
✗ Hanya butuh server push satu arah → gunakan SSE
✗ Data berubah jarang → HTTP polling sudah cukup
✗ Interaksi stateless biasa → HTTP/REST lebih sederhana
WebSocket dengan Ktor #
Ktor adalah framework Kotlin-native yang paling idiomatis untuk WebSocket. Ia menggunakan coroutine secara native sehingga menangani ribuan koneksi simultan dengan efisien.
Setup Dependensi #
// build.gradle.kts
dependencies {
implementation("io.ktor:ktor-server-core:2.3.9")
implementation("io.ktor:ktor-server-netty:2.3.9")
implementation("io.ktor:ktor-server-websockets:2.3.9")
implementation("io.ktor:ktor-server-content-negotiation:2.3.9")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.9")
implementation("ch.qos.logback:logback-classic:1.5.3")
}
Server WebSocket Dasar #
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*
import io.ktor.websocket.*
import java.time.Duration
fun main() {
embeddedServer(Netty, port = 8080) {
install(WebSockets) {
pingPeriod = Duration.ofSeconds(15) // kirim PING tiap 15 detik
timeout = Duration.ofSeconds(15) // tutup koneksi jika tidak ada respons 15 detik
maxFrameSize = Long.MAX_VALUE
masking = false
}
routing {
webSocket("/ws") {
println("Klien terhubung: ${call.request.local.remoteAddress}")
// Kirim pesan sambutan
send("Selamat datang di server WebSocket!")
// Terima dan proses frame dari klien
try {
for (frame in incoming) {
when (frame) {
is Frame.Text -> {
val pesan = frame.readText()
println("Diterima: $pesan")
// Echo balik ke klien
send("Echo: $pesan")
}
is Frame.Binary -> {
val bytes = frame.readBytes()
println("Binary frame: ${bytes.size} byte")
}
is Frame.Ping -> {
// Ktor menangani PONG secara otomatis
}
is Frame.Close -> {
val alasan = frame.readReason()
println("Klien menutup koneksi: ${alasan?.message}")
break
}
else -> {}
}
}
} catch (e: Exception) {
println("Koneksi terputus: ${e.message}")
} finally {
println("Sesi ditutup")
}
}
}
}.start(wait = true)
}
Server Chat Broadcast — Kelola Banyak Koneksi #
Use case paling umum WebSocket adalah chat atau notifikasi yang perlu dikirim ke semua klien terhubung. Kuncinya adalah menyimpan referensi ke semua sesi yang aktif secara thread-safe.
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*
import io.ktor.websocket.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.time.Duration
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
// Model pesan chat
data class PesanChat(
val pengirim: String,
val isi: String,
val waktu: String = java.time.LocalTime.now().toString()
)
// Manajemen sesi terpusat
class ManagerSesi {
private val sesiAktif = ConcurrentHashMap<String, DefaultWebSocketSession>()
private val mutex = Mutex()
private val hitungId = AtomicInteger(0)
fun buatId(): String = "pengguna-${hitungId.incrementAndGet()}"
suspend fun tambah(id: String, sesi: DefaultWebSocketSession) {
mutex.withLock { sesiAktif[id] = sesi }
println("[$id] Bergabung. Total sesi: ${sesiAktif.size}")
}
suspend fun hapus(id: String) {
mutex.withLock { sesiAktif.remove(id) }
println("[$id] Keluar. Total sesi: ${sesiAktif.size}")
}
suspend fun siarkan(pesan: String, kecuali: String? = null) {
val sesiSaatIni = mutex.withLock { sesiAktif.toMap() }
sesiSaatIni.forEach { (id, sesi) ->
if (id != kecuali) {
runCatching { sesi.send(Frame.Text(pesan)) }
.onFailure { println("Gagal kirim ke $id: ${it.message}") }
}
}
}
suspend fun kirimKe(id: String, pesan: String) {
sesiAktif[id]?.send(Frame.Text(pesan))
}
fun daftarPengguna(): List<String> = sesiAktif.keys.toList()
}
fun main() {
val manager = ManagerSesi()
embeddedServer(Netty, port = 8080) {
install(WebSockets) {
pingPeriod = Duration.ofSeconds(30)
timeout = Duration.ofSeconds(30)
}
routing {
webSocket("/chat") {
val idSesi = manager.buatId()
manager.tambah(idSesi, this)
// Beritahu semua pengguna lain
manager.siarkan("""{"tipe":"sistem","pesan":"$idSesi bergabung"}""", kecuali = idSesi)
send("""{"tipe":"sistem","pesan":"Kamu terhubung sebagai $idSesi"}""")
send("""{"tipe":"sistem","pesan":"Pengguna aktif: ${manager.daftarPengguna().joinToString(", ")}"}""")
try {
for (frame in incoming) {
if (frame !is Frame.Text) continue
val teks = frame.readText()
println("[$idSesi] $teks")
// Format pesan untuk dikirim ke semua
val pesanJson = """{"tipe":"pesan","pengirim":"$idSesi","isi":"${teks.replace("\"", "\\\"")}","waktu":"${java.time.LocalTime.now()}"}"""
// Siarkan ke semua termasuk pengirim
manager.siarkan(pesanJson)
}
} catch (e: Exception) {
println("[$idSesi] Error: ${e.message}")
} finally {
manager.hapus(idSesi)
manager.siarkan("""{"tipe":"sistem","pesan":"$idSesi meninggalkan chat"}""")
}
}
// Endpoint untuk melihat statistik server
webSocket("/admin") {
send("""{"pengguna_aktif": ${manager.daftarPengguna().size}, "daftar": ${manager.daftarPengguna()}}""")
close(CloseReason(CloseReason.Codes.NORMAL, "Info terkirim"))
}
}
}.start(wait = true)
}
Klien WebSocket dari Kotlin #
Ktor juga menyediakan klien WebSocket untuk berkomunikasi dengan server WebSocket dari kode Kotlin (berguna untuk testing atau microservice):
// build.gradle.kts — tambahkan
// implementation("io.ktor:ktor-client-core:2.3.9")
// implementation("io.ktor:ktor-client-cio:2.3.9")
// implementation("io.ktor:ktor-client-websockets:2.3.9")
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.websocket.*
import io.ktor.websocket.*
import kotlinx.coroutines.*
suspend fun main() {
val client = HttpClient(CIO) {
install(WebSockets)
}
client.webSocket(host = "localhost", port = 8080, path = "/chat") {
println("Terhubung ke server!")
// Kirim pesan di coroutine terpisah
val pekerjaan = launch {
repeat(5) { i ->
send("Pesan dari klien #$i")
delay(1000)
}
send("Selesai!")
close(CloseReason(CloseReason.Codes.NORMAL, "Klien selesai"))
}
// Terima pesan dari server
try {
for (frame in incoming) {
if (frame is Frame.Text) {
println("Server: ${frame.readText()}")
}
}
} catch (e: Exception) {
println("Koneksi ditutup: ${e.message}")
}
pekerjaan.join()
}
client.close()
}
Heartbeat dan Reconnect #
Koneksi WebSocket bisa terputus secara diam-diam karena firewall, proxy, atau jaringan yang tidak stabil. Penting mengimplementasikan mekanisme heartbeat:
// Di sisi server — Ktor menangani ini otomatis dengan pingPeriod
install(WebSockets) {
pingPeriod = Duration.ofSeconds(30) // server kirim PING tiap 30 detik
timeout = Duration.ofSeconds(30) // tutup jika tidak ada PONG dalam 30 detik
}
// Di sisi klien browser (JavaScript) — contoh referensi
// const ws = new WebSocket('ws://localhost:8080/ws')
// ws.onclose = () => setTimeout(() => reconnect(), 3000) // reconnect otomatis
Untuk klien Kotlin yang perlu reconnect otomatis:
suspend fun koneksiDenganReconnect(
url: String,
onPesan: suspend (String) -> Unit,
maksPercobaan: Int = 5
) {
val client = HttpClient(CIO) { install(WebSockets) }
var percobaan = 0
while (percobaan < maksPercobaan) {
runCatching {
client.webSocket(url) {
percobaan = 0 // reset counter saat berhasil konek
println("Terhubung ke $url")
for (frame in incoming) {
if (frame is Frame.Text) {
onPesan(frame.readText())
}
}
}
}.onFailure { e ->
percobaan++
val jeda = (percobaan * 2000L).coerceAtMost(30_000L)
println("Koneksi gagal ($percobaan/$maksPercobaan): ${e.message}. Coba lagi dalam ${jeda}ms...")
delay(jeda)
}
}
client.close()
println("Menyerah setelah $maksPercobaan percobaan")
}
Autentikasi WebSocket #
WebSocket tidak punya mekanisme autentikasi bawaan — kamu perlu menambahkannya sendiri. Ada dua pendekatan umum:
routing {
// Pendekatan 1: Token di query parameter (sederhana tapi token terlihat di URL)
webSocket("/ws") {
val token = call.request.queryParameters["token"]
?: run {
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Token wajib"))
return@webSocket
}
val pengguna = validasiToken(token)
?: run {
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Token tidak valid"))
return@webSocket
}
println("${pengguna.nama} terhubung")
// Lanjutkan...
}
// Pendekatan 2: Token di pesan pertama setelah koneksi (lebih aman)
webSocket("/ws/secure") {
// Minta autentikasi di pesan pertama
send("""{"tipe":"auth","pesan":"Kirim token autentikasi"}""")
val framePertama = incoming.receive()
if (framePertama !is Frame.Text) {
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Frame tidak valid"))
return@webSocket
}
val token = framePertama.readText()
val pengguna = validasiToken(token)
?: run {
send("""{"tipe":"error","pesan":"Token tidak valid"}""")
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Autentikasi gagal"))
return@webSocket
}
send("""{"tipe":"auth_ok","pesan":"Selamat datang, ${pengguna.nama}!"}""")
// Lanjutkan sesi yang sudah terautentikasi...
}
}
WebSocket dengan Spring Boot #
Spring Boot menyediakan dukungan WebSocket enterprise-grade dengan STOMP (Simple Text Oriented Messaging Protocol):
// build.gradle.kts
// implementation("org.springframework.boot:spring-boot-starter-websocket")
import org.springframework.context.annotation.Configuration
import org.springframework.messaging.handler.annotation.MessageMapping
import org.springframework.messaging.handler.annotation.SendTo
import org.springframework.stereotype.Controller
import org.springframework.web.socket.config.annotation.*
// Konfigurasi WebSocket + STOMP
@Configuration
@EnableWebSocketMessageBroker
class KonfigurasiWebSocket : WebSocketMessageBrokerConfigurer {
override fun configureMessageBroker(config: MessageBrokerRegistry) {
config.enableSimpleBroker("/topic", "/queue") // prefix untuk subscribe
config.setApplicationDestinationPrefixes("/app") // prefix untuk send
}
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS() // fallback ke polling jika WebSocket tidak tersedia
}
}
// Model pesan
data class PesanMasuk(val isi: String)
data class PesanKeluar(val pengirim: String, val isi: String)
// Controller pesan
@Controller
class ChatController {
@MessageMapping("/chat.kirim") // klien kirim ke /app/chat.kirim
@SendTo("/topic/pesan") // server broadcast ke /topic/pesan
fun tanganiPesan(pesan: PesanMasuk): PesanKeluar {
return PesanKeluar("Server", "Diterima: ${pesan.isi}")
}
}
Spring Boot + STOMP cocok ketika kamu sudah menggunakan Spring dan butuh fitur enterprise seperti autentikasi terintegrasi dengan Spring Security, message broker eksternal (RabbitMQ/ActiveMQ), dan manajemen subscription yang kompleks.
WebSocket dengan Vert.x #
Vert.x menggunakan model event loop reaktif yang sangat efisien untuk koneksi simultan dalam jumlah sangat besar:
// build.gradle.kts
// implementation("io.vertx:vertx-web:4.5.1")
import io.vertx.core.AbstractVerticle
import io.vertx.core.Vertx
import io.vertx.ext.web.Router
import io.vertx.ext.web.handler.StaticHandler
class WebSocketVerticle : AbstractVerticle() {
override fun start() {
val router = Router.router(vertx)
// Route statis untuk UI
router.route("/static/*").handler(StaticHandler.create())
// Route WebSocket
val server = vertx.createHttpServer()
server.webSocketHandler { ws ->
println("Klien terhubung: ${ws.remoteAddress()}")
ws.textMessageHandler { pesan ->
println("Diterima: $pesan")
ws.writeTextMessage("Echo: $pesan")
}
ws.closeHandler {
println("Klien ${ws.remoteAddress()} terputus")
}
ws.exceptionHandler { e ->
println("Error: ${e.message}")
}
}
server.requestHandler(router)
.listen(8080) { result ->
if (result.succeeded()) println("Vert.x WebSocket server di port 8080")
else println("Gagal: ${result.cause().message}")
}
}
}
fun main() {
Vertx.vertx().deployVerticle(WebSocketVerticle())
}
Ringkasan #
- WebSocket untuk komunikasi dua arah real-time — gunakan WebSocket ketika server perlu push data ke klien tanpa diminta, dan klien juga perlu kirim data ke server secara aktif (chat, game, kolaborasi).
- SSE untuk server push satu arah — jika hanya butuh notifikasi dari server ke klien (feed berita, progress bar), Server-Sent Events lebih sederhana dan lebih proxy-friendly dari WebSocket.
- Ktor adalah pilihan utama untuk Kotlin — native coroutine, idiomatis, ringan. Cocok untuk semua skala dari prototype hingga produksi.
ConcurrentHashMapuntuk manajemen sesi — simpan referensi sesi dalamConcurrentHashMapuntuk broadcast. Selalu bungkus operasi siarkan denganrunCatching— satu sesi yang error tidak boleh menghentikan pengiriman ke yang lain.- Aktifkan
pingPerioddi Ktor — tanpa heartbeat, koneksi yang idle bisa terputus oleh firewall/proxy tanpa notifikasi. SetpingPerioddantimeoutyang wajar (30 detik adalah umum).- Autentikasi wajib — WebSocket tidak punya autentikasi bawaan. Validasi di query parameter atau di pesan pertama setelah koneksi. Tutup segera dengan
CloseReason.VIOLATED_POLICYjika tidak valid.- Spring Boot + STOMP untuk lingkungan enterprise — jika sudah di ekosistem Spring dan butuh autentikasi terintegrasi, message broker, dan subscription management, STOMP di atas WebSocket adalah pilihan yang matang.
- Implementasikan reconnect di klien — koneksi WebSocket bisa terputus karena jaringan. Klien yang baik selalu mencoba reconnect dengan exponential backoff.