В этой главе мы рассмотрим способ организации COMET, то есть непрерывного получения данных с сервера, который очень прост и подходит в 90% реальных случаев.
Частые опросы
Первое решение, которое приходит в голову для непрерывного получения событий с сервера – это «частые опросы» (polling), т.е периодические запросы на сервер: «эй, я тут, изменилось ли что-нибудь?». Например, раз в 10 секунд.
В ответ сервер во-первых помечает у себя, что клиент онлайн, а во-вторых посылает сообщение, в котором в специальном формате содержится весь пакет событий, накопившихся к данному моменту.
При этом, однако, возможна задержка между появлением и получением данных, как раз в размере этих 10 секунд между запросами.
Другой минус – лишний входящий трафик на сервер. При каждом запросе браузер передаёт множество заголовков и в ответ получает, кроме данных, также заголовки. Для некоторых приложений трафик заголовков может в 10 и более раз превосходить трафик реальных данных.
- Задержки между событием и уведомлением.
- Лишний трафик и запросы на сервер.
- Простота реализации.
Причём, простота реализации тут достаточно условная. Клиентская часть – довольно проста, а вот сервер получает сразу большой поток запросов.
Даже если клиент ушёл пить чай – его браузер каждые 10 секунд будет «долбить» сервер запросами. Готов ли сервер к такому?
Длинные опросы
Длинные опросы – отличная альтернатива частым опросам. Они также удобны в реализации, и при этом сообщения доставляются без задержек.
Схема:
- Отправляется запрос на сервер.
- Соединение не закрывается сервером, пока не появится сообщение.
- Когда сообщение появилось – сервер отвечает на запрос, пересылая данные.
- Браузер тут же делает новый запрос.
Ситуация, когда браузер отправил запрос и держит соединение с сервером, ожидая ответа, является стандартной и прерывается только доставкой сообщений.
Схема коммуникации:
При этом если соединение рвётся само, например, из-за ошибки в сети, то браузер тут же отсылает новый запрос.
Примерный код клиентской части:
function subscribe(url) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState != 4) return;
if (this.status == 200) {
onMessage(this.responseText);
} else {
onError(this);
}
subscribe(url);
}
xhr.open("GET", url, true);
xhr.send();
}
Функция subscribe
делает запрос, при ответе обрабатывает результат, и тут же запускает процесс по новой.
Сервер, конечно же, должен уметь работать с большим количеством таких «ожидающих» соединений.
Демо: чат
Демо:
// Посылка запросов -- обычными XHR POST
function PublishForm(form, url) {
function sendMessage(message) {
var xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
// просто отсылаю сообщение "как есть" без кодировки
// если бы было много данных, то нужно было бы отослать JSON из объекта с ними
// или закодировать их как-то иначе
xhr.send(message);
}
form.onsubmit = function() {
var message = form.message.value;
if (message) {
form.message.value = '';
sendMessage(message);
}
return false;
};
}
// Получение сообщений, COMET
function SubscribePane(elem, url) {
function showMessage(message) {
var messageElem = document.createElement('div');
messageElem.appendChild(document.createTextNode(message));
elem.appendChild(messageElem);
}
function subscribe() {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState != 4) return;
if (this.status == 200) {
if (this.responseText) {
// сервер может закрыть соединение без ответа при перезагрузке
showMessage(this.responseText);
}
subscribe();
return;
}
if (this.status != 502) {
// 502 - прокси ждал слишком долго, надо пересоединиться, это не ошибка
showMessage(this.statusText); // показать ошибку
}
setTimeout(subscribe, 1000); // попробовать ещё раз через 1 сек
}
xhr.open("GET", url, true);
xhr.send();
}
subscribe();
}
var http = require('http');
var url = require('url');
var querystring = require('querystring');
var static = require('node-static');
var fileServer = new static.Server('.');
var subscribers = {};
function onSubscribe(req, res) {
var id = Math.random();
res.setHeader('Content-Type', 'text/plain;charset=utf-8');
res.setHeader("Cache-Control", "no-cache, must-revalidate");
subscribers[id] = res;
//console.log("новый клиент " + id + ", клиентов:" + Object.keys(subscribers).length);
req.on('close', function() {
delete subscribers[id];
//console.log("клиент "+id+" отсоединился, клиентов:" + Object.keys(subscribers).length);
});
}
function publish(message) {
//console.log("есть сообщение, клиентов:" + Object.keys(subscribers).length);
for (var id in subscribers) {
//console.log("отсылаю сообщение " + id);
var res = subscribers[id];
res.end(message);
}
subscribers = {};
}
function accept(req, res) {
var urlParsed = url.parse(req.url, true);
// новый клиент хочет получать сообщения
if (urlParsed.pathname == '/subscribe') {
onSubscribe(req, res); // собственно, подписка
return;
}
// отправка сообщения
if (urlParsed.pathname == '/publish' && req.method == 'POST') {
// принять POST-запрос
req.setEncoding('utf8');
var message = '';
req.on('data', function(chunk) {
message += chunk;
}).on('end', function() {
publish(message); // собственно, отправка
res.end("ok");
});
return;
}
// всё остальное -- статика
fileServer.serve(req, res);
}
// -----------------------------------
if (!module.parent) {
http.createServer(accept).listen(8080);
console.log('Сервер запущен на порту 8080');
} else {
exports.accept = accept;
process.on('SIGINT', function() {
for (var id in subscribers) {
var res = subscribers[id];
res.end();
}
});
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="browser.js"></script>
</head>
<body>
Несколько человек при заходе на эту страницу будут получать сообщения друг друга.
<form name="publish">
<input type="text" name="message" />
<input type="submit" value="Отправить" />
</form>
<div id="subscribe">
</div>
<script>
new PublishForm(document.forms.publish, 'publish');
// random url to fix https://code.google.com/p/chromium/issues/detail?id=46104
new SubscribePane(document.getElementById('subscribe'), 'subscribe?random=' + Math.random());
</script>
</body>
</html>
Область применения
Длинные опросы отлично работают в тех случаях, когда сообщения приходят редко.
При большом количестве частых сообщений график приёма-отправки, приведённый выше, превращается в «пилу». Каждое сообщение – это новый запрос, дополнительный трафик заголовков.
В этих случаях используются другие способы получения данных, подразумевающие непрерывное соединение с сервером. Мы рассмотрим их в следующих главах.