プライベートメッセージング - パート II
このガイドは4つのパートに分かれています
パート1の最後に、ここまで進みました。

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

これは…あまり良くありません。修正しましょう!
インストール
パート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();
}
// ...
}
これで、タブを更新してもセッションが失われることはありません。

サーバー側では、セッションはインメモリストア(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,
});
});
// ...
});
何が起こるか見てみましょう。

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

切断ハンドラー
サーバー側では、Socketインスタンスは2つの特別なイベントをemitします。disconnectingとdisconnect
セッションはタブ間で共有できるようになったため、「切断」ハンドラーを更新する必要があります。
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にメッセージを保存することで修正できますが、さらに厄介な影響があります。
- 送信者が切断されると、送信するすべてのパケットは再接続までバッファリングされます(ほとんどの場合、これは素晴らしいことです)。

- しかし、受信者が切断されると、指定されたルームにリスニングしているSocketインスタンスがないため、パケットは失われます。

このガイドのパート3でこれを修正してみましょう。
読んでいただきありがとうございます!