Table of Contents
- Prerequisites
- Understanding WebSockets & Ktor
- Setting Up the Project
- Implementing the WebSocket Server
- Building the WebSocket Client
- Testing the Application
- Enhancements & Next Steps
- Troubleshooting Common Issues
- 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
- Open IntelliJ IDEA and create a new Kotlin/JVM project with Gradle.
- Name the project
kotlin-websocket-chat-server. - Update the
build.gradle.ktsfile 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
- Create a new Kotlin/JVM project named
kotlin-websocket-chat-client. - Update
build.gradle.ktswith 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
/chatendpoint. - 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
- In IntelliJ, run the
mainfunction inMain.kt(server project). You should see:[main] INFO Application - Responding at http://0.0.0.0:8080
Step 2: Run Multiple Clients
- Run the
mainfunction inClient.kt(client project) twice to simulate two users. - In the first client, type a message (e.g., “Hello from Client 1!”) and press Enter.
- The second client will display:
Other user: Hello from Client 1! You: - 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:
- User Authentication: Add usernames and validate clients (e.g., JWT tokens).
- Message Persistence: Store messages in a database (e.g., SQLite, PostgreSQL) for history.
- GUI Interface: Use JavaFX (desktop) or Compose Multiplatform (mobile/desktop) for a graphical client.
- JSON Messages: Send structured data (e.g.,
{"user": "Alice", "message": "Hi"}) instead of plain text (use Kotlinx Serialization). - Error Handling: Add retries for failed connections or invalid messages.
- 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
connectionslist logic and ensure theMutexis properly protecting access. - Client Freezes: Ensure coroutines are used for blocking operations (e.g.,
launch(Dispatchers.IO)for I/O tasks).
References
- Ktor WebSocket Documentation
- WebSocket Protocol Specification (RFC 6455)
- Ktor Client WebSocket
- Kotlin Coroutines Guide
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! 🚀