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

PythonのWEBフレームワーク[「tornado」](https://www.tornadoweb.org/en/stable/)を使った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」で止めることができないので、対応しておく。

[このサイト](https://www.doraxdora.com/blog/2018/03/03/post-4093/)にあるやり方がいいと思いました。


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](https://github.com/FSX/momoko/blob/master/docs/tutorial.rst)を選択しています。

以下の例では、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

```sql: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

```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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246741/106266d6-95f3-e214-56d9-43d92d65b82f.png)