Советы по обеспечению подключения и улучшению UX с данными в реальном времени

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

Не являясь целью статьи, стоит отметить, что в зависимости от сценария WebSockets может быть излишним и другие альтернативы например, события на стороне сервера (SSE) или опрос может быть достаточно хорошим решением для большинства распространенных случаев.

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

Ниже мы постараемся осветить следующие темы:

  1. Прилипание сеанса
  2. Кэш подписок
  3. Видимость приложения
  4. Статус сети
  5. Конфигурация сокета

1. Липкая сессия

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

Обычно это достигается путем создания файла cookie или отслеживания их IP-данных.

Липкость сама по себе зависит только от ваших технических характеристик и характеристик продукта

Вы планируете масштабировать свою систему?

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

Нужно ли предоставлять резервное соединение?

WebSocket — это открытое TCP-соединение, при котором сервер и клиент могут отправлять и получать данные в любой момент времени, пока соединение открыто.

Когда клиент запрашивает обновление соединения до WebSockets (также известное как рукопожатие), цель, возвращающая код состояния HTTP 101 для принятия обновления соединения, является целью, используемой в соединении WebSocket. После обновления привязка на основе файлов cookie использоваться не будет.

Соединения WebSocket по своей природе липкие

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

Рукопожатие клиента, подключающееся к пространству имен /prices, будет выглядеть так:

 GET /prices HTTP/1.1
 Host: example.com
 Upgrade: websocket
 Connection: Upgrade
 Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
 Origin: https://example.com

И ответ от сервера:

 HTTP/1.1 101 Switching Protocols
 Upgrade: websocket
 Connection: Upgrade
 Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Sec-WebSocket-Key – это случайный 16-байтовый одноразовый номер (в кодировке base64), который используется на сервере вместе с предопределенным GUID для получения Sec-WebSocket-Accept. заголовок, чтобы клиент удостоверился, что сервер поддерживает WS и не интерпретирует данные как HTTP-запрос

Протокол WebSocket — это независимый протокол на основе TCP. Его единственная связь с HTTP заключается в том, что его рукопожатие интерпретируется HTTP-серверами как запрос на обновление.

Однако механизмы сокетов, такие как Socket.io и SockJS, по умолчанию устанавливают соединение с транспортом HTTP с длительным опросом, чтобы предотвратить возможные проблемы с установлением соединения WebSocket из-за корпоративных прокси-серверов, персональных брандмауэров и т. д. .

Долгий опрос (или «опрос») в основном состоит из последовательных HTTP-запросов:

  • Длительные GET запросы: для получения данных с сервера (эквивалентно, когда сервер отправляет)
  • Кратковременные POST запросы: для отправки данных на сервер (эквивалентно, когда клиент отправляет)

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

В начале соединения (1) сервер отправляет идентификатор сеанса sid среди другой информации:

Request
GET https://example.com/prices/?EIO=3&transport=polling&t=NymEVAg
Response
{
  "sid": "aBM2xUVW5zx0WC3ZAAQE",
  "upgrades": ["websocket"],
  "pingInterval": 25000,
  "pingTimeout": 20000
}

Этот идентификатор сеанса должен быть включен во все последующие HTTP-запросы в качестве параметра запроса как часть протокола, пока не будет выполнено обновление до WS:

(3) POST https://example.com/prices/EIO=3&transport=polling&t=NymEVR0&sid=aBM2xUVW5zx0WC3ZAAQE
(2) GET
http://example.com/prices/EIO=3&transport=polling&t=NymEVR4&sid=aBM2xUVW5zx0WC3ZAAQE

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

В зависимости от движка вы можете отключить этот запасной вариант. Например, с помощью Socket.io вы можете избежать длительного опроса через параметр транспорт при инициации соединения: transport = ['websocket']

Предоставляете ли вы персонализированные данные для каждого пользователя?

По сравнению с http, протокол WebSocket не имеет метаданных, поэтому в случае потери соединения балансировщик нагрузки должен реализовать балансировку нагрузки на уровне TCP.

После потери соединения между балансировщиком нагрузки и браузером произойдет согласование http-обновления.

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

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

Даже предоставляя настраиваемые обновления, вам не понадобится привязка сеанса, если вы убедитесь, что можете обслуживать правильный канал во всех своих экземплярах.

2. Кэш подписок

В зависимости от характера вашего приложения вам необходимо обратить особое внимание на то, как оно обрабатывает оперативные обновления.

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

Однако при работе с более сложными приложениями следует заранее задать себе несколько вопросов:

  • Сколько подписчиков требуется моему приложению? Являются ли они фиксированными или, с другой стороны, создаются динамически на основе взаимодействия с пользователем?
  • Как они распределяются? Используют ли они данные из одного и того же источника? Распределяются ли они на уровне приложения, страницы или компонента?
  • Нужно ли мне использовать какой-либо канал в фоновом режиме при входе в систему независимо от навигации?

В соответствии с ответами обдумать следующие моменты.

Отслеживание подписок для данной комнаты

Например, смоделируйте объект подписки в соответствии с вашими требованиями:

import { Subscription } from "rxjs"
class SourceSub {
 // Subscriber -> Subscription
 private subscribers: Map<string, Subscription> = new Map()
}

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

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

Кэшировать последнюю ленту

Учитывая комнату, кешируем последнее полученное значение:

class SourceSub {
 private subscribers: Map<string, Subscription> = new Map()
 private lastValue? //Your feed model
}

Это поможет улучшить взаимодействие с пользователем при отображении нового компонента со значением по умолчанию, пока он не получит новое обновление фида.

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

Кэшировать подписки в вашем приложении

Кэшируйте все активные подписки в приложении в любой момент времени:

class MyApp {
 // Room -> SourceSub
 private appSubCache: Map<string, SourceSub> = new Map()
}

Структура кеша в соответствии с вашими требованиями и конфигурацией сокета сервера

Каждый раз, когда монтируется новый компонент, отображающий оперативные данные, вам необходимо отслеживать его и создавать подходящую подписку. Изображениекомпонента header, впервые присоединяющегося к каналу today:

const channel = 'today'
const subscriber = 'header'
const source$ = my_feed_stream
// Component subscription receiving live updates
const headerSubscription = source$.subscribe(update)
// New source subscription for the first time
const roomSubscription = new SourceSub()
roomSubscription.add(subscriber, headerSubscription)
// Store it
this.appSubCache.add(channel, roomSubscription)
// Join room
join(channel)

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

Отменить подписку

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

Допустим, теперь пользователь переходит на новую страницу без компонента header:

const channel = 'today'
const subscriber = 'header'
const subscription = this.appSubCache.get(channel)
// Unsubscribe and delete 
subscription.remove(subscriber)
if (subscription.hasNoSubscribers()) {
  // Remove SourceSub *
  this.appSubCache.delete(channel)
  // Leave room
  leave(channel)
}

Это предотвратит утечку памяти и улучшит обработку без игл обновлений с сервера.

* Имейте в виду, что вы можете сохранить экземпляр SourceSub для данной комнаты без подписчиков, но с lastValue, поэтому для следующего нового подписчика всегда будет доступно значение по умолчанию для отображения.

3. Видимость приложения

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

Пользовательские агенты обычно имеют определенные политики для смягчения влияния фоновых или скрытых вкладок на производительность.

Например, большинство браузеров перестают отправлять requestAnimationFrameобратные вызовы, а такие таймеры, как setTimeout, регулируются для повышения производительности и времени автономной работы.

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

Тем не менее, такие процессы, как воспроизведение звука, индексированная база данных и соединения в реальном времени (WebSocket и WebRTC) не подвержены этому регулированию, чтобы избежать закрытия соединений тайм-аут.

Имея это в виду, вы можете использовать API Видимость страницы для веб-приложений:

function getPageVisibility() {
  let hidden, visibilityChange
  if (typeof document.hidden !== "undefined") {
    hidden = "hidden"
    visibilityChange = "visibilitychange"
  } else if (typeof document.msHidden !== "undefined") {
    hidden = "msHidden"
    visibilityChange = "msvisibilitychange"
  } else if (typeof document.webkitHidden !== "undefined") {
    hidden = "webkitHidden"
    visibilityChange = "webkitvisibilitychange"
  }
  return { hidden, visibilityChange }
}

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

document.addEventListener(visibilityChange, () => {
 if(!document[hidden]) {
   // Ensure connectivity 
 }
}, false)

Слушатель будет срабатывать при переключении вкладок или сворачивании браузера.

Page Visibility API широко поддерживается, но те браузеры, которые его не поддерживают, могут вместо этого использовать альтернативные события onblur и onfocus.

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

В качестве альтернативы вы также можете попробовать использовать события pagehide и pageshow, но с учетом их ограничений при запуске событий.

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

На собственных мобильных платформах и в зависимости от ОС фоновое приложение или его подключение могут быть прекращены системой для экономии ресурсов.

Например, приложения на основе Cordova или Capacitor могут использовать события пауза и возобновление для управления видимостью приложения.

В приложениях для Android вы можете отслеживать это с помощью методов жизненного цикла активности onPause и onResume, а в iOS вы можете использовать комбинацию между передним планом уведомление и applicationState.

Видео с WWDC 2020 объясняет основные причины, по которым ваше приложение может быть убито

4. Статус сети

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

В этом случае вы можете прослушивать онлайн/офлайн события, чтобы определить, когда приложение онлайн или нет, и действовать соответствующим образом:

window.addEventListener('online',  event => {
  // Ensure connectivity
})

Конкретно не связано, но если вы хотите собрать статистику о поведении пользователей, вы можете использовать сетевые прослушиватели или API видимости страницы вместе с методом sendBeacon:

window.addEventListener('offline',  event => {
  navigator.sendBeacon('/info', data)
})

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

5. Конфигурация сокета

Как уже отмечалось, WebSocket — это одно открытое TCP-соединение между клиентом и сервером.

Связь

По умолчанию механизмы сокетов, такие как SocketIO, используют мультиплексирование, что означает, что у вас может быть несколько каналов связи (так называемое пространство имен) по одному общему соединению.

Когда это соединение будет потеряно, ваше приложение перестанет получать новые каналы по всем каналам.

Однако установка флага конфигурации forceNew: true при инициации подключения изменит это, таким образом получив независимое подключение к сокету для каждого пространства имен (в этом случае вы увидите в devTools как многие записи WebSockets как отдельные соединения сокетов)

Если вам интересно, вы можете записывать и загружать сетевую активность более низкого уровня через chrome://net-export и https://netlog-viewer.appspot.com соответственно.

Соответствие версии

Следя за механизмами сокетов, всегда следите за тем, чтобы клиент и сервер использовали одну и ту же версию. Такие протоколы определяют формат пакетов, которыми обмениваются обе стороны, поэтому необходимо совпадать, чтобы понимать друг друга.

Такие инструменты, как PieSocket, помогут вам протестировать соединение с сервером с родным WS или с другими версиями socketIO.

Обработчики событий и переподключения

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

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

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

Заключение

Подводя итог, мы только что выделили несколько моментов, которые следует учитывать при работе с WebSockets.

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

Наконец, говоря о связи в реальном времени, если вы также хотите углубиться в другие технологии потоковой передачи, такие как WebRTC, ознакомьтесь с моими предыдущими статьями! 😁





Повышение уровня кодирования

Thanks for being a part of our community! More content in the Level Up Coding publication.
Follow: Twitter, LinkedIn, Newsletter

Level Up меняет подход к подбору персонала ➡️ Присоединяйтесь к нашему коллективу талантов