サクッとWSGI・ASGIに触れてみる

普段FlaskやFastAPIなどのpythonフレームワークを使っている方は、起動時のメッセージやエラーメッセージなどでWSGIやASGIという言葉をよく目にすることがあると思います。でもフレームワークを使っているだけではWSGIやASGIについてあまり意識する必要はありません。私も言葉だけ知ってるだけで何者かよく知らない状態だったので、今回調べてことにしました。

WSGIとは

WSGIとはWeb Server Gateway Interfaceの略で、Pythonにおいて、WebサーバとWebアプリケーションが通信するための、標準化されたインタフェース定義のことです。アプリケーション(またはフレームワークやツールキット)がWSGI仕様で書かれていれば、WSGI をサポートするサーバ(gunicornなど)上であればどこでも動作させることができます。
サーバ側とアプリケーション側の両方のインタフェース定義については、PEP 3333で規定されています。

WSGIに沿ったアプリケーションの実装

よりWSGIを理解するためにWSGIアプリケーションを実装したいと思います。 WSGIアプリケーションは、呼び出し可能オブジェクトとして実装されます。(関数・クラス・object.call()メソッドを持つインスタンスなどが呼び出し可能オブジェクトです。) これはリクエストを受けてレスポンスを返す単一の同期呼び出し可能なもので、非同期処理やWebSocketのようなプロトコルは許可されていません。

WSGIアプリケーションについて以下のことが定義されてます。

  1. 次の2つの引数を持つ
    • 環境変数を含む辞書(environ)
    • HTTP ステータスコード/メッセージと HTTP ヘッダをサーバに送信するためにアプリケーションが使用するコールバック関数(start_response)
  2. ResposeBodyをイテレータブルな文字列としてサーバに返す

上の定義にしたがって、環境変数(environ)を羅列したtextを返すだけのWSGIアプリケーションを簡単に実装してみました。

#app.py
from wsgiref.simple_server import make_server

# WSGIアプリケーションを作成(今回は関数)
# 環境変数environとコールバック関数start_responseの二つの引数を持つ
def application(environ, start_response):

    response_body = [
        '%s: %s' % (key, value) for key, value in sorted(environ.items())
    ]
    response_body = '\n'.join(response_body)

    status = '200 OK'
    response_headers = [
        ('Content-Type', 'text/plain'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)

    return [response_body.encode('utf-8')]

# applicationへのコネクションを受け付けるWSGIサーバを作成。戻り値はserver_classのインスタンス
httpd = make_server(
    'localhost', # ホストネーム
    8051, # ポート番号
    application # 上で実装しているapplication classを使ってリクエストを処理
)

# 一つのリクエストだけ処理します。killするまでずっと立たせる場合はserve_foreverを使います。
httpd.handle_request()

作成したapp.pyを実行して Postmanなどを利用してhttp://localhost:8051/にGETリクエストを送信してみます。
以下のような環境変数が羅列されたテキストが返ってきたら成功です。

f:id:okiyasi:20200809230046p:plain

Pythonの標準モジュールであるwsgirefを使用してWEBサーバを立てましたが、WSGIに則っているのでもちろんgunicornなどを使ってもアプリケーションを動作させることができます。 より本格的なフレームワークやアプリケーションを作成したい場合にはもっと細かい仕様について理解する必要があります。詳しくはPEP 3333をご覧ください。

ASGIとは

ASGIとはAsynchronous Server Gateway Interfaceの略です。WSGIの精神的な後継仕様であり、asyncioを介して非同期で動作するように設計されていて、またWebSocketなど複数のプロトコルをサポートしています。 Djangoの開発組織がASGIのドキュメントを管理しており、2020年8月現在、ASGIはまだPEP化はされてないようです。 DjangoやFastapiがASGIに対応しており、非同期処理を簡単に実装できます。

ASGIに沿ったアプリケーションの実装

WSGIと同じようにASGIアプリケーションを実装していきます。

ASGIアプリケーションでは以下のことが定義されています。

  1. 次の3つの引数を持つ
    • 受信したリクエストに関する情報を含む辞書(scope)
    • ASGI イベントメッセージを受信するために使用される非同期関数(receive)
    • ASGI イベントメッセージを送信するために使用する非同期関数(send)

上の定義にしたがって、実装していきます。 ここでは時間を測るTimingMiddlewareというものでappをラップしています。 また、starletteというASGI frameworkを使用します。starletteを使うことで簡潔にASGIアプリケーションを実装することができます。

# app.py
import asyncio
from starlette.responses import PlainTextResponse
import time


class TimingMiddleware:
    def __init__(self, application):
        self.application = application

    # ちゃんとsleepされているか検証
    async def __call__(self, scope, receive, send):
        start_time = time.time()
        await self.application(scope, receive, send)
        end_time = time.time()
        print(f"Took {end_time - start_time:.2f} seconds")


async def application(scope, receive, send):
    await asyncio.sleep(3)
    response_body = [
        '%s: %s' % (key, value) for key, value in sorted(scope.items())
    ]
    response_body = '\n'.join(response_body)
    response = PlainTextResponse(response_body)
    await response(scope, receive, send)


application = TimingMiddleware(application)

今回はASGIサーバとしてuvicornを使用します。uvicornをインストールしてapp.pyがあるディレクトリでuvicorn app:applicationを実行します。するとASGIサーバーが立ち上がりASGIアプリケーションが動作します。
WSGIの場合と同様にPostmanを使ってhttp://localhost:8000/GETリクエストを送信します。3秒待った後にtextが返ってきたら成功です。

f:id:okiyasi:20200810181927p:plain
コンソールには次のように出力されているはずです。
f:id:okiyasi:20200810182638p:plain:w600

ASGIについてより詳しい仕様を知りたい方は公式ドキュメンテーションをご覧ください。

まとめ

今回はWSGIとASGIの簡単なアプリケーションを実装しました。 いつもはあまり意識しないWSGIとASGIについて少しは知れたかと思います。Django version3になってASGIに対応したり、Fastapiが少しずつ人気になってきてたりフレームワークのASGIへの移行はトレンドになっているので今後も注目していきたいですね。

参考

wsgiref --- WSGI ユーティリティとリファレンス実装 — Python 3.9.3 ドキュメント Introduction — WSGI Tutorial ASGI Documentation — ASGI 3.0 documentation