In the previous part of this series, we discovered how to create persistent connections in Rack in theory, but now we'll put what we learned into practice.
The web has two formalized specifications for communication over a persistent connection: server-sent events (SSEs) and WebSockets.
WebSockets are widely used and highly popular, but SSEs are far less well-known. Let's explore them first.
Server-sent Events
Server-sent events (SSEs) enable a client to hold an open connection with the server, but only the server can publish messages to the client. It isn't a bi-directional protocol.
SSEs are a JavaScript API, so let's modify our app to serve an HTML page with the required script:
The API is encapsulated in the EventSource
class and new messages from the server trigger events that we listen for. Next, we need to build the endpoint that sends the events:
From a server point of view, this is fairly similar to the streaming bodies example we used in the previous part of this series. It's worth noting the content-type
header and the string format written back to the client.
Run the server (make sure you switch back to Puma):
Open up localhost:9292
on your web browser, and you'll see the time written to the document five times at one-second intervals.
This technique is great when the server just needs to notify the client about updates. The above example is fairly contrived, though, as it uses a loop, so let's look at how we can use this technique in a real application.
Ruby Queue
s
Ruby provides a Queue
data structure for communication between threads. We can use that to publish data back to a client. Let's stick with the same use case of publishing the current time five times at one-second intervals, but now we'll publish from a background thread.
In the above example, we spawn another background thread to push the current time to the queue every second. In the SSE thread, we call queue.pop
, which blocks until something is added to the queue.
Using this technique, we can use a pub/sub system such as Redis to add data to the queue
from a background thread, which is then published to the client.
That's SSEs covered! Next, let's look at WebSockets.
WebSockets
WebSockets are a bi-directional, full-duplex communication protocol that supports both binary and text data for client-server communication. They're widely used in the modern web and underpin Rails' Action Cable framework.
A WebSocket is created using an HTTP connection, but as a protocol, it's completely independent of HTTP.
To create a WebSocket connection, the client must make an HTTP request with these headers:
The server will respond with the status 101
, meaning Switching Protocols
. The TCP connection used for the HTTP request is upgraded to a WebSocket connection.
We won't get into the nitty-gritty of the WebSocket protocol in this post. It's fairly fiddly since it's a binary protocol. If you're curious, Starr Horne has written an amazing article on WebSockets.
Let's look at how to upgrade a TCP socket to a WebSocket connection.
Upgrading from HTTP to WebSockets
As described above, we'll need to send a 101
response. After this, we'll write to the socket using WebSockets' binary protocol for the communication to work.
We have to create a response key to securely create the connection. The UUID used to generate it is a global constant found in the specification. We won't go into the binary format of the string we're writing into the WebSocket connection, but it's all described in 'Building a simple websockets server from scratch in Ruby' by Starr Horne if you're curious.
Demo
Run the server:
The easiest way to create a connection is to use a WebSocket client. I recommend websocat
.
You'll see the string Hello world!
printed out! The connection is now active. In theory, we can write and receive messages over this socket now. We still need to implement receiving or publishing messages on the server for it to work in practice, but that's for another post.
A final word of warning: always remember that persistent connections come with challenges when using a threaded web server like Puma. A persistent connection ties up a thread and can cause significant performance issues unless you open a sizeable can of worms to implement your own threading mechanism.
Wrapping Up
That concludes our three-part deep dive into Rack! We first looked at how to set up a basic Rack app, before diving into socket hijacking for persistent connections.
Lastly, in this part, we used two specifications provided by our web platform to communicate over persistent connections: server-sent events (SSEs) and WebSockets.
I hope you've found this series useful. Happy coding!
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!