【Python】Tornadoで非同期通信を行なうAPIをつくりAPIサーバーを建てる

#### 背景

同時接続数の多いシステムではパフォーマンスを求められますが、そのような際には非同期通信なるものを使うと良いと非同期通信に対応した言語やライブラリを幾つかご教示いただきました。
今回はその非同期通信を行なうAPIを作成した際のメモです。

先人たちの知恵をお借りするなどして解決できたことを、この場をお借りして感謝するとともに、大変恐縮ですが**自分のメモ**として、こちらへまとめておきます。

### 1.非同期通信とは

> “ネットワークなどでつながれているコンピュータ間で、送信者のデータ送信タイミングと受信者のデータ受信タイミングを合わせずに通信を行う通信方式。
> 
> 　同期通信はデータ通信のリクエストを出してからレスポンスが来るまでほかの処理を行わずにレスポンスを待ち続けるが、非同期通信ではレスポンスを待っている間にほかの処理を行える。ほかの処理を行っている際に、レスポンスを受信すると受信処理を実行する。
> 
> 　通信方式は、送信者が通信データにデータの始まりを示す「スタートビット」とデータの終わりを示す「ストップビット」という信号を付加してデータを送信し、受信者は「スタートビット」を受信するとデータの受信を開始し、データ受信中に「ストップビット」を受信するとデータの受信を終了するという方法で行っている。
> 
> 　そのメリットばかりが注目される非同期通信だが、「スタートビット」と「ストップビット」というデータ内容とは無関係の冗長なデータも送信する必要があるため、送信タイミングと受信タイミングを合わせて通信を行う同期通信に比べて送信するデータ量が多くなり伝送効率が悪くなってしまうというデメリットがある。”

### 2.Tornado ?

ご教示いただいた言語は[Node.js](https://nodejs.org/ja/)と[Python](https://www.python.org/)でした。（私の経験した言語を前提としたため）
強いて言えばPythonのほうが利用経験が長いため、今回はPythonを採用し、非同期通信を得意とするフレームワーク（兼Webサーバー）である **[Tornado](https://www.tornadoweb.org/)** を利用することにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/902793/68ff96c1-fd61-4bc1-1f35-83c06d4f59ad.png)

#### Tornado （フレームワーク）とは 

> “TornadoはFriendFeedで開発されたPythonのWebフレームワーク（非同期通信ライブラリ）です。
> ノンブロッキングネットワークI / Oを使用することで、Tornadoは数万のオープン接続に拡張でき、ロングポーリング、WebSocket、および各ユーザーへの長時間の接続が必要なその他のアプリケーションに最適です。”

詳細は [https://agency-star.co.jp/column/tornado](https://agency-star.co.jp/column/tornado)をご参照。


[Wikipedia](https://ja.wikipedia.org/wiki/Tornado_(Web%E3%82%B5%E3%83%BC%E3%83%90))によれば、以下のようなパフォーマンスを発揮するという。
tornadoのパフォーマンスが抜群に高いことが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/902793/cd5f291c-aeec-7784-f8ee-31f9836836eb.png)

### 3.環境

-AWS EC2 (Amazon Linux 2)
-[Python](https://www.python.org/) 3.10.1
-[Tornado](https://www.tornadoweb.org/) 6.2
-Amazon Aurora/MySQL 57
-[Postman](https://www.postman.com/)

### 4.インストール

```powershell
pip install tornado
```

### 5.サンプルコード

```python:jsonpost.py
import os
import json
import logging

import tornado.ioloop
import tornado.web
from tornado.httpclient import AsyncHTTPClient

logging.basicConfig(
    format='%(asctime)-15s %(levelname)s %(message)s',
    level=logging.INFO)

class AsyncHandler(tornado.web.RequestHandler):
    # Postでアクセスした際、Handlerクラスの`post`メソッドが呼び出される。
    # Get等も同様。リクエストメソッドを意識せず処理したい場合は`prepare`メソッドを実装するとそこにルーティングされる。
    async def post(self):
        body = await self.postRate()

        # JSON以外のリクエストは400 Bad Requestにする
        if (self.request.headers.get('Content-Type')
                != 'application/json'):
            raise tornado.web.HTTPError(400)

        # JSONをパースする
        data = json.loads(self.request.body)

        # ここで処理を実施する
        logging.info(f'data = {data}')

        # レスポンスを書き込む
        self.write({'result': 'OK'})

    async def postRate(self):
        http_client = AsyncHTTPClient()
        try:
            response =  await http_client.fetch("https://{domain}/api/v1/{api-name}?{id}=1")
            return response.body
        except Exception as e:
            print("Error: %s" % e)

def main():
    # タプルの先頭にURL、次にHnadlerクラスを指定して、ルーティングできる。
    app = tornado.web.Application([
        (r'/async', AsyncHandler),
    ])
    aapp.listen(os.environ.get("PORT", 8080))
    tornado.ioloop.IOLoop.current().start()
```

### 6.サーバー起動

```powershell
python jsonpost.py
```

### 7.リクエスト

別ウィンドウから[curl](https://curl.se/)コマンドでリクエストする。

```powershell
curl -v localhost:8080/json -H 'Content-Type: application/json' -d '{
  "data": {
    "date": "2022-10-08"
  }
}'
* Trying ::1…
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0) 
> POST /json HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.85.0
> Accept: /
> Content-Type: application/json
> Content-Length: 44
*   upload completely sent off: 44 out of 44 bytes
< HTTP/1.1 200 OK
< Server: TornadoServer/6.2
< Content-Type: application/json; charset=UTF-8
< Date: Sat, 08 Oct 2022 04:30:03 GMT
< Content-Length: 16
<
* Connection #0 to host localhost left intact
{"result": "OK"}* Closing connection 0
```