[解決済み] Websocketトランスポートの信頼性(再接続時のSocket.ioデータ損失)
質問
中古
NodeJS, Socket.io
問題点
2人のユーザがいるとします U1 &です。 U2 は、Socket.ioを介してアプリに接続しました。アルゴリズムは以下の通りです。
- U1 インターネットに完全に接続できなくなる(例:インターネットを切断する)
- U2 にメッセージを送信します。 U1 .
- U1 インターネットがダウンしているため、まだメッセージを受信していません。
- サーバ が検出する U1 ハートビートタイムアウトによる切断
- U1 socket.ioに再接続します。
- 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%の配信をしたいのです。
解決方法
- ランダムなemitIDで識別されるemit(emit名とデータ)を{} userで収集する。エミットを送信する
- クライアント側でエミットを確認する(emitIDをサーバに送り返す)。
- 確認された場合 - emitIDで識別される{}からオブジェクトを削除します。
- ユーザーが再接続された場合 - このユーザーの {} を確認し、{} 内の各オブジェクトに対してステップ 1 を実行してループします。
- 切断または接続された場合、必要に応じてユーザーの{}をフラッシュします。
// 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が単なる配信メカニズムであり、あなたが はできません は、信頼できる配信のためにそれだけに依存することはできません。メッセージがクライアントに正常に配信されたことを確実に知っている唯一の人物は クライアント自身です。 . このようなシステムの場合、以下のようなアサーションを行うことをお勧めします。
- メッセージはクライアントに直接送信されるのではなく、サーバーに送信され、ある種のデータ ストアに保存されます。
- クライアントは、再接続時に "「何を見逃したか」を尋ねる責任があり、データ ストアに保存されたメッセージを照会して状態を更新します。
- メッセージがサーバーに送信された場合 が受信側クライアントの接続中に送信された場合。 の場合、そのメッセージはクライアントにリアルタイムで送信されます。
もちろん、アプリケーションのニーズに応じて、この一部を調整することができます。たとえば、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
});
これは最後の提案と非常に似ていますが、単に永続的なデータストアがないだけです。
の概念にも興味があるかもしれません。 イベント ソーシング .
関連
-
[解決済み】WebSocket接続に失敗しました。WebSocket のハンドシェイク中にエラーが発生しました。予期しない応答コードです。400
-
[解決済み】Passport.js - エラー: ユーザーのセッションへのシリアライズに失敗しました。
-
[解決済み】POSTできない/expressを使用するとエラーが発生する
-
[解決済み] create-react-app、インストールエラー("コマンドが見つからない")。
-
[解決済み] EventEmitter のメモリリークの可能性が検出された
-
[解決済み] AWS lambda function error - Unable to import module 'index': エラー
-
[解決済み] Heroku "状態が起動から停止に変更されました SIGTERMで全プロセスを停止"
-
[解決済み] nodejs - http.requestでresponse.writeを使用する場合、第一引数は文字列またはBufferでなければなりません。
-
[解決済み] Mongoose Schema がモデルとして登録されていません。
-
[解決済み] socket.ioとwebsocketの違いについて
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み】 console.logの出力をどこに永久保存するか?
-
[解決済み】MongoDBでコレクションを日付で並べ替えるには?
-
[解決済み】MongoDBのデータ/DBが見つからない
-
[解決済み】Passport.js - エラー: ユーザーのセッションへのシリアライズに失敗しました。
-
[解決済み】Heroku + node.jsのエラー(Webプロセスが起動後60秒以内に$PORTにバインドできなかった)。
-
[解決済み】npm 5で作成されたpackage-lock.jsonファイルはコミットするのでしょうか?
-
[解決済み] Passport JSのreq.isAuthenticated()はどのように実装されていますか?[クローズド]
-
[解決済み] AWS lambda function error - Unable to import module 'index': エラー
-
[解決済み] ExpressJS : res.redirect()が期待通りに動作しない?
-
[解決済み] Macでポート3000をロックしているプロセスを見つける(そして殺す)【終了