プライベートメッセージング - パート 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でこれを修正してみましょう。
読んでいただきありがとうございます!