Книга: JavaScript. Подробное руководство, 6-е издание

18.3. Архитектура Comet на основе стандарта «Server-Sent Events»

18.3. Архитектура Comet на основе стандарта «Server-Sent Events»

Проект стандарта «Server-Sent Events» определяет объект EventSource, который делает практически тривиальным создание приложений с архитектурой Comet. При его использовании достаточно передать URL-адрес конструктору EventSource() и затем обрабатывать события «message» в полученном объекте:

var ticker = new EventSourcefstockprices. php”);
ticker.onmessage = function(e) {
  var type = e.type;
  var data = e.data;
// Обработать строки type и data.
}

Объект события «message» имеет свойство data, хранящее строку, отправленную сервером с этим событием в качестве полезной нагрузки. Кроме того, объект события имеет свойство type, как и все другие объекты событий. По умолчанию это свойство имеет значение «message», но источник события может указать в этом свойстве другую строку. Все события от данного сервера, источника событий, обрабатываются единственным обработчиком onmessage, который при необходимости может передавать их другим обработчикам, опираясь на свойство type объекта события.

Протокол обмена, определяемый стандартом «Server-Sent Event», достаточно прост. Клиент устанавливает соединение с сервером (когда создает объект EventSource), а сервер сохраняет это соединение открытым. Когда происходит событие, сервер передает через соединение текстовую строку. Передача события через сеть выглядит примерно следующим образом:

event: bid   установка свойства type объекта события
data:  G00G  установка свойства data
data:  999   добавляется перевод строки и дополнительные данные
             пустая строка генерирует событие message

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

Одно из очевидных применений архитектуры Comet - реализация чатов: клиент может посылать в чат новые сообщения с помощью объекта XMLHttpRequest и подписываться на поток сообщений, поступающих от собеседников, с помощью объекта EventSource. Пример 18.15 демонстрирует, насколько просто реализовать клиента на основе объекта EventSource.

Пример 18.15. Простой клиент чата на основе объекта EventSource

<script>
window.onload = function() {
  // Позаботиться о некоторых особенностях пользовательского интерфейса
  var nick = prompt("Введите ваше имя"); // Получить имя пользователя
  var input = document.getElementById("input"); // Отыскать поле ввода
  input.focus(); // Передать фокус ввода
  // Подписаться на получение новых сообщений с помощью объекта EventSource
  var chat = new EventSource("/chat");
  chat.onmessage = function(event) {         // Получив новое сообщение,
    var msg = event.data;                    // Извлечь текст
    var node = document.createTextNode(msg); // Преобр. в текстовый узел
    var div = document.createElementC'div"); // Создать <div>
    div.appendChild(node);                   // Добавить текст, узел в div
    document.body.insertBefore(div, input);  // И добавить div перед input
    input.scrollIntoView();                  // Прокрутить до появления
  }                                          // input в видимой области
  // Передавать сообщения пользователя на сервер с помощью XMLHttpRequest
  input.onchange = function() { // При нажатии клавиши Enter
    var msg = nick + ": " + input.value; // Имя пользователя + введ. текст
    var xhr = new XMLHttpRequest();      // Создать новый XHR
    xhr.open("POST", "/chat");           // POST-запрос к /chat,
    xhr.setRequestHeader("Content-Type", // Тип - простой текст UTF-8
                      "text/plain;charset=UTF-8");
    xhr.send(msg);                       // Отправить сообщение
    input.value = "";                    // Приготовиться к вводу
  }                                      // следующего сообщения
};
</script>
<!-- Пользовательский интерфейс чата состоит из единственного поля ввода -->
<!-- Новые сообщения, отправленные в чат, вставляются перед полем ввода -->
<input/>

На момент написания этих строк объект EventSource поддерживался в Chrome и Safari и должен был быть реализован компанией Mozilla в первом же выпуске Firefox, вышедшем после версии 4.0. В броузерах (таких как Firefox), где реализация объекта XMLHttpRequest возбуждает событие «readystatechange» в ходе загрузки ответа (для значения 3 в свойстве readyState) многократно, поведение объекта EventSource относительно легко имитировать с помощью объекта XMLHttpRequest, как демонстрируется в примере 18.16. С модулем имитации пример 18.15 будет работать в Chrome, Safari и Firefox. (Пример 18.16 не будет работать в броузерах ІE и Opera, поскольку реализации объекта XMLHttpRequest в этих броузерах не генерируют события в ходе загрузки.)

Пример 18.16. Имитация объекта EventSource с помощью XMLHttpRequest

// Имитация прикладного интерфейса EventSource в броузерах, не поддерживающих его.
// Требует, чтобы XMLHttpRequest генерировал многократные события readystatechange
// в ходе загрузки данных из долгоживущих HTTP-соединений. Учтите, что это не полноценная
// реализация API: она не поддерживает свойство readyState, метод close(), а также
// события open и error. Кроме того, регистрация обработчика события message выполняется
// только через свойство onmessage -- эта версия не определяет метод addEventListener
if (window.EventSource === undefined) { // Если EventSource не поддерживается,
  window.EventSource = function(url) { // использовать эту его имитацию,
    var xhr; // HTTP-соединение...
    var evtsrc = this: // Используется в обработчиках,
    var charsReceived = 0; // Так определяется появление нового текста
    var type = null; // Для проверки свойства type ответа,
    var data = ""; // Хранит данные сообщения
    var eventName = "message": // Значение поля type объектов событий
    var lastEventld = ""; // Для синхронизации с сервером
    var retrydelay = 1000: // Задержка между попытками соединения
    var aborted = false: // Установите в true, чтобы разорвать соединение
    // Создать объект XHR
    xhr = new XMLHttpRequest():
    // Определить обработчик события для него
    xhr.onreadystatechange = function() {
      switch(xhr.readyState) {
      case 3: processData(): break; // При получении фрагмента данных
      case 4: reconnectO: break; // По завершении запроса
      }
    };
    // И установить долгоживущее соединение
    connect();
    // Если соединение было закрыто обычным образом, ждать секунду
    // и попробовать восстановить соединение
    function reconnect() {
      if (aborted) return; // He восстанавливать после
                         // принудительного прерывания
      if (xhr.status >= 300) return;
      // He восстанавливать после ошибки
      setTimeout(connect, retrydelay); // Ждать и повторить попытку
    };
    // Устанавливает соединение
    function connect() {
      charsReceived = 0; type = null; xhr.open("GET", url);
      xhr.setRequestHeader(“Cache-Control", "no-cache");
      if (lastEventld)
        xhr.setRequestHeader("Last-Event-ID", lastEventld);
     xhr.send();
    }
    // При получении данных обрабатывает их и вызывает обработчик onmessage.
    // Эта функция реализует работу с протоколом Server-Sent Events
    function processData() {
      if (!type) { // Проверить тип ответа, если это еще не сделано
        type = xhr.getResponseHeader(’Content-Type’);
        if (type !== "text/event-stream") {
          aborted = true;
          xhr.abort();
          return;
        }
      }
      // Запомнить полученный объем данных и извлечь только ту часть ответа,
      // которая еще не была обработана,
      var chunk = xhr.responseText.substring(charsReceived);
      charsReceived = xhr.responseText.length;
      // Разбить текст на строки и обойти их в цикле.
      var lines = chunk.replace(/(rn|r|n)$/,"").split(/rn|r|n/);
      for(var і = 0; і < lines.length; i++) {
        var line = lines[i], pos = line.indexOf(":"), name.value="";
        if (pos == 0) continue; // Игнорировать комментарии
        if (pos > 0) { // поле name:value
          name = line.substring(0,pos);
          value = line.substring(pos+1);
          if (value.charAt(O) == " ") value = value.substrings);
        }
        else name = line; // только поле name
        switch(name) {
        case "event": eventName = value; break;
        case "data": data += value + "n"; break;
        case "id": lastEventld = value; break;
        case "retry": retrydelay = parselnt(value) || 1000; break;
        default: break; // Игнорировать любые другие строки
        }
        if (line === "") { // Пустая строка означает отправку события
          if (evtsrc.onmessage && data !== "") {
            // Отсечь завершающий символ перевода строки
            if (data.charAt(data.length-1) == "n")
            data = data.substrings, data.length-1);
            evtsrc.onmessage({ // Имитация объекта Event
                   type: eventName, // тип события
                   data: data, // данные
                   origin: url // происхождение данных
            });
          }
          data = "";
          continue;
        }
      }
    }
  };
}

Завершим описание архитектуры Comet примером серверного сценария. В примере 18.17 приводится реализация HTTP-сервера на серверном JavaScript, который выполняется под управлением интерпретатора Node (раздел 12.2). Когда клиент обращается к корневому URL «/», сервер отправляет ему реализацию клиента чата, представленную в примере 18.15, и реализацию имитации, представленную в примере 18.16. Когда клиент выполняет GET-запрос по URL-адресу «/chat», сервер сохраняет поток ответа в массиве и поддерживает соединение открытым. А когда клиент выполняет POST-запрос к адресу «/chat», сервер интерпретирует тело запроса как текст сообщения и добавляет префикс «data:», как того требует протокол Server-Sent Events, во все открытые потоки сообщений. Если вы установите интерпретатор Node, вы сможете запустить этот пример сервера локально. Он прослушивает порт 8000, поэтому после запуска сервера в броузере необходимо будет указать адрес http://localhost:8000, чтобы соединиться с сервером и начать общение с самим собой.

Пример 18.17. Сервер чата, поддерживающий протокол Server-Sent Events

// Этот программный код на серверном JavaScript предназначен для выполнения
// под управлением NodeJS. Он реализует очень простую, полностью анонимную комнату чата.
// Для отправки новых сообщений в чат следует использовать POST-запросы к URL /chat,
// а для получения текста/потока-событий сообщений следует использовать GET-запросы
// к тому же URL. При выполнении GET-запроса к / возвращается простой HTML-файл,
// содержащий пользовательский интерфейс чата для клиента.
var http = require('http'); // Реализация API HTTP-сервера в NodeJS
// HTML-файл для клиента чата. Используется ниже.
var clientui = require('fs').readFileSync( "chatclient.html");
var emulation = require( fs').readFileSync("EventSourceEmulation.js");
// Массив объектов ServerResponse, который будет использоваться для отправки событий
var clients = [];
// Отправлять комментарий клиентам каждые 20 секунд, чтобы предотвратить
// закрытие соединения с последующими попытками восстановить его
setInterval(function() {
  clients.forEach(function(client) {
    client. write('': pingn");
  });
}, 20000);
// Создать новый сервер
var server = new http.Server();
// Когда сервер получит новый запрос, он вызовет эту функцию
server.on( "request", function (request, response) {
  // Проанализировать запрошенный URL
  var url = require(’url').parse(request.url);
  // Если запрошен URL "/", отправить пользовательский интерфейс чата,
  if (url.pathname === "/") { // Запрошен пользовательский интерфейс чата
    response.writeHead(200, {"Content-Type": "text/html"});
    response.write("<script>" + emulation + "</script>");
    response.write(clientui);
    response.end();
    return;
  }
  // Если запрошен любой другой URL, кроме "/chat", отправить код 404
  else
    if (url.pathname !== "/chat") {
    response.writeHead(404);
    response.end();
    return;
  }
  // Если был выполнен POST-запрос - клиент отправил новое сообщение
  if (request.method === "POST") {
    request.setEncoding("utf8");
    var body = "";
    // При получении фрагмента данных добавить его в переменную body
    request.on("data", function(chunk) { body += chunk; });
    // По завершении запроса отправить пустой ответ
    // и широковещательное сообщение всем клиентам,
    request.on("end", function() {
      response.writeHead(200); // Ответ на запрос
      response.end();
      // Преобразовать сообщение в формат текст/поток-событий.
      // Убедиться, что все строки начинаются с префикса "data:"
      // и само сообщение завершается двумя символами перевода строки,
      message = 'data: ' + body, replace('n’, 'ndata: ') + "rnrn";
      // Отправить сообщение всем клиентам
      clients.forEach(function(client) { client.write(message); });
    });
  }
  // Если иначе, клиент запросил поток сообщений
  else {
    // Установить тип содержимого и отправить начальное событие message
    response.writeHead(200, {'Content-Type': "text/event-stream" });
    response.write("data: Connectednn");
    // Если клиент закрыл соединение, удалить соответствующий
    // объект response из массива активных клиентов
    request.connection.on("end", function() {
      clients.splice(clients.index0f(response), 1);
      response.end();
    }):
    // Запомнить объект response, чтобы в дальнейшем посылать сообщения с его помощью
    clients.push(response);
  }
});
// Запустить сервер на порту 8000. Чтобы подключиться к нему, используйте
// адрес http://localhost:8000/
server.listen(8000);

Оглавление книги


Генерация: 3.580. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз