Wireless・のおと

WebSocket のはなし

ブログ
規格 技術解説

前回はブローカー型のプッシュ型メッセージ配送プロトコル MQTT を紹介しました。MQTT についてネットで検索すると、MQTT とペアで使われる技術要素として「WebSocket」というキーワードがよく引っかかってきます。今回はこの WebSocket について簡単に紹介します。

WebSocket とは

WebSocket は HTTP 上の相乗りプロトコルで、確立済みの HTTP/HTTPS 回線上で任意長・任意フォーマットデータの双方向通信を実現するものです。RFC6445 として IETF で標準化されています。
以前に「HTTP のはなし」で 「HTTP/2  はTCP の上で動く HTTP の上に TCP のようなものを実装したもの」と紹介しましたが、立ち位置的には WebSocket も非常によく似ています。ただし WebSocket は HTTP/2 よりも簡易な実装になっていて、例えば HTTP/2 が1本の HTTP セッション上で複数の仮想回線を扱えるのに対し、WebSocket の仮想回線は HTTP セッションと 1:1 の関係になります。同じ問題を同じ方法論で解決するのに違う方向からアプローチがあった結果、似て非なる規格が並立してしまった格好(しかも提唱元は同じ Google!)ですが、コンピュータの世界では「よくあること」です。

WebSocket は JavaScript を筆頭とするスクリプト系言語から使うために開発されています。JavaScript API では WebSocket オブジェクトが用意されており、これに対して open, send, message, close といった読み書き操作を行うことで、あたかも素の TCP 回線で通信しているようなプログラミングを行うことができます。
「素の TCP のように見える」というのはソースコード上の話であって、WebSocket はあくまで HTTP/HTTPS 上に相乗りして動くプロトコルであり、JavaScript に socket API を与えるものではないことに注意してください。WebSocket を socket の代わりに使うことでポートスキャンやプロキシ偽装などのハッキングに悪用されないことは設計上の一大懸念となっており、これを防ぐために幾つか奇妙とも思える仕様が導入されています(※註)。

※註:「Sec-WebSocket-Key」や「ペイロードマスク」などですが、詳細には触れません。
 

何でそんなものが必要になったのか

「HTTP のはなし」で紹介したように、HTTP はもともとネット上での文書共有のために開発されました。HTTP の基本動作原理はリクエスト(クライアント/ブラウザ)~レスポンス(サーバ)の往復形式です。HTTP/1.0 以降では POST コマンドを用いてクライアント→サーバ方向にデータを送ることができ、HTTP/1.1 以降では Chunked Encoding を用いてデータ長不定のまま通信を始めることもできますが、サーバ→クライアント方向の通信はあくまでリクエストに対するレスポンスとしてしか送信できず、リクエストが完結するまでレスポンスは返せません。
つまり、HTTP では任意タイミングで任意データをサーバ→クライアント方向に流すことが原則としてできません。このため AJAX では細切れのリクエストを 10 秒間隔などでサーバに送ってレスポンスを促すポーリングという実装が使われたりしましたが、どうしても反応時間に上限がありますし、反応時間を早めようとポーリング間隔を縮めれば通信効率は悪化する相克があります。

何だか大昔の電話接続 TSS システムでもそんな事をやっていたような記憶があり、いったい人類は何をやっているんだろうと思わされますが、「インターネット」の使われ方が「WEB サーフィン」からメッセージングやチャットなど双方向リアルタイムに発展するにつれ HTTP では無理が目立つようになり、それを手っ取り早く(※註)解決するために開発されたものが WebSocket、もう少し本腰を入れて解決するために開発されたものが HTTP/2 といった位置づけです。

※註:もっとも「手っとり早い」筈だった WebSocket は RFC ドラフトで叩かれまくって仕様が二転三転し、数次にわたる改訂途中のドラフトに基づいた実装が世に出てしまったので、それら過渡ドラフトバージョンまで含めた仕様を全て網羅するのは大変になってしまっています。

何で素のソケットではいけないのか

これも「HTTP のはなし」で触れましたが、今の「インターネット」が「WEB さえ見れればいい」という前提で発展普及してしまったため、プロキシや NAT やファイヤウォールといった壁だらけになっていることが最大の理由だと思います。できてしまった壁を壊す(世界中のインターネット機材を一斉に IPv6 対応に入れ換える!)ことや、壁に新たな通用口を設ける(世界中のインターネット機材に HTTP 以外の新ポートを開けさせる)ことは現実的とは思えません。しかし「NAT やプロキシを抜けられ」「セキュリティもサポートしている」HTTP というベースがあるのだから、それに相乗りするのが現実的ということになります。
1995 年に制定された IPv6 は一対多の通信を実現するマルチキャスト、状態非依存でパケットを追跡できるフローラベル、セキュリティを実現する IPsec(AH/ESP) の実装義務付けなどまさに「こんな使われ方」をすることを想定した仕様が盛り込まれていたのですが、現実の世の中はそれをスルーして「HTTP/TLS の上に TCP のようなものを乗せる」という、技術的には奇形ともいえる方法論を選択することになってしまいました。これもやっぱり「よくあること」です(※註)。

※註:どれくらい「よくあること」かというと、例えば連続最大 76 文字の英数しか扱えないシリアル回線上でメールを通すために開発された UUCP がそのまま SMTP としてインターネット上に移植され、SMTP 上で任意バイナリデータを扱うために英数 76 文字幅に変換する BASE64 エンコードが開発され、更にセキュリティ懸念で SMTP over TLS が実装されて、「HTML を 76 文字幅英数の BASE64 に変換し暗号化したうえで TCP で流す」なんて無駄なことが日常的に行われたりしています。

WebSocket の動作

WebSocket についても日本語で豊富な情報を得ることができるので、このブログでは動作シーケンスやらフォーマットの詳細には踏み込みません。一番基本的な動作原理について述べるにとどめます。
WebSocket の初期化はクライアントからのリクエストとサーバからのレスポンス往復によって始まり、この段階での通信フォーマットは HTTP そのものです。「純粋な」HTTP と異なるのは Connection:Upgrade というヘッダが付く(※註)ことで、これによってリクエスト~レスポンス完了後にも TCP/TLS のコネクションは維持され、ただしその上を HTTP ならぬ WebSocket プロトコルが流れること(これを示すため Upgrade:webSocket というヘッダも付けられる)が合意されます。

※註:通常の HTTP なら Connection:Close か Connection:Keep-Alive のどちらか。

websocket01-large.jpg

WebSocket のセッション開設

初期化完了後はコネクション上を右に左にデータが流れるだけですが、素の TCP ソケットと違って「ただのバイト列(Byte Stream)」ではなく、簡単なヘッダが付いてデータ単位長が示されます。これは TLS におけるメッセージ/レコードと少し似ています。WebSocket が TLS 上で動作する場合、WebSocket ヘッダを TLS レコードが包みそれに TCP ヘッダが付いて IP パケットとして飛んでゆくわけで、何か二度手間というか遠回りなことをしているなぁと感じますが、WebSocket じたい屋上屋根を重ねたような経緯で開発されたものですから仕方ないです。

websocket-hdr.jpg

WebSocket のヘッダ

WebSocket ヘッダのペイロード長は少々変わったエンコードになっており、0~125 バイトまでは1バイトで表現され、Palyload len が 126 の場合は 16bit、127 の場合は 32bit の拡張レングスフィールドが付加されるルールになっています。先日の MQTT のメッセージレングス規則とは似て非なる仕様ですが、どちらも悪名高い OSI ASN.1 BER 規則の可変長レングスエンコーディングに似ていないこともありません。

length-websocket.jpg

WebSocket の Length エンコード規則
先頭1バイトが125未満の場合はLengthそのもの
126の場合は後続16bitのLengthフィールド、
127の場合は後続32bitのLengthフィールドが付加される

バイト順序はBig-Endian

length-mqtt.jpg

MQTT のメッセージ長エンコード規則
7bit毎に格納され、bit7は後続フィールドの有無を示す

バイト順序はLittle-Endian

length-asn1.jpg

ASN.1 の Length エンコード規則
先頭1バイトのbit7が0の場合はbit0-6がLengthそのもの
先頭1バイトbit7が1の場合はbit0-6が後続するLengthフィールドの長さを示す
バイト順序はBig-Endian

OSI で多用された可変長ヘッダ仕様は IETF において極めて悪評で「伝送路効率偏重」「実装効率無視」「電話屋の呪い」などと槍玉に上げられ、IPv6 では偏執的なくらい 64bit 境界を徹底(※註)した固定長ヘッダ仕様が採用されたのに、それから 20 年も経つと再び「やっぱり可変長エンコードのほうが効率的」と評価されるなんて「人類はいったい何をやっているんだ」という気になりますが、これも「よくあること」です。技術の方法論には絶対善も絶対悪もなく、それが使用される環境との兼ね合いによって評価されるもので、そして環境は常に変化し続けるものだからです。

しかし似て非なるLengthエンコードが3種類もあるとか、いい加減にして欲しいとは思います。

※註:どのくらい「偏執的」かというと、MAC アドレスを格納する Source/Target Link-layer Address Option フィールドは1バイトのタイプ(1または2)、1バイトのヘッダ長の後に MAC アドレスが付き、MACアドレス長が48bit =6バイトのときヘッダ全長がちょうど64bit=8バイトに収まるようになっています。しかしピッチリ詰め過ぎて「MAC アドレスのタイプ/長さ」を識別するフィールドが無く、例えばIEEE802.15.4などで使われている64bitのEUI-64アドレスを送ろうとすると2バイトはみ出し64bit単位に切り上げてヘッダ長は2x64bit=16バイトになり、しかもアドレス長/パディング長はどこにも示されないので、プロトコルの枠外(リンク層のドライバディスクリプタなど)からアドレス長を参照するというあまり「美しくない」仕様になってしまいます。64bit 境界に「綺麗に」収めようとし過ぎた無理がある仕様だと私は思っています。

まとめ

WebSocket は必要に迫られて開発された技術ですが、何度も述べたように「屋上屋根を重ねたような代物」「技術的には奇形」であり、こんなものが必要とされること自体が IPv6 の現状を如実に示していると思います。そういえば前回の MQTT も、IETF の「あるべき次世代インターネット」ならばマルチキャストによって実現されるべき機能であり、1000 セッションのサブスクライバーに対し同じメッセージを 1000 回反復送信するなんて「無駄なこと」はやらなくて済む筈だったんですよね。
IPv6 が失敗だとか無駄だとか言うつもりはなく、アドレス空間の拡大は必要とされていることで、いずれ緩やかに IPv4 を置き換えてゆくでしょう。しかしマルチキャストやエニーキャストやフローラベルや IPsec といった、次世代インターネット標準制定という一大プロジェクトに挑んだ IETF の若きエンジニアたちが託した「未来に撒いた種」は、結局芽を吹かずじまいでした。
しかし、それも今から 10 年 20 年後にはどうなっているかわかりません。「まだブローカー配送なんてやってるの?マルチキャストを使うのが常識じゃん!」とか「まだ TLS 使ってるシステムなんて稼動してたの?IPsec 使うのが常識じゃん!」と言っているかも知れません。技術の最適解は環境によって変化し続けるものですから。

関連リンク

関連記事

製品のご購入・サービスカスタマイズ・資料請求など
お気軽にお問い合わせください