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

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.

  1. routing {
  2. webSocket("/chat") { // this: DefaultWebSocketSession
  3. while (true) {
  4. val frame = incoming.receive() // suspend
  5. when (frame) {
  6. is Frame.Text -> {
  7. val text = frame.readText()
  8. outgoing.send(Frame.Text(text)) // suspend
  9. }
  10. }
  11. }
  12. }
  13. }

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.

  1. routing {
  2. val wsConnections = Collections.synchronizedSet(LinkedHashSet<DefaultWebSocketSession>())
  3. webSocket("/chat") { // this: DefaultWebSocketSession
  4. wsConnections += this
  5. try {
  6. while (true) {
  7. val frame = incoming.receive()
  8. // ...
  9. }
  10. } finally {
  11. wsConnections -= this
  12. }
  13. }
  14. }

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.

  1. routing {
  2. val wsConnections = Collections.synchronizedSet(LinkedHashSet<DefaultWebSocketSession>())
  3. webSocket("/chat") { // this: DefaultWebSocketSession
  4. wsConnections += this
  5. try {
  6. while (true) {
  7. val frame = incoming.receive()
  8. when (frame) {
  9. is Frame.Text -> {
  10. val text = frame.readText()
  11. // Iterate over all the connections
  12. for (conn in wsConnections) {
  13. conn.outgoing.send(Frame.Text(text))
  14. }
  15. }
  16. }
  17. }
  18. } finally {
  19. wsConnections -= this
  20. }
  21. }
  22. }

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:

  1. class ChatClient(val session: DefaultWebSocketSession) {
  2. companion object { var lastId = AtomicInteger(0) }
  3. val id = lastId.getAndIncrement()
  4. val name = "user$id"
  5. }
  6. routing {
  7. val clients = Collections.synchronizedSet(LinkedHashSet<ChatClient>())
  8. webSocket("/chat") { // this: DefaultWebSocketSession
  9. val client = ChatClient(this)
  10. clients += client
  11. try {
  12. while (true) {
  13. val frame = incoming.receive()
  14. when (frame) {
  15. is Frame.Text -> {
  16. val text = frame.readText()
  17. // Iterate over all the connections
  18. val textToSend = "${client.name} said: $text"
  19. for (other in clients.toList()) {
  20. other.session.outgoing.send(Frame.Text(textToSend))
  21. }
  22. }
  23. }
  24. }
  25. } finally {
  26. clients -= client
  27. }
  28. }
  29. }

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