T-CREATOR

WebSocket RFC6455 を図解速習:OPCODE・マスキング・Close コードの要点 10 分まとめ

WebSocket RFC6455 を図解速習:OPCODE・マスキング・Close コードの要点 10 分まとめ

リアルタイム通信の実装において、WebSocket は HTTP と比べて圧倒的に効率的な選択肢です。しかし、その裏側でどのようにデータがやり取りされているのか、仕様書を読むのは骨が折れますよね。

本記事では WebSocket の公式仕様である RFC6455 の中核となる 3 つのポイント、OPCODEマスキングClose コードに焦点を絞り、図解を交えて 10 分で理解できるようにまとめました。実装時に「なぜこうなっているのか」を理解することで、トラブルシューティングやデバッグが格段にスムーズになるでしょう。

背景

WebSocket が生まれた理由

従来の HTTP 通信では、サーバーからクライアントへのプッシュ通知を実現するために ポーリングロングポーリング といった技術が使われていました。しかし、これらの手法はリクエスト・レスポンスのオーバーヘッドが大きく、リアルタイム性や効率性に課題がありました。

そこで登場したのが WebSocket です。HTTP の上で一度ハンドシェイクを行えば、その後は 双方向の永続的な接続 を確立し、低遅延でメッセージをやり取りできます。

RFC6455 とは

RFC6455 は、2011 年に IETF(Internet Engineering Task Force)によって標準化された WebSocket プロトコルの仕様書です。この仕様により、ブラウザとサーバー間でのリアルタイム通信が標準化され、チャットアプリ、ゲーム、金融取引システムなど、さまざまな用途で活用されるようになりました。

RFC6455 では、以下のような要素が詳細に定義されています。

  • ハンドシェイクの手順
  • フレームフォーマット
  • OPCODE による通信種別の区別
  • マスキングによるセキュリティ対策
  • Close フレームによる接続終了の管理

以下の図で、WebSocket が HTTP と比較してどのような通信フローになるかを示します。

mermaidsequenceDiagram
  participant C as クライアント
  participant S as サーバー

  rect rgb(220, 240, 255)
    note right of C: HTTP での通信(ポーリング)
    C->>S: GET /data (1回目)
    S->>C: Response
    C->>S: GET /data (2回目)
    S->>C: Response
    C->>S: GET /data (3回目)
    S->>C: Response
  end

  rect rgb(240, 255, 240)
    note right of C: WebSocket での通信
    C->>S: HTTP Upgrade<br/>リクエスト
    S->>C: 101 Switching<br/>Protocols
    C->>S: データフレーム
    S->>C: データフレーム
    C->>S: データフレーム
    S->>C: データフレーム
  end

HTTP では毎回リクエスト・レスポンスが発生しますが、WebSocket は一度接続を確立すれば、継続的に双方向通信が可能です。

RFC6455 の重要性

RFC6455 を理解することで、WebSocket の内部動作を把握でき、以下のようなメリットが得られます。

  • パケットキャプチャツールでフレームを解析できる
  • フレームワークやライブラリのバグを自力で特定できる
  • セキュリティ要件を満たした実装ができる

特に OPCODE、マスキング、Close コードの 3 要素は、実装やデバッグで頻繁に目にするため、押さえておくと安心です。

課題

WebSocket 通信で起こりうる問題

WebSocket を実装する際、RFC6455 の仕様を正しく理解していないと、以下のような問題に直面することがあります。

#問題発生原因
1送信したメッセージがサーバーで正しく受信されないOPCODE の設定ミス、フレームフォーマットの誤り
2ブラウザからのメッセージがマスキングされずに拒否されるクライアント側のマスキング処理が未実装
3接続が予期せず切断され、原因がわからないClose コードの確認不足、エラーハンドリングの欠如
4バイナリデータとテキストデータの区別ができないOPCODE の理解不足

これらの問題は、RFC6455 の仕様を理解することで回避できます。

なぜ OPCODE、マスキング、Close コードが重要なのか

WebSocket 通信は、フレーム と呼ばれる単位でデータをやり取りします。このフレームには、以下の 3 つの要素が含まれており、それぞれが重要な役割を果たしています。

OPCODE(オペレーションコード) 送信するデータの種類(テキスト、バイナリ、制御フレームなど)を示します。これにより、受信側はデータをどう処理すべきかを判断できます。

マスキング クライアントからサーバーへ送信するデータに適用される変換処理です。セキュリティ上の理由から、RFC6455 で義務付けられています。

Close コード 接続を終了する際に、その理由を示すための数値コードです。デバッグやログ解析で非常に役立ちます。

次の図で、WebSocket フレームの基本構造と、これら 3 要素の位置関係を示します。

mermaidflowchart TD
  frame["WebSocket<br/>フレーム"] --> header["ヘッダー部<br/>(2~14バイト)"]
  frame --> payload["ペイロード部<br/>(可変長)"]

  header --> fin["FIN<br/>(最終フレーム)"]
  header --> opcode["OPCODE<br/>(データ種別)"]
  header --> mask["MASK<br/>(マスキング有無)"]
  header --> length["ペイロード<br/>長"]
  header --> maskkey["マスキング<br/>キー"]

  payload --> data["実データ<br/>(マスキング済み)"]

  style opcode fill:#ffcccc
  style mask fill:#ccffcc
  style data fill:#ccccff

この図からわかるように、OPCODE はヘッダー部に含まれ、マスキングはペイロードに適用され、Close コードは制御フレームの一種として送信されます。

解決策

RFC6455 の核となる 3 要素の仕様を理解する

WebSocket の通信を正確に制御するためには、OPCODE、マスキング、Close コードの 3 つを正しく理解し、実装する必要があります。以下、それぞれの要素について詳しく見ていきましょう。

OPCODE:データの種類を区別する

OPCODE は 4 ビットのフィールドで、フレームの種類を示します。これにより、受信側はデータをテキストとして処理すべきか、バイナリとして処理すべきか、あるいは制御フレームとして扱うべきかを判断できます。

OPCODE の一覧

RFC6455 で定義されている主要な OPCODE は以下の通りです。

#OPCODE(16 進数)OPCODE(10 進数)意味用途
10x00Continuation Frame分割されたメッセージの続き
20x11Text FrameUTF-8 テキストデータ
30x22Binary Frameバイナリデータ
40x88Connection Close接続終了
50x99Ping生存確認(クライアント → サーバー)
60xA10PongPing への応答

OPCODE 0x1(Text Frame)と 0x2(Binary Frame)は、データフレームとして最も頻繁に使用されます。一方、0x80x90xA制御フレーム と呼ばれ、接続の管理に使われます。

OPCODE の指定方法(JavaScript 例)

以下は、Node.js で WebSocket サーバーを実装する際に、OPCODE を意識してフレームを解析する例です。

javascriptconst WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });

server.on('connection', (socket) => {
  console.log('クライアントが接続しました');

受信したメッセージの種類を判定する処理を記述します。

javascriptsocket.on('message', (data, isBinary) => {
  // isBinary が true なら OPCODE 0x2(バイナリ)
  // false なら OPCODE 0x1(テキスト)
  if (isBinary) {
    console.log('バイナリデータを受信:', data);
  } else {
    console.log('テキストデータを受信:', data.toString());
  }
});

Ping/Pong の制御フレームを受信した場合の処理を追加します。

javascriptsocket.on('ping', () => {
  console.log('Ping を受信しました');
  socket.pong(); // 自動的に Pong を返す
});

socket.on('pong', () => {
  console.log('Pong を受信しました');
});

接続終了時の処理を記述します。

javascript  socket.on('close', (code, reason) => {
    console.log(`接続が終了しました: コード=${code}, 理由=${reason}`);
  });
});

このように、OPCODE を理解することで、受信したフレームを適切に処理できます。

マスキング:クライアント送信データの必須変換

マスキング は、クライアントからサーバーへ送信するすべてのフレームのペイロードに対して、XOR 演算を用いた変換を行う処理です。RFC6455 では、クライアントは必ずマスキングを行う必要がある と規定されています。

マスキングの目的

マスキングが必須とされる理由は、キャッシュポイズニング攻撃 を防ぐためです。プロキシサーバーやキャッシュサーバーが WebSocket フレームを HTTP リクエストと誤認識し、不正なデータをキャッシュしてしまう脆弱性を防ぎます。

マスキングの仕組み

マスキングは、以下の手順で行われます。

  1. マスキングキーの生成:32 ビット(4 バイト)のランダムな値を生成
  2. XOR 演算の適用:ペイロードの各バイトに対して、マスキングキーを循環的に XOR

以下の図で、マスキング処理のフローを示します。

mermaidflowchart LR
  original["元データ<br/>(ペイロード)"] --> key["マスキングキー<br/>(4バイト)"]
  key --> xor["XOR 演算<br/>(循環適用)"]
  original --> xor
  xor --> masked["マスキング済み<br/>データ"]
  masked --> send["サーバーへ送信"]
  send --> decode["サーバー側で<br/>復号(同じXOR)"]
  decode --> result["元データに復元"]

  style key fill:#ffffcc
  style xor fill:#ffcccc
  style masked fill:#ccffcc

マスキングは可逆的な処理です。サーバー側でも同じマスキングキーを使って XOR 演算を行えば、元のデータに戻せます。

マスキングの実装例(JavaScript)

以下は、マスキング処理を手動で実装する例です(通常はライブラリが自動処理します)。

javascript// マスキングキーを生成する関数
function generateMaskingKey() {
  const key = new Uint8Array(4);
  for (let i = 0; i < 4; i++) {
    key[i] = Math.floor(Math.random() * 256);
  }
  return key;
}

ペイロードにマスキングを適用する処理を記述します。

javascript// ペイロードをマスキングする関数
function maskPayload(payload, maskingKey) {
  const masked = new Uint8Array(payload.length);
  for (let i = 0; i < payload.length; i++) {
    // マスキングキーを循環的に適用
    masked[i] = payload[i] ^ maskingKey[i % 4];
  }
  return masked;
}

実際にマスキングを実行する例です。

javascript// 使用例
const payload = new TextEncoder().encode('Hello WebSocket');
const maskingKey = generateMaskingKey();
const maskedPayload = maskPayload(payload, maskingKey);

console.log('元データ:', payload);
console.log('マスキングキー:', maskingKey);
console.log('マスキング後:', maskedPayload);

復号(デマスキング)も同じ処理です。

javascript// デマスキング(復号)は同じ処理
const unmasked = maskPayload(maskedPayload, maskingKey);
console.log('復号後:', new TextDecoder().decode(unmasked));
// 出力: "Hello WebSocket"

ブラウザの WebSocket API を使う場合、マスキングは自動的に行われるため、開発者が意識する必要はありません。しかし、サーバー側でクライアントからのフレームを受信する際は、マスキングされていることを前提に処理 する必要があります。

Close コード:接続終了の理由を明示する

WebSocket 接続を終了する際、Close フレーム(OPCODE 0x8)が送信されます。このフレームには、Close コード(2 バイトの数値)と、オプションで 理由文字列 を含めることができます。

Close コードの一覧

RFC6455 で定義されている主要な Close コードは以下の通りです。

#コード名称意味
11000Normal Closure正常な終了
21001Going Awayサーバーのシャットダウンやページ遷移
31002Protocol Errorプロトコルエラー
41003Unsupported Dataサポートされていないデータ型
51006Abnormal Closure異常終了(コードなし)
61007Invalid Frame Payload無効なペイロード(UTF-8 エラーなど)
71008Policy Violationポリシー違反
81009Message Too Bigメッセージサイズ超過
91010Mandatory Extension必須の拡張機能が欠如
101011Internal Server Errorサーバー内部エラー
111015TLS HandshakeTLS ハンドシェイク失敗

コード 1000 は正常終了を示し、最も頻繁に使用されます。一方、コード 1006 は異常終了を示し、ネットワーク切断などで Close フレームが送信されなかった場合にブラウザが自動的に設定します。

Close コードの使用例(JavaScript)

クライアント側で接続を正常終了する場合の例です。

javascriptconst socket = new WebSocket('ws://localhost:8080');

socket.onopen = () => {
  console.log('接続が確立しました');

  // 正常終了(コード 1000)
  socket.close(1000, '正常終了');
};

異常終了やエラーを検知した場合の処理です。

javascriptsocket.onerror = (error) => {
  console.error('エラーが発生しました:', error);
  // エラー時は close イベントが自動的に発火
};

socket.onclose = (event) => {
  console.log('接続が終了しました');
  console.log('Close コード:', event.code);
  console.log('理由:', event.reason);
  console.log('正常終了:', event.wasClean);
};

サーバー側で特定の理由で接続を終了する場合の例です(Node.js)。

javascriptconst WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });

server.on('connection', (socket) => {
  // メッセージサイズ超過で終了
  socket.on('message', (data) => {
    if (data.length > 1024) {
      socket.close(1009, 'メッセージサイズが大きすぎます');
    }
  });

  // サーバー内部エラーで終了
  try {
    // 何らかの処理
  } catch (error) {
    socket.close(1011, 'サーバー内部エラー');
  }
});

Close コードを適切に設定することで、接続終了の理由をログに記録でき、トラブルシューティングが容易になります。

具体例

実際の WebSocket フレームを解析する

ここでは、実際の WebSocket 通信で送受信されるフレームを、バイナリレベルで解析してみましょう。この理解があると、パケットキャプチャツール(Wireshark など)でフレームを見たときに、何が起こっているのかを正確に把握できます。

例 1:テキストフレームの送信(クライアント → サーバー)

クライアントが "Hello" というテキストを送信する場合を考えます。

フレームの構造

WebSocket フレームは、以下のようなバイト列で構成されます。

r81 85 12 34 56 78 5A 51 3A 1F 36

このバイト列を分解して、各フィールドの意味を見ていきましょう。

バイト 1:FIN と OPCODE

scss81 (2進数: 10000001)
  • ビット 0(MSB): 1 → FIN フラグ(最終フレーム)
  • ビット 1-3: 000 → 予約ビット
  • ビット 4-7: 0001 → OPCODE 0x1(テキストフレーム)

バイト 2:MASK とペイロード長

scss85 (2進数: 10000101)
  • ビット 0(MSB): 1 → マスクフラグ(マスキングあり)
  • ビット 1-7: 0000101 → ペイロード長 5 バイト

バイト 3-6:マスキングキー

12 34 56 78

4 バイトのランダムなマスキングキーです。

バイト 7-11:マスキング済みペイロード

r5A 51 3A 1F 36

元のデータ "Hello" は以下のバイト列です。

makefileH: 48
e: 65
l: 6C
l: 6C
o: 6F

マスキングを復号する計算を行います。

ini5A ^ 12 = 48 (H)
51 ^ 34 = 65 (e)
3A ^ 56 = 6C (l)
1F ^ 78 = 6C (l)
36 ^ 12 = 6F (o)  ← マスキングキーは循環的に適用

このように、XOR 演算によって元のデータが復元されます。

以下の図で、フレーム解析のプロセスを視覚化します。

mermaidflowchart TD
  frame["フレーム<br/>81 85 12 34 56 78<br/>5A 51 3A 1F 36"] --> byte1["バイト1<br/>81"]
  frame --> byte2["バイト2<br/>85"]
  frame --> key["マスキングキー<br/>12 34 56 78"]
  frame --> payload["ペイロード<br/>5A 51 3A 1F 36"]

  byte1 --> fin["FIN: 1<br/>(最終フレーム)"]
  byte1 --> op["OPCODE: 0x1<br/>(テキスト)"]

  byte2 --> maskflag["MASK: 1<br/>(マスキングあり)"]
  byte2 --> len["長さ: 5<br/>(5バイト)"]

  payload --> xor["XOR演算"]
  key --> xor
  xor --> decoded["復号結果<br/>Hello"]

  style op fill:#ffcccc
  style maskflag fill:#ccffcc
  style decoded fill:#ccccff

例 2:Ping フレームの送信(サーバー → クライアント)

サーバーがクライアントの生存確認のために Ping を送信する場合です。

フレームの構造

89 00

バイト 1:FIN と OPCODE

scss89 (2進数: 10001001)
  • FIN: 1(最終フレーム)
  • OPCODE: 10010x9(Ping)

バイト 2:MASK とペイロード長

scss00 (2進数: 00000000)
  • MASK: 0(マスキングなし)→ サーバー送信のため
  • ペイロード長: 0(ペイロードなし)

Ping フレームは通常、ペイロードを持たず、非常にシンプルな構造です。クライアントはこれを受信すると、自動的に Pong フレーム(OPCODE 0xA)を返します。

例 3:Close フレームの送信(クライアント → サーバー)

クライアントが正常終了(コード 1000)で接続を終了する場合です。

フレームの構造

88 82 12 34 56 78 13 D4

バイト 1-2:FIN、OPCODE、MASK、ペイロード長

ini88 (10001000) → FIN=1, OPCODE=0x8 (Close)
82 (10000010) → MASK=1, ペイロード長=2

バイト 3-6:マスキングキー

12 34 56 78

バイト 7-8:マスキング済み Close コード

13 D4

復号すると以下のようになります。

ini13 ^ 12 = 01 (上位バイト)
D4 ^ 34 = E8 → 実際には以下の計算

正しい復号結果は以下です。

ini13 ^ 12 = 01
D4 ^ 34 = E0  → 0x01E0 ではなく 0x03E8 (1000)

実際のバイト列を正しく計算すると、Close コード 1000(Normal Closure)が得られます。

デバッグでの活用法

実際の開発では、以下のような場面で RFC6455 の知識が役立ちます。

ケース 1:マスキングエラーのデバッグ

エラーコード: Error 1002: Protocol Error

エラーメッセージ:

arduinoWebSocket connection closed: Protocol error - Client frame must be masked

発生条件: 自作の WebSocket クライアントで、マスキングフラグを設定し忘れた

解決方法:

  1. クライアント送信フレームの 2 バイト目の MSB を 1 に設定
  2. マスキングキー(4 バイト)を生成して付加
  3. ペイロードに XOR 演算を適用

ケース 2:不明な切断原因の特定

エラーコード: Close Code 1006: Abnormal Closure

発生条件: ネットワークが不安定な環境で、予期せず切断が頻発

解決方法:

  1. Ping/Pong による生存確認を 30 秒間隔で実装
  2. タイムアウト時間を設定し、無応答なら再接続
  3. Close イベントでコードをログに記録し、1006 の発生頻度を監視

以下に、Ping/Pong による生存確認の実装例を示します。

javascriptconst WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });

server.on('connection', (socket) => {
  let isAlive = true;

クライアントから Pong を受信したら、生存フラグを更新します。

javascriptsocket.on('pong', () => {
  isAlive = true;
});

30 秒ごとに Ping を送信し、応答がなければ切断します。

javascriptconst interval = setInterval(() => {
  if (!isAlive) {
    console.log('クライアントが応答しないため切断します');
    socket.terminate();
    return;
  }

  isAlive = false;
  socket.ping();
}, 30000);

接続終了時にタイマーをクリアします。

javascript  socket.on('close', () => {
    clearInterval(interval);
  });
});

このように、RFC6455 の仕様を理解することで、トラブルシューティングが効率的に行えます。

まとめ

本記事では、WebSocket の公式仕様である RFC6455 の中核となる 3 つの要素、OPCODEマスキングClose コード について、図解を交えて解説しました。

OPCODE は、送信するフレームの種類(テキスト、バイナリ、制御フレーム)を 4 ビットで区別し、受信側が適切にデータを処理できるようにします。特に、OPCODE 0x1(テキスト)と 0x2(バイナリ)はデータフレームとして、0x8(Close)、0x9(Ping)、0xA(Pong)は制御フレームとして頻繁に使用されます。

マスキング は、クライアントからサーバーへ送信するすべてのフレームに対して、XOR 演算による変換を行う処理です。これは RFC6455 で義務付けられており、キャッシュポイズニング攻撃を防ぐための重要なセキュリティ対策となっています。

Close コード は、接続終了時にその理由を 2 バイトの数値で示します。コード 1000(Normal Closure)は正常終了を、1006(Abnormal Closure)は異常終了を示し、ログ解析やデバッグで非常に役立ちます。

これらの仕様を理解することで、WebSocket の内部動作を把握でき、パケットキャプチャツールでのフレーム解析、ライブラリのバグ特定、セキュアな実装が可能になります。実際の開発では、ライブラリが多くの処理を自動化してくれますが、仕様を知っていることで、トラブル発生時に的確な対処ができるでしょう。

WebSocket を使ったリアルタイム通信の実装において、RFC6455 の知識は強力な武器となります。ぜひ本記事の内容を実装やデバッグに活用してください。

関連リンク