レベル: 中級 David Mertz (mertz@gnosis.cx), Efficient server, Gnosis Software, Inc.
2007年 11月 13日 Ajax ベースのアプリケーションを始めとする Web アプリケーションを可能な限り RESTful な方法で作成すると、さまざまな種類のバグを回避することができます。しかし REST (REpresentational State Transfer) の落とし穴は、似たような XMLHttpRequest で重複したデータを送ってしまうことです。このヒントでは、セッション・クッキーを適切に使うことでサーバー・サイドの状態を必要最低限だけ保持することができ、それによってクライアントとサーバー間のトラフィックを大幅に削減でき、しかもクッキーを必要としない操作にも戻れることを説明します。
はじめに
HTTP に関する単純な事実として、ステートレス・プロトコルであることが HTTP の最大の長所であり、また HTTP の弱点の中心でもあります。HTTP サーバー・リソースへの各リクエストは、冪等 (べきとう) であることになっています。つまり同じリクエストを呼び出せば、どの呼び出しに対しても同じ結果を返す必要があります。冪等は REST の中心概念でもあり、同じリクエスト (例えばクライアント情報のエンコーディング) によって、常に同じデータが返される必要があります。
REST の原則とは逆に、Ajax アプリケーションは多くの場合、ステートフルです。Web アプリケーションでは、あるフィールドや領域は、何らかのサーバー・データの現在の状態を反映しており、その現在の状態をクライアントの JavaScript ポーリングを使って周期的に問い合わせます (これをもっとプッシュ指向にする方法はありますが、このヒントにとっては本質的な話題ではありません)。しかし Web アプリケーションは多かれ少なかれ、そのアプリケーションが次回のポーリング・イベントで知らなければならないこと (例えばクライアントがどんなデータを見たか、あるいは見なかったか、どんな対話動作が起きたか、など) を、サーバーが追跡するものと想定しています。
Ajax アプリケーションを技術的に RESTful にする一般的な方法の 1 つは、最新データに対するすべてのクエリーがグローバルに一意の URI を持つように調整する方法です。例えば、あるクエリーは、URL にエンコードされたパラメーターの中やフォームの隠し変数として、UUID を持っているかもしれません。一例として、XMLHttpRequest オブジェクトは下記のリソースを GET するかもしれません。
http://myserver.example.com?uuid=4b879324-8ec0-4120-bba6-890eb0aa3fc0
|
この次のポーリング・イベントでは、たとえそのイベントがほんの 1 秒後だったとしても、別の URI が開かれることになります。
冪等は注意が必要
「同じデータ」の意味するところを理解するのは、思ったよりも微妙で難しいものです。同じ URI が必ず同じデータを返すのは、漫画の中だけです。結局のところ、たとえ静的な Web ページであっても、コンテンツが修正されればそのページは変更される可能性があります (例えば公開記事の中のタイプミスが修正されるなど)。冪等の背後にある考え方は、行われる変更は GET リクエストのリクエストに含まれる内容を直に反映したものであってはならない、ということだけです。そのため、常に変化する下記のようなリソースを持つことは、完全に理にかなっています。
http://myserver.example.com/latest_data/
|
ここでの問題は、このデータが取得されたのかどうか、いつ、誰によって取得されたか、という以外の事柄によって「latest_data」の構成が決まる、という点です。サーバーは、完璧に RESTful でありつつ「世界情勢」を反映できるのです。
最新のデータを取得する
同僚の Miki Tebeka と私は、まさにこの状況、つまり JavaScript の XMLHttpRequest() オブジェクトを使ってサーバーから最新データを頻繁にポーリングする Web アプリケーションを開発する状況に直面しました。ここで示す Python サーバーの例は、Miki が社内用に作成した便利なモジュールに着想を得たものですが、さらに単純化し、改善してあります。
私達がここで解決しなければならない問題は 2 つありました。1 番目の問題は、前回のリクエストから何も変更されていない場合には、本体を含むメッセージを送らないようにすることです。2 番目の問題は、重複データを生成する際にデータベースや計算のリソースを使いすぎないようにすることです。
1 番目の「変更されていない」という問題は、実際にはまさに HTTP プロトコルの中で対応されていますが、この正しいソリューションはあまり使われていません。私達がするとよいこと (そしてすべきこと) は、HTTP の 304 ステータス・コードを返すことだけです。Ajax コードの役割として、304 ステータスがないかチェックし、もし見つけたら、ポーリングによって送信されたデータ (が存在しないこと) に基づき、クライアント・アプリケーションの状態を変更しないようにします。
サーバー・リソースの問題は、前のデータをキャッシュし、そして最も新しく追加されたものを集約することで対応することができます。このソリューションは一般的に、「最新データ」が比較的独立したデータ項目で構成される場合しか有効ではなく、すべてのデータ・セットが相互依存している場合には、あまり有効ではありません。キャッシュされた、クライアント・セッションの状態は、クライアントのクッキーを使って追跡することができます。リスト 1 は、それをまとめたものです。
リスト 1. セッションを有効にしたサーバー・コード: server.cgi
from datetime import datetime
session = ClientSession()
old_stuff = session.get("data", []) # Retrieve cached data
last_query = session.get("last", None)
prune_data(old_stuff, last_query) # Age out really-old data
new_stuff = get_new_stuff() # Look for brand-new data
if not new_stuff:
print "Status: 304" # "Not Modified" status
else
print session.cookie # New or existing cookie
print "Content-Type: text/plain"
print
all_stuff = old_stuff + new_stuff
session["data"] = all_stuff
session["last"] = datetime.now().isoformat()
print encode_data(all_stuff) # XML, or JSON, or...
session.save()
|
ClientSession クラスでは、さほどたいしたことはしていませんが、少しだけ賢いことをしています。このため、基本的に私達がしなければならないのは、キャッシュされた old_stuff に対応するクッキーを持っている可能性のある各クライアントを追跡することだけです。
リスト 2. セッションを維持する
from os import environ
from Cookie import SimpleCookie
from random import shuffle
from string import letters
from cPickle import load, dump
COOKIE_NAME = "my.server.process"
class ClientSession(dict):
def __init__(self):
self.cookie = SimpleCookie()
self.cookie.load(environ.get("HTTP_COOKIE",""))
if COOKIE_NAME not in cookie:
# Real UUID would be better
lets = list(letters)
shuffle(lets)
self.cookie[COOKIE_NAME] = "".join(lets[:15])
self.id = self.cookie[COOKIE_NAME].value
try:
session = load(open("session."+self.id, "rb"))
self.update(session)
except: # If nothing cached, just do not update
pass
def save(self):
fh = open("session."+self.id, "wb")
dump(self.copy(), fh, protocol=-1) # Save the dictionary
fh.close()
|
Ajax を呼び出す
キャッシュ・サーバーが用意できると、そのデータをポーリングする JavaScript は非常に単純になります。必要なものは、リスト 3 のようなもののみです。
リスト 3. 最新データを求めてサーバーをポーリングする
var r = new XMLHttpRequest();
r.onreadystatechange=function() {
if (r.readyState==4) {
if (r.status==200) { // "OK status"
displayData(r.responseText);
}
else if (r.status==304) {
// "Not Modified": No change to display
}
else {
alertProblem(r);
}
}
}
r.open("GET",'http://myserver.example.com/latest_data/',true)
r.send(null);
|
この単純な例では、displayData() と alertProblem() の実装は指定してありません。おそらく前者は、受信されたレスポンスを何らかの方法で構文解析するか、処理する必要があります。この詳細は、データの送信に JSON が使われているのか、あるいは XML やその他のフォーマットが使われているのかに依存し、また実際のアプリケーションの要件にも依存します。
さらに、この簡単な例では、ポーリングを 1 回行う方法しか示してありません。長期間実行するアプリケーションでは、このリクエストを setTimeout() または setInterval() コールバックの中で繰り返し実行すればよいのです。あるいはアプリケーションによっては、ある特定のクライアント・アプリケーションのアクションあるいはイベントに続いてポーリングが行われるかもしれません。
まとめ
このヒントでは Python で作成したサーバー・コードを示しましたが、ほとんど同じ設計を、CGI やその他のサーバー・プロセスのプログラムに使われている、ほとんどすべての言語にも適用することができます。大まかな概念は単純です。キャッシュされたデータを特定するために、(もし使えるようであれば) クライアント・クッキーを使い、そして最後のポーリング・イベント以降新しいデータが生成されてなければ 304 ステータスを送信します。サーバー用のプログラミング言語が何であれ、プログラムはどれもほとんど同じように見えるはずです。
エラーが発生した場合についてはほとんど触れませんでしたが、この設計は堅牢であり、クッキーが利用できない場合には適切な動作に戻ります。もしクライアントが (クッキーを受け付けないため、あるいは、これが新しいセッションの最初のポーリングであるため)、必要なセッション・クッキーを持っていない場合には、old_stuff は単なる空のリストであり、返されるデータはすべて new_stuff に含まれることになります。もう 1 つ、追加する価値のある機能として、現在のセッション状態を送信する特別なクライアント・メッセージがあります。これはアプリケーションのデバッグにも便利であり、また何か障害が起きたことをクライアントが検出した場合に一貫性のない状態をクリアーするための方法としても便利です。キャッシュをフラッシュすることで失うものは、わずかのサーバー負荷と帯域幅のみです。それによって基本である冪等に違反することはありません。
参考文献
著者について
記事の評価
|