Introduction : Pourquoi un WebSocket avancé en Kotlin ?
\nLes applications web modernes exigent de plus en plus souvent une transmission instantanée des données : chats, notifications, jeux en ligne, cotations financières et tableaux de bord. Le protocole WebSocket de base permet d'établir une connexion bidirectionnelle permanente entre le client et le serveur, mais en pratique, le développement d'une application temps réel fiable et évolutive en Kotlin nécessite bien plus que la simple ouverture d'un socket.
\nDans cet article, nous dépasserons le cadre d'un simple serveur écho. Nous examinerons les patrons architecturaux pour gérer des milliers de connexions, la gestion des erreurs avec les coroutines Kotlin, le clustering de serveurs avec des courtiers de messages et les questions de sécurité. Ce contenu est destiné aux développeurs déjà familiers avec les bases de Kotlin et Ktor.
\n\n1. Architecture d'un serveur WebSocket à haute charge avec Ktor
\nPour une solution de production, il ne suffit pas d'ajouter un gestionnaire webSocket. Il est nécessaire de concevoir la gestion des sessions, le cycle de vie de la connexion et l'isolation des gestionnaires. Examinons les éléments clés.
Gestion des sessions et leur regroupement
\nChaque connexion WebSocket dans Ktor est représentée par un objet DefaultWebSocketSession. Pour envoyer des messages à un utilisateur spécifique ou à un groupe (par exemple, "salle de chat"), il faut stocker les sessions actives. Utilisez ConcurrentHashMap pour la sécurité des threads.
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\nGestion du cycle de vie avec les coroutines
\nKtor utilise les coroutines pour gérer chaque connexion. Il est important de traiter correctement les exceptions et la fermeture du socket pour éviter les fuites mémoire. Utilisez try-catch à l'intérieur du bloc webSocket et appelez removeUser dans le bloc finally.
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("Erreur WebSocket pour l'utilisateur $userId : ${e.localizedMessage}")\n } finally {\n sessionManager.removeUser(userId)\n log.info("Utilisateur $userId déconnecté de la salle $roomId")\n }\n }\n }\n}.start(wait = true)\n\n2. Mise à l'échelle et clustering WebSocket en Kotlin
\nUn seul serveur ne peut pas gérer des dizaines de milliers de connexions simultanées. La solution est la mise à l'échelle horizontale. Cependant, une connexion WebSocket est