2.x から 3.0 への移行
このリリースでは、Socket.IOライブラリのほとんどの不整合を修正し、エンドユーザーにとってより直感的な動作を提供することを目指しています。これは長年にわたるコミュニティからのフィードバックの結果です。ご協力いただいた皆様に感謝いたします!
TL;DR: いくつかの破壊的な変更により、v2クライアントはv3サーバーに接続できません(その逆も同様です)
更新: Socket.IO 3.1.0から、v3サーバーはv2クライアントと通信できるようになりました。下記に詳細情報があります。ただし、v3クライアントはv2サーバーに接続できません。
詳細については、以下を参照してください。
変更点の完全なリストを以下に示します。
- io.set() は削除されました
- デフォルトの名前空間への暗黙的な接続はなくなりました
- Namespace.connected は Namespace.sockets に名前が変更され、Map になりました
- Socket.rooms は Set になりました
- Socket.binary() は削除されました
- Socket.join() と Socket.leave() は同期処理になりました
- Socket.use() は削除されました
- ミドルウェアエラーは Error オブジェクトを emit するようになりました
- Manager クエリオプションと Socket クエリオプションの明確な区別を追加
- Socket インスタンスは、その Manager が emit したイベントを転送しなくなりました
- Namespace.clients() は Namespace.allSockets() に名前が変更され、Promise を返すようになりました
- クライアントバンドル
- 遅延を取得するための "pong" イベントはなくなりました
- ES モジュール構文
emit()
チェインは使用できなくなりました- ルーム名は文字列に強制変換されなくなりました
設定
より適切なデフォルト値
maxHttpBufferSize
のデフォルト値が100MB
から1MB
に減少しました。- WebSocket の permessage-deflate 拡張 は、デフォルトで無効になりました。
- 許可されるドメインを明示的にリストする必要があります(CORS については、下記を参照)。
withCredentials
オプションは、クライアント側でデフォルトでfalse
になりました。
CORS の処理
v2 では、Socket.IO サーバーは、クロスオリジンリソース共有 (CORS) を許可するために必要なヘッダーを自動的に追加しました。
この動作は便利でしたが、セキュリティの面では優れていませんでした。なぜなら、origins
オプションで別途指定しない限り、すべてのドメインが Socket.IO サーバーにアクセスできることを意味していたからです。
そのため、Socket.IO v3 からは
- CORS はデフォルトで無効になりました。
- 承認されたドメインのリストを提供するために使用される
origins
オプションと、Access-Control-Allow-xxx
ヘッダーを編集するために使用されるhandlePreflightRequest
オプションは、cors パッケージに転送されるcors
オプションに置き換えられました。
オプションの完全なリストは こちらにあります。
以前
const io = require("socket.io")(httpServer, {
origins: ["https://example.com"],
// optional, useful for custom headers
handlePreflightRequest: (req, res) => {
res.writeHead(200, {
"Access-Control-Allow-Origin": "https://example.com",
"Access-Control-Allow-Methods": "GET,POST",
"Access-Control-Allow-Headers": "my-custom-header",
"Access-Control-Allow-Credentials": true
});
res.end();
}
});
現在
const io = require("socket.io")(httpServer, {
cors: {
origin: "https://example.com",
methods: ["GET", "POST"],
allowedHeaders: ["my-custom-header"],
credentials: true
}
});
デフォルトでは Cookie を使用しない
以前のバージョンでは、io
Cookie がデフォルトで送信されていました。この Cookie はスティッキセッションを有効にするために使用できますが、複数のサーバーと HTTP ロングポーリングが有効になっている場合に必要です(詳細はこちら)。
ただし、この Cookie は単一サーバーのデプロイメント、IP ベースのスティッキセッションなど、いくつかのケースでは必要ないため、明示的に有効にする必要があります。
以前
const io = require("socket.io")(httpServer, {
cookieName: "io",
cookieHttpOnly: false,
cookiePath: "/custom"
});
現在
const io = require("socket.io")(httpServer, {
cookie: {
name: "test",
httpOnly: false,
path: "/custom"
}
});
その他のすべてのオプション(ドメイン、maxAge、sameSite など)がサポートされるようになりました。オプションの完全なリストについては、こちらを参照してください。
API の変更
下位互換性のない変更点を以下に示します。
io.set() は削除されました
このメソッドは 1.0 リリースで非推奨となり、下位互換性のために保持されていました。現在は削除されています。
ミドルウェアに置き換えられました。
以前
io.set("authorization", (handshakeData, callback) => {
// make sure the handshake data looks good
callback(null, true); // error first, "authorized" boolean second
});
現在
io.use((socket, next) => {
var handshakeData = socket.request;
// make sure the handshake data looks good as before
// if error do this:
// next(new Error("not authorized"));
// else just call next
next();
});
デフォルトの名前空間への暗黙的な接続はなくなりました
この変更は、マルチプレクシング機能(Socket.IO では名前空間と呼ばれています)のユーザーに影響します。
以前のバージョンでは、クライアントは別の名前空間にアクセスを要求した場合でも、常にデフォルトの名前空間 (/
) に接続していました。これは、デフォルトの名前空間に登録されたミドルウェアがトリガーされることを意味し、非常に意外な場合があります。
// client-side
const socket = io("/admin");
// server-side
io.use((socket, next) => {
// not triggered anymore
});
io.on("connection", socket => {
// not triggered anymore
})
io.of("/admin").use((socket, next) => {
// triggered
});
また、今後は「デフォルト」の名前空間ではなく「メイン」の名前空間を参照します。
Namespace.connected は Namespace.sockets に名前が変更され、Map になりました
connected
オブジェクト(指定された名前空間に接続されているすべての Socket を格納するために使用)を使用して、ID から Socket オブジェクトを取得できました。現在は ES6 の Map です。
以前
// get a socket by ID in the main namespace
const socket = io.of("/").connected[socketId];
// get a socket by ID in the "admin" namespace
const socket = io.of("/admin").connected[socketId];
// loop through all sockets
const sockets = io.of("/").connected;
for (const id in sockets) {
if (sockets.hasOwnProperty(id)) {
const socket = sockets[id];
// ...
}
}
// get the number of connected sockets
const count = Object.keys(io.of("/").connected).length;
現在
// get a socket by ID in the main namespace
const socket = io.of("/").sockets.get(socketId);
// get a socket by ID in the "admin" namespace
const socket = io.of("/admin").sockets.get(socketId);
// loop through all sockets
for (const [_, socket] of io.of("/").sockets) {
// ...
}
// get the number of connected sockets
const count = io.of("/").sockets.size;
Socket.rooms は Set になりました
rooms
プロパティには、Socket が現在属しているルームのリストが含まれています。オブジェクトでしたが、現在は ES6 の Set です。
以前
io.on("connection", (socket) => {
console.log(Object.keys(socket.rooms)); // [ <socket.id> ]
socket.join("room1");
console.log(Object.keys(socket.rooms)); // [ <socket.id>, "room1" ]
});
現在
io.on("connection", (socket) => {
console.log(socket.rooms); // Set { <socket.id> }
socket.join("room1");
console.log(socket.rooms); // Set { <socket.id>, "room1" }
});
Socket.binary() は削除されました
binary
メソッドは、指定されたイベントにバイナリデータが含まれていないことを示すために使用できました(ライブラリによって行われるルックアップをスキップし、特定の状況でのパフォーマンスを向上させるため)。
Socket.IO 2.0 で追加された独自のパーサーを提供する機能に置き換えられました。
以前
socket.binary(false).emit("hello", "no binary");
現在
const io = require("socket.io")(httpServer, {
parser: myCustomParser
});
例については、socket.io-msgpack-parser を参照してください。
Socket.join() と Socket.leave() は同期処理になりました
非同期処理は Redis アダプターの最初のバージョンでは必要でしたが、現在は必要ありません。
参考として、アダプターは Socket と ルーム の間の関係を格納するオブジェクトです。公式のアダプターには、インメモリアダプター(組み込み)と Redis の パブリッシュ・サブスクライブ機構 をベースにした Redis アダプター の 2 つがあります。
以前
socket.join("room1", () => {
io.to("room1").emit("hello");
});
socket.leave("room2", () => {
io.to("room2").emit("bye");
});
現在
socket.join("room1");
io.to("room1").emit("hello");
socket.leave("room2");
io.to("room2").emit("bye");
注: カスタムアダプターは Promise を返す可能性があるため、前の例は次のようになります。
await socket.join("room1");
io.to("room1").emit("hello");
Socket.use() は削除されました
socket.use()
はキャッチオールリスナーとして使用できましたが、その API はあまり直感的ではありませんでした。socket.onAny() に置き換えられました。
更新: Socket.use()
メソッドは socket.io@3.0.5
で復元されました。
以前
socket.use((packet, next) => {
console.log(packet.data);
next();
});
現在
socket.onAny((event, ...args) => {
console.log(event);
});
ミドルウェアエラーは Error オブジェクトを emit するようになりました
error
イベントは connect_error
に名前が変更され、emit されるオブジェクトは実際の Error になりました。
以前
// server-side
io.use((socket, next) => {
next(new Error("not authorized"));
});
// client-side
socket.on("error", err => {
console.log(err); // not authorized
});
// or with an object
// server-side
io.use((socket, next) => {
const err = new Error("not authorized");
err.data = { content: "Please retry later" }; // additional details
next(err);
});
// client-side
socket.on("error", err => {
console.log(err); // { content: "Please retry later" }
});
現在
// server-side
io.use((socket, next) => {
const err = new Error("not authorized");
err.data = { content: "Please retry later" }; // additional details
next(err);
});
// client-side
socket.on("connect_error", err => {
console.log(err instanceof Error); // true
console.log(err.message); // not authorized
console.log(err.data); // { content: "Please retry later" }
});
Manager クエリオプションと Socket クエリオプションの明確な区別を追加
以前のバージョンでは、query
オプションは 2 つの異なる場所で使用されていました。
- HTTP リクエストのクエリパラメーター内 (
GET /socket.io/?EIO=3&abc=def
) CONNECT
パケット内
次の例を考えてみましょう。
const socket = io({
query: {
token: "abc"
}
});
内部的には、io()
メソッドでは次のことが行われました。
const { Manager } = require("socket.io-client");
// a new Manager is created (which will manage the low-level connection)
const manager = new Manager({
query: { // sent in the query parameters
token: "abc"
}
});
// and then a Socket instance is created for the namespace (here, the main namespace, "/")
const socket = manager.socket("/", {
query: { // sent in the CONNECT packet
token: "abc"
}
});
この動作は、Manager が別の名前空間(マルチプレクシング)で再利用された場合など、奇妙な動作につながる可能性があります。
// client-side
const socket1 = io({
query: {
token: "abc"
}
});
const socket2 = io("/my-namespace", {
query: {
token: "def"
}
});
// server-side
io.on("connection", (socket) => {
console.log(socket.handshake.query.token); // abc (ok!)
});
io.of("/my-namespace").on("connection", (socket) => {
console.log(socket.handshake.query.token); // abc (what?)
});
そのため、Socket.IO v3 では、Socket インスタンスの query
オプションは auth
に名前が変更されました。
// plain object
const socket = io({
auth: {
token: "abc"
}
});
// or with a function
const socket = io({
auth: (cb) => {
cb({
token: "abc"
});
}
});
// server-side
io.on("connection", (socket) => {
console.log(socket.handshake.auth.token); // abc
});
注: Manager の query
オプションは、HTTP リクエストに特定のクエリパラメーターを追加するために引き続き使用できます。
Socket インスタンスは、その Manager が emit したイベントを転送しなくなりました
以前のバージョンでは、Socket インスタンスは基になる接続の状態に関連するイベントを emit していました。これ以上はそうなりません。
これらのイベントには、Manager インスタンス (ソケットの io
プロパティ) からアクセスできます。
以前
socket.on("reconnect_attempt", () => {});
現在
socket.io.on("reconnect_attempt", () => {});
Manager が emit するイベントの更新されたリストを以下に示します。
名前 | 説明 | 以前 (異なる場合) |
---|---|---|
open | 成功した (再)接続 | - |
error | (再)接続失敗、または成功した接続後のエラー | connect_error |
close | 切断 | - |
ping | ping パケット | - |
packet | データパケット | - |
reconnect_attempt | 再接続の試行 | 再接続試行と再接続中 |
再接続 | 再接続成功 | - |
再接続エラー | 再接続失敗 | - |
再接続失敗 | すべての試行後の再接続失敗 | - |
Socketによって発行されるイベントの更新済みリストを以下に示します。
名前 | 説明 | 以前 (異なる場合) |
---|---|---|
接続 | 名前空間への接続成功 | - |
connect_error | 接続失敗 | error |
切断 | 切断 | - |
最後に、アプリケーションで使用できない予約済みイベントの更新済みリストを以下に示します。
connect
(クライアント側で使用)connect_error
(クライアント側で使用)disconnect
(両側で使用)disconnecting
(サーバー側で使用)newListener
とremoveListener
(EventEmitter 予約済みイベント)
socket.emit("connect_error"); // will now throw an Error
Namespace.clients()はNamespace.allSockets()に名前が変更され、Promiseを返すようになりました
この関数は、この名前空間に接続されているソケットIDのリストを返します。
以前
// all sockets in default namespace
io.clients((error, clients) => {
console.log(clients); // => [6em3d4TJP8Et9EMNAAAA, G5p55dHhGgUnLUctAAAB]
});
// all sockets in the "chat" namespace
io.of("/chat").clients((error, clients) => {
console.log(clients); // => [PZDoMHjiu8PYfRiKAAAF, Anw2LatarvGVVXEIAAAD]
});
// all sockets in the "chat" namespace and in the "general" room
io.of("/chat").in("general").clients((error, clients) => {
console.log(clients); // => [Anw2LatarvGVVXEIAAAD]
});
現在
// all sockets in default namespace
const ids = await io.allSockets();
// all sockets in the "chat" namespace
const ids = await io.of("/chat").allSockets();
// all sockets in the "chat" namespace and in the "general" room
const ids = await io.of("/chat").in("general").allSockets();
注:この関数はRedisアダプターでサポートされていました(現在もサポートされています)。つまり、すべてのSocket.IOサーバー間でソケットIDのリストを返します。
クライアントバンドル
現在、3つの異なるバンドルがあります。
名前 | サイズ | 説明 |
---|---|---|
socket.io.js | 34.7 kB gzip | debugを含む、未縮小バージョン |
socket.io.min.js | 14.7 kB min+gzip | debugを含まない、本番環境バージョン |
socket.io.msgpack.min.js | 15.3 kB min+gzip | debugを含まず、msgpackパーサーを含む、本番環境バージョン |
デフォルトでは、これらはすべてサーバーによって/socket.io/<name>
で提供されます。
以前
<!-- note: this bundle was actually minified but included the debug package -->
<script src="/socket.io/socket.io.js"></script>
現在
<!-- during development -->
<script src="/socket.io/socket.io.js"></script>
<!-- for production -->
<script src="/socket.io/socket.io.min.js"></script>
レイテンシ取得のための「pong」イベントはもうありません
Socket.IO v2では、クライアント側でpong
イベントをリッスンすることができ、これには最後のヘルスチェックのラウンドトリップ時間の長さが含まれていました。
ハートビートメカニズムの変更(詳細はこちら)により、このイベントは削除されました。
以前
socket.on("pong", (latency) => {
console.log(latency);
});
現在
// server-side
io.on("connection", (socket) => {
socket.on("ping", (cb) => {
if (typeof cb === "function")
cb();
});
});
// client-side
setInterval(() => {
const start = Date.now();
// volatile, so the packet will be discarded if the socket is not connected
socket.volatile.emit("ping", () => {
const latency = Date.now() - start;
// ...
});
}, 5000);
ESモジュール構文
ECMAScriptモジュール構文は、TypeScriptのものと似ています(下記参照)。
以前(デフォルトインポートを使用)
// server-side
import Server from "socket.io";
const io = new Server(8080);
// client-side
import io from 'socket.io-client';
const socket = io();
その後(名前付きインポートを使用)
// server-side
import { Server } from "socket.io";
const io = new Server(8080);
// client-side
import { io } from 'socket.io-client';
const socket = io();
emit()
チェーンはもう使用できません
emit()
メソッドは現在、EventEmitter.emit()
メソッドのシグネチャと一致し、現在のオブジェクトではなくtrue
を返します。
以前
socket.emit("event1").emit("event2");
現在
socket.emit("event1");
socket.emit("event2");
ルーム名は文字列に変換されなくなりました
内部的にはプレーンオブジェクトではなくMapとSetを使用するようになったため、ルーム名は暗黙的に文字列に変換されなくなりました。
以前
// mixed types were possible
socket.join(42);
io.to("42").emit("hello");
// also worked
socket.join("42");
io.to(42).emit("hello");
現在
// one way
socket.join("42");
io.to("42").emit("hello");
// or another
socket.join(42);
io.to(42).emit("hello");
新機能
これらの新機能の一部は、ユーザーのフィードバックに応じて2.4.x
ブランチにバックポートされる可能性があります。
キャッチオールリスナー
この機能は、EventEmitter2ライブラリから着想を得ています(ブラウザバンドルのサイズを増やさないように、直接使用されていません)。
サーバー側とクライアント側の両方で使用できます。
// server
io.on("connection", (socket) => {
socket.onAny((event, ...args) => {});
socket.prependAny((event, ...args) => {});
socket.offAny(); // remove all listeners
socket.offAny(listener);
const listeners = socket.listenersAny();
});
// client
const socket = io();
socket.onAny((event, ...args) => {});
socket.prependAny((event, ...args) => {});
socket.offAny(); // remove all listeners
socket.offAny(listener);
const listeners = socket.listenersAny();
揮発性イベント(クライアント)
揮発性イベントとは、低レベルのトランスポートの準備ができていない場合に削除される可能性のあるイベントです(たとえば、HTTP POSTリクエストが既に保留中の場合)。
この機能はサーバー側では既に利用可能でした。クライアント側でも役立つ場合があります。たとえば、ソケットが接続されていない場合(デフォルトでは、パケットは再接続までバッファリングされます)。
socket.volatile.emit("volatile event", "might or might not be sent");
msgpackパーサーを含む公式バンドル
socket.io-msgpack-parserを含むバンドルが提供されるようになりました(CDN上、またはサーバーによって/socket.io/socket.io.msgpack.min.js
で提供)。
利点
- バイナリコンテンツを含むイベントは、1つのWebSocketフレームとして送信されます(デフォルトのパーサーでは2つ以上)。
- 多数の数値を含むペイロードは、より小さくなります。
欠点
- IE9サポートなし(https://caniuse.dokyumento.jp/mdn-javascript_builtins_arraybuffer)
- わずかに大きなバンドルサイズ
// server-side
const io = require("socket.io")(httpServer, {
parser: require("socket.io-msgpack-parser")
});
クライアント側で追加の構成は必要ありません。
その他
Socket.IOコードベースはTypeScriptに書き直されました
つまり、npm i -D @types/socket.io
はもう必要ありません。
サーバー
import { Server, Socket } from "socket.io";
const io = new Server(8080);
io.on("connection", (socket: Socket) => {
console.log(`connect ${socket.id}`);
socket.on("disconnect", () => {
console.log(`disconnect ${socket.id}`);
});
});
クライアント
import { io } from "socket.io-client";
const socket = io("/");
socket.on("connect", () => {
console.log(`connect ${socket.id}`);
});
プレーンなJavaScriptは当然ながら引き続き完全にサポートされています。
IE8とNode.js 8のサポートは正式に終了しました
IE8はSauce Labsプラットフォームではもはやテストできず、非常に少ないユーザー(もしいるとしても)のために多くの労力を必要とするため、サポートを終了します。
また、Node.js 8は現在EOLです。できるだけ早くアップグレードしてください!
既存の本番環境のデプロイをアップグレードする方法
- まず、
allowEIO3
をtrue
に設定してサーバーを更新します(socket.io@3.1.0
に追加)。
const io = require("socket.io")({
allowEIO3: true // false by default
});
注:Redisアダプターを使用してノード間でパケットをブロードキャストする場合は、socket.io@2
にはsocket.io-redis@5
を、socket.io@3
にはsocket.io-redis@6
を使用する必要があります。両方のバージョンは互換性があるため、サーバーを1つずつ更新できます(ビッグバンは必要ありません)。
- 次に、クライアントを更新します。
この手順には時間がかかる場合があります。一部のクライアントは、v2クライアントをキャッシュに保持している可能性があるためです。
接続のバージョンは次のように確認できます。
io.on("connection", (socket) => {
const version = socket.conn.protocol; // either 3 or 4
});
これは、HTTPリクエストのEIO
クエリパラメーターの値と一致します。
- 最後に、すべてのクライアントが更新されたら、
allowEIO3
をfalse
(デフォルト値)に設定します。
const io = require("socket.io")({
allowEIO3: false
});
allowEIO3
をfalse
に設定すると、v2クライアントは接続時にHTTP 400エラー(Unsupported protocol version
)を受信するようになります。
既知の移行上の問題
stream_1.pipelineは関数ではありません
TypeError: stream_1.pipeline is not a function
at Function.sendFile (.../node_modules/socket.io/dist/index.js:249:26)
at Server.serve (.../node_modules/socket.io/dist/index.js:225:16)
at Server.srv.on (.../node_modules/socket.io/dist/index.js:186:22)
at emitTwo (events.js:126:13)
at Server.emit (events.js:214:7)
at parserOnIncoming (_http_server.js:602:12)
at HTTPParser.parserOnHeadersComplete (_http_common.js:116:23)
このエラーはおそらくNode.jsのバージョンが原因です。pipelineメソッドはNode.js 10.0.0で導入されました。
エラーTS2416:型'Namespace'の'emit'プロパティは、基底型'EventEmitter'の同じプロパティに割り当てることができません。
node_modules/socket.io/dist/namespace.d.ts(89,5): error TS2416: Property 'emit' in type 'Namespace' is not assignable to the same property in base type 'EventEmitter'.
Type '(ev: string, ...args: any[]) => Namespace' is not assignable to type '(event: string | symbol, ...args: any[]) => boolean'.
Type 'Namespace' is not assignable to type 'boolean'.
node_modules/socket.io/dist/socket.d.ts(84,5): error TS2416: Property 'emit' in type 'Socket' is not assignable to the same property in base type 'EventEmitter'.
Type '(ev: string, ...args: any[]) => this' is not assignable to type '(event: string | symbol, ...args: any[]) => boolean'.
Type 'this' is not assignable to type 'boolean'.
Type 'Socket' is not assignable to type 'boolean'.
emit()
メソッドのシグネチャはバージョン3.0.1
で修正されました(コミット)。
- 大きなペイロード(> 1MB)を送信すると、クライアントが切断されます。
これはおそらく、maxHttpBufferSize
のデフォルト値が現在1MB
であるためです。これよりも大きいパケットを受信すると、サーバーは悪意のあるクライアントによるサーバーの過負荷を防ぐためにクライアントを切断します。
サーバーの作成時に値を調整できます。
const io = require("socket.io")(httpServer, {
maxHttpBufferSize: 1e8
});
クロスオリジンリクエストがブロックされました:同一オリジンポリシーにより、xxx/socket.io/?EIO=4&transport=polling&t=NMnp2WIの リモートリソースの読み取りが許可されていません。(理由:CORSヘッダー 'Access-Control-Allow-Origin'がありません)。
Socket.IO v3以降、クロスオリジンリソース共有(CORS)を明示的に有効にする必要があります。ドキュメントはこちらにあります。
キャッチされていないTypeError:packet.dataは未定義です
v3クライアントを使用してv2サーバーに接続しようとしているようです。これは不可能です。次のセクションを参照してください。
オブジェクトリテラルは既知のプロパティのみを指定でき、'extraHeaders'は型'ConnectOpts'に存在しません。
コードベースがTypeScriptに書き直されたため(詳細はこちら)、@types/socket.io-client
はもう必要なくなり、実際にはsocket.io-client
パッケージからの型定義と競合します。
- クロスオリジンコンテキストでCookieがありません
フロントエンドがバックエンドと同じドメインから提供されていない場合は、Cookieを明示的に有効にする必要があります。
サーバー
import { Server } from "socket.io";
const io = new Server({
cors: {
origin: ["https://front.domain.com"],
credentials: true
}
});
クライアント
import { io } from "socket.io-client";
const socket = io("https://backend.domain.com", {
withCredentials: true
});
参照
- CORSの処理
cors
オプションwithCredentials
オプション