井上真
New Bamboo
2010/10/5
今回から3回の予定でWebSocket登場の背景、基本的な使い方の解説、応用サービスの例、ブラウザの実装状況などを解説します(編集部)こんにちは、ロンドンのNew Bambooという会社でWebエンジニアとして働いている@makoto_inoueです。ここのところ、PusherというWebSocketのクラウドサービスの開発に関わっています。今回から3回に渡ってWebSocketに関する短期連載を担当させていただきます。
私を含めたNew Bambooの面々(我々は自分たちのことをBambinoと呼んでいます)がWebSocketになぜ興味を持ったかということからはじまり、実際にクラウドサービスを始めるまでにいたったストーリーをお話ししたいと思います。そのストーリーを通じて、WebSocketが切り開く「リアルタイムWeb」な世界への可能性や技術的課題を皆さんと共有できればと思っています。
- 第1回 node.jsの衝撃とWebSocketが拓く未来
- 第2回 WebSocketの現状と技術的課題
- 第3回 WebSocketでWebは変わる? 大胆予想!
途中でいくつかのサンプルコードをJavaScript(サーバサイドはnode.jsを使います)かRubyで掲載しますが、そのほかの言語で使えるツールやフレームワークも同時に紹介します。また、弊社のサービス「Pusher」は言語非依存ですので、好きな言語で試せます。
そして連載の最後で、WebSocketで作られたオープンソースプロジェクトのいくつかを紹介するとともに、今後リアルタイムWebがどのような変化をもたらすかを自分なりに大胆予測してみます。これをきっかけに、少しでも多くの方がWebSocketにトライしてみようと思っていただければと思います。
リアルタイムWebてどんなもの?そもそも「リアルタイムWebって、どんなもの?」と読者の皆さんは思われるかもしれません。例は身近にたくさんあります。FacebookチャットやGmailでオンラインのユーザーが見えるプレゼンス機能などがその例といってよいでしょう。Twitterがワールドカップの期間中に各試合ごとのリアルタイムツイート実況ページを開設していたのも記憶に新しいと思います。また、最近開発が中止されたGoogle Waveですが、チャットの入力が1文字ごとに更新される模様に度肝を抜かれたのをご記憶の方も多いのではないかと思います。
そうなんです、Google、Facebook、Twitterといった大手のプレーヤーは、すでにリアルタイムWebをサービスの要に投入しているのです。もちろんユーザーとしては彼らのサービスをただ使うだけで十分楽しいのですが、やはり開発者やサイトオーナーであれば、そういった機能を自分のサイトにも取り入れてみたいですよね。ただ彼らが使っている技術の詳細は断片的にしか漏れていませんし、特にJavaScriptレベルでの共通した規格というのはGoogle Waveプロトコルを除いてはほぼ皆無でした。
そこに登場したのがWebSocketです。これはリアルタイムWebを実現するHTML5の準規格の1つで、私たちもすぐに興味を持ちました。
まず初めに、私たちが自社のアプリにWebSocketを利用してリアルタイムWebの機能を取り入れた例を見ていきましょう。
最初の例はプロジェクト管理アプリの「TrueStory」です。もともと自分たちのクライアントプロジェクトの管理をするために社内で使っていたのをサービス化したものです。左右に並べたブラウザの左側でプロジェクトを変更すると、右側でも変更されているのを見ていただけるでしょうか。
次の例はドラッグ&ドロップ操作と最近アジャイル開発手法の1つとして再び注目を集めている「Kanban」の文字を掛け合わせて作った「Draggan」というアプリのプロトタイプです。
先ほどの例と同じく、変更がリアルタイムで更新されるのが見ていただけると思います。
こういった日頃から使うアプリケーションは、ずっとブラウザのタブで開きっぱなしになっていることも多いと思いますが、いざ自分が更新するときになって、更新しようと思っていたデータがすでになくなっていて、エラーが出てしまったりすると非常に不便に思ったりすることはないでしょうか?
私たちが実装した2つの例では、WebSocket対応によって何か機能が変わったということはありません。しかし、刻々と変わりゆく情報をリアルタイムに更新することで、ユーザーの利便性が飛躍的に向上しています。ちょうどGmailやGoogle MapがAjaxを全面的に導入することでユーザーエクスペリエンスを劇的に向上し、Web2.0という言葉まで生み出したのに匹敵するぐらいのインパクトがリアルタイムWebにはあるのではないか、と思っています。
それではリアルタイムWebを作り上げるのに必要な基礎技術の数々を私たちが体験した順にたどっていきましょう。
node.jsの衝撃昨年の2009年11月に話はさかのぼります。イギリス南部Brightonという町で「Full Frontal 2009」 というJavaScriptのカンファレンスに参加しました。BBCやYahoo!といった著名な会社のトップレベルのエンジニアたちが、JavaScriptの最新テクニックやトレンドについて語るイベントだったのですが、そのときにPythonのWebフレームワーク、Djangoの開発者であるSimon Willisonさんがnode.jsとついて熱く講演した内容が、その日の参加者の話題を独り占めしました。
もともと彼はJSONPを使ったクロスドメインスクリプティングについて話す予定だったのですが、講演の2週間前にnode.jsのことを知り、急遽トピックを変えたそうです。そこまで彼を駆り立てたnode.jsとはいったい何なのでしょうか?
「Node.js is genuinely exciting.」(node.jsは本当にエキサイティングだ)というタイトルの彼のブログから引用するnode.jsの説明は以下の通りです。
A toolkit for writing extremely high performance non-blocking event driven network servers in JavaScript.
「とてもハイパフォーマンスでイベント駆動なネットワークサーバをJavaScriptで書くための一連のツール群」
はっきり言って、これを最初に読んだときには「なんのこっちゃ」という印象しか残りませんでした。
「ハイパフォーマンス」というのは分かりますが、「イベント駆動なネットワークサーバ」と言われてもピンときません。
そこで彼は既存のWebサーバをウサギ、 イベント駆動のネットワークサーバをタコに例えて説明してくれました。以下の図を見てください
最初の例ではたくさんのウサギたちが並んでいて、次々に来るリクエスト(ネズミ)をさばいています。各ウサギは
- A:ネズミを受け取って
- B:何か処理をして
- C:それをレスポンスとして返す
という一連の処理をしています。実際にはAはHTTPリクエスト、Bはデータベースクエリ、CはHTTPレスポンスということになるでしょう。
ここで重要な点は、ウサギは1匹のネズミに対してA~Cの処理が終わるまで、次のネズミの処理に取りかかれないことです。ここでもしBの処理が重かったりすると、徐々に順番待ちのキューが膨れ上がってしまいます。これに対処するには、複数のウサギを用意する必要があります。これが現在、多くのWebアプリケーションが取っている構成で、ウサギに相当するHTTPサーバを複数並べることで、並列性を高めています。
一方、2つ目の例として出てくるイベント駆動はタコです。タコもネズミを1匹ずつ処理するのに変わりないのですが、A~Cまでのすべての処理が終わるのを待ったりはしません。Aのリクエストを受け取ると、データベースクエリを投げたまま、結果を待たずに次のネズミの処理に入ります。そして先ほどのデータベースクエリの結果が返ってきたら、それをキャッチしてレスポンスとして戻します(複数のリクエストを同時にさばくという意味で、タコなのでしょう。タコには手(足?)がたくさんあるので、一度に多くの処理ができそうです)。 こうすることで、タコは1人であるにもかかわらず、大量のネズミをさばくことができるのです。
これを実際のコードでベンチマークしてみましょう。
もし実際に試してみたい方は、node.jsをインストールしてください。UNIX系のOSの場合、インストールは非常に簡単です。node.jsのサイトからソースをダウンロードし、makeコマンドを以下のように実行するだけです。
ここではデータベースアクセスのかわりにsetTimeoutファンクションを使用しています。
Simonさんのブログは昨年のもので、node.jsのAPIはかなり変更されているため、最新のもの(v0.1.104)のAPIを使用したものに変更してみました。
まずは通常のHTTPリクエスト/レスポンスの例です。
上のコードをhello.jsという名前で保存し、サーバとして立上げてみましょう。
このサーバに対して、ab(Apache Bench)で1000回(並列度100)のアクセスをしてベンチマークを計測します。私のマシンはMacBook ProでCPUコアは2つです。
結果は以下の通りです。
次に2秒の待ち時間を加えた例です。
結果は以下の通りです。
Time per request がTimeoutタイマーと同じほぼ2秒だというのが見て取れます。
「最初の例が0.2秒しかかかっていないのに、今回は100倍の20秒も時間かかっている。それって遅くない?」と思ったあなた、では次のコードを見てみてください。これはSimonのブログには載っていない例ですが、Timeoutファンクションの代わりにSleepをするファンクションに付け替えてしまいました。
結果は以下の通りです。
2秒×1000リクエスト=2000秒という結果と同等なのが見て取れますね。せっかくabの並列度を100にしているのに、まったくそれが生かされていない結果となりました。
逆にsetTimeoutの方は2秒×1000リクエスト/100=20秒と並列性が有効であることが分かります。並列度さえ上げれば、もっと速くなることでしょう。
両者を分けた違いは、setTimeoutがノンブロッキングなファンクションで、Sleepがブロッキングなファンクションであることにあります。setTimeoutが実行された後、JavaScriptは結果を待たずに次の処理に取りかかり(これを非同期処理と呼びます)、 setTimeoutに渡された関数(これをコールバック関数と呼びます)は2秒経った後に実行されます。
このコールバック、非同期処理を主体としたイベント駆動プログラミング手法は、通常のWebサーバを書くときには使わない概念ですが、ブラウザ上で実行されるJavaScriptはシングルスレッドしか使えないので日常的に使われています(ブラウザ上で先ほどのようなsleepファンクションを実行するとブラウザが固まってしまいます)。
このコールバック、非同期処理をサーバ側で全面に打ち出したフレームワークとして登場したのがnode.jsなのです。
「なるほど、イベント駆動でノンブロッキングだと、たくさんの同時アクセスがある場合にパフォーマンスが高いのか。でも私のサイトはそんなに同時アクセスがないから関係ないよね」。そう思われた方もいらっしゃるかもしれません。しかし、ノンブロッキングが真価を発揮するエリアがいくつかあります。
- ファイルのアップロード
ビデオなど、大きなファイルアップロードというのは大変に難しい問題です。先ほどのTimeOutの例では2秒のブロックでしたが、何百MBにもなるようなファイルを送るとすると、何十秒、場合によっては分単位でのブロッキングが発生します。またアップロードの最中に、進捗具合をリアルタイムで知りたいという要求も出てきます。これまでにも進捗具合を表示するFlashベースのSWFUploadや、Webサーバのモジュールがありましたが、自分に合うようにカスタマイズするのが難しかったりします。
node.jsの初期からのコントリビューターであるFelix Geisendorferさんは、こうした処理が、node.jsを使っていかに簡単にできるかについてのブログを書いています。彼の会社が最近リリースしたTransloaditは、ファイルのアップロードと変換を提供するクラウドサービスなのですが、これはnode.jsで構築された最初の商業サイトの1つです。
チャット以前「Lingr」というチャットサービスがあったのですが、作者の江島健太郎さんが「Lingr and Comet - 技術解説編」の中でチャットサービスのスケーリングの難しさを以下のように述べています。
通常、Apacheなどの一般的なウェブサーバは、短い応答時間で返せる処理を大量にこなすというスループット重視の前提で設計されています。このため、リクエストを受けたらそのリクエストに対してプロセスまたはスレッドをあてがい、最後まで面倒を見るという方式が一般的です。
ところが、先ほども言ったようにCometではコネクションはつなぎっぱなしになっていますから、いつまで経ってもプロセスやスレッド(およびメモリ資源)が解放されません。しかも、それらのスレッドは仕事もせずにアイドリングしており、メモリとCPUを浪費しているだけです。これは重大な問題です。どのぐらい重大かというと、そもそも千や万のオーダーの同時接続を実現することができません。
「複数のスレッドやプロセスを用いて同時接続を実現する」というのはリソースの浪費になるのですが、node.jsのイベント駆動の場合、立ち上げているプロセスは1つだけなので、リソースの観点から大変効率の良いモデルといえます。そしてこの「接続をつなぎっぱなしにする」チャットモデルは、これからお話しするWebSocketの世界と密接に関係してきます。
「いや~、node.jsってすごいよね」。カンファレンス参加後、しばらくの間はNew Bamboo社内ではnode.jsの話題が何度も上がってきたのですが、実際にnode.jsを使って何をすれば良いかはちょっと考えあぐねていました。その頃は、node.jsを使用したWebフレームワークなどが雨後のタケノコのように出てきていたのですが、「今までのWebサーバで出来ることをただ置き換えるだけっていうのはあんまり面白くないよね」というのが正直な気持ちだったと思います。
それから1カ月ほど経った2009年の12月、Webの世界に新たなニュースがありました。Googleが開発するブラウザのオープンソース版であるChromiumにWebSocketという新たな機能が加わるというのです。
WebSocketを一言で言うと「WebのためのTCP」です。今までのWebはHTTPプロトコルを基本とした一方向のものでした。クライアント(ブラウザ)はサーバにリクエストを渡し、サーバはクラインとにレスポンスを返すというスタイルです。
TCPはHTTPより下層に位置するネットワークプロトコルで、クライアントとサーバ間の双方向通信を可能とするものです。
TCPサーバをnode.jsで書くと以下のようになります。
この状態でtelnetすると、以下のようにTCPサーバとやりとりすることができます。
ここでTCPサーバは各セッションごとにarrayという配列を用意しておき、入力リクエストが来ると、そのデータを配列に代入し、全配列の結果をレスポンスとして戻しています。
HTTPとの重要な違いですが、HTTPは各リクエスト、レスポンスはまったく別個のものであり、各リクエスト間で状態(ステート)を共有することはありません。逆に、TCPの方は一度クライアントとサーバとの間で接続が確立されると、その後のやり取りで状態を共有することができます。HTTPのことをステートレスと呼ぶのに対し、TCPのことをステートフルと呼びます。
今までHTTPで擬似的にステートを共有するには、以下の方法が取られていました。
- クッキーに情報を保存する
- リクエストのURIパラメータにすべての値を指定する(例:http://example.com/?val=[a,b,c])
もともとHTTPはHTML文書を表示するために規定されたものなので、「リクエスト/リスポンス」の単純なモデルで十分だったのですが、多くのWebサイトが「アプリ化」してきている現在では、上のような方法で擬似的にダイナミックなアプリを作るには限界があります。WebSocketはそんな限界を打ち破る可能性を秘めた新たなプロトコルといって良いでしょう。
そしてWebSocketのステートフルな性質を利用することで「サーバサイドプッシュ」を実現することができます。必要なときにサーバサイドから情報を送ることができれば、ムダなトラフィックを減らせます。
例えば、雪が降ったり、災害のときなどに交通機関のサイトやニュースサイトがアクセス超過でサイトがダウンすることがあります。これは、情報が更新されるタイミングが分からないために、みんなが何度も「リフレッシュボタンを」押すからでしょう。これはリクエスト/レスポンスというモデルの弊害と言ってよいでしょう。
もしこういったサイトをWebブラウザで開いておいて(TCPでいう接続の確立)、ニュースがあったときのみサーバからクライアントの方に情報をプッシュしてくれれば、余計なトラフィックを抑えられるはずです。
前置きが長くなってしまいましたが、WebSocketをブラウザから使うためのJavaScript APIは以下です。
APIは非常に単純ではないでしょうか。
まず、1行目でWebSocketオブジェクトを作成した後、「onopen,onmessage,onclose」という3つのコールバックファンクションを定義するだけです。サーバの方にメッセージを送りたい場合は「send()」というファンクションを呼び出すだけです。
このWebSocketのAPI、先ほど例として挙げたnode.jsのTCPサーバサイドのコードに非常に似ていると思いませんか? 私はこの例を見たときに「WebSocketとnode.jsって合うのでは」と思いました。早速node.jsのWebSocketサーバを探してみたところ、プロトタイプ版のようなライブラリが2つほど見つかりました。そこで、これを使い、週末を利用してMacのActivity MonitorのようなもののWeb版を作ってみることにしました。
ソースの全文はgithub上にあります。当時は「websocket-server-node.js」というライブラリを使っていたのですが、今はすでにメンテされていないようなので「node-websocket-server」というライブラリに変更して書き直してみました。
なお、今回のサンプルのiostat-client.htmlですが、わざわざWebサーバ上に置かなくとも、ファイルをローカル上で開くだけで実行可能なはずです。
「connection」と「close」という2つのコールバック関数を指定している点は、TCPサーバの例と大変似ていますね。少し違う点としては、ブラウザからの入力を取らず、
のところで子プロセスを作り、iostatというコマンドが1秒おきにCPUやIO情報を出力するようにしている点です。そして、
のところでチャイルドプロセスの出力結果を1秒ごとに出力するようにしています。
ブラウザ側のコードもいたってシンプルです。
「onmessage」コールバックにデータが送られてくるたびに、JSONデータを解析し、その結果をグラフとして再描画しています。
AjaxやCometと何が違うの?ここまで読んだ読者の皆さんは思うかもしれません。「AjaxやCometでも同じことができるんじゃないの?」
まず以下の図を見てみてください。
一番最初の例はAjaxです。毎秒ごとにHTTPリクエストを送っています。 HTTPのリクエストとレスポンスにはヘッダー部分に以下のようにいろいろな付帯情報を付けなければいけません。
1リクエスト/レスポンスヘッダー自体は数KBと、そう大きくありませんが、リクエストの更新頻度が密になってくると、毎回のリクエスト量は馬鹿になりません。更新頻度を遅くすると、欲しい情報がすぐに手に入らないし、更新頻度を上げるとサーバへのリソース要求が高くなる、というジレンマを抱えることになります。
また疑似サーバプッシュ技術を総称する用語として「Comet」というものがあります。これはいろいろな実装方法が混在しており、なかなか分かりづらい用語なのですが、一般的なものに「Long Polling」と呼ばれるものがあります。これは次のように流れになります。
まず、サーバがクライアントからリクエストを受けた際、すぐにレスポンスを返すのではなく、 コネクションをつなぎっぱなしにしておきます。そして何か情報を更新するときになってからレスポンスを返します。これは私が一番最初に述べた「災害情報の更新」やチャットなど数秒以上に一度しかレスポンスを返さないようなケースでは有効ですが、更新頻度が上がってくるにつれ、AJAXと同じような問題を抱えることになります。さらにCometで定期的に疑似プッシュしようとした場合、「どの時点までのデータをクライアントに送信したか」というステート情報も別に管理しなければいけません。
私のActivity Monitorの例を見た方から「node.jsとLong Polling方式で実装しました」と教えていただいたので、彼のコードの一部を見てみましょう。
「/update」のURIにクライアントが最後にデータを受け取った時間を毎回送付して、サーバの現在時刻と比較。差分のデータを送るようにしています。毎リクエストごとのステートを管理するコードを別途書かないといけない上に、今回のように時間を扱う場合、「クライアントマシンの時間設定がちゃんと設定されていない場合はどうなるの」といったケースにも対応する必要があるでしょう。
一方のWebSocketですが最初に接続を確立する時に以下のようなリクエストをサーバの方に送ります。
サーバサイドはリクエストを受け取った後、以下のようなレスポンスを返します。
基本的には通常のHTTPリクエスト/レスポンスとほぼ同じです。
ここまでだとAJAXやCometと同じように見えるかもしれませんが、このやりとりを行ってクライアントとサーバの間で接続が確立された後は、以下のようにデータの前後に1バイトずつ付けた「データフレーミング」という形式でデータのやりとりを行うことになります。
接続時のHTTPリクエスト/レスポンスに比べて明らかにデータ転送のオーバヘッドが少なくなるのが見て取れると思います。
こういったパフォーマンスの劇的な向上をもたらす可能性を秘めたWebSocketですが、ドラフト起草者であるGoogleのIan Hicksonさんはメーリングリストの中で以下のように述べています。
Reducing kilobytes of data to 2 bytes…and reducing latency from 150ms to 50ms is far more than marginal. In fact, these two factors alone are enough to make WebSocket seriously interesting to Google.
「数キロバイトのデータ転送量を(最低)2バイトへ、150msの遅延を50msにできるとしたら、それはわずかな向上といったような生やさしいものではない。実際この2つの事実だけをもっても、WebSocketをGoogleにとって本気にさせるには十分なものだ」
またWebSocketのうれしい点としてクロスドメインが可能な点も挙げられます。
AJAXやCometの場合、原則クロスドメインスクリプティングができないため、JSONPなどのテクニックと併用する必要がありますが、WebSocketは原則可能です(最初のリクエストヘッダーの部分でSec-WebSocket-Locationを指定しているので、それを利用して既知のドメインのみアクセスを許すといった制限をかけることも可能です)。
さて、今回はここまでです。リアルタイムWebとは何かということと、WebSocketの実例を示し、AjaxやCometとの違いについて述べました。次回は、WebSocketのブラウザの対応状況などを交えつつ、技術的課題やサーバサイドの実装例などについてご紹介したいと思います。