В статье речь пойдет о критичном по времени взаимодействии. Передача данных от клиента на сервер с помощью простых HTTP-запросов — не проблема. В обратном же направлении — совсем другая история.
Что понадобится
- Учетная запись на Amazon Web Services, бесплатного уровня для первого года достаточно.
- Учетная запись OutSystems 11 — достаточно бесплатной персональной среды — или доступ к арендованному облаку разработчика OutSystems.
- OutSystems Service Studio или ODC Studio.
- Коннектор AWS AppSync, доступный бесплатно в O11 или ODC.
Проблема
В веб-приложениях клиент обычно взаимодействует с сервером посредством веб-сервисов — REST или SOAP.
В пользовательском интерфейсе информация обновляется OutSystems так называемыми экранными заполнителями или экшенами выборки данных. В фоновом режиме это просто веб-сервисы REST, основанные также на HTTP, протоколе передачи гипертекста Hyper Text Transfer Protocol.
HTTP разработан на заре интернета для передачи — с веб-сервера клиенту — документов на языке гипертекстовой разметки HTML вместе с соответствующими ресурсами, например изображениями. Таким образом строится однонаправленное взаимодействие без состояний:
Взаимодействие всегда начинается с запроса от клиента и завершается ответом с сервера. Это прекрасно вписывается в дизайн интернета, где серверами по общеизвестному адресу предоставляются информация и услуги.
С другой стороны, клиенты обычно находятся за брандмауэром и точно не должны вызываться извне. Пока событие, которым инициируется обновление, всегда происходит на стороне клиента, например нажатие кнопки, этого достаточно.
Если же это событие случается на стороне сервера — например, из-за того, что в системе обмена сообщениями должно быть доставлено новое сообщение, — запрос должен отправляться с сервера клиенту, а это не предусмотрено и не поддерживается.
Три подхода
В интернете имеются обратные веб-взаимодействия, но как они осуществляются?
Опрашивание
Самое простое и очевидное решение — регулярная проверка клиентом, доступны ли на сервере новые данные. Называется этот процесс «Опрашивание» и реализуется небольшим кодом JavaScript с периодическими вызовами экшена выборки данных.
Недостаток такого подхода заключается в том, что данные запрашиваются клиентом, даже если новые данные недоступны. Это чревато повышением нагрузки на сервер, что нивелируется лишь увеличением интервала опроса. Однако увеличивается и время между появлением обновления данных и его получением клиентом.
Следовательно, это решение не для сценариев взаимодействия в режиме реального времени.
Длительный запрос / потоковая передача
Проблема опрашивания заключается в том, что соединение между клиентом и сервером закрывается после каждого ответа.
Что, если бы соединение оставалось открытым? Когда новые данные были бы доступны, сервером бы отправлялся ответ. Это метод длительных запросов.
Хотя для HTTP-соединения имеются тайм-ауты, в которые запрос необходимо выполнить, ответ с большими объемами данных передается дольше.
Кроме того, в протоколе определен статус ожидания, применяемый сервером для информирования клиента о том, что результата ждать чуть дольше.
Поэтому клиентом выполняется такой длительный запрос, а сервером соединение поддерживается открытым: периодически отправляется статус ожидания либо выполняется потоковая передача следующего обновления данных.
Если соединение прервано, клиент просто инициирует следующий запрос.
Недостаток этого подхода заключается в том, что по умолчанию каждым браузером на один сервер одновременно отправляется, а веб-сервером единовременно обрабатывается только ограниченное количество запросов. Чтобы это не сказывалось на производительности других запросов, для такого решения предусматриваются отдельные веб-серверы.
Кроме того, при открытом соединении невозможно двунаправленное взаимодействие.
Веб-сокет
В сценариях, где необходимо двунаправленное взаимодействие в реальном времени, идеальны соединения по веб-сокету. Они основаны не на простом принципе запроса/ответа, а аналогичны телефонному звонку: хотя клиент по-прежнему устанавливает соединение с сервером, оно остается открытым, пока одна из сторон не отсоединится. Пока соединение открыто, обе стороны могут отправлять сообщения друг другу.
Несмотря на задержку передачи, так обеспечивается взаимодействие в реальном времени. Хотя в дополнение к серверу сайта нужен минимум один отдельный сервер с веб-сокетами, соединения в реальном времени поддерживаются исключительно этими серверами, на веб-сервере(-ах) это не сказывается.
Решение
Похоже, веб-сокет — оптимальный вариант, не так ли?
У всех трех подходов имеются разумные основания:
- Опрашивание очень просто реализовать, это экономичное решение для длительного интервала опроса или краткосрочного применения всего с несколькими клиентами одновременно.
- Длительные запросы хороши для меньшего числа одновременных соединений и, в отличие от веб-сокетов, работают с обычными HTTP-веб-серверами. Подобно опрашиванию, это решение реализуется с OutSystems, хотя поддерживать очень длинные запросы сложновато и рекомендуется применять пользовательское расширение кода.
- Недостатки первых двух подходов преодолеваются веб-сокетами, которыми обеспечивается свободный двунаправленный обмен сообщениями.
В OutSystems нативно реализовать сервер с веб-сокетами не получится, понадобится помощь извне. Но где ее взять, если такого сервера нет?
В облаке, конечно. В одном из недавних проектов я наткнулся на API-шлюз в Amazon Web Services и заметил, что там поддерживаются и соединения по веб-сокету.
Реализовать простую систему обмена сообщениями с соответствующей логикой в AWS Lambda несложно. Для классического шаблона «Издатель-подписчик» понадобится принимать и распределять сообщения, управлять подписчиками, позволяя им также подписываться только на те или иные события.
Похоже, работы по реализации здесь немало. Притом вовсе не быстрой и легкой, как с low code.
Мы воспользуемся low code: не будем писать много текстового кода, а настроим функциональность, где это возможно. Тут пригодится AWS AppSync. Это управляемый сервис, при помощи которого разработчики создают API-интерфейсы GraphQL и шаблона «Издатель-подписчик» для подключения приложений к источникам данных и событиям реального времени.
В основу принципа работы AppSync положен GraphQL. В отличие от традиционных REST API, где структура ответов определяется сервером, в языке запросов GraphQL клиентами точно указываются нужные им данные. Этим уменьшается избыточная или недостаточная выборка данных. Возможности API, включая типы и структуры запрашиваемых данных, определяются строгой системой типов.
Кроме того, в GraphQL одним запросом получают несколько ресурсов, чем также сокращается количество необходимых сетевых запросов.
AWS AppSync
Сконфигурируем сервер с веб-сокетами в AWS AppSync:
- Зайдя в AWS, переходим в консоль AppSync. Она находится в меню Services («Сервисы») категории Front-end Web & Mobile («Фронтенд-веб и мобильные») или вводом
AppSync
в поле поиска. - Выбираем в верхней строке корректный регион, где развернуть сервис, и нажимаем Create API («Создать API»).
- В качестве типа API выбираем GraphQL API и в разделе GraphQL API Data Source («Источник данных GraphQL API») выбираем Create a real-time API («Создайте API реального времени»). Нажимаем Next («Далее»).
- Вводим информативное название для API, например
OS-AWS-AppSync-Connector-Demo-API
. Оставляем поле для галочки Use private API features («Использовать функционал приватного API») неотмеченным и нажимаем Next («Далее»). - В AWS AppSync клиенты подписываются на каналы с помощью простой конфигурации «Издатель-подписчик». Конфигурируем GraphQL API с нуля, поэтому убираем галочку и нажимаем Next («Далее»).
- На последней странице мастера проверяем все настройки и, нажимая Create API («Создать API»), завершаем процесс создания.
Схема
Определим для API-интерфейса AppSync схему в GraphQL. Помимо типов объектов, которыми определяется структура обрабатываемой информации, в схему включается три специальных типа:
- запрос для определения того, как клиентами запрашивается информация;
- мутация для указания на то, как информация изменяется;
- подписка для описания событий, к которым подключается подписчик.
Воспользуемся очень простой схемой:
type Message {
message: String
}
type Mutation {
sendMessage(msg: String): Message!
}
type Query {
dummy(param: String): String
}
type Subscription {
subscribeToMessages: Message
@aws_subscribe(mutations: ["sendMessage"])
}
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
- Сначала определяется тип объекта
Message
, это базовое сообщение для взаимодействия в реальном времени. Здесь только одно поле типаString
, где содержится текст сообщения. - Ниже располагается специальный тип
Mutation
, в котором клиентами API посредствомsendMessage
отправляются сообщения с передачей строкиString
в качестве параметраmsg
. Затем из API возвращается измененныйMessage
. - Специальный тип
Query
обязателен в AppSync. Чтобы отправить сообщение и отразить эту мутацию как подписку, здесь определяетсяdummy
. - Интересной частью схемы является специальный тип
Subscription
, где клиенты с помощьюsubscribeToMessages
подписываются на сообщения, отправляемые с мутациейsendMessage
, и активно получают уведомления объектомMessage
, передаваемым через соединение по веб-сокету. - В конце мы сообщаем AppSync, какие типы использовать в качестве
query
,mutation
иsubscription
.
Прежде чем продолжить, сохраняем схему: нажимаем Save Schema.
Источники данных
Одно из преимуществ GraphQL заключается в том, что информация получается из нескольких источников. Сделаем эти источники известными для AppSync.
В нашем случае сообщения не запрашиваются или сохраняются в базе данных, а только распределяются по подписке. Поэтому реальный источник данных в фоновом режиме не нужен. Для этой цели в AppSync имеется тип источника данных NONE.
Создадим такой источник данных:
- Переходим в Data sources («Источники данных») на панели навигации и нажимаем Create data source («Создать источник данных»).
- Вводим название источника данных, например
NoneDataSource
, выбираем тип источника данных NONE и нажимаем Create («Создать»).
Распознаватели
Поля данных внутри схемы AppSync подключаются к источникам данных распознавателями.
Добавим в мутацию sendMessage
функционал, прикрепив к ней распознаватель:
- Возвращаемся к схеме Schema.
- Находим поле
sendMessage
в типеMutation
и нажимаем кнопку Attach («Прикрепить») в столбце Resolver («Распознаватель»). - Выбираем тип распознавателя Unit resolver и в Additional setting («Дополнительный параметр») выбираем Velocity Template Language («Язык шаблонов скорости»).
- Источником данных выбираем созданный ранее
NoneDataSource
и нажимаем Create. - Конфигурируем шаблон отображения запросов, скопировав такой фрагмент кода:
{
"version": "2017-02-28",
"payload": {
"message": $util.toJson($context.arguments.msg)
}
}
- Для этого шаблона используем:
$util.toJson($context.result)
- Нажимаем Save («Сохранить»).
Тестирование API-интерфейса AppSync
Чтобы протестировать сконфигурированный сервер с веб-сокетами, переходим к Queries в меню и запускаем такой запрос:
subscription MySubscription {
subscribeToMessages {
message
}
}
Во втором окне браузера также открываем страницу Queries API-интерфейса AppSync и запускаем такой запрос:
mutation MyMutation {
sendMessage(msg: "Hello World!") {
message
}
}
В итоге видим объект message
, возвращаемый из sendMessage
:
{
"data": {
"sendMessage": {
"message": "Hello World!"
}
}
}
А также сообщение, полученное в первом окне с запущенной подпиской:
{
"data": {
"subscribeToMessages": {
"message": "Hello World!"
}
}
}
Параметры подключения
Чтобы использовать только что созданный API AppSync, нужны параметры подключения со страницы настроек Settings в меню.
Поэтому копируем и сохраняем на будущее такую информацию:
- Конечная точка GraphQL — это URL-адрес конечной точки REST для запрашивания запросов и мутаций GraphQL.
- Конечная точка реального времени — это URL-адрес для отправки запросов подписки и получения сообщений подписки через соединение по веб-сокету.
- API-ключ — требуется аутентификации на обеих конечных точках.
Приложение OutSystems
Запустив сервер с веб-сокетами, задействуем его в приложении OutSystems, где веб-сокеты нативно не поддерживаются. Зато этот фреймворк расширяется пользовательским кодом. Здесь нужна функциональность на стороне клиента, поэтому обращаемся к JavaScript. В документации AppSync содержится все, что необходимо реализовать для взаимодействия в реальном времени через соединение по веб-сокету: рукопожатие для инициирования подключения, как запускать и прекращать подписку, отправлять и получать сообщения.
На этой схеме показан весь процесс взаимодействия:
Получатель веб-сокета реализовывать не нужно, достаточно бесплатно установить компонент коннектора AWS AppSync. Он также доступен для ODC — облака разработчика OutSystems. Просто находим AWS AppSync Connector
от Telelink Business Services EAD и устанавливаем в свое арендованное облако.
Клиентская реализация
Но вернемся к исходной проблеме. Чтобы запустить обработку на стороне клиента, требуется сообщение от сервера. То есть для клиента нужно событийно-ориентированное программирование.
В OutSystems оно реализуется событиями веб-блоков. В коннекторе содержится виджет AWSAppSyncClient, в котором инкапсулируется соединение по веб-сокету и контролируются все активные подписки. С виджетом связаны экшены клиента, ими вызываются команды для клиентской реализации.
Виджету указываются параметры подключения для AppSync API. Для этого применяются такие входные параметры:
- SubscriberId обязательный для идентификации экземпляра, называемого подписчиком. Позже он передается экшенам клиента. Используется любой текст, лишь бы он был уникальным. Рекомендую назвать виджет и применять его идентификатор в качестве SubscriberId.
- RealtimeHost необходимо задавать в названии хоста конечной точки AWS RealTime без протокола
wss://
и пути/graphql
.
Формат:[api-host].appsync-realtime-api.[region].amazonaws.com
. - GraphQLHost необходимо задавать в названии хоста конечной точки AWS GraphQL без протокола
https://
и пути/graphql
.
Формат:[api-host].appsync-api.[region].amazonaws.com
. - ApiKey — это API-ключ аутентификации AWS AppSync.
И дополнительный входной параметр:
- IsAutoReconnect для автоматического переподключения, по умолчанию —
True
. Когда он активирован, при потере соединения компонент автоматически переподключается к веб-сокету и переподписывается на все активные подписки.
Во время инициализации веб-сокета, не поддерживаемого браузером, виджетом запускается событие
- OnWebSocketUnsupported, которое при взаимодействии в реальном времени должно быть обработано. В противном случае событие игнорируется.
После того как виджеты инициализированы, клиентским экшеном
- AWSAppSyncClient_SendGraphQLRequest — поскольку им используется конечная точка GraphQL по HTTPS — запрашиваются запросы или мутации данных GraphQL.
Следовательно, соединение по веб-сокету требуется только для подписок и получения сообщений в реальном времени, оно не устанавливается автоматически. Клиентским экшеном
- AWSAppSyncClient_Connect соединение открывается, и рукопожатие инициализируется.
Когда состояние соединения меняется, виджетом запускается событие
- OnConnection с новым состоянием, передаваемым в текстовом параметре:
Установив соединение, можно подписаться или отказаться от определенных в схеме API AppSync подписок, используя такие клиентские экшены:
- AWSAppSyncClient_StartSubscription для запуска новой подписки на основе запроса GraphQL и переменных, указываемых как входные параметры;
- AWSAppSyncClient_StopSubscription для прекращения подписки, идентифицируемой по указываемому во входных параметрах SubscriptionId.
Когда состояние подписки меняется, виджетом запускается
- OnSubscription с SubscriptionId и новым состоянием, передаваемыми как текстовые параметры:
Когда при минимум одной активной подписке получается новое сообщение, виджетом запускается событие
- OnMessage с SubscriptionId и полученными данными в виде текста JSON.
И последнее событие, запускаемое виджетом:
- OnError, им передаются источник ошибки —
appsync
илиwebsocket
— и дополнительные сведения в виде текста JSON.
Напомним, сейчас AWS AppSync бесплатен только для бесплатного уровня в течение первого года. Но и здесь Amazon взимается плата при достижении определенного количества запрашиваний запросов и мутаций, доставленных сообщений в реальном времени и продолжительности соединения.
Поэтому для большинства пользователей — в их же интересах — закрывать подключения, которые больше не требуются. Клиентским экшеном
- AWSAppSyncClient_Disconnect прекращаются все активные подписки, после чего соединение по веб-сокету закрывается.
Вызовом клиентского экшена
- AWSAppSyncClient_GetDetail в любой момент проверяются текущее состояние соединения, параметр автоматического переподключения и перечень подписок.
Последним клиентским экшеном
- AWSAppSyncClient_SendRealtimeMessage в конечную точку реального времени через соединение веб-сокета отправляется пользовательское сообщение.
Серверная реализация
Недостающая часть пазла — серверная сторона. Ведь с сервера еще нужно активно отправлять клиенту сообщение. При тестировании API-интерфейса AppSync мы уже отправляли сообщение подписанным клиентам, запустив мутацию GraphQL. Для этого в коннекторе имеется серверный экшен
- AWSAppSync_SendGraphQLRequest, которым в конечную точку AWS AppSync GraphQL по HTTPS отправляется запрос.
Посмотрим все это в действии
Вы только что узнали все, что нужно знать для реализации взаимодействия в реальном времени с AWS AppSync в OutSystems. А может быть, пока читали статью, уже создали приложение? Если нет, загружайте демоприложение и пробуйте в нем настроенный вами выше API AppSync:
Вот демо для O11.
Чтобы найти вариант ODC, на портале ODC вводим в поиске AWS AppSync Connector Demo от Telelink Business Services EAD.
Читайте также:
- Terraform: реализация технологии “инфраструктура как код”
- Пакет Lambda-слоев AWS для Python
- Настройка сервера AWS Aurora PostgreSQL и мониторинг его производительности
Читайте нас в Telegram, VK и Дзен
Перевод статьи Sebastian Krempel: Communicate real-time in OutSystems