Книга: 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);
- Events
- БП стандарта AT
- Тестирование Web-сервиса XML с помощью WebDev.WebServer.exe
- InterBase Super Server для Windows
- Каталог BIN в SuperServer
- Минимальный состав сервера InterBase SuperServer
- InterBase Classic Server под Linux
- Каталог BIN в InterBase Classic Server для Linux
- SuperServer
- Classic vs SuperServer
- Рекомендации по выбору архитектуры: Classic или SuperServer?
- Улучшенное время отклика для версии SuperServer