MDN’s new design is in Beta! A sneak peek: https://blog.mozilla.org/opendesign/mdns-new-design-beta/

Writing WebSocket servers

草案
このページは完成していません。

Overview

WebSocketとはサーバーのポートをListenするTCPアプリケーションです。独自のサーバーを作ることは難しいように聞こえますが、シンプルなWebSocketサーバーを作るのはそれほど難しくありません。

WebSocketサーバーはC(++)、Python、PHPサーバーサイドJavascriptといった、Berkeley socketsをサポートするプログラミング言語で記述することができます。このドキュメントは特定のプログラミング言語のチュートリアルではなく、自分でWebSocketサーバーを作る一般的なガイドとなります。

このドキュメントを理解するには、HTTPの仕組みと中級程度のプログラミング経験が必要となります。また、言語によって、TCP socketsの知識も必要になるかもしれません。このドキュメントはWebSocketサーバーを書くための必要最低限の情報を解説します。

WebSocketsの最新の規格(RFC 6455)にも目を通してください。特にSection 1, 4~7はサーバー開発に役に立ちます。また、WebSocketsのセキュリティについて解説しているセクション10は、アプリケーションを公開する前に必ず精読するようにしてください。

Step 1: The WebSocket Handshake

まず、サーバーは標準的なTCP socketを使って接続をListenする必要があります。選択するプラットフォームによっては、自動で行われているかもしれません。本ドキュメントでは、example.comの8000ポートをListenし、/chatのHTTP/GETのリクエストを受け付けるWebSocketサーバーを例として解説します。

Warning: サーバーはどんなポートもListenすることができますが、80か443以外のポートを選択すると、ファイアーウォールとプロキシで問題が発生するかも知れません。443ポートは問題無い可能性が高いですが、TLS/SSL によるセキュアな接続が必要になります。また、大抵のブラウザ(Firefox 8+)は、セキュアなページからの非セキュアなWebSocketサーバーへの接続を許可していないことも留意してください。

ハンドシェイクはWebSocketsにおける"Web"で、HTTPとWebSocketsの橋渡しを行います。ハンドシェイクでは、接続のパラメータ等の詳細が取り交わされ、条件が満たされない場合はどちらかの側が引き下がります。サーバー側がクライアントが要求することを間違って解釈した場合は、セキュリティ問題に発展する可能性があります。

Client Handshake Request

WebSocketサーバー開発においても、まずはクライアントからのハンドシェイクから始める必要があります。そのため、クライアントのリクエストを理解する必要があります。clientは以下のような標準的なHTTPリクエストを送ります:

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

クライアントはヘッダに拡張やサブプロトコルを指定することができます。詳細はMiscellaneousを参照してください。また、User-AgentCookie等の一般的なヘッダも指定することができます。それらのヘッダはWebSocketに直接関係しないため、適宜必要なものを指定してください。無視してもらっても構いません。

間違いがあったり、解釈されないヘッダがある場合は、サーバーは"400 Bad Request"を返してソケットを閉じてください。サーバーはHTTPレスポンスボディに、ハンドシェイクが失敗した理由を設定することができますが、セキュリティの観点からブラウザには表示されません。 サーバーが特定のWebSocketバージョンを扱うことができない場合は、サーバーがサポートするバージョンをSec-WebSocket-Version ヘッダに指定しえてクライアントに返してください。(本ドキュメントではv13を使用します)。では次に、一番興味深いSec-WebSocket-Keyを説明します。

Tip: すべてのブラウザはOriginヘッダを送ります。このヘッダを送信元のチェック、ホワイト/ブラックリストに使用してください。サーバーが送信元を許可しない場合は、403 Forbiddenをクライアントに返してください。ただし、ブラウザ以外のユーザーエージェントは 偽のOriginを使うこともできるので注意してください。Originヘッダの無いリクエストは拒否することをお薦めします。

Tip: リクエストURI(ここでは/chatは規格上は特別な意味はありませんので、WebSocketに複数のアプリケーションを割り当てることができます。 例えば、 example.com/chatをチャットアプリ、/gameをゲームに割り当てることができます。

Note: 一般的な HTTP status codesはハンドシェイクの前だけ使用されます。ハンドシェイク成功後は、別のコード(規格7.4に定義されています)を使用してください。 

Server Handshake Response

サーバーがハンドシェイクのリクエストを受け取ると、以下のような、ちょっと変わった(でもHTTPです)レスポンスをクライアントへ送ります。(各ヘッダの末尾には \r\n 、、最後のヘッダにはもう一つ\r\nが付与されます):

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

加えて、サーバーは拡張とサブプロトコルをここで選ぶことができます。詳細はMiscellaneousを参照してください。Sec-WebSocket-Acceptヘッダは興味深いです。サーバーはクライアントから送られてきたSec-WebSocket-Key とマジックナンバー"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"を文字連結してSHA-1 hashを取り、base64>エンコーディングして Sec-WebSocket-Accept を生成します。

FYI: この部分はとても複雑に思えますが、サーバー側がWebSocketをサポートするかを判断するのに必要なプロセスなのです。これはとても重要なことで、もしサーバーがWebSocketをHTTPリクエストとして解釈してしまうとセキュリティ問題に発展しかねないからです。

例えば、キーが"dGhlIHNhbXBsZSBub25jZQ=="ならば、 Sec-WebSocket-Acceptヘッダは"s3pPLMBiTxaQ9kYGzzhZRbK+xOo="になります。サーバーがここで説明したヘッダーを送ったらハンドシェイクの完了です。データの送受信ができます!

Step 2: Exchanging Data Frames

サーバー、クライアントはいつでもメッセージを送れます。 まるで魔法のようです。しかし、残念ながらデータフレームと呼ばれるデータから情報を抽出するのは魔法のような経験とは言えないかもしれません。データフレームにはフォーマットがあり、32-bit keyでXOR encryptionが行われます。この部分の詳細については規格のSection 5を参照してください。

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 ...                |
+---------------------------------------------------------------+

RSV1-3は今後の拡張のための領域です。無視してください。MASKはメッセージがエンコードされているかのビットです。クライアントからサーバーへのメッセージは常にエンコードされますので、常に1が立っていると考えてください。

次の2つは一緒に使います。WebSocketはメッセージを複数のデータフレームに分割送信できることを思い出しましょう。FINはメッセージに続きがあるのかを表すビットです。このビットが0の場合はサーバーは次のメッセージを待ち、1の場合は 送信完了と判断します。opcode はこのデータフレームが何のためであるかを表します。opcodeが0x0の場合、サーバーはこのデータフレームのpayloadを直前のpayloadに連結して、次を待ちます。0x1の場合はpayloadはテキスト、0x2の場合はpayloadはバイナリデータになります。 (opcodeについては後に詳しく説明します) 例えば:

Client: FIN=1, opcode=0x1, msg="hello"
Server: OK. Hi.
Client: FIN=0, opcode=0x0, msg="and a"
Server: (listening silently)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening silently)
Client: FIN=1, opcode=0x0, msg="year!"
Server: OK. Happy new year to you too.

Decoding Payload Length

payloadの長さを表すpayload lengthというフィールドがあります。payload lengthを読み込むには、残念ながら少し複雑な手順が必要となります。手順は以下の通りです:

  1. 9-15番目のビットをunsigned integerとして読み込み、 値が125以下の場合は、それがpayload lengthとなります。値が126の場合は手順2に、127の場合は手順3に進みます。
  2. 次の16ビットををunsigned integerとして読み込みんだ値がpayload lenghtになります。
  3. 次の64ビットををunsigned integerとして読み込みんだ値がpayload lenghtになります。

Reading and Unmasking the Data

TODO. このセクションは追記が必要です。

Miscellaneous

WebSocketのコード、拡張、サブプロトコルはIANA WebSocket Protocol Registryに登録されています。このページでは上記以外のIANAへのリンクはありません。

WebSocketの拡張とサブプロトコルの使用ハンドシェイク時に取り決められます。時々、拡張とサブプロトコルはほとんど違いがないと思われがちですが、 明確な違いがひとつあります。拡張はデータフレームを管理し、payloadに変更を加えます。一方、サブプロトコルはpayloadに変更は加えませんExtensions are usually optional and generalized; subprotocols are usually mandatory and localized. 拡張、サブプロトコルのユースケースと例を見て行きましょう。

Extensions

このセクションは追記が必要です。どなたかお願いします。

拡張はファイルをEメールで送信する前に圧縮するようなものと考えてください。Dropboxやファイル共有サービスでも構いません。 もしくは、天気予報でデータを補完するようなものでもいいです... 何にしろあなたは同じインターネットを使って、ことなるフォーマットのファイルを送ることができます。受信者はあなたの手元のコピーと全く同じファイルを受け取れるはずです。これが拡張です。WebSocketsはデータ送信のプロトコルと基本的な方法を定義します。拡張は同じプロトコルで、データを圧縮したり統合(multiplex)して送信することができます。

拡張は規格の5.8, 9, 11.3.2および、11.4に記載されています。

TODO

Subprotocols

サブプロトコルはカスタムXML doctype宣言のようなものと考えてください。 XMLのシンタックスを叱っていますが、特定の構造に書き方に従う必要があります。WebSocket のプロトコルはそういったものと考えてください。サブプロトコルは派手なものではありませんが、構造を定義することができます。doctypeやschemaのように使用者はサブプロトコルに従う必要があります。doctypeやschemaと異なるのは、サブプロトコルはサーバーサイドで実装されており、クライアントからは明示的に見ることはできません。

サブプロトコルは規格の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

サーバーはサポートするサブプロトコルの中から一つを選びます。もし一つ以上候補がある場合は、最初の一つを選びます。この例では、サーバーがsoapwampのサポートするので、ハンドシェイクのレスポンスとして以下を返します。

Sec-WebSocket-Protocol: soap

サーバーがサブプロトコルの使用を許可しない場合、Sec-Websocket-Protocolヘッダを送ってはいけません。空のヘッダを送るのは間違いです。クライアントは要求したサブプロトコルのヘッダが受け取れない場合は、接続を閉じることができます。

サーバーを特定のサブプロトコルをサポートさせるためには、それ用のコードが必要になってきます。例えば送受信データをJSON形式にするjsonサブプロトコルを使いたい場合、っサーバー側にJSONパーサーが必要となります。実際にはこの部分はライブラリで行われますが、それでもサーバーはライブラリにデータを受け渡し等をする必要があります。

Tip: サブプロトコルの名前の衝突を避けるために、サブプロトコルの名前をドメインの一部にすることをお薦めします。Example Inc.という会社がプロプライエタリなチャットアプリを開発した場合は、このようなヘッダにすると良いかもしれませんSec-WebSocket-Protocol: chat.example.com。また、プロトコルのバージョンを表すのによく使われるのは、 chat.example.com/2.0とすることです。この命名ルールは必須ではなく単なる慣習です。あなたの好きな文字をサブプロトコル名としても構いません。

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

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