В статье речь пойдет о критичном по времени взаимодействии. Передача данных от клиента на сервер с помощью простых 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.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Sebastian Krempel: Communicate real-time in OutSystems

Предыдущая статьяКак Ktlint облегчает жизнь разработчикам