Socket.IO 4.8
概要
このスキルには、Socket.IO 4.8の間違えやすいルールがまとめられています。一時的なsocket.idをユーザーキーとして使ってしまう、リスナーが二重登録される、timeout()で確認応答のコールバックの形状が変わる、CORSがWebSocketを保護しない、ロードバランサー配下でスティッキーセッションを忘れる、v2やv3のAPIが動作しなくなる――こうした落とし穴を避けるための情報が詰まっています。すべての主張には、その根拠となるページURLが付記されています。参照ファイルには各ドメインの詳細が含まれています。
対象バージョン
Socket.IO 4.8(サーバー socket.io、クライアント socket.io-client)、ドキュメント v4。JS クライアント 3.x および 4.x は、サーバー 3.x および 4.x と通信できます。https://socket.io/docs/v4/client-installation/#version-compatibility
クイックスタート
- 作成したHTTPサーバーにSocket.IOをアタッチし、そのサーバーに対して
listenを呼び出します。Expressのappに対してlistenを呼び出してはいけません。app.listen()は別のHTTPサーバーを作成し、Socket.IOはそのサーバーにアタッチされません。https://socket.io/docs/v4/server-initialization/#with-express
import express from "express";
import { createServer } from "node:http";
import { Server } from "socket.io";
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, { cors: { origin: ["https://example.com"] } });
io.on("connection", (socket) => {
socket.on("hello", (arg, callback) => callback("got it"));
});
httpServer.listen(3000);
https://socket.io/docs/v4/server-initialization/#with-express
- クライアント: 同一オリジンでは
io()、クロスオリジンではio("https://server-domain.com")を使用します(サーバーでCORSを有効にする必要があります)。インポートはimport { io } from "socket.io-client"で行います。https://socket.io/docs/v4/client-initialization/#from-a-different-domain
import { io } from "socket.io-client";
const socket = io("https://server-domain.com", { auth: { token: "abc" } });
const answer = await socket.timeout(5000).emitWithAck("hello", "world");
https://socket.io/docs/v4/client-api/#socketemitwithackeventname-args
- 型定義は
socket.io-clientに同梱されています。@types/socket.io-clientをインストールしないでください。同梱の型定義と競合します。https://socket.io/docs/v4/client-installation/#from-npm
ルールと落とし穴
接続と識別
- Socket.IOはWebSocketの実装ではありません。プレーンなWebSocketクライアントはSocket.IOサーバーに接続できず、Socket.IOクライアントはプレーンなWebSocketサーバーに接続できません。https://socket.io/docs/v4/#what-socketio-is-not
- 接続状態の復旧が有効でない限り、
socket.idは再接続やページ更新のたびに変更され、タブごとに異なり、メッセージキューもありません。ユーザーを識別するには、Cookieやauthで送信する独自のセッションIDを使用し、ユーザーのすべてのタブにリーチするには、ユーザーIDを名前としたルームに参加させます。https://socket.io/docs/v4/server-socket-instance/#socketid
io.on("connection", async (socket) => {
const userId = await computeUserIdFromHeaders(socket.handshake.headers);
socket.join(userId);
io.to(userId).emit("hi");
});
https://socket.io/docs/v4/rooms/#sample-use-cases
- 接続ミドルウェアでの認証は
socket.handshake.authを使用します。すべてのコードパスでnext()を呼び出さないと、接続はタイムアウトで閉じられるまでハングしたままになります。next(err)はconnect_errorとしてクライアントに到達し、err.dataで詳細を伝えます。https://socket.io/docs/v4/middlewares/
io.use((socket, next) => {
if (isValid(socket.handshake.auth.token)) return next();
const err = new Error("not authorized");
err.data = { content: "Please retry later" };
next(err);
});
https://socket.io/docs/v4/middlewares/#handling-middleware-error
- ミドルウェアによって拒否された接続は再試行されません。
connect_errorにおいて、socket.active === falseは、サーバーによって接続が拒否されたことを意味し、クライアントは再接続するために自分でsocket.connect()を呼び出す必要があります。https://socket.io/docs/v4/client-socket-instance/#connect_error - クライアントのリスナーはトップレベルで登録し、決して
connectハンドラ内で登録してはいけません。connectは再接続のたびに再度発火し、そのたびに重複したリスナーが追加されます。https://socket.io/docs/v4/client-socket-instance/#connect - サーバーでは、
connectionハンドラ内で、awaitの前に同期的にsocket.on(...)を登録します。そうしないと、初期のパケットがハンドラに届きません。https://socket.io/docs/v4/troubleshooting-connection-issues/#delayed-event-handler-registration - Expressミドルウェアは
io.engine.use(...)に設定し、io.use(...)には設定しません。https://socket.io/docs/v4/middlewares/#compatibility-with-express-middleware
イベントと確認応答
- オブジェクトはそのまま渡します。ペイロードに対して
JSON.stringify()を決して行わないでください。Dateは文字列として到着し、MapやSetは手動で変換する必要があります。https://socket.io/docs/v4/emitting-events/#basic-emit - 確認応答コールバックは最後の引数です。
timeout()なしの場合は(response)を受け取り、timeout(ms)ありの場合は(err, response)を受け取ります。https://socket.io/docs/v4/emitting-events/#with-timeout tryブロック内でawait socket.timeout(ms).emitWithAck(...)を使用することを推奨します。タイムアウト時にrejectされます。受信側はコールバックを1つの引数で呼び出します。https://socket.io/docs/v4/client-api/#socketemitwithackeventname-args- 確認応答付きのブロードキャストには
timeout()が必要で、クライアントごとに1つの応答を返します。https://socket.io/docs/v4/server-api/#serveremiteventname-args
const responses = await io.timeout(10000).emitWithAck("some-event");
https://socket.io/docs/v4/server-api/#serveremitwithackeventname-args
- リスナーには組み込みのエラーハンドリングやバリデーションはありません。各ハンドラでエラーをキャッチし、スキーマライブラリでペイロードを検証し、コールバックを使用する前に
typeof callback === "function"を確認してください。https://socket.io/docs/v4/listening-to-events/#validation connect、connect_error、disconnect、disconnecting、newListener、removeListenerをアプリケーションイベントとして決して使用しないでください。socket.emit("disconnecting")のように予約名を発行するとエラーがスローされます。https://socket.io/docs/v4/emit-cheatsheet/
ブロードキャストとルーム
socket.to(room)とsocket.broadcastは送信者を除外しますが、io.to(room)は除外しません。socket.to(socket.id).emit()は何も送信しません。io.to(socket.id).emit()を使用してください。https://socket.io/docs/v4/server-api/#sockettoroom- チェーンされた
to()呼び出しは和集合となり、各ソケットはイベントを1回だけ受信します。https://socket.io/docs/v4/rooms/#joining-and-leaving - 各ブロードキャスト修飾子は新しい不変の演算子を返します。
io.to()を呼び出した後にio.emit()を呼び出す代わりに、変数に保持してください。https://socket.io/docs/v4/migrating-from-3-x-to-4-0/#ioto-is-now-immutable - ルーム名は型変換されません。
socket.join(42)はio.to("42")と一致しません。https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#room-names-are-not-coerced-to-string-anymore - ルームは切断時に自動的に退出します。
disconnectingイベントでルームを読み取ってください。disconnectイベントではsocket.roomsは空です。https://socket.io/docs/v4/rooms/#disconnection - アダプターは
io.of("/").adapterで読み取ります。ドキュメントでは、io.adapter()はセッターであり、この目的には使用できないと警告しています。https://socket.io/docs/v4/adapter/#api
配信
- デフォルトの保証は「最大1回」であり、サーバーはメッセージを保存しません。切断されたクライアントは、ブロードキャストを永久に失います。
authで送信されたオフセットを使用してデータベースからリプレイするか、接続状態の復旧を有効にしてください。https://socket.io/docs/v4/delivery-guarantees#at-most-once - クライアントはオフライン中にemitをバッファリングし、再接続時にフラッシュします。
socket.connectedを確認するか、破棄しても問題のないデータにはsocket.volatile.emit(...)を使用してください。https://socket.io/docs/v4/client-offline-behavior/#buffered-events - クライアントからサーバーへの「最低1回」の配信:
io({ retries: 3, ackTimeout: 10000 })。厳密に1回の場合は、一意制約のもとでクライアント生成のIDを保存します。成功時と重複時には確認応答を返し、それ以外のエラーでは確認応答を返さないようにすることで、クライアントをタイムアウトさせて再試行させます。https://socket.io/docs/v4/tutorial/step-8 - 接続状態の復旧はオプトイン方式で、上限のある
maxDisconnectionDurationが必要であり、失敗する可能性があります。socket.recoveredで分岐してください。Redis (Pub/Sub) アダプターはこの機能をサポートしておらず、Redis Streams アダプターはサポートしています。https://socket.io/docs/v4/connection-state-recovery
const io = new Server(httpServer, {
connectionStateRecovery: { maxDisconnectionDuration: 2 * 60 * 1000, skipMiddlewares: false },
});
https://socket.io/docs/v4/connection-state-recovery#usage
CORSとデプロイメント
- CORSはデフォルトでオフです。オプションは
cors.originであり、originsではありません。origin: "*"とcredentials: trueの組み合わせはブラウザで失敗します。明示的なオリジンを列挙してください。https://socket.io/docs/v4/server-options/#cors - CORSはHTTPロングポーリング上のブラウザのみを対象とし、WebSocketは対象としません。
allowRequestで接続を制限してください。https://socket.io/docs/v4/handling-cors/ - プロキシは
UpgradeヘッダーとConnectionヘッダーを転送する必要があり、その読み取りタイムアウトはpingInterval + pingTimeout(デフォルト45秒)を超えている必要があります。そうしないと、クライアントはtransport closeを認識します。https://socket.io/docs/v4/reverse-proxy/#nginx - 複数のサーバーでHTTPロングポーリングが有効な場合、アダプターの種類に関わらず、スティッキーセッションが依然として必要です(代替案はロングポーリングの無効化またはクライアント側のロードバランシングです)。https://socket.io/docs/v4/using-multiple-nodes/ および https://socket.io/docs/v4/adapter/
- スティッキーセッションがない場合、クライアントは
{"code":1,"message":"Session ID unknown"}というHTTP 400応答を受け取ります。https://socket.io/docs/v4/troubleshooting-connection-issues/#you-didnt-enable-sticky-sessions-in-a-multi-server-setup path(デフォルトは/socket.io/)は両側で一致している必要があります。io("/my-path/")はパスではなく名前空間を選択します。https://socket.io/docs/v4/troubleshooting-connection-issues/#the-request-path-does-not-match-on-both-sidesio.close()で閉じます。これによりHTTPサーバーも閉じられます。https://socket.io/docs/v4/server-api/#serverclosecallback
参照ファイル
| ファイル | 参照先 |
|---|---|
| references/server-setup.md | インストール、Express、Koa、Fastify、Hono、HTTPS、HTTP/2、Bun、uWebSockets.jsによる初期化、サーバーオプションとデフォルト値、バンドル、ハンドラのレイアウト |
| references/middleware-and-auth.md | 接続、ソケットおよびエンジンミドルウェア、資格情報、ユーザー識別、ヘッダーフック、allowRequest、クライアント証明書 |
| references/cors-and-proxies.md | CORSオプションとエラー、Cookie、nginx、Apache、Caddy、http-proxy、クラウドロードバランサー、wsとのポート共有 |
| references/server-api.md | ServerおよびSocketインスタンス、io上の名前空間、ユーティリティメソッド、serverSideEmit、切断理由、エンジンイベントとエラーコード、クローズ |
| references/client.md | バンドル、初期化、Socketイベント、connect_errorとdisconnect、Managerと多重化、すべてのクライアントオプション、バンドラー |
| references/events-rooms-namespaces.md | シリアライゼーション、確認応答、キャッチオールリスナー、バリデーション、エラーハンドリング、ブロードキャスト、ルーム、動的な名前空間 |
| references/delivery-and-recovery.md | 配信保証、再試行、厳密に1回、オフセットリプレイ、接続状態の復旧 |
| references/multiple-nodes.md | スティッキーセッション、複数ノードのプロキシ設定、代替案、Node.jsクラスタ、PM2 |
| references/adapters.md | Redis、シャード化Redis、Redis Streams、Postgres、MongoDB、Cluster、AWS SQS、Azure、GCPアダプター、機能マトリックス、エミッター |
| references/performance-and-operations.md | ネイティブアドオン、wsEngine、メモリ、カスタムパーサー、OS制限、負荷テスト、Admin UI、デバッグログ |
| references/typescript.md | サーバー、クライアント、名前空間でのイベントインターフェース、SocketData、型付きテスト |
| references/testing-and-troubleshooting.md | サーバー統合テスト、フェイクソケットを使ったクライアントテスト、接続失敗、切断、ロングポーリングのスタック |
| references/migrations-and-protocol.md | v2からv3、v3からv4の変更点、allowEIO3を使ったローリングアップグレード、Engine.IOおよびSocket.IOワイヤープロトコル |
古いパターン
| 古い使用法 | 新しい使用法 | ソース |
|---|---|---|
origins または handlePreflightRequest オプション | cors: { origin: [...] } | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#cors-handling |
io.set("authorization", fn) | io.use((socket, next) => ...) | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#ioset-is-removed |
Socketのqueryオプション内のトークン | authオプション、socket.handshake.authとして読み取る | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
接続拒否時のクライアントerrorイベント | connect_error、err.messageとerr.dataを伴う | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
Socket上のsocket.on("reconnect_attempt") | Manager上のsocket.io.on("reconnect_attempt") | https://socket.io/docs/v4/client-socket-instance/#events |
レイテンシ取得のためのクライアントpongイベント | インターバルでのsocket.volatile.emit("ping", cb) | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#no-more-pong-event-for-retrieving-latency |
Namespace.connected オブジェクト | Namespace.sockets(Map) | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
Object.keys(socket.rooms) | socket.roomsはSet | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
socket.join(room, cb) | socket.join(room)、非同期アダプターでawait | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
Namespace.clients(cb)(v2)、次にallSockets()(v3、次のメジャーで削除予定) | fetchSockets() | https://socket.io/docs/v4/server-api/#namespaceallsockets |
socket.binary(false) | カスタムparser | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
socket.emit("a").emit("b") | 個別の呼び出し、emit()はtrueを返す | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
socket.ioまたはsocket.io-clientのデフォルトインポート | 名前付きインポート(import { Server } from "socket.io"、import { io } from "socket.io-client") | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
io.of("/").adapter(v4) | 変更なし |