2011年9月24日土曜日

GAEでIf-Modified-Sinceに応答

だいぶ前になってしまいましたが、GAEの課金体系の変更が話題になりました。

一番大きな変更を大雑把にまとめると
  • CPU時間ごとの課金($0.10/CPU hour、毎日6.5時間まで無料)が無くなる
  • インスタンス時間ごとの課金($0.08 / hour、毎日28時間まで無料)が導入される
という感じ。

CPU時間というのは、CPUが計算を行っている時間。そのまま。
対してインスタンス時間というのは何か。

GAEでは、アプリケーションがGoogleのサーバで動いてて、ユーザからのリクエストに応答します。これがインスタンス。
リクエストが増えて応答に時間がかかるようになると、GAEのスケジューラは新しいインスタンスを起動してリクエストの処理に当たらせます。なのでインスタンス時間とは、全インスタンスが起動している「のべ」の時間。

で、それぞれ具体的にどんな数字が入るかというと、例えばdrawlrのケースでは

CPU時間: 0.1~0.6時間/日 ぐらい
インスタンス時間: 100時間/日 ぐらい

なので、改定前だと毎日無料、改定後だと毎日$5.76かかる計算に。やってられん!

ということで、GAEユーザの間で、どうするんだみたいな状況になっているようです。経緯説明終わり。

で、どうするんだ

まあ僕もGAEで公開しているサイトがいくつかあるのですが、既にチューニングしきってて大量のアクセスをギリギリ捌いているならともかく全然そんなレベルではないので、改定後の無料割り当てに収まるように地道に工夫してこうかと。

まずはキャッシュをちゃんと使ってもらう

こっから本題です。言うまでもなくHTTPにはCachingという仕組みがあります。

GAEでは、staticなリソースに対しては最初からキャッシュが効くよう動いてくれますが、ハンドラを使った動的な応答では、自前でキャッシュが効くようにレスポンスヘッダを設定する必要があります。

例えばdrawlrのトップページには20枚のサムネイルが表示されていますが、これらはデータストアに保存されているものをハンドラが動的に返しています。特に何もしていない実装だと、ビジターがページを開くたびに20枚のサムネイルがリクエストされ、画像がデータストアから取得され、ネットワークをデータが流れ、その全てのステップで僕の口座からお金が減ります。こいつはいけない!

何もしていない状態のハンドラ
class ImageHandler(webapp.RequestHandler):
  """ 画像出力 """
  def get(self, drawing_id):
    drawing = Drawing.get_by_key_name(drawing_id)
    self.response.headers['Content-Type'] = 'image/png'
    self.response.out.write(drawing.image)

実際はもう少し複雑ですが、大体こういうことをしています。

では、ブラウザにキャッシュしてもらうためには何をすれば良いか。
  • Expiresレスポンスヘッダを設定する
  • Cache-Controlレスポンスヘッダを設定する
  • Last-Modifiedレスポンスヘッダを設定し、次からのIf-Modified-Sinceリクエストヘッダを正しく処理する
これら全てを行います。
(本当はLast-Modifiedより利点のあるETagというのもあるのですが、また今度。)

Expiresは、レスポンスの有効期限を指定するHTTP/1.0のヘッダです。
ここで指定した期日までレスポンスは有効、言い換えればサーバへの確認なしにキャッシュを信じて良い期間を指定しています。例えば以下の例では、キャッシュされたレスポンスが10日間はサーバへの問い合わせを行わずに再利用されることが期待できます。
expires = datetime.datetime.now() + datetime.timedelta(days=10)
self.response.headers['Expires'] = expires.strftime('%a, %d %b %Y %H:%M:%S GMT')

Cache-Controlは、キャッシュをどのように行わせるかを詳細に指示するHTTP/1.1のヘッダです。
以下のように指定することで、やはり10日間の有効期限を指定しています。
publicの指示は、複数ユーザが使う共有キャッシュに入れても良いことを示します。このレスポンスが今回のクライアントの為だけに用意されたものであれば、代わりにprivateを指示します。
self.response.headers['Cache-Control'] = 'public, max-age=864000'

Last-Modifiedは、今回のエンティティ(この場合はdrawing)が最後に更新された時間を指定するヘッダです。drawlrの場合は、drawing.updated_at というプロパティがあるのでそれを使っています。
self.response.headers['Last-Modified'] = drawing.updated_at.strftime('%a, %d %b %Y %H:%M:%S GMT')
上記で指定した有効期限が切れた場合などで"キャッシュの正当性"を検証する必要がある場合、ブラウザは前回のLast-Modifiedの値をIf-Modified-Sinceリクエストヘッダに入れてリクエストを行います。
この際、サーバがステータスコード304(Not Modified)を返せば、キャッシュは引き続き正当となり、画像自体がネットワークを流れる分の帯域と時間の節約になります。

これらを実装して、こんな感じになりました。
class ImageHandler(webapp.RequestHandler):
  """ 画像出力 """
  def get(self, drawing_id):
    expires = datetime.datetime.now() + datetime.timedelta(days=10)
    self.response.headers['Last-Modified'] = drawing.updated_at.strftime('%a, %d %b %Y %H:%M:%S GMT')
    self.response.headers['Expires'] = expires.strftime('%a, %d %b %Y %H:%M:%S GMT')
    self.response.headers['Cache-Control'] = 'public, max-age=864000'
    if 'If-Modified-Since' in self.request.headers:
      cached = datetime.datetime.strptime(self.request.headers['If-Modified-Since'], '%a, %d %b %Y %H:%M:%S GMT')
      current = drawing.updated_at - datetime.timedelta(seconds=1)
      if cached > current:
        self.response.set_status(304)
        return
    drawing = Drawing.get_by_key_name(drawing_id)
    self.response.headers['Content-Type'] = 'image/png'
    self.response.out.write(drawing.image)
※drawlrの場合は描き足しがあるので、10日ではなくかなり短い時間にしていますが、描き足しがない場合は365日とかで良いはず。


動いているか確認

キャッシュされてない状態で訪問。画像の応答に約1.5秒、ステータスは200。

キャッシュされた状態で再訪問。画像のリクエストは行われない。(Sizeの欄にfrom cacheと出る)

キャッシュされた状態でリロード。画像の応答に約0.2秒、ステータスは304。

実際にページを再訪問(リロードせずにTopのリンクを開くなど)すると効果が体験できます。
応答が早い時間で終わるほど、一つのインスタンスが多くのリクエストを捌けるので、こういった工夫を積み重ねればまだまだGAEを使えるんじゃないかな!

次回はこれを応用して、JavaScriptコードをごにょごにょする記事を書きます。

2011年8月30日火曜日

drawlrのデータをWebGLで描画(Google Chromeのみ)

先週の8/21、グリッチワークショップのアフターパーティにて。


音で絵を動かしたい欲が高まり、なにか作りだす。

現場でできたものを調整したのがこれ。

http://www.drawlr.com/assets/experiments/webgl-gc.html

※WebGLの動くマシンじゃないと見れないです。
※Web Audio APIを使ってるので、設定を変えたGoogle Chromeじゃないと動かないです。

Web Audio APIの設定のしかた
  1. アドレスバーに about:flags と打ち込む
  2. 「ウェブ オーディオ」の項目を有効にする

もうちょっと整理して、ひとつの絵をgl.drawArrays一発で描くようにしたのがこちらなので、ソース見るならこちらでお願いします。でも表現はまだ微妙に前のほうが良い。

http://www.drawlr.com/assets/experiments/webgl-gc2.html

それと、drawlrに描いていただいた絵を使用しましたが、もし使われたくない!という方がいましたらTwitterにてお知らせください。

(将来的には、drawlrの機能として二次利用の可否を表明できるようにしたいです)

2011年7月14日木曜日

drawlrオープンしました。

前回の投稿と前後してしまいましたが、そういうわけで先週の7/8に、drawlrというサイトをオープンしました。

まずは突然これを作った経緯について説明しますと
というのが発端です。そのときのURLはこれで、「描いた絵に途中から描き足せる」「描いた手順を全て記録・再生できる」という特徴はこの段階でついていました。

当初はこの段階でさらっと紹介して終わるところだったのですが、絵を描いてくれた見富君に予想外に気に入ってもらえたので(?)機能を整え、デザインを@sskhybridさんにお願いして、サイトとして公開することになり。

ちょこちょこ機能を相談しながら作っていったら2ヶ月も経ってしまいましたが、その分「こういう機能あったらいいよね」を盛り込めたし、今後も増やしていける形にできました。

技術的には、HTML5 Canvas、JavaScript、Google App Engine/Pythonあたりで作っています。ので、重さを気にしなければiPhoneからでも再生・投稿できます。(まだiPhoneに対し最適化していないので、もっとスムーズに使えるようにはしたいところ)

いきなり多くの人に見てもらえたので、公開後1時間経たずにGoogle App Engineの無料割り当て分を突破してしまいました(ちなみに初日のサーバ代は$1.17でした)。ありがとうございます。

今後も隙を見て機能増やしていきますので、欲しい機能ありましたら僕 @psyark まで気軽にご意見ください。

2011年7月10日日曜日

ベクターデータ

説明が前後してしまいますが。



いまベクターデータを取得するプログラムを書いたので、そのうち何らかの形で機能にしたい。

drawlr自体の説明は次のエントリーで!

2011年5月6日金曜日

VJやるよ(3) やったこと報告 - コロラマ

4/30にAPMT NIGHTというイベントでlisglitch名義でVJやりました。
この日のための新ネタとして作ったエフェクトのうち1つを解説&公開します。

もともと、作りやすくて使い勝手の良いエフェクトってどんなの?と細金君に聞いてみたところ「コロラマというエフェクトがAfter Effectsにあるので、それをVDMXで動かしたい」と返事がきたので、Quartz Composerで作ってみました。

(合ってるかどうか分からないですが)コロラマがどんな感じのものか、実際に動かすとどうなるかは以下の動画をどうぞ。




ダウンロードはこちらから。VDMXで動かせるImage Filterと、QCで動作を確認しやすいサンプルが入っています。



仕組みはいたってシンプルで、入力画像の各ピクセルを、その明るさに応じて指定した配列中の色で置き換えるColor Mapを使い、色の配列のスケールと色のセットをいじりやすく加工しているだけのものです。

このエフェクトにデフォルトで埋め込まれている画像。


このうち、パラメータに応じたY座標の1ラインが切り出され、Affine Tileで並べられてColor Mapに渡されます。上下方向にグラデがかかっているのは、このパラメータをいじったときにスムーズにつながらせるため。
もちろん、画像を入れ替えて独自のパレットにすることもできます。

というわけで、やってることとしては超単純なんですが、画面全体の表情に影響を及ぼせるので、かなり効果的に使えると好評です。

他のエフェクトと素材に関してはまた次回。

2011年5月4日水曜日

久しぶりにTwitter API触ったらIDが64bitになっていた

ちょっと遅いネタ。

Twitter検索結果を表示するJavaScriptをメンテしてたのですが、少し前からIDが巨大な数値になっていたようで。

TwitterのIDが64bitになるとJavaScript等で問題が出るので対策を

この記事によると、JavaScriptでそのまま扱うと精度を保てない(※) 53bitのIDは11/26から出現していたそうです。
(※JavaScriptで使われるIEEE754の倍精度浮動小数点数は仮数部が52bitのため)

そうなるとどうなるかというと、例えば僕の以下のツイート

@psyark 食後にMacBookを膝の上に置いて作業すると、タッチパッドが誤作動しますよね。誤作動っていうか膨れたお腹がタッチパッド押してるだけなんですけどそうなりますよね。ならない?おかしいなあ。

のidは 64596791194169344 なのですが、これをそのままConsoleに打ち込むと

誤差が出る・・・。

と僕の場合どうなったかというと、検索リクエストをかけて、次の検索リクエストにその続きをまかせてたのですが、max_idの指定が少しずれてしまい、まれに複数の検索リクエストで結果が重複していました。

Twitter側も、 idと同じ内容を id_strという文字列でくれるようになったのですが、僕がやりたいのは「前回の検索結果の一番若いidを -1 して次回のmax_idにする」ということなので、これを減算しないといけない。

こういう場合の定石として BigIntでぐぐると、やっぱりbigint.jsなるものがあり、弾さんが9月に改良をいれたりしてる。タイムリー。

が、良く考えたら-1するだけなのだから、文字列処理で十分できるんじゃね、と思い。こんなんなった。

function decrement_str(strnum) {
 return strnum.replace(/([1-9])(0*)$/, function (match, a, b, index, all) {
  return (a - 1) + b.replace(/0/g, '9');
 }).replace(/^0(\d)/, '$1');
}

数値文字列の末尾の0を全部9に置換して、先行する[1-9]一文字だけ算術で-1。

0並びと同じ文字数の9並びとか、もっと軽く作れそうだけど横着。

もっと良い方法あったら教えてください。

2011年4月29日金曜日

VJやるよ(2) 告知

告知が遅くなったけど、VJやります。

@psyark(←僕) @hsgn @_maki の三人で lisglitch(りすぐりっち)という名義で去年から活動?していたのですが、新メンバーの漫画家 @mitomitakuya も入ったことだし何かやりますかということで、彼に描いてもらった絵を素材に使う予定です。

実はその描画データを撮るために作ったのが、半月前にこのブログで描いたお絵かきツールだったりする。
(しかも僕がlisglitchでまともに活動したのもほぼ始めてだったりする)

ので、今回からはちゃんとlisglitchしてますよ。良かったら見に来てください。

日時: 4/30(土) OPEN 21:30 START 22:30
場所: 渋谷WWW

REPUBLIC vol.7 映像作家100人2011 リリースパーティー
日時: 5/14(土) OPEN 13:00
場所: WOMB


ちなみに上記のお絵かきツールですが、せっかく作ったので、もう少し体裁整えてちゃんと使えるツールとして公開するつもり。そのスケジュールはまた今度。

2011年4月13日水曜日

EasyWebSocketでWebSocketを簡単にお試し

次世代のブラウザ・サーバ間の持続的接続用の規格であるWebSocket、
ちょっとこれ以上さりげなく説明できないので詳細はWikipediaの項目を見てもらうとして、

昨日のIE9/10のプレゼンでも触れられていたみたいだし、そろそろ実際に触ってみるかーと思いつつ、自分でサーバ立てるのは大変だし…

と思っていたらこんなの見つけた。

EasyWebSocket

リンク先の翻訳(適当)
WebSocket使いたい?接続されてるクライアント全員にメッセージを送りたい?ブラウザの互換性で悩みたくない?サーバ立ち上げたくない?君に必要なコードはこれだけ!
<script src="http://easywebsocket.org/easyWebSocket.min.js"></script>
<script>
    var socket = new EasyWebSocket("ws://example.com/resource");
    socket.onopen = function(){
        socket.send("hello world!")
    }
    socket.onmessage = function(event){
        alert("received "+ event.data)
    }
</script>
new WebSocketでソケット作る代わりにnew EasyWebSocketと書くことで、互換性の問題だけ吸収してくれるライブラリだよね、きっとIE6だとiframe使って擬似的にやってくれるんだよね、とか思ってたのだけど、「サーバ立ち上げたくない?」って部分が気になったので、試しに上記のコードを実行してみると


あれ、なんか接続してるっぽい。

コンストラクタに渡したURLはexample.comドメイン(実際に使われないよう予約されている)なので、ここに接続されてるってことは無い。実際の通信を見てみると、ws://88.191.76.230:8950/socket.io/websocketという見知らぬ所に繋がってる。

どうやらEasyWebSocketは、何を渡しても上記のサーバに繋ぎに行くものらしい。

ということは、サーバサイドでは自由に、というか一切何も作れない。そのかわり、JavaScriptだけごにょごにょ書いて何かちょっとしたものを作りたいとか、とりあえずWebSocketを触ってみたい、どのぐらいの時間で反応返ってくるのか見てみたいけどサーバ立てるのはちょっと・・・という、まさに冒頭で書いたような人には最適なものでした。

ので、同じような人いたら試してみると良いと思います。


余談

上のコード中のonopenだとかonmessageが、以前ここで書いたGAEのChannel APIのクライアント側コードとそっくりなのは、多分以下のような理由です。

現在Channel APIはCometを使って動作しているけれど、今後の予定として、WebSocketが使えるブラウザではWebSocketで動作し、そうでないブラウザのときのみ代替としてCometで動くようになるようです。

ので、あらかじめWebSocketに近いインターフェースで提供されているようです。

というわけで、EasyWebSocketだと物足りないけど自分でサーバ用意するのはやっぱり大変、という人はGAEやるといいんじゃないかな!(結局これが言いたかった)

2011年4月9日土曜日

お絵かきツールをざっく り作ってみた (2)

前回の続き。


変わったところ
  • アンドゥがちょっとましになってリドゥが付いた
  • タイムライン的な表示が付いた
  • ズームツールボタンが付いた
  • ズーム・スクロール位置を覚えておいて再生時に再現
  • 既存の絵を描き足せるようになった
  • URLが若干変更
描き足しは漫画家(みとみくん)に時間のかかる絵を描いてもらうために必須なので付けたのだけど、以前描いてくれた絵に実行してみると面白い。やっぱり人が作業したタイムラインって良いな。

    技術的には何も難しいことしていない。効率よく作ろうとしたら面倒な箇所はあるけど、まだそこまで詰めていないので・・・。

    ただ「スクロール位置を再生させたい」という漫画家の要望と、「編集ヒストリーとタイムラインが一緒だったらどうだろう」と思いつきを同時に実行しちゃったら、「アンドゥして眺めてるときスクロールすると絵が消える」という鬼のような仕様になってしまい。

    結局タイムラインを拡張して現在の形にしてるけど、正しい修正方法は「スクロールバー=単なるビュー変更」「手のひらツール=記録もするビュー変更」と2通りの手段を作ることな気がしてきた。

    とりあえず今回の目的は現状で果たせそう。

    じゃ次のVJ準備に入るよ。

    2011年4月7日木曜日

    お絵かきツールをざっくり作ってみた

    仕事の合間にお絵かきツールを作ってみた。

    これ

    ※実験で作ってるので、アップデートとかでデータ消えると思います…ご了承ください


    現在の機能
    • ペンの太さ選択
    • 色(白・黒のみ。良いカラーピッカー知りませんか…)
    • 保存して再生
    • アンドゥ(とても効率が悪い。いっぱい描いて実行すると多分固まります)
    まだこれだけ。


    何で作ったの

    一緒に働いている漫画家(@mitomitakuya)の絵が動いているところが見たかったので。

    でもアニメ描いてもらうのも方向性として違うのかなーと思ったので、お絵かき掲示板とかで良くある「描いていく経過が再生できる」ものを作ってみた。

    既に世の中にたくさんあるのにわざわざ作ったのは、保存した頂点データを取り出して遊びたいから。あと趣味の車輪作り。


    実際に使ってもらった



    最低限の機能しかないのに普通に描きだしてあせった。

    描いているところ




    実装したい機能

    • 再生速度調整。現状ざっくり倍速ができてるけど、数を大きくするほど描画順が前後して絵が壊れるのを何とかする
    • 何もしていない時間を良い感じに飛ばして再生
    • アニメGIF書き出し
    • TwitterとTumblrにポスト
    • 見た目をちゃんとする
    • ペンの太さに応じたカーソル

    2011年4月5日火曜日

    VJやるよ(1) Quartz Composerでフレームループエフェクト

    今月末にVJやるので、そこで使うエフェクトをQuartz Composerで作ってみるよ。

    まず作るのは「再生中の動画の直近のNフレームをキャッシュ / キャッシュしたフレームをリピートして再生」というもの。実は去年の冬に作っていたんだけど、640*480pxの素材だと動くのに、実際に使う1920*480pxの素材が全く動かずお蔵入りになってた。最近新しいMacBook Proで試したら1920でも動いたので、次回から使っていくつもり。

    Sample & Holdで頑張ってみました版

    上記の仕様をパッチで表そうとすると、普通に考えてSample & Holdだろう、と作ったもの。

    第一階層。Imageと、モード切替のブール値Holdを受け取ってImageを返す。


    Render in Imageを開いた第二階層。DelayerというマクロパッチからBillboardに描画してるだけ。


    Delayerを開いた第三階層。書くのめんどかった・・・。


    Sample & Holdは「入力されたデータを保持 / 保持したデータを出力」なので、これを8個並べてDemultiplexerとMultiplexerでラウンドロビンを組み、8フレームを分担してキャッシュするというもの。

    左上にあるJavaScriptは「コンポジションが実行されるたびに1ずつ増えて% 8」という数値を出力する。ImageをJavaScriptにも入力しているのは、動画のフレームごとにJavaScriptを再評価させるため。

    実はこれ3階層も要らないんじゃないか?Billboardで描画したのをRender in Imageで捕まえなくても、第三階層の出力をそのままコンポジションの出力にしちゃえば?と何度も思うのだけど、少なくともVDMXからこのエフェクトを動かす場合、この「一旦レンダリングしてから渡す」手順がないと動かない。この環境では「理由は良く分からないけど守らないと動かない」というのは珍しくないみたい。

    実はJavaScriptで簡単に作れました版

    上記のJavaScriptはこんな感じになっている。

    _index = 0;
    function (__number index) main (__image frame) {
           _index = (_index + 1) % 8;
           return { index: _index };
    }

    何となく関数の外に何か書いちゃいけない気がしてたので、この使い方に気づくまでは何かと不便だった。引数に追加だけしている__image(理由は前述)と組み合わせると非常に便利になる。

    で、別にSample & Holdを使わなくても、この関数外の変数に画像も保存できちゃうんじゃないの、と思って試したのが以下。

    第一階層。Sizeというパラメータが増えているの以外は一緒。



    第二階層。これで全部!


    実質何もしていないBillboardとRender in Imageを除くと完全にJavaScriptだけ。中はこんな感じ。

    _queue = [];
    _index = 0;
    
    function (__image Output) main (__image Image, __number Size, __boolean Hold) {
           if (++_index >= Size) {
                   _index = 0;
           }
           var result = {};
           if (Hold) {
                   result.Output = _queue[_index];
           } else {
                   _queue[_index] = Image;
                   result.Output = Image;
           }
           return result;
    }

    _queueに変数として画像を突っ込んで取り出すだけ。これでもちゃんと動く。

    qtzはこちら。



    実際に動作する様子は、月末にぜひ見にきてください。イベントの詳細はまた追って書きます。

    2011年4月4日月曜日

    Pogoplugで差分同期はできるのか

    今年からPogoplugを使ってる。

    オンラインストレージはDropbox等いくつか使ってるのだけど、Pogoplugはそれらと使用感が全く違う。提供してる機能はほとんど共通なのに、何でだろう。「クラウドに置かれたくないデータ」みたいな分類をする本能があるのかも。邪推してもいいよ。

    で最近聞かれたのが、「Pogoplugは差分同期できるの?」という質問。なので調べてみた。

    そもそも差分同期って何か

    多分カジュアルな言葉なので、検索しても唯一の定義は出てこないようだ。なので以下は僕の解釈。

    ここで言う差分とは、データを同期する必要がある部分、言い換えれば前回から変更された部分のことだろう。必要な部分だけをアップロードすることで、転送にかかる時間を最小限にできる。

    ただその「部分」の単位、よく考えると二通りの範囲がある。

    「変更があったファイルだけをアップロード」

    同期するように設定したフォルダ(Pogoplugでは、この設定を「アクティブ・コピー」と呼んでいる)のうち、既にアップロードされたファイルが変更された場合、それを検出して再度アップロードはしてくれるかどうか。

    さすがにこれを対応していないと使いようがないと思いつつ、話をしてた@hsgnくんが「リネームしたらアップされる」とか言っていた気がするので念のため確認。

    まず適当なフォルダを作ってアクティブ・コピーの設定。


    適当なファイルを置いてみる。 hoge.txt fuga.txt piyo.txt


    同期が始まって、my.pogoplug.comからも見えるようになる。

    で、ここでhoge.txtに書き込んでみる。

    ・・・

    あるぇー?何かが起きた気配が全くない。でもさすがに何かの間違いだろうと5分ほどクライアントソフトとにらめっこしてみたところ、同期が始まった。

    どうやらファイルの変更を見張る間隔がかなり長いみたいで、その設定も見当たらない。使うとき気をつける必要がありそうだけど、いちいちリネームする必要はないようだ。

    「ファイル中の変更があった範囲だけをアップロード」

    例えば1GBのファイルの末尾10byteが変わったとき、いちいちファイルを丸ごとアップしていたら効率が悪い。そういう場合に10byte + 変更箇所情報 だけアップしてマージしてくれるかどうか。

    まずhoge.txtに10MBの適当なテキストを書き込み。


    同期されたら、最後にhelloと追記。


    これも同期されたら、この間の通信量を見てみる。


    2箇所の赤が送信で、黄色が確認のためmy.pogoplug.comから受信したもの。

    左側と右側のサイズが同じで、黄色と面積がほぼ同じことから、Pogoplugのアクティブ・コピーはファイル全体に対して行われているようだ。

    ちなみに赤と黄色で山の高さが違うのは、Pogoplugを置いている家のLANで確認したから。Pogoplugは同じLANであればインターネットを経由せずファイルをやりとりできるので、my.pogoplug.comを経由した黄色に比べて短時間で通信が終わっている。

    ということで

    おそらく質問の意図であった「ファイルの変更された部分だけを同期」は行われないようだ。Dropboxにはこの機能があるようなので、その点は一歩負けている。

    一方で同じLANだと転送が早いのは地味に嬉しいが、それよりもファイル変更を検出するタイミングをもっと短くしてほしい。

    さらにDropboxにはLAN syncという機能があるらしく、同期が行われた際、複数のマシンが同じLANにいる場合にはそのLANの中だけでファイルをやりとりするようになっているらしい。(使い道は違うけど)こちらでも凄さで負けている気がする。もっと頑張れ。

    2011年4月2日土曜日

    GAEのChannel APIをもう一度最初から(2)

    前回の続き。

    ここまででChannel APIでの接続ができたので、メッセージのやりとりの簡単なサンプルとして、画面上の座標を互いに通信するマルチユーザアプリケーションを作ってみた。


    10人接続した状態でiPhoneから見てみる。


    まだまだ余裕っぽい。今までは同時接続制限5クライアントのFMSでなんとかしようとしてたので、これはかなり色々期待できる。

    ちなみに何人ぐらいぶらさがって大丈夫なんだろう。今度計測してみる。



    以下、手順。

    まず、クライアントIDと現在の状態を保持するモデルを作っておく。
    モデルはGAEで使われるデータベースのテーブル定義。
    class Channel(db.Model):
      id   = db.StringProperty()
      name = db.StringProperty(default=None)
      x    = db.FloatProperty(default=0.0)
      y    = db.FloatProperty(default=0.0)
    

    MainPageHandlerのget中に3行追加。これで今まで捨てていたidをDBに取っておける。
    c = Channel()
        c.id = id
        c.put()
    

    次にクライアント側。接続の際にTwitterアカウントを入力させ、Ajaxでポスト。
    socket.onopen = function (event) {
      var name = prompt('適当なTwitterのアカウント名を入れてください(他のユーザの画面にもアイコンが表示されます)', 'psyark');
      if (name) {
        $.post('/test/channel/name', { id:id, name:name });
      }
    };
    

    サーバ側で受け取って、取っておいたエンティティ(モデルのインスタンス)を更新。
    一番下の二行で全クライアントにブロードキャスト。
    class NameHandler(webapp.RequestHandler):
      def post(self):
        id = self.request.get('id')
        name = self.request.get('name')
        c = Channel.all().filter('id =', id).get()
        if c is not None:
          c.name = name
          c.put()
          for c in Channel.all():
            channel.send_message(c.id, json.dumps({ 'id':id, 'name':name }))
    

    最後にクライアント側で、send_messageされたデータを受け取って画像を作成。
    Twitterのアイコン取得にはtweetimag.esのサービスを使ってます。
    socket.onmessage = function (event) {
      var data = $.parseJSON(event.data);
      $('').attr({ id:data.id, src: 'http://img.tweetimag.es/i/' + data.name + '_n' }).appendTo(document.body);
    };
    

    ここまでをブラウザで開くと…、入力したTwitterアカウントのアイコンが、少し遅れて画面に出る!
    ので、Channel APIを上って下ってブラウザに届いていることが分かる。

    同じ要領で、
    1. クライアント側でクリックに応じて座標をポスト
    2. サーバ側で受け取り、エンティティを更新、ブロードキャスト
    3. 受け取ったIDのアイコンを更新
    を追加し、ページを開いたときに既に接続している他のユーザを復元するようにしたものが冒頭のこれ。ソースも置いておきます。



    なお、現段階では、Channelが閉じたことをサーバ側で知る方法は無い模様。
    将来的に実装される予定とのことだけど、放っておくとブロードキャスト相手のクライアントが増えすぎてパンクするので、とりあえず30分間触っていないクライアントは消すようにしている。

    GAEのChannel APIをもう一度最初から(1)

    今年の正月にこんなツイートをした。
    @psyark
    今年の年越し趣味コーディング → http://psyark.appspot.com/metan/ これは何:対戦可能なドミニオン 言語/技術:Python, GAE+Channel API かかった時間:5時間 売り:iPadでも動く
    以来ほとんど触ってなかったのだけど、このChannel APIは今後使えると面白そうなので、ここでまとめてみる。

    まず GAE (Google App Engine) というのは Googleが提供するサービスで、自分のアプリケーションをGoogleのインフラ上で実行できるというもの。
    それまでは例えばWebサーバを借りてPHP+MySQLで開発してみたいなことやってたけど、今はもっぱらGAEでどうにかしようとしている。まあ、詳しくはおいおい。

    Channel API というのは去年の年末にGAEで使えるようになった機能で、ブラウザへのpush通信を行えるようにするもの。つまりサーバ側でイベントが発生した時点でブラウザに通知できるので、例えばリアルタイムチャットを作るのに必要となる機能。

    今までそういったものをお金をかけず作りたい場合は、FMSを安く提供してるサーバを探してFlashで作ってたのだけど、それの代替案となるものをGoogleさんが無料または格安で提供してる。しかもこっちはiPhoneでもiPadでも動く。わーい。

    というわけで、早速簡単なサンプルを作ってみる。

    Channel APIの流れは

    接続処理
    1. サーバがクライアントIDを発行、それを元にトークンを作成してクライアントに渡す
    2. クライアントは、受け取ったトークンを渡してchannelインスタンスを作る。このとき bodyが見つからないと怒られるので、body中に書くなどする。
      (このJSクラスは /_ah/channel/jsapi を読み込むと入っている)
      var channel = new goog.appengine.Channel(data.token);
    3. チャンネルを開き、開通時とサーバからメッセージが来たときのハンドラを作る
      var socket = channel.open();
      socket.onopen = function (event) { };
      socket.onmessage = function (event) { };

    クライアント→サーバ
    1. クライアントが普通にAjaxリクエストを発行
      $.post('/hoge', { foo:'bar' });
    2. サーバは普通のリクエストとして処理
      class HogeHandler(webapp.RequestHandler):
        def get(self):
          # hogehoge

    サーバ→クライアント
    1. クライアントをIDで指定してchannel.send_messageを呼び出す
      channel.send_message(<クライアントID>, <データ>)
    2. クライアントのsocket.onmessage

    なので、接続さえ済んじゃえばほとんど悩むところがない。

    では、接続するところまで制作。

    #coding: utf-8
    
    from google.appengine.ext.webapp.util import run_wsgi_app
    from google.appengine.ext import webapp
    from google.appengine.api import memcache, channel
    import uuid
    
    class MainPageHandler(webapp.RequestHandler):
      def get(self):
        id = str(uuid.uuid4())
        token = channel.create_channel(id)
        self.response.out.write('''
          <body>
            <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.min.js"></script>
            <script src="/_ah/channel/jsapi"></script>
            <script>
              var channel = new goog.appengine.Channel('%s');
              var socket = channel.open();
              socket.onopen = function (event) { alert('open') };
              socket.onmessage = function (event) { alert('data') };
            </script>
          </body>
        ''' % token)
    
    application = webapp.WSGIApplication([
      ('/test/channel/', MainPageHandler)
    ], debug=True)
    
    def main():
      run_wsgi_app(application)
    
    if __name__ == "__main__":
      main()
    

    たったこんだけ!

    シンプルにするために、ページ自体のHTMLに生成したトークンを埋め込んでしまっている。なのでページを開くといきなりサーバ側でチャンネルの準備が行われてる。

    channel.create_channelに使うIDは、クライアントを識別するためユニークである必要がある。uuidをそのまま使えるのでとても楽。もちろんこれは後で使うので取っておかないといけない。(今は省略)

    ブラウザで開くと、ページ→jsapi→jQuery→devと読み込んだ後、開通時に設定したアラートが表示された。


    画面の下半分はGoogle Chromeのデベロッパーツール。
    このdevというのがChannel APIの実際の通信のようだけど、特に気にしなくて良い。

    メッセージのやりとりと、実際に動かしたものは次のエントリーで。

    ブログ再開

    2004年から2007年頃まで 主にFlash/ASネタメインでやってたpsyark.jpブログを再開した。

    今後はFlashに限らずWebを中心にいろんな勉強ネタを書いていきますよ。