本文へスキップ

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

このガイドでは、次のアプリケーションを作成します。

Chat

次のトピックについて説明します。

前提条件

  • Socket.IOの基本的な知識
  • Vue.jsの基本的な知識(ただし、他の一般的なフロントエンドフレームワークの知識でも機能します)
  • (最終パートのために)Redisの基本的な知識

このガイドは4つの異なるパートで構成されています。

  • パートI (現在):最初のインプリメンテーション
  • パートII:永続的なユーザーID
  • パートIII:永続的なメッセージ
  • パートIV:スケールアップ

始めましょう!

インストール

最初に、チャットアプリケーションの最初のインプリメンテーションを取得しましょう。

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を開くと、以下が表示されます。

Username selection

サーバーの実行

それでは、サーバーを起動しましょう。

cd server
npm install
npm start

コンソールには以下が出力されます。

server listening at http://localhost:3000

今のところ順調です!複数のタブを開いて、それらの間でいくつかのメッセージを送信できるはずです。

Chat

動作方法

サーバーの初期化

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`に設定して開始されるため、ユーザー名を選択するためのフォームが表示されます。

Username selection

フォームが送信されると、`onUsernameSelection`メソッドに到達します。

onUsernameSelection(username) {
this.usernameAlreadySelected = true;
socket.auth = { username };
socket.connect();
}

`auth`オブジェクトに`username`をアタッチし、`socket.connect()`を呼び出します。

開発者ツールのネットワークタブを開くと、いくつかのHTTPリクエストが表示されます。

Network monitor upon success

  1. Engine.IOハンドシェイク(セッションID —ここでは`zBjrh...AAAK`—が含まれており、後続のリクエストで使用されます)
  2. Socket.IOハンドシェイクリクエスト(`auth`オプションの値が含まれています)
  3. Socket.IOハンドシェイクレスポンス(Socket#idが含まれています)
  4. WebSocket接続
  5. 最初の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);
});

ユーザーの一覧が左側のパネルに表示されます。

Users list

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

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

Chat

プライベートメッセージの実装方法は次のとおりです。

クライアント(送信者)

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;
}
});
});

サーバーを停止してテストできます。

Connection status

レビュー

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

Duplicate users

説明:再接続時に新しいSocket IDが生成されるため、ユーザーが切断して再接続するたびに、新しいユーザーIDが取得されます。

そのため、永続的なユーザーIDが必要です。これは、このガイドの第2部の主題です。

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