Cómo crear aplicaciones en tiempo real utilizando WebSockets con AWS API Gateway y Lambda

Recientemente, AWS anunció el lanzamiento de una característica muy solicitada: WebSockets para Amazon API Gateway. Con WebSockets, podemos crear una línea de comunicación bidireccional que se puede utilizar en muchos escenarios, como aplicaciones en tiempo real. Esto plantea la pregunta: ¿qué son las aplicaciones en tiempo real? Entonces, primero respondamos esa pregunta.

La mayoría de las aplicaciones actualmente operativas utilizan una arquitectura cliente-servidor. En la arquitectura cliente-servidor, el cliente envía las solicitudes a través de Internet utilizando la comunicación de red y luego el servidor procesa esa solicitud y envía la respuesta al cliente.

Aquí puede ver que es el cliente el que inicia la comunicación con el servidor. Entonces, primero el cliente inicia la comunicación y el servidor responde a la solicitud enviada por el servidor. Entonces, ¿qué pasa si el servidor quiere iniciar la comunicación y enviar respuestas sin que el cliente las solicite primero? Ahí es donde entran en juego las aplicaciones en tiempo real.

Las aplicaciones en tiempo real son aplicaciones en las que el servidor tiene la capacidad de enviar a los clientes sin que el cliente solicite datos primero. Supongamos que tenemos una aplicación de chat donde dos clientes de chat pueden comunicarse a través de un servidor. En esta situación, es un desperdicio si todos los clientes de chat solicitan datos del servidor como cada segundo. Lo que es más eficiente es si el servidor envía datos a las aplicaciones de chat del cliente cuando se recibe un chat. Esta funcionalidad se puede realizar mediante aplicaciones en tiempo real.

Amazon anunció que admitirán WebSockets en API Gateway en AWS re: Invent 2018. Más tarde, en diciembre, lo lanzaron en API Gateway. Así que ahora, con la infraestructura de AWS, podemos crear aplicaciones en tiempo real utilizando API Gateway.

En esta publicación, vamos a crear una aplicación de chat simple usando API Gateway WebSockets. Antes de comenzar a implementar nuestra aplicación de chat, hay algunos conceptos que debemos comprender con respecto a las aplicaciones en tiempo real y API Gateway.

Conceptos de la API de WebSocket

Una API de WebSocket se compone de una o más rutas. Existe una expresión de selección de ruta para determinar qué ruta debe usar una solicitud entrante en particular, que se proporcionará en la solicitud entrante. La expresión se evalúa frente a una solicitud entrante para producir un valor que corresponde a uno de los valores routeKey de su ruta . Por ejemplo, si nuestros mensajes JSON contienen una acción de llamada de propiedad y desea realizar diferentes acciones basadas en esta propiedad, su expresión de selección de ruta podría ser ${request.body.action}.

Por ejemplo: si su mensaje JSON se parece a {“action”: “onMessage”, “message”: “Hello Everyone”}, entonces se elegirá la ruta onMessage para esta solicitud.

De forma predeterminada, hay tres rutas que ya están definidas en la API de WebSocket. Además de las rutas mencionadas a continuación, podemos agregar rutas personalizadas para nuestras necesidades.

  • $ default : se usa cuando la expresión de selección de ruta produce un valor que no coincide con ninguna de las otras claves de ruta en sus rutas API. Esto se puede utilizar, por ejemplo, para implementar un mecanismo genérico de manejo de errores.
  • $ connect : la ruta asociada se utiliza cuando un cliente se conecta por primera vez a su API de WebSocket.
  • $ desconectar : la ruta asociada se usa cuando un cliente se desconecta de su API.

Una vez que un dispositivo se conecta correctamente a través de la API de WebSocket, al dispositivo se le asignará una identificación de conexión única. Esta identificación de conexión se mantendrá durante toda la vida útil de la conexión. Para enviar mensajes de vuelta al dispositivo, debemos utilizar la siguiente solicitud POST utilizando el ID de conexión.

POST //{api-id}.execute-api.us-east 1.amazonaws.com/{stage}/@connections/{connection_id}

Implementando la aplicación de chat

Después de aprender los conceptos básicos de la API de WebSocket, veamos cómo podemos crear una aplicación en tiempo real usando la API de WebSocket. En esta publicación, vamos a implementar una aplicación de chat simple usando WebSocket API, AWS LAmbda y DynamoDB. El siguiente diagrama muestra la arquitectura de nuestra aplicación en tiempo real.

En nuestra aplicación, los dispositivos estarán conectados a API Gateway. Cuando un dispositivo se conecta, una función lambda guardará el ID de conexión en una tabla de DynamoDB. En una instancia en la que queremos enviar un mensaje al dispositivo, otra función lambda recuperará la identificación de la conexión y los datos POST al dispositivo usando una URL de devolución de llamada.

Creación de la API de WebSocket

Para crear la API de WebSocket, primero debemos ir al servicio Amazon API Gateway usando la consola. Allí elige crear una nueva API. Haga clic en WebSocket para crear una API de WebSocket, proporcione un nombre de API y nuestra expresión de selección de ruta. En nuestro caso, agregue $ request.body.action como nuestra expresión de selección y presione Crear API.

Después de crear la API, seremos redirigidos a la página de rutas. Aquí podemos ver tres rutas ya predefinidas: $ connect, $ desconectar y $ default. También crearemos una ruta personalizada $ onMessage. En nuestra arquitectura, las rutas $ connect y $ desconect logran las siguientes tareas:

  • $ connect: cuando se llama a esta ruta, una función de Lambda agregará el ID de conexión del dispositivo conectado a DynamoDB.
  • $ desconectar: ​​cuando se llama a esta ruta, una función de Lambda eliminará el ID de conexión del dispositivo desconectado de DynamoDB.
  • onMessage: cuando se llama a esta ruta, el cuerpo del mensaje se enviará a todos los dispositivos que estén conectados en ese momento.

Antes de agregar la ruta de acuerdo con lo anterior, necesitamos hacer cuatro tareas:

  • Cree una tabla de DynamoDB
  • Crear función de conexión lambda
  • Crear función lambda de desconexión
  • Crear función lambda onMessage

Primero, creemos la tabla DynamoDB. Vaya al servicio DynamoDB y cree una nueva tabla llamada Chat. Agregue la clave principal como 'connectionid'.

A continuación, creemos la función conectar Lambda. Para crear la función Lambda, vaya a Servicios Lambda y haga clic en crear función. Seleccione Autor desde cero y proporcione el nombre como 'ChatRoomConnectFunction' y un rol con los permisos necesarios. (El rol debe tener permiso para obtener, colocar y eliminar elementos de DynamoDB, llamar a las llamadas a la API en la puerta de enlace de la API).

En el código de la función lambda agregue el siguiente código. Este código agregará el ID de conexión del dispositivo conectado a la tabla DynamoDB que hemos creado.

exports.handler = (event, context, callback) => { const connectionId = event.requestContext.connectionId; addConnectionId(connectionId).then(() => { callback(null, { statusCode: 200, }) });}
function addConnectionId(connectionId) { return ddb.put({ TableName: 'Chat', Item: { connectionid : connectionId }, }).promise();}

A continuación, creemos también la función desconectar lambda. Usando los mismos pasos, cree una nueva función lambda llamada

'ChatRoomDonnectFunction'. Agregue el siguiente código a la función. Este código eliminará el ID de conexión de la tabla de DynamoDB cuando se desconecte un dispositivo.

const AWS = require('aws-sdk');const ddb = new AWS.DynamoDB.DocumentClient();
exports.handler = (event, context, callback) => { const connectionId = event.requestContext.connectionId; addConnectionId(connectionId).then(() => { callback(null, { statusCode: 200, }) });}
function addConnectionId(connectionId) { return ddb.delete({ TableName: 'Chat', Key: { connectionid : connectionId, }, }).promise();}

Ahora hemos creado la tabla DynamoDB y dos funciones lambda. Antes de crear la tercera función lambda, volvamos nuevamente a API Gateway y configuremos las rutas usando nuestras funciones lambda creadas. Primero, haga clic en $ connect route. Como tipo de integración, seleccione la función Lambda y seleccione la función ChatRoomConnection.

También podemos hacer lo mismo en la ruta $ desconectar, donde la función lambda será ChatRoomDisconnectionFunction:

Now that we have configured our $connect and $disconnect routes, we can actually test whether out WebSocket API is working. To do that we must first to deploy the API. In the Actions button, click on Deploy API to deploy. Give a stage name such as Test since we are only deploying the API for testing.

After deploying, we will be presented with two URLs. The first URL is called WebSocket URL and the second is called Connection URL.

The WebSocket URL is the URL that is used to connect through WebSockets to our API by devices. And the second URL, which is Connection URL, is the URL which we will use to call back to the devices which are connected. Since we have not yet configured call back to devices, let’s first only test the $connect and $disconnect routes.

To call through WebSockets we can use the wscat tool. To install it, we need to just issue the npm install -g wscat command in the command line. After installing, we can use the tool using wscat command. To connect to our WebSocket API, issue the following command. Make sure to replace the WebSocket URL with the correct URL provided to you.

wscat -c wss://bh5a9s7j1e.execute-api.us-east-1.amazonaws.com/Test

When the connection is successful, a connected message will be displayed on the terminal. To check whether our lambda function is working, we can go to DynamoDB and look in the table for the connection id of the connected terminal.

As above, we can test the disconnect as well by pressing CTRL + C which will simulate a disconnection.

Now that we have tested our two routes, let us look into the custom route onMessage. What this custom route will do is it will get a message from the device and send the message to all the devices that are connected to the WebSocket API. To achieve this we are going to need another lambda function which will query our DynamoDB table, get all the connection ids, and send the message to them.

Let’s first create the lambda function in the same way we created other two lambda functions. Name the lambda function ChatRoomOnMessageFunction and copy the following code to the function code.

const AWS = require('aws-sdk');const ddb = new AWS.DynamoDB.DocumentClient();require('./patch.js');
let send = undefined;function init(event) { console.log(event) const apigwManagementApi = new AWS.ApiGatewayManagementApi({ apiVersion: '2018-11-29', endpoint: event.requestContext.domainName + '/' + event.requestContext.stage }); send = async (connectionId, data) => { await apigwManagementApi.postToConnection({ ConnectionId: connectionId, Data: `Echo: ${data}` }).promise(); }}
exports.handler = (event, context, callback) => { init(event); let message = JSON.parse(event.body).message getConnections().then((data) => { console.log(data.Items); data.Items.forEach(function(connection) { console.log("Connection " +connection.connectionid) send(connection.connectionid, message); }); }); return {}};
function getConnections(){ return ddb.scan({ TableName: 'Chat', }).promise();}

The above code will scan the DynamoDB to get all the available records in the table. For each record, it will POST a message using the Connection URL provided to us in the API. In the code, we expect that the devices will send the message in the attribute named ‘message’ which the lambda function will parse and send to others.

Since WebSockets API is still new there are some things we need to do manually. Create a new file named patch.js and add the following code inside it.

require('aws-sdk/lib/node_loader');var AWS = require('aws-sdk/lib/core');var Service = AWS.Service;var apiLoader = AWS.apiLoader;
apiLoader.services['apigatewaymanagementapi'] = {};AWS.ApiGatewayManagementApi = Service.defineService('apigatewaymanagementapi', ['2018-11-29']);Object.defineProperty(apiLoader.services['apigatewaymanagementapi'], '2018-11-29', { get: function get() { var model = { "metadata": { "apiVersion": "2018-11-29", "endpointPrefix": "execute-api", "signingName": "execute-api", "serviceFullName": "AmazonApiGatewayManagementApi", "serviceId": "ApiGatewayManagementApi", "protocol": "rest-json", "jsonVersion": "1.1", "uid": "apigatewaymanagementapi-2018-11-29", "signatureVersion": "v4" }, "operations": { "PostToConnection": { "http": { "requestUri": "/@connections/{connectionId}", "responseCode": 200 }, "input": { "type": "structure", "members": { "Data": { "type": "blob" }, "ConnectionId": { "location": "uri", "locationName": "connectionId" } }, "required": [ "ConnectionId", "Data" ], "payload": "Data" } } }, "shapes": {} } model.paginators = { "pagination": {} } return model; }, enumerable: true, configurable: true});
module.exports = AWS.ApiGatewayManagementApi;

I took the above code from this article. The functionality of this code is to automatically create the Callback URL for our API and send the POST request.

Now that we have created the lambda function we can go ahead and create our custom route in API Gateway. In the New Route Key, add ‘OnMessage’ as a route and add the custom route. As configurations were done for other routes, add our lambda function to this custom route and deploy the API.

Now we have completed our WebSocket API and we can fully test the application. To test that sending messages works for multiple devices, we can open and connect using multiple terminals.

After connecting, issue the following JSON to send messages:

{"action" : "onMessage" , "message" : "Hello everyone"}

Here, the action is the custom route we defined and the message is the data that need to be sent to other devices.

That is it for our simple chat application using AWS WebSocket API. We have not actually configured the $defalut route which is called on every occasion where there no route is found. I will leave the implementation of that route to you. Thank you and see you in another post. :)