接続の問題のトラブルシューティング
管理 UI は、Socket.IO デプロイメントのステータスに関する追加の洞察を提供します。
一般的な/既知の問題
その他の一般的な落とし穴
問題: ソケットが接続できない
トラブルシューティング手順
クライアント側では、`connect_error` イベントが追加情報を提供します
socket.on("connect_error", (err) => {
// the reason of the error, for example "xhr poll error"
console.log(err.message);
// some additional description, for example the status code of the initial HTTP response
console.log(err.description);
// some additional context, for example the XMLHttpRequest object
console.log(err.context);
});
サーバー側では、`connection_error` イベントも追加の洞察を提供する場合があります
io.engine.on("connection_error", (err) => {
console.log(err.req); // the request object
console.log(err.code); // the error code, for example 1
console.log(err.message); // the error message, for example "Session ID unknown"
console.log(err.context); // some additional error context
});
考えられるエラーコードのリストを以下に示します
コード | メッセージ | 考えられる説明 |
---|---|---|
0 | "トランスポート不明" | これは通常の状況では発生しません。 |
1 | "セッション ID 不明" | 通常、これはスティッキーセッションが有効になっていないことを意味します(以下を参照)。 |
2 | "ハンドシェイクメソッドが不正です" | これは通常の状況では発生しません。 |
3 | "不正なリクエスト" | 通常、これはサーバーの前にあるプロキシが WebSocket ヘッダーを正しく転送していないことを意味します(こちらを参照)。 |
4 | "禁止" | allowRequest() メソッドによって接続が拒否されました。 |
5 | "サポートされていないプロトコルバージョン" | クライアントのバージョンがサーバーと互換性がありません(こちらを参照)。 |
考えられる説明
プレーンな WebSocket サーバーに接続しようとしています
"Socket.IO ではないもの" セクションで説明されているように、Socket.IO クライアントは WebSocket 実装ではないため、`transports: ["websocket"]` を使用しても WebSocket サーバーとの接続を確立できません
const socket = io("ws://echo.websocket.org", {
transports: ["websocket"]
});
サーバーに到達できません
Socket.IO サーバーが指定された URL で実際に到達可能であることを確認してください。以下を使用してテストできます
curl "<the server URL>/socket.io/?EIO=4&transport=polling"
これは次のようなものを返します
0{"sid":"Lbo5JLzTotvW3g2LAAAA","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":20000}
そうでない場合は、Socket.IO サーバーが実行されていること、および接続を妨げるものが間にないことを確認してください。
v1/v2 サーバー (プロトコルの v3 を実装しているため、`EIO=3` となります) は、次のようなものを返します
96:0{"sid":"ptzi_578ycUci8WLB9G1","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000}2:40
クライアントはサーバーのバージョンと互換性がありません
下位互換性の維持は最優先事項ですが、特定のケースでは、プロトコルレベルで破壊的な変更を実装する必要がありました
- Javascript 以外のクライアントとの互換性を向上させるために、v1.x から v2.0.0 (2017 年 5 月リリース) へ ( こちらを参照)
- プロトコルの長年の問題を一度に解決するために、v2.x から v3.0.0 (2020 年 11 月リリース) へ ( こちらを参照)
`v4.0.0` には、JavaScript サーバーの API に破壊的な変更がいくつか含まれています。Socket.IO プロトコル自体は更新されていないため、v3 クライアントは v4 サーバーに到達でき、その逆も可能です ( こちらを参照)。
たとえば、v1/v2 クライアントで v3/v4 サーバーに到達すると、次の応答が返されます
< HTTP/1.1 400 Bad Request
< Content-Type: application/json
{"code":5,"message":"Unsupported protocol version"}
JS クライアントの互換性テーブルを以下に示します
JS クライアントバージョン | Socket.IO サーバーバージョン | |||
---|---|---|---|---|
1.x | 2.x | 3.x | 4.x | |
1.x | はい | いいえ | いいえ | いいえ |
2.x | いいえ | はい | はい1 | はい1 |
3.x | いいえ | いいえ | はい | はい |
4.x | いいえ | いいえ | はい | はい |
[1]allowEIO3: trueで、はい
Java クライアントの互換性テーブルを以下に示します
Java クライアントバージョン | Socket.IO サーバーバージョン | ||
---|---|---|---|
2.x | 3.x | 4.x | |
1.x | はい | はい1 | はい1 |
2.x | いいえ | はい | はい |
[1]allowEIO3: trueで、はい
Swift クライアントの互換性テーブルを以下に示します
Swift クライアントバージョン | Socket.IO サーバーバージョン | ||
---|---|---|---|
2.x | 3.x | 4.x | |
v15.x | はい | はい1 | はい2 |
v16.x | はい3 | はい | はい |
[1]allowEIO3: true (サーバー) と ` .connectParams(["EIO": "3"])` (クライアント) で、はい
SocketManager(socketURL: URL(string:"http://localhost:8087/")!, config: [.connectParams(["EIO": "3"])])
[2]allowEIO3: true (サーバー)、はい
[3]` .version(.two)` (クライアント) で、はい
SocketManager(socketURL: URL(string:"http://localhost:8087/")!, config: [.version(.two)])
サーバーは必要な CORS ヘッダーを送信しません
コンソールに次のエラーが表示された場合
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at ...
それはおそらく次のことを意味します
- 実際に Socket.IO サーバーに到達していない (上記を参照)
- または、サーバー側で クロスオリジンリソースシェアリング (CORS) を有効にしていない。
ドキュメントはこちらを参照してください。
スティッキーセッションを有効にしていません (マルチサーバー設定の場合)
複数の Socket.IO サーバーにスケーリングする場合、特定の Socket.IO セッションのすべてのリクエストが同じ Socket.IO サーバーに到達することを確認する必要があります。説明はこちらにあります。
そうしないと、コード `{"code":1,"message":"Session ID unknown"}` で HTTP 400 応答が返されます
ドキュメントはこちらを参照してください。
リクエストパスが両側で一致しません
デフォルトでは、クライアントはリクエストパス "/socket.io/" を使用した HTTP リクエストを送信し、サーバーはそれを予期します。
これは `path` オプションで制御できます
サーバー
import { Server } from "socket.io";
const io = new Server({
path: "/my-custom-path/"
});
io.listen(3000);
クライアント
import { io } from "socket.io-client";
const socket = io(SERVER_URL, {
path: "/my-custom-path/"
});
この場合、HTTP リクエストは `<SERVER_URL>/my-custom-path/?EIO=4&transport=polling[&...]` のようになります。
import { io } from "socket.io-client";
const socket = io("/my-custom-path/");
これは、クライアントが "/my-custom-path/" という名前の namespace に接続しようとすることを意味しますが、リクエストパスは "/socket.io/" のままです。
問題: ソケットが切断される
トラブルシューティング手順
まず第一に、安定したインターネット接続であっても、切断は一般的であり、予期されるものであることに注意してください。
- ユーザーと Socket.IO サーバーの間のあらゆるものが一時的な障害に遭遇したり、再起動されたりする可能性があります。
- サーバー自体がオートスケーリングポリシーの一環として強制終了される場合があります。
- モバイルブラウザの場合、ユーザーが接続を失ったり、WiFi から 4G に切り替えたりする可能性があります。
- ブラウザ自体が非アクティブなタブをフリーズさせる場合があります。
そうは言っても、Socket.IO クライアントは、特に 指示がない限り、常に再接続を試みます。
disconnect
イベントは追加情報を提供します。
socket.on("disconnect", (reason, details) => {
// the reason of the disconnection, for example "transport error"
console.log(reason);
// the low-level reason of the disconnection, for example "xhr post error"
console.log(details.message);
// some additional description, for example the status code of the HTTP response
console.log(details.description);
// some additional context, for example the XMLHttpRequest object
console.log(details.context);
});
考えられる理由は こちら に記載されています。
考えられる説明
サーバーとクライアントの間の何かが接続を閉じている
切断が一定の間隔で発生する場合、これはサーバーとクライアントの間の何かが正しく構成されておらず、接続を閉じていることを示している可能性があります。
- nginx
nginx の proxy_read_timeout
の値(デフォルトは 60 秒)は、Socket.IO の pingInterval + pingTimeout
(デフォルトは 45 秒)よりも大きくする必要があります。そうでない場合、指定された遅延後にデータが送信されないと接続が強制的に閉じられ、クライアントは「transport close」エラーを受け取ります。
- Apache HTTP Server
httpd の ProxyTimeout
の値(デフォルトは 60 秒)は、Socket.IO の pingInterval + pingTimeout
(デフォルトは 45 秒)よりも大きくする必要があります。そうでない場合、指定された遅延後にデータが送信されないと接続が強制的に閉じられ、クライアントは「transport close」エラーを受け取ります。
ブラウザタブが最小化され、ハートビートが失敗した
ブラウザタブにフォーカスがない場合、一部のブラウザ(Chrome など)は JavaScript タイマーを調整します。これは、ハートビートメカニズムがクライアント側で setTimeout
関数に依存していたため、Socket.IO v2 では ping タイムアウトによる切断につながる可能性があります。
回避策として、サーバー側で pingTimeout
値を増やすことができます。
const io = new Server({
pingTimeout: 60000
});
Socket.IO v4(少なくとも socket.io-client@4.1.3
、これ による)にアップグレードすると、ハートビートメカニズムが逆転したため(サーバーが PING パケットを送信するようになったため)、この種の問題を防ぐことができることに注意してください。
クライアントがサーバーのバージョンと互換性がない
WebSocket トランスポートを介して送信されるパケットの形式は v2 と v3/v4 で似ているため、互換性のないクライアントで接続できる場合がありますが(上記 を参照)、接続は最終的に一定の遅延後に閉じられます。
そのため、30 秒後に定期的な切断が発生する場合(これは Socket.IO v2 の pingTimeout と pingInterval の値の合計でした)、これは確かにバージョンの非互換性が原因です。
巨大なペイロードを送信しようとしている
巨大なペイロードを送信中に切断される場合、これはデフォルトで 1 MB の maxHttpBufferSize
値に達したことを意味している可能性があります。必要に応じて調整してください。
const io = require("socket.io")(httpServer, {
maxHttpBufferSize: 1e8
});
pingTimeout
オプションの値よりもアップロードに時間がかかる巨大なペイロードも、(ハートビートメカニズム がアップロード中に失敗するため)切断をトリガーする可能性があります。必要に応じて調整してください。
const io = require("socket.io")(httpServer, {
pingTimeout: 60000
});
問題: ソケットが HTTP ロングポーリングでスタックしている
トラブルシューティング手順
ほとんどの場合、次のようなものが表示されます。
- Engine.IO ハンドシェイク(後続のリクエストで使用されるセッション ID(ここでは
zBjrh...AAAK
)が含まれています) - Socket.IO ハンドシェイクリクエスト(
auth
オプションの値が含まれています) - Socket.IO ハンドシェイクレスポンス(Socket#id が含まれています)
- WebSocket 接続
- 最初の HTTP ロングポーリングリクエスト。WebSocket 接続が確立されると閉じられます。
4 番目のリクエストに対して HTTP 101 Switching Protocols レスポンスが表示されない場合、サーバーとブラウザの間の何かが WebSocket 接続を妨げていることを意味します。
HTTP ロングポーリングで接続が確立されているため、これは必ずしもブロックされているわけではありませんが、効率は低下します。
現在のトランスポートの名前は、次のようにして取得できます。
クライアント側
socket.on("connect", () => {
const transport = socket.io.engine.transport.name; // in most cases, "polling"
socket.io.engine.on("upgrade", () => {
const upgradedTransport = socket.io.engine.transport.name; // in most cases, "websocket"
});
});
サーバー側
io.on("connection", (socket) => {
const transport = socket.conn.transport.name; // in most cases, "polling"
socket.conn.on("upgrade", () => {
const upgradedTransport = socket.conn.transport.name; // in most cases, "websocket"
});
});
考えられる説明
サーバーの前にあるプロキシが WebSocket 接続を受け入れない
nginx や Apache HTTPD などのプロキシが WebSocket 接続を受け入れるように正しく構成されていない場合、TRANSPORT_MISMATCH
エラーが発生する可能性があります。
io.engine.on("connection_error", (err) => {
console.log(err.code); // 3
console.log(err.message); // "Bad request"
console.log(err.context); // { name: 'TRANSPORT_MISMATCH', transport: 'websocket', previousTransport: 'polling' }
});
これは、Socket.IO サーバーが必要な Connection: upgrade
ヘッダーを受信していないことを意味します(err.req.headers
オブジェクトを確認できます)。
ドキュメントは こちら をご覧ください。
express-status-monitor
は独自の socket.io インスタンスを実行する
解決策は こちら をご覧ください。
その他の一般的な落とし穴
イベントの重複登録
クライアント側では、ソケットが再接続されるたびに connect
イベントが発行されるため、イベントリスナーは connect
イベントリスナーの外部に登録する必要があります。
悪い ⚠️
socket.on("connect", () => {
socket.on("foo", () => {
// ...
});
});
良い 👍
socket.on("connect", () => {
// ...
});
socket.on("foo", () => {
// ...
});
そうでない場合、イベントリスナーが複数回呼び出される可能性があります。
イベントハンドラの登録の遅延
悪い ⚠️
io.on("connection", async (socket) => {
await longRunningOperation();
// WARNING! Some packets might be received by the server but without handler
socket.on("hello", () => {
// ...
});
});
良い 👍
io.on("connection", async (socket) => {
socket.on("hello", () => {
// ...
});
await longRunningOperation();
});
socket.id
属性の使用
接続状態のリカバリ が有効になっていない限り、id
属性はアプリケーションで使用することを意図していない(またはデバッグ目的でのみ使用することを意図している)一時的な ID であることに注意してください。理由は次のとおりです。
- この ID は、再接続ごとに再生成されます(たとえば、WebSocket 接続が切断された場合、またはユーザーがページを更新した場合)。
- 2 つの異なるブラウザタブには、2 つの異なる ID があります。
- サーバー上に特定の ID 用にメッセージキューが保存されていません(つまり、クライアントが切断された場合、サーバーからこの ID に送信されたメッセージは失われます)。
代わりに、通常のセッション ID を使用してください(Cookie で送信するか、localStorage に保存して auth
ペイロードで送信します)。
関連項目
サーバーレスプラットフォームへのデプロイ
ほとんどのサーバーレスプラットフォーム(Vercel など)はリクエストハンドラの継続時間に基づいて課金されるため、Socket.IO(またはプレーン WebSocket)との長期接続を維持することはお勧めしません。
参考資料