Hogyan készítsünk valós idejű alkalmazásokat az AWS API Gateway és a Lambda WebSockets használatával

A közelmúltban az AWS bejelentette egy széles körben kért szolgáltatás elindítását: WebSockets for Amazon API Gateway. A WebSockets segítségével képesek vagyunk létrehozni egy kétirányú kommunikációs vonalat, amely sok esetben alkalmazható, például valós idejű alkalmazásokban. Ez felveti a kérdést: melyek a valós idejű alkalmazások? Tehát először válaszoljunk erre a kérdésre.

A legtöbb jelenleg működő alkalmazás kliens-kiszolgáló architektúrát használ. Kliens-kiszolgáló architektúrában az ügyfél hálózati kommunikáció segítségével küldi az interneten keresztül a kéréseket, majd a szerver feldolgozza a kéréseket, és visszaküldi a választ az ügyfélnek.

Itt láthatja, hogy az ügyfél kezdi a kommunikációt a szerverrel. Tehát először az ügyfél kezdeményezi a kommunikációt, és a szerver válaszol a szerver által küldött kérésre. Tehát mi van akkor, ha a szerver el akarja kezdeni a kommunikációt és a válaszokat úgy akarja leadni, hogy az ügyfél nem kéri őket először? Itt jönnek létre a valós idejű alkalmazások.

A valós idejű alkalmazások olyan alkalmazások, amelyeknél a szerver képes arra, hogy az ügyfelek felé lépjen tovább anélkül, hogy az ügyfél először adatokat kérne. Tegyük fel, hogy van egy csevegőalkalmazásunk, ahol két csevegő kliens kommunikálhat egy szerveren keresztül. Ebben a helyzetben pazarlás, ha az összes csevegő kliens minden másodperchez hasonlóan adatokat kér a szervertől. A hatékonyabb az, ha a szerver adatokat küld az ügyfél csevegőalkalmazásainak, amikor csevegés érkezik. Ez a funkció valós idejű alkalmazásokon keresztül érhető el.

Az Amazon bejelentette, hogy támogatni fogják a WebSocketeket az API Gateway-ben az AWS re: Invent 2018 alkalmazásban. Később decemberben elindították az API Gateway-ben. Tehát az AWS infrastruktúra használatával valós idejű alkalmazásokat hozhatunk létre az API Gateway segítségével.

Ebben a bejegyzésben egy egyszerű csevegőalkalmazást fogunk létrehozni az API Gateway WebSockets használatával. Mielőtt elkezdenénk a csevegőalkalmazásunkat bevezetni, van néhány koncepció, amelyet meg kell értenünk a valós idejű alkalmazások és az API Gateway tekintetében.

WebSocket API fogalmak

A WebSocket API egy vagy több útvonalból áll. A útvonal kiválasztása kifejezést azért van, hogy melyik útvonal egy adott bejövő kérést kell használni, amely biztosítja a bejövő kérést. A kifejezést egy bejövő kérés alapján értékelik, amely olyan értéket állít elő, amely megfelel az útvonal egyik routeKey értékének. Például, ha a JSON-üzeneteink tartalmaznak egy tulajdonsághívási műveletet, és különböző műveleteket szeretne végrehajtani ezen tulajdonság alapján, akkor az útválasztó kifejezés lehet ${request.body.action}.

Például: ha JSON-üzenete a következőnek tűnik: „action”: „onMessage”, „message”: „Hello mindenkinek”}, akkor az onMessage útvonal kerül kiválasztásra ehhez a kéréshez.

Alapértelmezés szerint három útvonal van, amelyeket már meghatároz a WebSocket API. Az alább említett útvonalakon felül egyedi útvonalakat is felvehetünk igényeink szerint.

  • $ alapértelmezett - Akkor használatos, ha az útvonalválasztó kifejezés olyan értéket állít elő, amely nem felel meg az API útvonalainak egyik többi útválasztó kulcsának sem. Ez felhasználható például egy általános hibakezelő mechanizmus megvalósítására.
  • $ connect - A társított útvonal akkor kerül felhasználásra, amikor az ügyfél először csatlakozik a WebSocket API-hoz.
  • $ disconnect - A társított útvonalat akkor használják, amikor az ügyfél leválik az API-ról.

Miután egy eszköz sikeresen csatlakozott a WebSocket API-n keresztül, az eszköz egyedi kapcsolatazonosítóval lesz lefoglalva. Ez a kapcsolatazonosító a kapcsolat teljes élettartama alatt megmarad. Az üzenetek visszaküldéséhez az eszközre a következő POST kérést kell használnunk a kapcsolati azonosító használatával.

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

Csevegőalkalmazás megvalósítása

Miután megismerte a WebSocket API alapfogalmait, nézzük meg, hogyan hozhatunk létre valós idejű alkalmazást a WebSocket API használatával. Ebben a bejegyzésben egy egyszerű csevegőalkalmazást fogunk megvalósítani a WebSocket API, az AWS LAmbda és a DynamoDB használatával. Az alábbi ábra a valós idejű alkalmazásunk architektúráját mutatja be.

Alkalmazásunkban az eszközöket összekapcsolják az API-átjáróval. Amikor egy eszköz csatlakozik, a lambda függvény elmenti a kapcsolat azonosítóját egy DynamoDB táblába. Abban az esetben, ha üzenetet akarunk küldeni az eszközre, egy másik lambda függvény visszahívja a kapcsolat azonosítóját és a POST adatokat az eszközre egy visszahívási URL segítségével.

WebSocket API létrehozása

A WebSocket API létrehozásához először a konzol használatával meg kell látogatnunk az Amazon API Gateway szolgáltatást. Ott válassza az új API létrehozását. Kattintson a WebSocket gombra a WebSocket API létrehozásához, adja meg az API nevét és az Útválasztás kifejezést. Esetünkben adjuk meg a $ request.body.action elemet kiválasztási kifejezésünkként, és nyomjuk meg az API létrehozását.

Az API létrehozása után átirányítunk az útvonalak oldalra. Itt láthatunk már előre definiált három útvonalat: $ connect, $ disconnect és $ default. Létrehozunk egy egyedi $ onMessage útvonalat is. Architektúránkban a $ connect és a $ disconnect útvonalak a következő feladatokat érik el:

  • $ connect - ha ezt az útvonalat meghívják, egy Lambda funkció hozzáadja a csatlakoztatott eszköz csatlakozási azonosítóját a DynamoDB-hez.
  • $ disconnect - ha ezt az útvonalat meghívják, a Lambda funkció törli a leválasztott eszköz csatlakozási azonosítóját a DynamoDB-ből.
  • onMessage - ha ezt az útvonalat meghívja, az üzenet törzse el lesz küldve az összes eszközhöz, amely akkor csatlakozik.

Mielőtt hozzáadnánk az útvonalat a fentiek szerint, négy feladatot kell elvégeznünk:

  • Hozzon létre egy DynamoDB táblázatot
  • Hozzon létre connect lambda függvényt
  • Hozza létre a lambda leválasztásának funkcióját
  • Hozzon létre onMessage lambda függvényt

Először hozzuk létre a DynamoDB táblázatot. Nyissa meg a DynamoDB szolgáltatást, és hozzon létre egy új táblát, amelynek neve Chat. Adja hozzá az elsődleges kulcsot „connectionid” néven.

Ezután hozzuk létre a connect Lambda függvényt. A Lambda funkció létrehozásához lépjen a Lambda szolgáltatások oldalra, és kattintson a Létrehozás funkcióra. Válassza ki a Szerzőt a semmiből, és adja meg a nevet „ChatRoomConnectFunction” néven, valamint egy szerepkört a szükséges engedélyekkel. (A szerepkörnek engedéllyel kell rendelkeznie elemek megszerzésére, elhelyezésére és törlésére a DynamoDB-ből, API-hívások hívására az API-átjáróban.)

A lambda függvény kódjához adja hozzá a következő kódot. Ez a kód hozzáadja a csatlakoztatott eszköz csatlakozási azonosítóját az általunk létrehozott DynamoDB táblához.

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

Ezután hozzuk létre a disconnect lambda függvényt is. Ugyanezekkel a lépésekkel hozzon létre egy új lambda függvényt

'ChatRoomDonnectFunction'. Adja hozzá a következő kódot a függvényhez. Ez a kód eltávolítja a kapcsolat azonosítóját a DynamoDB táblából, amikor egy eszköz megszakad.

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

Most létrehoztuk a DynamoDB táblázatot és két lambda függvényt. A harmadik lambda függvény létrehozása előtt térjünk vissza az API Gateway-hez, és állítsuk be az útvonalakat a létrehozott lambda-függvényeinkkel. Először kattintson a $ connect route elemre. Integrációs típusként válassza a Lambda funkciót, majd válassza a ChatRoomConnectionFunction lehetőséget.

Ugyanezt megtehetjük a $ disconnect útvonalon is, ahol a lambda függvény ChatRoomDisconnectionFunction lesz:

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. :)