Πώς να δημιουργήσετε εφαρμογές σε πραγματικό χρόνο χρησιμοποιώντας WebSockets με AWS API Gateway και Lambda

Πρόσφατα, η AWS ανακοίνωσε την κυκλοφορία μιας ευρείας ζήτησης: WebSockets για το Amazon API Gateway. Με το WebSockets, είμαστε σε θέση να δημιουργήσουμε μια αμφίδρομη γραμμή επικοινωνίας που μπορεί να χρησιμοποιηθεί σε πολλά σενάρια όπως εφαρμογές σε πραγματικό χρόνο. Αυτό θέτει το ερώτημα: ποιες είναι οι εφαρμογές σε πραγματικό χρόνο; Ας απαντήσουμε λοιπόν πρώτα σε αυτήν την ερώτηση.

Οι περισσότερες από τις εφαρμογές που λειτουργούν αυτήν τη στιγμή χρησιμοποιούν αρχιτεκτονική πελάτη-διακομιστή. Στην αρχιτεκτονική πελάτη-διακομιστή, ο πελάτης στέλνει τα αιτήματα μέσω Διαδικτύου χρησιμοποιώντας επικοινωνία δικτύου και στη συνέχεια ο διακομιστής επεξεργάζεται το αίτημα και στέλνει την απάντηση πίσω στον πελάτη.

Εδώ μπορείτε να δείτε ότι ο πελάτης ξεκινά την επικοινωνία με τον διακομιστή. Έτσι πρώτα ο πελάτης ξεκινά την επικοινωνία και ο διακομιστής ανταποκρίνεται στο αίτημα που έστειλε ο διακομιστής. Τι γίνεται λοιπόν εάν ο διακομιστής θέλει να ξεκινήσει την επικοινωνία και να προωθήσει τις απαντήσεις χωρίς να ζητήσει πρώτα από τον πελάτη; Εκεί μπαίνουν οι εφαρμογές σε πραγματικό χρόνο.

Οι εφαρμογές σε πραγματικό χρόνο είναι εφαρμογές όπου ο διακομιστής έχει τη δυνατότητα να σπρώχνει στους πελάτες χωρίς ο πελάτης να ζητήσει πρώτα δεδομένα. Ας υποθέσουμε ότι έχουμε μια εφαρμογή συνομιλίας όπου δύο πελάτες συνομιλίας μπορούν να επικοινωνούν μέσω ενός διακομιστή. Σε αυτήν την περίπτωση, είναι άχρηστο εάν όλοι οι πελάτες συνομιλίας ζητούν δεδομένα από το διακομιστή όπως κάθε δευτερόλεπτο. Αυτό που είναι πιο αποτελεσματικό είναι εάν ο διακομιστής στέλνει δεδομένα σε εφαρμογές συνομιλίας πελάτη κατά τη λήψη μιας συνομιλίας. Αυτή η λειτουργικότητα μπορεί να γίνει μέσω εφαρμογών σε πραγματικό χρόνο.

Η Amazon ανακοίνωσε ότι πρόκειται να υποστηρίξει το WebSockets στο API Gateway στο AWS re: Invent 2018. Αργότερα τον Δεκέμβριο, το κυκλοφόρησαν στο API Gateway. Τώρα λοιπόν χρησιμοποιώντας την υποδομή AWS είμαστε σε θέση να δημιουργήσουμε εφαρμογές σε πραγματικό χρόνο χρησιμοποιώντας το API Gateway.

Σε αυτήν την ανάρτηση, πρόκειται να δημιουργήσουμε μια απλή εφαρμογή συνομιλίας χρησιμοποιώντας το API Gateway WebSockets. Πριν αρχίσουμε να εφαρμόζουμε την εφαρμογή συνομιλίας μας, υπάρχουν ορισμένες έννοιες που πρέπει να κατανοήσουμε σχετικά με τις εφαρμογές σε πραγματικό χρόνο και το API Gateway.

Έννοιες API WebSocket

Ένα WebSocket API αποτελείται από μία ή περισσότερες διαδρομές. Υπάρχει μια έκφραση επιλογής διαδρομής για να προσδιοριστεί ποια διαδρομή θα πρέπει να χρησιμοποιήσει ένα συγκεκριμένο εισερχόμενο αίτημα, το οποίο θα παρέχεται στο εισερχόμενο αίτημα. Η έκφραση αξιολογείται σε σχέση με ένα εισερχόμενο αίτημα για την παραγωγή μιας τιμής που αντιστοιχεί σε μία από τις τιμές της διαδρομής Routey . Για παράδειγμα, εάν τα μηνύματά μας JSON περιέχουν μια ενέργεια κλήσης ιδιοκτησίας και θέλετε να εκτελέσετε διαφορετικές ενέργειες βάσει αυτής της ιδιότητας, η έκφραση της επιλογής της διαδρομής σας μπορεί να είναι ${request.body.action}.

Για παράδειγμα: εάν το μήνυμά σας JSON μοιάζει με {"action": "onMessage", "message": "Hello all"}, τότε η διαδρομή onMessage θα επιλεγεί για αυτό το αίτημα.

Από προεπιλογή, υπάρχουν τρεις διαδρομές που έχουν ήδη οριστεί στο WebSocket API. Εκτός από τις παρακάτω διαδρομές, μπορούμε να προσθέσουμε προσαρμοσμένες διαδρομές για τις ανάγκες μας.

  • $ default - Χρησιμοποιείται όταν η έκφραση επιλογής διαδρομής παράγει μια τιμή που δεν ταιριάζει με κανένα από τα άλλα κλειδιά διαδρομής στις διαδρομές API σας. Αυτό μπορεί να χρησιμοποιηθεί, για παράδειγμα, για την εφαρμογή ενός γενικού μηχανισμού χειρισμού σφαλμάτων.
  • $ connect - Η σχετική διαδρομή χρησιμοποιείται όταν ένας πελάτης συνδέεται για πρώτη φορά στο WebSocket API σας.
  • $ disconnect - Η σχετική διαδρομή χρησιμοποιείται όταν ένας πελάτης αποσυνδέεται από το API σας.

Μόλις μια συσκευή συνδεθεί επιτυχώς μέσω του WebSocket API, η συσκευή θα εκχωρηθεί με ένα μοναδικό αναγνωριστικό σύνδεσης. Αυτό το αναγνωριστικό σύνδεσης θα διατηρηθεί καθ 'όλη τη διάρκεια της ζωής, εάν η σύνδεση. Για την αποστολή μηνυμάτων πίσω στη συσκευή πρέπει να χρησιμοποιήσουμε το ακόλουθο αίτημα POST χρησιμοποιώντας το αναγνωριστικό σύνδεσης.

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

Εφαρμογή εφαρμογής συνομιλίας

Αφού μάθουμε τις βασικές έννοιες του WebSocket API, ας δούμε πώς μπορούμε να δημιουργήσουμε μια εφαρμογή σε πραγματικό χρόνο χρησιμοποιώντας το WebSocket API. Σε αυτήν την ανάρτηση, θα εφαρμόσουμε μια απλή εφαρμογή συνομιλίας χρησιμοποιώντας το WebSocket API, το AWS LAmbda και το DynamoDB. Το παρακάτω διάγραμμα δείχνει την αρχιτεκτονική της εφαρμογής μας σε πραγματικό χρόνο.

Στην εφαρμογή μας, οι συσκευές θα συνδεθούν με το API Gateway. Όταν μια συσκευή συνδεθεί, μια συνάρτηση lambda θα αποθηκεύσει το αναγνωριστικό σύνδεσης σε έναν πίνακα DynamoDB. Σε μια περίπτωση όπου θέλουμε να στείλουμε ένα μήνυμα πίσω στη συσκευή, μια άλλη λειτουργία lambda θα ανακτήσει το αναγνωριστικό σύνδεσης και τα δεδομένα POST στη συσκευή χρησιμοποιώντας μια διεύθυνση URL επιστροφής κλήσης.

Δημιουργία API WebSocket

Για να δημιουργήσουμε το API WebSocket, πρέπει πρώτα να μεταβούμε στην υπηρεσία Amazon API Gateway χρησιμοποιώντας την κονσόλα. Εκεί επιλέξτε να δημιουργήσετε νέο API. Κάντε κλικ στο WebSocket για να δημιουργήσετε ένα WebSocket API, δώστε ένα όνομα API και την έκφραση επιλογής διαδρομής. Στην περίπτωσή μας προσθέστε $ request.body.action ως έκφραση επιλογής μας και πατήστε Δημιουργία API.

Μετά τη δημιουργία του API θα ανακατευθυνθούμε στη σελίδα διαδρομών. Εδώ βλέπουμε ήδη προκαθορισμένες τρεις διαδρομές: $ connect, $ disconnect και $ default. Θα δημιουργήσουμε επίσης μια προσαρμοσμένη διαδρομή $ onMessage. Στην αρχιτεκτονική μας, οι διαδρομές $ connect και $ αποσύνδεση επιτυγχάνουν τις ακόλουθες εργασίες:

  • $ connect - όταν καλείται αυτή η διαδρομή, μια συνάρτηση Lambda θα προσθέσει το αναγνωριστικό σύνδεσης της συνδεδεμένης συσκευής στο DynamoDB.
  • $ disconnect - όταν καλείται αυτή η διαδρομή, μια συνάρτηση Lambda θα διαγράψει το αναγνωριστικό σύνδεσης της αποσυνδεδεμένης συσκευής από το DynamoDB.
  • onMessage - όταν καλείται αυτή η διαδρομή, το σώμα του μηνύματος θα σταλεί σε όλες τις συσκευές που είναι συνδεδεμένες εκείνη τη στιγμή.

Πριν προσθέσουμε τη διαδρομή σύμφωνα με τα παραπάνω, πρέπει να κάνουμε τέσσερις εργασίες:

  • Δημιουργήστε έναν πίνακα DynamoDB
  • Δημιουργήστε τη λειτουργία σύνδεσης lambda
  • Δημιουργήστε τη λειτουργία αποσύνδεσης λάμδα
  • Δημιουργήστε τη συνάρτηση onMessage lambda

Αρχικά, ας δημιουργήσουμε τον πίνακα DynamoDB. Μεταβείτε στην υπηρεσία DynamoDB και δημιουργήστε έναν νέο πίνακα που ονομάζεται Chat. Προσθέστε το πρωτεύον κλειδί ως «σύνδεση».

Στη συνέχεια, ας δημιουργήσουμε τη λειτουργία σύνδεσης Lambda. Για να δημιουργήσετε τη λειτουργία Lambda, μεταβείτε στις υπηρεσίες Lambda και κάντε κλικ στη λειτουργία δημιουργία. Επιλέξτε Συντάκτης από το μηδέν και δώστε το όνομα ως 'ChatRoomConnectFunction' και έναν ρόλο με τα απαραίτητα δικαιώματα. (Ο ρόλος θα πρέπει να έχει την άδεια λήψης, τοποθέτησης και διαγραφής στοιχείων από το DynamoDB, κλήση κλήσεων API στην πύλη API.)

Στον κώδικα της συνάρτησης λάμδα προσθέστε τον ακόλουθο κωδικό. Αυτός ο κωδικός θα προσθέσει το αναγνωριστικό σύνδεσης της συνδεδεμένης συσκευής στον πίνακα DynamoDB που έχουμε δημιουργήσει.

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

Στη συνέχεια, ας δημιουργήσουμε επίσης τη λειτουργία αποσύνδεσης λάμδα. Χρησιμοποιώντας τα ίδια βήματα δημιουργήστε μια νέα συνάρτηση λάμδα που ονομάζεται

‘ChatRoomDonnectFunction’. Add the following code to the function. This code will remove the connection id from the DynamoDB table when a device gets disconnected.

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

Now we have created the DynamoDB table and two lambda functions. Before creating the third lambda function, let us go back again to API Gateway and configure the routes using our created lambda functions. First, click on $connect route. As integration type, select Lambda function and select the ChatRoomConnectionFunction.

We can do the same on $disconnect route as well where the lambda function will be 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.

Αυτό είναι για την απλή εφαρμογή συνομιλίας μας χρησιμοποιώντας το AWS WebSocket API. Στην πραγματικότητα δεν έχουμε ρυθμίσει τη διαδρομή $ defalut που καλείται σε κάθε περίπτωση όπου δεν υπάρχει διαδρομή. Θα σας αφήσω την εφαρμογή αυτής της διαδρομής σε εσάς. Σας ευχαριστώ και θα σας δούμε σε άλλη ανάρτηση. :)