Web Socket

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: CLOSE

Setelah 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:

AspekHTTP PollingServer-Sent EventsWebSocket
ArahClient → ServerServer → Client sajaDua arah (full-duplex)
KoneksiDibuka-tutup tiap requestPersisten, satu arahPersisten, dua arah
OverheadTinggi (header HTTP tiap request)RendahMinimal
Cocok untukData yang jarang berubahNotifikasi, feed beritaChat, game, kolaborasi
Dukungan browserUniversalBaik (kecuali IE)Sangat baik
Proxy friendlySangat baikBaikKadang 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.
  • ConcurrentHashMap untuk manajemen sesi — simpan referensi sesi dalam ConcurrentHashMap untuk broadcast. Selalu bungkus operasi siarkan dengan runCatching — satu sesi yang error tidak boleh menghentikan pengiriman ke yang lain.
  • Aktifkan pingPeriod di Ktor — tanpa heartbeat, koneksi yang idle bisa terputus oleh firewall/proxy tanpa notifikasi. Set pingPeriod dan timeout yang 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_POLICY jika 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.

← Sebelumnya: Socket   Berikutnya: Web Server →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact