Socket.IOとは?WebSocketとの違いから実装・スケールまで
Socket.IOは、ブラウザとサーバーの間でリアルタイムの双方向通信を行うためのJavaScriptライブラリだ。チャット、通知、共同編集、オンラインゲームのように「相手の変化を待たずに即座に反映したい」処理を、WebSocketだけを直接扱うより少ないコードで実装できる。ただしSocket.IOはWebSocketそのものではなく、独自プロトコルで再接続やフォールバック、ルーム分割などを上乗せした別物だ。この違いを知らずに使うと、素のWebSocketクライアントから繋がらずに詰まる。
この記事では、Socket.IOの仕組みとWebSocketとの違いを整理したうえで、Node.jsでのサーバー・クライアント実装、イベントの送受信、ブロードキャストとルーム、複数サーバーへのスケール、本番運用のセキュリティまでを、最新のsocket.io 4.8.3系のコードで順に解説する。
まとめ:Socket.IOの要点
- Socket.IOはWebSocketの実装ではない。WebSocketをトランスポートに使いつつ、HTTPロングポーリングへの自動フォールバック・自動再接続・確認応答・ブロードキャストを独自プロトコルで上乗せしたライブラリ。素のWebSocketクライアントとは相互接続できない。
- サーバーは
socket.io、ブラウザはsocket.io-clientを使う。2026年7月時点の最新は4.8.3、動作要件はNode.js 10.2以上。 - 通信はイベント名ベース。
socket.emit()で送りsocket.on()で受ける。応答が要るときは確認応答(acknowledgement)とタイムアウトを使う。 - 複数クライアントへの一斉配信はブロードキャスト、宛先の絞り込みはルーム、論理的な分割は名前空間で行う。
- サーバーを複数台に増やすとメモリ上のルーム情報が分断されるため、Redisアダプターでイベントを橋渡しする。本番ではCORS制限と接続時認証を必ず入れる。
Socket.IOとは:リアルタイム双方向通信を担うライブラリ
通常のWebはクライアントが要求し、サーバーが応答して終わる一方向のやり取りだ。これに対しSocket.IOは、接続を張りっぱなしにしてサーバーからも任意のタイミングでデータを送れる。ページを再読み込みせずに新着メッセージや在庫の変化を届けられるのはこの常時接続のためだ。2010年に登場して以来、Node.jsのリアルタイム通信で最も広く使われるライブラリのひとつになっている。
トランスポート層の仕組み(Engine.IOとWebSocket)
Socket.IOは接続確立と維持を下位ライブラリのEngine.IOに任せている。接続はまずHTTPロングポーリングで始まり、環境がWebSocketに対応していれば透過的にWebSocketへ昇格する。プロキシやファイアウォールでWebSocketが塞がれている環境でも、ロングポーリングで接続を保てるのが強みだ。この二段構えのおかげで、開発者はトランスポートの違いを意識せずにイベントの送受信だけを書けばよい。
Socket.IOとWebSocketの違いと使い分け
最もつまずくのがここだ。公式ドキュメントは「Socket.IOはWebSocketの実装ではない」と明言している。WebSocketをトランスポートとして使う場面はあるが、各パケットに独自のメタデータを付与するため、素のWebSocketクライアントはSocket.IOサーバーに接続できず、Socket.IOクライアントも素のWebSocketサーバーには繋がらない。「WebSocketの薄いラッパー」と誤解して混ぜると必ず失敗する。素のWebSocketそのものの仕組みと実装はWebSocketとは?仕組み・ハンドシェイクから実装・運用の判断までで解説している。
| 観点 | 素のWebSocket | Socket.IO |
|---|---|---|
| 正体 | W3C/IETFの標準プロトコル | WebSocket等を使う独自プロトコルのライブラリ |
| フォールバック | なし(非対応環境は接続不可) | HTTPロングポーリングへ自動フォールバック |
| 自動再接続 | 自前で実装 | 標準搭載(指数バックオフ・ハートビート) |
| 確認応答 | なし | あり(タイムアウト付き) |
| ブロードキャスト・ルーム | なし | 標準搭載 |
| 相互接続 | Socket.IOサーバーには繋がらない | 素のWebSocketサーバーには繋がらない |
判断基準はシンプルだ。再接続やフォールバック、ルーム分割を自前で作りたくないならSocket.IOが速い。逆に、最小のオーバーヘッドで軽量に通したい、あるいはSSEやWebTransportも含めて技術選定から比べたい場合は素のWebSocketや他方式が候補になる。プロトコルレベルの比較はSSEとWebSocketの違いと使い分けで整理しているので、方式選定から入る場合はあわせて読んでほしい。
Socket.IOのインストールと動作要件
サーバー側はsocket.io、ブラウザ側はsocket.io-clientをインストールする。両者はバージョンを揃えるのが原則で、2026年7月時点の最新はいずれも4.8.3だ。動作にはNode.js 10.2以上が必要で、それより古いバージョンはサポートされない。Node.jsのバージョン管理はNodebrewによるインストール・切り替えを、採用するLTSの選定はNode.js 26のLTSスケジュールを参照してほしい。
# サーバー側
npm install socket.io
# クライアント側(バンドラを使う場合)
npm install socket.io-client
ブラウザ用のクライアントは、サーバーを起動すると/socket.io/socket.io.jsのパスで自動配信されるため、CDNやnpmを使わずにスクリプトタグで読み込むこともできる。
サーバーとクライアントの実装(接続の確立)
まずは接続を張るところまでを作る。サーバーはNode.jsのHTTPサーバー(ここではExpressを併用)にSocket.IOを載せ、クライアントはio()で接続する。ブラウザとサーバーのオリジンが異なる場合は、サーバー側でcorsを設定しないと接続がブロックされる点に注意する。
サーバー側のセットアップ
const express = require('express');
const { createServer } = require('node:http');
const { Server } = require('socket.io');
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: { origin: 'https://example.com', methods: ['GET', 'POST'] }
});
io.on('connection', (socket) => {
console.log('接続:', socket.id);
socket.on('disconnect', (reason) => {
console.log('切断:', socket.id, reason);
});
});
httpServer.listen(3000, () => console.log('listening on :3000'));
io.on('connection', ...)は新しいクライアントが繋がるたびに発火し、コールバックのsocketがそのクライアント個別の接続を表す。socket.idは接続ごとに振られる識別子で、再接続すると変わる。disconnectイベントのreasonには切断理由が入るので、ログや再接続処理の分岐に使える。
クライアント側の接続
import { io } from 'socket.io-client';
const socket = io('https://example.com');
socket.on('connect', () => {
console.log('接続完了 id=', socket.id);
});
socket.on('disconnect', (reason) => {
console.log('切断:', reason);
});
クライアントは接続が切れても自動で再接続を試みる。再接続の間に送ろうとしたイベントはバッファされ、再接続後に自動送信される。この再接続とバッファリングを自前で書かずに済むのがSocket.IOを使う大きな理由のひとつだ。
イベントの送受信と確認応答
Socket.IOの通信はイベント名ベースだ。emit('イベント名', データ)で送り、同じイベント名をon()で受ける。イベント名とデータ構造は開発者が自由に決められ、オブジェクトや配列もそのまま送れる。ここではチャットの1メッセージを往復させる。
基本の送信と受信
// サーバー: クライアントから受け取り、全員へ送り返す
io.on('connection', (socket) => {
socket.on('chat message', (msg) => {
io.emit('chat message', { id: socket.id, text: msg });
});
});
// クライアント: 送信と受信
socket.emit('chat message', 'こんにちは');
socket.on('chat message', (payload) => {
console.log(payload.id, payload.text);
});
確認応答(acknowledgement)で到達を確かめる
送りっぱなしでは相手が受け取れたか分からない。返事が欲しいときは、emitの最後にコールバックを渡す確認応答を使う。さらにtimeout()を挟むと、指定ミリ秒以内に応答がなければエラーとして扱える。ネットワークが不安定な本番では、重要な送信はタイムアウト付きにしておくと取りこぼしを検知できる。
// クライアント: 5秒以内に応答がなければ err が入る
socket.timeout(5000).emit('save', data, (err, response) => {
if (err) {
console.error('サーバーが期限内に応答しませんでした');
} else {
console.log('保存結果:', response.status);
}
});
// サーバー: 第2引数のコールバックを呼び返すと応答になる
socket.on('save', (data, callback) => {
// 保存処理 ...
callback({ status: 'ok' });
});
ブロードキャスト・ルーム・名前空間
複数クライアントを相手にする配信は、宛先の広さで書き分ける。全員か、送信者以外か、特定グループかを取り違えると、自分の発言が自分に二重表示されるといった不具合になる。
ブロードキャスト(全員/送信者以外)
io.emit('notice', 'サーバーからの全体通知'); // 接続中の全員(送信者含む)
socket.broadcast.emit('joined', socket.id); // 送信者以外の全員
ルームで宛先を絞る
ルームはサーバー側だけの概念で、任意の名前でクライアントをグループ化できる。チャットルームや、ユーザーIDごとの通知配信に使う。joinで参加、to()で宛先指定、leaveで退出する。
io.on('connection', (socket) => {
socket.join('room:general'); // ルームに参加
io.to('room:general').emit('notice', 'ようこそ'); // そのルーム全員へ
// socket.leave('room:general'); // 退出
});
名前空間でロジックを分割する
名前空間は1本の接続の上でアプリを論理分割する仕組みだ。管理画面用と一般用でイベントやミドルウェアを分けたいときに使う。ルームが「同じ名前空間内のグループ」なのに対し、名前空間は「別のエンドポイント」に近い。
const adminNs = io.of('/admin');
adminNs.on('connection', (socket) => {
adminNs.emit('metrics', getMetrics());
});
複数サーバーへのスケール(Redisアダプター)
ここが実運用で最初に詰まる落とし穴で、入門記事では手薄になりやすい。Socket.IOのルームや接続情報は各サーバープロセスのメモリ上にある。ロードバランサーの後ろにサーバーを2台以上並べると、Aサーバーに繋いだ利用者とBサーバーに繋いだ利用者は別々のメモリを見るため、io.emit()が同じサーバーの利用者にしか届かない。1台構成では動いていたブロードキャストが、増設した途端に片方に届かなくなる。
解決策がアダプターだ。@socket.io/redis-adapter(2026年7月時点で8.3.0)を挟むと、各サーバーが発行したイベントをRedisのPub/Sub経由で他サーバーへ橋渡しし、全台にまたがって配信できるようになる。
const { createClient } = require('redis');
const { createAdapter } = require('@socket.io/redis-adapter');
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
// CommonJSではトップレベルawaitが使えないため非同期関数で包む
(async () => {
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
})();
あわせて、ロードバランサーでは同一クライアントを同じサーバーへ振り分けるスティッキーセッションを有効にする。ロングポーリングのハンドシェイクが複数サーバーに分散すると接続が確立しないためだ。「まず1台で作り、増設時にアダプターとスティッキーセッションを足す」という順序を最初から想定しておくと移行が楽になる。
本番運用のセキュリティと信頼性
Socket.IOは既定では接続元を制限しない。開発中にcorsを緩く開けたまま公開すると、任意のサイトからイベントを送り込まれる。本番では次の2点を最低ラインとして入れる。
CORSと接続時認証
CORSは許可するオリジンを明示的に列挙する。誰が接続してよいかの認証は、接続確立時に一度だけ走るミドルウェアで行うのが定石だ。socket.handshake.authでクライアントが渡したトークンを検証し、無効ならnext()にエラーを渡して接続を拒否する。
// サーバー: 接続時にトークンを検証
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (isValidToken(token)) {
next();
} else {
next(new Error('認証に失敗しました'));
}
});
// クライアント: 接続時に認証情報を渡す
const socket = io('https://example.com', {
auth: { token: localStorage.getItem('token') }
});
切断と再接続を前提に設計する
モバイル回線では切断と再接続が頻発する。再接続時にsocket.idは変わるため、利用者の同一性はIDに依存せず自前のユーザーIDで管理する。また再接続直後にサーバー側の状態(未読、参加ルーム)を復元する処理を用意しておく。Socket.IOはバッテリー消費の観点から、モバイルアプリのバックグラウンド常駐用途には推奨されておらず、その場合はプッシュ通知基盤(FCMなど)と併用するのが公式の指針だ。
よくある質問
Socket.IOとWebSocketはどちらを使うべきですか?
再接続・フォールバック・ルームを自前で実装したくなければSocket.IOが手早い。逆に依存を最小にしたい、標準プロトコルだけで軽量に通したい場合は素のWebSocketを選ぶ。両者は互換性がないため、後から乗り換えるとサーバー・クライアント双方の書き換えが必要になる点は最初に把握しておく。
Socket.IOの最新バージョンと必要なNode.jsは?
2026年7月時点の最新はsocket.io・socket.io-clientともに4.8.3、動作にはNode.js 10.2以上が必要だ。サーバーとクライアントはメジャー・マイナーを揃えて使う。正確な最新版は公式のリリースノートで確認してほしい。
Socket.IOはJavaScript以外でも使えますか?
公式・コミュニティのクライアント実装があり、Java(socket.io-client-java)、Swift、Python、C++などから接続できる。ただしSocket.IOの独自プロトコルに対応した実装が必要で、素のWebSocketライブラリでは接続できない。サーバー側は基本的にNode.jsで動かす。
ブロードキャストが一部のクライアントに届きません。
サーバーを複数台に増やした後にこの症状が出たら、アダプター未設定が原因の可能性が高い。@socket.io/redis-adapterを導入してサーバー間でイベントを共有し、ロードバランサーでスティッキーセッションを有効にすると解消する。
接続はできるのにイベントが受信できません。
送信側と受信側でイベント名が一致しているか、まず確認する。次に、宛先の指定(io.emit/socket.broadcast.emit/io.to(room).emit)が意図と合っているかを見直す。ルームに参加していないクライアントにはto(room)のイベントは届かない。