Kako izraditi aplikacije u stvarnom vremenu pomoću WebSockets s AWS API Gateway i Lambda

Nedavno je AWS najavio pokretanje široko tražene značajke: WebSockets for Amazon API Gateway. Pomoću WebSockets-a možemo stvoriti dvosmjernu komunikacijsku liniju koja se može koristiti u mnogim scenarijima poput aplikacija u stvarnom vremenu. Ovdje se postavlja pitanje: što su aplikacije u stvarnom vremenu? Pa prvo odgovorimo na to pitanje.

Većina trenutno aktivnih aplikacija koristi arhitekturu klijent-poslužitelj. U arhitekturi klijent-poslužitelj, klijent šalje zahtjeve putem Interneta pomoću mrežne komunikacije, a zatim poslužitelj obrađuje taj zahtjev i šalje odgovor natrag klijentu.

Ovdje možete vidjeti da je klijent taj koji započinje komunikaciju s poslužiteljem. Dakle, prvo klijent započinje komunikaciju, a poslužitelj odgovara na zahtjev koji je poslao poslužitelj. Pa što ako poslužitelj želi započeti komunikaciju i gurnuti odgovore, a da ih klijent prethodno nije zatražio? Tu u igru ​​ulaze aplikacije u stvarnom vremenu.

Aplikacije u stvarnom vremenu su aplikacije kod kojih poslužitelj dobiva mogućnost guranja prema klijentima bez da klijent prvo zatraži podatke. Pretpostavimo da imamo aplikaciju za chat gdje dva klijenta za chat mogu komunicirati putem poslužitelja. U ovoj je situaciji rasip ako svi klijenti za chat zahtijevaju podatke od poslužitelja kao i svake sekunde. Učinkovitije je ako poslužitelj šalje podatke aplikacijama za chat klijenta kada primite chat. Ova se funkcija može izvršiti putem aplikacija u stvarnom vremenu.

Amazon je najavio da će podržati WebSockets u API Gatewayu na AWS re: Invent 2018. Kasnije u prosincu pokrenuli su ga u API Gatewayu. Dakle, sada koristeći AWS infrastrukturu u mogućnosti smo stvarati aplikacije u stvarnom vremenu koristeći API Gateway.

U ovom ćemo postu stvoriti jednostavnu aplikaciju za chat pomoću API Gateway WebSockets. Prije nego što započnemo s implementacijom naše aplikacije za chat, postoji nekoliko koncepata koje moramo razumjeti u vezi s aplikacijama u stvarnom vremenu i API pristupnikom.

Koncepti API-ja WebSocket

API WebSocket sastoji se od jedne ili više ruta. Izraz odabir rute je tu kako bi se utvrdilo koji put određeni ulazni zahtjev treba koristiti, koji će se u ulazni zahtjev. Izraz se procjenjuje prema ulaznom zahtjevu kako bi se dobila vrijednost koja odgovara jednoj od vrijednosti routeKey vaše rute . Na primjer, ako naše JSON poruke sadrže radnju poziva svojstva i želite izvršiti različite radnje na temelju ovog svojstva, vaš izraz za odabir rute mogao bi biti ${request.body.action}.

Na primjer: ako vaša JSON poruka izgleda kao "radnja": "onMessage", "message": "Pozdrav svima"}, tada će za ovaj zahtjev biti odabrana ruta onMessage.

Prema zadanim postavkama postoje tri rute koje su već definirane u WebSocket API-ju. Pored dolje navedenih ruta, možemo dodati i prilagođene rute za svoje potrebe.

  • $ default - Koristi se kada izraz za odabir rute stvara vrijednost koja se ne podudara ni s jednim drugim ključem rute u vašim API rutama. To se može koristiti, na primjer, za implementaciju generičkog mehanizma za rukovanje pogreškama.
  • $ connect - Povezana ruta koristi se kada se klijent prvi put poveže s vašim WebSocket API-jem.
  • $ disconnect - pridružena ruta koristi se kada se klijent prekine s vašim API-jem.

Nakon što se uređaj uspješno poveže putem WebSocket API-ja, uređaj će dobiti jedinstveni ID veze. Ovaj će se ID veze zadržati tijekom cijelog trajanja veze. Za slanje poruka natrag na uređaj trebamo upotrijebiti sljedeći POST zahtjev koristeći ID veze.

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

Implementacija aplikacije za chat

Nakon učenja osnovnih pojmova WebSocket API-ja, pogledajmo kako možemo stvoriti aplikaciju u stvarnom vremenu pomoću WebSocket API-ja. U ovom ćemo postu implementirati jednostavnu aplikaciju za chat koristeći WebSocket API, AWS LAmbda i DynamoDB. Sljedeći dijagram prikazuje arhitekturu naše aplikacije u stvarnom vremenu.

U našoj aplikaciji uređaji će biti povezani s API pristupnikom. Kad se uređaj poveže, lambda funkcija spremit će ID veze u DynamoDB tablicu. U slučaju da želimo poslati poruku natrag na uređaj, druga će lambda funkcija dohvatiti ID veze i POST podatke natrag na uređaj pomoću URL-a za povratni poziv.

Izrada API-ja WebSocket

Da bismo stvorili WebSocket API, prvo moramo otići na uslugu Amazon API Gateway pomoću konzole. Tamo odaberite stvoriti novi API. Kliknite WebSocket da biste kreirali WebSocket API, dajte naziv API-ja i naš izraz za odabir rute. U našem slučaju dodajte $ request.body.action kao naš izraz za odabir i pritisnite Create API.

Nakon stvaranja API-ja bit ćemo preusmjereni na stranicu ruta. Ovdje možemo vidjeti već unaprijed definirane tri rute: $ connect, $ disconnect i $ default. Također ćemo stvoriti prilagođenu rutu $ onMessage. U našoj arhitekturi rute $ connect i $ disconnect postižu sljedeće zadatke:

  • $ connect - kada se pozove ova ruta, Lambda funkcija će dodati DynamoDB ID veze povezanog uređaja.
  • $ disconnect - kada se pozove ova ruta, Lambda funkcija će izbrisati ID veze odspojenog uređaja s DynamoDB.
  • onMessage - kad se pozove ova ruta, tijelo poruke bit će poslano na sve uređaje koji su u to vrijeme povezani.

Prije dodavanja rute u skladu s gore navedenim, moramo obaviti četiri zadatka:

  • Stvorite DynamoDB tablicu
  • Stvorite povezivanje lambda funkcije
  • Stvorite odspajanje lambda funkcije
  • Stvorite onMessage lambda funkciju

Prvo, napravimo DynamoDB tablicu. Idite na uslugu DynamoDB i stvorite novu tablicu pod nazivom Chat. Dodajte primarni ključ kao 'connectionid'.

Dalje, kreirajmo funkciju connect Lambda. Da biste stvorili Lambda funkciju, idite na Lambda usluge i pritisnite create function. Odaberite Autor od nule i dajte mu ime kao 'ChatRoomConnectFunction' i ulogu s potrebnim dozvolama. (Uloga bi trebala imati dozvolu za dobivanje, stavljanje i brisanje stavki iz DynamoDB-a, pozivanje API poziva u API gatewayu.)

U kôd lambda funkcije dodajte sljedeći kôd. Ovaj će kôd dodati ID veze povezanog uređaja u DynamoDB tablicu koju smo stvorili.

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

Dalje, kreirajmo i funkciju odspajanja lambda-veze. Koristeći iste korake stvorite novu lambda funkciju s imenom

'ChatRoomDonnectFunction'. U funkciju dodajte sljedeći kod. Ovaj će kôd ukloniti ID veze iz DynamoDB tablice kada se uređaj prekine.

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

Sada smo stvorili DynamoDB tablicu i dvije lambda funkcije. Prije stvaranja treće lambda funkcije, vratimo se opet na API Gateway i konfigurirajte rute pomoću naših stvorenih lambda funkcija. Prvo kliknite $ connect route. Kao vrstu integracije odaberite Lambda funkciju i odaberite ChatRoomConnectionFunction.

To možemo učiniti i na $ disconnect ruti, gdje će lambda funkcija biti 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. :)