Sockets #

A socket is a low-level interface for network communication — it gives you full control over how data is sent and received between processes, whether on the same machine or across different machines. Before HTTP, gRPC, or WebSocket existed, all network communication was built on top of sockets. Understanding sockets gives you the foundation to understand how those higher-level protocols work underneath. Kotlin uses Java’s mature, battle-tested socket API, but it can be combined with coroutines for a much cleaner concurrency model. This article covers TCP and UDP sockets, multi-client servers, custom protocols, TLS security, and patterns used in production.

TCP vs UDP — Choosing the Right Protocol #

Sockets come in two main “flavors” that reflect two different philosophies of data delivery:

flowchart LR
    A[Sender] -- "TCP\nConnection handshake\nGuaranteed ordering\nAutomatic retransmission\nSlower" --> B[Receiver]
    C[Sender] -- "UDP\nNo connection\nOrder not guaranteed\nNo retries\nFaster" --> D[Receiver]
AspectTCPUDP
ConnectionNeeds a handshake (connect)Send directly
Data orderingGuaranteedNot guaranteed
RetransmissionAutomatic if a packet is lostNone
OverheadLargerMinimal
Best forHTTP, databases, file transferVideo streaming, DNS, real-time games

TCP Socket — Basic Server #

ServerSocket waits for incoming connections on a specific port. Each accept() blocks until a client connects.

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

fun main() {
    val port = 9999
    ServerSocket(port).use { serverSocket ->
        println("Server running on port $port")
        println("Waiting for connections...")

        // Infinite loop — accept clients one at a time (single-threaded)
        while (true) {
            val clientSocket = serverSocket.accept()
            val clientAddress = clientSocket.inetAddress.hostAddress
            println("Client connected from: $clientAddress")

            // Handle the client
            handleClient(clientSocket)
        }
    }
}

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

        // Read a message from the client
        val message = reader.readLine()
        println("Received: $message")

        // Send a response
        writer.write("Server received: $message\n")
        writer.flush()
    }
    println("Connection closed")
}
The server above is single-threaded — it can only serve one client at a time. A second client must wait for the first to finish. For a production server, you need multi-threading or coroutines.

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 a timeout — don't let the socket wait forever
            socket.soTimeout = 5000  // 5 second read timeout

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

            // Send a message
            val message = "Hello from client!"
            writer.write("$message\n")
            writer.flush()
            println("Sent: $message")

            // Read the response
            val response = reader.readLine()
            println("Server response: $response")
        }
    } catch (e: ConnectException) {
        println("Failed to connect to $host:$port — is the server running?")
    } catch (e: SocketTimeoutException) {
        println("Timeout: the server didn't respond within 5 seconds")
    } catch (e: IOException) {
        println("Network error: ${e.message}")
    }
}

Multi-Client Server with Threads #

A real server needs to handle many clients simultaneously. The classic approach: one thread per client.

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

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

    fun start() {
        ServerSocket(port).use { serverSocket ->
            println("Multi-client server running on port $port")

            while (true) {
                val clientSocket = serverSocket.accept()
                val clientId = activeClientCount.incrementAndGet()

                executor.submit {
                    handleClient(clientSocket, clientId)
                    activeClientCount.decrementAndGet()
                }
            }
        }
    }

    private fun handleClient(socket: Socket, id: Int) {
        val address = socket.inetAddress.hostAddress
        println("[Client #$id] Connected from $address")

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

            // Session loop — one client can send many messages
            try {
                while (true) {
                    val message = reader.readLine() ?: break  // null = connection closed
                    println("[Client #$id] Message: $message")

                    if (message.lowercase() == "quit") {
                        writer.write("Goodbye!\n")
                        writer.flush()
                        break
                    }

                    val response = processCommand(message)
                    writer.write("$response\n")
                    writer.flush()
                }
            } catch (e: IOException) {
                println("[Client #$id] Connection lost: ${e.message}")
            }
        }

        println("[Client #$id] Session ended. Active clients: ${activeClientCount.get()}")
    }

    private fun processCommand(message: String): String {
        return when (message.uppercase()) {
            "PING"  -> "PONG"
            "TIME"  -> "Server time: ${java.time.LocalTime.now()}"
            "INFO"  -> "Active clients: ${activeClientCount.get()}"
            else    -> "Echo: $message"
        }
    }
}

fun main() {
    MultiClientServer(9999).start()
}

Multi-Client Server with Coroutines #

The coroutine approach is more efficient than a thread pool for servers with many concurrent connections — each connection only consumes very small coroutine memory, not a 1MB thread stack.

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

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

    // Dedicated dispatcher for blocking I/O
    withContext(Dispatchers.IO) {
        while (isActive) {
            // accept() is a blocking operation — runs on the IO dispatcher
            val clientSocket = serverSocket.accept()

            // Each client gets its own coroutine
            launch {
                handleClientCoroutine(clientSocket)
            }
        }
    }
}

suspend fun handleClientCoroutine(socket: Socket) {
    val address = socket.inetAddress.hostAddress
    println("Client connected: $address")

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

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

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

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

    println("[$address] Disconnected")
}

Custom Text Protocols #

For more complex applications, you need to define your own communication protocol — the rules for how clients and servers exchange messages. Example: a simple chat protocol.

// Protocol:
// LOGIN:<name>        → server replies ACCEPTED or REJECTED
// MESSAGE:<text>      → server forwards to all other clients
// LIST                → server replies with the active user list
// EXIT                → close the connection

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

object ChatServer {
    private val activeClients = ConcurrentHashMap<String, BufferedWriter>()

    fun start(port: Int) {
        ServerSocket(port).use { server ->
            println("Chat server running on port $port")

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

    private fun handleClient(socket: Socket) {
        var username: String? = null

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

            fun send(message: String) {
                writer.write("$message\n")
                writer.flush()
            }

            fun broadcast(message: String, except: String? = null) {
                activeClients.forEach { (name, w) ->
                    if (name != except) {
                        runCatching { w.write("$message\n"); w.flush() }
                    }
                }
            }

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

                    when (command.uppercase()) {
                        "LOGIN" -> {
                            val name = argument.trim()
                            if (name.isBlank() || activeClients.containsKey(name)) {
                                send("REJECTED:Name invalid or already in use")
                            } else {
                                username = name
                                activeClients[name] = writer
                                send("ACCEPTED:Welcome, $name!")
                                broadcast("INFO:$name joined the chat", except = name)
                                println("[$name] Joined")
                            }
                        }

                        "MESSAGE" -> {
                            val name = username ?: run { send("ERROR:Not logged in"); return@while }
                            broadcast("MESSAGE:$name: $argument")
                            println("[$name] $argument")
                        }

                        "LIST" -> {
                            val list = activeClients.keys.joinToString(", ")
                            send("LIST:$list")
                        }

                        "EXIT" -> {
                            send("BYE:Goodbye!")
                            break
                        }

                        else -> send("ERROR:Unknown command: $command")
                    }
                }
            } catch (e: Exception) {
                println("[${username ?: "?"}] Error: ${e.message}")
            } finally {
                username?.let { name ->
                    activeClients.remove(name)
                    broadcast("INFO:$name left the chat")
                    println("[$name] Exited")
                }
            }
        }
    }
}

fun main() {
    ChatServer.start(9999)
}

UDP Sockets #

UDP (User Datagram Protocol) doesn’t require a connection — data packets (datagrams) are sent directly without a handshake. Suitable for applications that tolerate packet loss but need low latency.

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

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

        while (true) {
            val packet = DatagramPacket(buffer, buffer.size)
            socket.receive(packet)  // blocks until a datagram arrives

            val message = String(packet.data, 0, packet.length)
            println("Received from ${packet.address}:${packet.port}: $message")

            // Send a response back to the sender
            val response = "Pong: $message".toByteArray()
            val responsePacket = DatagramPacket(
                response, response.size,
                packet.address, packet.port
            )
            socket.send(responsePacket)
        }
    }
}

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

        val message = "Ping from client"
        val data = message.toByteArray()
        val serverAddress = InetAddress.getByName(host)

        // Send the datagram
        val sendPacket = DatagramPacket(data, data.size, serverAddress, port)
        socket.send(sendPacket)
        println("Sent: $message")

        // Receive the response
        val buffer = ByteArray(1024)
        val receivePacket = DatagramPacket(buffer, buffer.size)
        socket.receive(receivePacket)

        val response = String(receivePacket.data, 0, receivePacket.length)
        println("Response: $response")
    }
}

Sockets with TLS/SSL #

For secure communication, use SSLSocket which encrypts all data sent.

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

// SSL Server — needs a keystore with a certificate
fun sslServer(port: Int) {
    // Keystore configuration (usually via System properties or an 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 running on 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 message = reader.readLine()
                    println("SSL: received '$message'")
                    writer.write("SSL: echo '$message'\n")
                    writer.flush()
                }
            }
        }
    }
}

// SSL Client
fun sslClient(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("Secret message\n")
        writer.flush()

        println(reader.readLine())
    }
}

Important Socket Configurations #

import java.net.Socket

fun configureSocket(socket: Socket) {
    // Timeout for read operations — important so it doesn't hang forever
    socket.soTimeout = 30_000  // 30 seconds

    // TCP_NODELAY — disable Nagle's algorithm for low latency
    // Useful for request-response protocols that often send small packets
    socket.tcpNoDelay = true

    // Keep-alive — periodically send probes to detect dead connections
    // Important for connections that may idle for a long time
    socket.keepAlive = true

    // Buffer sizes — adjust according to throughput needs
    socket.receiveBufferSize = 64 * 1024  // 64KB receive buffer
    socket.sendBufferSize    = 64 * 1024  // 64KB send buffer

    // Linger — wait for data transmission to finish before close()
    socket.setSoLinger(true, 5)  // wait at most 5 seconds
}

// ServerSocket options
fun configureServerSocket(serverSocket: java.net.ServerSocket) {
    // REUSE_ADDRESS — allow binding to a recently used port
    // Important so the server can restart fast without "Address already in use"
    serverSocket.reuseAddress = true

    // Backlog — how many connections can be queued before accept()
    // ServerSocket(port, backlog = 50)
}

Production Tips #

Some things to keep in mind when running a socket server in production:

// 1. Always set a timeout — don't let connections hang forever
socket.soTimeout = 30_000

// 2. Use use{} or try-finally to make sure the socket is always closed
socket.use { s ->
    // operations...
}

// 3. Log useful information for debugging
println("[${socket.inetAddress.hostAddress}:${socket.port}] Client connected")

// 4. Limit the size of received messages — prevent memory overflow from malicious clients
val message = reader.readLine()?.take(4096)  // max 4KB per line

// 5. Use an AtomicInteger for a thread-safe client counter
val clientCount = AtomicInteger(0)

// 6. Handle graceful server shutdown
Runtime.getRuntime().addShutdownHook(Thread {
    println("Server shutting down, closing all connections...")
    serverSocket.close()
})

Summary #

  • TCP for reliability, UDP for speed — choose TCP when ordering and reliability matter (HTTP, databases, file transfer). Choose UDP when low latency matters more than reliability (video streaming, games, DNS).
  • use {} for all socketsSocket, ServerSocket, and DatagramSocket are all Closeable. Always wrap them with use {} so they’re definitely closed even when an exception occurs.
  • Set soTimeout on every socket — without a timeout, read() can block forever if the client doesn’t send data. This leaves a thread/coroutine hanging with no way to free it.
  • Single-threaded servers are for demos only — in production, use a thread pool (Executors.newCachedThreadPool()) or coroutines (launch {} in Dispatchers.IO) to serve many clients concurrently.
  • Coroutines are more efficient than one thread per client — one coroutine consumes far less memory than one OS thread. For servers with thousands of concurrent connections, coroutines are the right choice.
  • Define the protocol clearly — for complex TCP servers, decide the message format, delimiter, and command set from the start. Use a COMMAND:ARGUMENT\n format or binary with a length header.
  • reuseAddress = true on ServerSocket — without this, restarting a server after a crash can fail with “Address already in use” because the port is still in TIME_WAIT status.
  • Use TLS/SSL for security — data sent via plain sockets can be intercepted. SSLSocket encrypts all data transparently without significantly changing application logic.

← Previous: I/O   Next: WebSocket →

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