メインコンテンツにスキップ
バージョン: 4.x

接続の問題のトラブルシューティング

ヒント

管理 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.x2.x3.x4.x
1.xはいいいえいいえいいえ
2.xいいえはいはい1はい1
3.xいいえいいえはいはい
4.xいいえいいえはいはい

[1]allowEIO3: trueで、はい

Java クライアントの互換性テーブルを以下に示します

Java クライアントバージョンSocket.IO サーバーバージョン
2.x3.x4.x
1.xはいはい1はい1
2.xいいえはいはい

[1]allowEIO3: trueで、はい

Swift クライアントの互換性テーブルを以下に示します

Swift クライアントバージョンSocket.IO サーバーバージョン
2.x3.x4.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 サーバーにスケーリングする場合、特定の 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 の pingTimeoutpingInterval の値の合計でした)、これは確かにバージョンの非互換性が原因です。

巨大なペイロードを送信しようとしている

巨大なペイロードを送信中に切断される場合、これはデフォルトで 1 MB の maxHttpBufferSize 値に達したことを意味している可能性があります。必要に応じて調整してください。

const io = require("socket.io")(httpServer, {
maxHttpBufferSize: 1e8
});

pingTimeout オプションの値よりもアップロードに時間がかかる巨大なペイロードも、(ハートビートメカニズム がアップロード中に失敗するため)切断をトリガーする可能性があります。必要に応じて調整してください。

const io = require("socket.io")(httpServer, {
pingTimeout: 60000
});

問題: ソケットが HTTP ロングポーリングでスタックしている

トラブルシューティング手順

ほとんどの場合、次のようなものが表示されます。

Network monitor upon success

  1. Engine.IO ハンドシェイク(後続のリクエストで使用されるセッション ID(ここでは zBjrh...AAAK)が含まれています)
  2. Socket.IO ハンドシェイクリクエスト(auth オプションの値が含まれています)
  3. Socket.IO ハンドシェイクレスポンス(Socket#id が含まれています)
  4. WebSocket 接続
  5. 最初の 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)との長期接続を維持することはお勧めしません。

参考資料