Skip to content
This repository was archived by the owner on May 17, 2023. It is now read-only.

Commit 5f15eeb

Browse files
Add "Creating a WebSocket Chat with Ktor" hands-on tutorial
1 parent 360f53a commit 5f15eeb

11 files changed

+345
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Creating a WebSocket Chat with Ktor
2+
3+
Learn how to create a simple Chat application using Ktor including both a JVM server and a JVM client.
4+
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Introduction
2+
3+
In this hands-on, we will learn how to create a simple chat application which uses WebSockets. We will develop both the client and server application using [Ktor](https://ktor.io/) – an asynchronous Kotlin framework for creating web applications.
4+
5+
## What we will build
6+
7+
Throughout this tutorial, we will implement a simple chat service, which will consist of two applications:
8+
9+
- The **chat server application** will accept and manage connections from our chat users, receive messages, and distribute them to all connected clients.
10+
- The **chat client application** will allow users to join a common chat server, send messages to other users, and see messages from other users in the terminal.
11+
12+
![app_in_action](./assets/app_in_action.gif)
13+
14+
For both parts of the application, we will make use of Ktor's support for [WebSockets](https://ktor.io/docs/servers-features-websockets.html). Because Ktor is both a server-side and client-side framework, we will be able to reuse the knowledge we acquire building the chat server when it comes to building the client.
15+
16+
After completing this hands-on, you should have a basic understanding of how to work with WebSockets using Ktor and Kotlin, how to exchange information between client and server, and get a basic idea of how to manage multiple connections at the same time.
17+
18+
## Why WebSockets?
19+
20+
WebSockets are a great fit 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. Chat sessions are also bidirectional – clients want to send chat messages, and see chat messages from others.
21+
22+
Unlike regular HTTP requests, WebSocket connections can be kept open for a long time and have an easy interface for exchanging data between client and server in the form of frames. We 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, we can even concentrate on text and binary frames, and leave the handling of other frames to the framework.
23+
24+
WebSockets are also a widely supported technology. All modern browsers can work with WebSockets out of the box, and frameworks to work with WebSockets exist in many programming languages and on many platforms.
25+
26+
Now that we have confidence in the technology we want to use for the implementation of our project, let’s start with the set up!
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Project setup
2+
3+
Because our application will be two independent parts (chat server and chat client) we structure our application as two separate Gradle projects. Since these two projects are completely independent, they could be created manually, via the online [Ktor Project Generator](https://start.ktor.io/#), or the [plugin for IntelliJ IDEA](https://plugins.jetbrains.com/plugin/10823-ktor).
4+
5+
To skip over these configuration steps, a starter template is available for this specific tutorial, which includes all configuration and required dependencies for our two projects already.
6+
7+
[**Please clone the repository from GitHub, and open it in IntelliJ IDEA.**](https://github.com/kotlin-hands-on/chat-app-websockets)
8+
9+
The template repository contains two barebones Gradle projects for us to build our project: the `client` and `server` projects. Both of them are already preconfigured with the dependencies that we will need throughout the hands-on, so you **don't need to make any changes to the Gradle configuration.**
10+
11+
It might still be beneficial to understand what artifacts are being used for the application, so let's have a closer look at the two projects and the dependencies they rely on.
12+
13+
### Understanding the project configuration
14+
15+
Our two projects both come with their individual sets of configuration files. Let's examine each one of them a bit closer.
16+
17+
#### Dependencies for the `server` project
18+
19+
The server application specifies three dependencies in its `server/build.gradle.kts` file:
20+
21+
```kotlin
22+
dependencies {
23+
implementation("io.ktor:ktor-server-netty:$ktor_version")
24+
implementation("io.ktor:ktor-websockets:$ktor_version")
25+
implementation("ch.qos.logback:logback-classic:$logback_version")
26+
}
27+
```
28+
29+
- `ktor-server-netty` adds Ktor together with the Netty engine, allowing us to use server functionality without having to rely on an external application container.
30+
- `ktor-websockets` allows us to use the [WebSocket Ktor feature](https://ktor.io/docs/servers-features-websockets.html), the main communication mechanism for our chat.
31+
- `logback-classic` provides an implementation of [SLF4J](http://www.slf4j.org/), allowing us to see nicely formatted logs in our console.
32+
33+
#### Configuration for the `server` project
34+
35+
Ktor uses a [HOCON](https://github.com/lightbend/config/blob/master/HOCON.md) configuration file to set up its basic behavior, like its entry point and deployment port. It can be found at `server/src/main/resources/application.conf`:
36+
37+
```kotlin
38+
ktor {
39+
deployment {
40+
port = 8080
41+
}
42+
application {
43+
modules = [ com.jetbrains.handson.chat.ApplicationKt.module ]
44+
}
45+
}
46+
```
47+
48+
Alongside this file is also a barebones `logback.xml` file, which sets up the `logback-classic` implementation.
49+
50+
#### Dependencies for the `client` project
51+
52+
The client application specifies two dependencies in its `client/build.gradle.kts` file:
53+
54+
```kotlin
55+
dependencies {
56+
implementation("io.ktor:ktor-client-websockets:$ktor_version")
57+
implementation("io.ktor:ktor-client-cio:$ktor_version")
58+
}
59+
```
60+
61+
- `ktor-client-cio` provides a [client implementation of Ktor](https://ktor.io/clients/http-client/engines.html#cio) on top of coroutines ("Coroutine-based I/O").
62+
- `ktor-client-websockets` is the counterpart to the `ktor-websockets` dependency on the server, and allows us to consume [WebSockets from the client](https://ktor.io/docs/clients-websockets.html) with the same API as the server.
63+
64+
Now that we have some understanding of the parts that will make our project run, it's time to start building our project! Let's start by implementing a simple WebSocket echo server!
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# A first echo server
2+
3+
### Implementing an echo server
4+
5+
Let’s start our server development journey by building a small “echo” service which accepts WebSocket connections, receives text content, and sends it back to the client. We can implement this service with Kotlin and Ktor by adding the following implementation for `Application.module()` to `server/src/main/kotlin/com/jetbrains/handson/chat/server/Application.kt`:
6+
7+
```kotlin
8+
fun Application.module() {
9+
install(WebSockets)
10+
routing {
11+
webSocket("/chat") {
12+
send("You are connected!")
13+
for(frame in incoming) {
14+
frame as? Frame.Text ?: continue
15+
val receivedText = frame.readText()
16+
send("You said: $receivedText")
17+
}
18+
}
19+
}
20+
}
21+
```
22+
23+
We first enable WebSocket-related functionality provided by the Ktor framework by installing the `WebSockets` Ktor feature. This allows us to define endpoints in our routing which respond to the WebSocket protocol (in our case, the route is `/chat`). Within the scope of the `webSocket` route function, we can use various methods for interacting with our clients (via the `DefaultWebSocketServerSession` receiver type). This includes convenience methods to send messages and iterate over received messages.
24+
25+
Because we are only interested in text content, we skip any non-text `Frame`s we receive when iterating over the incoming channel. We can then read any received text, and send it right back to the user with the prefix `"You said:"`.
26+
27+
At this point, we have already built a fully-functioning echo server – a little service that just sends back whatever we send it. Let's try it out!
28+
29+
### Trying out the echo server
30+
31+
For now, we can use a web-based WebSocket client to connect to our echo service, send a message, and receive the echoed reply. Once we have finished implementing the server-side functionality, we will also build our own chat client in Kotlin.
32+
33+
Let's start the server by pressing the play button in the gutter next to the definition of fun main in our server's Application.kt. After our project has finished compiling, we should see a confirmation that the server is running in IntelliJ IDEAs "Run" window: `Application - Responding at http://0.0.0.0:8080`. To try out the service, we can open the WebSocket client provided at https://www.websocket.org/echo.html and use it to connect to `ws://localhost:8080/chat`.
34+
35+
![image-20201022122125926](./assets/image-20201022122125926.png)
36+
37+
Then, we can enter any kind of message in the "Message" window, and send it to our local server. If everything has gone according to plan, we should see the `SENT` and `RECEIVED` messages and in the logs, indicating that our echo-server is functioning just as intended.
38+
39+
With this, we now have a solid foundation for bidirectional communication through WebSockets. Next, let's expand our program more closely resemble a chat server, allowing multiple participants to share messages with others.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Exchanging messages
2+
3+
Let’s turn our echo server into a real chat server! To do this, we need to make sure messages from the same user are all tagged with the same username. Also, we want to make sure that messages are actually broadcast – sent to all other connected users.
4+
5+
### Modeling connections
6+
7+
Both of these features need us to be able to keep track of the connections our server is holding – to know which user is sending the messages, and to know who to broadcast them to.
8+
9+
Ktor manages a WebSocket connection with an object of the type `DefaultWebSocketSession`, which contains everything required for communicating via WebSockets, including the `incoming` and `outgoing` channels, convenience methods for communication, and more. For now, we can simplify the problem of assigning user names, and just give each participant an auto-generated user name based on a counter. Add the following implementation to a new file in `server/src/main/kotlin/com/jetbrains/handson/chat/server/` called `Connection.kt`:
10+
11+
```kotlin
12+
import io.ktor.http.cio.websocket.*
13+
import java.util.concurrent.atomic.*
14+
15+
class Connection(val session: DefaultWebSocketSession) {
16+
companion object {
17+
var lastId = AtomicInteger(0)
18+
}
19+
val name = "user${lastId.getAndIncrement()}"
20+
}
21+
```
22+
23+
Note that we are using `AtomicInteger` 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 two Connection objects are created simultaneously on separate threads.
24+
25+
### Implementing connection handling and message propagation
26+
27+
We can now adjust our server's program to keep track of our Connection objects, and send messages to all connected clients, prefixed with the correct user name. Adjust the implementation of the `routing` block in `server/src/main/kotlin/com/jetbrains/handson/chat/server/Application.kt` to the following code:
28+
29+
```kotlin
30+
routing {
31+
val connections = Collections.synchronizedSet<Connection?>(LinkedHashSet())
32+
webSocket("/chat") {
33+
println("Adding user!")
34+
val thisConnection = Connection(this)
35+
connections += thisConnection
36+
try {
37+
send("You are connected! There are ${connections.count()} users here.")
38+
for (frame in incoming) {
39+
frame as? Frame.Text ?: continue
40+
val receivedText = frame.readText()
41+
val textWithUsername = "[${thisConnection.name}]: $receivedText"
42+
connections.forEach {
43+
it.session.send(textWithUsername)
44+
}
45+
}
46+
} catch (e: Exception) {
47+
println(e.localizedMessage)
48+
} finally {
49+
println("Removing $thisConnection!")
50+
connections -= thisConnection
51+
}
52+
}
53+
}
54+
```
55+
56+
Our server now stores a (thread-safe) collection of `Connection`s. When a user connects, we create their `Connection` object (which also assigns itself a unique username), and add it to the collection. We then greet our user and let them know how many users are currently connecting. When we receive a message from the user, we prefix it with the unique name associated with their `Connection` object, and send it to all currently active connections. Finally, we remove the client's `Connection` object from our collection when the connection is terminated – either gracefully, when the incoming channel gets closed, or with an `Exception` when the network connection between client and server gets interrupted unexpectedly.
57+
58+
To see that our server is now behaving correctly – assigning user names and broadcasting them to everybody connected – we can once again run our application using the play button in the gutter and use the browser-based WebSocket client on https://www.websocket.org/echo.html to connect to `ws://localhost:8080/chat`. This time, we can use two separate browser tabs to validate that messages are exchanged properly.
59+
60+
![image-20201111155400473](./assets/image-20201111155400473.png)
61+
62+
As we can see, our finished chat server can now receive and send messages with multiple participants. Feel free to open a few more browser windows and play around with what we have built here!
63+
64+
In the next chapter, we will write a Kotlin chat client for our server, which will allow us to send and receive messages directly from the command line. Because our clients will also be implemented using Ktor, we will get to reuse much of what we learned about managing WebSockets in Kotlin.

0 commit comments

Comments
 (0)