Sending JSON over websockets in Rust
I like the JSON format but I feel it’s important to contain it to the job of passing a message from one system to another. Ideally the sender generates the text automatically from some strongly-typed object and the receiver does the reverse. If any required parts are missing or a field is the wrong type the entire message is rejected. Skipping this step and operating directly on the fields, as you can do in Javascript or with NSJSONSerialization, relies on the programmer not stuffing it up. I usually stuff it up.
I’m working on software that needs to send regular status updates over a TCP connection to a central server. JSON is a nice human-readable choice for the data format. Websockets are great because I can lean on an Apache proxy to provide LetsEncrypt-signed TLS, and unlike a raw TCP stream the communication is already packetised into discrete messages.
I felt like using Rust but had no idea how hard it would be compared with a dynamically typed language, so I prototyped it. It turned out relatively straightforward thanks to a couple of excellent crates, serde_json and ws-rs:
[dependencies]
serde = "1.0.9"
serde_json = "1.0"
serde_derive = "1.0.9"
ws = "0.7.3"
This is the data model, which is similar to what I have planned for the real app. To exercise the parser I made sure it has a few tricky bits: an enum with associated values, an array, and a struct of one type within another. I didn’t need to tweak them at all. The default implementations of Serialize and Deserialize cope perfectly.
The None
type of Option
is represented as a JSON null, and the serde parser will quietly provide a None
if the field is missing entirely in the JSON. While serde_json
supports various enum formats, without any customisation I already get a very nice representation for the overall PlayerState
. (serde_json
’s output is minified.)
{
"station_name": null,
"playback_state": {
"Playing": {
"item": 1,
"position": 8
}
},
"playlist": [
{
"filename": "jinglebells.mp3",
"duration": 242.5
},
{
"filename": "hark_ye.mp3",
"duration": 147
},
{
"filename": "hakuna.mp3",
"duration": 158.3
}
]
}
Very little is required on the server. The listen
function provided by ws-rs blocks, using an mio event loop under the hood to handle an arbitrary number of connected clients. Right now I’m only providing a handler for each incoming message. If I can parse the JSON I do so and print the debug representation of the PlayerState
.
As I hoped, the PlayerState
is fully filled in from the JSON:
Received status:
PlayerState { station_name: None, playback_state: Playing { item: 1, position: 8 }, playlist: [PlaylistItem { filename: "jinglebells.mp3", duration: 242.5 }, PlaylistItem { filename: "hark_ye.mp3", duration: 147 }, PlaylistItem { filename: "hakuna.mp3", duration: 158.3 }] }
Over in the client it needs to juggle both the websocket connection and a timer to send the JSON at intervals.
Based on the ws-rs guide, I’ve made a struct that represents the established connection. The Handler trait lets me respond to messages and all the websocket lifecycle events by implementing the relevant functions.
Since I don’t care about messages from the server I only implement on_open
and use it to start a new thread for sending updates. ws-rs takes care of thread safety—all I need to do is clone the Sender and give it to my new thread where it can use it to send messages. For this test I’ve created a hard-coded status and simulated playback by increasing the position on every update.
Similarly to listen
, connect
blocks for the duration of a complete connection, after which it returns and the program terminates. This disposes of the update thread, which would normally need to be cleaned up properly.
All this makes me very happy. I love using enums in data models and enums with associated values are even better. In my experience translating enums to numbers or string placeholders can be tedious and a great source of bugs. Having them transparently travel through the JSON means I can use them heavily and be guaranteed that they will have valid values at the other end. And if I did want to create a browser interface it would be easy to replicate the format in Javascript, albeit with none of the safety. I think I’m going to carry on with Rust for this project.
In the meantime Swift 4 finally has good support for JSON encoding so I am looking forward to using that at work.