Introduction: Why Do You Need Advanced WebSocket in Kotlin?
\nModern web applications increasingly require instant data transmission: chats, notifications, online games, financial quotes, and dashboards. The basic WebSocket protocol allows establishing a persistent bidirectional connection between a client and a server, but in practice, developing a reliable and scalable real-time application in Kotlin requires much more than just opening a socket.
\nIn this article, we will go beyond a simple echo server. We will look at architectural patterns for handling thousands of connections, error handling using Kotlin coroutines, server clustering with message brokers, and security issues. The material is intended for developers already familiar with the basics of Kotlin and Ktor.
\n\n1. Architecture of a High-Load WebSocket Server on Ktor
\nFor a production solution, simply attaching a webSocket handler is not enough. It is necessary to think through session management, the connection lifecycle, and handler isolation. Let's look at the key elements.
Session Management and Grouping
\nEach WebSocket connection in Ktor is represented by a DefaultWebSocketSession object. To send messages to a specific user or group (e.g., a "chat room"), you need to store active sessions. Use ConcurrentHashMap for thread safety.
import io.ktor.websocket.*\nimport java.util.concurrent.ConcurrentHashMap\n\nclass SessionManager {\n private val sessions = ConcurrentHashMap<String, WebSocketSession>()\n private val roomSessions = ConcurrentHashMap<String, MutableSet<String>>()\n\n fun addUserToRoom(roomId: String, userId: String, session: WebSocketSession) {\n sessions[userId] = session\n roomSessions.computeIfAbsent(roomId) { mutableSetOf() }.add(userId)\n }\n\n suspend fun broadcastToRoom(roomId: String, message: String) {\n roomSessions[roomId]?.forEach { userId ->\n sessions[userId]?.send(Frame.Text(message))\n }\n }\n\n fun removeUser(userId: String) {\n sessions.remove(userId)\n roomSessions.forEach { (_, users) -> users.remove(userId) }\n }\n}\n\nLifecycle Handling with Coroutines
\nKtor uses coroutines to handle each connection. It is important to correctly handle exceptions and socket closure to avoid memory leaks. Use try-catch inside the webSocket block and call removeUser in the finally block.
install(WebSockets) {\n pingPeriod = Duration.ofSeconds(15)\n timeout = Duration.ofSeconds(30)\n maxFrameSize = Long.MAX_VALUE\n masking = false\n}\n\nembeddedServer(Netty, port = 8080) {\n val sessionManager = SessionManager()\n routing {\n webSocket("/chat/{roomId}") {\n val roomId = call.parameters["roomId"] ?: "general"\n val userId = call.request.queryParameters["userId"] ?: "anon"\n sessionManager.addUserToRoom(roomId, userId, this)\n try {\n for (frame in incoming) {\n if (frame is Frame.Text) {\n val text = frame.readText()\n sessionManager.broadcastToRoom(roomId, "[$userId]: $text")\n }\n }\n } catch (e: Exception) {\n log.error("WebSocket error for user $userId: ${e.localizedMessage}")\n } finally {\n sessionManager.removeUser(userId)\n log.info("User $userId disconnected from room $roomId")\n }\n }\n }\n}.start(wait = true)\n\n2. Scaling and Clustering WebSocket in Kotlin
\nA single server cannot handle tens of thousands of simultaneous connections. The solution is horizontal scaling. However, a WebSocket connection is "p