はじめに
チャット、通知、ライブダッシュボード、共同編集——「サーバーからクライアントへ、今すぐデータを届けたい」という要件に直面したことはありませんか?
「WebSocketを使えばいいのでは」と思うかもしれません。確かにその通りなのですが、素のWebSocketで実装を始めると、すぐに現実的な壁にぶつかります。切断されたらどう再接続するのか。再接続までの間に発生したイベントはどうするのか。特定のユーザーグループだけに送信するには? プロキシがWebSocketを通してくれない環境では?
これらの「WebSocketの周辺で必ず必要になる面倒ごと」をまとめて引き受けてくれるのが、今回紹介するSocket.IOです。GitHubスター数63k超、登場から10年以上経った今もリアルタイム通信の定番であり続けているライブラリの実力を見ていきましょう。
Socket.IOとは
Socket.IOは、クライアントとサーバー間で低遅延・双方向・イベントベースの通信を実現するライブラリです。Node.js製のサーバーと、ブラウザやNode.jsで動くJavaScriptクライアントが公式に提供されているほか、Python・Java・Swift・Kotlinなど多数の言語の実装が存在します。
「Socket.IO = WebSocketのラッパー」と説明されることもありますが、正確には少し違います。Socket.IOはWebSocketを主要なトランスポート(通信手段)として使いつつ、その上に再接続・バッファリング・ブロードキャストといった実用機能を積み上げた、より高レベルな通信フレームワークです。
主な特徴
- HTTP long-pollingへの自動フォールバック - WebSocketが使えない環境(古いプロキシや制限の厳しい企業ネットワークなど)では、自動的にHTTP long-pollingに切り替えて接続を維持します
- 自動再接続 - ハートビートで接続状態を監視し、切断を検知すると指数バックオフで自動的に再接続します。自前でリトライ処理を書く必要はありません
- パケットバッファリング - 切断中に送信されたイベントは自動的にバッファされ、再接続後にまとめて届きます
- 確認応答(Acknowledgement) - イベントが相手に届いたことを、コールバックやPromiseで確認できます
- RoomsとNamespaces - 「このチャットルームの参加者だけに送る」「管理者チャネルだけ分離する」といったグルーピングが標準機能でできます
- 軽量 - ブラウザ用バンドルは圧縮後わずか約10.4kBです
インストール
サーバー側とクライアント側でパッケージが分かれています。
# サーバー側
npm install socket.io
# クライアント側
npm install socket.io-client
yarnやpnpmでも同様にインストールできます。
pnpm add socket.io # サーバー
pnpm add socket.io-client # クライアント
基本的な使い方
まずは最小構成で「クライアントとサーバーがイベントをやり取りする」ところまで動かしてみましょう。
サーバー側(Node.js)
// server.js
import { createServer } from "node:http";
import { Server } from "socket.io";
const httpServer = createServer();
const io = new Server(httpServer, {
cors: {
origin: "http://localhost:5173", // フロントエンドのオリジンを許可
},
});
io.on("connection", (socket) => {
console.log(`クライアント接続: ${socket.id}`);
// クライアントからのイベントを受信
socket.on("message", (data) => {
console.log("受信:", data);
// 送信元以外の全クライアントへブロードキャスト
socket.broadcast.emit("message", data);
});
socket.on("disconnect", (reason) => {
console.log(`切断: ${socket.id} (${reason})`);
});
});
httpServer.listen(3000, () => {
console.log("Socket.IOサーバー起動 http://localhost:3000");
});
クライアント側(ブラウザ)
// client.js
import { io } from "socket.io-client";
const socket = io("http://localhost:3000");
socket.on("connect", () => {
console.log(`接続完了: ${socket.id}`);
socket.emit("message", { text: "こんにちは!" });
});
socket.on("message", (data) => {
console.log("受信:", data.text);
});
socket.on("disconnect", () => {
console.log("切断されました(自動で再接続を試みます)");
});
これだけで双方向通信が動きます。注目してほしいのは、イベント名を自由に決められる点です。素のWebSocketではonmessageで受け取った文字列を自前でパースして振り分ける必要がありますが、Socket.IOではsocket.on("イベント名", ...)とsocket.emit("イベント名", ...)で自然にルーティングされます。しかもオブジェクトを渡せば自動でシリアライズしてくれます。
確認応答(Acknowledgement)を使う
「このイベント、ちゃんと届いた?」を確認したい場面は意外と多いものです。Socket.IOならコールバック1つで実現できます。
// クライアント側: 第3引数にコールバックを渡す
socket.emit("save-note", { title: "買い物メモ" }, (response) => {
console.log(response.status); // "ok"
});
// サーバー側: 最後の引数として受け取る
socket.on("save-note", (data, callback) => {
// 保存処理...
callback({ status: "ok" });
});
タイムアウト付きのPromiseとして扱うこともできます。
try {
const response = await socket.timeout(5000).emitWithAck("save-note", {
title: "買い物メモ",
});
console.log(response.status);
} catch (err) {
console.error("5秒以内に応答がありませんでした");
}
実践的なユースケース
Roomsでチャットルームを実装する
Socket.IOの真価が発揮されるのがRooms機能です。ソケットを任意の「部屋」に参加させ、その部屋のメンバーだけにイベントを送信できます。チャットルームの実装例を見てみましょう。
// server.js
io.on("connection", (socket) => {
// ルームへの参加
socket.on("join-room", (roomId, callback) => {
socket.join(roomId);
// 参加者本人以外のルームメンバーに通知
socket.to(roomId).emit("user-joined", { socketId: socket.id });
callback({ status: "ok" });
});
// ルーム内へのメッセージ送信
socket.on("room-message", ({ roomId, text }) => {
// 送信者を含むルーム全員に配信
io.to(roomId).emit("room-message", {
from: socket.id,
text,
sentAt: Date.now(),
});
});
// ルームからの退出
socket.on("leave-room", (roomId) => {
socket.leave(roomId);
socket.to(roomId).emit("user-left", { socketId: socket.id });
});
});
クライアント側はこうなります。
// ルームに参加してメッセージを送る
await socket.emitWithAck("join-room", "room-42");
socket.on("room-message", ({ from, text }) => {
console.log(`[room-42] ${from}: ${text}`);
});
socket.emit("room-message", { roomId: "room-42", text: "よろしく!" });
「特定のグループにだけ配信する」仕組みを自前で作ると、参加者リストの管理や退出時のクリーンアップなど考えることが山ほどありますが、Roomsを使えばjoinとtoだけで完結します。切断時にルームから自動的に外れるのも地味にありがたいポイントです。
再接続時の状態復元
リアルタイムアプリで避けて通れないのが「ユーザーの回線が一瞬切れた」ケースです。Socket.IOは自動再接続してくれますが、切断中に見逃したデータの復元はアプリ側の設計次第です。よくあるパターンは、再接続時に最終受信時刻以降のデータをサーバーへ問い合わせる方法です。
let lastReceivedAt = 0;
socket.on("room-message", (msg) => {
lastReceivedAt = msg.sentAt;
renderMessage(msg);
});
// 再接続時に取りこぼしを取得
socket.io.on("reconnect", async () => {
const missed = await socket.emitWithAck("fetch-messages-since", {
roomId: "room-42",
since: lastReceivedAt,
});
missed.forEach(renderMessage);
});
さらにSocket.IO 4.6以降にはConnection State Recoveryという機能があり、短時間の切断であればサーバー側がルーム参加状態や未送信イベントを復元してくれます。
const io = new Server(httpServer, {
connectionStateRecovery: {
// 2分以内の再接続なら状態を復元
maxDisconnectionDuration: 2 * 60 * 1000,
},
});
こんな場面で活躍します
- チャット・コメント機能 - Roomsとブロードキャストの組み合わせが最も活きる王道ユースケースです
- ライブダッシュボード - サーバー側のメトリクス更新を
io.emitで全クライアントに配信するだけで、リアルタイムなグラフ更新が実現できます - 通知システム - ユーザーIDごとのRoomを作っておけば、
io.to(userId).emit(...)で特定ユーザーへのプッシュ通知が送れます - マルチプレイヤーゲームや共同編集 - 低遅延な双方向通信と確認応答の組み合わせで、状態同期のベースとして使えます
まとめ
Socket.IOについて、基本の双方向通信からRooms・確認応答・再接続時の状態復元まで見てきました。
素のWebSocketとの違いは「自分で書かなくていいコードの量」に集約されます。再接続、フォールバック、イベントルーティング、グループ配信——これらを自前で堅牢に実装するのは相当な労力ですが、Socket.IOなら最初から揃っています。10年以上定番であり続けているのは、この「リアルタイム通信の面倒ごとを全部引き受ける」という設計思想が、今も色あせていないからでしょう。
まずは本記事のサーバーとクライアントのコードをコピーして、手元で2つのブラウザタブを開いてみてください。片方の操作がもう片方へ即座に届く体験は、想像以上に楽しいはずです。慣れてきたら、公式ドキュメントのNamespacesやRedisアダプタによるスケールアウトにも挑戦してみてください。