WebSocket の基本

ここでは HTML5 で導入された WebSocket について説明します。

そもそも WebSocket はなぜ導入されたのでしょうか?

それは任意のタイミングでサーバーからクライアントに対して、データを送る (プッシュする) 手段を提供するためです。

従来の Web では HTTP プロトコルを使用しています。HTTP プロトコルは GET や POST などのリクエストをクライアントからサーバーに送り、 サーバーはそれに応答してクライアントにデータを送信します。

サーバーからデータを取得するために、クライアントからリクエストを投げる必要があります。

このため、ウェブ上でチャットアプリケーション等を実装する場合は不便です。なぜなら、チャットアプリでは通常、 あるチャット参加者の発言は、それが発言されたタイミングで他のチャット参加者に対して表示したいはずですが、 ある人がいつ発言するかわからないのに、そのデータを取得するためにサーバーにリクエストを送信しないといけないからです。

このため、ウェブ上でリアルタイムのデータ取得は難しかったのです。

この問題を回避するために、いろいろな手法が考案されて来ました。

例えば、サーバーからクライアントへ任意のタイミングでデータをプッシュしたい場合には、 これまで Ajax のロングポーリング等の方法があります。

この方法ではバックエンドであらかじめリクエストを送信しておきます。サーバー側ではそれへの応答を保留しておき、 サーバーが好きなタイミングでクライアントへ応答します。

こうした手法を広義で Comet と言います。

WebSocket は Comet で実現しようとしていた全二重化通信を実現する方法です。

WebSocket を構成する要素

WebSocket を実現するには、クライアント (ブラウザ) とサーバー側の両方のサポートが必要です。

クライアント側の WebSocket

まずブラウザ側では HTML5 イニシアチブの一環として WebSocket オブジェクトが導入されました。 2015年現在、主要ブラウザの最新版では WebSocket は実装されています。

WebSocket オブジェクトを生成し利用する典型的なコードは次のようになります。

output = $('#output');
var ws = new WebSocket("ws://192.168.1.10:8080/chat");

ws.onopen = function(e){
	log("Connected");
	sendMessage("Hello, world!");
}

ws.onclose = function(e){
	log("Disconnected");
}

ws.onerror = function(e){
	log("Error ");
}

ws.onmessage = function(e){
	log("Message received: " + e.data);
}

function sendMessage(msg){
	ws.send(msg);
	log("Message sent");
}

function log(s){
	var div = document.createElstrongent("div");
	div.textContent = s;
	output.append(div);
}

このコードのように WebSocket オブジェクトを new で作成して、そのメソッドを呼びます。

onopen, onmessage, onerror, onclose にイベントハンドラをセットすると、それぞれのイベント発生時にそのハンドラが呼び出されます。それぞれのイベントの意味は 名前から容易に推測可能なように、接続がオープンした時 (onopen)、メッセージ取得時 (onmessage)、エラー発生時 (onerror)、接続クローズ時 (onclose) です。

WebSocket のインターフェイスについては、W3C The WebSocket API に記載があります。

WebSocket の作成時に URI を渡していますが、ここで ws: というのは WebSocket プロトコルで導入されたスキーマで、websocket 接続であることを示しています。 あとで説明しますが、WebSocket プロトコルは HTTP の拡張として実装されているので、ws: を指定したときのデフォルトのポート番号は 80番です。 セキュアチャネルを指定する場合は wss: で、ポートは 443番です。これは http に対する https と同様です。

サーバー側の WebSocket

WebSocket を利用するためには、サーバー側では WebSocket プロトコルをサポートしなければなりません。

WebSocketプロトコルは RFC6455で定義されています。

詳細は RFC をみていただくことにして、ここではざっくり説明すると、WebSocketプロトコルではクライアントから次のようなリクエストを送ります。

   GET /chat HTTP/1.1
   Host: server.example.com
   Upgrade: websocket
   Connection: Upgrade
   Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
   Origin: http://example.com
   Sec-WebSocket-Protocol: chat, superchat
   Sec-WebSocket-Version: 13

Connection が Upgrade となっており、Upgrade ヘッダーは websocket を指定。さらにいくつか WebSocket という文字を含むヘッダーが追加されています。

これを受け取ったサーバー側では、ステータスコード 101 "Switching Protocols" を返し、次のように応答します。

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

これをWebSocketのハンドシェイクとします。サーバーはこのハンドシェイクを行った接続を確立したままにして、 その接続上で WebSocket フレームをクライアントと交換します。

WebSocketフレームは非常にシンプルなフォーマットになっています。

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

先頭の16ビットにフラグ (FIN, MASK)、フレームの種類 (opcode)、及びフレームのペイロード長が記載されます。 ペイロードの長さが 125以下で MASK フラグが 立っている場合は、それに続いて 4バイトでマスキングキー (Masking-key) が続きます。ペイロードはその後に続きます。

MASK ビットはクライアントからサーバー方向では 1 になり、サーバーからクライアントへのフレームではクリアされます。 したがって、サーバーからクライアントでのヘッダーにはマスキングキーも入らないので、ヘッダーは最小で 2 バイトです。

フレームの種類はヘッダーの opcode に記載されます。文字データの時は 1, バイナリデータは 2 です。 (opcode が 0 はフレームの分割 (フラグメンテーション) に使用します) これらは特にデータフレームといいます。

8 は接続のクローズ、9 は Ping フレーム、10 は Pong フレームで、これらは制御フレーム (Control frame) と言われます。

これ以上の説明はまた別の機会にしておきますが、基本的には小さなデータを送るなら通常フラグメンテーションもありませんし、 拡張ペイロード長のフィールドも使いません。したがって、データフレームのヘッダーは 6 バイト (サーバーからクライアントでは 2 バイト) で構成されており、この単純なフレームが WebSocket 接続上を行き来します。

以上、ざっと WebSocket プロトコルについて説明しましたが、Windows でも IIS8.0 からは WebSocket プロトコルをサポートしますので、 その上で ASP.NET であれば SignalR などのライブラリを利用して、WebSocket のアプリケーションを実装できます。

そうしたライブラリについてはまた別の機会に書きたいと思います。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Web/DB プログラミング徹底解説