cyberangles guide

Building a Simple Chat Application with Kotlin and WebSockets

In today’s interconnected world, real-time communication has become a cornerstone of modern applications—from instant messengers to collaborative tools. Traditional HTTP-based communication, which relies on short-lived request-response cycles, is ill-suited for real-time scenarios where low latency and continuous data flow are critical. This is where **WebSockets** shine: they enable full-duplex, persistent connections between a client and server, allowing bidirectional communication in real time. In this blog, we’ll build a simple chat application using **Kotlin** and WebSockets. We’ll use **Ktor**—a powerful, coroutine-based framework for building servers and clients in Kotlin—to implement both the WebSocket server and client. By the end, you’ll have a working chat app where multiple clients can connect and broadcast messages to each other.

Table of Contents

  1. Prerequisites
  2. Understanding WebSockets & Ktor
  3. Setting Up the Project
  4. Implementing the WebSocket Server
  5. Building the WebSocket Client
  6. Testing the Application
  7. Enhancements & Next Steps
  8. Troubleshooting Common Issues
  9. References

Prerequisites

Before diving in, ensure you have the following tools and knowledge:

  • Kotlin (1.8+ recommended): Familiarity with basic Kotlin syntax and coroutines.
  • JDK 17+: To run Kotlin applications.
  • IntelliJ IDEA (Community Edition or higher): For coding (optional but recommended).
  • Gradle or Maven: For dependency management (we’ll use Gradle).
  • Basic understanding of networking concepts (e.g., ports, HTTP).

Understanding WebSockets & Ktor

What Are WebSockets?

WebSockets are a communication protocol that provides full-duplex, persistent connections over a single TCP socket. Unlike HTTP, which is request-response-based, WebSockets allow the server to push data to the client at any time, enabling real-time features like chat, live updates, and multiplayer games.

Why Ktor?

Ktor is a lightweight, idiomatic Kotlin framework for building asynchronous servers and clients. It natively supports WebSockets via its WebSockets plugin, making it easy to handle real-time connections with coroutines (Kotlin’s async/await alternative). Ktor’s design aligns with Kotlin’s strengths, ensuring concise, readable code.

Setting Up the Project

We’ll split the project into two parts: a WebSocket server (to manage connections and broadcast messages) and a WebSocket client (to send/receive messages from the server). Let’s start with the server.

Step 1: Create the Server Project

  1. Open IntelliJ IDEA and create a new Kotlin/JVM project with Gradle.
  2. Name the project kotlin-websocket-chat-server.
  3. Update the build.gradle.kts file to include Ktor dependencies:
plugins {
    application
    kotlin("jvm") version "1.9.0"
    kotlin("plugin.serialization") version "1.9.0" // For JSON (optional later)
}

application {
    mainClass.set("MainKt")
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("io.ktor:ktor-server-core:2.3.3")
    implementation("io.ktor:ktor-server-websockets:2.3.3") // WebSocket support
    implementation("io.ktor:ktor-server-netty:2.3.3") // Netty server engine
    implementation("io.ktor:ktor-server-content-negotiation:2.3.3") // For JSON (optional)
    implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.3") // JSON serialization
    implementation("ch.qos.logback:logback-classic:1.4.8") // Logging
}

Sync the project to download dependencies.

Implementing the WebSocket Server

The server will:

  • Accept WebSocket connections from clients.
  • Manage a list of connected clients (sessions).
  • Broadcast messages from one client to all other connected clients.

Step 1: Configure the Server

Create a Main.kt file in src/main/kotlin with the following code:

import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.logging.*
import io.ktor.server.plugins.websockets.*
import io.ktor.server.routing.*
import io.ktor.websocket.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.time.Duration

fun main() {
    // Start the server on port 8080
    embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
        .start(wait = true)
}

fun Application.module() {
    // Install required plugins
    install(Logging) // For logging requests/connections
    install(WebSockets) {
        // Configure WebSocket timeout (optional)
        pingPeriod = Duration.ofSeconds(15)
        timeout = Duration.ofSeconds(30)
    }

    // Manage connected clients (sessions) thread-safely
    val connections = mutableListOf<DefaultWebSocketSession>()
    val mutex = Mutex() // To protect concurrent access to 'connections'

    // Define WebSocket route
    routing {
        // WebSocket endpoint: ws://localhost:8080/chat
        webSocket("/chat") {
            // Add the new client session to the list
            mutex.withLock { connections.add(this) }
            println("New client connected. Total clients: ${connections.size}")

            try {
                // Listen for incoming messages from this client
                for (frame in incoming) {
                    frame as? Frame.Text ?: continue // Ignore non-text frames
                    val message = frame.readText()
                    println("Received message: $message")

                    // Broadcast the message to all other clients
                    mutex.withLock {
                        connections.filter { it != this }.forEach { session ->
                            // Launch a coroutine to send the message without blocking
                            launch(Dispatchers.IO) {
                                session.send(Frame.Text(message))
                            }
                        }
                    }
                }
            } catch (e: Exception) {
                println("Error handling connection: ${e.localizedMessage}")
            } finally {
                // Remove the client session when disconnected
                mutex.withLock { connections.remove(this) }
                println("Client disconnected. Total clients: ${connections.size}")
            }
        }
    }
}

Key Server Components Explained:

  • embeddedServer: Starts a Netty server on port 8080.
  • install(WebSockets): Enables WebSocket support with optional ping/timeout settings (to detect dead connections).
  • connections: A list to track active client sessions (WebSocket connections).
  • Mutex: Ensures thread-safe access to connections (critical for coroutine-based apps).
  • webSocket(“/chat”): Defines the WebSocket endpoint. Clients connect to ws://localhost:8080/chat.
  • incoming: A flow of incoming frames (messages) from the client. We filter for text frames and broadcast them.
  • session.send(): Sends a message to a client session.

Building the WebSocket Client

Next, we’ll build a simple console-based client to connect to the server, send messages, and display incoming messages. We’ll use the Ktor client for WebSocket support.

Step 1: Create the Client Project

  1. Create a new Kotlin/JVM project named kotlin-websocket-chat-client.
  2. Update build.gradle.kts with Ktor client dependencies:
plugins {
    application
    kotlin("jvm") version "1.9.0"
}

application {
    mainClass.set("ClientKt")
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("io.ktor:ktor-client-core:2.3.3")
    implementation("io.ktor:ktor-client-websockets:2.3.3") // WebSocket client
    implementation("io.ktor:ktor-client-cio:2.3.3") // CIO client engine
}

Step 2: Implement the Client Logic

Create a Client.kt file in src/main/kotlin:

import io.ktor.client.*
import io.ktor.client.plugins.websocket.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.websocket.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.Scanner

fun main() = runBlocking {
    // Create a Ktor client with WebSocket support
    val client = HttpClient(CIO) {
        install(WebSockets)
    }

    try {
        // Connect to the server's WebSocket endpoint
        client.webSocketSession(Url("ws://localhost:8080/chat")).use { session ->
            println("Connected to chat! Type a message and press Enter to send (Ctrl+C to exit).")

            // Launch a coroutine to read user input and send messages
            val inputJob = launch {
                val scanner = Scanner(System.`in`)
                while (true) {
                    print("You: ")
                    val message = scanner.nextLine()
                    session.send(Frame.Text(message)) // Send message to server
                }
            }

            // Listen for incoming messages from the server
            try {
                for (frame in session.incoming) {
                    frame as? Frame.Text ?: continue
                    println("\nOther user: ${frame.readText()}")
                    print("You: ") // Reprompt user after receiving a message
                }
            } finally {
                inputJob.cancel() // Stop input coroutine when connection closes
                println("Disconnected from chat.")
            }
        }
    } catch (e: Exception) {
        println("Connection failed: ${e.localizedMessage}")
    } finally {
        client.close() // Close the client when done
    }
}

Key Client Components Explained:

  • HttpClient(CIO): Uses the CIO (Coroutine-based I/O) engine for non-blocking operations.
  • webSocketSession: Connects to the server’s /chat endpoint.
  • inputJob: A coroutine that reads user input from the console and sends it to the server via session.send().
  • incoming: A flow of frames from the server; incoming messages are printed to the console.

Testing the Application

Now, let’s test the chat app with multiple clients.

Step 1: Run the Server

  1. In IntelliJ, run the main function in Main.kt (server project). You should see:
    [main] INFO  Application - Responding at http://0.0.0.0:8080

Step 2: Run Multiple Clients

  1. Run the main function in Client.kt (client project) twice to simulate two users.
  2. In the first client, type a message (e.g., “Hello from Client 1!”) and press Enter.
  3. The second client will display:
    Other user: Hello from Client 1!
    You: 
  4. Type a message in the second client, and the first client will receive it.

Example Output:

Client 1:

Connected to chat! Type a message and press Enter (Ctrl+C to exit).
You: Hello from Client 1!

Other user: Hi there! This is Client 2.
You: 

Client 2:

Connected to chat! Type a message and press Enter (Ctrl+C to exit).
You: 

Other user: Hello from Client 1!
You: Hi there! This is Client 2.
You: 

Enhancements & Next Steps

The basic chat app works, but here are ideas to improve it:

  1. User Authentication: Add usernames and validate clients (e.g., JWT tokens).
  2. Message Persistence: Store messages in a database (e.g., SQLite, PostgreSQL) for history.
  3. GUI Interface: Use JavaFX (desktop) or Compose Multiplatform (mobile/desktop) for a graphical client.
  4. JSON Messages: Send structured data (e.g., {"user": "Alice", "message": "Hi"}) instead of plain text (use Kotlinx Serialization).
  5. Error Handling: Add retries for failed connections or invalid messages.
  6. Typing Indicators: Broadcast “User is typing…” statuses.

Troubleshooting Common Issues

  • Port 8080 in Use: Change the server port in embeddedServer(Netty, port = 8081, ...).
  • Connection Refused: Ensure the server is running and the client uses the correct URL (ws://localhost:8080/chat).
  • Messages Not Broadcasting: Check the connections list logic and ensure the Mutex is properly protecting access.
  • Client Freezes: Ensure coroutines are used for blocking operations (e.g., launch(Dispatchers.IO) for I/O tasks).

References

By following this guide, you’ve built a functional real-time chat app using Kotlin and WebSockets. The Ktor framework simplifies WebSocket handling, and Kotlin’s coroutines make asynchronous communication manageable. Experiment with the enhancements to take your chat app to the next level! 🚀