1. ホーム
  2. node.js

[解決済み] Websocketトランスポートの信頼性(再接続時のSocket.ioデータ損失)

2023-04-02 16:43:07

質問

中古

NodeJS, Socket.io

問題点

2人のユーザがいるとします U1 &です。 U2 は、Socket.ioを介してアプリに接続しました。アルゴリズムは以下の通りです。

  1. U1 インターネットに完全に接続できなくなる(例:インターネットを切断する)
  2. U2 にメッセージを送信します。 U1 .
  3. U1 インターネットがダウンしているため、まだメッセージを受信していません。
  4. サーバ が検出する U1 ハートビートタイムアウトによる切断
  5. U1 socket.ioに再接続します。
  6. U1 からのメッセージを受信することはありません。 U2 - からのメッセージを受け取ることはなく、ステップ4で失われていると思われる。

考えられる説明

なぜそうなるのか、わかったような気がします。

  • ステップ 4 で サーバー はソケットインスタンスとメッセージのキューを U1 と同様に
  • さらにステップ5で U1 となり サーバー は新しい接続を作成します (再利用されません)。そのため、メッセージがまだキューに残っていても、前の接続はいずれにせよ失われます。

ヘルプが必要

このようなデータ損失を防ぐにはどうしたらよいでしょうか。私は、アプリで永遠にハングアップしたくないので、hearbeats を使用する必要があります。また、アプリの新しいバージョンをデプロイするときに、ダウンタイムをゼロにしたいので、再接続の可能性を与えなければなりません。

追伸:私が「メッセージ」と呼んでいるものは、データベースに保存できる単なるテキストメッセージではなく、配信が保証されなければUIが台無しになるような貴重なシステムメッセージです。

ありがとうございます。


追加1

私はすでにユーザー アカウント システムを持っています。さらに、私のアプリケーションはすでに複雑になっています。オフライン/オンラインのステータスを追加しても、すでにこの種のものを持っているので、役に立ちません。問題は別のところにあります。

ステップ 2 を見てください。このステップでは、技術的に U1 がオフラインになったかどうかを知ることはできません。 彼は2秒間だけ接続を失いました。おそらくインターネットの調子が悪かったのでしょう。U2は彼にメッセージを送りますが、U1はまだインターネットがダウンしているので、それを受け取りません(ステップ3)。ステップ4はオフラインのユーザーを検出するために必要で、例えばタイムアウトを60秒とします。その後、10秒後にU1のインターネット接続が回復し、socket.ioに再接続されます。しかし、U2からのメッセージは、サーバー上でU1がタイムアウトにより切断されたため、空間的に失われています。

これが問題で、100%の配信をしたいのです。


解決方法

  1. ランダムなemitIDで識別されるemit(emit名とデータ)を{} userで収集する。エミットを送信する
  2. クライアント側でエミットを確認する(emitIDをサーバに送り返す)。
  3. 確認された場合 - emitIDで識別される{}からオブジェクトを削除します。
  4. ユーザーが再接続された場合 - このユーザーの {} を確認し、{} 内の各オブジェクトに対してステップ 1 を実行してループします。
  5. 切断または接続された場合、必要に応じてユーザーの{}をフラッシュします。
// Server
const pendingEmits = {};

socket.on('reconnection', () => resendAllPendingLimits);
socket.on('confirm', (emitID) => { delete(pendingEmits[emitID]); });

// Client
socket.on('something', () => {
    socket.emit('confirm', emitID);
});

解決策2 (ちょっとだけ)

2020 年 2 月 1 日追加。

これはWebsocketの解決策とは言えませんが、それでも便利だと思う人がいるかもしれません。私たちは Websockets から SSE + Ajax に移行しました。SSE では、クライアントから接続して持続的な TCP 接続を維持し、サーバーからリアルタイムでメッセージを受信することができます。クライアントからサーバにメッセージを送るには、単にAjaxを使用します。待ち時間やオーバーヘッドなどのデメリットはありますが、SSEはTCP接続であるため信頼性が保証されています。

私たちはExpressを使用しているので、SSEのために以下のライブラリを使用します。 https://github.com/dpskvn/express-sse を使用していますが、自分に合ったものを選んでください。

SSE は IE とほとんどの Edge のバージョンでサポートされていないため、ポリフィルが必要になります。 https://github.com/Yaffle/EventSource .

どのように解決するのですか?

他の方が他の回答やコメントでヒントにしていますが、根本的な問題は、Socket.IOが単なる配信メカニズムであり、あなたが はできません は、信頼できる配信のためにそれだけに依存することはできません。メッセージがクライアントに正常に配信されたことを確実に知っている唯一の人物は クライアント自身です。 . このようなシステムの場合、以下のようなアサーションを行うことをお勧めします。

  1. メッセージはクライアントに直接送信されるのではなく、サーバーに送信され、ある種のデータ ストアに保存されます。
  2. クライアントは、再接続時に "「何を見逃したか」を尋ねる責任があり、データ ストアに保存されたメッセージを照会して状態を更新します。
  3. メッセージがサーバーに送信された場合 が受信側クライアントの接続中に送信された場合。 の場合、そのメッセージはクライアントにリアルタイムで送信されます。

もちろん、アプリケーションのニーズに応じて、この一部を調整することができます。たとえば、Redisのリストやソートされたセットをメッセージに使用し、クライアントが最新であることが事実であればそれらを消去することができます。


ここにいくつかの例があります。

ハッピーパス :

  • U1、U2ともに接続されている。
  • U2はU1が受信すべきメッセージをサーバーに送信します。
  • サーバーはメッセージをある種の永続的なストアに保存し、U1に対してある種のタイムスタンプまたはシーケンシャルIDでマークします。
  • サーバーは Socket.IO を介して U1 にメッセージを送信します。
  • U1 のクライアントは、メッセージを受信したことを (おそらく Socket.IO コールバックを介して) 確認します。
  • サーバーは、データストアから永続化されたメッセージを削除します。

オフラインのパス :

  • U1 がインターネット接続を失いました。
  • U2がU1が受信すべきメッセージをサーバーに送信します。
  • サーバーはメッセージをある種の永続的なストアに保存し、U1に対してある種のタイムスタンプまたはシーケンシャルIDでマークします。
  • サーバーは Socket.IO を介して U1 にメッセージを送信します。
  • U1のクライアント はオフラインなので、受信を確認しない。
  • おそらく U2 は U1 にさらにいくつかのメッセージを送信し、それらはすべて同じ方法でデータストアに保存されます。
  • U1 が再接続すると、サーバーに「最後に見たメッセージは X / 状態は X、何を見逃したか」を尋ねます。
  • サーバーはU1のリクエストに基づき、データストアから見逃したすべてのメッセージをU1に送信します。
  • U1のクライアントは受信を確認し、サーバーはデータストアからそれらのメッセージを削除します。

もし絶対に保証された配信を望むのであれば、接続されていることは実際には重要ではなく、リアルタイム配信は単に ボーナス これはほとんど常にある種のデータストアを伴います。user568109がコメントで言及したように、メッセージの保存と配信を抽象化したメッセージングシステムがありますし、そのような構築済みのソリューションを調べる価値があるかもしれません。(おそらく、Socket.IO の統合を自分で書かなければならないでしょう)。

サーバーは U1 にメッセージを送信しようとし、U1 のクライアントがそれを受け取ったことを確認するまで、それを保留中のメッセージのリストに格納します。クライアントがオフラインの場合、クライアントが戻ってきたときにサーバーに "おい、俺は接続されていなかった。

幸運なことに、Socket.IO は、ネイティブ JS コールバックのように見えるメッセージにクライアントが "応答" できるメカニズムを提供します。ここにいくつかの疑似コードを示します。

// server
pendingMessagesForSocket = [];

function sendMessage(message) {
  pendingMessagesForSocket.push(message);
  socket.emit('message', message, function() {
    pendingMessagesForSocket.remove(message);
  }
};

socket.on('reconnection', function(lastKnownMessage) {
  // you may want to make sure you resend them in order, or one at a time, etc.
  for (message in pendingMessagesForSocket since lastKnownMessage) {
    socket.emit('message', message, function() {
      pendingMessagesForSocket.remove(message);
    }
  }
});

// client
socket.on('connection', function() {
  if (previouslyConnected) {
    socket.emit('reconnection', lastKnownMessage);
  } else {
    // first connection; any further connections means we disconnected
    previouslyConnected = true;
  }
});

socket.on('message', function(data, callback) {
  // Do something with `data`
  lastKnownMessage = data;
  callback(); // confirm we received the message
});

これは最後の提案と非常に似ていますが、単に永続的なデータストアがないだけです。


の概念にも興味があるかもしれません。 イベント ソーシング .