<CodeLearn/>
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)で送信者を除外
  • 名前空間は通信の論理的な分離に使い、異なる機能を独立して管理できる
  • ユーザー固有ルームのパターンでプライベートメッセージを実現できる