あなたにWebSocketは必要ないかも

(訳注:2015/8/4、いただいた翻訳フィードバックを元に記事を修正いたしました。)

本題に入る前に強調しておきます。WebSocketは優れた通信プロトコルです。実際私はこのRFC6455を、Fanoutのサービスで使っている(ZurlPushpinといったパーツで採用しています。Fanoutではまた、 Primus(異なるリアルタイムフレームワーク間での通信を可能とするラッパー)を利用し、 XMPP-FTWインターフェースを介したWebSocket通信をサポートしています。

しかしながら私はこれまで、多くの広く普及しているアプリケーションにかなりの時間を費やし、おかげでRESTやメッセージングパターンについては多少なりとも理解が深まってきた今、実はWebSocketを実装した典型的なWebアプリケーション(もしくはWebSocketライクな抽象化レイヤ)の大部分は、WebSocket以外の別の手段を使った方がより良い動きをするのでは、と感じているのです。

HTTPストリーミングとSever-Sent Events

ブラウザへのプッシュ送信を効率よく行う方法は、WebSocketだけではありません。XMLHttpRequestやEventSource/Server-Sent Events(SSE)の仕組みを使って不定長のHTTPレスポンスをコンシュームし続けることでも、最小限のハックで、今普及しているあらゆるブラウザに継続的にデータを送信し続けることが可能です。この方法は確かにサーバからブラウザの方向にのみデータを送信しますが、典型的なアプリケーションにとって解決が必要な課題はそれだけで、そしてこの課題はそんなに大きな問題ではありません。ブラウザ側からサーバにデータを送る時に問題が生じることはほとんどないからです。

アルノート・カゼミア―(Primus作者、講演「WebSuckets)は最近、EventSourceについて次のような考察を投稿しています。「(EventSourceが)あります。まさにクールなツールです。自動的に再接続できる機能があります。プロキシを通じて通信します。(中略)そしてストリーミングもできるのです。なぜ皆さんこんな便利なツールを使わないのですか?」全くその通りです。

HTTPロングポーリング

ロングポーリングにはかなり厳しい小言が寄せられています。ロングポーリングにはかなり厳しい小言が寄せられています。リアルタイムWebの世界においてロングポーリングは当初、ストリームをエミュレーションするのに必要なハックとして大抵利用されていました。かつてBOSHが開発されたころ、私はXMPPのコミュニティで活動していたのですが、もちろん私たちはBOSHも巨大なハックとみなしていました。当時私たちが求めていたものはブラウザで動くストリーミング機能だけだったので、ソリューションはまるでHTTPにマッシュされているようなものでした。もちろん、socket.ioSockJSのような別のアイデアも同様の状況でした。

ご存知の通り、ロングポーリングはもちろんハックとしてではなくても使えます。FriendFeedは直感的に使えるロングポーリングAPIを採用していますし、WebhookInboxでも同様です。LivefyreもこのAPIを使って、1億人以上のユーザへのサービス提供を実現しています。RealCrowdは最近、彼らの採用するRESTful Realtimeアプローチについて公表しました。何が重要かというと、この中でストリームをエミュレートするためにハックを使っているサービスは1つもないということです。クライアントがこれらのAPIにリクエストする際、クライアントは実はデータの同期を試みています。リアルタイムプッシュは結局データの同期を目的としてよく使われるものですから、こういったロングポーリングAPIの方がより正確に、クライアントが実際に達成しようとしていることを実現できる、と言えるでしょう。

ロングポーリングが批判される最も大きな理由は、クライアントがサーバを繰り返し呼び出さなければならない、という点です。とはいえ例えWebSocketを使っていてもほぼ確実に、ネットワークが落ちていないかどうか確かめるべく、ある程度定期的にピングは打ち続けるでしょう。ロングポーリングを用いて継続的にHTTP接続を提供し続けるということとWebSocketを使うということは、結局同じネットワークトラヒック量になるのです。

あなたはもしかして、ロングポーリングなんてIEの古くからのユーザをサポートし続けるだけの時代遅れの無意味なものだと思っていませんか?正反対ですよ。昔ながらのブラウザをサポートすることとは全く関係なく、ロングポーリングはその他多くの理由によって優れていると言えるのです。

メッセージングパターン

かなり長いこと私はZeroMQをむやみに信奉しすぎていたような気がします。双方向性ストリームを検討するよりもメッセージングパターンから考えた方がネットワークアプリケーションを簡単に設計できる、と感じていたのです。ではメッセージングパターンとは何のことでしょうか?最もよく挙げられる事例は、HTTPの基本的な動きを表すrequest-response型となります。さて、ブラウザからのAjaxリクエストを送る時ですが、あなたはTCPのことを気にする必要がない、ということに気付いていましたか?かつてそこに潜んでいた複雑な要素はどこかに行ってしまいました。実はリクエストに呼応して、新たなTCP接続が張られていたかもしれません。あるいは継続的な接続がまた設定されていたかもしれません。もしかしたら、リクエストないしはレスポンスが複数のIPパケットに入れ替わったのかもしれません。こうした事象をアプリケーションは一切気にすることなく、ただただリクエストを送ってレスポンスを受け取る、という動作だけに集中しています。ブラウザ側のSocketライブラリが、アプリケーションに文字通り”接続”を強いる傾向はありますが、これはもう時代遅れです。たいていの場合、接続はもうあまり重要な要素ではありません。

極言すれば、Webアプリケーション(もしくは同様のシンクライアント)はまさにrequest-response型/publish-subscribe型のメッセージングパターンさえあれば、必要なタスクを実行できるのです。そういう意味ではどんな双方向性ストリームレイヤでも、こうしたパターンを実装できる場合に限り有益です。request-response型は昔からのHTTP自体に既に実装されているので、publish-subscribe型の代替案のみ考える必要があります。

一般に連合化されたWebはこれらの2つのパターンが等しく実装されています。サーバ間での相互通信のほとんどは伝統的なrequest-response型HTTPによって発生し、同様にリアルタイムアップデートの際に使われるpublish-subscribe型によって、HTTPコールバック(旧名Webhooks)がしばしば発生するようになります。受信するためには、ブラウザ自体がパブリッシャに働きかける動的なエンティティとなりますが、それでもpublish-subscribe型が適用できるのです。

私たちが行う全てはHTTPベースでなければならない、ということを意図しているわけではありません。私はXMPPの標準化に長年注力していますから、こういった代替プロトコルを率先して広めてきている人物でもあるのです。ただ、ある特定のメッセージングパターンをHTTPにマッピングすることは、ディベロッパが想定していたより案外機能的だ、と最近気付いてしまったのです。

データ同期

私はどちらかと言えば、 Firebaseのアナント・ナーラーヤンに賛成です。彼は、ブラウザでリアルタイムメッセージングを使っているほとんどのディベロッパが懸命に 同期の問題を解決しようとしていると言っています。WebSocketは、ほとんどのアプリケーションで直接使う必要のないプリミティブであり、ディベロッパは、WebSocket を利用する可能性があるデータ同期関連のAPIを活用する方がずっといいというのが彼の主張です。

この点を考慮して、(コミュニティとしての)私たちは、なぜ躍起になってWebSocketをサポートしていないブラウザでストリームをエミュレートしようとするのでしょうか?大変な思いをしてHTTP上で何かをハックするくらいなら、手間を省いてpublish-subscribeやデータ同期などの実際に必要なインターフェースをエミュレートすればよいのです。その方が必然的に何か他のレイヤを作ってストリームをエミュレートするよりいいはずです。

私はこの考えを基に、Fanout PubSub Protocolを作成しました。これは、シンプルなpublish-subscribeプロトコルで、コネクション型双方向中間ストリーミングレイヤのないロングポーリングをベースにしています。よって、このプロトコルは、ストリームエミュレーションで動作する類似のpublish-subscribeプロトコル(例えばロングポーリング方式においてSockJS上で動作するMeteor DDP)に比べ、より軽く、より単純な構造になっています。また、ほとんど例外なく、ネイティブなWebSocket上で動作するどのプロトコルとも同じくらい高性能です。

確かに、アナントの説明したシステムでは、publish-subscribe型のメッセージングは依然としてプリミティブと捉えられていますが、それは構いません。このパターンをベースにデータ同期を行えば、エミュレートされたストリームの損失を考慮する必要がなく、効率もよくなります。

WebSocketが必要なケース

もちろん中には、WebSocketによって現状よりも大きなベネフィットがもたらされる事例もあります。

クライアントがサーバに高速送信しなければならない:理論上は、XMLHttpRequestを使って無限のデータストリームをアップロードすることもできます。しかし、ブラウザやサーバでこれに対する充実したサポートがあったら、私はきっと驚くでしょう。ここで留意すべきなのは、サーバからクライアントに高速送信されるデータが大量にある場合、WebSocketが絶対に必要とは言えないということです。なぜなら、高速送信はEventSourceでも可能だからです。

ステートフルなコネクションプロトコル:インバウンドデータとアウトバウンドデータを異なる伝送路で処理するのは、時に厄介です。何らかのスティッキーセッションがあって、クライアントからのデータ送信とクライアントへのデータ送信を常に同じサーバを介して行うための方法が必要なこともあるでしょう。この手法に頼ることなくWebがこれだけ長い間うまく続いているとしても、私はこの方法によって得られる効率性があると思います。

通信が自然に流れている:ある特別なプロトコルをブラウザに実装しようとしていて、そのプロトコルは双方向ストリームで動作するようになっているとしましょう。この一例としてはWebSocket上でXMPPを使うケースが挙げられます。この場合、WebSocketの代わりにSockJSのようなエミュレートされたストリームを使うのは妥当だと言えます。というのも、ストリームはXMPPにとって最も自然な伝送だからです。でも、XMPPへのRESTバインディングもいいアイデアだと思います。

まとめ

選択肢を知り、技術を理解しましょう。多くの人にとって、”リアルタイム”はWebSocketと同義で、他のものは全て時代遅れのレッテルを貼られています。私の見解では、WebSocketが絶対に必要なアプリケーションはほとんどないため、その使用に伴う複雑さも必要ありません。自分が解決しようとしている問題をメッセージングパターンの観点から考え、データ同期についても考慮してください。そうすれば、シンプルで、より適切なHTTPベースの手法が見つかるでしょう。


訳注: 記事修正

一部翻訳が適切ではな箇所がありましたので、下記の点を変更いたしました。
ご迷惑をお掛けし申し訳ございません。ご指摘いただいた皆様、ありがとうございました!

  • タイトルが適切でなかったため、「WebSocketはもう必要ないかも」より変更
  • ロングポーリング部分の訳が適切でなかったため、変更