Dlouhé hlasování

Long polling je nejjednodušší způsob trvalého spojení se serverem, který nepoužívá žádný specifický protokol, jako je WebSocket nebo Server Side Events.

Vzhledem k tomu, že se velmi snadno implementuje, je v mnoha případech také dostačující.

Pravidelné hlasování

Nejjednodušší způsob, jak získat nové informace ze serveru, je pravidelné dotazování. Tedy pravidelné požadavky na server:„Dobrý den, jsem tady, máte pro mě nějaké informace?“. Například jednou za 10 sekund.

V odezvě si server nejprve všimne, že je klient online, a zadruhé odešle balíček zpráv, které do té chvíle obdržel.

Funguje to, ale má to své nevýhody:

  1. Zprávy jsou předávány se zpožděním až 10 sekund (mezi požadavky).
  2. I když nejsou žádné zprávy, server je každých 10 sekund bombardován požadavky, i když uživatel přepne jinam nebo spí. Z hlediska výkonu je to docela zátěž.

Pokud tedy mluvíme o velmi malé službě, přístup může být životaschopný, ale obecně potřebuje zlepšení.

Dlouhé hlasování

Takzvané „dlouhé dotazování“ je mnohem lepší způsob dotazování serveru.

Je také velmi snadné jej implementovat a doručuje zprávy bez zpoždění.

Průtok:

  1. Na server je odeslán požadavek.
  2. Server neuzavře připojení, dokud nebude mít zprávu k odeslání.
  3. Když se objeví zpráva – server jí na požadavek odpoví.
  4. Prohlížeč okamžitě odešle nový požadavek.

Situace, kdy prohlížeč odeslal požadavek a má čekající spojení se serverem, je pro tuto metodu standardní. Pouze po doručení zprávy je spojení obnoveno.

Pokud dojde ke ztrátě připojení, například kvůli chybě sítě, prohlížeč okamžitě odešle nový požadavek.

Náčrt subscribe na straně klienta funkce, která dělá dlouhé požadavky:

async function subscribe() {
  let response = await fetch("/subscribe");

  if (response.status == 502) {
    // Status 502 is a connection timeout error,
    // may happen when the connection was pending for too long,
    // and the remote server or a proxy closed it
    // let's reconnect
    await subscribe();
  } else if (response.status != 200) {
    // An error - let's show it
    showMessage(response.statusText);
    // Reconnect in one second
    await new Promise(resolve => setTimeout(resolve, 1000));
    await subscribe();
  } else {
    // Get and show the message
    let message = await response.text();
    showMessage(message);
    // Call subscribe() again to get the next message
    await subscribe();
  }
}

subscribe();

Jak můžete vidět, subscribe funkce provede načtení, pak čeká na odpověď, zpracuje ji a znovu se zavolá.

Server by měl být v pořádku s mnoha čekajícími připojeními

Architektura serveru musí být schopna pracovat s mnoha čekajícími připojeními.

Některé serverové architektury provozují jeden proces na připojení, což má za následek tolik procesů, kolik je připojení, přičemž každý proces spotřebovává poměrně dost paměti. Takže příliš mnoho připojení to všechno spotřebuje.

To je často případ backendů napsaných v jazycích jako PHP a Ruby.

Servery napsané pomocí Node.js obvykle takové problémy nemají.

To znamená, že to není problém programovacího jazyka. Většina moderních jazyků, včetně PHP a Ruby, umožňuje implementovat správný backend. Jen se prosím ujistěte, že architektura vašeho serveru funguje dobře s mnoha současnými připojeními.

Ukázka:chat

Zde je ukázkový chat, můžete si ho také stáhnout a spustit lokálně (pokud znáte Node.js a umíte instalovat moduly):

Resultbrowser.jsserver.jsindex.html
// Sending messages, a simple POST
function PublishForm(form, url) {

  function sendMessage(message) {
    fetch(url, {
      method: 'POST',
      body: message
    });
  }

  form.onsubmit = function() {
    let message = form.message.value;
    if (message) {
      form.message.value = '';
      sendMessage(message);
    }
    return false;
  };
}

// Receiving messages with long polling
function SubscribePane(elem, url) {

  function showMessage(message) {
    let messageElem = document.createElement('div');
    messageElem.append(message);
    elem.append(messageElem);
  }

  async function subscribe() {
    let response = await fetch(url);

    if (response.status == 502) {
      // Connection timeout
      // happens when the connection was pending for too long
      // let's reconnect
      await subscribe();
    } else if (response.status != 200) {
      // Show Error
      showMessage(response.statusText);
      // Reconnect in one second
      await new Promise(resolve => setTimeout(resolve, 1000));
      await subscribe();
    } else {
      // Got message
      let message = await response.text();
      showMessage(message);
      await subscribe();
    }
  }

  subscribe();

}
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);

  // new client wants messages
  if (urlParsed.pathname == '/subscribe') {
    onSubscribe(req, res);
    return;
  }

  // sending a message
  if (urlParsed.pathname == '/publish' && req.method == 'POST') {
    // accept POST
    req.setEncoding('utf8');
    let message = '';
    req.on('data', function(chunk) {
      message += chunk;
    }).on('end', function() {
      publish(message); // publish it to everyone
      res.end("ok");
    });

    return;
  }

  // the rest is static
  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 running on port 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>

All visitors of this page will see messages of each other.

<form name="publish">
  <input type="text" name="message" />
  <input type="submit" value="Send" />
</form>

<div id="subscribe">
</div>

<script>
  new PublishForm(document.forms.publish, 'publish');
  // random url parameter to avoid any caching issues
  new SubscribePane(document.getElementById('subscribe'), 'subscribe?random=' + Math.random());
</script>

Kód prohlížeče je browser.js .

Oblast použití

Dlouhé dotazování funguje skvěle v situacích, kdy jsou zprávy vzácné.

Pokud zprávy přicházejí velmi často, pak výše nakreslená tabulka žádostí a příjemců bude vypadat jako pila.

Každá zpráva je samostatný požadavek, opatřený hlavičkami, autentizační režií a tak dále.

V tomto případě je tedy preferována jiná metoda, například Websocket nebo Server Sent Events.