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.io | ws | µWebSockets.js | Socket.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" }); // 型チェックされる
});
});
💡 デバッグ方法
問題が発生した場合は `