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 進数) | 意味 | 用途 |
|---|---|---|---|---|
| 1 | 0x0 | 0 | Continuation Frame | 分割されたメッセージの続き |
| 2 | 0x1 | 1 | Text Frame | UTF-8 テキストデータ |
| 3 | 0x2 | 2 | Binary Frame | バイナリデータ |
| 4 | 0x8 | 8 | Connection Close | 接続終了 |
| 5 | 0x9 | 9 | Ping | 生存確認(クライアント → サーバー) |
| 6 | 0xA | 10 | Pong | Ping への応答 |
OPCODE 0x1(Text Frame)と 0x2(Binary Frame)は、データフレームとして最も頻繁に使用されます。一方、0x8、0x9、0xA は 制御フレーム と呼ばれ、接続の管理に使われます。
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 リクエストと誤認識し、不正なデータをキャッシュしてしまう脆弱性を防ぎます。
マスキングの仕組み
マスキングは、以下の手順で行われます。
- マスキングキーの生成:32 ビット(4 バイト)のランダムな値を生成
- 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 コードは以下の通りです。
| # | コード | 名称 | 意味 |
|---|---|---|---|
| 1 | 1000 | Normal Closure | 正常な終了 |
| 2 | 1001 | Going Away | サーバーのシャットダウンやページ遷移 |
| 3 | 1002 | Protocol Error | プロトコルエラー |
| 4 | 1003 | Unsupported Data | サポートされていないデータ型 |
| 5 | 1006 | Abnormal Closure | 異常終了(コードなし) |
| 6 | 1007 | Invalid Frame Payload | 無効なペイロード(UTF-8 エラーなど) |
| 7 | 1008 | Policy Violation | ポリシー違反 |
| 8 | 1009 | Message Too Big | メッセージサイズ超過 |
| 9 | 1010 | Mandatory Extension | 必須の拡張機能が欠如 |
| 10 | 1011 | Internal Server Error | サーバー内部エラー |
| 11 | 1015 | TLS Handshake | TLS ハンドシェイク失敗 |
コード 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→ OPCODE0x1(テキストフレーム)
バイト 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:
1001→0x9(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 クライアントで、マスキングフラグを設定し忘れた
解決方法:
- クライアント送信フレームの 2 バイト目の MSB を
1に設定 - マスキングキー(4 バイト)を生成して付加
- ペイロードに XOR 演算を適用
ケース 2:不明な切断原因の特定
エラーコード: Close Code 1006: Abnormal Closure
発生条件: ネットワークが不安定な環境で、予期せず切断が頻発
解決方法:
- Ping/Pong による生存確認を 30 秒間隔で実装
- タイムアウト時間を設定し、無応答なら再接続
- 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 の知識は強力な武器となります。ぜひ本記事の内容を実装やデバッグに活用してください。
関連リンク
- RFC6455: The WebSocket Protocol - WebSocket の公式仕様書
- MDN Web Docs: WebSocket - WebSocket API のリファレンス
- ws: a Node.js WebSocket library - Node.js 用の WebSocket ライブラリ
- Wireshark - パケットキャプチャ・解析ツール
- WebSocket Test Page - WebSocket の動作確認用ページ
articleWebSocket RFC6455 を図解速習:OPCODE・マスキング・Close コードの要点 10 分まとめ
articleWebSocket SLO/SLI 設計:接続維持率・遅延・ドロップ率の目標値と計測方法
articleWebSocket が「200 OK で Upgrade されない」原因と対処:プロキシ・ヘッダー・TLS の落とし穴
articleWebSocket 活用事例:金融トレーディング板情報の超低遅延配信アーキテクチャ
articleWebSocket でリアルタイム在庫表示を実装:購買イベントの即時反映ハンズオン
articleWebSocket プロトコル設計:バージョン交渉・機能フラグ・後方互換のパターン
articleReact でデータ取得を最適化:TanStack Query 基礎からキャッシュ戦略まで実装
articleAnsible Jinja2 テンプレート速攻リファレンス:filters/tests/macros
articlePython Dev Containers 完全レシピ:再現可能な開発箱を VS Code で作る
articleStorybook で Zustand をモックする:Controls 連動とシナリオ駆動 UI
articlePrisma を Monorepo で使い倒す:パス解決・generate の共有・依存戦略
articleプラグイン競合の特定術:WordPress で原因切り分けを高速化する手順
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来