以前、
を自作しました。
これらの自作により、「Webサーバを作りながら学ぶ 基礎からのWebアプリケーション開発入門」の第3章以降もPythonで書く見通しが立ちました。
gihyo.jp
第3章以降では、URLルーティングやクッキー・セッションなどの機能が登場します。
ただ、それらの機能を
のどちらの形で実装するか悩みました。
それでも、せっかくなので、後者のWSGIフレームワークを自作してみようと考えました。
そこで今回、URLルーティング機能だけがあるWSGIフレームワークを自作してみました。
なお、学習用途で作ったため、セキュリティ面は考えていませんのであしからず。
目次
環境
- 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ルーティング機能を作る(クラス版)
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)
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
ソースコード全体ですが、WSGIフレームワークは
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
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
参考
もし、FlaskやBottleのようにデコレータを使ったURLルーティング機能を実装する場合には、以下のBottleのソースコード解説も参考になるかもしれません。
Python Bottleのソースを読む ルータ編 - TAISA BLOG
2016/9/28 追記 ここから
PyConJP2016にてWSGIフレームワークの作り方に関する発表がありました。
#pyconjp でWeb(WSGI)フレームワークの作り方について話してきました - c-bata web
以前kobinのソースコードを読んだとき、ルーティング・リクエスト・レスポンスについて「なぜこのような形で実装したのか」をつみきれないところがありました。
上記の資料ではそれに対する回答が記載されていたため、スッキリするとともにとても参考になりました。ありがとうございました。
2016/9/28 追記 ここまで