Googleの強さはStructured Chaosにあり
「足あとライブ!」

JSON+COMETでリアルタイム・ページビュー・カウンターを作ってみた

Fire 最近Linuxの勉強もかねて作っているのが、超シンプルなアーキテクチャーのHTTPサーバー。そこそこ動き始めたのだが、それだけでは面白くないので、サーバー側からイベントに応じてデータをPushできるCometの機能を足してみた。

 ストレステストのために、昨日からこのブログにこっそりとテスト用のIFRAMEを貼り付けてあったのだが(そのおかげで、バグを三つばかり見つけることができた―感謝、感謝^^)、安定して動き始めたので、見栄えを整えてこのブログの右上に貼り付けてみた。

 題して、「リアルタイム・ページビュー・カウンター(RPV Counter)」。Totalはこのカウンターをリセットしてからのページビューの数、Currentはその時点でこのブログを見ている人の数(ただしノイズあり)、PeakはCurrentの過去最大値だが、ページを再ロードせずとも、それぞれのカウンターが自動的にアップデートされるところが従来のものと異なる「おもてなし」だ。

【解説】
 右クリックで「ソースを表示」をしてもらえばJavascriptのソースを見ることができるが、サーバーの挙動を知らないと、少し分かりにくいので解説する。

まず最初に、

<script type="text/javascript" src="/~onload"></script>

がサーバーにページが表示されたことを知らせているのだが、その戻り値が

onLoadCallback({"Count":10, "Total":100, "Peak":15})

の様に、表示すべきデータを含んだ関数呼び出しである。ただし、この時点ではドキュメントはまだ準備できていないので、この時点では渡されたJSONオブジェクトをjsonInitというグローバル変数にコピーしておく。

 これをBODYタグのonloadイベントにひも付けられたload()関数がsetCount()にjsonInitを渡して最初の表示は完了する。

 ミソはそれが呼んでいるreload()。これが、Jason Levittが提唱するDynamic Scriptの仕組みで、動的に"/~listen"へのスクリプトタグを生成するのだが、これをサーバーがイベントがあるまで(この場合は、同じページを見ている人の数が変化するまで)保留しておく仕組みになっているのだ。そして、数値に変化があると、

updateCount({"Count":11, "Total":101, "Peak":15})

の様に、更新すべきデータを含んだ関数呼び出しが返って来る。つまり、実質的にサーバーからクライアントの関数呼び出しをしているのだ(サーバー側が呼び出す関数を決めているところがJSONPとは異なる)。updateCount()は、setCount()にサーバーから渡されたjsonを渡した後、再度reload()を呼び、サーバーからのイベントを待ちに入る、という仕組みだ。

【追記】 サーバー側は、Totalに関しては、/~onloadへのHTTP GETの数を数えているだけ。/~onloadへのHTTP GETに加え、クライアント側からコネクションが閉じられた時を「増減イベント」としてとらえ、/~listenへのHTTP GET待ちをしているクライアントすべてに対して、"updateCount(...);"を返している。Currentは、「増減イベント」のたびに、/~listen待ちのクライアントの数を数えて報告している(そのため、タイミングによっては実際の数より少なくなってしまう)。

【追記2】 ここで公開してから、サーバーが二度ほど落ちた。原因追求中。

【追記3】 まだCometサーバーは時々止めたりアップデートしたりする必要があるので、HTML/JS/CSSファイルは通常のウェブサーバーから取得するように変更。こうしておけば、Cometサーバーが止まっていても少なくともHTMLファイルを見ることはできる。ちなみに、Cometサーバーが停止しているときには、数字の代わりに"..."が表示されたままになる。

Comments

ぜんがめ

面白い試みですね。
ただ、うまく表示されないようです。
 MacOS XのSafari, Firefox
 WinXPのIE, Firefox
でダメでした。

コードを見て修正コメントできなくてすみません。

satoshi

ぜんがめさん、たぶんたまたまサーバーが落ちていたんだと思います(公開してから、2度ほど落ちています―原因は調査中です)。今のところ、MacのSafari、XPのIEおよびFirefoxで動作確認済みです。

ぜんがめ

はい、改めてアクセスしたところ表示されています。(Mac Firefox)
失礼しました。

SQ

最近私も知ったのですが、Ajaxをさらに加速させる仕組みだと思います。
データ構造さえうまく作れば、共有したデータを同時に操作することもできるので、ゲームやグループウェアなどで面白いものが作れそうですね。

サーバをそれように実装する必要があることが難点ですが、Apacheなど主要なサーバで対応するようになるとなかなか面白くなりそうです。

satoshi

>データ構造さえうまく作れば、共有したデータを同時に操作することもできるので、ゲームやグループウェアなどで面白いものが作れそうですね。

ですね。次はこれを使って何を作ろうかと、色々と妄想中です。

kenn

なかじまさん
おぉ早速ですね!
Webで読者が自分以外の人の存在の「気配」みたいなものが感じられるというのは面白いですよね。色々と応用できそう。
ところでSafariだとスピナーが回ったままになるのですが、これはiframe内でlong-lived connectionを貼っているからかな?親ドキュメント本体のエレメントを直接更新するようにすれば解消するかも知れません。。。
では、次回作も楽しみにしています!

satoshi

 kennさん、先日はありがとうございました。Cometは本当に楽しいですね。Safariのスピナーに関しては少し調べてみます。

wakufactory

これは面白いですね。
途中にproxyが居る場合、サーバ側でのイベントが長時間起こらなかった時に、接続がtimeoutで切られてしまうことがあるのではないかと思います。
クライアント側で何らかのリトライ処理は必要かもしれません。
この例だとまず大丈夫と思いますが・・・・

Miyazima

はじめまして。
CometはWebアプリを根本的に変えうる面白い仕組みだと思いますが、同時接続ユーザ数が多いとサーバ側のファイルディスクリプタが枯渇しやすいアーキテクチャですよね。
クライアントからのリトライは、サーバからのリトライ要求を一定時間で返せば問題なく可能だと思いますが、ネットワーク周りでの障害が気になりそうだなーと思っています。
でもなかじまさんのこのBlogでも今現在Peakで同時88だから、そこまで気にする必要はないんでしょうかね。

実際サーバが落ちたときの状況はいかがでしたか?原因が気になります。

satoshi

>CometはWebアプリを根本的に変えうる面白い仕組みだと思いますが、同時接続ユーザ数が多いとサーバ側のファイルディスクリプタが枯渇しやすいアーキテクチャですよね。

 そうですね。ファイルディスクリプタがLinuxの標準構成だと1024までしか使えませんから。

>実際サーバが落ちたときの状況はいかがでしたか?原因が気になります。

最初は、アルゴリズムが悪くて"Bad Pipe"で落ちまくりました。それを修正したのちは、かなり安定したのですが、それでも2~3時間に一回は"Bad Pipe"で落ちていました。全てが非同期で動いている限り、クライアント側が閉じてしまったソケットに書き込んでしまう可能性を0にすることはどうしても無理なようです。

 結局、

 signal( SIGPIPE , SIG_IGN );

で"Bad Pipe"エラーが起こっても、プロセスが落ちないように変更して回避しました。

Miyazima

>全てが非同期で動いている限り、クライアント側が閉じてしまったソケットに書き込んでしまう可能性を0にすることはどうしても無理なようです。

なるほど。ご説明ありがとうございます。
やってみないとわからないものですね。まさにWebが実験台w
普通のWebサーバでは回避できているのでしょうけれど。
MSG_NOSIGNAL指定でも回避できそうですね。

comet利用例として身近で面白かったです。SNSなどに実装して友達のログイン通知とかも面白そうです。
蛇足ですが、ご参考までに。
http://cometd.com/

富嶋@ITM

はじめまして。富嶋@ITMと申します。直前のご連絡となってしまい申し訳ございませんが、本日公開のAjaxうきうきWatchでこちらを取り上げさせていただきたいと思います。
恐れ入りますが、問題がございましたら、ご連絡くださいますと幸いです。どうぞよろしくお願いします。

Verify your Comment

Previewing your Comment

This is only a preview. Your comment has not yet been posted.

Working...
Your comment could not be posted. Error type:
Your comment has been posted. Post another comment

The letters and numbers you entered did not match the image. Please try again.

As a final step before posting your comment, enter the letters and numbers you see in the image below. This prevents automated programs from posting comments.

Having trouble reading this image? View an alternate.

Working...

Post a comment

Your Information

(Name is required. Email address will not be displayed with the comment.)