Skip to main content

Kotlin WebSockets

Introduction

WebSockets provide a powerful protocol for creating interactive, real-time applications by establishing a persistent connection between a client and server. Unlike traditional HTTP requests which are stateless and disconnected, WebSockets maintain an open connection that allows for seamless bi-directional communication.

In this tutorial, we'll explore how to implement WebSockets in Kotlin backend applications. You'll learn how to create WebSocket servers, handle client connections, send and receive messages in real-time, and build practical real-world applications.

What are WebSockets?

WebSockets are a communication protocol that provides full-duplex communication channels over a single TCP connection. Simply put, they allow data to flow between client and server continuously without having to repeatedly establish new connections.

Key benefits of WebSockets include:

  • Real-time data: Updates occur immediately rather than requiring polling
  • Reduced overhead: Less HTTP header data is transmitted after the initial handshake
  • Bi-directional: Both server and client can initiate data transmission
  • Persistent connection: No need to reestablish connections for each data exchange

Setting Up WebSockets in Kotlin

There are several frameworks that support WebSockets in Kotlin. We'll focus on two popular options: Ktor and Spring Boot.

WebSockets with Ktor

Ktor is a lightweight framework built by JetBrains specifically for Kotlin. It has excellent WebSocket support built in.

Setting Up a Ktor Project

First, let's add the necessary dependencies to your build.gradle.kts file:

kotlin
dependencies {
implementation("io.ktor:ktor-server-core:2.3.5")
implementation("io.ktor:ktor-server-netty:2.3.5")
implementation("io.ktor:ktor-server-websockets:2.3.5")
implementation("ch.qos.logback:logback-classic:1.4.11")
}

Creating a Basic WebSocket Server

Now, let's create a simple WebSocket echo server:

kotlin
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)
timeout = Duration.ofSeconds(15)
maxFrameSize = Long.MAX_VALUE
masking = false
}

routing {
webSocket("/chat") {
send(Frame.Text("You are connected!"))

for (frame in incoming) {
frame as? Frame.Text ?: continue
val receivedText = frame.readText()
send(Frame.Text("Echo: $receivedText"))
println("Received: $receivedText")
}
}
}
}.start(wait = true)
}

In this example:

  1. We install the WebSockets feature with configuration options
  2. We create a route at /chat that handles WebSocket connections
  3. When a client connects, we send them a welcome message
  4. For each message received, we echo it back with "Echo: " prefixed

Connecting Multiple Clients

Let's enhance our example to support a chat room where multiple clients can communicate:

kotlin
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
import java.util.concurrent.ConcurrentHashMap

fun main() {
// Track all active connections
val connections = ConcurrentHashMap<String, DefaultWebSocketSession>()

embeddedServer(Netty, port = 8080) {
install(WebSockets) {
pingPeriod = Duration.ofSeconds(15)
timeout = Duration.ofSeconds(15)
maxFrameSize = Long.MAX_VALUE
masking = false
}

routing {
webSocket("/chat") {
val userId = generateUserId()
connections[userId] = this

try {
send(Frame.Text("You are connected as User-$userId"))

// Notify others about new user
broadcast("User-$userId joined the chat!", userId, connections)

for (frame in incoming) {
frame as? Frame.Text ?: continue
val receivedText = frame.readText()

// Broadcast message to all clients
broadcast("User-$userId: $receivedText", userId, connections)
}
} catch (e: Exception) {
println("Error: ${e.localizedMessage}")
} finally {
// Clean up when client disconnects
connections.remove(userId)
broadcast("User-$userId left the chat", userId, connections)
}
}
}
}.start(wait = true)
}

fun generateUserId(): String = System.currentTimeMillis().toString().takeLast(6)

suspend fun broadcast(message: String, sourceUserId: String, connections: ConcurrentHashMap<String, DefaultWebSocketSession>) {
connections.forEach { (userId, session) ->
session.send(Frame.Text(message))
}
}

This expanded example:

  1. Maintains a list of all active connections in a thread-safe map
  2. Assigns each user a unique ID
  3. Broadcasts messages to all connected clients
  4. Handles join and leave events
  5. Performs cleanup when clients disconnect

WebSockets with Spring Boot

Spring Boot is another popular framework for backend development in Kotlin. Here's how to implement WebSockets with Spring:

Setting Up a Spring Boot Project

Add these dependencies to your build.gradle.kts:

kotlin
dependencies {
implementation("org.springframework.boot:spring-boot-starter-websocket:3.1.4")
implementation("org.jetbrains.kotlin:kotlin-reflect")
}

Creating a WebSocket Handler

kotlin
import org.springframework.stereotype.Component
import org.springframework.web.socket.CloseStatus
import org.springframework.web.socket.TextMessage
import org.springframework.web.socket.WebSocketSession
import org.springframework.web.socket.handler.TextWebSocketHandler
import java.util.concurrent.CopyOnWriteArrayList

@Component
class ChatWebSocketHandler : TextWebSocketHandler() {
private val sessions = CopyOnWriteArrayList<WebSocketSession>()

override fun afterConnectionEstablished(session: WebSocketSession) {
sessions.add(session)
val userId = session.id.takeLast(6)
session.sendMessage(TextMessage("You are connected as User-$userId"))
broadcastMessage("User-$userId has joined the chat!", session)
}

override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
val userId = session.id.takeLast(6)
broadcastMessage("User-$userId: ${message.payload}", session)
}

override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
val userId = session.id.takeLast(6)
sessions.remove(session)
broadcastMessage("User-$userId has left the chat", session)
}

private fun broadcastMessage(message: String, sourceSession: WebSocketSession) {
for (session in sessions) {
session.sendMessage(TextMessage(message))
}
}
}

Configure WebSocket Endpoints

kotlin
import org.springframework.context.annotation.Configuration
import org.springframework.web.socket.config.annotation.EnableWebSocket
import org.springframework.web.socket.config.annotation.WebSocketConfigurer
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry

@Configuration
@EnableWebSocket
class WebSocketConfig(private val chatWebSocketHandler: ChatWebSocketHandler) : WebSocketConfigurer {
override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
registry.addHandler(chatWebSocketHandler, "/chat").setAllowedOrigins("*")
}
}

Spring Boot Application Class

kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class WebSocketApplication

fun main(args: Array<String>) {
runApplication<WebSocketApplication>(*args)
}

Real-World Example: A Live Stock Ticker

Let's build something practical: a real-time stock price updater. This example simulates stock price changes and broadcasts them to all connected clients.

kotlin
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.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.Duration
import java.util.concurrent.ConcurrentHashMap
import kotlin.random.Random

@Serializable
data class StockUpdate(val symbol: String, val price: Double, val change: Double)

fun main() {
val connections = ConcurrentHashMap<String, DefaultWebSocketSession>()

// Sample stock data
val stocks = mutableMapOf(
"AAPL" to 150.0,
"GOOGL" to 2800.0,
"MSFT" to 340.0,
"AMZN" to 3400.0,
"TSLA" to 900.0
)

embeddedServer(Netty, port = 8080) {
install(WebSockets) {
pingPeriod = Duration.ofSeconds(15)
timeout = Duration.ofSeconds(15)
}

// Start a background job to update stock prices
val stockUpdateJob = GlobalScope.launch {
while (isActive) {
if (connections.isNotEmpty()) {
updateStockPrices(stocks)
broadcastStockUpdates(stocks, connections)
}
delay(2000) // Update every 2 seconds
}
}

routing {
webSocket("/stocks") {
val sessionId = generateSessionId()
connections[sessionId] = this

try {
// Send initial stock data
val initialUpdates = stocks.map { (symbol, price) ->
StockUpdate(symbol, price, 0.0)
}
send(Frame.Text(Json.encodeToString(initialUpdates)))

// Keep the connection open
for (frame in incoming) {
// Handle client messages if needed
when (frame) {
is Frame.Text -> {
val text = frame.readText()
println("Received from client: $text")
}
else -> {}
}
}
} catch (e: Exception) {
println("Error: ${e.localizedMessage}")
} finally {
connections.remove(sessionId)
}
}
}
}.start(wait = true)
}

fun generateSessionId(): String = System.currentTimeMillis().toString().takeLast(6)

fun updateStockPrices(stocks: MutableMap<String, Double>) {
stocks.keys.forEach { symbol ->
val currentPrice = stocks[symbol] ?: return@forEach
val changePercent = Random.nextDouble(-0.01, 0.01)
val newPrice = currentPrice * (1 + changePercent)
stocks[symbol] = "%.2f".format(newPrice).toDouble()
}
}

suspend fun broadcastStockUpdates(stocks: Map<String, Double>, connections: ConcurrentHashMap<String, DefaultWebSocketSession>) {
val updates = stocks.map { (symbol, price) ->
val previousPrice = stocks[symbol] ?: price
val change = price - previousPrice
StockUpdate(symbol, price, change)
}

val jsonUpdates = Json.encodeToString(updates)
connections.forEach { (_, session) ->
session.send(Frame.Text(jsonUpdates))
}
}

To use this stock ticker on the client side (browser), here's a simple HTML/JavaScript example:

html
<!DOCTYPE html>
<html>
<head>
<title>Live Stock Ticker</title>
<style>
.up { color: green; }
.down { color: red; }
table { border-collapse: collapse; width: 400px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<h1>Live Stock Ticker</h1>
<table id="stockTable">
<thead>
<tr>
<th>Symbol</th>
<th>Price</th>
<th>Change</th>
</tr>
</thead>
<tbody id="stockData"></tbody>
</table>

<script>
const stockData = {};

const socket = new WebSocket('ws://localhost:8080/stocks');

socket.onopen = function(e) {
console.log('Connection established');
};

socket.onmessage = function(event) {
const updates = JSON.parse(event.data);
updates.forEach(update => {
stockData[update.symbol] = {
price: update.price.toFixed(2),
change: update.change.toFixed(2)
};
});
updateTable();
};

socket.onclose = function(event) {
console.log('Connection closed', event);
};

socket.onerror = function(error) {
console.error('WebSocket error:', error);
};

function updateTable() {
const tableBody = document.getElementById('stockData');
tableBody.innerHTML = '';

Object.keys(stockData).forEach(symbol => {
const row = document.createElement('tr');

const symbolCell = document.createElement('td');
symbolCell.textContent = symbol;

const priceCell = document.createElement('td');
priceCell.textContent = '$' + stockData[symbol].price;

const changeCell = document.createElement('td');
const change = parseFloat(stockData[symbol].change);
changeCell.textContent = change > 0 ? '+' + stockData[symbol].change : stockData[symbol].change;
changeCell.className = change > 0 ? 'up' : change < 0 ? 'down' : '';

row.appendChild(symbolCell);
row.appendChild(priceCell);
row.appendChild(changeCell);

tableBody.appendChild(row);
});
}
</script>
</body>
</html>

WebSocket Security Considerations

When implementing WebSockets in production applications, consider these security practices:

  1. Authentication: Validate users before establishing WebSocket connections
  2. Rate Limiting: Prevent abuse by limiting connection frequency and message rates
  3. Message Validation: Always validate incoming WebSocket messages
  4. HTTPS/WSS: Use secure WebSocket protocol (WSS) in production
  5. CORS: Configure proper cross-origin resource sharing settings

Here's a simple authentication example with Ktor:

kotlin
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.sessions.*
import io.ktor.util.*
import java.util.*

data class UserSession(val id: String, val username: String)

fun Application.module() {
install(Sessions) {
cookie<UserSession>("USER_SESSION") {
cookie.path = "/"
cookie.maxAgeInSeconds = 3600
}
}

routing {
post("/login") {
val parameters = call.receiveParameters()
val username = parameters["username"] ?: return@post call.respondText(
"Missing username",
status = HttpStatusCode.BadRequest
)

// In a real app, validate username/password against database

// Create a new session
val newSession = UserSession(UUID.randomUUID().toString(), username)
call.sessions.set(newSession)
call.respondText("Logged in successfully")
}

webSocket("/chat") {
// Get the session, disconnect if not authenticated
val session = call.sessions.get<UserSession>()
if (session == null) {
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Not authenticated"))
return@webSocket
}

// Proceed with authenticated WebSocket connection
try {
for (frame in incoming) {
when (frame) {
is Frame.Text -> {
val text = frame.readText()
// Handle the message from authenticated user
send(Frame.Text("${session.username}: $text"))
}
else -> {}
}
}
} finally {
// Clean up on disconnect
}
}
}
}

Handling WebSocket Disconnections

One of the challenges with WebSockets is handling disconnections gracefully. Networks can be unreliable, and you need strategies to handle reconnection:

kotlin
// Client-side JavaScript reconnection example
function connectWebSocket() {
const socket = new WebSocket('ws://localhost:8080/chat');

socket.onopen = function() {
console.log('Connected!');
// Reset reconnection attempts when successful
reconnectAttempts = 0;
};

socket.onclose = function(event) {
console.log('Connection lost. Attempting to reconnect...');
// Implement exponential backoff for reconnection
setTimeout(function() {
reconnectAttempts++;
connectWebSocket();
}, Math.min(1000 * Math.pow(2, reconnectAttempts), 30000)); // Cap at 30 seconds
};

socket.onerror = function(error) {
console.error('WebSocket error:', error);
};

// Store the socket for later use
return socket;
}

let reconnectAttempts = 0;
let activeSocket = connectWebSocket();

Summary

WebSockets provide a powerful way to implement real-time features in your Kotlin backend applications. Through this tutorial, you've learned:

  • The basics of WebSockets and their advantages over traditional HTTP
  • How to implement WebSocket servers using both Ktor and Spring Boot
  • Techniques for managing multiple WebSocket connections
  • Building a practical real-time stock ticker application
  • Security considerations and best practices
  • Handling disconnections and reconnection strategies

With these fundamentals, you can now implement real-time chat applications, live dashboards, collaborative tools, gaming applications, and much more.

Additional Resources

Exercises

  1. Extend the chat application to show a list of active users
  2. Implement private messaging between two users
  3. Add message persistence so new users can see previous messages
  4. Create a collaborative drawing app where multiple users can draw on the same canvas
  5. Build a real-time multiplayer game (like tic-tac-toe) using WebSockets

By mastering WebSockets in Kotlin, you've added a powerful tool to your backend development skills that enables a whole new category of interactive applications!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)