LoginSignup
9
12

More than 3 years have passed since last update.

python+tornadoでWEBアプリケーションを作成する

Last updated at Posted at 2019-09-02

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

main.py
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を上記サイトを参考に変更する。

main.py
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
main.py
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)をそれぞれ指定します。

layout.html
<!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>
hello.html
{% extends "layout.html" %}

{% block content %}
  <h1>Welcome</h1>
  <p>Hello <b>{{ username }}</b></p>
  <br/> 
{% end %}

3.JSON

main.py
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
main.py
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()
login.html
<!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

main.py
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

users.sql
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

index.html
  <h1>Welcome</h1>
  <ul>
    {% for row in result %}
      <li>{{ escape(row['username']) }}</li>
    {% end %}
  </ul> 

(5) 実行結果

http://127.0.0.1:8888/index
image.png

9
12
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
12