Real-time в вебе
29 апреля 2021 г. • ☕️ 5 мин.
Веб-приложения изначально проектировались, как простая клиент-серверная модель, где клиент посылал HTTP
запрос и получал ответ от сервера. Такая модель еще может называться, синхронной или блокирующей, так как за запросом всегда следует ответ и это не работает в обратную сторону.
Запрос-ответ
Сценарий запрос-ответ всегда выглядит одинаково.
- Клиент делает запрос на сервер
- Сервер подготавливает данные для ответа, в то время, как клиент ожидает ответа (заблокирован)
- Сервер отправляет ответ на запрос
Real-time
Со временем к веб-приложениям появлялось все больше требований, в том числе возможность обновлять данные в режиме реального времени. Для примера это могли быть:
- Чат
- Трейдинговые площадки
- Игры
Как же можно решить данные задачи в вебе?
Polling (Short-polling)
Polling
подразумевает опрос сервера на наличие новых данных с определенным интервалом.
- Для опроса используется
XMLHttpRequest
илиfetch
- Клиент посылает запрос каждые
n
секунд - Сервер отвечает как обычно
Этот подход рабочий, хотя его основным недостатоком является чрезмерное расходование ресурсов сервера.
Для каждого запроса нужно
- Установить
TCP
соединение (исключениеKeep-Alive
) и передать заголовки запроса - Сделать запрос в базу данных
- Вернуть данные (чаще всего устаревшие, так как новых еще не было) и закрыть соединение
async function subscribe() {
// этот запрос выполняется быстро и повторяется каждые 5 секунд
const response = await fetch("/messages")
handleResponse(response)
setTimeout(subscribe, 5000)
}
В HTTP/1.1
появилась поддержка заголовков Keep-Alive
, которая позволяет держать TCP
соединение открытым, для передачи по нему последующих HTTP
запросов.
Это был не настоящий Real-time
, так как при появлении новых данных, они приходили не раньше, чем следующий “тик” интервала.
Long-Polling
Long-Polling
тоже подразумевает опрос, но с отличиями.
- Для опроса используется
XMLHttpRequest
илиfetch
- Клиент посылает запрос сразу же, как только приходит ответ на предыдущий
- Сервер поддерживает запрос в состоянии ожидания, до тех пор пока не появятся новые данные и только потом отвечает
async function subscribe() {
// этот запрос может висеть очень долго в ожидании ответа
const response = await fetch("/messages")
handleResponse(response)
subscribe()
}
Это уже настоящий Real-time
. Здесь мы не делаем лишних запросов, но не каждый веб-сервер сможет поддерживать большое количество запросов в памяти одновременно.
Для каждого запроса нужно
- Установить
TCP
соединение (исключениеKeep-Alive
) и передать заголовки запроса - Поддерживать запрос в ожидании, до тех пор пока не появятся новые данные (либо запрос не разорвется клиентом/таймаутом)
- Вернуть новые данные и закрыть соединение
Такой подход хорошо работает для серверов, работающих по асинхронной модели (как Node.js
), но достаточно сложен для масштабирования (поэтому и мало популярен) на серверах, которые работают по модели процесс/поток на запрос.
Так же перед разработчиками стоит задача по синхронизации подключенных клиентов, при масштабировании сервера более чем на 1 процесс.
WebSockets
Это совершенно новый протокол для обмена данными, предоставляющий full-duplex
двустороннюю коммуникацию по верх TCP
.
WebSockets
позволяет серверу и клиенту делать меньше лишней работы и обмениваться сообщениями в режиме реального времени.
- Для соединения используется
WebSocket
- Клиент посылает специальный HTTP запрос, который трансформируется в
WebSocket
- Сервер с клиентом поддерживают соединение и могут обмениваться сообщениями
- Любая из сторон может закрыть соединение
const ws = new WebSocket("ws://socket")
ws.addEventListener("open", function(e) {
ws.send("Hello")
})
ws.addEventListener("message", function(e) {
console.log(e.data)
})
ws.addEventListener("close", function(e) {
// ...
})
ws.addEventListener("error", function(err) {
// ...
})
WebSockets
имеет схожие проблемы с Long-Polling
, так как тоже требует соответствующего сервера для поддержки большого количества соединений в памяти, а так же требует синхронизации при масштабировании сервера более чем на 1 процесс.
WebSockets
часто блокируется балансировщиками нагрузки и файерволами, что может требовать дополнительной настройки.
Server Sent Events (SSE)
В отличии от WebSockets
, SSE
позволяет передавать поток событий только в одну сторону (от сервера на клиент) за счет поддержания HTTP
соединения.
- Для соединения используется
EventSource
- Клиент посылает
HTTP
запрос, который поддерживается с обеих сторон - Сервер может посылать события в специальном формате
text/event-stream
- Любая из сторон может закрыть соединение
const sse = new EventSource('/sse')
sse.addEventListener("notice", function(e) {
console.log(e.data)
})
sse.addEventListener("update", function(e) {
console.log(e.data)
})
// "message" специальное событие, которое не имеет имени, как например "notice" или "update"
sse.addEventListener("message", function(e) {
console.log(e.data)
})
Все те же “проблемы”, что и у WebSockets
, но плюс ко всему:
- Есть ограничение на количество открытых соединений тут
- Хуже поддержка браузерами
- Нет возможности передачи бинарных данных, как в
WebSockets
- Меньше готовых библиотек, упрощающих работу
Что использовать?
WebSockets
является лучшим и универсальным решением, но так же имеет свою цену в сложности первоначальной настройки и поддержке.
SSE
я не рекомендую использовать.
Для проектов, где нужно “совсем чуть-чуть” Real-time
, подойдет Long-Polling
за счет своей простоты.
Short-polling
остается выбором для старых проектов, в которые сложно добавить поддержку других технологий.