WSGIとPythonでスマートなWebアプリケーション開発を

第3回WSGIミドルウェアの作成

はじめに

前回は、メッセージボードアプリケーションを作成し、実際のWSGIアプリケーションについて見てきました。 WSGI を使用した Web アプリケーションの作成がどのようなものかわかって頂けたかと思います。

今回は、 WSGIの仕組みの中でも重要な、ミドルウェアという概念を取り上げます。⁠ミドルウェア」はアプリケーションの機能をコンポーネント化し、コードの再利用性を高めるための概念です。このように書くと、少し難しいもののように思えるかもしれませんが、その仕組みは非常に簡単なものです。

まずは、⁠ミドルウェア」の概念について説明します。

ミドルウェアとは

ミドルウェアとは、サーバとWSGIアプリケーションの両方のインターフェースを持つオブジェクトです。そのため、Webサーバ側からはWSGIアプリケーションのように見え、WSGIアプリケーション側からはWebサーバのように見えます。ミドルウェアは図1のようにサーバとアプリケーションの中間に位置し、Webサーバからのリクエストに対する処理と、アプリケーションからのレスポンスに対する処理を行います。

たとえば

  • リクエストURLに応じたアプリケーションのマッピング
  • ユーザ認証
  • レスポンスヘッダへの情報追加
  • アプリケーションで生成された動的コンテンツのキャッシュ

といったような処理です。

図1 ミドルウェアの動作概念
図1 ミドルウェアの動作概念

図1では、WebサーバとWSGIアプリケーションの間にミドルウェアの層が幾つも重なっています。ミドルウェアは、それ自身がWSGIアプリケーションのように振る舞うため、⁠ミドルウェアが呼び出したアプリケーションがミドルウェアで、そのミドルウェアが呼び出したアプリケーションがまたミドルウェアで…」と最終的にWSGIアプリケーションが呼び出されるまで続きます。このミドルウェアの連鎖をミドルウェアスタックといいます。

実際のミドルウェア

ここで、簡単なミドルウェアを例に、処理の流れを追ってみます。

今回は、

  • 何もしないミドルウェア
  • URLマッピングを行うミドルウェア

の2種類です。

今回使用するサンプルのソースはmiddleware.zip(ZIP圧縮)にあります。適宜ダウンロードしてください。

今回は、第1回第2回で使用したsample1.pyとmessage.pyを使用しています。middleware.zipにはそれらも含まれているので、展開して同じディレクトリに置いてください。

実行は前回と同様に

$ python middleware.py

とします。

実行した後に、http://localhost:8080/の/helloと/messageにアクセスすると、それぞれ第1回、第2回で作成したアプリケーションが実行されます。それ以外のURLにアクセスした場合は、404エラーが返ってきます。

図2 /hello にアクセスしたとき
図2 /hello にアクセスしたとき
図3 /message にアクセスしたとき
図3 /message にアクセスしたとき
図4 それ以外にアクセスしたとき
図4 それ以外にアクセスしたとき

何もしないミドルウェアも使用していますが、文字通り何もしないので変化がなく、ブラウザからアクセスしても変化はわかりません。確認はソース上のみで可能です。

それでは、それぞれのミドルウェアのソースを見ていきます。

何もしないミドルウェア

最初は、何もしないミドルウェアです。

リスト1 何もしないミドルウェアNopクラス
class Nop(object):
    ''' 何もしないミドルウェア (クラス版) '''
    
    def __init__(self, application):
        self.application = application


    def __call__(self, environ, start_response):
        return self.application(environ, start_response)

ミドルウェアの基本的な処理は、コンストラクタで呼び出すアプリケーションを受け取り、自身がアプリケーションとしてサーバ側から呼び出された時に、コンストラクタで受け取ったアプリケーションを呼び出す、というものです。

何もしないミドルウェアは、以下のように使用します。

リスト2 ミドルウェアの使い方
>>> application = Nop(application)

このようにしておくと、 WebサーバからNopの__call__メソッドが呼び出され、その中でコンストラクタで渡したアプリケーションが呼び出されます。

URLマッピングを行うミドルウェア

次は、実際に機能するミドルウェアの例として、URLマッピングを行うミドルウェアを扱います。具体的には、アクセスされたURLごとにアプリケーションを割り当てるような処理を行います。

たとえば、WSGIアプリケーションにアクセスするための URLがhttp://somedomain.jp/であったとき、http://somedomain.jp/path1http://somedomain.jp/path2で別のアプリケーションに処理を割り振る、といったことができます。

リスト3 URLマッピングを行うミドルウェア
class SelectApp(object):
    ''' パスによるアプリケーション振り分けを行う '''

    def __init__(self, table, notfound=notFound):

        # パスは長い順にマッチさせたいので、あらかじめソートしておく
        tmp = sorted(table, key=lambda x:len(x), reverse=True)

        # 扱いやすいように、タプルのリストにしておく
        table = [(x, table[x]) for x in tmp]

        self.table = table

        # 割り振るパスが見つからなかったときに呼び出すアプリケーション
        self.notfound = notfound


    def __call__(self, environ, start_response):
        ''' リクエストのパスを見て振り分ける '''

        name = 'SCRIPT_NAME'
        info = 'PATH_INFO'

        scriptname = environ.get(name, '')
        pathinfo = environ.get(info, '')

        for p, app in self.table:

            if p == '' or p == '/' and pathinfo.startswith(p):
                return app(environ, start_response)

            # 同じパスならそのまま
            # 同じパスで始まっていて、その後にスラッシュがある
            if pathinfo == p or pathinfo.startswith(p) and \
                    pathinfo[len(p)] == '/':

                scriptname = scriptname + p
                pathinfo = pathinfo[len(p):]

                # リクエスト情報を書き換える
                environ[name] = scriptname
                environ[info] = pathinfo

                return app(environ, start_response)

        return self.notfound(environ, start_response)

このミドルウェアは、以下のように使用します。

リスト4 URLマッパの使い方
>>> application = SelectApp({'/path1':app1, '/path2':app2})

こうしておくと、/path1へのアクセスはapp1に、/path2へのアクセスはapp2に振り分けられるようになります。

それでは実装の解説に入ります。

SelectAppのコンストラクタでは、2つの引数を取っています。tableは、リスト4でコンストラクタに渡しているように、割り当てるURLをキーに、呼び出すアプリケーションを値に持つ辞書オブジェクトです。notfoundは、割り当てるURLが存在しなかったときに呼び出すアプリケーションです。デフォルトでは404を返すアプリケーションが使われます。割り当てテーブルは、キーであるURLの長い順にマッチさせるため、キーと値のタプルをキーが長い順に並べ替えたリストを生成して保持しておきます。

__call__ メソッドでは、リクエストされたURLごとにアプリケーションに処理を割り振ります。environ辞書の中で使用するのは、SCRIPT_NAMEとPATH_INFOです。

SCRIPT_NAMEはURL上でアプリケーションのルートがどの位置にあるかを表します。たとえば、http://localhost/app/pathにあるならば、 SCRIPT_NAME は "/app/path" が入っています。

PATH_INFO は、アプリケーション上の仮想的な URL を表しています。http://localhost/app/pathがアプリケーションのある位置で、http://localhost/app/path/extraとアクセスした場合、PATH_INFOには"/extra"が入っています。

振り分けは、PATH_INFOとコンストラクタで受け取ったテーブルのパスとを1つずつマッチさせて調べます。その際に、 Pythonの文字列型のメソッドstartswithを使用しています。これでpathinfoの文字列の先頭がpであるかどうかを調べています。

ただし、startswithだけでは'/path'の振り分けに'/path/path2'だけでなく'/pathother'なども含まれてしまいます。なので、 PATH_INFO の文字列の長さがテーブルのパスよりも長い場合は、 pathinfo[len(p)] が / になっているかどうかを調べています。

判定で一致した場合は、 PATH_INFOの先頭からlen(p)分だけ文字を取り除き、SCRIPT_NAMEの末尾にpをそのまま加えて、environ辞書を更新したうえでアプリケーションを呼び出します。こうしておくことで、SelectAppを再帰的に適用して振り分けを行うことができます。

すべてのパスに一致しない場合は、notfoundアプリケーションを呼び出して、そのままreturnします。

まとめ

以上が WSGI アプリケーションにおけるミドルウェアの仕組みです。

このように、機能ごとに完全に独立したミドルウェアを作成し、それらを組み合わせて1つのアプリケーションとすることができるのがミドルウェアの利点です。

このようなミドルウェアは、すでにライブラリとして提供されているものもあります。代表的なミドルウェアライブラリにはpasteがあります。pasteには、今回作成したURLマッパと同機能のミドルウェアや、 gzip圧縮をサポートするミドルウェアなどが存在します。これからWSGIアプリケーションを作成する方は利用してみてください。

今回までで、WSGIの基本的な仕組みの説明は終わりです。

次回は、実際にWSGIアプリケーションを動かすサーバとしてGoogle App Engine(以下GAE)を取り上げます。GAEはGoogleが提供するWebアプリケーションサーバです、GAEを利用すると、自分が作ったWebアプリケーションをGoogleのサーバ上で動かすことができます。作成したアプリケーションを非常に簡単にインターネット上で公開できるので、ぜひ試していただきたいサービスです。

おすすめ記事

記事・ニュース一覧