WebSocketについて調べてみた。

実はけっこう前からWebSocketの詳しい仕組みについて気になってて、遂に一念発起して調べてみた。何かとても良さげっぽい。

そもそもWebSocketとは

Webにおいて双方向通信を低コストで行う為の仕組み。インタラクティブなWebアプリケーションではサーバから任意のタイミングでクライアントに情報の送信とかしたい事があって、例えばFacebookのチャットアプリみたいに多数のクライアントが一つのページにアクセスしてて誰かがメッセージを投稿するとそれをその他のユーザーに通知したい場合があって、そういった時に双方向通信の必要性が出てくる。

元々はWebにおいてはHTTPしか通信の選択肢が無くてHTTPのロングポーリング使って無理矢理双方向通信実現したりしてたんだけど、流石に無駄が多すぎるし辛いよねって事でWebSocketというプロトコルが作られた。

WebSocketにおいては、TCP上で低コストで双方向通信が実現出来る様になってる。もちろん新しいプロトコルだからブラウザもサーバーも対応してないと使えないんだけど、最近は対応が進んでるんじゃ無いかと思う。

WebSocketの通信の仕組み

WebSocketは次の手順で通信を行う。

  1. HTTP(厳密にはそれをwebsocketにupgradeしたもの)でクライアントとサーバー間で情報をやり取りしてコネクション確立
  2. 確立されたコネクション上で、低コストな双方向通信

順番に解説してみる。ちなみに、この辺の手順は以下のページに詳しい。

参考: Windows 8 と WebSocket プロトコル
参考: RFC6455 — The WebSocket Protocol 日本語訳

Windows8とWebSocketプロトコル」って方は前半でWebSocketについてざっくり分かり易くまとめてくれてて良い。読みやすさは大事。

RFC日本語訳の方は最新でない可能性があるとはいえガチの仕様を載せてるので、読んどいて損は無い気がした。

1. コネクション確立

WebSocketは、クライアントからサーバーにリクエストを送ってコネクションを確立するところから始まる。このリクエストやレスポンスがHTTPに乗っ取った形式になってるのが、WebSocketの大きな特徴。

まず、クライアント(ブラウザと思ってもらえば大体OK)から以下の様なリクエストを送る。

GET /resource HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: E4WSEcseoWr4csPLS2QJHA==

これはexample.comというhostの/resourceという相対URIに対するリクエストになってる。普段HTTPだとhttp://example.com/resourceとか叩いてるのと同じイメージ。

1行目にRequest-Lineと呼ばれるメソッド名(GET)、リクエストURI(/resource)、プロトコルバージョン(HTTP/1.1)の並びが来てて、完全にHTTPとして正しいリクエストになっているのが印象的である。(HTTPリクエストの条件は、Request-Lineが存在する事とリクエストURIが相対URIであればHostヘッダーにホスト名を記載する事のみ。上記のリクエストはどちらの条件も満たしている為、正しいHTTPリクエストと言える。)

特徴的なのはUpgradeヘッダやConnectionヘッダが存在する事で、この二つでHTTPからWebSocketへのプロトコルのアップグレードを表現している。アップグレードって言われると良く分からないけど、ただのHTTPでないんだって事をサーバーに伝えたいんだなーくらいに思っておけばOK。

Sec-WebSocket-Versionというヘッダは、接続のプロトコルバージョンを指示するためにクライアントからサーバへ送信される。サーバーは、送られてきたバージョンに対応出来なければコネクションを切断する。現在のWebSocketの最新バージョンは13だから、13を指定しなければならないらしい。

Sec-Websocket-Keyヘッダは、特定のクライアントとのコネクションの確立を立証する為に使われる。サーバはSec-Websocket-Keyヘッダに指定された値を元に新しく値を生成してSec-WebSocket-Acceptヘッダにその値を指定してレスポンスを返すので、クライアントとしては自分のSec-Websocket-Keyの値が使われているかどうかが確認出来る様になっている。その為、自分のリクエストに対するレスポンスである事が保証出来て嬉しかったりする。

ここまでがクライアントからのリクエストで、次はサーバからのレスポンスなんだけど、これも例の如く完全にHTTPの形式になっている。

HTTP/1.1 101 OK
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: 7eQChgCtQMnVILefJAO6dK5JwPc=

1行目がStatus-Lineと呼ばれるやつで、プロトコルバージョン(HTTP/1.1)、ステータスコード(101)、テキストフレーズ(OK)の並びから構成されている。Status-Lineが存在する時点で、HTTPレスポンスの条件は満たしている。

UpgradeConnectionはリクエストのとこで説明したのと同じ役割で、Sec-WebSocket-AcceptSec-WebSocket-Keyのとこで説明した通り。

レスポンスはこれだけで、めちゃくちゃシンプルになってる。このレスポンスを受け取った時点でWebSocketのコネクションが確立された事になって、双方向通信が可能になる。

ちなみに、この辺のコネクション確立の為の一連の流れはWebSocket opening ハンドシェイクと呼ばれる。(ハンドシェイクという呼び名はコネクション確立のシチュエーションでしばしば登場する様で、例えばTCPのコネクション確立の手順は3ウェイ・ハンドシェイクと呼ばれたりする。)

2. 双方向通信の実現

ハンドシェイクが終わると、TCP上で双方向通信が可能になる。ハンドシェイク時はHTTPを使うからその前にTCPのコネクションを確立してる訳だけど、ハンドシェイク後はその確立されたTCPコネクション上で、双方向にデータをやり取りする事になる。

WebSocketにおいては、 フレーム と呼ばれる単位でデータが送信される。フレームは、下図の様にビットの並びから構成されていて、小さなデータサイズで必要な情報を送る事が出来る。Payload Dataという部分にWebアプリケーションが送りたいデータ(メッセージアプリケーションのメッセージとか)を格納するんだけど、Payload Data意外の情報は最小2byte, 最大でも14byteに収まる様になってて、HTTPに比べるとかなり低コストになっている。(HTTPだとリクエストヘッダで数百byteになったりする。)

この辺のデータサイズの話は、このスライドの45~54ページの説明が分かり易かった。

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

それぞれのビット(FINとかRSV1とか)の意味合いはRFC6455を見てもらえば良いと思う。

一つだけ説明しておくと、opcodeと呼ばれる4ビットの値(%x0から%xFまでの値)は、フレームの種類(解釈の仕方)を定義する。フレームには現状6種類があって、

  • %x0 は 継続フレーム
  • %x1 は テキストフレーム
  • %x2 は バイナリフレーム
  • %x8 は Closeフレーム(接続切断)
  • %x9 は ping
  • %xA は pong

を表す。%x1,%x2がDataフレーム(非制御フレーム)、%x8,%x9,%xAが制御フレームと呼ばれる。DataフレームがWebアプリケーションがデータを送信する為に使うフレームで、テキストデータを送りたいかBinaryデータを送りたいかで%x1%x2を使い分ける事になる。制御フレームはWebSocketの通信を制御する為に使うヤツで、WebSocketの通信の切断にCloseが使われたり、通信が生きている事を確認する為にPing, Pongが使われたりする。

ちなみに、opcodeには現状は使われてないけど将来の拡張を考えて予約されてる値もあって、

  • %x3-7 は追加の非制御フレーム用に予約済み
  • %xB-F は追加の制御フレーム用に予約済み

となっている。

以下、RFC6455より引用

データフレームは、クライアントとサーバのいずれからでも, opening ハンドシェイクの完了後から その端点が Close フレームを送信し終える前までのいつでも,伝送されてよい。

WebSocketの利用に適したサーバについて

WebSocketはクライアントとサーバーの間でTCPコネクションを張りっぱなしにして通信を行う為、Blocking I/Oを持つWebサーバとは相性が悪い。Blocking I/Oを持つサーバープロセスは一度に1つのクライアントとしかコネクションを張れない為、WebSocketを使いたければクライアントの数だけサーバープロセスが必要になってしまう。

そういった状況を避ける為に、WebSocketを利用する場合には通常Non Blocking I/Oを持つサーバーが使われる。WebSocketの文脈で一番有名なのはNode.jsだと思うが、他にも例えばrubyであればThinやRainbows!などのappサーバーが利用出来る。WebSocketに対応する為のライブラリとしては、socket.ioやwebsocket-railsなんかが有名だと思う。

reverse proxyとしてよく利用されるnginxもWebSocketに対応したようで(参考: NGINX as a WebSockets Proxy)、WebSocketを利用する為の環境はどんどん整ってきてるんじゃ無いだろうか。

まとめ

WebSocketみたいな、需要ドリブンで新たな仕様が出来てWeb自体が改善されるのは、良い方向性の動きだと思う。より高速なWebとしてHTTP 2.0も提案されてたりしていて、Webもどんどん進化してくんだろう。

参考: HTTP/2 入門

以下、どうでもいい話

「Webを支える技術」って本でHTTPの仕組みが詳しく説明されていて、その中のステータスコードについての説明の部分で「101はSwitching Protocolsを表しててHTTP/1.1からプロトコルをアップグレードする時に使うけど、現時点だとぶっちゃけ使いどころ無い」みたいに書かれててとても悲しい気持ちになってたのが、WebSocketが登場した事によってちゃんと使いどころが見つかったみたいで良かったねって気分になった。