WebSockets — классная и впечатляющая часть современных приложений, но их может быть сложно реализовать.

Пару лет назад я работал над проектом по внедрению уведомлений в реальном времени в свое веб-приложение. Я был в восторге от идеи «в реальном времени» и сразу понял, что у меня будет шанс реализовать WebSockets.

Я знал, что WebSockets делает, но я не знал, что они были — то есть я знал, что вы можете отправлять сообщения с сервера в браузер, но понятия не имел как. Я не знал ничего, кроме того факта, что существуют «соединения», которые можно использовать для передачи данных как в серверную часть, так и из нее.

Я отправился строить то, что, как я думал, будет двухдневной задачей. Что возможно могло пойти не так?

Затем я погрузился в нисходящую спираль сложности, которая заставила меня переосмыслить профессию инженера-программиста. Давай поговорим об этом.

Структура API веб-сокета

Я исхожу из фона REST. Конечные точки имеют пути на основе ресурсов с намерением, которое показывает, какой метод http вы используете (GET = загрузить данные, POST = создать данные, PUT = обновить данные и т. д.).

Первое, что я увидел в документации AWS API Gateway, были эти странные маршруты $connect и $disconnect. По соглашению об именах я предполагал, что делают эти маршруты, но не знал, что с ними делать.

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

В конце концов я обнаружил, что с AWS API Gateway соединения управляются самой службой, но вы (разработчик) несете ответственность за отслеживание того, кто подключен и какую информацию они получают. Я также узнал, что данные не просто свободно перемещаются туда и обратно.

Для взаимодействия, идущего от клиента к серверу, вы должны определить свои собственные маршруты и указать их на резервные вычислительные ресурсы. Для каждого маршрута требовался маршрут API Gateway V2, интеграция API Gateway V2, функция Lambda и разрешение функции Lambda. определено в моей инфраструктуре как код, что составляло около 50 строк на маршрут.

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

Несоответствие между клиентом и сервером и сервером и клиентом поставило меня в тупик. Один был очень жестким и структурированным, в то время как другой был рыхлым. Это не совсем похоже на способ создания масштабируемого, удобного в сопровождении программного обеспечения.

Управление подключением

Как я уже говорил ранее, API Gateway управляет поддержанием соединений для вас, но вы несете ответственность за выяснение того, какие данные отправлять в какое соединение. Возьмем пример:

Представьте, что наш пользователь, Мэллори, хочет получать уведомления, когда билеты на Тейлор Свифт, Адель или Эда Ширана станут доступны. Когда она подключается к нашему сайту продавца билетов, мы сохраняем 4 записи в нашей базе данных:

  • Одна запись, идентифицирующая соединение и пользовательские метаданные.
  • Одна запись для каждого исполнителя, о котором она хочет получать уведомления

Для записей исполнителя pk – это идентификатор ее подключения, а sk – запись о подписке. Мы добавляем имя артиста в качестве GSI, поэтому, когда мы получаем событие, указывающее, что билеты на Эда Ширана поступили в продажу, мы можем немедленно уведомить все соединения, подписанные на него.

Чтобы уведомить подписчиков с бессерверной серверной частью AWS, мы запускали функцию Lambda в событии EventBridge, говорящую, у какого исполнителя есть доступные билеты. Эта функция будет запрашивать GSI artist в DynamoDB, чтобы найти все подключения, подписанные на входящего исполнителя. Затем мы перебирали каждую запись, публикуя информацию о билетах для подключенных пользователей. Это много работы!

Когда пользователь отключается, мы можем запросить в базе данных все записи с pk, содержащим идентификатор соединения, и удалить их. На случай, если мы пропустим событие отключения от шлюза API, мы установим время жизни (TTL) для записей о подключении на 24 часа (или любое другое значение, соответствующее вашему варианту использования), чтобы удалить их автоматически.

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

Безопасность

Я родом из GovTech фона. Приложение не является безопасным, пока оно не станет чрезмерно безопасным. Поэтому, когда я узнал, что единственным маршрутом в WebSocket API, который поддерживает аутентификацию, является $connect, я был немного ошеломлен. Как только соединение установлено, оно может свободно вызывать любой маршрут, который он хочет, без передачи заголовка аутентификации или любой другой формы учетных данных.

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

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

Клиентские SDK

Кажется, людям нравится SocketIO. Он имеет более 4 миллионов еженедельных загрузок на npm и, возможно, является одним из лучших способов подключения к WebSocket. Но то, что это популярно, не означает, что это легко.

По какой-то причине я изо всех сил пытался заставить его работать с API Gateway. Что-то с протоколом WebSocket (wss вместо https) и тем, как AWS настроил API, просто не ладилось.

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

Более простой способ

С Momento Topics все сложные части WebSockets абстрагируются. Нет структуры API для построения. Подписчики могут подключаться и регистрироваться для получения обновлений определенных каналов с помощью одного вызова API:

await topicClient.subscribe('websocket', 'mychannel', {
 onItem: (data) => { handleItem(data.valueString()); },
 onError: (err) => { console.error(err); }
});

Чтобы опубликовать на канал, вызов еще проще:

await topicClient.publish('websocket', 'mychannel', JSON.stringify({ detail }));

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

Как и в случае с другими бессерверными сервисами, Momento Topics ставит безопасность на первое место, но также предоставляет вам гибкие возможности для ограничения доступа. Благодаря тонкому контролю доступа вы можете настроить свои токены API так, чтобы их область действия была как можно более узкой. Примером политики доступа может быть:

const tokenScope = {
 permissions: [
   {
     role: 'subscribeonly',
     cache: 'websocket',
     topic: 'mychannel'
   }
 ]
};

Токен API, созданный с этим набором разрешений, может подписываться только на тему mychannel в кэше websocket . Если кто-то перехватит токен и попытается опубликовать данные или подписаться на другую тему, он получит ошибку авторизации.

У Momento есть множество SDK, с которыми вы можете интегрироваться. Для браузеров можно использовать Web SDK. Для серверной разработки доступен сервис Topics для TypeScript/Javascript, Python и Go, с поддержкой .NET, Java, Elixir, PHP, Ruby. , а скоро появится Rust.

В чем подвох?

Надеюсь, это звучит слишком хорошо, чтобы быть правдой. Это сделало меня в первую очередь. Черт возьми, это все еще так. Но подвоха нет. Миссия Momento — предоставить лучший в своем классе опыт разработчиков для своих бессерверных сервисов и максимально снять с разработчиков бремя.

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

Цены просты: 0,50 долларов США за ГБ входящей и исходящей передачи данных с бессрочным уровнем бесплатного пользования объемом 5 ГБ. Нет причин не попробовать!

Ищете примеры? Оцените это полнофункциональное приложение для чата, созданное с помощью Topics в Next.js. Вы также можете попробовать нашу незавершенную игру Acorn Hunt, созданную как на Momento Cache, так и на Topics.

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

Удачного кодирования!