The WebSocket protocol is one of the convenient and common ways to maintain a persistent connection between the client and the server.
Support for the WebSocket protocol in Ktor is implemented using the WebSockets plugin. Let's figure out in more detail what WebSocket is and how to use the WebSockets plugin in your Ktor project!
What is WebSocket and when to use it?
WebSocket is a computer communication protocol that provides real-time data exchange between the server and the client. To accomplish this, WebSocket creates only one connection between the client and the server and maintains this connection until the need no longer exists (for example, exiting the application). Inside this connection, the server can send data to the client without being first requested by the client, so messages can be sent back and forth while the connection is closed.
To better understand the differences between WebSocket and HTTP protocols, let's assume that we have a chatting app on HTTP and WebSocket.
In the case of HTTP, the client has to continuously send requests to the server and ask "is there any message?". And the server responds "No". And this is repeated every second (or any other fixed time). And all these "empty" actions are only meant to catch a message at the end. A new connection is also created for each request.
In the case of WebSocket, you send only one request (called the handshake) that creates a persistent connection and inside this connection, the server sends new messages on its own without any additional requests.
In the WebSocket protocol, data is transmitted using messages consisting of a sequence of frames (one or more frames). This solution is primarily used based on security.
There are Control frames and Data frames.
Control frames are divided into:
- Close frame: serves to close the connection between the client and the server.
- Ping frame: either verifies that the connection is still responsive or keeps the connection alive.
- Pong frame: works together with ping frame and performs the same function.
Data frames are divided into:
- Text frame: provides an opportunity to exchange UTF-8 text data.
- Binary frame: same as text frame but consists of arbitrary binary data.
Of course you don't always need to update data in real-time, for instance, if your site consists of plain text, it's enough to use HTTP protocol. But if you have a game, chatting app, stock tickers, or something similar WebSocket is a convenient way to realize your ideas.
Installation
To use the WebSockets plugin, you need to include the ktor-server-websockets artifact in the build script:
implementation("io.ktor:ktor-server-websockets:$ktor_version")
And pass the plugin to the install function inside the embeddedServer function call:
import io.ktor.server.application.*
import io.ktor.server.websocket.*
// ...
fun main() {
embeddedServer(Netty, port = 8080) {
install(WebSockets)
}.start(wait = true)
}
Or inside the explicitly defined module:
import io.ktor.server.application.*
import io.ktor.server.websocket.*
// ...
fun Application.module() {
install(WebSockets)
}
To configure the plugin, you should pass WebSocketOptions class to install function:
install(WebSockets) {
pingPeriod = Duration.ofSeconds(15)
timeout = Duration.ofSeconds(15)
maxFrameSize = Long.MAX_VALUE
masking = false
}
pingPeriod: sets duration of ping sending.
timeout: is a time to wait for pong to reply ping, otherwise the connection will be terminated.
maxFrameSize: defines the maximum frame that could be received or sent.
masking: roughly enables/disables security for frames.
In our learning process, we are unlikely to need these options, but you will surely use these options to adjust the WebSocket connection for your particular purposes in your future projects.
Handle Websockets sessions
WebSockets plugin uses the webSocket function to handle WebSocket session by calling this function inside the routing block:
routing {
webSocket("/greetings") {
send(Frame.Text("Hello, I'm websocket handler!"))
}
}
We use send function to send text content to the client.
To try out our server we can use Postman.
If we send a WebSocket request to ws://localhost:8080/greetings, we will see our greeting message:
That's all fine, but it doesn't look like a persistent connection!
Let's figure the WebSocket session out in more detail.
The WebSocket session is represented by the DefaultWebSocketServerSession class by default. To access the channels for receiving and sending WebSocket frames, you should use the incoming and outgoing properties of this class. Frames are represented by the Frame class.
routing {
webSocket("/greetings") {
send("Hello, I'm websocket handler!")
for (frame in incoming) {
// sipping any non-text frames
frame as? Frame.Text ?: continue
// retrieving String value from the frame
val receivedText = frame.readText()
// sending received text back to client
send(Frame.Text("You enter: \"$receivedText\""))
}
}
}
To maintain a persistent connection, we use for cycle.
Let's try to send a WebSocket request via Postman:
It works!
In addition, you may notice that our connection is not broken like last time. We got a persistent connection.
Now, to implement the ability to disconnect from the server, we add the close function to our code:
routing {
webSocket("/greetings") {
send("Hello, I'm websocket handler!")
for (frame in incoming) {
frame as? Frame.Text ?: continue
val receivedText = frame.readText()
if (receivedText.equals("exit", ignoreCase = true)) {
close(CloseReason(CloseReason.Codes.NORMAL, "Client exit"))
} else {
send(Frame.Text("You enter: \"$receivedText\""))
}
}
}
}
Now, if we send "exit" message we will be disconnected from our server:
Handle multiple websocket sessions
To handle multiple WebSocket sessions, we need to store each session on a server. To do this, first, let's create a Connection class:
import io.ktor.websocket.*
import java.util.concurrent.atomic.*
class Connection (val session: DefaultWebSocketSession) {
companion object {
val lastId = AtomicInteger(1)
}
val name = "user${lastId.getAndIncrement()}"
}
Using AtomicInteger ensures that different users will never receive the same ID.
Then, inside the webSocket function, we will create a new Connection every time someone connects to our server and store all connections in the connections variable:
routing {
val connections = Collections.synchronizedSet<Connection?>(LinkedHashSet())
webSocket("/greetings") {
val thisConnection = Connection(this)
connections += thisConnection
try {
send("You are connected! There are ${connections.count()} testers 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
}
}
}
Now, with the help of Postman, let's try what we got:
Here we just created two separate WebSocket requests in Postman and sent a message from each of them. And we have two persistent connections to our server!
Conclusion
Let's recap what we have learned on this topic:
- Ktor uses the
WebSocketsplugin to support the WebSocket protocol. - We have learned how to install and configure the
WebSocketsplugin. - We have learned to handle single and multiple WebSocket sessions.