Статьи

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

  1. Вступление
  2. Подписка на поток: объект EventSource
  3. Отправка событий с сервера
    1. Отправка message событиях
    2. Отправка custom событий
    3. Управление переподключениями с интервалом retry
    4. Установка уникального идентификатора с помощью поля id
  4. Обработка событий
  5. Обработка ошибок
  6. Расхождения в реализации браузера
  7. Поддержка браузера и альтернативные стратегии

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

К счастью для вас, в вашей национальной службе новостей есть команда веб-разработчиков. Они создали спортивный тикер, который обновляется с каждым фолом или корзиной. Вы посещаете URL, и обновления помещаются прямо в ваш браузер. Конечно, вам интересно, как они это сделали. Ответ? Отправленные сервером события .

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

Но ждать! Я слышал, как вы говорили: « Разве мы не можем сделать это с такими технологиями, как XMLHttpRequest или Web Sockets ? Ну да. Однако для этого необходимо расширить эти объекты, чтобы сделать то, что EventSource делает изначально.

Соображения на стороне сервера

Поскольку отправляемые сервером события являются потоками данных, они требуют долгоживущих соединений. Вы хотите использовать сервер может обрабатывать большое количество одновременных подключений. Управляемые событиями серверы, конечно, особенно хорошо подходят для потоковых событий. К ним относятся Node.js , Джаггернаут и Twisted . Для Nginx есть модуль nginx-push-stream-module . Однако настройка сервера выходит за рамки данной статьи, и она будет зависеть от используемого вами сервера.

Давайте посмотрим на подписку на поток с использованием объекта EventSource . Затем мы рассмотрим отправку и обработку событий.

Подписка на поток событий: объект EventSource

Создать объект EventSource просто.

 var evtsrc = new EventSource('./url_of/event_stream/',{withCredentials:false}); 

Функция конструктора EventSource принимает до двух параметров:

  • строка URL , которая требуется; и
  • необязательный параметр словаря, который определяет значение свойства withCredentials .

Словари по своему синтаксису напоминают объекты, но на самом деле это ассоциативные массивы данных с определенными парами имя-значение. В этом случае withCredentials является единственным возможным членом словаря. Его значение может быть true или false . (Чтобы узнать больше о словарях в целом, обратитесь к спецификации Web IDL .)

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

Когда открывается соединение EventSource , оно запускает open событие . Мы можем определить функцию для обработки этого события, установив атрибут onopen .

 var evtsrc = new EventSource('./url_of/event_stream/'); evtsrc.onopen = function(openevent){ // do something when the connection opens }
var evtsrc = new EventSource('./url_of/event_stream/'); evtsrc.onopen = function(openevent){ // do something when the connection opens } 

Если что-то пойдет не так с нашей связью, будет сгенерирована error . Мы можем определить функцию-обработчик для этих событий, используя атрибут onerror . Мы обсудим некоторые причины возникновения ошибок в разделе « Обработка ошибок ».

 evtsrc.onerror = function(openevent){ // do something when there's an error }
evtsrc.onerror = function(openevent){ // do something when there's an error } 

Потоковые события являются message по умолчанию. Для обработки событий сообщений мы можем использовать атрибут onmessage чтобы определить функцию-обработчик.

 evtsrc.onmessage = function(openevent){ // do something when we receive a message event. }
evtsrc.onmessage = function(openevent){ // do something when we receive a message event. } 

Мы также можем использовать addEventListener() для прослушивания событий. Это единственный способ обработки пользовательских событий, как мы увидим в разделе « Обработка событий ».

 var onerrorhandler = function(openevent){ // do something } evtsrc.addEventListener('error',onerrorhandler,false); 

Чтобы закрыть соединение, используйте метод close() .

 evtsrc.close(); 

Итак, мы создали наш объект EventSource и определили обработчики для событий open , message и error . Но для того, чтобы это работало, нам нужен URL, который транслирует события.

Отправка событий с сервера

Отправленное сервером событие — это фрагмент текста, доставляемого как часть потока с URL-адреса. Чтобы браузеры воспринимали наши данные как поток, мы должны:

  • обслуживать наш контент заголовком Content-type , значением которого является text/event-stream ;
  • используйте кодировку UTF-8.

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

  • data : информация для отправки.
  • event : тип отправляемого события.
  • id : идентификатор события, которое будет использоваться при повторном подключении клиента.
  • retry : Сколько миллисекунд должно пройти до того, как браузер попытается повторно подключиться к URL.

Из них требуется только поле data .

Отправка message событиях

В этом примере мы отправим событие, объявляющее, какие команды играют в нашем чемпионате. Когда браузер получает этот текст, он отправляет событие message .

data: Brazil v. United States 

Значение поля data становится значением свойства data события сообщения. Как упоминалось выше, отправленные сервером события являются message по умолчанию. Но, как мы обсудим чуть позже, мы также можем отправлять пользовательские события , включая поле event .

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

 data: Brazil v. United States :Comments begin with a colon. Events must be followed a blank line. data: Air Canada Centre data: Toronto, Ontario, Canada data: Attendance: 19,800 

Для этого события значение свойства data будет следующим: Air Canada CentrenToronto, Ontario, CanadanAttendance: 19,800 .

Обратите внимание на пустую строку между событиями. Чтобы клиент получил событие, оно должно сопровождаться пустой строкой. Комментарии начинаются с двоеточия.

Отправка пользовательских событий

События имеют тип message если не указано иное. Для этого нам нужно будет включить поле event . В следующем примере мы добавим два события startfive в наш поток и отправим наши данные в виде строки в формате JSON .

 event: startingfive data: {"team":{"country":"Brazil","players":[{"id":15,"name":"de Sousa","position":"C"},{"id":12,"name":"Dantas","position":"F"}, {"id":7,"name":"Jacintho","position":"F"},{"id":6,"name":"de Oliveira Ferreira","position":"G"},{"id":4,"name":"Moisés Pinto","position":"G"}]}} event: startingfive data: {"team":{"country":"USA","players":[{"id":15,"name":"Charles","position":"C"},{"id":11,"name":"Cash","position":"F"}, {"id":5,"name":"Jones","position":"F"},{"id":7,"name":"Montgomery","position":"G"},{"id":4,"name":"Pondexter","position":"G"}]}}
event: startingfive data: {"team":{"country":"Brazil","players":[{"id":15,"name":"de Sousa","position":"C"},{"id":12,"name":"Dantas","position":"F"}, {"id":7,"name":"Jacintho","position":"F"},{"id":6,"name":"de Oliveira Ferreira","position":"G"},{"id":4,"name":"Moisés Pinto","position":"G"}]}} event: startingfive data: {"team":{"country":"USA","players":[{"id":15,"name":"Charles","position":"C"},{"id":11,"name":"Cash","position":"F"}, {"id":5,"name":"Jones","position":"F"},{"id":7,"name":"Montgomery","position":"G"},{"id":4,"name":"Pondexter","position":"G"}]}} 

Здесь нам нужно прослушивать событие startfive вместо события message . Однако наше поле data все равно станет значением свойства data события.

Мы обсудим свойство data и интерфейс MessageEvent в разделе « Обработка событий ».

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

Теперь, несмотря на то, что сервер отправляет события в браузер, реальность немного более тонкая. Если сервер поддерживает соединение открытым, запрос EventSource будет одним расширенным запросом. Если он закроется, браузер подождет несколько секунд, а затем снова подключится. Соединение может закрыться, например, если URL отправляет маркер конца файла.

Каждый браузер устанавливает свой интервал повторного подключения по умолчанию. Большинство воссоединяется через 3-6 секунд. Однако вы можете контролировать этот интервал, включив поле retry . Поле retry указывает количество миллисекунд, которые клиент должен ждать перед повторным подключением к URL. Давайте построим наш пример сверху и изменим наше событие, включив 5-секундный (5000 миллисекунд) интервал повторения.

 event: startingfive data: {"team":{"country":"USA","players":[{"id":15,"name":"Charles","position":"C"},{"id":11,"name":"Cash","position":"F"}, {"id":5,"name":"Jones","position":"F"},{"id":7,"name":"Montgomery","position":"G"},{"id":4,"name":"Pondexter","position":"G"}]}}
 Повторите: 5000

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

Установка уникального идентификатора с помощью поля id

Когда браузер подключится к URL-адресу, он получит все доступные данные на момент повторного подключения. Но в случае тикера игры, мы можем позволить нашему посетителю наверстать упущенное. Вот почему рекомендуется устанавливать id для каждого события. В приведенном ниже примере мы отправляем id как часть события score .

 event: score retry: 3000 data: Brazil 14 data: USA 13 data: 2pt, de Sousa id: 09:42 

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

Поле id становится свойством lastEventId этого объекта события. Но это служит другой цели. Если соединение закрывается, браузер будет включать заголовок Last-Event-ID со своим следующим запросом. Думайте об этом как о закладке для потока. Если присутствует заголовок Last-Event-ID , вы можете настроить ответ приложения, чтобы отправлять только те события, которые произошли после него.

Обработка событий

Как упоминалось выше, все события по умолчанию являются message . Каждое событие message имеет три атрибута, определенных интерфейсом MessageEvent .

event.data
Возвращает данные или сообщение, отправленное как часть события сообщения.
event.origin
Возвращает источник сообщения, который обычно представляет собой строку, содержащую схему (например, http, https), имя хоста и порт, с которого было отправлено сообщение.
event.lastEventId
Возвращает уникальный идентификатор последнего полученного события.

Каждый раз, когда происходит событие message , будет вызываться наша функция onmessage . Это прекрасно работает для приложений, в которых вы будете отправлять только сообщения. Но его ограничения становятся очевидными, если вы хотите отправить score или startingfive событий, как в нашем примере. Использование addEventListener более гибко. В приведенном ниже коде мы обрабатываем событие addEventListener используя addEventListener .

 var evtsrc = new EventSource('./url_of/event_stream/'); var startingFiveHandler = function(event){ var data = JSON.parse(event.data), numplayers, pl; console.log( data.team.country ); numplayers = data.team.players.length; for(var i=0; i 

Обработка ошибок

Интеллектуальная обработка ошибок требует немного больше работы, чем просто установка атрибута onerror . Нам также необходимо знать, не привела ли ошибка к ошибочному соединению или к временно прерванному. После неудачного подключения браузер не будет пытаться восстановить соединение. Если это временное прерывание — как может произойти, если компьютер спит или сервер закрывает соединение — браузер попытается снова. Браузеры отправят сообщение об error по любой из следующих причин.

  • URL отправляет заголовок ответа Content-type с неправильным значением.
  • URL возвратил заголовок HTTP-ошибки, такой как 404 File Not Found или 500 Internal Server Error.
  • Проблема с сетью или DNS не позволила установить соединение.
  • Сервер закрыл соединение.
  • Запрашивающий источник не разрешен URL.

Этот последний пункт заслуживает некоторого пояснения. На сегодняшний день ни один браузер не поддерживает отправленные сервером запросы о событиях из разных источников. В Firefox и Opera при попытке запроса из разных источников будет инициировано событие error для объекта EventSource , и соединение не будет установлено. В Chrome и Safari вместо этого вызывается исключение безопасности DOM.

readyState при обработке ошибок важно проверять свойство readyState . Давайте посмотрим на пример.

 var onerror = function(event){ var txt; switch( event.target.readyState ){ // if reconnecting case EventSource.CONNECTING: txt = 'Reconnecting...'; break; // if error was fatal case EventSource.CLOSED: txt = 'Connection failed. Will not retry.'; break; } alert(txt); } 

В приведенном выше коде, если значение e.target.readyState равно e.target.readyState (константа, определенная спецификацией; ее значение равно 0), мы оповестим пользователя, что мы воссоединяемся. Если его значение равно EventSource.CLOSED (еще одна константа, значение которой равно 2), мы предупредим пользователя, что браузер не будет повторно подключаться.

Расхождения в реализации браузера

Ни Firefox, ни Opera не изменяют свойство readyState объекта readyState когда компьютер выходит из спящего режима. Даже если соединение временно потеряно, значение EventSource.readyState остается равным 1. Chrome и Safari, напротив, изменяют значение readyState на 0, указывая, что браузер восстанавливает соединение. Однако в тестах все браузеры автоматически подключаются к URL через несколько секунд после пробуждения.

Поддержка браузера и альтернативные стратегии

На момент публикации Opera 11.60+, Firefox 6.0+, Safari 5.0+, iOS Safari 4.0+ и Chrome 6.0+ поддерживают отправляемые сервером события. Android WebKit и Opera Mini этого не делают. Поскольку EventSource является свойством глобального объекта (в браузерах это обычно объект window ), мы можем определить поддержку с помощью следующего кода.

 if(window.EventSource !== undefined){ // create an event source object. } else { // Use a fallback or throw an error. } 

XMLHttpRequest можно использовать в качестве запасного варианта для браузеров, которые не поддерживают EventSource . Полифиллы, использующие резерв XHR, включают EventSource от Yaffle и EventSource.js от Remy Sharp.

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

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

 var xhr = new XMLHttpRequest(); xhr.open('GET','./url_of/event_stream/'); xhr.setRequestHeader('X-Requestor','XHR'); xhr.send(null); 

Затем убедитесь, что ваше приложение закрывает соединение при наличии этого пользовательского заголовка. Сделайте это, установив значение Content-type: header в text/plain и (необязательно), включая заголовок Connection: close в ответе URL.

Изображение связанных узлов через Shutterstock