|
| 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 | + |
| 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