Ktor 2.3.9 Help

Creating a WebSocket chat

In this tutorial, you will learn how to create a simple chat application that uses WebSockets. The solution consists of two parts:

  • The chat server application will accept and manage connections from the chat users, receive messages, and distribute them to all connected clients.

  • The chat client application will allow users to join a common chat server, send messages, and read user messages in the terminal. To follow this tutorial, see Creating a WebSocket chat client.

App in action

Because Ktor is both a server-side and a client-side framework, you will reuse the knowledge you acquire from this part of the tutorial onto the client-side implementation.

This tutorial will teach you how to:

  • Work with WebSockets using Ktor.

  • Exchange information between a client and a server.

  • Manage multiple WebSocket connections simultaneously.

Why WebSockets?

WebSockets are exceptionally well-suited for applications like chats or simple games. Chat sessions are usually long-lived, with the client receiving messages from other participants over a long period of time. These chat sessions operate bidirectionally, allowing clients to both send and receive chat messages.

Unlike standard HTTP requests, WebSocket connections can remain open for extended durations, providing a straightforward interface for data exchange between the client and server through frames. You can think of frames as WebSocket messages which come in different types (text, binary, close, ping/pong). Because Ktor provides high-level abstractions over the WebSocket protocol, you can focus on handling text and binary frames, while Ktor takes care of managing other frame types.

Furthermore, WebSockets are a widely supported technology. All modern browsers support WebSockets out of the box, and many programming languages and platforms have existing support.

Prerequisites

Before starting this tutorial:

Create a new Ktor project

To create a base project for the application using the Ktor plugin, open IntelliJ IDEA and follow the steps below:

  1. On the Welcome screen, click New Project.

    Otherwise, from the main menu, select File | New | Project.

  2. In the New Project wizard, choose Ktor from the list on the left. On the right pane, specify the following settings:

    New Ktor project
    • Name: Specify a project name.

    • Location: Specify a directory for your project.

    • Build System: Make sure that Gradle Kotlin is selected as a build system.

    • Website: Leave the default example.com value as a domain used to generate a package name.

    • Artifact: This field shows a generated artifact name.

    • Ktor version: Choose the latest Ktor version.

    • Engine: Leave the default Netty engine.

    • Configuration in: Choose HOCON file to specify server parameters in a dedicated configuration file.

    • Add sample code: Disable this option to skip adding sample code for plugins.

    Click Next.

  3. On the next page, add the Routing and WebSockets plugins:

    Ktor plugins

    Click Create and wait until IntelliJ IDEA generates a project and installs the dependencies.

Examine the project

To see the structure of the generated project, invoke the Project view:

Initial project structure

Dependencies

First, open the build.gradle.kts file and examine the added dependencies:

dependencies { implementation("io.ktor:ktor-server-core-jvm") implementation("io.ktor:ktor-server-websockets-jvm") implementation("io.ktor:ktor-server-netty-jvm") testImplementation("io.ktor:ktor-server-tests-jvm") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") }
  • ktor-server-core-jvm adds Ktor's core components to the project.

  • ktor-server-websockets-jvm allows you to use the WebSocket plugin, the main communication mechanism for the chat.

  • ktor-server-netty-jvm adds the Netty engine to the project, allowing you to use server functionality without having to rely on an external application container.

  • ktor-server-tests-jvm and kotlin-test-junit allow you to test parts of your Ktor application without having to use the whole HTTP stack in the process.

Configurations: application.conf and logback.xml

The generated project also includes the application.conf and logback.xml configuration files located in the resources folder:

  • application.conf is a configuration file in HOCON format. Ktor uses this file to determine the port on which it should run, and it also defines the entry point of the application.

    ktor { deployment { port = 8080 port = ${?PORT} } application { modules = [ com.example.ApplicationKt.module ] } }

    To learn more about how a Ktor server is configured, see the Configuration in a file help topic.

  • logback.xml sets up the basic logging structure for the server. To learn more about logging in Ktor, see the Logging topic.

Source code

The application.conf file configures the entry point of your application to be com.example.ApplicationKt.module. This corresponds to the Application.module() function in Application.kt, which is an application module:

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) fun Application.module() { configureRouting() configureSockets() }

This module, in turn, calls the following extension functions:

  • configureRouting is a function defined in plugins/Routing.kt, which currently doesn't do anything:

    fun Application.configureRouting() { routing { } }
  • configureSockets is a function defined in plugins/Sockets.kt, which installs and configures the WebSockets plugin:

    fun Application.configureSockets() { install(WebSockets) { pingPeriod = Duration.ofSeconds(15) timeout = Duration.ofSeconds(15) maxFrameSize = Long.MAX_VALUE masking = false } routing { } }

A first echo server

Implement an echo server

You will begin by building an “echo” service which accepts WebSocket connections, receives text content, and sends it back to the client. To implement this service with Ktor, add the following implementation for Application.configureSockets() to plugins/Sockets.kt:

import io.ktor.websocket.* import io.ktor.server.application.* import io.ktor.server.routing.* import io.ktor.server.websocket.* fun Application.configureSockets() { install(WebSockets) { // ... } routing { webSocket("/chat") { send("You are connected!") for(frame in incoming) { frame as? Frame.Text ?: continue val receivedText = frame.readText() send("You said: $receivedText") } } } }

First, Ktor installs the WebSockets plugin to the server to enable routing to endpoints responding to the WebSocket protocol (in this case, the route is /chat). Within the scope of the webSocket route function, Ktor provides access to various methods for interacting with the clients (through the DefaultWebSocketServerSession receiver type). This includes convenience methods to send messages and iterate over received messages.

When iterating over the incoming channel, the server checks if the received Frame is of type text and only then reads the text and sends it back to the user with the prefix "You said:".

With this, you have built a fully-functioning echo server.

Test the application

To test the application, use a web-based WebSocket client, such as Postman, to connect to the echo service, send a message, and receive the echoed reply.

To start the server, click on the gutter icon next to the main function in Application.kt. After the project has finished compiling, you should see a confirmation that the server is running in IntelliJ IDEA's Run tool window:

Application - Responding at http://0.0.0.0:8080

Using a web-based client, you can now connect to ws://localhost:8080/chat and make a WebSocket request.

Echo Test

Enter a message in the editor pane and send it to the local server. You will then see sent and received messages in the Messages pane, indicating that the echo-server is functioning as intended.

You now have a solid foundation for establishing bidirectional communication through WebSockets. In the following chapter, you will expand the application to allow multiple participants to send messages to one another.

Exchange messages

To enable message exchange among multiple users, you will ensure that each user's messages are labeled with their respective usernames. Additionally, you will make sure that messages are sent to all other connected users, effectively broadcasting them.

Model connections

Both of these features require to keep track of the connections the server is holding – to know which user is sending the messages, and to know who to broadcast them to.

Ktor handles WebSocket connections using a DefaultWebSocketSession object, which contains all the required components for WebSocket communication, such as the incoming and outgoing channels, convenient communication methods, and more. To simplify the task of assigning usernames, one solution would be to automatically generate usernames for participants based on a counter:

Create a new file in the com.example package called Connection.kt and add the following implementation to it:

package com.example import io.ktor.websocket.* import java.util.concurrent.atomic.* class Connection(val session: DefaultWebSocketSession) { companion object { val lastId = AtomicInteger(0) } val name = "user${lastId.getAndIncrement()}" }

Note that AtomicInteger is used as a thread-safe data structure for the counter. This ensures that two users will never receive the same ID for their username – even when their Connection objects are created simultaneously on separate threads.

Implement connection handling and message propagation

You can now adjust the server to keep track of the Connection objects, and send messages to all connected clients, prefixed with the correct username. Adjust the implementation of the routing block in plugins/Sockets.kt to the following:

import com.example.* import io.ktor.websocket.* import io.ktor.server.application.* import io.ktor.server.routing.* import io.ktor.server.websocket.* import java.time.* import java.util.* import kotlin.collections.LinkedHashSet fun Application.configureSockets() { routing { val connections = Collections.synchronizedSet<Connection?>(LinkedHashSet()) webSocket("/chat") { println("Adding user!") val thisConnection = Connection(this) connections += thisConnection try { send("You are connected! There are ${connections.count()} users here.") for (frame in incoming) { frame as? Frame.Text ?: continue val receivedText = frame.readText() val textWithUsername = "[${thisConnection.name}]: $receivedText" connections.forEach { it.session.send(textWithUsername) } } } catch (e: Exception) { println(e.localizedMessage) } finally { println("Removing $thisConnection!") connections -= thisConnection } } } }

The server now stores a (thread-safe) collection of type Connection. When a user connects, the server creates a Connection object, which also self-assigns a unique username, and adds it to the collection.

It then sends a message to the user indicating the number of currently connected users. Upon receiving a message from a user, the server appends a unique identifier prefix associated with the user's Connection object and broadcasts it to all active connections. When the connection is terminated, the client's Connection object is removed from the collection – either gracefully, when the incoming channel gets closed, or with an Exception when an unexpected network interruption occurs between the client and server.

To test the new functionality, run the application by clicking on the gutter icon next to the main function in Application.kt and use Postman to connect to ws://localhost:8080/chat. This time, use two or more separate tabs to validate that messages are exchanged properly.

Echo Test
Echo Test

The finished chat server can now receive and send messages from and to multiple participants.

For the full example of the application, see tutorial-websockets-server.

What's next

Congratulations on creating a chat application using Kotlin, Ktor and WebSockets.

In the next tutorial, you will create a chat client for the server, which will allow you to send and receive messages directly from the command line. Because the client will also be implemented with Ktor, you will reuse much of what you just learned about managing WebSockets.

Additionally, you can expand on the server-side functionality. Use the following ideas to improve the application:

  • Ask users to enter a username on application startup, and persist this name alongside the Connection information on the server.

  • Implement a /whisper command, to allow users to share a message to a certain person only or a select group of participants.

Last modified: 13 November 2023