Socket

Socket #

Socket adalah antarmuka tingkat rendah untuk komunikasi jaringan — ia memberi kamu kendali penuh atas bagaimana data dikirim dan diterima antar proses, baik di mesin yang sama maupun antar mesin yang berbeda. Sebelum HTTP, gRPC, atau WebSocket ada, semua komunikasi jaringan dibangun di atas socket. Memahami socket memberimu fondasi untuk memahami bagaimana protokol-protokol tingkat tinggi itu bekerja di bawahnya. Kotlin menggunakan API socket dari Java yang sudah matang dan teruji, tapi bisa dikombinasikan dengan coroutine untuk model concurrency yang jauh lebih bersih. Artikel ini membahas socket TCP dan UDP, server multi-klien, protokol kustom, keamanan dengan TLS, dan pola-pola yang digunakan di produksi.

TCP vs UDP — Memilih Protokol yang Tepat #

Socket hadir dalam dua “rasa” utama yang mencerminkan dua filosofi berbeda dalam pengiriman data:

flowchart LR
    A[Pengirim] -- "TCP\nKoneksi handshake\nUrutan terjamin\nPengiriman ulang otomatis\nLebih lambat" --> B[Penerima]
    C[Pengirim] -- "UDP\nNo connection\nUrutan tidak dijamin\nTidak ada retry\nLebih cepat" --> D[Penerima]
AspekTCPUDP
KoneksiPerlu handshake (connect)Langsung kirim
Urutan dataTerjaminTidak dijamin
Pengiriman ulangOtomatis jika paket hilangTidak ada
OverheadLebih besarMinimal
Cocok untukHTTP, database, file transferStreaming video, DNS, game real-time

TCP Socket — Server Dasar #

ServerSocket menunggu koneksi masuk di port tertentu. Setiap accept() memblokir sampai ada klien yang terhubung.

import java.net.ServerSocket
import java.net.Socket
import java.io.IOException

fun main() {
    val port = 9999
    ServerSocket(port).use { serverSocket ->
        println("Server berjalan di port $port")
        println("Menunggu koneksi...")

        // Loop tak terbatas — terima klien satu per satu (single-threaded)
        while (true) {
            val clientSocket = serverSocket.accept()
            val alamatKlien = clientSocket.inetAddress.hostAddress
            println("Klien terhubung dari: $alamatKlien")

            // Tangani klien
            tanganiKlien(clientSocket)
        }
    }
}

fun tanganiKlien(socket: Socket) {
    socket.use { s ->
        val reader = s.getInputStream().bufferedReader()
        val writer = s.getOutputStream().bufferedWriter()

        // Baca pesan dari klien
        val pesan = reader.readLine()
        println("Diterima: $pesan")

        // Kirim respons
        writer.write("Server menerima: $pesan\n")
        writer.flush()
    }
    println("Koneksi ditutup")
}
Server di atas bersifat single-threaded — ia hanya bisa melayani satu klien sekaligus. Klien kedua harus menunggu klien pertama selesai. Untuk server production, kamu butuh multi-threading atau coroutine.

TCP Socket — Client #

import java.net.Socket
import java.net.ConnectException
import java.net.SocketTimeoutException

fun main() {
    val host = "localhost"
    val port = 9999

    try {
        Socket(host, port).use { socket ->
            // Set timeout — jangan biarkan socket menunggu selamanya
            socket.soTimeout = 5000  // 5 detik timeout untuk read

            val writer = socket.getOutputStream().bufferedWriter()
            val reader = socket.getInputStream().bufferedReader()

            // Kirim pesan
            val pesan = "Halo dari klien!"
            writer.write("$pesan\n")
            writer.flush()
            println("Terkirim: $pesan")

            // Baca respons
            val respons = reader.readLine()
            println("Respons server: $respons")
        }
    } catch (e: ConnectException) {
        println("Gagal terhubung ke $host:$port — apakah server berjalan?")
    } catch (e: SocketTimeoutException) {
        println("Timeout: server tidak merespons dalam 5 detik")
    } catch (e: IOException) {
        println("Error jaringan: ${e.message}")
    }
}

Server Multi-Klien dengan Thread #

Server yang nyata perlu menangani banyak klien secara bersamaan. Pendekatan klasik: satu thread per klien.

import java.net.ServerSocket
import java.net.Socket
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.thread

class ServerMultiKlien(private val port: Int) {
    private val jumlahKlienAktif = AtomicInteger(0)
    private val executor = Executors.newCachedThreadPool()

    fun mulai() {
        ServerSocket(port).use { serverSocket ->
            println("Server multi-klien berjalan di port $port")

            while (true) {
                val clientSocket = serverSocket.accept()
                val idKlien = jumlahKlienAktif.incrementAndGet()

                executor.submit {
                    tanganiKlien(clientSocket, idKlien)
                    jumlahKlienAktif.decrementAndGet()
                }
            }
        }
    }

    private fun tanganiKlien(socket: Socket, id: Int) {
        val alamat = socket.inetAddress.hostAddress
        println("[Klien #$id] Terhubung dari $alamat")

        socket.use { s ->
            val reader = s.getInputStream().bufferedReader()
            val writer = s.getOutputStream().bufferedWriter()

            // Loop sesi — satu klien bisa kirim banyak pesan
            try {
                while (true) {
                    val pesan = reader.readLine() ?: break  // null = koneksi ditutup
                    println("[Klien #$id] Pesan: $pesan")

                    if (pesan.lowercase() == "keluar") {
                        writer.write("Sampai jumpa!\n")
                        writer.flush()
                        break
                    }

                    val respons = prosesPerintah(pesan)
                    writer.write("$respons\n")
                    writer.flush()
                }
            } catch (e: IOException) {
                println("[Klien #$id] Koneksi terputus: ${e.message}")
            }
        }

        println("[Klien #$id] Sesi selesai. Klien aktif: ${jumlahKlienAktif.get()}")
    }

    private fun prosesPerintah(pesan: String): String {
        return when (pesan.uppercase()) {
            "PING"  -> "PONG"
            "WAKTU" -> "Waktu server: ${java.time.LocalTime.now()}"
            "INFO"  -> "Klien aktif: ${jumlahKlienAktif.get()}"
            else    -> "Echo: $pesan"
        }
    }
}

fun main() {
    ServerMultiKlien(9999).mulai()
}

Server Multi-Klien dengan Coroutine #

Pendekatan coroutine lebih efisien dari thread pool untuk server dengan banyak koneksi bersamaan — setiap koneksi hanya mengkonsumsi memori coroutine yang sangat kecil, bukan 1MB stack thread.

import kotlinx.coroutines.*
import java.net.ServerSocket
import java.net.Socket

fun main() = runBlocking {
    val port = 9999
    val serverSocket = ServerSocket(port)
    println("Coroutine server berjalan di port $port")

    // Dispatcher khusus untuk I/O blocking
    withContext(Dispatchers.IO) {
        while (isActive) {
            // accept() adalah operasi blocking — dijalankan di IO dispatcher
            val clientSocket = serverSocket.accept()

            // Setiap klien mendapat coroutine sendiri
            launch {
                tanganiKlienCoroutine(clientSocket)
            }
        }
    }
}

suspend fun tanganiKlienCoroutine(socket: Socket) {
    val alamat = socket.inetAddress.hostAddress
    println("Klien terhubung: $alamat")

    withContext(Dispatchers.IO) {
        socket.use { s ->
            val reader = s.getInputStream().bufferedReader()
            val writer = s.getOutputStream().bufferedWriter()

            try {
                var baris: String?
                while (reader.readLine().also { baris = it } != null) {
                    val pesan = baris ?: break
                    println("[$alamat] $pesan")

                    writer.write("Echo: $pesan\n")
                    writer.flush()

                    if (pesan == "BYE") break
                }
            } catch (e: Exception) {
                println("[$alamat] Error: ${e.message}")
            }
        }
    }

    println("[$alamat] Disconnected")
}

Protokol Teks Kustom #

Untuk aplikasi yang lebih kompleks, kamu perlu mendefinisikan protokol komunikasi sendiri — aturan bagaimana klien dan server bertukar pesan. Contoh: protokol chat sederhana.

// Protokol:
// MASUK:<nama>        → server membalas DITERIMA atau DITOLAK
// PESAN:<teks>        → server meneruskan ke semua klien lain
// DAFTAR              → server membalas daftar pengguna aktif
// KELUAR              → tutup koneksi

import java.net.ServerSocket
import java.net.Socket
import java.io.BufferedWriter
import java.util.concurrent.ConcurrentHashMap

object ServerChat {
    private val klienAktif = ConcurrentHashMap<String, BufferedWriter>()

    fun mulai(port: Int) {
        ServerSocket(port).use { server ->
            println("Server chat berjalan di port $port")

            while (true) {
                val socket = server.accept()
                kotlin.concurrent.thread(isDaemon = true) {
                    tanganiKlien(socket)
                }
            }
        }
    }

    private fun tanganiKlien(socket: Socket) {
        var namaPengguna: String? = null

        socket.use { s ->
            val reader = s.getInputStream().bufferedReader()
            val writer = s.getOutputStream().bufferedWriter()

            fun kirim(pesan: String) {
                writer.write("$pesan\n")
                writer.flush()
            }

            fun siarkan(pesan: String, kecuali: String? = null) {
                klienAktif.forEach { (nama, w) ->
                    if (nama != kecuali) {
                        runCatching { w.write("$pesan\n"); w.flush() }
                    }
                }
            }

            try {
                var baris: String?
                while (reader.readLine().also { baris = it } != null) {
                    val input = baris ?: break
                    val (perintah, argumen) = if (":" in input) {
                        input.substringBefore(":") to input.substringAfter(":")
                    } else {
                        input to ""
                    }

                    when (perintah.uppercase()) {
                        "MASUK" -> {
                            val nama = argumen.trim()
                            if (nama.isBlank() || klienAktif.containsKey(nama)) {
                                kirim("DITOLAK:Nama tidak valid atau sudah digunakan")
                            } else {
                                namaPengguna = nama
                                klienAktif[nama] = writer
                                kirim("DITERIMA:Selamat datang, $nama!")
                                siarkan("INFO:$nama bergabung ke chat", kecuali = nama)
                                println("[$nama] Bergabung")
                            }
                        }

                        "PESAN" -> {
                            val nama = namaPengguna ?: run { kirim("ERROR:Belum login"); return@while }
                            siarkan("PESAN:$nama: $argumen")
                            println("[$nama] $argumen")
                        }

                        "DAFTAR" -> {
                            val daftar = klienAktif.keys.joinToString(", ")
                            kirim("DAFTAR:$daftar")
                        }

                        "KELUAR" -> {
                            kirim("BYE:Sampai jumpa!")
                            break
                        }

                        else -> kirim("ERROR:Perintah tidak dikenal: $perintah")
                    }
                }
            } catch (e: Exception) {
                println("[${namaPengguna ?: "?"}] Error: ${e.message}")
            } finally {
                namaPengguna?.let { nama ->
                    klienAktif.remove(nama)
                    siarkan("INFO:$nama meninggalkan chat")
                    println("[$nama] Keluar")
                }
            }
        }
    }
}

fun main() {
    ServerChat.mulai(9999)
}

UDP Socket #

UDP (User Datagram Protocol) tidak memerlukan koneksi — paket data (datagram) dikirim langsung tanpa handshake. Cocok untuk aplikasi yang lebih toleran terhadap kehilangan paket tapi membutuhkan latensi rendah.

import java.net.DatagramSocket
import java.net.DatagramPacket
import java.net.InetAddress

// UDP Server
fun serverUdp(port: Int) {
    DatagramSocket(port).use { socket ->
        println("UDP Server berjalan di port $port")
        val buffer = ByteArray(1024)

        while (true) {
            val paket = DatagramPacket(buffer, buffer.size)
            socket.receive(paket)  // blokir sampai ada datagram masuk

            val pesan = String(paket.data, 0, paket.length)
            println("Diterima dari ${paket.address}:${paket.port}: $pesan")

            // Kirim respons balik ke pengirim
            val respons = "Pong: $pesan".toByteArray()
            val paketRespons = DatagramPacket(
                respons, respons.size,
                paket.address, paket.port
            )
            socket.send(paketRespons)
        }
    }
}

// UDP Client
fun clientUdp(host: String, port: Int) {
    DatagramSocket().use { socket ->
        socket.soTimeout = 3000  // 3 detik timeout

        val pesan = "Ping dari klien"
        val data = pesan.toByteArray()
        val alamatServer = InetAddress.getByName(host)

        // Kirim datagram
        val paketKirim = DatagramPacket(data, data.size, alamatServer, port)
        socket.send(paketKirim)
        println("Terkirim: $pesan")

        // Terima respons
        val buffer = ByteArray(1024)
        val paketTerima = DatagramPacket(buffer, buffer.size)
        socket.receive(paketTerima)

        val respons = String(paketTerima.data, 0, paketTerima.length)
        println("Respons: $respons")
    }
}

Socket dengan TLS/SSL #

Untuk komunikasi yang aman, gunakan SSLSocket yang mengenkripsi semua data yang dikirim.

import javax.net.ssl.SSLServerSocketFactory
import javax.net.ssl.SSLSocketFactory

// SSL Server — membutuhkan keystore dengan sertifikat
fun serverSsl(port: Int) {
    // Konfigurasi keystore (biasanya via System properties atau SSLContext)
    System.setProperty("javax.net.ssl.keyStore", "server.jks")
    System.setProperty("javax.net.ssl.keyStorePassword", "password")

    val factory = SSLServerSocketFactory.getDefault()
    factory.createServerSocket(port).use { server ->
        println("SSL Server berjalan di port $port")

        while (true) {
            val socket = server.accept()
            kotlin.concurrent.thread {
                socket.use { s ->
                    val reader = s.getInputStream().bufferedReader()
                    val writer = s.getOutputStream().bufferedWriter()

                    val pesan = reader.readLine()
                    println("SSL: diterima '$pesan'")
                    writer.write("SSL: echo '$pesan'\n")
                    writer.flush()
                }
            }
        }
    }
}

// SSL Client
fun clientSsl(host: String, port: Int) {
    System.setProperty("javax.net.ssl.trustStore", "client.jks")
    System.setProperty("javax.net.ssl.trustStorePassword", "password")

    val factory = SSLSocketFactory.getDefault()
    factory.createSocket(host, port).use { socket ->
        val writer = socket.getOutputStream().bufferedWriter()
        val reader = socket.getInputStream().bufferedReader()

        writer.write("Pesan rahasia\n")
        writer.flush()

        println(reader.readLine())
    }
}

Konfigurasi Socket Penting #

import java.net.Socket

fun konfigurasikanSocket(socket: Socket) {
    // Timeout untuk operasi baca — penting agar tidak hang selamanya
    socket.soTimeout = 30_000  // 30 detik

    // TCP_NODELAY — nonaktifkan algoritma Nagle untuk latensi rendah
    // Berguna untuk protokol request-response yang sering mengirim paket kecil
    socket.tcpNoDelay = true

    // Keep-alive — kirim probe secara berkala untuk deteksi koneksi yang mati
    // Penting untuk koneksi yang bisa idle lama
    socket.keepAlive = true

    // Buffer size — sesuaikan dengan kebutuhan throughput
    socket.receiveBufferSize = 64 * 1024  // 64KB receive buffer
    socket.sendBufferSize    = 64 * 1024  // 64KB send buffer

    // Linger — tunggu pengiriman data selesai sebelum close()
    socket.setSoLinger(true, 5)  // tunggu maksimal 5 detik
}

// ServerSocket options
fun konfigurasikanServerSocket(serverSocket: java.net.ServerSocket) {
    // REUSE_ADDRESS — izinkan bind ke port yang baru saja digunakan
    // Penting agar server bisa restart cepat tanpa "Address already in use"
    serverSocket.reuseAddress = true

    // Backlog — berapa banyak koneksi yang bisa diantri sebelum accept()
    // ServerSocket(port, backlog = 50)
}

Tips Produksi #

Beberapa hal yang harus diperhatikan saat menjalankan server socket di produksi:

// 1. Selalu set timeout — jangan biarkan koneksi hang selamanya
socket.soTimeout = 30_000

// 2. Gunakan use{} atau try-finally untuk memastikan socket selalu ditutup
socket.use { s ->
    // operasi...
}

// 3. Log informasi yang berguna untuk debugging
println("[${socket.inetAddress.hostAddress}:${socket.port}] Klien terhubung")

// 4. Batasi ukuran pesan yang diterima — cegah memory overflow dari klien jahat
val pesan = reader.readLine()?.take(4096)  // maks 4KB per baris

// 5. Gunakan AtomicInteger untuk counter klien yang thread-safe
val jumlahKlien = AtomicInteger(0)

// 6. Tangani shutdown server dengan graceful
Runtime.getRuntime().addShutdownHook(Thread {
    println("Server mati, tutup semua koneksi...")
    serverSocket.close()
})

Ringkasan #

  • TCP untuk keandalan, UDP untuk kecepatan — pilih TCP ketika urutan dan keandalan penting (HTTP, database, file transfer). Pilih UDP ketika latensi rendah lebih penting dari reliabilitas (video streaming, game, DNS).
  • use {} untuk semua socketSocket, ServerSocket, dan DatagramSocket semuanya Closeable. Selalu bungkus dengan use {} agar pasti ditutup meski terjadi exception.
  • Set soTimeout pada setiap socket — tanpa timeout, read() bisa memblokir selamanya jika klien tidak mengirim data. Ini membuat thread/coroutine tergantung tanpa bisa dibebaskan.
  • Server single-thread hanya untuk demo — di produksi, gunakan thread pool (Executors.newCachedThreadPool()) atau coroutine (launch {} dalam Dispatchers.IO) agar bisa melayani banyak klien bersamaan.
  • Coroutine lebih efisien dari thread per klien — satu coroutine mengkonsumsi jauh lebih sedikit memori dari satu thread OS. Untuk server dengan ribuan koneksi bersamaan, coroutine adalah pilihan yang tepat.
  • Definisikan protokol dengan jelas — untuk server TCP yang kompleks, tentukan format pesan, delimiter, dan command set sejak awal. Gunakan format PERINTAH:ARGUMEN\n atau biner dengan header panjang.
  • reuseAddress = true di ServerSocket — tanpa ini, restart server setelah crash bisa gagal dengan “Address already in use” karena port masih dalam status TIME_WAIT.
  • Gunakan TLS/SSL untuk keamanan — data yang dikirim via socket biasa bisa disadap. SSLSocket mengenkripsi semua data secara transparan tanpa mengubah logika aplikasi secara signifikan.

← Sebelumnya: I/O   Berikutnya: Web Socket →

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