Guides: How to implement a chat with WebSockets
In this tutorial, you will learn how to make a Chat application using Ktor.We are going to use WebSockets for a real-time bidirectional communication.
To achieve this, we are going to use the Routing, WebSockets and Sessions features.
This is an advanced tutorial and it assumes you have some basic knowledge about Ktor,so you should follow the guide about making a Website first.
Table of contents:
- Setting up the project
- Understanding WebSockets
- WebSocket route
- Keeping a set of opened connections
- Propagating a message among all the connections
- Assigning names to users/connections
- Exercises
Setting up the project
The first step is to set up a project. You can follow the Quick Start guide,or use the following form to create one:
the pre-configured generator form))
Understanding WebSockets
WebSockets is a subprotocol of HTTP. It starts as a normal HTTP request with an upgrade request header,and the connection switches to be a bidirectional communication instead of a request response one.
The smallest unit of transmission that can be sent as part of the WebSocket protocol, is a Frame
. A WebSocket Frame defines a type, a length and a payload that might be binary or text.Internally those frames might be transparently sent in several TCP packets.
You can see Frames as WebSocket messages. Frames could be the following types: text, binary, close, ping and pong.
You will normally handle Text
and Binary
frames, and the other will be handled by Ktor in most of the cases(though you can use a raw mode where you can handle those extra frame types yourself).
In its page, you can read more about the WebSockets feature.
WebSocket route
This first step is to create a route for the WebSocket. In this case we are going to define the /chat
route,but initially, we are going to make that route to act as an “echo” WebSocket route, that will send you back the same text messages that you send to it.
webSocket
routes are intended to be long-lived. Since it is a suspend block and uses lightweight Kotlin coroutines,it is fine and you can handle (depending on the machine and the complexity) hundreds of thousands of connectionsat once, while keeping your code easy to read and to write.
routing {
webSocket("/chat") { // this: DefaultWebSocketSession
while (true) {
val frame = incoming.receive() // suspend
when (frame) {
is Frame.Text -> {
val text = frame.readText()
outgoing.send(Frame.Text(text)) // suspend
}
}
}
}
}
Keeping a set of opened connections
We can use a Set to keep a list of opened connections. We can use a plain try…finally
to keep track of them.Since Ktor is multithreaded by default we should use thread-safe collections or limit the body to a single thread with newSingleThreadContext.
routing {
val wsConnections = Collections.synchronizedSet(LinkedHashSet<DefaultWebSocketSession>())
webSocket("/chat") { // this: DefaultWebSocketSession
wsConnections += this
try {
while (true) {
val frame = incoming.receive()
// ...
}
} finally {
wsConnections -= this
}
}
}
Propagating a message among all the connections
Now that we have a set of connections, we can iterate over them and use the sessionto send the frames we need.Everytime a user sends a message, we are going to propagate to all the connected clients.
routing {
val wsConnections = Collections.synchronizedSet(LinkedHashSet<DefaultWebSocketSession>())
webSocket("/chat") { // this: DefaultWebSocketSession
wsConnections += this
try {
while (true) {
val frame = incoming.receive()
when (frame) {
is Frame.Text -> {
val text = frame.readText()
// Iterate over all the connections
for (conn in wsConnections) {
conn.outgoing.send(Frame.Text(text))
}
}
}
}
} finally {
wsConnections -= this
}
}
}
Assigning names to users/connections
We might want to associate some information, like a name to an oppened connection,we can create a object that includes the WebSocketSession and store it insteadlike this:
class ChatClient(val session: DefaultWebSocketSession) {
companion object { var lastId = AtomicInteger(0) }
val id = lastId.getAndIncrement()
val name = "user$id"
}
routing {
val clients = Collections.synchronizedSet(LinkedHashSet<ChatClient>())
webSocket("/chat") { // this: DefaultWebSocketSession
val client = ChatClient(this)
clients += client
try {
while (true) {
val frame = incoming.receive()
when (frame) {
is Frame.Text -> {
val text = frame.readText()
// Iterate over all the connections
val textToSend = "${client.name} said: $text"
for (other in clients.toList()) {
other.session.outgoing.send(Frame.Text(textToSend))
}
}
}
}
} finally {
clients -= client
}
}
}
Exercises
Creating a client
Create a JavaScript client connecting to this endpoint and serve it with ktor.
JSON
Use kotlinx.serialization to send and receive VOs