Serverless WebSockets
Serverless development is a hot topic lately. Development & operations of a web service can be greatly simplified by writing your application logic as short-lived functions, and relying on outside organizations for the development of all the other components in your stack (e.g. databases, gateways, container engines, etc). The term “serverless” is a bit funny because of course there are still servers in your stack, and they may even be your own servers, but the main idea is you no longer have to worry about your own long-running application code.
This all sounds great, but an issue arises: in this serverless world, how do you support long-lived connections (e.g. HTTP streaming/WebSocket connections) for realtime data push, without long-running application code? By delegating connection management to another component, of course! In this article we’ll talk about how to build a simple WebSocket service with Pushpin, using Microcule for running the backend worker function.
First, a little about the tools being used:
Pushpin
Pushpin is an open source project that makes it easy to build realtime web services. It works as a proxy server to an HTTP backend. Traffic is forwarded through Pushpin to the backend service, and the backend decides how Pushpin should behave. Response traffic can be sent back unmodified, or it can include special instructions that Pushpin should act upon. For example, the backend could instruct Pushpin to keep an incoming connection open and associate it with one or more publish-subscribe channels, so that data can be sent down the connection later on.
Microcule
Microcule is an open source project that makes it easy to run HTTP microservices. You can think of it like an open source AWS Lambda. Each incoming HTTP request invokes a program in an isolated process to handle the request and then terminate. This ensures the backend code will be stateless as each request is given its own process. Microcule supports numerous programming languages and is used by the Hook.io service.
Handler code
For this article we’ve made a basic WebSocket service, where incoming messages are broadcasted to all open connections. Here’s the complete backend code, written in Python:
The code can be run as a microservice like this:
By default, Microcule listens on port 3000. Now we’ll start a Pushpin instance to point at it:
By default, Pushpin listens on port 7999 for external client traffic and port 5561 for receiving internal control commands.
You can then connect to the service, for example with wscat
, to send and receive messages:
It’s important to point out here that the backend worker only executes while it is processing an incoming WebSocket event. Otherwise it is not running at all. WebSocket connections stay open because they are being managed by Pushpin. You can even modify the backend handler code between executions without disconnecting clients.
Walkthrough
Now let’s walk through some key parts of the code.
The pub
object here is used for publishing data through Pushpin. Note that it doesn’t immediately connect to the specified URI. Instead, a connection is made only if publish
is called.
Pushpin exchanges a list of WebSocket events with the backend. Both the HTTP request and the response may contain events. The protocol is described here. We use the gripcontrol library to serialize/deserialize the events. The code in the handler loops over the incoming events, and assembles a list of output events to be sent back at the end. WebSocketEvent
objects contain type
and content
members.
The above code tells Microcule about response headers, using special Microcule control messages over stderr.
If an OPEN
event is received, then we send an OPEN
event back to Pushpin. This is how a connection is acknowledged. We enable the grip
WebSocket extension, which allows the handler to send control messages to Pushpin using WebSocket messages. Without this extension, Pushpin will treat the connection as a passthrough and not attempt to hijack messages. We also take this moment to send one such control message, to subscribe the connection to a channel called room
.
Here we route incoming messages out to all connections subscribed to the room
channel:
Conclusion
Pushpin and serverless backends are a powerful combination. Realtime services are normally incredibly complicated to build, as well as stateful. With Pushpin and Microcule, you can make realtime services that are easy to understand and maintain, with backend code that is stateless. Such a system is straightforward to scale, too. Both Pushpin and Microcule are horizontally scalable, and instances of each tier don’t need to talk to each other.
The example source is also on GitHub.
Happy realtiming!
Recent posts
-
We've been acquired by Fastly
-
A cloud-native platform for push APIs
-
Vercel and WebSockets
-
Rewriting Pushpin's connection manager in Rust
-
Let's Encrypt for custom domains