WebSocket サーバは、特定のプロトコルに従うサーバの任意のポートを待機する TCP アプリケーションです。カスタムサーバーを作成する作業は人々を悩ませる傾向があります。ただし、選択したプラットフォームに簡単な WebSocket サーバーを実装するのは簡単です。

WebSocket サーバは、C(++) や Python、PHP やサーバサイド JavaScript などの Berkeley sockets が利用可能なサーバサイドプログラミング言語で記述できます。これは特定の言語のチュートリアルではありませんが、独自のサーバーの作成を容易にするガイドとして役立ちます。

あなたはまだ HTTP がどのように動くのかを知り、中級プログラミング経験を得ている必要があるでしょう。言語サポートによっては、TCP ソケットに関する知識が必要な場合があります。このガイドの範囲は、WebSocket サーバーを作成するために必要な最小限の知識を提示することです。

最新の公式 WebSockets 仕様である RFC 6455 を参照してください。セクション 1 と 4-7 はサーバー実装者にとって特に興味深いものです。第 10 章ではセキュリティについて説明しています。サーバーを公開する前にセキュリティを正しく理解する必要があります。

ここでは WebSocket サーバについて非常に低いレベルで説明しています。WebSocket サーバは多くの場合、リバースプロキシ (通常の HTTP サーバなど) を使用して WebSocket ハンドシェイクを検出、事前処理し、それらのクライアントを実際の WebSocket サーバに送信します。つまり、(例えば) クッキーと認証ハンドラーを使用してサーバ側のコードを膨らませる必要はありません。

WebSocket ハンドシェイク

まず、サーバーは標準の TCP ソケットを使用して着信ソケット接続を待ち受ける必要があります。プラットフォームによっては、すでに処理されている可能性があります。たとえば、サーバーが example.com、8000番ポートで待ち受けているとし、ソケットサーバーが /chat で GET リクエストにレスポンスしたとします。

警告: サーバーは選択したポートで待機しますが、80 または 443 以外のポートを選択すると、ファイアウォールやプロキシに問題が発生する可能性があります。443番ポートの接続はより頻繁に成功する傾向がありますが、もちろんその接続には安全な接続 (TLS/SSL) が必要です。また、ほとんどのブラウザ (特に Firefox 8 以降) ではセキュリティで保護されていない WebSocket サーバーへの接続を許可していないことに注意してください。

ハンドシェイクは WebSockets の "Web" です。それは HTTP から WS への橋渡しです。ハンドシェイクでは、接続の詳細がネゴシエートされ、いずれの当事者も条件が悪い場合には完了前に取り消すことができます。 サーバーはクライアントがリクエストするすべてをのものを理解するように注意する必要があります。そうしないとセキュリティの問題が発生します。

クライアントハンドシェイクリクエスト

サーバーを構築しているにもかかわらず、依然としてクライアントは WebSocket ハンドシェイクプロセスを開始する必要があります。したがってクライアントのリクエストをどのように解釈するかを知っておく必要があります。クライアントは次のようなかなり標準的な HTTP リクエスト (HTTP バージョンは 1.1 以上でなければならず、メソッドはGET でなければなりません) を送信します。

GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

クライアントはここで拡張子 および/または サブプロトコルを求めることができます。詳細は「その他」を参照してください。また User-AgentRefererCookie、認証ヘッダーなどの一般的なヘッダーも存在する可能性があります。あなたはそれらで何でもしてください。WebSocket には直接関係しません。それらを無視することも安全です。多くの一般的な設定では、リバースプロキシは既にそれらを処理しています。

ヘッダーが解釈されていないか値が正しくない場合、サーバーは "400 Bad Request" を送信し、すぐにソケットを閉じる必要があります。通常は、HTTP レスポンス本体でハンドシェークが失敗した理由を示すかもしれませんが、メッセージは表示されないかもしれません (ブラウザはそれを表示しません)。 サーバーが WebSocket のバージョンを認識しない場合、サーバーは解釈可能なバージョンを含む Sec-WebSocket-Version ヘッダーを返す必要があります。(このガイドでは最新のv13について説明しています)。 ここで、最も興味深いヘッダーである Sec-WebSocket-Key に移動しましょう。

Tip: すべてのブラウザOrigin ヘッダーを送信します。 このヘッダをセキュリティ (同じ起点のチェック、ホワイトリスト/ブラックリストなど) に使用し、あなたが見ているものが気に入らなければ 403 Forbidden を送ることができます。ただし、ブラウザ以外のエージェントは、偽の Origin を送信するだけであることに注意してください。ほとんどのアプリケーションは、このヘッダーのない要求を拒否します。

Tip: request-uri (ここでは/chat) は仕様に定義された意味を持ちません。多くの人がうまくそれを使用して、あるサーバーが複数の WebSocket アプリケーションを処理できるようにします。たとえば、example.com/chat はマルチユーザチャットアプリを呼び出すことができ、同じサーバの /game はマルチプレイヤーゲームを呼び出すことができます。

Note: 通常の HTTP ステータスコードは、ハンドシェイクの前にのみ使用できます。ハンドシェイクが成功したら、別のコードセット (仕様の 7.4 節で定義されている) を使用する必要があります。

サーバーハンドシェイクレスポンス

このリクエストを受け取ったら、server はこれ (各ヘッダーは \r\n で終わり、最後の \r\n は最後に付く) と似たかなり奇妙な (ただしまだ HTTP の) レスポンスを送るべきです。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

さらに、サーバーはここでの 拡張/サブプロトコル リクエストを決定できます。詳細はその他を参照してください。Sec-WebSocket-Accept の部分は面白いです。サーバーは、クライアントが送信した Sec-WebSocket-Accept から派生しなければなりません。
これを取得するには、クライアントの Sec-WebSocket-Key"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" を連結して (これは "マジック文字列" です)、結果の SHA-1 ハッシュを取り、ハッシュのエンコーディングを base64 にして返します。

FYI: このように見た目は複雑すぎるプロセスが存在するため、サーバーが WebSocket をサポートしているかどうかはクライアントには明らかです。これはサーバーが WebSockets 接続を受け入れ、HTTP リクエストとしてデータを解釈する場合にセキュリティ上の問題が発生する可能性があるため重要です。

したがって、Key が "dGhlIHNhbXBsZSBub25jZQ==" だった場合、Accept は "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=" になります。サーバーがこれらのヘッダーを送信すると、ハンドシェイクは完了し、データのスワップを開始できます。

サーバーは、Set-Cookie のような他のヘッダーを送信したり、レスポンスハンドシェイクを送信する前に他のステータスコードで認証またはリダイレクトを要求したりすることができます。

クライアントを追跡する

これは WebSocket プロトコルには直接関係しませんが、ここで言及する価値はあります。サーバーはクライアントのソケットを追跡して、ハンドシェイクをすでに完了しているクライアントとハンドシェイクを再開しないようにする必要があります。同じクライアント IP アドレスが複数回接続しようとする可能性があります (サービス拒否攻撃から自分自身を守るためにサーバーが接続を多すぎると拒否することがあります)。

データフレームの交換

クライアントまたはサーバーのいずれかがいつでもメッセージを送信することができます。これが WebSocket の魔法です。しかし、これらのいわゆる「フレーム」のデータから情報を抽出することはあまり魔法のような経験ではありません。すべてのフレームは同じ特定のフォーマットに従いますが、クライアントからサーバーに向かうデータは XOR 暗号化 (32ビットキー) を使用してマスクされます。本明細書の第5節でこれについて詳細に説明する。

フォーマット

各データフレーム (クライアントからサーバーへ、またはその逆) は、次の同じ形式に従います。

Frame format:  
​​
      0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-------+-+-------------+-------------------------------+
     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
     | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     |     Extended payload length continued, if payload len == 127  |
     + - - - - - - - - - - - - - - - +-------------------------------+
     |                               |Masking-key, if MASK set to 1  |
     +-------------------------------+-------------------------------+
     | Masking-key (continued)       |          Payload Data         |
     +-------------------------------- - - - - - - - - - - - - - - - +
     :                     Payload Data continued ...                :
     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     |                     Payload Data continued ...                |
     +---------------------------------------------------------------+

MASK ビットは単にメッセージがエンコードされているかどうかを示します。クライアントからのメッセージはマスクされている必要がありますので、サーバはこれを 1 にする必要があります (実際、セクション5.1ではクライアントがマスクされていないメッセージを送信する場合、サーバはクライアントから切断する必要があります)。フレームをクライアントに戻すときは、マスクしたりマスクビットを設定しないでください。後でマスキングについて説明します。注意:セキュアソケットを使用している場合でも、メッセージをマスクする必要があります。RSV1-3 は無視することができますが、それは拡張のためのものです。

opcode フィールドは、ペイロードデータをどのように解釈するかを定義します。継続の場合 0x0 、テキスト (UTF-8 で常にエンコードされる) の場合は 0x1、バイナリの場合は 0x2、およびその他のいわゆる「制御コード」については後で説明します。この WebSocket バージョンでは、0x3 〜  0x7 および 0xB 0xF は意味を持ちません。

FIN ビットは、これがシリーズ内の最後のメッセージであるかどうかを示します。0 の場合、サーバーはメッセージのより多くの部分をリスニングし続けます。それ以外の場合、サーバーは配信されたメッセージを考慮する必要があります。これについては後で詳しく説明します。

ペイロード長のデコード

ペイロードデータを読み取るには、いつ読み終えるべきかを知っておく必要があります。そのためペイロードの長さを知ることが重要です。残念ながら、これはやや複雑です。それを読むには、次の手順を実行します。

  1. ビット9〜15 (インクルーシブ) を読み取り、それを符号なし整数として解釈します。それが 125 以下であれば、それが長さです。あなたはそれを読み終えました。126 の場合は手順2に、127 の場合は手順3に進みます。
  2. 次の16ビットを読み取り、それらを符号なし整数として解釈します。 それであなたは読み終えました。
  3. 次の 64 ビットを読んで、それらを符号なし整数として解釈する(最上位ビットは 0 でなければならない)。それであなたは読み終えました。

データの読み込みとマスク解除

MASK ビットがセットされていれば (クライアントからサーバへのメッセージではそうあるべきです)、次の4オクテット (32ビット) を読み込みます。これがマスキングキーです。ペイロード長とマスキングキーがデコードされたら、ソケットからそのバイト数を読み取ることができます。データを ENCODED、キーを MASK としましょう。DECODED を取得するには、ENCODED のオクテット (テキストデータの文字のバイト) をループし、オクテットを MASK の (iモジュロ4) オクテットを使用して XOR します。擬似コードの場合(JavaScript が有効な場合)

var DECODED = "";
for (var i = 0; i < ENCODED.length; i++) {
    DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}

これで、アプリケーションに応じて DECODED が何を意味するのかを理解することができます。

メッセージフラグメンテーション

FIN フィールドとオペコードフィールドは連携して、別々のフレームに分割されたメッセージを送信します。これはメッセージフラグメンテーションと呼ばれます。フラグメンテーションは、オペコード 0x00x2 でのみ使用できます。

オペコードはフレームの意味を示しています。0x1 の場合、ペイロードはテキストです。0x2 の場合、ペイロードはバイナリデータです。ただし、0x0 の場合、フレームは継続フレームです。つまりサーバーはフレームのペイロードをそのクライアントから受信した最後のフレームに連結する必要があります。ここでは、サーバーがテキストメッセージを送信するクライアントに反応する概略を示します。第1のメッセージは単一のフレームで送信され、第2のメッセージは3つのフレームにわたって送信されます。FIN とオペコードの詳細は、クライアントに対してのみ表示されます。

Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!

最初のフレームにメッセージ全体が含まれていることに注意してください(FIN=1 および opcode!=0x0)、それによりサーバは適切に処理またはレスポンスできます。クライアントが送信した2番目のフレームにはテキストペイロード (opcode=0x1) がありますが、メッセージ全体がまだ到着していません (FIN=0)。そのメッセージの残りの部分はすべて継続フレーム(opcode=0x0) と共に送信され、メッセージの最終フレームは FIN=1 でマークされます。仕様の 5.4 節では、メッセージフラグメンテーションについて説明があります。

Ping と Pong: WebSockets のハートビート

ハンドシェイク後の任意の時点で、クライアントまたはサーバのどちらかが、相手にpingを送信することを選択できます。 pingが受信されると、受信者はできるだけ早くポンを返さなければなりません。 これを使用して、たとえばクライアントがまだ接続されていることを確認できます。

Ping や Pong は単なる通常のフレームですが、コントロールフレームです。ping のオペコードは 0x9、pong のオペコードは 0xA です。ping を取得したら、ping と同じペイロードデータを持つ pong を送ります (ping と pong の場合、最大ペイロード長は125です)。ping を送信することなく pong を取得することもできます。その場合はこれを無視してください。

あなたが pong を送信する機会を得る前に複数の ping を取得した場合でも、1つの pong しか送信しません。

接続を閉じる

クライアントまたはサーバの接続を閉じるには指定した制御シーケンスを含むデータの制御フレームを送信して、終了ハンドシェイクを開始します (5.5.1 項を参照)。このようなフレームを受信すると、もう1つの peer はレスポンスとしてクローズフレームを送信します。最初の peer は接続を閉じます。接続の終了後に受信されたそれ以上のデータは、その後破棄されます。

その他

WebSocket コード、エクステンション、サブプロトコルなどは、IANA WebSocket プロトコルレジストリに登録されています。

WebSocket のエクステンションとサブプロトコルは、ハンドシェイク中にヘッダーを介してネゴシエートされます。エクステンションとサブプロトコルは異なるものというにはあまりにも似ていることがありますが、明確な区別があります。エクステンションは WebSocket フレームを制御し、ペイロードを変更しますが、サブプロトコルは WebSocket ペイロードを構造化しますが、何も変更しません。エクステンションは任意のもので一般化されています (圧縮など)。サブプロトコルは必須のもので、ローカライズされています (チャットや MMORPG ゲームなど)。

エクステンション

このセクションは拡張が必要です。あなたがそうする準備ができている場合は編集してください。

エクステンションはファイルを誰かに電子メールで送る前に圧縮していると考えてください。あなたが何をしても、同じデータをさまざまな形で送信しています。受信者は最終的にローカルコピーと同じデータを得ることができますが、別の方法で送信されます。それがエクステンションの機能です。WebSockets はプロトコルとデータを送信する簡単な方法を定義しますが、圧縮などのエクステンションでは同じデータを短い形式で送信することができます。

エクステンションについては、仕様の 5.8, 9, 11.3.2, 11.4 節で説明しています。

TODO

サブプロトコル

サブプロトコルをカスタム XML スキーマまたは doctype 宣言と考えてください。あなたはまだ XML とその構文を使用していますが、あなたが合意した構造によってさらに制限されます。WebSocket のサブプロトコルはまさにそのようなものです。それらは空想的な何かを導入しておらず、構造を確立するだけです。doctype やスキーマと同様に、両者はサブプロトコルに同意しなければなりません。doctype やスキーマとは異なり、サブプロトコルはサーバー上に実装されており、クライアントから外部参照することはできません。

サブプロトコルは、仕様のセクション 1.9, 4.2, 11.3.4、および 11.5 で説明されています。

クライアントは特定のサブプロトコルを要求する必要があります。 これを行うには、元のハンドシェイクの一部として次のようなものを送ります:

GET /chat HTTP/1.1
...
Sec-WebSocket-Protocol: soap, wamp

または同様に:

...
Sec-WebSocket-Protocol: soap
Sec-WebSocket-Protocol: wamp

これでサーバーはクライアントが提案してサポートしているプロトコルの1つを選択する必要があります。複数ある場合は、クライアントが送信した最初のものを送信します。私たちのサーバーが soapwamp の両方を使用できると想像してください。 次に、レスポンスハンドシェイクで次のメッセージが送信されます。

Sec-WebSocket-Protocol: soap

サーバーは複数の Sec-Websocket-Protocol ヘッダーを送信できません。
サーバーがサブプロトコルを使用したくない場合、Sec-WebSocket-Protocol ヘッダーを送信すべきではありません。 空白のヘッダーを送信するのが間違っています。
クライアントは、必要なサブプロトコルを取得できない場合に接続を閉じることがあります。

サーバーが特定のサブプロトコルに従うようにしたいのであれば、必然的にサーバー上に特別なコードが必要になります。json サブプロトコルを使用しているとしましょう。このサブプロトコルではすべてのデータが JSON として渡されます。クライアントがこのプロトコルを要求し、サーバーがそれを使用したい場合、サーバーは JSON パーサーを持つ必要があります。実際に言えば、これはライブラリの一部になりますが、サーバーはデータを渡す必要があります。

Tip: 名前の競合を避けるため、サブプロトコル名をドメイン文字列の一部にすることをお勧めします。Example Inc. 専用の独自の形式を使用するカスタムチャットアプリを構築する場合は、次のように使用します: Sec-WebSocket-Protocol: chat.example.com。これは必須ではないことに注意してください。これは単なるオプションです。任意の文字列を使用できます。

関連

ドキュメントのタグと貢献者

このページの貢献者: silverskyvicto, yukinarit, teoli
最終更新者: silverskyvicto,