2022年12月12日

ロングポーリング

ロングポーリングは、WebSocketやServer Sent Eventsのような特定のプロトコルを使用しない、サーバーとの永続的な接続を実現する最も簡単な方法です。

実装が非常に簡単であるため、多くの場合に十分に役立ちます。

定期的なポーリング

サーバーから新しい情報を取得する最も簡単な方法は、定期的なポーリングです。つまり、サーバーへの定期的なリクエストです。「こんにちは、私はここにいます。何か情報はありませんか?」たとえば、10秒に1回。

応答として、サーバーはまずクライアントがオンラインであることを認識し、次に、その時点までに取得したメッセージのパケットを送信します。

これは機能しますが、欠点もあります。

  1. メッセージは、リクエスト間で最大10秒の遅延を伴って渡されます。
  2. メッセージがない場合でも、ユーザーが別の場所に切り替えたり、スリープ状態になったりしても、サーバーは10秒ごとにリクエストが殺到します。パフォーマンスの観点から言えば、これはかなりの負荷になります。

そのため、非常に小規模なサービスの場合、このアプローチは実行可能かもしれませんが、一般的には改善が必要です。

ロングポーリング

いわゆる「ロングポーリング」は、サーバーをポーリングするためのより良い方法です。

また、実装が非常に簡単で、遅延なしでメッセージを配信します。

フロー

  1. リクエストがサーバーに送信されます。
  2. サーバーは、送信するメッセージがあるまで接続を閉じません。
  3. メッセージが表示されると、サーバーはそれを使用してリクエストに応答します。
  4. ブラウザはすぐに新しいリクエストを作成します。

ブラウザがリクエストを送信し、サーバーとの保留中の接続を維持するこの状況は、このメソッドの標準です。メッセージが配信された場合にのみ、接続が閉じられて再確立されます。

ネットワークエラーなどで接続が失われた場合、ブラウザはすぐに新しいリクエストを送信します。

長いリクエストを行うクライアント側のsubscribe関数のスケッチ

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

ご覧のとおり、subscribe関数はフェッチを行い、応答を待って、それを処理し、再度自身を呼び出します。

サーバーは多くの保留中の接続に対応できる必要があります

サーバーアーキテクチャは、多くの保留中の接続を処理できる必要があります。

特定のサーバーアーキテクチャは、接続ごとに1つのプロセスを実行するため、接続数と同じ数のプロセスが存在し、各プロセスはかなりの量のメモリを消費します。そのため、接続が多すぎると、すべて消費されてしまいます。

これは、PHPやRubyなどの言語で記述されたバックエンドの場合によくあります。

Node.jsを使用して記述されたサーバーは、通常、このような問題を抱えていません。

とは言え、これはプログラミング言語の問題ではありません。PHPやRubyを含むほとんどの最新言語では、適切なバックエンドを実装できます。サーバーアーキテクチャが多くの同時接続で正常に機能することを確認してください。

デモ:チャット

これがデモチャットです。ローカルでダウンロードして実行することもできます(Node.jsに精通していて、モジュールをインストールできる場合)。

結果
browser.js
server.js
index.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>

ブラウザコードはbrowser.jsにあります。

使用領域

ロングポーリングは、メッセージがまれな場合に最適です。

メッセージが非常に頻繁に来る場合、上記の要求-受信メッセージのチャートはのこぎりのようになります。

すべてのメッセージは、ヘッダー、認証オーバーヘッドなどを備えた個別のリクエストです。

したがって、この場合は、WebsocketServer Sent Eventsなどの別の方法が推奨されます。

チュートリアルマップ

コメント

コメントする前に読んでください…
  • 改善のための提案がある場合は、コメントする代わりに、GitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 数語のコードを挿入するには、<code>タグを使用し、複数行の場合は<pre>タグでラップし、10行を超える場合は、サンドボックス(plnkrjsbincodepen…)を使用してください。