Как на самом деле работают события, отправляемые сервером?

#server-sent-events

#отправленные сервером события

Вопрос:

Итак, я понимаю концепцию событий, отправляемых сервером ( EventSource ):

  • Клиент подключается к конечной точке через EventSource
  • Клиент просто прослушивает сообщения, отправленные с конечной точки

Меня смущает то, как это работает на сервере. Я рассмотрел разные примеры, но тот, который приходит на ум, — это Mozilla: http://hacks.mozilla.org/2011/06/a-wall-powered-by-eventsource-and-server-sent-events /

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

  • Что-то меняется в хранилище данных, например, в базе данных
  • Серверный скрипт опрашивает хранилище данных каждую N-ю секунду
  • Если сценарий опроса замечает изменение, отправленное сервером событие отправляется клиентам

Имеет ли это смысл? Это действительно так, как это работает с точки зрения простого использования?

Ответ №1:

На сайте HTML5 doctor есть отличная статья о событиях, отправляемых сервером, но я постараюсь представить (разумно) краткое резюме и здесь.

События, отправляемые сервером, по своей сути представляют собой длительное http-соединение, специальный mime-тип ( text/event-stream ) и пользовательский агент, предоставляющий EventSource API. Вместе они создают основу однонаправленного соединения между сервером и клиентом, при котором сообщения могут отправляться с сервера на клиент.

На стороне сервера это довольно просто. Все, что вам действительно нужно сделать, это установить следующие http-заголовки:

 Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
  

Обязательно отвечайте кодом 200 , а не 204 любым другим кодом, так как это приведет к отключению совместимых пользовательских агентов. Кроме того, убедитесь, что соединение не прерывается на стороне сервера. Теперь вы можете начать отправлять сообщения по этому соединению. В nodejs (с использованием express) это может выглядеть примерно так:

 app.get("/my-stream", function(req, res) {
    res.status(200)
       .set({ "content-type"  : "text/event-stream"
            , "cache-control" : "no-cache"
            , "connection"    : "keep-alive"
            })

    res.write("data: Hello, world!nn")
})
  

На клиенте вы просто используете EventSource API, как вы отметили:

 var source = new EventSource("/my-stream")
source.addEventListener("message", function(message) {
    console.log(message.data)
})
  

И это все, в основном.

Теперь, на практике, что на самом деле здесь происходит, так это то, что соединение поддерживается сервером и клиентом посредством взаимного контракта. Сервер будет поддерживать соединение до тех пор, пока считает нужным. Если он захочет, он может прервать соединение и ответить в 204 No Content следующий раз, когда клиент попытается подключиться. Это приведет к тому, что клиент прекратит попытки повторного подключения. Я не уверен, есть ли способ прервать соединение таким образом, чтобы клиенту было предложено вообще не переподключаться, тем самым пропуская попытку клиента переподключиться один раз.

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

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

  • id — идентификатор сообщения
  • данные — фактические данные
  • событие — тип события
  • повторная попытка — миллисекунды, которые агент пользователя должен подождать перед повторной попыткой неудачного соединения

Любые другие ключи следует игнорировать. Затем сообщения разделяются двумя символами новой строки: nn

Следующее является допустимым сообщением: (последние символы новой строки добавлены для уточнения)

 data: Hello, world!
n
  

Клиент увидит это как: Hello, world! .

Как это:

 data: Hello,
data: world!
n
  

Клиент увидит это как: Hello,nworld! .

Это в значительной степени подводит итог тому, что представляют собой события, отправляемые сервером: длительное некэшированное http-соединение, mime-тип и простой javascript API.

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

Комментарии:

1. Это отлично подходит для получения подробной информации о клиенте и о том, как работает соединение, но если событие необходимо отправить для изменения базы данных, я полагаю, что нет другого выбора, кроме как постоянно опрашивать базу данных на сервере? Я думаю, это то, о чем спрашивал op? Есть ли более эффективное решение на стороне сервера — очевидно, что не потребуется много подключений на сервере малого бизнеса, чтобы начать замедлять работу, если есть циклы, постоянно проверяющие изменения БД.

2. Клиент просто получает сообщение всякий раз, когда сервер решает его отправить, он никогда ничего не опрашивает. Сервер может самостоятельно определять, когда и зачем отправлять сообщение. Это может быть реализовано на стороне сервера с использованием опроса или любой другой подходящей техники. Соединения на сервере могут быть объединены в пул, и по каждому соединению может быть отправлено сообщение, что дает вам своего рода широковещательную функциональность. Таким образом, сервер может быть единственным подключением к БД, но транслироваться на множество клиентов, подключенных к серверу. Я думаю, что вопрос был более общим, чем этот.

3. Эта повторная попытка: бит в миллисекундах — гениально! Спас мою шкуру!

4. Хорошо, отличное объяснение, но я все еще не понимаю, как вы будете обновлять содержимое. вы хотите сказать, что вам нужно обновлять файл сервера каждый раз, когда вы хотите обновить поток? если я хочу обновить страницу погоды о накоплении дождя. каждые 10 минут накапливается дождь. в течение 20 минут может идти сильный дождь, затем он замедляется до мороси еще на 20 минут, поэтому, если в первые 10 минут уровень дождя составляет 1 дюйм, то в следующие 10 минут он составляет 2,7 дюйма, затем замедляется на 10 минут и достигает 3 дюймов. откуда сервер получит эту информацию? мы не узнаем точное количество, пока это не произойдет

5. О закрытии соединения: если я хочу запретить браузеру повторно подключаться, я просто отправляю определенное событие сервера (например, event: close ) и прослушиваю его на клиенте, а затем вызываю метод close .

Ответ №2:

Вы также должны убедиться, что вызываете res.flushHeaders() , иначе Node.js не будет отправлять HTTP-заголовки, пока вы не позвоните res.end() . Смотрите Этот учебник для полного примера.