Tornadoを使ってみる

RESTサーバ


前回、HTTPClient機能を紹介しましたが、TornadoではREST サー バーを構築することができます。
RESTサーバはGET POST PUT DELETEのAPIを使います。
単純なRESTサーバーのコードは以下の様になります。
実際にはPUTやDELETEの時に、データの登録や削除を行うことになります。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Simple REST Server

import json
import sys
import time
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.httputil

from tornado.options import define, options
define("port", default=8000, help="run on the given port", type=int)

#curl --dump-header - http://192.168.10.43:8000/get
class GetHandler(tornado.web.RequestHandler):
    def get(self):
        resp = json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}])
        self.write(resp)

#curl --dump-header - http://192.168.10.43:8000/get2
class Get2Handler(tornado.web.RequestHandler):
    def set_default_headers(self):
        print("set_default_headers")
        self.set_header("Content-Type", 'application/json')

    def get(self):
        resp = json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}])
        self.write(resp)

#curl -X POST -H "Content-Type: application/json" -d '{"Name":"hogehoge1973", "Age":"100"}' http://192.168.10.43:8000/post
class PostHandler(tornado.web.RequestHandler):
    def post(self):
        #print("{}".format(self.request.body))
        function = sys._getframe().f_code.co_name
        dict = tornado.escape.json_decode(self.request.body)
        print("{} dict={}".format(function, dict))
        print("Name={}".format(dict['Name']))
        print("Age={}".format(dict['Age']))
        resp = json.dumps(['result', 'ok'])
        self.write(resp)

#curl -X PUT -H "Content-Type: application/json"  http://192.168.10.43:8000/put/fugafuga/100
class PutHandler(tornado.web.RequestHandler):
    def put(self, Name, Age):
        print("Name={}".format(Name))
        print("Age={}".format(Age))
        resp = json.dumps(['result', 'ok'])
        self.write(resp)

#curl -X DELETE -H "Content-Type: application/json" http://192.168.10.43:8000/delete/gehogeho1926
class DeleteHandler(tornado.web.RequestHandler):
    def delete(self, Name):
        print("Name={}".format(Name))
        resp = json.dumps(['result', 'ok'])
        self.write(resp)

if __name__ == "__main__":
        tornado.options.parse_command_line()
        app = tornado.web.Application(
            handlers=[
                (r"/get", GetHandler),
                (r"/get2", Get2Handler),
                (r"/post", PostHandler),
                (r"/put/(.*)/(.*)", PutHandler),
                (r"/delete/(.*)", DeleteHandler),
            ],debug=True
        )
        http_server = tornado.httpserver.HTTPServer(app)
        http_server.listen(options.port)
        tornado.ioloop.IOLoop.current().start()

curlコマンドを使ってGETを発行してみます。
一見正しそうですが、curlでHTTPヘッダーを確認するとContent-Typeがtext/htmlになっています。
$ curl --dump-header - http://192.168.10.43:8000/get
HTTP/1.1 200 OK
Server: TornadoServer/4.5.3
Content-Type: text/html; charset=UTF-8
Date: Tue, 29 Jun 2021 12:17:59 GMT
Etag: "8ad8485c987c9a31574e9cc33fe49aec9334451f"
Content-Length: 39

["foo", {"bar": ["baz", null, 1.0, 2]}]

urlをget2に変えてみます。
set_default_headers()でContent-Typeヘッダーを変更しています。
これでHTTPヘッダーがContent-Type: application/jsonになります。
$ curl --dump-header - http://192.168.10.43:8000/get2
HTTP/1.1 200 OK
Server: TornadoServer/4.5.3
Content-Type: application/json
Date: Tue, 29 Jun 2021 12:18:00 GMT
Etag: "8ad8485c987c9a31574e9cc33fe49aec9334451f"
Content-Length: 39

["foo", {"bar": ["baz", null, 1.0, 2]}]



curlコマンドを使ってPOSTを発行します。
POSTはデータの追加に使われるので、ユニークキー(プライマリーキー)と値をURLパラメータとして送ります。
今回はURLパラメータでNameとAgeを送っていますが、これ以外の項目が有るときは、省略値が使われることになります。
$ curl -X POST -H "Content-Type: application/json" -d '{"Name":"hogehoge1973", "Age":"100"}' http://192.168.10.43:8000/post

サーバー側にはPOSTしたデータが表示されます。
post dict={'Name': 'hogehoge1973', 'Age': '100'}
Name=hogehoge1973
Age=100
[I 210629 21:25:37 web:2064] 200 POST /post (192.168.10.46) 0.71ms



curlコマンドを使ってPUTを発行します。
POSTとPUTはよく似ています。その違いは色々なところで紹介されていますが、こちらが分かり易いです。
PUTは既存データの更新に使われます。
ユニークキー(プライマリーキー)の値と、更新するデータをURLパス変数、あるいはURLパラメータのどちらかで送ります。
URLパス変数では、項目名が分からないので、全ての項目を更新することになります。
URLパラメータでは、項目名と値のペアを、更新したい項目だけ送ります。
$ curl -X PUT -H "Content-Type: application/json"  http://192.168.10.43:8000/put/fugafuga/100

サーバー側にはPUTしたデータが表示されます。
Name=fugafuga
Age=100
[I 210629 21:28:42 web:2064] 200 PUT /put/fugafuga/100 (192.168.10.43) 0.58ms



curlコマンドを使ってDELETEを発行します。
DELETEは既存データの削除に使われます。
ユニークキー(プライマリーキー)の値だけ分かればいいので、ユニークキー(プライマリーキー)の値をURLパス変数として送ります。
$ curl -X DELETE -H "Content-Type: application/json" http://192.168.10.43:8000/delete/gehogeho1926

サーバー側にはDELETEするユニークキー(プライマリーキー)が表示されます。
Name=gehogeho1926
[I 210629 21:33:52 web:2064] 200 DELETE /delete/gehogeho1926 (192.168.10.43) 0.70ms



上記のRESTサーバーはポート#8000で動きます。
そこで、ポート#8100で動く以下のスクリプトを別のターミナルから起動します。
#
# Basic Synchronous Call
#
# http://localhost:8100/?param=hogehoge
#
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.httpclient

import urllib
import json
import datetime
import time

from tornado.options import define, options
define("port", default=8100, help="run on the given port", type=int)

class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        param = self.get_argument('param', '')
        print("param=[{}]".format(param))
        print("len(param)={}".format(len(param)))
        url = "http://localhost:8000"
        if (len(param) != 0): url = url + "?param=" + param
        print("url={}".format(url))

        client = tornado.httpclient.HTTPClient()
        try:
            response = client.fetch(url)
            #print("response={}".format(response))
            body = json.loads(response.body)
            print("body={}".format(body))
        except Exception as e:
            print("Error:{}".format(e))

if __name__ == "__main__":
    tornado.options.parse_command_line()
    #app = tornado.web.Application(handlers=[(r"/", IndexHandler)],debug=True)
    app = tornado.web.Application([(r"/", IndexHandler)],debug=True)
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()

ポート番号=8000番のサーバーに問い合わせた結果を表示します。


問い合わせ先のサーバーがダウンしているときは以下のエラーとなります。




非同期通信を行う場合、Tornadoは自分では接続を閉じません。
callbackの最後でfinishメソッドを呼び出して、Tornadoにリクエストを閉じるように明示的に指示する必要があります。
#
# Basic Asynchronous Call
#
# http://localhost:8100/?param=hogehoge
#
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.httpclient

import urllib
import json
import datetime
import time

from tornado.options import define, options
define("port", default=8100, help="run on the given port", type=int)

class IndexHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        param = self.get_argument('param', '')
        print("param=[{}]".format(param))
        print("len(param)={}".format(len(param)))
        url = "http://localhost:8000"
        if (len(param) != 0): url = url + "?param=" + param
        print("url={}".format(url))

        client = tornado.httpclient.AsyncHTTPClient()
        client.fetch(url, callback=self.on_response)

    def on_response(self, response):
        if response.error:
            print "on_response fail:", response.error
        else:
            print response.body
            #print("response={}".format(response))
            body = json.loads(response.body)
            print("body={}".format(body))
        self.finish()


if __name__ == "__main__":
    tornado.options.parse_command_line()
    #app = tornado.web.Application(handlers=[(r"/", IndexHandler)],debug=True)
    app = tornado.web.Application([(r"/", IndexHandler)],debug=True)
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()

結果は同期通信と同じになります。


問い合わせ先のサーバーがダウンしているときは、CallBack側でエラーとなります。


続く...