Недавно AWS объявила о запуске широко востребованной функции: WebSockets для Amazon API Gateway. С помощью WebSockets мы можем создать двустороннюю линию связи, которую можно использовать во многих сценариях, например в приложениях реального времени. Возникает вопрос: что такое приложения реального времени? Итак, давайте сначала ответим на этот вопрос.

Большинство работающих в настоящее время приложений используют архитектуру клиент-сервер. В архитектуре клиент-сервер клиент отправляет запросы через Интернет, используя сетевое взаимодействие, а затем сервер обрабатывает этот запрос и отправляет ответ обратно клиенту.

Здесь вы можете видеть, что именно клиент начинает связь с сервером. Итак, сначала клиент инициирует связь, а сервер отвечает на запрос, отправленный сервером. Так что, если сервер хочет начать обмен данными и отправлять ответы без предварительного запроса клиента? Вот где в игру вступают приложения реального времени.

Приложения реального времени - это приложения, в которых сервер получает возможность отправлять запросы клиентам без предварительного запроса данных клиентом. Предположим, у нас есть приложение чата, в котором два клиента чата могут общаться через сервер. В этой ситуации будет напрасной тратой, если все клиенты чата будут запрашивать данные с сервера каждую секунду. Более эффективно, если сервер отправляет данные клиентским чат-приложениям при получении чата. Эта функция может быть реализована с помощью приложений реального времени.

Amazon объявила, что они собираются поддерживать WebSockets в API Gateway на AWS re: Invent 2018. Позже в декабре они запустили его в API Gateway. Итак, теперь, используя инфраструктуру AWS, мы можем создавать приложения в реальном времени с помощью API Gateway.

В этом посте мы собираемся создать простое чат-приложение с использованием API Gateway WebSockets. Прежде чем мы начнем реализовывать наше чат-приложение, нам необходимо понять некоторые концепции, касающиеся приложений реального времени и API-шлюза.

Концепции API WebSocket

API WebSocket состоит из одного или нескольких маршрутов. Выражение выбора маршрута предназначено для определения того, какой маршрут должен использовать конкретный входящий запрос, который будет предоставлен во входящем запросе. Выражение сравнивается с входящим запросом, чтобы получить значение, соответствующее одному из значений routeKey вашего маршрута. Например, если наши сообщения JSON содержат действие вызова свойства, и вы хотите выполнять различные действия на основе этого свойства, выражение выбора маршрута может быть ${request.body.action}.

Например: если ваше сообщение JSON выглядит как {«действие»: «onMessage», «сообщение»: «Всем привет»}, то для этого запроса будет выбран маршрут onMessage.

По умолчанию в WebSocket API уже определены три маршрута. В дополнение к маршрутам, указанным ниже, мы можем добавить собственные маршруты для наших нужд.

  • $ default - используется, когда выражение выбора маршрута дает значение, не совпадающее ни с одним из других ключей маршрута в ваших маршрутах API. Это можно использовать, например, для реализации универсального механизма обработки ошибок.
  • $ connect - связанный маршрут используется, когда клиент впервые подключается к вашему WebSocket API.
  • $ disconnect - связанный маршрут используется, когда клиент отключается от вашего API.

Как только устройство будет успешно подключено через WebSocket API, устройству будет присвоен уникальный идентификатор подключения. Этот идентификатор соединения будет сохраняться в течение всего срока службы, если соединение. Чтобы отправлять сообщения обратно на устройство, нам нужно использовать следующий запрос POST с использованием идентификатора подключения.

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

Реализация приложения чата

Изучив основные концепции WebSocket API, давайте посмотрим, как мы можем создать приложение реального времени с помощью WebSocket API. В этом посте мы собираемся реализовать простое чат-приложение с использованием WebSocket API, AWS LAmbda и DynamoDB. На следующей диаграмме показана архитектура нашего приложения реального времени.

В нашем приложении устройства будут подключены к API Gateway. Когда устройство подключается, лямбда-функция сохраняет идентификатор подключения в таблице DynamoDB. В случае, когда мы хотим отправить сообщение обратно на устройство, другая лямбда-функция получит идентификатор соединения и данные POST обратно на устройство, используя URL-адрес обратного вызова.

Создание WebSocket API

Чтобы создать WebSocket API, нам нужно сначала перейти к сервису Amazon API Gateway с помощью консоли. Там выберите создание нового API. Щелкните WebSocket, чтобы создать API WebSocket, укажите имя API и выражение выбора маршрута. В нашем случае добавьте $ request.body.action в качестве выражения выбора и нажмите Create API.

После создания API мы будем перенаправлены на страницу маршрутов. Здесь мы видим уже предопределенные три маршрута: $ connect, $ disconnect и $ default. Мы также создадим собственный маршрут $ onMessage. В нашей архитектуре маршруты $ connect и $ disconnect решают следующие задачи:

  • $ connect - при вызове этого маршрута функция Lambda добавит идентификатор подключения подключенного устройства в DynamoDB.
  • $ disconnect - при вызове этого маршрута функция Lambda удалит идентификатор подключения отключенного устройства из DynamoDB.
  • onMessage - при вызове этого маршрута тело сообщения будет отправлено на все устройства, подключенные в данный момент.

Перед добавлением маршрута в соответствии с вышеизложенным нам необходимо выполнить четыре задачи:

  • Создайте таблицу DynamoDB
  • Создать функцию подключения к лямбда
  • Создать функцию отключения лямбда
  • Создать лямбда-функцию onMessage

Сначала давайте создадим таблицу DynamoDB. Перейдите в сервис DynamoDB и создайте новую таблицу под названием Chat. Добавьте первичный ключ как «connectionid».

Затем давайте создадим функцию connect Lambda. Чтобы создать функцию Lambda, перейдите в Lambda services и нажмите кнопку create function. Выберите «Автор» с нуля и дайте имя «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». Добавьте в функцию следующий код. Этот код удалит идентификатор подключения из таблицы 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 и две лямбда-функции. Перед созданием третьей лямбда-функции давайте снова вернемся к API Gateway и настроим маршруты с помощью наших созданных лямбда-функций. Сначала щелкните по маршруту $ connect. В качестве типа интеграции выберите Lambda-функцию и выберите ChatRoomConnectionFunction.

Мы можем сделать то же самое и с маршрутом $ disconnect, где лямбда-функцией будет ChatRoomDisconnectionFunction:

Теперь, когда мы настроили наши маршруты $ connect и $ disconnect, мы можем фактически проверить, работает ли наш API WebSocket. Для этого мы должны сначала развернуть API. На кнопке Действия нажмите Развернуть API для развертывания. Дайте сценическое имя, например Test, поскольку мы развертываем API только для тестирования.

После развертывания нам будут представлены два URL-адреса. Первый URL-адрес называется URL-адресом WebSocket, а второй - URL-адресом подключения.

URL-адрес WebSocket - это URL-адрес, который используется для подключения устройств через WebSockets к нашему API. И второй URL-адрес, который является URL-адресом подключения, является URL-адресом, который мы будем использовать для обратного вызова на подключенные устройства. Поскольку мы еще не настроили обратный вызов устройств, давайте сначала протестируем только маршруты $ connect и $ disconnect.

Для звонка через WebSockets мы можем использовать инструмент wscat. Чтобы установить его, нам нужно просто ввести команду npm install -g wscat в командной строке. После установки мы можем использовать инструмент с помощью команды wscat. Чтобы подключиться к нашему WebSocket API, выполните следующую команду. Обязательно замените URL-адрес WebSocket на правильный URL-адрес, предоставленный вам.

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

После успешного подключения на терминале отобразится сообщение о подключении. Чтобы проверить, работает ли наша лямбда-функция, мы можем перейти в DynamoDB и посмотреть в таблице идентификатор подключения подключенного терминала.

Как и выше, мы также можем проверить отключение, нажав CTRL + C, что будет имитировать отключение.

Теперь, когда мы протестировали наши два маршрута, давайте рассмотрим настраиваемый маршрут onMessage. Этот настраиваемый маршрут будет получать сообщение от устройства и отправлять его на все устройства, подключенные к WebSocket API. Для этого нам понадобится еще одна лямбда-функция, которая будет запрашивать нашу таблицу DynamoDB, получать все идентификаторы соединений и отправлять им сообщение.

Давайте сначала создадим лямбда-функцию так же, как мы создали две другие лямбда-функции. Назовите лямбда-функцию ChatRoomOnMessageFunction и скопируйте следующий код в код функции.

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

Приведенный выше код просканирует DynamoDB, чтобы получить все доступные записи в таблице. Для каждой записи он отправит сообщение POST, используя URL-адрес подключения, предоставленный нам в API. В коде мы ожидаем, что устройства отправят сообщение в атрибуте с именем «message», которое лямбда-функция проанализирует и отправит другим.

Поскольку API WebSockets все еще является новым, некоторые вещи нам нужно делать вручную. Создайте новый файл с именем patch.js и добавьте в него следующий код.

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;

Приведенный выше код я взял из этой статьи. Функциональность этого кода заключается в автоматическом создании URL-адреса обратного вызова для нашего API и отправке запроса POST.

Теперь, когда мы создали лямбда-функцию, мы можем продолжить и создать собственный маршрут в API Gateway. В New Route Key добавьте «OnMessage» в качестве маршрута и добавьте собственный маршрут. Поскольку конфигурации были выполнены для других маршрутов, добавьте нашу лямбда-функцию к этому настраиваемому маршруту и ​​разверните API.

Теперь мы завершили наш WebSocket API и можем полностью протестировать приложение. Чтобы проверить, работает ли отправка сообщений для нескольких устройств, мы можем открывать и подключаться с помощью нескольких терминалов.

После подключения введите следующий JSON для отправки сообщений:

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

Здесь действие - это настроенный нами маршрут, а сообщение - это данные, которые необходимо отправить на другие устройства.

Это все, что касается нашего простого чат-приложения, использующего AWS WebSocket API. На самом деле мы не настроили маршрут $ defalut, который вызывается каждый раз, когда маршрут не найден. Я оставлю реализацию этого маршрута на ваше усмотрение. Спасибо и увидимся в другом посте. :)