本文へスキップ

プライベートメッセージング - パート II

このガイドは4つのパートに分かれています

パート1の最後に、ここまで進みました。

Chat

現在、プライベートメッセージの交換はsocket.id属性に基づいていますが、これは現在のSocket.IOセッションでのみ有効なIDであり、クライアントとサーバー間の低レベル接続が切断されるたびに変わってしまうため、問題があります。

そのため、ユーザーが再接続するたびに、新しいユーザーが作成されます。

Duplicate users

これは…あまり良くありません。修正しましょう!

インストール

パートIIのブランチをチェックアウトしましょう

git checkout examples/private-messaging-part-2

現在のディレクトリには次のものがあるはずです

├── babel.config.js
├── package.json
├── public
│ ├── favicon.ico
│ ├── fonts
│ │ └── Lato-Regular.ttf
│ └── index.html
├── README.md
├── server
│ ├── index.js (updated)
│ ├── package.json
│ └── sessionStore.js (created)
└── src
├── App.vue (updated)
├── components
│ ├── Chat.vue (updated)
│ ├── MessagePanel.vue
│ ├── SelectUsername.vue
│ ├── StatusIcon.vue
│ └── User.vue
├── main.js
└── socket.js

完全な差分はこちらで見ることができます。

動作方法

永続的なセッションID

サーバーサイド(server/index.js)では、2つのランダムな値を作成します。

  • 再接続時にユーザー認証に使用されるプライベートなセッションID
  • メッセージ交換の識別子として使用されるパブリックなユーザーID
io.use((socket, next) => {
const sessionID = socket.handshake.auth.sessionID;
if (sessionID) {
// find existing session
const session = sessionStore.findSession(sessionID);
if (session) {
socket.sessionID = sessionID;
socket.userID = session.userID;
socket.username = session.username;
return next();
}
}
const username = socket.handshake.auth.username;
if (!username) {
return next(new Error("invalid username"));
}
// create new session
socket.sessionID = randomId();
socket.userID = randomId();
socket.username = username;
next();
});

セッションの詳細がユーザーに送信されます。

io.on("connection", (socket) => {
// ...
socket.emit("session", {
sessionID: socket.sessionID,
userID: socket.userID,
});
// ...
});

クライアントサイド(src/App.vue)では、セッションIDをlocalStorageに保存します。

socket.on("session", ({ sessionID, userID }) => {
// attach the session ID to the next reconnection attempts
socket.auth = { sessionID };
// store it in the localStorage
localStorage.setItem("sessionID", sessionID);
// save the ID of the user
socket.userID = userID;
});

実際には、いくつかの実装方法がありました。

  • ストレージを使用しない: 再接続でセッションは保持されますが、ページを更新すると失われます。
  • sessionStorage: 再接続とページの更新でセッションは保持されます。
  • localStorage: 再接続とページの更新でセッションは保持され、このセッションはブラウザタブ間で共有されます。

ここではlocalStorageオプションを選択しました。そのため、すべてのタブが同じセッションIDにリンクされます。つまり、

  • 自分自身とチャットできます(やったー!)
  • 別のピアを作成するには、別のブラウザ(またはブラウザのプライベートモード)を使用する必要があります。

最後に、アプリケーションの起動時にセッションIDを取得します。

created() {
const sessionID = localStorage.getItem("sessionID");

if (sessionID) {
this.usernameAlreadySelected = true;
socket.auth = { sessionID };
socket.connect();
}
// ...
}

これで、タブを更新してもセッションが失われることはありません。

Persistent sessions

サーバー側では、セッションはインメモリストア(server/sessionStore.js)に保存されます。

class InMemorySessionStore extends SessionStore {
constructor() {
super();
this.sessions = new Map();
}

findSession(id) {
return this.sessions.get(id);
}

saveSession(id, session) {
this.sessions.set(id, session);
}

findAllSessions() {
return [...this.sessions.values()];
}
}

繰り返しますが、これは単一のSocket.IOサーバーでのみ機能します。このガイドの第4部で改めて説明します。

プライベートメッセージング(更新済み)

プライベートメッセージングは、サーバー側で生成されたuserIDに基づいているため、2つのことを行う必要があります。

  • Socketインスタンスに関連するルームに参加させる
io.on("connection", (socket) => {
// ...
socket.join(socket.userID);
// ...
});
  • 転送ハンドラーを更新する
io.on("connection", (socket) => {
// ...
socket.on("private message", ({ content, to }) => {
socket.to(to).to(socket.userID).emit("private message", {
content,
from: socket.userID,
to,
});
});
// ...
});

何が起こるか見てみましょう。

Private messaging

socket.to(to).to(socket.userID).emit(...)を使用して、受信者と送信者の両方のルームで(指定されたSocketインスタンスを除いて)ブロードキャストします。

つまり、現在

Chat (v2)

切断ハンドラー

サーバー側では、Socketインスタンスは2つの特別なイベントをemitします。disconnectingdisconnect

セッションはタブ間で共有できるようになったため、「切断」ハンドラーを更新する必要があります。

io.on("connection", (socket) => {
// ...
socket.on("disconnect", async () => {
const matchingSockets = await io.in(socket.userID).allSockets();
const isDisconnected = matchingSockets.size === 0;
if (isDisconnected) {
// notify other users
socket.broadcast.emit("user disconnected", socket.userID);
// update the connection status of the session
sessionStore.saveSession(socket.sessionID, {
userID: socket.userID,
username: socket.username,
connected: false,
});
}
});
});

allSockets()メソッドは、指定されたルームにあるすべてのSocketインスタンスのIDを含むSetを返します。

注: パートIのようにio.of("/").socketsオブジェクトを使用することもできますが、allSockets()メソッドは複数のSocket.IOサーバーでも機能するため、スケールアップ時に役立ちます。

ドキュメント: allSockets()メソッド

レビュー

さて…現状は改善されましたが、まだ別の問題があります。メッセージは実際にはサーバー上に永続化されていません。その結果、ユーザーがページを再読み込みすると、既存の会話がすべて失われます。

これは、たとえばブラウザのlocalStorageにメッセージを保存することで修正できますが、さらに厄介な影響があります。

  • 送信者が切断されると、送信するすべてのパケットは再接続までバッファリングされます(ほとんどの場合、これは素晴らしいことです)。
Chat with sender that gets disconnected
  • しかし、受信者が切断されると、指定されたルームにリスニングしているSocketインスタンスがないため、パケットは失われます。
Chat with recipient that gets disconnected

このガイドのパート3でこれを修正してみましょう。

読んでいただきありがとうございます!