以前、
を自作しました。
これらの自作により、「Webサーバを作りながら学ぶ 基礎からのWebアプリケーション開発入門」の第3章以降もPythonで書く見通しが立ちました。
第3章以降では、URLルーティングやクッキー・セッションなどの機能が登場します。
ただ、それらの機能を
のどちらの形で実装するか悩みました。
それでも、せっかくなので、後者のWSGIフレームワークを自作してみようと考えました。
そこで今回、URLルーティング機能だけがあるWSGIフレームワークを自作してみました。
なお、学習用途で作ったため、セキュリティ面は考えていませんのであしからず。
目次
- 環境
- 復習:単純なWSGIアプリケーション
- URLルーティング機能を作る(関数版)
- URLルーティング機能を作る(クラス版)
- フレームワーク化
- HTMLテンプレートやCSS、画像の配信
- ソースコード
- 参考
環境
- Windows10 x64
- Python 3.5.2 32bit
なお、WSGI準拠のWebサーバは自作したものを使うため、server.py
として保存しておきます。
wsgi_webserver-sample/multi_response_wsgi_server.py at master · thinkAmi-sandbox/wsgi_webserver-sample
復習:単純なWSGIアプリケーション
まずは復習として、単純なWSGIアプリケーションを作ってみます。
no1_base_app.py
def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"Hello, world."]
動作確認として、
(env) >python server.py no1_base_app:application
を実行し、ブラウザでhttp://localhost:8888/
へアクセスすると、ブラウザ上に 「Hello, world」 が表示されました。
URLルーティングは実装していないため、http://localhost:8888/hoge
のようなURLでも、「Hello, world」が表示されます。
URLルーティング機能を作る(関数版)
続いて、URLルーティング機能を持ったアプリを作ります。
ただ、最初からデコレータを使ってURLルーティング機能を実装すると、自分の理解が追いつかないかもしれないと考えました。
そこで、その代わりとなる方法を調べてみたところ、以下の記事がありました。
Serving Static Files and Routing with WSGI
この記事の方法が分かりやすかったため、no2_router_separately_by_func.py
として実装してみます。
まずはURLルーティングのためのタプルのリストを作ります。
routes = [ ('/', get), ('/hoge', hoge), ]
その内容は
- タプルは2つの要素を持つ
- ひとつ目の要素は、対象のURLパス
- ふたつ目の要素は、対象のURLパスにアクセスした時に呼ばれる関数
です。
続いて、WSGIサーバから呼ばれる関数を用意します。
def application(environ, start_response): for path, func in routes: if path == environ['PATH_INFO']: return func(environ, start_response) return not_found(environ, start_response)
タプルを順次読み込み、リクエストのパス(environ['PATH_INFO']
)と一致した場合に、タプルで指定した関数を呼び出しています。
あとは、application関数から呼び出される関数を
def get(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"Hello, world."] def hoge(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"hoge"] def not_found(environ, start_response): start_response('404 Not Found', [('Content-Type', 'text/plain')]) return [b"Not found."]
と用意して完成です。
動作確認として、
(env) >python server.py no2_router_separately_by_func:application
を実行し、ブラウザでアクセスすると以下のようになります。
アクセス先のURL | 表示内容 |
---|---|
http://localhost:8888/ | Hello, world |
http://localhost:8888/hoge | hoge |
http://localhost:8888/fuga | Not found. |
URLルーティング機能を作る(クラス版)
URLルーティング機能を、関数版からクラス版へと変更してみます。
no3_router_separately_by_class.py
class MyWSGIApplication(object): def __init__(self): self.routes = [ ('/', self.get), ('/hoge', self.hoge), ] def get(self, environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"Hello, world by class."] def hoge(self, environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"hoge by class"] def not_found(self, environ, start_response): start_response('404 Not Found', [('Content-Type', 'text/plain')]) return [b"Not found by class."] def __call__(self, environ, start_response): for path, method in self.routes: if path == environ['PATH_INFO']: return method(environ, start_response) return self.not_found(environ, start_response) app = MyWSGIApplication()
動作確認として、
(env) >python server.py no3_router_separately_by_class:app
として、ブラウザでアクセスしても、末尾に「 by class.」が付くだけで、それ以外の動作は変わっていません。
フレームワーク化
次は
の2つに機能を分割して、それぞれを実装します。
WSGIフレームワークへno3_router_separately_by_class.py
からURLルーティング機能を移植します。
no4_1_framework.py
class MyWSGIFramework(object): def __init__(self, routes): self.routes = routes def not_found(self, environ, start_response): start_response('404 Not Found', [('Content-Type', 'text/plain')]) return [b"Not found by framework."] def __call__(self, environ, start_response): for path, method in self.routes: if environ['PATH_INFO'] == path: return method(environ, start_response) return self.not_found(environ, start_response)
WSGIアプリはURLルーティング用のタプルのリストと、URLルーティングされた時に呼ばれる関数だけになります。
no4_2_app.py
from no4_1_framework import MyWSGIFramework def get(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"Hello, world by framework."] def hoge(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"hoge by framework"] app = MyWSGIFramework([ ('/', get), ('/hoge', hoge), ])
動作確認として、
(env) >python server.py no4_2_app:app
として、ブラウザでアクセスしても、末尾に「by framework」が付くだけで、それ以外の動作は変わっていません。
HTMLテンプレートやCSS、画像の配信
最後に、Jinja2を使ってHTMLテンプレートやCSS・画像を表示できるようにします。
CSSや画像の出力については定型的な処理のため、今回はフレームワーク側で処理します。
画像ファイルについては、自作WSGIサーバではsocket.sendfile()
を使って送信しています。そのため、画像ファイルをファイルライクオブジェクトの形にして自作WSGIサーバに渡す必要があります。
今回は、バイナリモードでファイルを読み込み、io.BytesIO
にてバイナリストリームとすることで、ファイルライクオブジェクトの形にしています*1。
16.2. io — ストリームを扱うコアツール — Python 3.5.1 ドキュメント
fullpath = './{}'.format(environ['PATH_INFO']) with open(fullpath, "rb") as f: r = f.read() binary_stream = io.BytesIO(r) # 変数content_typeには、ファイルの種類により'text/css'か'image/png'が設定済 start_response('200 OK', content_type) return binary_stream
それを受けて、自作WSGIサーバ(server.py
)では、
if 'image' in str_response: # 画像データの場合 self.connection.sendall(str_response.encode('utf-8')) self.connection.sendfile(byte_response_body) else: # 画像データ以外の場合 for byte_body in byte_response_body: str_response += byte_body.decode('utf-8') self.connection.sendall(str_response.encode('utf-8'))
のような形で、クライアントへデータを送信しています。
なお、PEP3333にはファイルの扱いに関しての記述もありましたが、今回は置いておきます。
PEP 3333: Python Web Server Gateway Interface v1.0.1 — knzm.readthedocs.org 2012-12-31 documentation
no5_1_framework.py
import re import io class MyWSGIFramework(object): def __init__(self, routes, css_dir=None, img_dir=None): self.routes = routes # 静的ファイル向けのディレクトリ設定 self.static_dir = '/static' css_dir = css_dir if css_dir else "/css/" self.css_dir = '{static}{css}'.format(static=self.static_dir, css=css_dir) img_dir = img_dir if img_dir else "/images/" self.img_dir = '{static}{img}'.format(static=self.static_dir, img=img_dir) def static(self, environ, start_response): content_type = [] if re.match(self.css_dir, environ['PATH_INFO']): content_type.append(('Content-Type', 'text/css')) elif re.match(self.img_dir, environ['PATH_INFO']): content_type.append(('Content-Type', 'image/png')) else: return self.not_found(environ, start_response) try: fullpath = './{}'.format(environ['PATH_INFO']) with open(fullpath, "rb") as f: r = f.read() binary_stream = io.BytesIO(r) except: start_response('500 Internal Server Error', [('Content-Type', 'text/plain')]) return [b"Internal server error"] start_response('200 OK', content_type) return binary_stream def not_found(self, environ, start_response): start_response('404 Not Found', [('Content-Type', 'text/plain')]) return [b"Not found."] def __call__(self, environ, start_response): if re.match(self.static_dir, environ['PATH_INFO']): return self.static(environ, start_response) for path, method in self.routes: if environ['PATH_INFO'] == path: return method(environ, start_response) return self.not_found(environ, start_response)
となりました。
また、WSGIアプリは、
no5_2_app.py
# python server.py no5_2_app:app from no5_1_framework import MyWSGIFramework from jinja2 import Environment, FileSystemLoader def get(environ, start_response): # (省略) def hoge(environ, start_response): # (省略) def index(environ, start_response): jinja2_env = Environment(loader=FileSystemLoader('./templates', encoding='utf8')) template = jinja2_env.get_template('index.html') html = template.render({'messages': ['hoge', 'fuga', 'piyo']}) start_response('200 OK', [('Content-Type', 'text/html')]) return [html.encode('utf-8')] app = MyWSGIFramework([ ('/', get), ('/hoge', hoge), ('/index', index), ])
となりました。
動作確認として、
(env) >python server.py no5_2_app:app
として、ブラウザでhttp://localhost:8888/index
へとアクセスすると、HTMLテンプレートやCSS、画像が表示されました。
ソースコード
GitHubに上げました。routing_only
ディレクトリの下が、今回のサンプルです。
wsgi_framework-sample/routing_only at master · thinkAmi-sandbox/wsgi_framework-sample
参考
- nasa9084/sakenet
- aiohttpを用いたWebフレームワークmeadを作った - minamorl.com
- python-advent-calendar2012/wsgi.rst at master · shomah4a/python-advent-calendar2012
- Web Server Gateway Interface — Fundamentals of Web Programming
もし、FlaskやBottleのようにデコレータを使ったURLルーティング機能を実装する場合には、以下のBottleのソースコード解説も参考になるかもしれません。
Python Bottleのソースを読む ルータ編 - TAISA BLOG
2016/9/28 追記 ここから
PyConJP2016にてWSGIフレームワークの作り方に関する発表がありました。
#pyconjp でWeb(WSGI)フレームワークの作り方について話してきました - c-bata web
以前kobinのソースコードを読んだとき、ルーティング・リクエスト・レスポンスについて「なぜこのような形で実装したのか」をつみきれないところがありました。
上記の資料ではそれに対する回答が記載されていたため、スッキリするとともにとても参考になりました。ありがとうございました。
2016/9/28 追記 ここまで
*1:この辺りの説明が正しいかどうか分からないため、誤りがあればご指摘いただけるとありがたいです