92

使用済み

NodeJS、Socket.io

問題

U1U2という 2 人のユーザーがいて、Socket.io 経由でアプリに接続されているとします。アルゴリズムは次のとおりです。

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

考えられる説明

なぜそれが起こるのか理解していると思います:

  • on ステップ 4サーバーはソケット インスタンスとU1へのメッセージのキューも削除します
  • さらに、ステップ 5 でU1サーバーは新しい接続を作成します (再利用されません)。そのため、メッセージがまだキューに入れられていても、以前の接続はとにかく失われます。

助けが必要

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

PS私が「メッセージ」と呼ぶものは、データベースに保存できる単なるテキストメッセージではなく、配信を保証する必要がある貴重なシステムメッセージです。そうしないと、UIが台無しになります。

ありがとう!


追加1

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

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

それが問題です。私は 100% 配信したくありません。


解決

  1. {} ユーザーのエミット (エミット名とデータ) を収集します。ランダムなemitID で識別されます。送信する
  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 というポリフィルが必要になります。

4

8 に答える 8

118

他の回答やコメントでこれをほのめかしている人もいますが、根本的な問題は、Socket.IO は単なる配信メカニズムであり、信頼できる配信を単独で行うことはできないということです。メッセージがクライアントに正常に配信されたことを確実に知っているのは、クライアント自身だけです。この種のシステムでは、次のアサーションを行うことをお勧めします。

  1. メッセージはクライアントに直接送信されません。代わりに、それらはサーバーに送信され、ある種のデータ ストアに格納されます。
  2. クライアントは、再接続時に「何を逃したか」を尋ねる責任があり、データ ストアに保存されているメッセージをクエリして状態を更新します。
  3. 受信側クライアントが接続しているときにメッセージがサーバーに送信されると、そのメッセージはリアルタイムでクライアントに送信されます。

もちろん、アプリケーションのニーズに応じて、この部分を調整できます。たとえば、Redis リストまたはメッセージのソート済みセットを使用し、クライアントが起動していることがわかっている場合はそれらを消去できます。現在まで。


以下にいくつかの例を示します。

ハッピーパス

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

オフライン パス:

  • U1 はインターネット接続を失います。
  • U2 は、U1 が受信する必要があるメッセージをサーバーに送信します。
  • サーバーは、ある種の永続ストアにメッセージを格納し、ある種のタイムスタンプまたはシーケンシャル ID で U1 をマークします。
  • サーバーは、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
});

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


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

于 2013-12-28T16:46:31.707 に答える
0

あなたが望むのは、各ユーザーに再利用可能なソケットを用意することです。次のようなものです。

クライアント:

socket.on("msg", function(){
    socket.send("msg-conf");
});

サーバ:

// Add this socket property to all users, with your existing user system
user.socket = {
    messages:[],
    io:null
}
user.send = function(msg){ // Call this method to send a message
    if(this.socket.io){ // this.io will be set to null when dissconnected
        // Wait For Confirmation that message was sent.
        var hasconf = false;
        this.socket.io.on("msg-conf", function(data){
            // Expect the client to emit "msg-conf"
            hasconf = true;
        });
        // send the message
        this.socket.io.send("msg", msg); // if connected, call socket.io's send method
        setTimeout(function(){
            if(!hasconf){
                this.socket = null; // If the client did not respond, mark them as offline.
                this.socket.messages.push(msg); // Add it to the queue
            }
        }, 60 * 1000); // Make sure this is the same as your timeout.

    } else {
        this.socket.messages.push(msg); // Otherwise, it's offline. Add it to the message queue
    }
}
user.flush = function(){ // Call this when user comes back online
    for(var msg in this.socket.messages){ // For every message in the queue, send it.
        this.send(msg);
    }
}
// Make Sure this runs whenever the user gets logged in/comes online
user.onconnect = function(socket){
    this.socket.io = socket; // Set the socket.io socket
    this.flush(); // Send all messages that are waiting
}
// Make sure this is called when the user disconnects/logs out
user.disconnect = function(){
    self.socket.io = null; // Set the socket to null, so any messages are queued not send.
}

その後、ソケット キューは切断間で保持されます。

socket各ユーザープロパティをデータベースに保存し、メソッドをユーザー プロトタイプの一部にするようにしてください。データベースは問題ではありません。保存するだけで、ユーザーを保存しています。

これにより、メッセージを送信済みとしてマークする前にクライアントからの確認を要求することで、補足 1 で述べた問題を回避できます。本当に必要な場合は、各メッセージに id を与え、クライアントにメッセージ id を に送信させてからmsg-conf確認することができます。

この例でuserは、すべてのユーザーのコピー元のテンプレート ユーザー、またはユーザー プロトタイプのようなものです。

注:これはテストされていません。

于 2013-12-27T20:45:37.483 に答える
0

ここを見てください:ブラウザのリロードを処理する socket.io

私が思いついたソリューションを使用できると思います。適切に変更すれば、思いどおりに動作するはずです。

于 2013-12-27T13:01:47.843 に答える
-2

最近このようなものを見ていて、別のパスの方が良いかもしれないと思います.

Azure サービス バス、クエリ、およびトピックを調べて、オフライン状態を処理してみてください。メッセージはユーザーが戻ってくるのを待ってから、メッセージを受け取ります。

キューを実行するためのコストですが、基本的なキューの 100 万回の操作あたり 0.05 ドルのようなものなので、開発のコストは、キュー システムを作成するために必要な時間の作業により多くなります。 https://azure.microsoft.com/en-us/pricing/details/service-bus/

Azure バスには、PHP、C#、Xarmin、Anjular、Java Script などのライブラリとサンプルがあります。

したがって、サーバーはメッセージを送信し、追跡について心配する必要はありません。クライアントは、必要に応じて負荷分散を処理できる手段として、メッセージを使用して送り返すこともできます。

于 2017-06-13T08:50:22.677 に答える