WebSocket レッスン3
ルームと名前空間
グループ通信とチャネル分離の仕組みを学ぼう
ルーム(Room)とは
ルームは、ソケットをグループ化する仕組みです。 チャットアプリのチャンネルや、ゲームの部屋のように、特定のグループに対してだけ メッセージを送信できます。
// ===== ルームの概念 =====
//
// Room: "general" Room: "random"
// ┌────────────┐ ┌────────────┐
// │ User A │ │ User B │
// │ User B │ │ User C │
// │ User C │ │ User D │
// └────────────┘ └────────────┘
//
// ※ 1人のユーザーが複数のルームに参加可能
// ※ ルームはサーバー側の概念(クライアントからは見えない)ルームへの参加・退出
joinでルームに参加し、leaveで退出します。 クライアントのリクエストに基づいてサーバー側で操作します。
// ===== サーバー側 =====
io.on("connection", (socket) => {
// ルームに参加
socket.on("room:join", (roomName) => {
socket.join(roomName);
console.log(`${socket.id} が ${roomName} に参加`);
// ルーム内の全員に通知
io.to(roomName).emit("room:notification", {
message: `新しいユーザーが参加しました`,
room: roomName,
});
});
// ルームから退出
socket.on("room:leave", (roomName) => {
socket.leave(roomName);
console.log(`${socket.id} が ${roomName} を退出`);
// ルーム内の残りのメンバーに通知
io.to(roomName).emit("room:notification", {
message: "ユーザーが退出しました",
room: roomName,
});
});
// 参加中のルーム一覧を取得
socket.on("room:list", (callback) => {
const rooms = Array.from(socket.rooms);
// rooms[0] はソケット自身のID(デフォルトルーム)
callback(rooms.slice(1));
});
// 切断時は自動的に全ルームから退出される
});ルームベースのブロードキャスト
ルームを指定してメッセージを送信することで、 特定のグループだけにデータを届けることができます。
io.on("connection", (socket) => {
// ルーム内チャット
socket.on("chat:message", (data) => {
const { room, text, user } = data;
// そのルームの全員に送信(送信者を含む)
io.to(room).emit("chat:message", {
user,
text,
timestamp: Date.now(),
});
});
// ルーム内の送信者以外に送信
socket.on("typing:start", (room) => {
socket.to(room).emit("typing:start", {
user: socket.id,
});
});
// 複数のルームに同時送信
socket.on("announcement", (data) => {
io.to("general").to("random").emit("announcement", data);
});
// ルーム内のメンバー数を取得
socket.on("room:members", async (roomName, callback) => {
const sockets = await io.in(roomName).fetchSockets();
callback({
room: roomName,
count: sockets.length,
members: sockets.map((s) => s.id),
});
});
});名前空間(Namespace)
名前空間は、Socket.IO接続を 論理的に分離する仕組みです。異なる機能やモジュールごとに通信を分けることができます。
// ===== サーバー側 =====
// デフォルト名前空間 (/)
io.on("connection", (socket) => {
console.log("メイン接続:", socket.id);
});
// /chat 名前空間
const chatNs = io.of("/chat");
chatNs.on("connection", (socket) => {
console.log("チャット接続:", socket.id);
socket.on("message", (data) => {
chatNs.emit("message", data);
});
});
// /notifications 名前空間
const notifNs = io.of("/notifications");
notifNs.on("connection", (socket) => {
console.log("通知接続:", socket.id);
// 5秒ごとに通知を送信
const interval = setInterval(() => {
socket.emit("notification", {
type: "info",
message: "新しい更新があります",
});
}, 5000);
socket.on("disconnect", () => clearInterval(interval));
});
// ===== クライアント側 =====
// チャット名前空間に接続
const chatSocket = io("http://localhost:3001/chat");
chatSocket.on("message", (data) => {
console.log("チャット:", data);
});
// 通知名前空間に接続
const notifSocket = io("http://localhost:3001/notifications");
notifSocket.on("notification", (data) => {
console.log("通知:", data);
});プライベートメッセージパターン
ルームを活用して1対1のプライベートメッセージを 実装するパターンです。ユーザーIDとソケットIDのマッピングを管理します。
// ユーザーIDとソケットの対応を管理
const userSockets = new Map();
io.on("connection", (socket) => {
// ユーザー登録
socket.on("user:register", (userId) => {
userSockets.set(userId, socket.id);
// ユーザー固有のルームに参加
socket.join(`user:${userId}`);
console.log(`${userId} を登録: ${socket.id}`);
});
// プライベートメッセージ送信
socket.on("dm:send", (data) => {
const { to, from, text } = data;
// 受信者のルームに送信
io.to(`user:${to}`).emit("dm:receive", {
from,
text,
timestamp: Date.now(),
});
// 送信者にも確認を送信
socket.emit("dm:sent", {
to,
text,
timestamp: Date.now(),
});
});
// 切断時にマッピングを削除
socket.on("disconnect", () => {
for (const [userId, socketId] of userSockets) {
if (socketId === socket.id) {
userSockets.delete(userId);
break;
}
}
});
});まとめ
- ルームはソケットをグループ化し、特定のグループにだけメッセージを送る仕組み
- join/leaveでルームの参加・退出を管理する(切断時は自動退出)
- io.to(room)でルーム内ブロードキャスト、socket.to(room)で送信者を除外
- 名前空間は通信の論理的な分離に使い、異なる機能を独立して管理できる
- ユーザー固有ルームのパターンでプライベートメッセージを実現できる