Tornadoウォークスルー

リクエストハンドラと、リクエスト引数

Tornadoウェブアプリケーションでは、URLあるいはURLパターンを tornado.web.RequestHandler のサブクラスにマップします。これらのクラスではリクエストされたURLへのHTTP GET/POSTリクエストを処理する get() / post() メソッドを定義する必要があります。

下記のコードではルートURL /MainHandler に、URLパターン /story/([0-9]+)StoryHandler にマップしています。正規表現化された箇所は RequestHandler() メソッドに引数として渡されます。

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("You requested the main page")

class StoryHandler(tornado.web.RequestHandler):
    def get(self, story_id):
        self.write("You requested the story " + story_id)

application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/story/([0-9]+)", StoryHandler),
])

get_argument() メソッドによってクエリ文字列引数の受け取りとPOSTの本体のパースを行うことができます。

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write('<html><body><form action="/" method="post">'
                   '<input type="text" name="message">'
                   '<input type="submit" value="Submit">'
                   '</form></body></html>')

    def post(self):
        self.set_header("Content-Type", "text/plain")
        self.write("You wrote " + self.get_argument("message"))

tornado.web.HTTPError 例外を投げると、クライアントに403 Unauthorizedのようなエラーレスポンスを返すことができます。

if not self.user_is_logged_in():
  raise tornado.web.HTTPError(403)

リクエストハンドラは現在のリクエストを表すオブジェクトの self.request でアクセス可能です。 HTTPRequest オブジェクトは多くの便利な属性があります。

  • arguments - すべてのGETとPOSTの引数
  • files - すべてのアップロードされたファイル(multipart/form-data POSTリクエスト経由)
  • path - リクエストパス(?以前すべて)
  • headers - リクエストヘッダ

httpserver 内にあるHTTPRequestのクラス定義を参照すると、すべての属性を見ることができます。

テンプレート

Pythonがサポートしているあらゆるテンプレート言語を用いることができますが、Tornadoでは他の有名なテンプレートシステムと比較して、格段に速くより柔軟な独自のテンプレート言語を提供しています。完全なドキュメントは templateモジュール のドキュメントを参照してください。

Tornadoテンプレートは、マークアップ内にPythonの制御構造と式が組み込まれた単なるHTML(あるいは他のテキストベースフォーマット)です。

<html>
  <head>
    <title>{{ title }}</title>
  </head>
  <body>
    <ul>
      {% for item in items %}
        <li>{{ escape(item) }}</li>
      {% end %}
    </ul>
  </body>
</html>

このテンプレートを template.html としてPythonファイルと同じディレクトリに保存した場合、以下のコードでレンダリングできます。

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        items = ["Item 1", "Item 2", "Item 3"]
        self.render("template.html", title="My title", items=items)

Tornadoテンプレートは制御構造と式をサポートします。制御構造は {%%} で囲むことによって表されます。たとえば {% if len(item) > 2 %} のような形です。式は {{}} で囲むことによって表現します。たとえば {{ items[0] }} といった具合です。

制御構造はほぼPythonの制御構造の表現と対応しています。if, for, while, tryがサポートされていて、終了は{% end %}で宣言します。また、extendsblock宣言によりテンプレートの継承も可能です。詳しくはtemplateモジュールのドキュメントを参照してください。

式には関数呼び出しを含む、あらゆるPythonの式を書くことが可能です。Tornadoではデフォルトで escape, url_escape, json_encode をサポートしており、さらに他の関数もテンプレートレンダリング関数にキーワード引数として渡すことで、テンプレート上で使用可能となります。

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("template.html", add=self.add)

    def add(self, x, y):
        return x + y

実アプリケーションを構築する際にはテンプレート継承といったTornadoテンプレートのすべての機能を利用したくなることでしょう。詳しくは templateモジュール の章に記載してあります。

Tornadoのテンプレートエンジンによって、Tornadoテンプレートは直接Pythonに変換されます。テンプレートに書かれた式は逐一Python関数としてコピーされます。 Tornadoのテンプレート言語は他のテンプレート言語とは異なりテンプレート上であらゆる式を書くことが可能で、明確な意味で柔軟性を実現します。 逆にその自由さがあるために、テンプレート上で書いた式があらゆるPythonのエラーを引き起こす可能性があることに注意してください。

クッキーと、安全なクッキー

ユーザのブラウザにクッキーを残したい場合は set_cookie() メソッドを用います:

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        if not self.get_cookie("mycookie"):
            self.set_cookie("mycookie", "myvalue")
            self.write("Your cookie was not set yet!")
        else:
            self.write("Your cookie was set!")

クッキーは悪意のあるクライアントによって容易に偽装されてしまいます。例えば現在ログインしているユーザのユーザIDを保存するためにクッキーをセットしたい場合は、偽造を防ぐためにあなたのクッキーを署名する必要があります。Tornadoではインストール直後でもset_secure_cookie()get_secure_cookie()メソッドを用いることでこれを実現できます。これらのメソッドを用いるにはアプリケーションを構築する際にcookie_secretという秘密鍵を指定する必要があります。これはアプリケーション設定内でキーワード引数としてアプリケーションに渡すことができます。

application = tornado.web.Application([
    (r"/", MainHandler),
], cookie_secret="61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=")

署名済みクッキーにはタイムスタンプとHMAC署名(日本語)に加えてクッキーのエンコードされた値が含まれています。もしクッキーが古いあるいは署名が適合しなければ、get_secure_cookie()メソッドがあたかもクッキーがセットされていないかのように None を返します。上記の例を安全なクッキーとして設定する場合は以下のようなコードになります。

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        if not self.get_secure_cookie("mycookie"):
            self.set_secure_cookie("mycookie", "myvalue")
            self.write("Your cookie was not set yet!")
        else:
            self.write("Your cookie was set!")

ユーザ認証

認証済みのユーザは、リクエストハンドラ内では self.current_user として、テンプレート内では current_user としてそれぞれ利用することができます。デフォルトでは current_userNone です。

アプリケーション内でユーザ認証を実装するには、例えばクッキーの値をもとにユーザを断定するには、リクエストハンドラ内で get_current_user() メソッドをオーバーライドする必要があります。下記の例では、ユーザがクッキー内に記録されているニックネームを用いてアプリケーションにログインする方法を示してします。

class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        return self.get_secure_cookie("user")

class MainHandler(BaseHandler):
    def get(self):
        if not self.current_user:
            self.redirect("/login")
            return
        name = tornado.escape.xhtml_escape(self.current_user)
        self.write("Hello, " + name)

class LoginHandler(BaseHandler):
    def get(self):
        self.write('<html><body><form action="/login" method="post">'
                   'Name: <input type="text" name="name">'
                   '<input type="submit" value="Sign in">'
                   '</form></body></html>')

    def post(self):
        self.set_secure_cookie("user", self.get_argument("name"))
        self.redirect("/")

application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/login", LoginHandler),
], cookie_secret="61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=")

Pythonデコレータtornado.web.authenticated()を用いてログイン済みユーザのリクエストのみを処理するコードを書くことができます。もしリクエストがこのデコレータが付いたメソッドまで達したときにユーザがログインしていなかったら、リクエストは[ login_urlにリダイレクトされます。(login_urlは別途アプリケーション設定を行います)上記サンプルは以下のように書き換えることができます:

class MainHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        name = tornado.escape.xhtml_escape(self.current_user)
        self.write("Hello, " + name)

settings = {
    "cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
    "login_url": "/login",
}

application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/login", LoginHandler),
], **settings)

もしpost()メソッドがauthenticated()デコレータ付きで実装されていて、ユーザがログインしていなかった場合、サーバは403レスポンスを返します。

TornadoはGoogle OAuthのようなサードパーティの認証方式もビルトインサポートしています。詳細はauth moduleを参照して下さい。ユーザ認証を用いたアプリケーションの例を確認したい場合はTornadoブログをご覧ください。(なおMySQLにユーザデータを保存する例も記載されています。)

クロスサイトリクエストフォージェリからの保護

クロスサイトリクエストフォージェリ(XSRF) (日本語)は、ウェブアプリケーションにおける一般的な問題です。XSRFがどの様な悪さをするのかは、Wikipediaの当該ページを参照してください。

一般的なXSRFに対する防衛策としては、ユーザ毎に予測できない値をクッキーとして格納し、ウェブサイトへのフォームの送信ごとにその値を追加の引数として入れるということが行われます。もしクッキーの値と、送信されたフォームの値が異なったら、そのリクエストはニセ者であるとみなします。

Tornadoは、XSRFプロテクション機能を持っています。アプリケーション設定内で xsrf_cookies を有効にする事であなたのサイトでXSRFプロテクションを利用する事ができます:

settings = {
    "cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
    "login_url": "/login",
    "xsrf_cookies": True,
}

application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/login", LoginHandler),
], **settings)

xsrf_cookies が設定されていると、Tornadoウェブアプリケーションは、 _xsrf クッキーをすべてのユーザにセットします。 そして、正式な _xsrf クッキーを持たないすべてのPOSTリクエストを拒否します。 もし、この設定を有効にした場合には、すべてのformのsubmit操作時に _xsrf 値を付加する必要があります。 xsrf_from_html() をテンプレート内のフォームに適用する事で、 _xsrf 値を付加する事ができます:

<form method="/login" method="post">
  {{ xsrf_form_html() }}
  <div>Username: <input type="text" name="username"/></div>
  <div>Password: <input type="password" name="password"/></div>
  <div><input type="submit" value="Sign in"/></div>
</form>

もし、AJAXのPOSTリクエストを行う場合には、リクエスト毎に _xsrf 値をJavascriptで埋め込む必要があります。 FriendFeedで使用している jQuery を利用して自動で_xsrf値を付加するサンプルを以下に示します:

function getCookie(name) {
    var r = document.cookie.match("¥¥b" + name + "=([^;]*)¥¥b");
    return r ? r[1] : undefined;
}

jQuery.postJSON = function(url, args, callback) {
    args._xsrf = getCookie("_xsrf");
    $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST",
        success: function(response) {
        callback(eval("(" + response + ")"));
    }});
};

静的ファイルと積極的なファイルキャッシュ

Tornadoで静的ファイルを提供するにはアプリケーション設定で static_path を指定する必要があります:

settings = {
    "static_path": os.path.join(os.path.dirname(__file__), "static"),
    "cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
    "login_url": "/login",
    "xsrf_cookies": True,
}

application = tornado.web.Application([
  (r"/", MainHandler),
  (r"/login", LoginHandler),
], **settings)

この設定では /static/ で始まるすべてのリクエストを自動的に静的なディレクトリからの’serve’とすることができます。例えば http://localhost:8888/static/foo.png というURLの場合は foo.png というファイルを指定された静的ディレクトリから提供します。また /robots.txt/favicon.ico も静的ディレクトリから自動的に配信されます。(たとえURLが /static から始まらなくても)

パフォーマンス向上の定石として、静的リソースをブラウザが積極的にキャッシュするという一般的な方法があります。これによってブラウザはページのレンダリングを妨げる不必要な If-Modified-SinceEtag リクエストを送信しなくなります。

この機能を利用するには、テンプレートの中で静的ファイルのURLをHTML内で直接記述するのではなく static_url() メソッドを使用します。

<html>
   <head>
      <title>FriendFeed - {{ _("Home") }}</title>
   </head>
   <body>
     <div><img src="{{ static_url("images/logo.png") }}"/></div>
   </body>
 </html>

static_url() メソッドは相対パスを /static/images/logo.png?v=aae54 というようなURIに変換します。 v という引数は logo.png というファイルの中身に対するハッシュであり、この引数によりTornadoサーバはユーザのブラウザがコンテンツを区別してキャッシュするようにキャッシュヘッダを送信します。

v 引数はファイルの中身に基づいているため、もしファイルをアップデートしてサーバを再起動したら、Tornadoサーバは新しい値を持った v を送信します。これによって、ユーザのブラウザは自動的に新しいファイルを取得します。もしファイルの中身が変わっていなければ、ブラウザはサーバ上のファイルが更新されたか確認することなく、ローカルにキャッシュされたファイルのコピーを使用します。これによりレンダリングのパフォーマンスは劇的に改善されます。

アプリケーション公開時には nginx のような、より最適化されたファイルサーバから静的ファイルを配信したくなるでしょう。たいていのウェブサーバではこのようなキャッシュ動作をサポートしています。たとえばFriendFeedで行っているnginxの設定は下記のようになります:

location /static/ {
    root /var/friendfeed/static;
    if ($query_string) {
        expires max;
    }
 }

多言語化

ユーザのロケールは、ユーザがログインしているかどうかに関わらず、リクエストハンドラの self.locale やテンプレートの locale で取得できます。

ロケールの名前(en_USなど)は locale.name で取得できます。また、 locale.translate() メソッドを使用することで翻訳を行うことができます。

テンプレート内ではグローバル関数 _() を翻訳に使うことができます。この関数は2通りの使い方があります:

_("Translate this string")

この呼び方では文字列を現在のロケールに基づいて翻訳します。

_("A person liked this", "%(num)d people liked this", len(people)) % {"num": len(people)}

この呼びかたでは、単数と複数で異なった形を取る文字列を第三引数の値に基づいて翻訳することができます。

上記の例では len(people) が1の時には最初の文字列が返され、それ以外の場合には二番目の文字列が返されます。

翻訳文で変数を使う場合はPythonの名前付きプレースホルダー(上記の例では %(num)d)を使うのが一般的です。これはプレースホルダーを翻訳文の好きな位置に置けるようにするためです。

適切に多言語化されたテンプレートの例を下に示します:

<html>
   <head>
      <title>FriendFeed - {{ _("Sign in") }}</title>
   </head>
   <body>
     <form action="{{ request.path }}" method="post">
       <div>{{ _("Username") }} <input type="text" name="username"/></div>
       <div>{{ _("Password") }} <input type="password" name="password"/></div>
       <div><input type="submit" value="{{ _("Sign in") }}"/></div>
       {{ xsrf_form_html() }}
     </form>
   </body>
 </html>

デフォルトでは、ユーザのブラウザが送る Accept-Language ヘッダの値をユーザのロケールを判断します。適切な値の Accept-Language ヘッダが見つからない場合は en_US を使います。ユーザにロケールを設定させる場合は、リクエストハンドラの get_user_locale() メソッドをオーバーライドすることでこの挙動を上書きすることができます。

class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        user_id = self.get_secure_cookie("user")
        if not user_id: return None
        return self.backend.get_user_by_id(user_id)

    def get_user_locale(self):
        if "locale" not in self.current_user.prefs:
            # Use the Accept-Language header
            return None
        return self.current_user.prefs["locale"]

get_user_locale() メソッドの返り値が None の場合には、 Accept-Language ヘッダの値に基づいてロケールを決定します。

tornado.locale.load_translations() メソッドで、すべての翻訳ファイルをロードすることができます。このメソッドは翻訳ファイルが入っているディレクトリ名を引数に取ります。翻訳ファイルはロケールの名前に基づいた名前(例: es_GT.csv, fr_CA.csv)のCSVファイルです。このメソッドはCVSファイルから翻訳文をロードし、各CVSファイルの有無を元にどのロケールがサポートされているかを決定します。通常はこのメソッドは main() メソッドの中で一度だけ呼びます。

def main():
    tornado.locale.load_translations(
        os.path.join(os.path.dirname(__file__), "translations"))
    start_server()

サポートされているロケールの一覧は tornado.locale.get_supported_locales() で取得できます。ユーザのロケールは、サポートされているロケールの中で最も近いものが選ばれます。例えばユーザのロケールが es_GT で、 es ロケールがサポートされている場合、そのリクエストの self.localees になります。近い名前が見つからない場合は en_US になります。

CSVファイルのフォーマットや他の他言語化の方法についての詳細は localeモジュール のドキュメントを参照してください。

ユーザインタフェースモジュール

Tornadoではアプリケーション全体で標準的で再利用可能なユーザインタフェースモジュールを簡単に利用しやすくするために、ユーザインタフェースモジュールを提供しています。ユーザインタフェースモジュールはウェブページ内のコンポーネントをレンダリングするための特別な関数呼び出しのようなもので、それぞれ独自のCSSとJavaScriptとともに提供されます。

たとえばあなたがブログを実装しているとして、ブログエントリがブログのホームページとそれぞれのエントリページの両方に表示されるようにしたいときに、Entryモジュールを両方のページをレンダリングするように実装することができます。まず、あなたのユーザインタフェースモジュールに uimodules.py のようなPythonモジュールを作成します:

class Entry(tornado.web.UIModule):
    def render(self, entry, show_comments=False):
        return self.render_string(
            "module-entry.html", show_comments=show_comments)

Tornadoにアプリケーションでui_modulesという設定を使用してuimodules.pyを利用するように設定します:

class HomeHandler(tornado.web.RequestHandler):
    def get(self):
        entries = self.db.query("SELECT * FROM entries ORDER BY date DESC")
        self.render("home.html", entries=entries)

class EntryHandler(tornado.web.RequestHandler):
    def get(self, entry_id):
        entry = self.db.get("SELECT * FROM entries WHERE id = %s", entry_id)
        if not entry: raise tornado.web.HTTPError(404)
        self.render("entry.html", entry=entry)

settings = {
    "ui_modules": uimodules,
}

application = tornado.web.Application([
    (r"/", HomeHandler),
    (r"/entry/([0-9]+)", EntryHandler),
], **settings)

home.htmlの中でHTMLを直接記述するのではなく、Entryモジュールを参照します。

{% for entry in entries %}
  {{ modules.Entry(entry) }}
{% end %}

entry.htmlの中でエントリの拡張されたフォームが表示されるようにEntryモジュールをshow_comments引数とともに参照します。

{{ modules.Entry(entry, show_comments=True) }}

モジュールでは embedded_css(), embedded_javascript(), javascript_file(), css_file() メソッドを各々オーバライドすることによりカスタムのCSSとJavaScriptを取り込むことができます:

class Entry(tornado.web.UIModule):
    def embedded_css(self):
        return ".entry { margin-bottom: 1em; }"

    def render(self, entry, show_comments=False):
        return self.render_string(
            "module-entry.html", show_comments=show_comments)

ノンブロッキング, 非同期リクエスト

リクエストハンドラが実行されたとき、リクエストは自動的に終了します。TornadoはノンブロッキングI/Oスタイルを使用するため、もしメインリクエストハンドラメソッドが値を返したあとに、リクエストを開けたままにしたい場合は tornado.web.asynchronous() デコレータを用いてデフォルトの振舞いをオーバーライドすることができます。

このデコレータを用いる場合は、HTTPリクエストを終了する場合は self.finish() メソッドをきちんと呼んであげなければいけません。そうしないとユーザのブラウザがハングしてしまいます:

class MainHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        self.write("Hello, world")
        self.finish()

ここでTornadoのビルトイン非同期HTTPクライアントを用いてFriendFeed APIを呼び出す実際の例をご紹介します:

class MainHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        http = tornado.httpclient.AsyncHTTPClient()
        http.fetch("http://friendfeed-api.com/v2/feed/bret",
                   callback=self.async_callback(self.on_response))

    def on_response(self, response):
        if response.error: raise tornado.web.HTTPError(500)
        json = tornado.escape.json_decode(response.body)
        self.write("Fetched " + str(len(json["entries"])) + " entries "
                   "from the FriendFeed API")
        self.finish()

get() メソッドが値を戻しても、リクエストは終わっていません。いずれまたHTTPクライアントが on_response() メソッドを呼び出し、リクエストがまだ開いていたとき、レスポンスは最終的には self.finish() を呼び出すことでクライアントにフラッシュされます。

もしコールバックを要求するような非同期ライブラリ関数を呼び出す場合は、コールバックを self.async_callback で常にラップすべきです。(例えば上の例でのHTTPフェッチ関数のような場合です)このシンプルなラッパーを用いることによって、コールバック関数が例外を発生させた場合あるいはプログラミングエラーがあった場合に、適切なHTTPエラーレスポンスがブラウザに送信され、コネクションが適切に閉じられることが確実になります。

さらに上級的な例を参考したい場合は、ロングポーリングを用いてAJAXのチャットルームを実装した例を見てください。

サードパーティ認証

Tornadoの認証モジュールは、いくつかのメジャーなWebサービスの認証と承認に対応しています。サービスは、Google/Gmail、Facebook、Twitter、Yahoo、FriendFeedが利用出来ます。このモジュールを使う事で、これらのサイトに、認証済みのアクセスを出来ます。例えばあなたのアドレスブックに載っている友達のTwitterのメッセージをダウンロードすることができます。

参考にグーグルの認証を使用したサンプルを紹介します。 このサンプルは、継続的なアクセスを行うために、グーグルの認証済みクッキーを保存します:

class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
    @tornado.web.asynchronous
    def get(self):
        if self.get_argument("openid.mode", None):
            self.get_authenticated_user(self.async_callback(self._on_auth))
            return
        self.authenticate_redirect()

    def _on_auth(self, user):
        if not user:
            self.authenticate_redirect()
            return
        # set_secure_cookie() などを使用してユーザを保存します。

更に詳しい情報は、認証モジュール(auth module)のドキュメントを参照してください。