プライベートメッセージング - パートI
このガイドでは、次のアプリケーションを作成します。

次のトピックについて説明します。
- ミドルウェア
- ルーム
- 複数のSocket.IOサーバーへのスケーリング
前提条件
このガイドは4つの異なるパートで構成されています。
始めましょう!
インストール
最初に、チャットアプリケーションの最初のインプリメンテーションを取得しましょう。
git clone https://github.com/socketio/socket.io.git
cd socket.io/examples/private-messaging
git checkout examples/private-messaging-part-1
現在のディレクトリに表示される内容は以下のとおりです。
├── babel.config.js
├── package.json
├── public
│   ├── favicon.ico
│   ├── fonts
│   │   └── Lato-Regular.ttf
│   └── index.html
├── README.md
├── server
│   ├── index.js
│   ├── package.json
└── src
    ├── App.vue
    ├── components
    │   ├── Chat.vue
    │   ├── MessagePanel.vue
    │   ├── SelectUsername.vue
    │   ├── StatusIcon.vue
    │   └── User.vue
    ├── main.js
    └── socket.js
フロントエンドのコードは`src`ディレクトリに、サーバーのコードは`server`ディレクトリにあります。
フロントエンドの実行
このプロジェクトは、`@vue/cli`を使用して作成された基本的なVue.jsアプリケーションです。
実行するには
npm install
npm run serve
その後、ブラウザでhttps://:8080を開くと、以下が表示されます。

サーバーの実行
それでは、サーバーを起動しましょう。
cd server
npm install
npm start
コンソールには以下が出力されます。
server listening at https://:3000
今のところ順調です!複数のタブを開いて、それらの間でいくつかのメッセージを送信できるはずです。

動作方法
サーバーの初期化
Socket.IOサーバーは、`server/index.js`ファイルで初期化されます。
const httpServer = require("http").createServer();
const io = require("socket.io")(httpServer, {
  cors: {
    origin: "https://:8080",
  },
});
ここでは、Socket.IOサーバーを作成し、Node.js HTTPサーバーにアタッチします。
ドキュメント
`cors`設定は、フロントエンド(`https://:8080`で実行)から送信されたHTTPリクエストがサーバー(`https://:3000`で実行)に到達できるようにするために必要です(クロスオリジン状況にあります)。
ドキュメント
クライアントの初期化
Socket.IOクライアントは、`src/socket.js`ファイルで初期化されます。
import { io } from "socket.io-client";
const URL = "https://:3000";
const socket = io(URL, { autoConnect: false });
export default socket;
`autoConnect`は`false`に設定されているため、接続はすぐに確立されません。ユーザーがユーザー名を選択した後、後で`socket.connect()`を手動で呼び出します。
ドキュメント: Socket.IOクライアントの初期化
また、キャッチオールリスナーも登録します。これは開発中に非常に役立ちます。
socket.onAny((event, ...args) => {
  console.log(event, args);
});
これにより、クライアントが受信したイベントがコンソールに出力されます。
ユーザー名の選択
次に、`src/App.vue`に進みましょう。
アプリケーションは`usernameAlreadySelected`を`false`に設定して開始されるため、ユーザー名を選択するためのフォームが表示されます。

フォームが送信されると、`onUsernameSelection`メソッドに到達します。
onUsernameSelection(username) {
  this.usernameAlreadySelected = true;
  socket.auth = { username };
  socket.connect();
}
`auth`オブジェクトに`username`をアタッチし、`socket.connect()`を呼び出します。
開発者ツールのネットワークタブを開くと、いくつかのHTTPリクエストが表示されます。

- Engine.IOハンドシェイク(セッションID —ここでは`zBjrh...AAAK`—が含まれており、後続のリクエストで使用されます)
- Socket.IOハンドシェイクリクエスト(`auth`オプションの値が含まれています)
- Socket.IOハンドシェイクレスポンス(Socket#idが含まれています)
- WebSocket接続
- 最初のHTTPロングポーリングリクエスト。これは、WebSocket接続が確立されると閉じられます。
これが表示された場合は、接続が正常に確立されたことを意味します。
サーバー側では、ユーザー名を確認して接続を許可するミドルウェアを登録します。
io.use((socket, next) => {
  const username = socket.handshake.auth.username;
  if (!username) {
    return next(new Error("invalid username"));
  }
  socket.username = username;
  next();
});
`username`は`socket`オブジェクトの属性として追加され、後で再利用されます。`socket.id`や`socket.handshake`などの既存のものを上書きしない限り、任意の属性をアタッチできます。
ドキュメント
クライアント側(`src/App.vue`)では、`connect_error`イベントのハンドラーを追加します。
socket.on("connect_error", (err) => {
  if (err.message === "invalid username") {
    this.usernameAlreadySelected = false;
  }
});
`connect_error`イベントは、接続失敗時に発生します。
- 低レベルエラーによる場合(たとえば、サーバーがダウンしている場合)
- ミドルウェアエラーによる場合
上記の関数では、低レベルエラーは処理されていません(たとえば、ユーザーに接続失敗を通知できます)。
最後の注意:`connect_error`のハンドラーは、destroyedフックで削除されます。
destroyed() {
  socket.off("connect_error");
}
そのため、`App`コンポーネントによって登録されたリスナーは、コンポーネントが破棄されるとクリーンアップされます。
すべてのユーザーの一覧表示
接続時に、既存のすべてのユーザーをクライアントに送信します。
io.on("connection", (socket) => {
  const users = [];
  for (let [id, socket] of io.of("/").sockets) {
    users.push({
      userID: id,
      username: socket.username,
    });
  }
  socket.emit("users", users);
  // ...
});
IDでインデックス付けされた、現在接続されているすべてのSocketインスタンスのマップである`io.of("/").sockets`オブジェクトをループしています。
ここに2つの注意点があります。
- アプリケーションのユーザーIDとして`socket.id`を使用しています。
- 現在のSocket.IOサーバーのユーザーのみを取得しています(スケールアップには適していません)。
後でこれに戻ります。
クライアント側(`src/components/Chat.vue`)では、`users`イベントのハンドラーを登録します。
socket.on("users", (users) => {
  users.forEach((user) => {
    user.self = user.userID === socket.id;
    initReactiveProperties(user);
  });
  // put the current user first, and then sort by username
  this.users = users.sort((a, b) => {
    if (a.self) return -1;
    if (b.self) return 1;
    if (a.username < b.username) return -1;
    return a.username > b.username ? 1 : 0;
  });
});
また、既存のユーザーにも通知します。
サーバー
io.on("connection", (socket) => {
  // notify existing users
  socket.broadcast.emit("user connected", {
    userID: socket.id,
    username: socket.username,
  });
});
`socket.broadcast.emit("user connected", ...)`は、`socket`自体を除くすべての接続済みクライアントに送信します。
別のブロードキャスト形式である`io.emit("user connected", ...)`は、「ユーザーが接続しました」イベントを新しいユーザーを含むすべての接続済みクライアントに送信します。
ドキュメント: イベントのブロードキャスト
クライアント
socket.on("user connected", (user) => {
  initReactiveProperties(user);
  this.users.push(user);
});
ユーザーの一覧が左側のパネルに表示されます。

プライベートメッセージング
特定のユーザーを選択すると、右側のパネルにチャットウィンドウが表示されます。

プライベートメッセージの実装方法は次のとおりです。
クライアント(送信者)
onMessage(content) {
  if (this.selectedUser) {
    socket.emit("private message", {
      content,
      to: this.selectedUser.userID,
    });
    this.selectedUser.messages.push({
      content,
      fromSelf: true,
    });
  }
}
サーバー
socket.on("private message", ({ content, to }) => {
  socket.to(to).emit("private message", {
    content,
    from: socket.id,
  });
});
ここでは、ルームの概念を使用しています。これらは、Socketインスタンスが出入りできるチャネルであり、ルーム内のすべてのクライアントにブロードキャストできます。
Socketインスタンスは、そのIDで識別されるルームに自動的に参加するため(`socket.join(socket.id)`が自動的に呼び出されます)、その事実を利用しています。
そのため、`socket.to(to).emit("private message", ...)`は、指定されたユーザーIDに送信します。
クライアント(受信者)
socket.on("private message", ({ content, from }) => {
  for (let i = 0; i < this.users.length; i++) {
    const user = this.users[i];
    if (user.userID === from) {
      user.messages.push({
        content,
        fromSelf: false,
      });
      if (user !== this.selectedUser) {
        user.hasNewMessages = true;
      }
      break;
    }
  }
});
接続状況
クライアント側では、Socketインスタンスは2つの特別なイベントを発生させます。
- `connect`:接続または再接続時
- `disconnect`:切断時
これらのイベントを使用して、接続の状態を追跡できます(`src/components/Chat.vue`内)。
socket.on("connect", () => {
  this.users.forEach((user) => {
    if (user.self) {
      user.connected = true;
    }
  });
});
socket.on("disconnect", () => {
  this.users.forEach((user) => {
    if (user.self) {
      user.connected = false;
    }
  });
});
サーバーを停止してテストできます。

レビュー
OK、今のところは素晴らしいですが、明らかな問題があります。

説明:再接続時に新しいSocket IDが生成されるため、ユーザーが切断して再接続するたびに、新しいユーザーIDが取得されます。
そのため、永続的なユーザーIDが必要です。これは、このガイドの第2部の主題です。
読んでいただきありがとうございます!