プライベートメッセージング - パート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
その後、ブラウザでhttp://localhost:8080を開くと、以下が表示されます。
サーバーの実行
それでは、サーバーを起動しましょう。
cd server
npm install
npm start
コンソールには以下が出力されます。
server listening at http://localhost:3000
今のところ順調です!複数のタブを開いて、それらの間でいくつかのメッセージを送信できるはずです。

動作方法
サーバーの初期化
Socket.IOサーバーは、`server/index.js`ファイルで初期化されます。
const httpServer = require("http").createServer();
const io = require("socket.io")(httpServer, {
cors: {
origin: "http://localhost:8080",
},
});
ここでは、Socket.IOサーバーを作成し、Node.js HTTPサーバーにアタッチします。
ドキュメント
`cors`設定は、フロントエンド(`http://localhost:8080`で実行)から送信されたHTTPリクエストがサーバー(`http://localhost:3000`で実行)に到達できるようにするために必要です(クロスオリジン状況にあります)。
ドキュメント
クライアントの初期化
Socket.IOクライアントは、`src/socket.js`ファイルで初期化されます。
import { io } from "socket.io-client";
const URL = "http://localhost: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部の主題です。
読んでいただきありがとうございます!