Dlouhé dotazování (long polling) je nejjednodušší způsob, jak udržovat stále spojení se serverem, bez nutnosti použít specifický protokol jako WebSocket nebo Server Sent Events.
Je velmi snadné na implementaci a v mnoha případech dostačuje.
Pravidelné dotazování
Nejjednodušší způsob, jak ze serveru dostávat nové informace, je periodické dotazování. To znamená pravidelně odesílat na server požadavek: „Ahoj, jsem tady, máš pro mě nějaké informace?“ Například každých 10 sekund.
Při odpovídání si server nejprve poznamená, že klient je online, a pak mu pošle paket se zprávami, které do této chvíle obdržel.
Funguje to, ale má to své nevýhody:
- Zprávy se předávají se zpožděním až 10 sekund (mezi požadavky).
- I když nejsou žádné zprávy, server je každých 10 sekund bombardován požadavky, i když se uživatel přepnul jinam nebo usnul. To představuje docela velkou zátěž, která snižuje výkon.
Když tedy jde o velmi malou službu, může tento přístup být použitelný, ale obecně potřebuje vylepšení.
Dlouhé dotazování
Mnohem lepším způsobem, jak se serveru dotazovat, je tzv. „dlouhé dotazování“ (long polling).
I to je velmi snadné na implementaci a navíc doručuje zprávy bez prodlení.
Průběh je následující:
- Na server je poslán požadavek.
- Server neuzavře spojení, dokud nebude mít zprávu k odeslání.
- Když se zpráva objeví, server odpoví touto zprávou na požadavek.
- Prohlížeč okamžitě učiní nový požadavek.
Tato situace, kdy prohlížeč pošle požadavek a pak udržuje se serverem čekající spojení, je pro tuto metodu standardní. Teprve až bude zpráva doručena, bude spojení uzavřeno a znovu vytvořeno.
Pokud je spojení ztraceno, například kvůli síťové chybě, prohlížeč okamžitě pošle nový požadavek.
Nástin funkce podpis na straně klienta, která vytváří dlouhé požadavky:
async function podpis() {
let odpověď = await fetch("/podpis");
if (odpověď.status == 502) {
// Status 502 je vypršení časového limitu spojení,
// může nastat, když spojení čeká příliš dlouho
// a vzdálený server nebo proxy je uzavřel
// pak se připojíme znovu
await podpis();
} else if (odpověď.status != 200) {
// Chyba - zobrazíme ji
zobrazZprávu(odpověď.statusText);
// Za jednu sekundu se připojíme znovu
await new Promise(splň => setTimeout(splň, 1000));
await podpis();
} else {
// Načteme a zobrazíme zprávu
let zpráva = await odpověď.text();
zobrazZprávu(zpráva);
// Znovu zavoláme podpis(), abychom obdrželi další zprávu
await podpis();
}
}
podpis();
Jak vidíte, funkce podpis vytvoří požadavek, pak počká na odpověď, zpracuje ji a znovu volá sama sebe.
Architektura serveru musí být schopna pracovat s mnoha čekajícími spojeními.
Některé serverové architektury spouštějí pro každé spojení jeden proces, což vede k tomu, že vznikne tolik procesů, kolik je spojení, přičemž každý proces zabírá určité množství paměti. Příliš mnoho spojení tedy může zahltit celou paměť.
Často je to případ backendů napsaných v jazycích jako PHP nebo Ruby.
Servery napsané v Node.js tento problém obvykle nemají.
Tím netvrdíme, že je to vina programovacího jazyka. Většina moderních jazyků, včetně PHP a Ruby, umožňuje implementovat vhodný backend. Jen se prosíme přesvědčte, že vaše serverová architektura dobře funguje i při mnoha spojeních současně.
Demo: chat
Následuje demonstrativní chat. Můžete si jej také stáhnout a spustit u sebe lokálně (pokud znáte Node.js a můžete instalovat moduly):
// Posílání zpráv, jednoduchý POST
function OdešliFormulář(form, url) {
function odešliZprávu(zpráva) {
fetch(url, {
method: 'POST',
body: zpráva
});
}
form.onsubmit = function() {
let zpráva = form.zpráva.value;
if (zpráva) {
form.zpráva.value = '';
odešliZprávu(zpráva);
}
return false;
};
}
// Přijímání zpráv dlouhým dotazováním
function ZapišZáložku(elem, url) {
function zobrazZprávu(zpráva) {
let elemZprávy = document.createElement('div');
elemZprávy.append(zpráva);
elem.append(elemZprávy);
}
async function podpis() {
let odpověď = await fetch(url);
if (odpověď.status == 502) {
// Vypršel čas spojení
// nastává, když spojení čeká příliš dlouho
// spojíme se znovu
await podpis();
} else if (odpověď.status != 200) {
// Zobrazíme chybu
zobrazZprávu(odpověď.statusText);
// Za jednu sekundu se spojíme znovu
await new Promise(splň => setTimeout(splň, 1000));
await podpis();
} else {
// Přijali jsme zprávu
let zpráva = await odpověď.text();
zobrazZprávu(zpráva);
await podpis();
}
}
podpis();
}let http = require('http');
let url = require('url');
let querystring = require('querystring');
let static = require('node-static');
let fileServer = new static.Server('.');
let subscribers = Object.create(null);
function onSubscribe(req, res) {
let id = Math.random();
res.setHeader('Content-Type', 'text/plain;charset=utf-8');
res.setHeader("Cache-Control", "no-cache, must-revalidate");
subscribers[id] = res;
req.on('close', function() {
delete subscribers[id];
});
}
function publish(message) {
for (let id in subscribers) {
let res = subscribers[id];
res.end(message);
}
subscribers = Object.create(null);
}
function accept(req, res) {
let urlParsed = url.parse(req.url, true);
// nový klient chce zprávy
if (urlParsed.pathname == '/subscribe') {
onSubscribe(req, res);
return;
}
// posílání zprávy
if (urlParsed.pathname == '/publish' && req.method == 'POST') {
// akceptuje POST
req.setEncoding('utf8');
let message = '';
req.on('data', function(chunk) {
message += chunk;
}).on('end', function() {
publish(message); // zveřejníme ji pro všechny
res.end("ok");
});
return;
}
// zbytek je statický
fileServer.serve(req, res);
}
function close() {
for (let id in subscribers) {
let res = subscribers[id];
res.end();
}
}
// -----------------------------------
if (!module.parent) {
http.createServer(accept).listen(8080);
console.log('Server běží na portu 8080');
} else {
exports.accept = accept;
if (process.send) {
process.on('message', (msg) => {
if (msg === 'shutdown') {
close();
}
});
}
process.on('SIGINT', close);
}<!DOCTYPE html>
<script src="browser.js"></script>
Všichni návštěvníci této stránky vidí své zprávy navzájem.
<form name="odeslání">
<input type="text" name="zpráva" />
<input type="submit" value="Odeslat" />
</form>
<div id="podpis">
</div>
<script>
new OdešliFormulář(document.forms.odeslání, 'odeslání');
// náhodný URL parametr, abychom se vyhnuli ukládání do mezipaměti
new ZapišZáložku(document.getElementById('podpis'), 'podpis?random=' + Math.random());
</script>Kód pro prohlížeč se nachází v browser.js.
Oblast použití
Dlouhé dotazování funguje výborně v situacích, kdy zpráv není příliš mnoho.
Jestliže však zprávy přicházejí velmi často, bude výše zobrazený nákres odesílání požadavků a přijímání zpráv vypadat jako zuby pily.
Každá zpráva je samostatný požadavek, vybavený hlavičkami, autentifikací a podobně.
Proto se v takovém případě dává přednost jiným metodám, například Websocket nebo Server Sent Events.
Komentáře
<code>, pro několik řádků je obalte značkou<pre>, pro více než 10 řádků vložte odkaz na pískoviště (plnkr, jsbin, codepen…)