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:

import sys
import json
from pubcontrol import Item
from gripcontrol import GripPubControl, WebSocketEvent, \
    WebSocketMessageFormat, decode_websocket_events, encode_websocket_events

pub = GripPubControl({'control_uri': 'http://localhost:5561'})

opening = False
out_headers = []
out = []
for e in decode_websocket_events(sys.stdin.read()):
    if e.type == 'OPEN':
        if not opening:
            opening = True

            # enable GRIP
            out_headers.append(('Sec-WebSocket-Extensions', 'grip'))

            # ack the open
            out.append(e)

            # subscribe connection to channel
            cm = {'type': 'subscribe', 'channel': 'room'}
            out.append(WebSocketEvent('TEXT', 'c:%s' % json.dumps(cm)))
    elif e.type == 'CLOSE':
        out.append(e) # ack
        break
    elif e.type == 'TEXT':
        # broadcast to everyone
        pub.publish('room', Item(WebSocketMessageFormat(e.content)))

out_headers.append(('Content-Type', 'application/websocket-events'))

for header in out_headers:
    cm = {
        'type': 'setHeader',
        'payload': {
            'name': header[0],
            'value': header[1]
        }
    }
    sys.stderr.write('%s\n' % json.dumps(cm))

sys.stdout.write(encode_websocket_events(out))

The code can be run as a microservice like this:

$ microcule handler.py

By default, Microcule listens on port 3000. Now we’ll start a Pushpin instance to point at it:

$ pushpin -m --route "* localhost:3000,over_http"

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:

$ wscat -c ws://localhost:7999
connected (press CTRL+C to quit)
> hello
  < hello

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.

pub = GripPubControl({'control_uri': 'http://localhost:5561'})

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.

out = []
for e in decode_websocket_events(sys.stdin.read()):
    ...

sys.stdout.write(encode_websocket_events(out))

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.

for header in out_headers:
    cm = {
        'type': 'setHeader',
        'payload': {
            'name': header[0],
            'value': header[1]
        }
    }
    sys.stderr.write('%s\n' % json.dumps(cm))

The above code tells Microcule about response headers, using special Microcule control messages over stderr.

if e.type == 'OPEN':
    if not opening:
        opening = True

        # enable GRIP
        out_headers.append(('Sec-WebSocket-Extensions', 'grip'))

        # ack the open
        out.append(e)

        # subscribe connection to channel
        cm = {'type': 'subscribe', 'channel': 'room'}
        out.append(WebSocketEvent('TEXT', 'c:%s' % json.dumps(cm)))

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:

elif e.type == 'TEXT':
    # broadcast to everyone
    pub.publish('room', Item(WebSocketMessageFormat(e.content)))

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!