PythonのWEBフレームワーク「tornado」を使ったWEBアプリケーションを作成してみます。
私見ですが、tornadoはresponderと同じようなframeworkなのですが、私の環境下において性能を比較するとtornadoのほうがよかったのと、responderはバージョンが比較的若く、実践では今しばらく様子見してからになるのではと考えています。
- 動作環境
カテゴリ | 値 |
---|---|
cpu | core i5-6200U 2.3GHz |
memory | 8GB |
os | windows10 home |
lang | Python 3.7.3 |
framework | tornado 6.0.3 |
database | PostgreSQL 10 |
database orm | momoko 2.2.5.1 |
- 性能比較
Hello World を表示するアプリをapache abで計測してみた。
ab -c 100 -n 1000 http://localhost:XXXX/
結果:
|framework|Tornado|aiohttp|Sanic|responder
|:--|:--|:--|:--|:--|:--
|Total transferred|207000|162000|110000|147000
|HTML transferred|12000|11000|11000|11000
|Requests per second|1292.11|688.33|488.17|534.83
|Time per request|77.393|145.279|204.847|186.974
|Time per request|0.774|1.453|2.048|1.87
|Transfer rate|261.2|108.9|52.44|76.78
かなり速い性能が出ています。私の環境では、aiohttpやSanicより速い。
0. install
pip install tornado
1. Hello World
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello world")
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
「ctl+c」でアプリが止まらない。
tornadoは、IPLoopを「ctl+c」で止めることができないので、対応しておく。
このサイトにあるやり方がいいと思いました。
Hello Worldを上記サイトを参考に変更する。
import tornado.ioloop
import tornado.web
import signal
from tornado.options import options
accect_ctlc = False
def signal_handler(signum, frame):
global accect_ctlc
accect_ctlc = True
def try_exit():
global accect_ctlc
if accect_ctlc:
tornado.ioloop.IOLoop.instance().stop()
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello world")
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
tornado.options.parse_command_line()
signal.signal(signal.SIGINT, signal_handler)
app = make_app()
app.listen(8888)
tornado.ioloop.PeriodicCallback(try_exit, 100).start()
tornado.ioloop.IOLoop.instance().start()
2.テンプレート
project-root
│ main.py
├─static
│ ├─css
│ │ bootstrap.min.css
│ │ bootstrap.min.css.map
│ │ main.css
│ │
│ └─js
└─templates
hello.html
layout.html
import os
import tornado.ioloop
import tornado.web
import signal
from tornado.options import options
from pathlib import Path
accect_ctlc = False
def signal_handler(signum, frame):
global accect_ctlc
accect_ctlc = True
def try_exit():
global accect_ctlc
if accect_ctlc:
tornado.ioloop.IOLoop.instance().stop()
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello world")
class StaticHandler(tornado.web.RequestHandler):
def get(self):
self.render("hello.html", username=" static")
def get_root_path():
return Path(__file__).resolve().parents[0]
def make_app():
BASE_DIR=get_root_path()
return tornado.web.Application([
(r"/", MainHandler),
(r"/static", StaticHandler),
],
static_path=os.path.join(BASE_DIR, "static"), #※1
template_path=os.path.join(BASE_DIR, "templates"), #※2
)
if __name__ == "__main__":
tornado.options.parse_command_line()
signal.signal(signal.SIGINT, signal_handler)
app = make_app()
app.listen(8888)
tornado.ioloop.PeriodicCallback(try_exit, 100).start()
tornado.ioloop.IOLoop.instance().start()
アプリケーションにスタティックパス(※1)、テンプレートパス(※2)をそれぞれ指定します。
<!DOCTYPE HTML>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<title>{% block title %}デフォルトタイトル{% end %}</title>
<link href="{{ static_url('css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ static_url('css/style.css') }}" rel="stylesheet">
</head>
<body>
<main>
{% block content %}
デフォルトコンテンツ
{% end %}
</main>
</body>
</html>
{% extends "layout.html" %}
{% block content %}
<h1>Welcome</h1>
<p>Hello <b>{{ username }}</b></p>
<br/>
{% end %}
3.JSON
import tornado.ioloop
import tornado.web
import signal
from tornado.options import options
import json
accect_ctlc = False
def signal_handler(signum, frame):
global accect_ctlc
accect_ctlc = True
def try_exit():
global accect_ctlc
if accect_ctlc:
tornado.ioloop.IOLoop.instance().stop()
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello world")
class JsonHandler(tornado.web.RequestHandler):
def get(self):
self.write(json.dumps({"jsontext":"Hello world"}))
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
(r"/json", JsonHandler),
],
)
if __name__ == "__main__":
tornado.options.parse_command_line()
signal.signal(signal.SIGINT, signal_handler)
app = make_app()
app.listen(8888)
tornado.ioloop.PeriodicCallback(try_exit, 100).start()
tornado.ioloop.IOLoop.instance().start()
4. user authentication
認証が必要なメソッドに「@tornado.web.authenticated」を追加します。
あとは、認証処理を実装するだけ。
(1) Project folder and file structure
project root
│ main.py
│
└─templates
login.html
import os
import tornado.ioloop
import tornado.web
import tornado.escape
import tornado.options
import signal
from pathlib import Path
accect_ctlc = False
def signal_handler(signum, frame):
global accect_ctlc
accect_ctlc = True
def try_exit():
global accect_ctlc
if accect_ctlc:
tornado.ioloop.IOLoop.instance().stop()
def get_root_path():
return Path(__file__).resolve().parents[0]
class BaseHandler(tornado.web.RequestHandler):
cookie_username = "username"
def get_current_user(self):
username = self.get_secure_cookie(self.cookie_username)
if not username: return None
return tornado.escape.utf8(username)
def set_current_user(self, username):
self.set_secure_cookie(self.cookie_username, tornado.escape.utf8(username))
def clear_current_user(self):
self.clear_cookie(self.cookie_username)
class MainHandler(BaseHandler):
@tornado.web.authenticated
def get(self):
user = self.get_current_user().decode()
self.write("Hello, <b>%s</b> <br> <a href=/auth/logout>Logout</a>" % user)
class AuthLoginHandler(BaseHandler):
def is_member(self, username, password):
return username == "user" and password == "pass"
def get(self):
self.render("login.html")
def post(self):
#csrf token check
self.check_xsrf_cookie()
username = self.get_argument("username")
password = self.get_argument("password")
if self.is_member(username, password):
#認証OKなら、セッションにusernameをセット
self.set_current_user(username)
self.redirect("/")
else:
#認証エラー
self.write_error(403)
class AuthLogoutHandler(BaseHandler):
def get(self):
#セッションのusernameをクリア
self.clear_current_user()
self.redirect('/')
class Application(tornado.web.Application):
def __init__(self):
BASE_DIR=get_root_path()
handlers = [
(r'/', MainHandler),
(r'/auth/login', AuthLoginHandler),
(r'/auth/logout', AuthLogoutHandler),
]
settings = dict(
cookie_secret='secret_key',
static_path=os.path.join(BASE_DIR, "static"),
template_path=os.path.join(BASE_DIR, "templates"),
login_url="/auth/login",
xsrf_cookies=True,
autoescape="xhtml_escape",
)
tornado.web.Application.__init__(self, handlers, **settings)
def make_app():
return Application()
if __name__ == "__main__":
tornado.options.parse_command_line()
signal.signal(signal.SIGINT, signal_handler)
app = make_app()
app.listen(8888)
tornado.ioloop.PeriodicCallback(try_exit, 100).start()
tornado.ioloop.IOLoop.instance().start()
<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>Login</h1>
<form action="/auth/login" method="post">
{% module xsrf_form_html() %}
<p>username : <input type="text" name="username"/></p>
<p>password : <input type="password" name="password"/></p>
<input type="submit" value="Login!"/>
</form>
</body>
</html>
5. database
tornadoなので、非同期に対応しているFrameworkのmomokoを選択しています。
以下の例では、main.pyで、IndexHandler::get()メソッドに「@tornado.gen.coroutine」をつけて、「yield」待ち受けします。
(1) structure
project-root
│ main.py
└─templates
index.html
(2) main
import os
import signal
from pathlib import Path
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import momoko
from psycopg2.extras import DictCursor
def get_root_path():
return Path(__file__).resolve().parents[0]
accect_ctlc = False
def signal_handler(signum, frame):
global accect_ctlc
accect_ctlc = True
def try_exit():
global accect_ctlc
if accect_ctlc:
tornado.ioloop.IOLoop.instance().stop()
class Application(tornado.web.Application):
def __init__(self):
handlers = handlers = [
(r'/', HomeHandler),
(r'/index', IndexHandler),
]
BASE_DIR=get_root_path()
settings = dict(
template_path=os.path.join(BASE_DIR, "templates"),
)
tornado.web.Application.__init__(self, handlers, **settings)
# Have one global connection to DB across all handlers
self.db = momoko.Pool(
dsn='dbname=%s user=%s password=%s '
'host=%s port=%s' % (
'sampledb',
'testuser',
'*********************',
'localhost',
'5432',
),
cursor_factory=DictCursor,
max_size=30
)
self.db.connect()
class BaseHandler(tornado.web.RequestHandler):
@property
def db(self):
return self.application.db
class HomeHandler(BaseHandler):
def get(self):
self.write("<ul>")
self.write("<li><a href='/index'>Test page</a>")
self.write("</ul>")
self.finish()
class IndexHandler(BaseHandler):
@tornado.gen.coroutine
def get(self):
cursor = yield self.db.execute("SELECT * from users")
rows = cursor.fetchall()
self.render("index.html", result=rows)
def main():
tornado.options.parse_command_line()
signal.signal(signal.SIGINT, signal_handler)
http_server = tornado.httpserver.HTTPServer(Application())
http_server.listen(8888)
tornado.ioloop.PeriodicCallback(try_exit, 100).start()
tornado.ioloop.IOLoop.instance().start()
if __name__ == "__main__":
main()
(3) table create
create table public.users (
username character varying(32) not null
, password character varying(64) not null
, email character varying(70) not null
, active character(1) not null
, userrole integer
, created_at timestamp without time zone default CURRENT_TIMESTAMP not null
, primary key (username)
)
(4) index.html
<h1>Welcome</h1>
<ul>
{% for row in result %}
<li>{{ escape(row['username']) }}</li>
{% end %}
</ul>