socket.io の使い方

node.js realtime framework server

v4.8.3/週MITWeb API
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

socket.io の使い方 — Node.js リアルタイム通信フレームワーク

一言でいうと

Socket.IO は、WebSocket をベースにしたリアルタイム双方向イベント駆動通信を実現する Node.js フレームワークです。自動再接続、切断検知、ルーム機能などを備え、素の WebSocket よりも堅牢なリアルタイム通信を簡単に構築できます。

どんな時に使う?

  • チャットアプリケーション — ユーザー間のリアルタイムメッセージング、タイピングインジケーター、オンラインステータス表示
  • リアルタイムダッシュボード — サーバーサイドのデータ変更(株価、IoTセンサー値、アクセス解析など)を即座にクライアントへプッシュ
  • コラボレーションツール — Google Docs のような同時編集機能、ホワイトボード共有、カーソル位置の同期
  • 通知システム — 特定ユーザーやグループへのリアルタイム通知配信

インストール

# npm
npm install socket.io

# yarn
yarn add socket.io

# pnpm
pnpm add socket.io

クライアント側には別途 socket.io-client が必要です。

npm install socket.io-client

基本的な使い方

最もよく使われる Express + Socket.IO の構成例です。

// server.ts
import express from "express";
import { createServer } from "http";
import { Server, Socket } from "socket.io";

const app = express();
const httpServer = createServer(app);

const io = new Server(httpServer, {
  cors: {
    origin: "http://localhost:5173", // フロントエンドのオリジン
    methods: ["GET", "POST"],
  },
});

// 接続イベント
io.on("connection", (socket: Socket) => {
  console.log(`Client connected: ${socket.id}`);

  // クライアントからのイベントを受信
  socket.on("chat:message", (data: { user: string; text: string }) => {
    console.log(`${data.user}: ${data.text}`);

    // 送信者以外の全クライアントにブロードキャスト
    socket.broadcast.emit("chat:message", data);
  });

  // 切断イベント
  socket.on("disconnect", (reason: string) => {
    console.log(`Client disconnected: ${socket.id}, reason: ${reason}`);
  });
});

httpServer.listen(3000, () => {
  console.log("Server listening on port 3000");
});
// client.ts(ブラウザ側)
import { io } from "socket.io-client";

const socket = io("http://localhost:3000");

socket.on("connect", () => {
  console.log(`Connected: ${socket.id}`);

  // メッセージ送信
  socket.emit("chat:message", { user: "Alice", text: "Hello!" });
});

// メッセージ受信
socket.on("chat:message", (data: { user: string; text: string }) => {
  console.log(`${data.user}: ${data.text}`);
});

socket.on("disconnect", (reason: string) => {
  console.log(`Disconnected: ${reason}`);
});

よく使う API

1. Room(ルーム)— グループへの配信

ルームは特定のソケットをグループ化し、そのグループにだけメッセージを送る仕組みです。

io.on("connection", (socket: Socket) => {
  // ルームに参加
  socket.on("room:join", (roomId: string) => {
    socket.join(roomId);
    console.log(`${socket.id} joined room: ${roomId}`);

    // そのルームの全員に通知
    io.to(roomId).emit("room:notification", {
      message: `${socket.id} が参加しました`,
    });
  });

  // ルームから退出
  socket.on("room:leave", (roomId: string) => {
    socket.leave(roomId);
  });

  // 特定ルームにメッセージ送信(送信者を除く)
  socket.on("room:message", (data: { roomId: string; text: string }) => {
    socket.to(data.roomId).emit("room:message", {
      from: socket.id,
      text: data.text,
    });
  });
});

2. Acknowledgement(確認応答)— 送達確認付きの通信

emit にコールバックを渡すことで、相手側の処理結果を受け取れます。

// サーバー側
io.on("connection", (socket: Socket) => {
  socket.on(
    "file:upload",
    (data: { name: string; content: Buffer }, callback: (response: { status: string; id?: string }) => void) => {
      try {
        const fileId = saveFile(data); // ファイル保存処理
        callback({ status: "ok", id: fileId });
      } catch {
        callback({ status: "error" });
      }
    }
  );
});

// クライアント側
socket.emit(
  "file:upload",
  { name: "report.pdf", content: buffer },
  (response: { status: string; id?: string }) => {
    if (response.status === "ok") {
      console.log(`Upload complete. File ID: ${response.id}`);
    }
  }
);

v4.6.0 以降では、Promise ベースの emitWithAck も利用できます。

// クライアント側(v4.6.0+)
try {
  const response = await socket.emitWithAck("file:upload", {
    name: "report.pdf",
    content: buffer,
  });
  console.log(response.status);
} catch (err) {
  console.error("Timeout or error:", err);
}

3. Namespace(名前空間)— 関心の分離

1つの接続上で論理的にチャネルを分離できます。

// サーバー側
const io = new Server(httpServer);

// デフォルト名前空間
io.on("connection", (socket) => {
  console.log("Main namespace connected");
});

// /admin 名前空間
const adminNamespace = io.of("/admin");

adminNamespace.use((socket, next) => {
  // 認証ミドルウェア
  const token = socket.handshake.auth.token;
  if (isValidAdminToken(token)) {
    next();
  } else {
    next(new Error("Unauthorized"));
  }
});

adminNamespace.on("connection", (socket) => {
  console.log("Admin namespace connected");
  socket.on("admin:action", (data) => {
    // 管理者専用の処理
  });
});
// クライアント側
import { io } from "socket.io-client";

// /admin 名前空間に接続
const adminSocket = io("http://localhost:3000/admin", {
  auth: { token: "my-admin-token" },
});

4. Middleware(ミドルウェア)— 接続時の認証・検証

import { Server, Socket } from "socket.io";
import jwt from "jsonwebtoken";

interface AuthenticatedSocket extends Socket {
  userId?: string;
}

io.use((socket: AuthenticatedSocket, next) => {
  const token = socket.handshake.auth.token;

  if (!token) {
    return next(new Error("Authentication required"));
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as {
      userId: string;
    };
    socket.userId = decoded.userId;
    next();
  } catch {
    next(new Error("Invalid token"));
  }
});

io.on("connection", (socket: AuthenticatedSocket) => {
  console.log(`Authenticated user: ${socket.userId}`);

  // ユーザー固有のルームに自動参加(マルチデバイス対応)
  if (socket.userId) {
    socket.join(`user:${socket.userId}`);
  }
});

5. ブロードキャストの使い分け

io.on("connection", (socket: Socket) => {
  // 全クライアントに送信(送信者含む)
  io.emit("global:announcement", { message: "サーバーメンテナンスのお知らせ" });

  // 送信者以外の全クライアントに送信
  socket.broadcast.emit("user:joined", { id: socket.id });

  // 特定ルームの全員に送信(送信者含む)
  io.to("room-1").emit("room:update", { data: "..." });

  // 特定ルームの送信者以外に送信
  socket.to("room-1").emit("room:update", { data: "..." });

  // 複数ルームに同時送信
  io.to("room-1").to("room-2").emit("multi:update", { data: "..." });

  // 特定ルームを除外して送信
  io.except("room-3").emit("filtered:update", { data: "..." });

  // 特定のソケットにだけ送信
  io.to(targetSocketId).emit("private:message", { text: "Hi" });
});

類似パッケージとの比較

特徴socket.iowsµWebSockets.jsSocket.IO 代替 (Ably, Pusher)
プロトコルSocket.IO 独自 + WebSocket純粋な WebSocket純粋な WebSocket独自プロトコル
自動再接続✅ 組み込み❌ 自前実装❌ 自前実装
フォールバック (long-polling)
ルーム / 名前空間✅ 組み込み❌ 自前実装❌ 自前実装✅ (チャネル)
バイナリサポート
パフォーマンス非常に高サービス依存
スケーリングRedis Adapter で可能自前実装自前実装マネージド
学習コスト
依存関係多い最小限なし (C++ バインディング)SDK 依存

選定の目安:

  • 高レベルな機能(ルーム、再接続、認証)が欲しい → Socket.IO
  • 最小限のオーバーヘッドで WebSocket だけ使いたい → ws
  • 極限のパフォーマンスが必要 → µWebSockets.js
  • インフラ管理を避けたい → マネージドサービス (Ably, Pusher)

注意点・Tips

⚠️ Socket.IO は WebSocket ではない

Socket.IO は独自プロトコルを使用しています。素の WebSocket クライアント(ブラウザの new WebSocket()ws ライブラリ)からは接続できません。必ず socket.io-client を使用してください。

⚠️ スケーリング時は Adapter が必須

複数の Node.js プロセス(クラスタリングや複数サーバー)で運用する場合、@socket.io/redis-adapter などの Adapter を導入する必要があります。

import { Server } from "socket.io";
import { createAdapter } from "@socket.io/redis-adapter";
import { createClient } from "redis";

const pubClient = createClient({ url: "redis://localhost:6379" });
const subClient = pubClient.duplicate();

await Promise.all([pubClient.connect(), subClient.connect()]);

const io = new Server(httpServer);
io.adapter(createAdapter(pubClient, subClient));

また、ロードバランサーを使う場合は sticky session が必要です。Engine.IO の long-polling フォールバックが同一サーバーに到達する必要があるためです。

⚠️ CORS 設定を忘れない

フロントエンドとバックエンドが異なるオリジンの場合、CORS 設定が必須です。

const io = new Server(httpServer, {
  cors: {
    origin: ["http://localhost:5173", "https://myapp.example.com"],
    credentials: true,
  },
});

💡 型安全なイベント定義(TypeScript)

v4 以降、ジェネリクスでイベントの型を定義できます。

// shared/types.ts(サーバー・クライアント共有)
interface ServerToClientEvents {
  "chat:message": (data: { user: string; text: string; timestamp: number }) => void;
  "user:online": (users: string[]) => void;
}

interface ClientToServerEvents {
  "chat:message": (data: { text: string }, callback: (result: { id: string }) => void) => void;
  "room:join": (roomId: string) => void;
}

interface InterServerEvents {
  ping: () => void;
}

interface SocketData {
  userId: string;
  username: string;
}

// server.ts
const io = new Server<
  ClientToServerEvents,
  ServerToClientEvents,
  InterServerEvents,
  SocketData
>(httpServer);

io.on("connection", (socket) => {
  // socket.data に型が付く
  socket.data.userId = "user-123";

  // イベント名・引数に型補完が効く
  socket.on("chat:message", (data, callback) => {
    io.emit("chat:message", {
      user: socket.data.username,
      text: data.text,       // string 型
      timestamp: Date.now(),
    });
    callback({ id: "msg-456" }); // 型チェックされる
  });
});

💡 デバッグ方法

問題が発生した場合は `