Windows 8 のネットワーク接続

Windows 8 と WebSocket プロトコル

Kenny Kerr

 

WebSocket プロトコルの目的は、接続を確立して要求と応答のペアを開始する全責任をクライアントが担う Web 環境で、双方向通信を実現することです。これにより、アプリケーションは TCP のメリットを Web に適した方法で活かすことができるようになります。WebSocket プロトコルは、2011 年 12 月にインターネット技術標準化委員会 (IETF) によって標準化されただけで、この記事の執筆時点では World Wide Web コンソーシアムでもいまだ検討中であることを考えると、Windows 8 でこの新しいインターネット技術がこれほどまでに包括的に採用されたのには驚かれるかもしれません。

今回は、まず WebSocket プロトコルのしくみを紹介し、大規模な TCP/IP スイートとの関係を明らかにします。次に、プログラマが Windows 8 のアプリケーションでこの新技術を簡単に採用できるようにするさまざまな方法を紹介します。

WebSocket が必要な理由

このプロトコルの主な目標は、ブラウザー ベースのアプリケーションが要求と応答の組み合わせに捕らわれず自由にサーバーと通信するために、標準化された効率的な方法を実現することです。数年前、Web 開発者の間では Asynchronous JavaScript and XML (AJAX) と、それを使って動的で対話型のシナリオをどのように実現するかについての話題でもちきりでした (実際に現実のものとなりました)。ただし、そのすべての引き金となった XMLHttpRequest オブジェクトは、ブラウザーが HTTP 要求を行えるようにするだけのものでした。サーバーからクライアントに帯域外でメッセージを送信する場合はどうすればよいのでしょう。これに答えるのが WebSocket プロトコルです。WebSocket プロトコルは、単にサーバーからクライアントにメッセージを送信できるようにするだけでなく、それを実行するのに HTTP のオーバーヘッドを必要としないで、TCP 接続そのものの速度に近い双方向通信を実現します。WebSocket プロトコルが登場するまでの Web 開発者は、サーバーに更新をポーリングし、Comet 形式のプログラミング技術を使用してアプリケーションを最新状態に保っていました。そして、そのためだけにプロトコルに多くのオーバーヘッドをかけるほど多大な HTTP 接続を行い、HTTP を乱用していました。サーバーは過負荷状態になり、帯域幅が浪費され、Web アプリケーションが過度に複雑になります。WebSocket プロトコルは、驚くほど単純かつ効率的な方法でこのような問題を解決しますが、その方法を説明するには、まずその土台となる歴史的な状況を説明しておく必要があるでしょう。

TCP/IP スイート

TCP/IP はプロトコル スイート、つまり相互に関連するプロトコルの集合で、インターネット アーキテクチャを実装します。現在の形式に進化するまで長い年月が費やされてきました。パケット交換ネットワークの考え方が初めて提唱されたのは 1960 年代のことですが、その後世界は劇的に変化しています。コンピューターははるかに高速になり、ソフトウェアはより高い処理能力が求められるようになり、インターネットが爆発的に普及してありとあらゆる情報、通信、対話のネットワークとなり、今日一般的に使用される多くのソフトウェアにとって不可欠な存在になっています。

TCP/IP スイートは、開放型システム間相互接続 (OSI) 階層モデルにおおまかに従ってモデル化されており、複数の階層から構成されます。各層のプロトコルは特に明確に区別されていませんが、TCP/IP はその有用性が明確に証明されており、階層化の問題点はハードウェアとソフトウェアの設計をうまく組み合わせることで乗り越えられてきました。TCP/IP が複数の層に分割されている (必ずしも明瞭にではなくても) ことで、ハードウェアと技術の変化に合わせて時代と共に進化することを可能にし、さまざまなスキルを持ったプログラマがさまざまなレベルで抽象化を行って、プロトコル スタック自体を構築したり、その多様な機能を利用するアプリケーションを開発するのに役立てられてきました。

最下層は有線メディアのアクセス制御や Wi-Fi などの物理プロトコルで、物理接続、ローカル アドレス指定、エラー検出を提供します。大半のプログラマは、これらのプロトコルについて考えることはあまりありません。

上の層に目を向けると、インターネット プロトコル (IP) 自体はネットワーク層に常駐し、TCP/IP が異なる物理層をまたがって相互運用できるようにしています。この層は、コンピューター アドレスを物理アドレスに対応付け、コンピューター間でパケットをルーティングします。

次に、付加プロトコルがあります。付加プロトコルがどの層に存在するかを説明することも可能ですが、これらが実際に行うのは、自動構成、名前解決、検出、ルーティングの最適化、診断などをサポートする不可欠な役割の提供です。

さらに上部の層には、トランスポートやアプリケーションのプロトコルがあります。トランスポート プロトコルは、下層のパケットの多重化と多重化解除を処理し、物理層とネットワーク層が 1 つしかなかったとしても、多くの異なるアプリケーションが通信チャネルを共有できるようにします。さらに、多くの場合トランスポート層はエラー検出、信頼性の高い配信、輻輳やフロー制御などのパフォーマンスに関連する機能までも提供します。アプリケーション層は、従来から HTTP (Web ブラウザーとサーバーが実装) や SMTP (電子メール クライアントとサーバーが実装) などのプロトコルが配置される場所です。HTTP などのプロトコルを利用することが一層増え、パフォーマンスを向上し、異なるアプリケーションで実装を共有するために、これらの実装は OS の深部へと移動しています。

TCP と HTTP

TCP/IP スイートのプロトコルのうち、多くのプログラマに最もよく知られているのは、おそらくトランスポート層にある TCP とユーザー データグラム プロトコル (UDP) です。どちらもプロトコルが IP アドレスと組み合わせて使用する "ポート" の抽象化を定義し、パケットの着信時と発信時に多重化および多重化解除を行います。

UDP は動的ホスト構成プロトコル (DHCP) や DNS などの他の TCP/IP プロトコル用に頻繁に使用され、プライベート ネットワーク アプリケーションで広く採用されてきましたが、インターネット全般での採用は TCP ほどには普及しませんでした。一方 TCP は、ほぼ HTTP のおかげで、あらゆるところで幅広く採用されてきました。TCP は UDP と比べてはるかに複雑ですが、この複雑さのほとんどはアプリケーション層からは隠ぺいされており、アプリケーションは複雑さの影響を受けることなく TCP のメリットを利用できます。

TCP は、コンピューター間で信頼性の高いデータのフローを実現しますが、その実装はきわめて複雑です。TCP は、パケットの順序とデータの再構築、エラー検出と復旧、輻輳制御とパフォーマンス、タイムアウト、再転送など、他にもたくさんのことを管理します。ただし、アプリケーションが確認するのはポート間の双方向接続のみで、送受信されるデータは正しい順序で転送されることを前提にできます。

現代の HTTP は、信頼性の高い接続指向のプロトコルを前提としており、TCP が最もふさわしい普遍的な選択肢であることは明らかです。このモデルでは、HTTP がクライアント サーバー プロトコルとして機能します。クライアントからサーバーに TCP 接続を開きます。次に要求を送信し、サーバーが評価して応答します。これは、毎日世界中で 1 秒間に数え切れないくらい繰り返されていることです。

おわかりのように、これは TCP が提供する機能の単純さまたは制約を示しています。TCP では、双方が同時にデータを送信できます。他方が要求を送信するのを待ってから応答する必要はありません。ただし、この単純さが、サーバー側での応答のキャッシュを可能にし、Web の拡張能力に大きな影響を与えました。HTTP の普及は、当初の単純さに後押しされたことは疑いようがありません。TCP がバイナリ データの双方向チャネル (一組のストリームと言ってもかまいません) を提供するのに対して、HTTP は応答メッセージに先行して要求メッセージを提供します。メッセージ本文は他の方法でエンコードされる場合もありますが、どちらのメッセージも ASCII 文字で構成されます。単純な要求は、たとえば次のようになります。

GET /resource HTTP/1.1\r\n
host: example.com\r\n
\r\n

各行は、復帰 (\r) と改行 (\n) で終わります。最初の行は要求行と呼ばれ、リソースにアクセスするメソッド (ここでは GET)、リソースのパス、そして最後に使用する HTTP のバージョンを指定します。下層のプロトコルと同様に、HTTP はこのリソース パスを介して多重化と多重化解除を実現します。この要求行の後には、1 行または複数行のヘッダー行を続けます。ヘッダーは、上記の例で示しているように名前と値から構成します。host など必須のヘッダーもありますが、ほとんどはブラウザーとサーバーのより効率的な通信や機能の取り決めをサポートするだけです。

応答は、たとえば次のようになります。

HTTP/1.1 200 OK\r\n
content-type: text/html\r\n
content-length: 1307\r\n
\r\n
<!DOCTYPE HTML><html> ... </html>

基本的に形式は同じですが、要求行の代わりに、応答行は、使用する HTTP のバージョン、状態コード (200)、および状態コードの概要を示します。状態コード 200 は、要求が正常に処理され、すべての結果が直後に続くヘッダー行に含まれていることをクライアントに伝えます。たとえばサーバーが状態コード 404 を返す場合は要求されたリソースが存在しないことを示しています。ヘッダーの形式は、要求ヘッダーと同じです。上記の content-type ヘッダーは、メッセージ本文で要求されるリソースが HTML として解釈されることを、content-length ヘッダーは、メッセージ本文に含まれるバイト数をブラウザーに伝えます。HTTP メッセージはメッセージの境い目を表わさない TCP 経由で通信されるため、これは重要です。コンテンツの長さがわからなければ、HTTP アプリケーションはさまざまなヒューリスティックを使用してメッセージ本文の長さを決める必要があります。

このようにすべてはきわめて単純で、HTTP の設計がわかりやすいことを示しています。しかし、HTTP はこの単純さを保てなくなっています。今日の Web ブラウザーやサーバーは、相互に関連する数千もの機能を持った最新鋭のプログラムで、HTTP は主戦力としてそのすべてに対応しなければなりません。複雑さのほとんどは、高速化のニーズから生まれます。メッセージ本文の圧縮方法を取り決めるヘッダー、メッセージ本文の転送を回避するためのキャッシュと有効期限を表すヘッダーなど、さまざまなヘッダーが使われるようになっています。さまざまなリソースを組み合わせて、HTTP 要求の数を削減する手法が開発されています。コンテンツ配信ネットワーク (CDN) は、世界中に配布されるまでになり、頻繁に利用するリソースを、それを使用する Web ブラウザーの近くでホストすることが試みられています。

このようなすべての進化にもかかわらず、多くの Web アプリは、場面に応じて HTTP から離れ、TCP のストリーミング モデルに回帰するなんらかの方法があれば、優れた拡張性に加え、単純さまでも実現できます。まさにこれを実現するのが WebSocket プロトコルです。

WebSocket ハンドシェイク

WebSocket プロトコルは TCP よりも上位かつ HTTP と同じレベルで TCP/IP スイートにぴったりと適合します。新しいプロトコルをインターネットに導入する際の課題の 1 つは、無数のルーター、プロキシ、およびファイアウォールに何も変わっていないと思わせる方法です。WebSocket プロトコルは、同じ基盤となる TCP 接続で、独自の WebSocket データ転送に切り替える前に HTTP として見せかけ、この目的を達成します。これによって、疑うことを知らない多くの中継サービスをアップグレードして、WebSocket 通信がそのネットワーク接続を通過できるようにする必要がなくなります。実際には、これは必ずしもスムーズに行われるわけではありません。熱意が空回りしたルーターが HTTP の要求や応答をいじり、プロキシのキャッシュ、アドレス、リソース変換などを、自身の目的に合うように書き直そうとするためです。短期間で行える効果的な解決策は、セキュリティが確保されるチャネル、つまりトランスポート層セキュリティ (TLS) で WebSocket プロトコルを使用する方法です。こうすれば、多くの場合こうした書き直しを最小限に抑えられます。

WebSocket プロトコルは、IP、UDP、TCP、HTTP などのさまざまなソースからアイデアを取り入れ、それらの概念をより単純な形で Web ブラウザーなどのアプリケーションで使用できるようにします。これはすべて、HTTP の要求と応答のペアとまったく同じように見せかけ、動作するように設計されたハンドシェイクから始まります。これは、クライアントまたはサーバーが WebSocket を使用していることを互いに隠すためではなく、他の TCP 接続が HTTP を利用しているかのようにさまざまな中継サービスに思わせるために行われます。実際、WebSocket プロトコルは、すべての当事者が誤って接続を受け入れないようにすることを意図して設計されています。これは、クライアントがあらゆる意図と目的でハンドシェイク、つまり HTTP 要求を送信することから始まります。たとえば次のようになります。

GET /resource HTTP/1.1\r\n
host: example.com\r\n
upgrade: websocket\r\n
connection: upgrade\r\n
sec-websocket-version: 13\r\n
sec-websocket-key: E4WSEcseoWr4csPLS2QJHA==\r\n
\r\n

ご覧のとおり、これが完全に有効な HTTP 要求ではないと思えるところは 1 つもありません。疑うことを知らない中継サービスは、サーバーが WebSocket サーバーを包み込んだ HTTP サーバーだとしても、この要求を単純にそのサーバーに渡します。この例の要求行では、標準の GET 要求を指定しています。これは、WebSocket サーバーが、大半の HTTP サーバーと同じ方法で、1 つのサーバーで複数のエンドポイントにサービスを提供できるようにしていることも意味します。host ヘッダーはこのヘッダーを必須とする HTTP 1.1 と同じ目的で機能し、双方が共有するホスティング シナリオで、ホスティング ドメインに合意することを保証します。upgrade ヘッダーと connection ヘッダーは、接続で使用されるプロトコルのアップグレードをクライアントが要求するために使用する標準 HTTP ヘッダーです。この手法は、まれに HTTP クライアントが TLS 接続のセキュリティ保護に移行するために使用されることもあります。ただし、これらは WebSocket プロトコル必須のヘッダーです。具体的には、upgrade ヘッダーは接続を WebSocket プロトコルにアップグレードすべきであることを示し、connection ヘッダーはこの upgrade ヘッダーがその接続に限定的なもので、他の接続経由でプロキシが通信してはいけないことを示します。

sec-websocket-version ヘッダーも必須でその値は 13 でなければなりません。サーバーが WebSocket サーバーでも、このバージョンをサポートしていなければハンドシェイクを中止し、その旨を示す HTTP 状態コードを返します。すぐにおわかりになるように、サーバーが WebSocket プロトコルについて一切把握しておらず、なんの疑いもなく正常な応答を返しても、クライアントは接続を中止するように設計されています。

sec-websocket-key ヘッダーは、実際に WebSocket ハンドシェイクのキーになります。WebSocket プロトコルの設計者は、実際には WebSocket クライアントではないクライアントからの接続を、サーバーが受け入れないようにすることを意図しました。悪意のあるスクリプトがフォームの送信を構築したり、XMLHttpRequest オブジェクトを使用して sec-* ヘッダーを追加することで WebSocket 接続を装ったりしないように考えられています。双方に正式な接続が確立されていることを証明するために、sec-websocket-key ヘッダーはクライアント ハンドシェイクにも存在する必要があります。値はランダムに (理想的には暗号化を使用して) 選択した 16 バイトの数値 (セキュリティ用語では Nonce 値として知られています) にする必要があります。この数値はその後このヘッダー値用に base64 エンコードされます。

クライアント ハンドシェイクを送信したら、クライアントは応答を待機し、サーバーが本当に WebSocket 接続を確立しようとしており、それが可能であるかを検証します。サーバーが拒否しなければ、次のようにサーバー ハンドシェイクを HTTP 応答として送信します。

HTTP/1.1 101 OK
upgrade: websocket\r\n
connection: upgrade\r\n
sec-websocket-accept: 7eQChgCtQMnVILefJAO6dK5JwPc=\r\n
\r\n

先ほどと同様に、これも完全に有効な HTTP 応答です。応答行には状態コードを添えた HTTP バージョンが含まれますが、正常完了したことを示す通常の状態コード 200 の代わりに、サーバーは、アップグレード要求を把握し、プロトコルを切り替えようとしていることを示す標準の状態コード 101 を返す必要があります。状態コードの英語による説明は、なんでもかまいません。"OK" や "Switching to WebSocket (WebSocket への切り替え)"、さらにはマーク・トウェインからお好きなセンテンスを引用してもまったく問題ありません。重要なのは状態コードであり、クライアントはそれが状態コード 101 であることを確認する必要があります。たとえば、サーバーは要求を拒否し、WebSocket クライアント ハンドシェイクを受け入れる前に状態コード 401 の使用を認証することをクライアントに求めることもできます。ただし、応答が成功するには、ここでも誤解を生まないように、upgrade ヘッダーと connection ヘッダーを含め、状態コード 101 が WebSocket プロトコルへの切り替えについて言及していることを認める必要があります。

最後に、ハンドシェイクを検証するため、クライアントは sec-websocket-accept ヘッダーを応答に含め、値が正しいことを確認する必要があります。サーバーは、クライアントが送信した base64 エンコード値をデコードする必要はありません。サーバーは、単純にこの文字列を受け取り、文字列表現を既知の GUID に連結し、SHA-1 アルゴリズムと組み合わせてハッシュを生成し、20 バイト値を作ります。これを base64 エンコードし、sec-websocket-accept ヘッダーの値として使用します。これでクライアントは、サーバーが必要なことを確かに実行したことを簡単に検証でき、双方が WebSocket 接続に同意していることが保証されます。

すべてうまく進んだ場合、この時点で有効な WebSocket 接続が確立され、双方が WebSocket データ フレームを使用して自由かつ同時に双方向通信できます。WebSocket プロトコルについて学ぶと、Web のセキュリティの問題についての警告を踏まえて設計されていることがよくわかります。それ以前のほとんどのプロトコルとは異なり、WebSocket プロトコルはセキュリティに留意して設計されています。WebSocket プロトコルは、クライアントが実際に Web ブラウザーの場合は本来のヘッダーを含めることも要求します。これにより、ブラウザーは cross-origin 攻撃に対する保護を提供できます。もちろん、これが機能するのは、ブラウザーのホスティング環境など、信頼されているホスティング環境のコンテキストに限られます。

WebSocket データ転送

WebSocket プロトコルは、複雑でオーバーヘッドを生じる層を追加することなく、IP と TCP が提供する比較的パフォーマンスが高く、オーバーヘッドの少ない通信モデルに Web を戻すことを目的としています。このため、ハンドシェイクが完了したら、WebSocket のオーバーヘッドは最小限に保たれます。WebSocket プロトコルは、IP をパケット化する TCP の上位にパケットのフレーム化メカニズムを提供します。TCP 自体もこのパケット化に基づいており、このため UDP はこれほどまで普及したのですが、WebSocket プロトコルでは、プロトコルの負担となるパケット サイズの制限がありません。TCP がストリームに基づく抽象化を提供するのに対し、WebSocket はメッセージに基づく抽象化をアプリケーションに提供します。また、TCP ストリームがセグメントを使用して送信されるのに対し、WebSocket メッセージは一連のフレームとして送信されます。これらのフレームは同じ TCP 接続経由で送信されるため、信頼性が高く、シーケンシャルに配信されることを自然なかたちで想定できます。フレーム化のプロトコルは、いくぶん手が込んではいますが、きわめて小さく設計されており、多くの場合フレーム化のオーバーヘッドはわずか数バイトの追加でしかありません。最初のハンドシェイクが終わったら、クライアントまたはサーバーからデータ フレームを送信できます。

各フレームには、フレームの種類とペイロードのサイズを表すオペコードを含めます。このペイロードは、アプリケーションが通信する実際のデータと、事前に決められた拡張データを表します。興味深いのは、プロトコルはメッセージをフラグメントに分解できるようにしていることです。本格的なネットワーキングの知識があれば、IP レベルのフラグメント化がパフォーマンスについて持つ意味と、TCP がフラグメント化を回避する理由となっている難点について思い出すかもしれません。しかし、WebSocket のフラグメント化の考え方はまったく違います。ここでの考え方は、WebSocket プロトコルが、サイズの制限なく、ネットワーク パケットの利便性を提供できるようにすることです。送信するメッセージの正確な長さを送信者が把握していない場合、メッセージをフラグメント化し、各フレームは提供するデータの量とそれが最後のフラグメントかどうかを示します。さらに、フレームはバイナリ データか UTF-8 エンコード テキストのどちらを含んでいるかを単に示します。

コントロール フレームも定義され、主に接続を閉じるために使用されますが、他のエンドポイントに対して ping を実行するハートビートとしても使用され、まだ応答可能であることを保証するか、TCP 接続を継続した状態に保つことをサポートします。最後に指摘しておきますが、Wireshark などのネットワーク プロトコル アナライザーを使用して、クライアントが送信した WebSocket フレームを調べることになった場合は、データ フレームにエンコード済みのデータが含まれているように見えるかもしれません。WebSocket プロトコルは、クライアントからサーバーに送信されるすべてのデータ フレームがマスクされることを要求します。マスク処理は、データ バイトとマスキング キーの排他的論理和 ("XOR") を求める単純なアルゴリズムです。マスキング キーがフレームに含まれるため、セキュリティに関連はしているものの、セキュリティ機能と呼べるほどのものではありません。前述のように、WebSocket プロトコルの設計者は、セキュリティに関連するさまざまなシナリオに多大な労力を費やし、プロトコルが攻撃される多様な方法を予測することを試みました。そうした攻撃手段の 1 つに、インターネット インフラストラクチャの他の部分 (ここでは、プロキシ サーバー) を侵害することで、WebSocket プロトコルを間接的に攻撃する方法があると分析されました。疑うことを知らないプロキシ サーバーは、WebSocket ハンドシェイクが GET 要求と似ていることを認識しておらず、攻撃者からの偽の GET 要求のデータをキャッシュしてしまい、その結果一部のユーザーが有害なキャッシュを利用する可能性があります。各フレームを新しいキーでマスクすれば、フレームを予測できなくなり、サーバーで誤って認識されることがなくなるため、この脅威を軽減できます。この攻撃についてはまだまだ検討すべきことがあり、近い将来、専門家がさらなる悪用の可能性について明らかにするでしょう。それでもなお、さまざまな形態の攻撃を予測しようと、設計者がこれほどまでに労力を費やしてきたのには感銘を受けます。

Windows 8 と WebSocket プロトコル

WebSocket プロトコル自体をよく理解することも役立ちますが、Windows 8 が間違いなく提供する、幅広いサポートを備えたプラットフォームに取り組むことも非常に役立ちます。実際にプロトコルを自分で実装することなく、WebSocket プロトコルを使用する方法をいくつか見てみましょう。

Windows 8 は Microsoft .NET Framework を提供し、ネイティブ コードにもマネージ コードにも Windows ランタイムを使ってクライアントをサポートしており、C++ で Windows HTTP サービス (WinHTTP) API を使用して WebSocket クライアントを作成できるようにしています。最後に、IIS 8 はネイティブ WebSocket モジュールを提供し、当然ながら Internet Explorer は WebSocket プロトコルのネイティブ サポートを提供します。これにより、さまざまな環境が混在することになりますが、もっと驚きなのは、Windows 8 に含まれるのは WebSocket の 1 つの実装のみで、それがすべての環境で共有されることです。WebSocket プロトコル コンポーネント API は、実際のネットワーク接続を一切作成することなく、ハンドシェイクとフレーム化のためのプロトコルの規則をすべて実装します。これで、さまざまなプラットフォームとランタイムがこの共通の実装を使用でき、任意のネットワーキング スタックにフックできるようになります。

.NET クライアントとサーバー

.NET Framework は、ASP.NET に拡張機能を提供し、それ自体が IIS によって使用されるネイティブ HTTP サーバー API に基づく HttpListener を提供して、WebSocket プロトコルのサーバー サポートを利用できるようにします。ASP.NET の場合は、新しい HttpContext.AcceptWebSocketRequest メソッドを呼び出して特定のエンドポイントで WebSocket 要求を受け取る HTTP ハンドラーを記述するだけです。HttpContext.IsWebSocketRequest プロパティを使用して、要求が間違いなく WebSocket クライアント ハンドシェイクであることを検証できます。ASP.NET 以外では、単に HttpListener クラスを使用して WebSocket サーバーをホストできます。多くの場合、実装も両者の間で共有されます。図 1 は、こうしたサーバーの簡単な例を示しています。

図 1 HttpListener を使用する WebSocket サーバー

static async Task Run()
{
  HttpListener s = new HttpListener();
  s.Prefixes.Add("http://localhost:8000/ws/");
  s.Start();
  var hc = await s.GetContextAsync();
  if (!hc.Request.IsWebSocketRequest)
  {
    hc.Response.StatusCode = 400;
    hc.Response.Close();
    return;
  }
  var wsc = await hc.AcceptWebSocketAsync(null);
  var ws = wsc.WebSocket;
  for (int i = 0; i != 10; ++i)
  {
    await Task.Delay(2000);
    var time = DateTime.Now.ToLongTimeString();
    var buffer = Encoding.UTF8.GetBytes(time);
    var segment = new ArraySegment<byte>(buffer);
    await ws.SendAsync(segment, WebSocketMessageType.Text,
      true, CancellationToken.None);
  }
  await ws.CloseAsync(WebSocketCloseStatus.NormalClosure,
    "Done", CancellationToken.None);
}

ここでは、C# の非同期メソッドを使用して、シーケンシャルで一貫したコードにしていますが、実際にはすべて非同期です。まず、エンドポイントを登録し、要求が着信するのを待ちます。次に、要求が実際に WebSocket ハンドシェイクとして認証されているかを確認し、認証されていない場合は、状態コード 400 "無効な要求" を返します。その後、AcceptWebSocketAsync を呼び出し、クライアント ハンドシェイクを受け入れ、ハンドシェイクが完了するのを待機します。これで、WebSocket オブジェクトを使用して自由に通信できるようになります。この例では、短い遅延の後、それぞれが時刻を含む UTF-8 を 10 フレーム送信します。各フレームは、SendAsync メソッドを使用して非同期に送信されます。このメソッドは非常に強力で、UTF-8 かバイナリ フレームを全体としてまたはフラグメント化して送信できます。3 つ目のパラメーター (ここでは、true) は、この SendAsync の呼び出しがメッセージの終了を表しているかどうかを示します。そのため、このメソッドを繰り返し使用すれば、自動的にフラグメント化される長いメッセージを送信できます。最後に、CloseAsync メソッドを使用して WebSocket 接続の完全な終了を実行し、クローズ コントロール フレームを送信してクライアントが自身のクローズ フレームに確認応答を返すのを待機します。

クライアント側の新しい ClientWebSocket クラスは HttpWebRequest オブジェクトを内部で使用して、WebSocket サーバーに接続する機能を提供します。図 2 は、図 1 のサーバーに接続するのに使用できるクライアントの簡単な例を示しています。

図 2 ClientWebSocket を使用する WebSocket クライアント

static async Task Client()
{
  ClientWebSocket ws = new ClientWebSocket();
  var uri = new Uri("ws://localhost:8000/ws/");
  await ws.ConnectAsync(uri, CancellationToken.None);
  var buffer = new byte[1024];
  while (true)
  {
    var segment = new ArraySegment<byte>(buffer);
    var result =
      await ws.ReceiveAsync(segment, CancellationToken.None);
    if (result.MessageType == WebSocketMessageType.Close)
    {
      await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "OK",
        CancellationToken.None);
      return;
    }
    if (result.MessageType == WebSocketMessageType.Binary)
    {
      await ws.CloseAsync(WebSocketCloseStatus.InvalidMessageType,
        "I don't do binary", CancellationToken.None);
      return;
    }
    int count = result.Count;
    while (!result.EndOfMessage)
    {
      if (count >= buffer.Length)
      {
        await ws.CloseAsync(WebSocketCloseStatus.InvalidPayloadData,
          "That's too long", CancellationToken.None);
        return;
      }
      segment =
        new ArraySegment<byte>(buffer, count, buffer.Length - count);
      result = await ws.ReceiveAsync(segment, CancellationToken.None);
      count += result.Count;
    }
    var message = Encoding.UTF8.GetString(buffer, 0, count);
    Console.WriteLine("> " + message);
  }
}

ここでは、ConnectAsync メソッドを使用して接続を確立し、WebSocket ハンドシェイクを実行します。URL は新しい "ws" URI 手法を使用して、これを WebSocket エンドポイントとして識別しています。HTTP と同様、ws の既定のポートはポート 80 です。"wss" 手法も、セキュリティ保護された TLS 接続を表すように定義され、対応するポート 443 を使用します。次にクライアントは、ループ内で ReceiveAsync を呼び出し、サーバーが送信すると考えられる最大数のフレームを受信します。フレームを受信したら、最初にクローズ コントロール フレームを表しているかどうか確認します。この場合、クライアントは独自のクローズ フレームを送信して応答し、サーバーが迅速に接続を終了できるようにします。次にクライアントは、フレームにバイナリ データが含まれているかどうかを確認し、含まれている場合は、フレームの種類がサポートされていないことを示すエラーを返して、接続を終了します。最後に、フレーム データを読み取ります。フラグメント化されたメッセージに対応するため、while ループは最終フラグメントを受信するまで待機します。新しい ArraySegment 構造体を使用してバッファのオフセットを管理し、フラグメントが正しく再構成されるようにします。

WinRT クライアント

Windows ランタイムの WebSocket プロトコルのサポートは、もう少し限定的です。サポートされるのはクライアントのみで、フラグメントされた UTF-8 メッセージは読み取られる前にすべてバッファーに格納されている必要があります。この API でストリーミングできるのはバイナリ メッセージだけです。図 3 は、図 1 のサーバーに接続する際にも使用できる簡単なクライアントの例を示しています。

図 3 Windows ランタイムを使用する WebSocket クライアント

static async Task Client()
{
  MessageWebSocket ws = new MessageWebSocket();
  ws.Control.MessageType = SocketMessageType.Utf8;
  ws.MessageReceived += (sender, args) =>
  {
    var reader = args.GetDataReader();
    var message = reader.ReadString(reader.UnconsumedBufferLength);
    Debug.WriteLine(message);
  };
  ws.Closed += (sender, args) =>
  {
    ws.Dispose();
  };
  var uri = new Uri("ws://localhost:8000/ws/");
  await ws.ConnectAsync(uri);
}

この例も C# で記述していますが、大部分はイベント ハンドラーに依存しており、C# の非同期メソッドを使用できるのは MessageWebSocket オブジェクトを非同期に接続できるようにするためだけです。ただし、コードは少し不規則な面もありますが、非常にシンプルです。すべての (おそらくフラグメント化された) メッセージを受信し、読み取りの準備ができたら、MessageReceived イベント ハンドラーが呼び出されます。すべてのメッセージを受信しても、UTF-8 文字列である必要があり、ストリームに格納され、DataReader オブジェクトを使用してコンテンツを読み取り、文字列を返す必要があります。最後に、Closed イベント ハンドラーによって、サーバーがクローズ コントロール フレームを送信したことを伝えられますが、.NET ClientWebSocket クラスと同様、クローズ コントロールフレームをサーバーに送り返すことも必要です。ただし、MessageWebSocket クラスは、オブジェクト自体が破棄される直前にこのフレームを送信するだけです。これを C# で迅速に行うには、Dispose メソッドを呼び出す必要があります。

典型的な JavaScript クライアント

WebSocket プロトコルが最も影響を与える環境が JavaScript であることは疑いようがなく、API は驚くほど単純です。図 1 のサーバーに接続するために必要なのは、以下ですべてです。

var ws = new WebSocket("ws://localhost:8000/ws/");
ws.onmessage = function (args)
{
  var time = args.data;
  ...
};

Windows の他の API とは異なり、ブラウザーはクローズ コントロール フレームを受信したら自動的に WebSocket 接続を終了する役割を担います。もちろん、明示的に接続を終了するか、OnClose イベントを処理することもできますが、ハンドシェイクの終了を完了するのに追加操作は必要ありません。

C++ 用の WinHTTP クライアント

WinRT WebSocket クライアント API は、もちろんネイティブ C++ でも同様に使用できますが、もう少し制御できるものを探している場合は、WinHTTP がぴったりです。図 4 は、WinHTTP を使用して図 1 のサーバーに接続する簡単な例を示しています。この例は、わかりやすいように WinHTTP API を同期モードで使用していますが、非同期でも同様にうまく機能します。

図 4 WinHTTP を使用する WebSocket クライアント

auto s = WinHttpOpen( ... );
auto c = WinHttpConnect(s, L"localhost", 8000, 0);
auto r = WinHttpOpenRequest(c, nullptr, L"/ws/", ... );
WinHttpSetOption(r, WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, nullptr, 0);
WinHttpSendRequest(r, ... );
VERIFY(WinHttpReceiveResponse(r, nullptr));
DWORD status;
DWORD size = sizeof(DWORD);
WinHttpQueryHeaders(r,
  WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
  WINHTTP_HEADER_NAME_BY_INDEX,
  &status,
  &size,
  WINHTTP_NO_HEADER_INDEX);
ASSERT(HTTP_STATUS_SWITCH_PROTOCOLS == status);
auto ws = WinHttpWebSocketCompleteUpgrade(r, 0);
char buffer[1024];
DWORD count;
WINHTTP_WEB_SOCKET_BUFFER_TYPE type;
while (NO_ERROR ==
  WinHttpWebSocketReceive(ws, buffer, sizeof(buffer), &count, &type))
{
  if (WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE == type)
  {
    WinHttpWebSocketClose(
      ws, WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS, nullptr, 0);
    break;
  }
  if (WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE == type ||
    WINHTTP_WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE == type)
  {
    WinHttpWebSocketClose(
      ws, WINHTTP_WEB_SOCKET_INVALID_DATA_TYPE_CLOSE_STATUS, nullptr, 0);
    break;
  }
  std::string message(buffer, count);
  while (WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE == type)
  {
    WinHttpWebSocketReceive(ws, buffer, sizeof(buffer), &count, &type);
    message.append(buffer, count);
  }
  printf("> %s\n", message.c_str());
}

あらゆる WinHTTP クライアントがそうであるように、WinHTTP セッション、接続、および要求オブジェクトを作成する必要があります。これに関しては、新しいことは何もないので、一部の詳細は省きました。実際に要求を送信する前に、新しい WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET オプションを要求に設定し、WinHTTP が WebSocket ハンドシェイクを実行するように指示する必要があります。次に、WinHttpSendRequest 関数を使ってこの要求を送信する準備をします。次に、応答を待機するのに通常の WinHttpReceiveResponse 関数を使用し、この場合 WebSocket ハンドシェイクの結果を含めます。通常どおり、要求の結果を判断するため、サーバーから返された状態コードを読み取るという特定の目的で WinHttpQueryHeaders 関数を呼び出します。この時点で、WebSocket 接続が確立され、直接使用できるようになります。WinHTTP API は、自動的にフレーム化を処理し、この機能は、要求オブジェクトで WinHttpWebSocketCompleteUpgrade 関数を呼び出すことで取得される新しい WinHTTP WebSocket オブジェクトを使って公開されます。

これで、少なくとも概念上は、サーバーからのメッセージの受信が完了します。これは、図 2 の例とほとんど同じ方法です。WinHttpWebSocketReceive 関数は、次のデータ フレームが受信されるのを待機します。また、あらゆる種類の WebSocket メッセージのフラグメントを読み取り可能にし、図 4 の例ではこれがループで実行される方法を示しました。クローズ コントロール フレームを受信した場合、WinHttpWebSocketClose 関数を使用して対応するクローズ フレームをサーバーに送信します。バイナリ データ フレームを受信した場合、同様に接続を閉じます。閉じるのは WebSocket 接続だけであることに注意します。所有するすべての WinHTTP オブジェクトに対して実行する必要があるため、まだ WinHttpCloseHandle を呼び出して WinHTTP WebSocket オブジェクトを解放する必要があります。2011 年 7 月号のコラム「C++ と Windows API」(msdn.microsoft.com/magazine/hh288076、英語) で説明した例のように、ハンドル ラッパー クラスが役立ちます。

WebSocket プロトコルは、Web アプリケーション環境における大きな新発明で、比較的単純ですが、大規模なプロトコルの TCP/IP スイートへの歓迎すべき追加です。近い将来、WebSocket プロトコルが HTTP 自体と同じくらい普遍的になり、アプリケーションとさまざまな種類の接続型システムがより簡単かつ効率的に通信できるようになることは、疑う余地がありません。Windows 8 がその一端を担い、WebSocket クライアントとサーバーの両方を構築するための API の包括的なセットを提供しました。

Kenny Kerr は、ネイティブの Windows 開発に情熱を注いでいるソフトウェア設計者です。彼のブログ (kennykerr.ca、英語) をご覧ください。

この記事のレビューに協力してくれた技術スタッフの Piotr Kulaga と Henri-Charles Machalani に心より感謝いたします。