Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

WebSockets With AWS Lambda

DZone's Guide to

WebSockets With AWS Lambda

Let's see how Fanout Cloud can integrate with FaaS backends. We'll be working with AWS Lambda here to build an API that supports WebSockets.

· Cloud Zone ·
Free Resource

See why enterprise app developers love Cloud Foundry. Download the 2018 User Survey for a snapshot of Cloud Foundry users’ deployments and productivity.

Fanout Cloud handles long-lived connections, such as HTTP streaming and WebSocket connections, on behalf of API backends. For projects that need to push data at scale, this can be a smart architecture. It also happens to be handy with function-as-a-service backends, such as AWS Lambda, which are not designed to handle long-lived connections on their own. By combining Fanout Cloud and Lambda, you can build serverless real-time applications.

Of course, Lambda can integrate with services such as AWS IoT to achieve a similar effect. The difference with Fanout Cloud is that it works at a lower level, giving you access to raw protocol elements. For example, Fanout Cloud enables you to build a Lambda-powered API that supports plain WebSockets.

To make integration easy, we’ve introduced FaaS libraries for Node.js and Python. Read on to learn how it all works.

An Example

Let’s jump right into an example. The following Node.js code implements a WebSocket echo service:

var grip = require('grip');
var faas_grip = require('faas-grip');

exports.handler = function (event, context, callback) {
    var ws;
    try {
        ws = faas_grip.lambdaGetWebSocket(event);
    } catch (err) {
        callback(null, {
            statusCode: 400,
            headers: {'Content-Type': 'text/plain'},
            body: 'Not a WebSocket-over-HTTP request\n'
        });
        return;
    }

    // if this is a new connection, accept it
    if (ws.isOpening()) {
        ws.accept();
    }

    // here we loop over any messages
    while (ws.canRecv()) {
        var message = ws.recv();

        // if return value is null, then the connection is closed
        if (message == null) {
            ws.close();
            break;
        }

        // echo the message
        ws.send(message);
    }

    callback(null, ws.toResponse());
};

The lambdaGetWebSocket method returns a WebSocketContext object, which has a socket-like API. You can call methods on it such as accept(), send(), recv(), and close(). This makes it possible to write code that almost looks like normal socket handling code.

Don’t be fooled by the while loop; at first glance, it might look like it runs for the lifetime of the WebSocket connection, but it really only runs while processing each batch of incoming events.

In the next two sections, we’ll explain the magic going on behind the scenes.

WebSocket-over-HTTP Protocol

When Fanoud Cloud receives a WebSocket connection from a client, it converts events from that connection into a series of HTTP requests and sends them to a configured backend server. Events are encoded in the HTTP request body using the application/websocket-events content type. The backend can respond with events to the WebSocket client by encoding events in the HTTP response.

Below are some example exchanges. The format is inspired by HTTP chunked encoding. Note that the characters \r\n represent a two-byte carriage return and newline sequence. Linebreaks are also inserted for readability.

Fanout tells the backend server about a new connection:

POST /target HTTP/1.1
Connection-Id: b5ea0e11
Content-Type: application/websocket-events

OPEN\r\n

Backend server accepts connection:

HTTP/1.1 200 OK
Content-Type: application/websocket-events

OPEN\r\n

Fanout relays message from client:

POST /target HTTP/1.1
Connection-Id: b5ea0e11
Content-Type: application/websocket-events

TEXT 5\r\n
hello\r\n

Backend server responds with two messages:

HTTP/1.1 200 OK
Content-Type: application/websocket-events

TEXT 5\r\n
world\r\n
TEXT 1B\r\n
here is another nice message\r\n

For details, see the spec. Note that it’s not necessary to completely understand the protocol since our libraries take care of it for you, through the WebSocketContext object.

WebSocketContext

Our Node.js and Python libraries each provide a socket-like object called WebSocketContext that handles the event marshalling over HTTP. The object contains methods like accept(), send(), recv(), etc. What’s interesting is that these methods don’t operate directly on a real WebSocket. When recv() is called, it simply iterates over the events received in the current HTTP request. When send() is called, events are temporarily enqueued, and later serialized into the HTTP response. WebSocketContext objects are not long-lived, and a fresh one is created for each handler invocation and destroyed afterwards.

Now that you know how the WebSocket-over-HTTP protocol and WebSocketContext object work, we can explain the earlier example code. Recall this part:

// if this is a new connection, accept it
if (ws.isOpening()) {
    ws.accept();
}

What happens here is isOpening() returns true if an OPEN event was received in the current request. The accept() call sets a flag on the object that it should include an OPEN event when it responds.

And here’s the main message loop:

// here we loop over any messages
while (ws.canRecv()) {
    var message = ws.recv();

    // if return value is null, then the connection is closed
    if (message == null) {
        ws.close();
        break;
    }

    // echo the message
    ws.send(message);
}

The canRecv() call returns true if TEXT, BINARY, or CLOSE events were received in the current request, and they haven’t been read yet using recv(). When there are no more events to read in the batch, canRecv() returns false and the loop exits. The send() method enqueues an outgoing message into the WebSocketContext.

It’s important to note here that canRecv() returning false does not mean the physical WebSocket connection has closed. It only means there are no more events to read in the current request. The loop can exit and the Lambda function can terminate, and Fanout Cloud will keep the WebSocket connection open with the client.

Finally, the last part of the function:

callback(null, ws.toResponse());

The toResponse() method generates an HTTP response to send back to Fanout Cloud, containing WebSocket events. If the context was flagged using accept(), then an OPEN event will be included. If send() and/or close() were called, then TEXT and/or CLOSE events will be included as appropriate. Fanout Cloud will then receive these events from the Lambda function, and translate them into native WebSocket communication with the client.

Pushing Data

So far we’ve discussed how to accept incoming WebSocket connection requests and respond to incoming messages, but the main reason to use WebSockets is to have the ability to push data! This is done using publish/subscribe messaging between Fanout Cloud and a backend publisher.

To subscribe a WebSocket connection to a channel, call subscribe():

ws.subscribe('mychannel');

Data can then be published to subscribed connections like this:

faas_grip.publish('mychannel', new grip.WebSocketMessageFormat('some data'));

Note that the WebSocket client has no awareness of the publish/subscribe layer. The channel schema is private between Fanout Cloud and your backend, and clients don’t directly subscribe to channels.

Connection Metadata

The Lambda function terminates after processing each set of events, making it seem like stateful APIs would be impossible to implement. However, Fanout Cloud and its libraries make this really easy to do via the metadata feature.

For example, suppose you have a WebSocket API with an authentication step. You could do something like this:

var msg = JSON.parse(ws.recv());
if (msg.type == 'auth') {
    if (authCheck(msg.username, msg.password)) {
        // apply 'username' to the metadata
        ws.meta.username = msg.username;
    } else {
        ws.send(JSON.stringify({
            'type': 'error',
            'reason': 'auth-failed'
        }));
    }
} else if (msg.type == 'do-thing') {
    if (ws.meta.username) {
        // if user was previously authorized, perform the action
        doThingAs(ws.meta.username);
    } else {
        ws.send(JSON.stringify({
            'type': 'error',
            'reason': 'not-authorized'
        }));
    }
}

Any values set on the meta property of the WebSocketContext will be sent back to Fanout Cloud and preserved across invocations.

Chat Example

Tying it all together, here’s an example of a chat service that allows clients to connect, set a nickname by sending an IRC-like /nick message, and send messages to other connected clients:

var grip = require('grip');
var faas_grip = require('faas-grip');

exports.handler = function (event, context, callback) {
    var ws;
    try {
        ws = faas_grip.lambdaGetWebSocket(event);
    } catch (err) {
        callback(null, {
            statusCode: 400,
            headers: {'Content-Type': 'text/plain'},
            body: 'Not a WebSocket-over-HTTP request\n'
        });
        return;
    }

    // if this is a new connection, accept it and subscribe it to a channel
    if (ws.isOpening()) {
        ws.accept();
        ws.subscribe('room');
    }

    // here we loop over any messages
    while (ws.canRecv()) {
        var message = ws.recv();

        // if return value is null, then the connection is closed
        if (message == null) {
            ws.close();
            break;
        }

        if (message.startsWith('/nick ')) {
            var nick = message.substring(6);
            ws.meta.nick = nick;
            ws.send('nickname set to [' + nick + ']');
        } else {
            // send the message to all clients
            var nick = ws.meta.nick || 'anonymous';
            faas_grip.publish(
                'room',
                new grip.WebSocketMessageFormat(nick + ': ' + message)
            );
        }
    }

    callback(null, ws.toResponse());
};

This is a real-time WebSocket API driven by a Lambda function! Did we mention it also scales?

HTTP Streaming and Long-Polling

In addition to WebSockets, long-lived HTTP connections are also supported. See the library documentation for details.

Implement WebSockets on Lambda Today

Fanout Cloud and AWS Lambda are a powerful combination. Check out the library for your preferred environment (Node.js, Python), and get started for free.

Cloud Foundry saves app developers $100K and 10 weeks on average per development cycle. Download the 2018 User Survey for a snapshot of Cloud Foundry users’ deployments and productivity. Find out what people love about the industry standard cloud application platform.

Topics:
cloud ,aws lambda ,websocket ,faas ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}