كيفية إنشاء تطبيقات في الوقت الفعلي باستخدام WebSockets مع AWS API Gateway و Lambda

أعلنت AWS مؤخرًا عن إطلاق ميزة مطلوبة على نطاق واسع: WebSockets for Amazon API Gateway. باستخدام WebSockets ، يمكننا إنشاء خط اتصال ثنائي الاتجاه يمكن استخدامه في العديد من السيناريوهات مثل تطبيقات الوقت الفعلي. هذا يثير السؤال: ما هي تطبيقات الوقت الفعلي؟ لذلك دعونا نجيب أولاً على هذا السؤال.

تستخدم معظم التطبيقات التي تعمل حاليًا بنية خادم العميل. في بنية خادم العميل ، يرسل العميل الطلبات عبر الإنترنت باستخدام اتصال الشبكة ثم يعالج الخادم الذي يطلب الاستجابة ويرسلها مرة أخرى إلى العميل.

هنا يمكنك أن ترى أن العميل هو الذي يبدأ الاتصال بالخادم. لذلك يبدأ العميل الاتصال أولاً ويستجيب الخادم للطلب الذي أرسله الخادم. إذن ماذا لو أراد الخادم بدء الاتصال ودفع الردود دون أن يطلبها العميل أولاً؟ هذا هو المكان الذي تدخل فيه تطبيقات الوقت الفعلي.

تطبيقات الوقت الفعلي هي تطبيقات يحصل فيها الخادم على القدرة على الدفع إلى العملاء دون أن يطلب العميل البيانات أولاً. افترض أن لدينا تطبيق دردشة حيث يمكن لاثنين من عملاء الدردشة التواصل عبر خادم. في هذه الحالة ، يعد طلب جميع عملاء الدردشة بيانات من الخادم مثل كل ثانية مضيعة. والأكثر فاعلية هو ما إذا كان الخادم يرسل البيانات إلى تطبيقات الدردشة العميلة عند تلقي الدردشة. يمكن تنفيذ هذه الوظيفة من خلال تطبيقات الوقت الفعلي.

أعلنت أمازون أنها ستدعم WebSockets في API Gateway في AWS re: Invent 2018. في وقت لاحق من ديسمبر ، أطلقوها في بوابة API. لذلك ، باستخدام البنية التحتية لـ AWS ، أصبح بإمكاننا إنشاء تطبيقات في الوقت الفعلي باستخدام بوابة API.

في هذا المنشور ، سننشئ تطبيق دردشة بسيطًا باستخدام API Gateway WebSockets. قبل أن نبدأ في تنفيذ تطبيق الدردشة الخاص بنا ، هناك بعض المفاهيم التي نحتاج إلى فهمها فيما يتعلق بالتطبيقات في الوقت الفعلي وبوابة API.

مفاهيم WebSocket API

تتكون واجهة برمجة تطبيقات WebSocket من مسار واحد أو أكثر. يوجد تعبير اختيار المسار لتحديد المسار الذي يجب أن يستخدمه طلب وارد معين ، والذي سيتم توفيره في الطلب الوارد. يتم تقييم التعبير مقابل طلب وارد لإنتاج قيمة تتوافق مع إحدى قيم مفتاح المسار الخاص بك . على سبيل المثال ، إذا كانت رسائل JSON الخاصة بنا تحتوي على إجراء استدعاء خاصية ، وتريد تنفيذ إجراءات مختلفة بناءً على هذه الخاصية ، فقد يكون تعبير اختيار المسار الخاص بك ${request.body.action}.

على سبيل المثال: إذا كانت رسالة JSON تبدو مثل {"إجراء": "onMessage" ، "message": "مرحبًا بالجميع"} ، فسيتم اختيار مسار onMessage لهذا الطلب.

بشكل افتراضي ، هناك ثلاثة مسارات محددة بالفعل في WebSocket API. بالإضافة إلى المسارات المذكورة أدناه ، يمكننا إضافة مسارات مخصصة لاحتياجاتنا.

  • افتراضي - يُستخدم عندما ينتج عن تعبير تحديد المسار قيمة لا تتطابق مع أي من مفاتيح المسار الأخرى في مسارات واجهة برمجة التطبيقات الخاصة بك. يمكن استخدام هذا ، على سبيل المثال ، لتنفيذ آلية معالجة الأخطاء العامة.
  • $ connect - يتم استخدام المسار المرتبط عندما يتصل العميل لأول مرة بواجهة برمجة تطبيقات WebSocket الخاصة بك.
  • قطع الاتصال $ - يتم استخدام المسار المرتبط عند قطع اتصال العميل بواجهة برمجة التطبيقات الخاصة بك.

بمجرد توصيل الجهاز بنجاح من خلال 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. عند توصيل أحد الأجهزة ، ستحفظ وظيفة lambda معرف الاتصال في جدول DynamoDB. في الحالة التي نريد فيها إرسال رسالة مرة أخرى إلى الجهاز ، ستقوم وظيفة lambda أخرى باسترداد معرف الاتصال وبيانات POST مرة أخرى إلى الجهاز باستخدام عنوان URL لمعاودة الاتصال.

إنشاء WebSocket API

من أجل إنشاء WebSocket API ، نحتاج أولاً إلى الانتقال إلى خدمة Amazon API Gateway باستخدام وحدة التحكم. هناك اختر إنشاء واجهة برمجة تطبيقات جديدة. انقر فوق WebSocket لإنشاء WebSocket API ، وأعطي اسم API وتعبير اختيار المسار الخاص بنا. في حالتنا أضف $ request.body.action كتعبير اختيار واضغط على Create API.

بعد إنشاء API ، سيتم إعادة توجيهنا إلى صفحة المسارات. هنا يمكننا أن نرى ثلاثة مسارات محددة مسبقًا: $ connect و $ disconnect و $ default. سننشئ أيضًا مسارًا مخصصًا $ onMessage. في بنيتنا ، تحقق مسارات $ connect و $ disconnect المهام التالية:

  • $ connect - عند استدعاء هذا المسار ، ستضيف وظيفة Lambda معرف الاتصال الخاص بالجهاز المتصل إلى DynamoDB.
  • قطع الاتصال $ - عند استدعاء هذا المسار ، ستحذف وظيفة Lambda معرف الاتصال الخاص بالجهاز غير المتصل من DynamoDB.
  • onMessage - عند استدعاء هذا المسار ، سيتم إرسال نص الرسالة إلى جميع الأجهزة المتصلة في ذلك الوقت.

قبل إضافة المسار وفقًا لما سبق ، نحتاج إلى القيام بأربع مهام:

  • قم بإنشاء جدول DynamoDB
  • إنشاء وظيفة ربط لامدا
  • إنشاء وظيفة فصل لامدا
  • إنشاء وظيفة onMessage lambda

أولاً ، دعونا ننشئ جدول DynamoDB. انتقل إلى خدمة DynamoDB وقم بإنشاء جدول جديد يسمى الدردشة. أضف المفتاح الأساسي كـ "connectid".

بعد ذلك ، لنقم بإنشاء وظيفة اتصال Lambda. لإنشاء وظيفة Lambda ، انتقل إلى خدمات Lambda وانقر فوق إنشاء وظيفة. حدد المؤلف من البداية وقم بتسمية الاسم كـ "ChatRoomConnectFunction" ودور مع الأذونات اللازمة. (يجب أن يحصل الدور على إذن للحصول على العناصر ووضعها وحذفها من DynamoDB واستدعاء استدعاءات واجهة برمجة التطبيقات في بوابة واجهة برمجة التطبيقات).

أضف الكود التالي في كود دالة لامدا. سيضيف هذا الرمز معرف الاتصال الخاص بالجهاز المتصل إلى جدول 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();}

بعد ذلك ، دعنا ننشئ وظيفة فصل lambda أيضًا. باستخدام نفس الخطوات ، قم بإنشاء دالة lambda جديدة باسم

"ChatRoomDonnectFunction". أضف التعليمات البرمجية التالية إلى الوظيفة. سيؤدي هذا الرمز إلى إزالة معرف الاتصال من جدول DynamoDB عند فصل الجهاز.

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

لقد أنشأنا الآن جدول DynamoDB ودالتين من وظائف lambda. قبل إنشاء وظيفة lambda الثالثة ، دعنا نعود مرة أخرى إلى بوابة API وتكوين المسارات باستخدام وظائف lambda التي أنشأناها. أولاً ، انقر فوق $ connect route. كنوع تكامل ، حدد وظيفة Lambda وحدد وظيفة ChatRoomConnectionFunction.

يمكننا فعل الشيء نفسه على مسار قطع الاتصال $ وكذلك حيث ستكون وظيفة lambda هي 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 $ الذي يتم استدعاؤه في كل مناسبة لا يوجد فيها مسار. سأترك تنفيذ هذا الطريق لك. شكرا لك ونراكم في منشور آخر. :)