Artigo original: How to build real-time applications using WebSockets with AWS API Gateway and Lambda

Escrito por: Janitha Tennakoon

Recentemente, a AWS anunciou o lançamento de um recurso altamente requisitado: o WebSockets para Amazon API Gateway. Com os Websockets, conseguimos criar uma linha de comunicação de via dupla, que pode ser usada em vários cenários, como aplicações em tempo real. Isso levanta a seguinte pergunta: o que são aplicações em tempo real? Vamos, primeiro, responder a essa pergunta.

A maioria das aplicações que estão operando atualmente usam a arquitetura client-servidor. Na arquitetura client-servidor, o client envia requisições pela Internet usando redes de comunicação. O servidor, então, processa aquela requisição e envia a resposta de volta para o client.

Aqui, você pode ver que é o client que começa a comunicação com o servidor. Primeiro, o client inicia a comunicação e o servidor responde àquela requisição enviada pelo client. Se o servidor quiser começar a comunicação e enviar respostas sem que o client faça a requisição em primeiro lugar, como é possível fazer isso? É aqui que as aplicações em tempo real entram em campo.

Aplicações em tempo real são aquelas onde o servidor tem a habilidade de push para os clients sem que o estes solicitem os dados primeiro. Considere que temos uma aplicação de bate-papo, onde dois clients podem se comunicar via um servidor. Nessa situação, é um desperdício se todos os clients da aplicação solicitarem dados do servidor a cada segundo. É mais eficiente se o servidor enviar dados para a aplicação do client quando uma conversa for recebida. Essa funcionalidade pode ser feita através de aplicações em tempo real.

A Amazon anunciou que daria suporte a WebSockets na API Gateway na AWS re:Invent 2018. Em dezembro, eles lançaram esse suporte na API Gateway. Então, agora, usando a infraestrutura da AWS podemos criar aplicações em tempo real usando a API Gateway.

Neste artigo, criaremos uma aplicação simples de bate-papo usando os WebSockets da API Gateway. Antes de começarmos a implementar nossa aplicação de bate-papo, existem alguns conceitos que precisamos entender no que diz respeito às aplicações em tempo real e à API Gateway.

Conceitos da API com WebSockets

Uma API com WebSockets é composta por uma ou mais rotas. Existe uma expressão de seleção de rota para determinar qual rota uma solicitação de entrada específica deve usar, que será fornecida na solicitação de entrada. A expressão é avaliada em relação a uma solicitação de entrada para produzir um valor que corresponda a um dos valores routeKey da sua rota. Por exemplo, se nossas mensagens JSON contiverem uma ação de chamada de propriedade e você quiser executar ações diferentes com base nessa propriedade, sua expressão de seleção de rota poderá ser ${request.body.action}.

Por exemplo: se sua mensagem JSON for parecida com {"action" : "onMessage", "message" : "Olá, pessoal"}, então a rota onMessage será escolhida para essa requisição.

Por padrão, existem três rotas já definidas na API com WebSockets. Além das rotas mencionadas abaixo, podemos adicionar rotas personalizadas de acordo com nossas necessidades.

  • $default — usada quando a expressão de seleção de rota produz um valor que não corresponde a nenhuma outra chave de rota nas rotas da API. Isso pode ser usado, por exemplo, para implementar um mecanismo genérico de tratamento de erros.
  • $connect — a rota associada é usada quando um client se conecta pela primeira vez à sua API WebSocket.
  • $disconnect — a rota associada é usada quando um client se desconecta da sua API.

Depois que um dispositivo for conectado com sucesso por meio da API com WebSockets, o dispositivo será alocado com um ID de conexão exclusivo. Esse ID de conexão persistirá durante toda a vida útil da conexão. Para enviar mensagens de volta ao dispositivo, precisamos usar a seguinte solicitação POST usando o ID de conexão.

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

Implementando a aplicação de bate-papo

Depois de aprender os conceitos básicos das APIs com WebSockets, vamos ver como podemos criar uma aplicação em tempo real usando uma API com WebSockets. Neste artigo, vamos implementar uma aplicação de bate-papo simples usando uma API com WebSockets, o AWS Lambda e o DynamoDB. O diagrama a seguir mostra a arquitetura de nossa aplicação em tempo real.

EiHc2HzJkq621fqsah0gFRGo6J0298tfu5KF

Na nossa aplicação, os dispositivos serão conectados ao API Gateway. Quando um dispositivo é conectado, uma função lambda salva o ID da conexão em uma tabela do DynamoDB. Em uma instância em que desejamos enviar uma mensagem de volta ao dispositivo, outra função lambda recuperará o ID de conexão e os dados POST de volta ao dispositivo usando um URL de callback.

Criando a API com WebSockets

Para criar a API com WebSockets, precisamos primeiro acessar o serviço da Amazon API Gateway usando o console. Estando lá, escolha criar uma API (Create a new API). Clique em WebSocket para criar uma API com WebSockets, forneça um nome de API e nossa expressão de seleção de rota. No nosso caso, adicione $request.body.action como nossa expressão de seleção e clique em Create API.

Fu3WT1nNu67o1AEQbVvyqPjF4Xfgdy5DIdU8

Após criar a API, seremos redirecionados para a página de rotas. Aqui podemos ver três rotas já predefinidas: $connect, $disconnect e $default. Também criaremos uma rota personalizada $onMessage. Em nossa arquitetura, as rotas $connect e $disconnect realizam as seguintes tarefas:

  • $connect — quando essa rota for chamada, uma função Lambda adicionará o ID de conexão do dispositivo conectado ao DynamoDB.
  • $disconnect — quando essa rota for chamada, uma função Lambda excluirá o ID de conexão do dispositivo desconectado do DynamoDB.
  • onMessage — quando essa rota for chamada, o corpo da mensagem será enviado para todos os dispositivos que estiverem conectados no momento.

Antes de adicionar a rota de acordo com o que está acima, precisamos realizar quatro tarefas:

  • Criar uma tabela no DynamoDB
  • Criar uma função lambda de conexão
  • Criar uma função lambda de desconexão
  • Criar uma função lambda para onMessage

Primeiro, vamos criar a tabela do DynamoDB. Acesse o serviço DynamoDB e crie uma tabela chamada Chat. Adicione a chave primária como 'connectionid'.

tC0qYzoiulwhYS3pdneXVn1uzu927lCTm2Mz

A seguir, vamos criar a função Lambda de connect. Para criar a função Lambda, acesse Lambda Services e clique em Create function. Selecione Author do zero e forneça o nome 'ChatRoomConnectFunction' e uma função com as permissões necessárias (a função deve ter permissão para obter, colocar e excluir itens do DynamoDB, fazer chamadas de API no gateway de API).

Na função lambda, adicione o seguinte código. Ele adicionará o ID de conexão do dispositivo conectado à tabela do DynamoDB que criamos.

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 seguir, vamos criar também a função lambda de desconexão. Usando as mesmas etapas, crie uma função lambda chamada 'ChatRoomDisconnectFunction'. Adicione o seguinte código à função. Ele código removerá o ID de conexão da tabela do DynamoDB quando um dispositivo for desconectado.

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();
}

Agora, criamos a tabela do DynamoDB e duas funções lambda. Antes de criar a terceira função lambda, vamos voltar novamente à API Gateway e configurar as rotas usando nossas funções lambda já criadas. Primeiro, clique na rota $connect. Como tipo de integração, selecione a função Lambda e selecione ChatRoomConnectFunction.

Jm4gDZE2iTvkM7jjEIbD5dJwxMpfQqNJcxaw

Podemos fazer o mesmo na rota $disconnect, onde a função lambda será ChatRoomDisconnectFunction:

xydcct1RcCF4MMgATbCUE7RoOlVw5Zx-rBzy

Depois de configuramos nossas rotas $connect e $disconnect, podemos realmente testar se nossa API com WebSockets está funcionando. Para fazer isso, devemos primeiro implantar a API. No botão Actions, clique em Deploy API para implantar. Dê um nome como "Teste", pois estamos implantando a API apenas para teste.

Bflmr7zIfPBtVbdcQw-FhWrdWLomxD5wSMIu

Após a implantação, seremos apresentados a dois URLs. O primeiro URL é chamado de WebSocket URL e o segundo é chamado de Connection URL.

81K7bxiFXv-JMJx8b5jScxBmnjkKM4brxY9Q

O WebSocket URL é o URL usado para a conexão por meio de WebSockets à nossa API pelos dispositivos. O segundo URL, o Connection URL, é o URL que usaremos para retornar a chamada aos dispositivos que estão conectados. Como ainda não configuramos o callback para os dispositivos, vamos primeiro testar apenas as rotas $connect e $disconnect.

Para fazer chamadas através de WebSockets, podemos usar a ferramenta wscat. Para instalá-la, precisamos apenas usar o comando npm install -g wscat na linha de comando. Após a instalação, podemos usar a ferramenta usando o comando wscat. Para se conectar à nossa API com WebSockets, use o comando abaixo. Certifique-se de substituir o WebSocket URL pelo URL correto fornecido para você.

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

Quando a conexão for bem-sucedida, uma mensagem "connected" será exibida no terminal. Para verificar se nossa função lambda está funcionando, podemos ir ao DynamoDB e procurar na tabela o ID de conexão do terminal conectado.

uMqXnECECOiDAkC4NcS4OYjWgpvLOsUBNA8z

Como podemos ver acima, também podemos testar a desconexão pressionando CTRL + C, o que simulará uma desconexão.

Agora que testamos nossas duas rotas, vamos dar uma olhada na rota personalizada onMessage. O que essa rota personalizada fará é receber uma mensagem do dispositivo e enviá-la para todos os dispositivos que estão conectados à API com WebSockets. Para conseguir isso, precisaremos de outra função lambda que consultará nossa tabela do DynamoDB, obterá todos os IDs de conexão e enviará a mensagem para eles.

Vamos, primeiro, criar a função lambda da mesma maneira que criamos as outras duas funções lambda. Dê o nome 'ChatRoomOnMessageFunction' para a função e copie o código a seguir para a função.

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();
}

O código acima verificará o DynamoDB para obter todos os registros disponíveis na tabela. Para cada registro, ele postará uma mensagem usando o URL de conexão fornecido na API. No código, nós esperamos que os dispositivos enviem a mensagem no atributo denominado 'message' que a função lambda analisará e enviará para os outros.

Como a API com WebSockets ainda é nova, existem algumas coisas que precisamos fazer manualmente. Crie um arquivo chamado patch.js e adicione o seguinte código a ele:

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;

Peguei o código acima deste artigo. A funcionalidade desse código é criar automaticamente o URL de callback para nossa API e enviar a solicitação POST.

Uq12ZG3KNn38ut5jQQLRjOPebZBuLIxxqesW

Agora que criamos a função lambda, podemos prosseguir e criar nossa rota personalizada no API Gateway. Na nova chave de rota, adicione 'OnMessage' como rota e adicione a rota personalizada. Conforme as configurações foram feitas para outras rotas, adicione nossa função lambda a essa rota customizada e implante a API.

Concluímos nossa API com WebSockets e podemos testar a aplicação por completo. Para testar se o envio de mensagens funciona para vários dispositivos, podemos abrir e conectar usando vários terminais.

Após conectar, emita o seguinte JSON para enviar mensagens:

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

Aqui, a ação é a rota customizada que definimos e a mensagem são os dados que precisam ser enviados para outros dispositivos.

hHo2bGE-lEcSiKIF9CNUpHwXJrKj05h2F5mV

Assim, terminamos nossa aplicação de bate-papo simples usando a API AWS WebSocket. Na verdade, não configuramos a rota $default, que é chamada sempre que nenhuma rota é encontrada. Vou deixar a implementação dessa rota para você. Obrigado pela leitura e até o próximo artigo. 🙂