「Socket.IO は必要か?」または「WebSocket は通るのか?」問題について 2016 年版

Intro

「Socket.IO 使ったほうがいいですか?」 という主旨の質問をもらった。

これは、 WebSocket が繋がらない環境に向けて、フォールバック機能を有する Socket.IO にしておいた方が良いのかという意味である。

WebSocket が出てきた当初と比べて、 Web を取り巻く状況は変わったが、変わってないところもある。

念のためと Socket.IO を使うのもよいが、「本当に必要なのか」を問うのは重要である。

Rails も ActionCable で WebSocket に対応し、ユーザも増えるかもしれないことも踏まえ、

ここで、もう一度現状について、把握している範囲で解説しておく。

“繋がらない” とは

最初に、なぜ 繋がらない ことがあるのかを、きちんと把握したい。

まず WebSocket の有史全体をみれば、繋がらないとして語られていた現象は、大きく三つ挙げられる。

  • ブラウザが対応してないため、 JS レベルで動作しない
  • HTTP の Connection: Upgrade ヘッダをみたら、接続を落とすミドルボックスがある
  • 長時間接続されている TCP コネクションがあると、接続を落とすミドルボックスがある

もちろん、エッジケースをみればキリが無いが、大きくはこの三つを抑えたい。

以下、順に問題の詳細と、解決方法を解説していく。

ブラウザ対応

問題の詳細

WebSocket は標準仕様であるが、ブラウザが実装しているものを指す場合、これは大きく二つの仕様からなる。

  • JS の API (WebSocket オブジェクトとそのメソッド) を定義した W3C の仕様
  • ネットワークプロトコルを定義した IETF の仕様

前者は、仕様としては比較的初期から固まっており、後のバイナリ対応など細かい変更はあれど、簡単なテキストチャットを作るレベルでは、早い段階から落ちついていた。

つまり、これはブラウザが実装するかしないかという、いつもの問題とほぼ等しい。

次に後者のプロトコルだが、 WebSocket という技術が注目を集め始めたのは、このプロトコル自体が RFC になるよりも前だった。

最初は Hixie というエンジニアが書いたドラフトで始まり、その後 HyBi という専門の Working Group に移され、そのしばらく後に RFC になる。

ところが、 Hixie 時代からもブラウザやサーバはこの機能を実装し始めていたため、ドラフトのバージョンが上がりプロトコルフォーマットが変更されるたびに、それに追従していた時期がある。

この時期は、例えばあるブラウザはまだ HyBi-07 だが、サーバは HyBi-14 である、といったように、同じ WebSocket を話しているつもりが動かない場合があった。

対応

Socket.IO は、ブラウザとサーバ両方のライブラリを有していたわけだが、この両方の問題に対応し以下を提供していた。

  • WebSocket の実装が無いブラウザ向けの Polyfill
  • WebSocket のどのバージョンのドラフトでパケットを送ってきても、対応できるサーバ

これが、 Socket.IO が行っていた「互換性担保」の部分の実態である。

2016 年現在、 IE11 以降のモダンブラウザは RFC に対応したプロトコルをしゃべる WebSocket API を実装済みであるため、この部分の心配は IE11 に依存すると言える。

WebSocket | Can I use

つまり RFC の WebSocket を実装したサーバと、モダンブラウザにある WebSocket API を直接使った実装で、十分 Interoperability が担保されているわけである。

補足

ほとんど無いと思うが、仮に「いまだに RFC 以前のプロトコルで話しかけてくるクライアントがいる」場合は、基本は落とせばいいだろう。

ただし、それらを等しくサポートする、非常に苦しい選択を迫られている場合は、 Socket.IO の中でも使われている WS というモジュールが、古いプロコトルをカバーする実装になっている。

Socket.IO もこれを中で使っているため、もちろん Socket.IO を使っても良いが、その場合は通信の内容(つまり WebSocket の上で流すペイロード)にも、特定のフォーマット(Socket.IO プロトコルと呼ばれる)をサポートする必要があるので、 WS 単位ならそれがいらなくなる。

ヘッダで落とすミドルボックス

問題の詳細

WebSocket のネゴシエーションは、 HTTP リクエスト形式のパケットで始まる。

このリクエストが Connection: Upgrade というヘッダを持っていることで、以降を WebSocket で通信する合意をとるという仕組みだ。

ただし、 Proxy や LoadBalancer や Firewall といったネットワーク経路上に挟まっている、俗にいうミドルボックス(intermedialies ともいう)の中には、こうしたヘッダの存在を許可しないものがある。

理由は様々だが、多くの場合はその後の通信をサポートしてない、未知のヘッダであるなどの理由から、ヘッダがあった時点でコネクションを切断したりする。

他にも、 Personal Firewall (要するにウィルス対策ソフト)の中に、こうしたヘッダを落とす製品があることも確認されていた。

この情報は、かつて Socket.IO の Wiki にまとめられていたが、現在は消えてしまったようだ。

消されたのは、内容が古いからだろうが、自分が昔翻訳した方が残っていたので参考までに貼っておく

解決方法

実はこの問題の解決は意外と簡単で、暗号化してしまえばいい。

つまり ws:// ではなく wss:// にすることで、 TLS で暗号化する。 End-To-End 暗号化であれば、ミドルボックスはそもそも HTTP ヘッダが見えないため、それを元に落とすことができない。

そうでなかったとしても、 HTTPS 化が推奨されている現状、新規で作るなら wss:// にしない積極的な理由はほぼ無いだろう。サイトが http でも wss を使うことはできるため、導入の負荷はサイト全体の TLS 化と比べても低いと思われる。

補足

TLS を一旦解くようなミドルボックスの存在は否定できない。

ただし、多くのクライアントに対してこれを行う場合は、相当なリソースを必要とするため、セキュリティポリシーが非常に高い環境で実施されていると考えられる。そうした状況では、通常強固なフィルタリングなどが行われているため、そもそも WebSocket 以前にまともに見れるコンテンツは限られている場合が多い。

こうしたエッジケースにあるクライアントも、本当にサポートすべきターゲットなのかによる。

接続を切るミドルボックス

WebSocket 対応していなかった時期の Nginx や ALB 以前の ELB なども含まれるが、それらは対応したものを選び設定する以外にない。

ここで特筆すべき点は、パーソナルファイアウォールと呼ばれる類いのものである。

マルウェアなどに感染した場合、怪しい通信を外部と行う場合がある。ウィルス対策ソフトの中にはそれらを検出する機能を備えたものもある。

基本的に HTTP は通信毎に TCP を切るため、長時間の TCP 接続は発生しないが、 WebSocket は基本的に貼りっぱなしにすることを目的としている。

HTTP っぽいリクエストで始まった貼りっぱなしの TCP 接続が、ウィルス対策ソフトによって「怪しい接続」であるとみなされ、切られてしまうのは、言ってみればウィルス対策ソフトがちゃんと仕事したというだけである。

しかし、それではサービスが成り立たないので、褒めてもいられない。

近年のウィルス対策ソフトが、 WebSocket に対してどう挙動するかはデータが無いのでわからないが、少なくとも息の長い TCP 接続が切られる問題は TLS にしても解決しない。

つまり、この問題は本質的には避けられないのである。

解決方法

この問題の特徴は、すぐに切れるわけでは無いというところである。

どのくらいで切れるかは、ウィルス対策ソフトが長い接続を「怪しい」と判断する閾値によるが、あまりにも短いとちょっと遅いサーバを HTTP で叩いてる間にも切れてしまう。

何十秒かに一回切れてしまうのが問題なだけなら、単純に再接続すれば解決する。

WebSocket は PING 機能を持っているため、切断したら onclose イベントで判明するはずである。

そこでもう一度コネクションを貼り直す方法が考えられる。

ws.addEventListener('close', () => {
  if (意図しない切断) {
    return reconnect();
  }
});

コネクションに状態を紐付ける場合、サーバでも再接続時に処理を継続できるように(切断中の情報を保持しておくなど)実装が必要なのは言うまでもない。

補足

切断 - 再接続のループが、なんらかの原因により非常に速いサイクルで発生していしまう可能性もある。

おそらくこちらが想定していない理由での、短時間の切断が発生しており、これはどんなにリトライしても解決しないどころか、ブラウザの負荷が上がるだけである。

理想的には、リトライの回数や、発生時間を監視するなどの実装があったほうがよくなる。

また、同等のことが onerror で必要になる場合は、両方に対応する。

生 WebSocket を使うなら

ActionCable の登場もあって、また生 WebSocket を使う人も増えるかもしれない。

(Rails のことだから ActionCable 上にプラグインを当てて XHR Polling を入れる人もすぐ出るだろうが)

生の WebSocket を使う場合は、ここまでをまとめて以下を行うことを勧める。

  • 暗号化した wss:// を使う
  • onclose での再接続を検討する
  • 自分のサービスのクライアントで、どの程度「繋がらない」人がいるか計測する
  • 繋がらない理由が分かるように、上記を参考に監視を設定する

それでも繋がらないことがある

インターネットは、簡単に想像できるほど単純な作りにはなっていない。

ここまで、筆者が持っている知識から挙げた代表的なケース以外にも、エッジケースをあげればキリがない。

2016 年現在、 WebSocket がどのくらい通るのか通らないなら原因は何か については、結局のところ誰かが大規模なデプロイを行い、実際にデータを集めてみないとわからない。

一方で、なんらかの理由で WebSocket が通らなくても HTTP なら通るという発想に基づくと、 XHR Polling へのフォールバックという発想が生まれる。

これをいち早くからサポートし、それによって「よくわからないが Socket.IO を使っておけばいい」という一つの定石を生み出したのも Socket.IO の強みの一つだった。

現状、十分なデータが無い以上 WebSocket が絶対に通ると言い切ることは、誰にもできないだろう。

「Socket.IO を使ったほうがいいですか?」と言われれば「まあ、心配なら念のためそうしたら良いと思う」と消極的に答えざるをえない。

サービスで WebSocket を使う全ての人へ

足らないのはデータだ。

2009 年くらいに Google が内部で調査した結果が、何かのスライドでちょろっと出た以外に、 WebSocket の接続に関して信頼に足る調査は聞いてない。

ということで、このブログでも微力ながら疎通の状況を調査してみようと思う。偏ったユーザのデータしか取れないと思うが、収集できたら詳細は追って紹介したい。

他にも、大規模なサービスで WebSocket を使っているサービスは多いと思う、是非状況を調査して統計情報を公開して欲しい。

それは自分のサービスの現状を知るだけでなく、より深い情報共有になるだろう。

一番期待したいのは、ブラウザベンダだ。

昔、 Google 時代の及川さんにも直訴したけど、今はもう辞めてしまわれた、各ブラウザベンダにおかれましては、是非実体の調査と結果の公開を是非お願いします 。