WSGI:Python用のサーバー-アプリケーションインターフェイス

公開: 2022-03-11

1993年、Webはまだ初期段階にあり、約1,400万人のユーザーと100のWebサイトがありました。 ページは静的でしたが、最新のニュースやデータなどの動的なコンテンツを作成する必要がすでにありました。 これに応えて、Rob McCoolと他の貢献者は、National Center for Supercomputing Applications(NCSA)HTTPd Webサーバー(Apacheの前身)にCommon Gateway Interface(CGI)を実装しました。 これは、別のアプリケーションによって生成されたコンテンツを提供できる最初のWebサーバーでした。

それ以来、インターネット上のユーザー数は爆発的に増加し、動的なWebサイトは至る所に存在するようになりました。 新しい言語を最初に学ぶとき、またはコードを最初に学ぶときでさえ、開発者はすぐに、コードをWebにフックする方法について知りたいと思っています。

Web上のPythonとWSGIの台頭

CGIの作成以来、多くの変化がありました。 CGIアプローチは、要求ごとに新しいプロセスを作成する必要があり、メモリとCPUを浪費するため、実用的ではなくなりました。 FastCGI](http://www.fastcgi.com/)(1996)やmod_python(2000)など、他のいくつかの低レベルのアプローチが登場し、PythonWebフレームワークとWebサーバーの間に異なるインターフェイスを提供します。 さまざまなアプローチが急増するにつれて、開発者がフレームワークを選択した結果、Webサーバーの選択が制限され、その逆も同様でした。

この問題に対処するために、2003年にPhillip J. Ebyは、Python Web Server Gateway Interface(WSGI)であるPEP-0333を提案しました。 アイデアは、PythonアプリケーションとWebサーバーの間に高レベルのユニバーサルインターフェイスを提供することでした。

2003年、PEP-3333はWSGIインターフェイスを更新してPython3サポートを追加しました。 現在、ほとんどすべてのPythonフレームワークは、Webサーバーと通信するための手段としてWSGIを使用しています。 これは、Django、Flask、および他の多くの一般的なフレームワークがそれを行う方法です。

この記事は、WSGIがどのように機能するかを読者に垣間見せ、読者が単純なWSGIアプリケーションまたはサーバーを構築できるようにすることを目的としています。 ただし、これは網羅的なものではありません。本番環境に対応したサーバーまたはアプリケーションを実装する予定の開発者は、WSGI仕様をさらに詳しく調べる必要があります。

PythonWSGIインターフェース

WSGIは、サーバーとアプリケーションが準拠する必要のある単純なルールを指定します。 この全体的なパターンを確認することから始めましょう。

PythonWSGIサーバー-アプリケーションインターフェイス。

アプリケーションインターフェイス

Python 3.5では、アプリケーションインターフェイスは次のようになります。

 def application(environ, start_response): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return [body]

Python 2.7では、このインターフェースはそれほど変わりません。 唯一の変更点は、本体がbytesではなくstrオブジェクトで表されることです。

この場合は関数を使用しましたが、呼び出し可能な関数であれば何でもかまいません。 ここでのアプリケーションオブジェクトのルールは次のとおりです。

  • environおよびstart_responseパラメーターを使用して呼び出し可能である必要があります。
  • 本文を送信する前に、 start_responseコールバックを呼び出す必要があります。
  • ドキュメント本文の一部で反復可能を返す必要があります。

これらのルールを満たし、同じ効果を生み出すオブジェクトの別の例は次のとおりです。

 class Application: def __init__(self, environ, start_response): self.environ = environ self.start_response = start_response def __iter__(self): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] self.start_response(status, headers) yield body

サーバーインターフェース

WSGIサーバーは、次のようにこのアプリケーションとインターフェイスする場合があります。

 def write(chunk): '''Write data back to client''' ... def send_status(status): '''Send HTTP status code''' ... def send_headers(headers): '''Send HTTP headers''' ... def start_response(status, headers): '''WSGI start_response callable''' send_status(status) send_headers(headers) return write # Make request to application response = application(environ, start_response) try: for chunk in response: write(chunk) finally: if hasattr(response, 'close'): response.close()

お気づきかもしれませんが、 start_response呼び出し可能オブジェクトは、アプリケーションがクライアントにデータを送り返すために使用できるwrite呼び出し可能オブジェクトを返しましたが、アプリケーションのコード例では使用されていません。 このwriteインターフェースは非推奨であり、今のところ無視できます。 これについては、この記事の後半で簡単に説明します。

サーバーの責任のもう1つの特徴は、応答イテレーターでオプションのcloseメソッドが存在する場合はそれを呼び出すことです。 ここのGrahamDumpletonの記事で指摘されているように、これはWSGIの見過ごされがちな機能です。 このメソッドが存在する場合はそれを呼び出すと、アプリケーションはまだ保持している可能性のあるすべてのリソースを解放できます。

アプリケーションCallableのenviron引数

environパラメータはディクショナリオブジェクトである必要があります。 これは、CGIとほぼ同じ方法で、要求およびサーバー情報をアプリケーションに渡すために使用されます。 実際、すべてのCGI環境変数はWSGIで有効であり、サーバーはアプリケーションに適用されるすべてのものを渡す必要があります。

渡すことができるオプションのキーはたくさんありますが、いくつかは必須です。 例として、次のGETリクエストを取り上げます。

 $ curl 'http://localhost:8000/auth?user=obiwan&token=123'

これらは、サーバーが提供する必要のあるキーと、それらが取る値です。

価値コメントコメント
REQUEST_METHOD "GET"
SCRIPT_NAME "" サーバーのセットアップに依存
PATH_INFO "/auth"
QUERY_STRING "token=123"
CONTENT_TYPE ""
CONTENT_LENGTH ""
SERVER_NAME "127.0.0.1" サーバーのセットアップに依存
SERVER_PORT "8000"
SERVER_PROTOCOL "HTTP/1.1"
HTTP_(...) クライアントが提供するHTTPヘッダー
wsgi.version (1, 0) WSGIバージョンのタプル
wsgi.url_scheme "http"
wsgi.input ファイルのようなオブジェクト
wsgi.errors ファイルのようなオブジェクト
wsgi.multithread False サーバーがマルチスレッドの場合はTrue
wsgi.multiprocess False サーバーが複数のプロセスを実行している場合はTrue
wsgi.run_once False サーバーがこのスクリプトが1回だけ実行されることを期待している場合はTrue (例:CGI環境で)

この規則の例外は、これらのキーの1つが空の場合(上記の表のCONTENT_TYPEのように)、辞書から省略でき、空の文字列に対応していると見なされることです。

wsgi.inputおよびwsgi.errors

ほとんどのenvironキーは単純ですが、そのうちの2つはもう少し明確にする必要があります。クライアントからのリクエスト本文を含むストリームを含むwsgi.inputと、アプリケーションが発生したエラーを報告するwsgi.errorsです。 アプリケーションからwsgi.errorsに送信されるエラーは、通常、サーバーのエラーログに送信されます。

これらの2つのキーには、ファイルのようなオブジェクトが含まれている必要があります。 つまり、Pythonでファイルまたはソケットを開いたときに取得するオブジェクトと同じように、ストリームとして読み取りまたは書き込みを行うインターフェイスを提供するオブジェクトです。 これは最初は難しいように思えるかもしれませんが、幸いなことに、Pythonはこれを処理するための優れたツールを提供してくれます。

まず、どのようなストリームについて話しているのですか? WSGIの定義に従って、 wsgi.inputwsgi.errorsはPython3のbytesオブジェクトとPython2のstrオブジェクトを処理する必要があります。いずれの場合も、メモリ内バッファーを使用してWSGIを介してデータを渡したり取得したりする場合インターフェイスでは、クラスio.BytesIOを使用できます。

例として、WSGIサーバーを作成している場合、次のようにアプリケーションにリクエスト本文を提供できます。

  • Python2.7の場合
import io ... request_data = 'some request body' environ['wsgi.input'] = io.BytesIO(request_data)
  • Python3.5の場合
import io ... request_data = 'some request body'.encode('utf-8') # bytes object environ['wsgi.input'] = io.BytesIO(request_data)

アプリケーション側では、受け取ったストリーム入力を文字列に変換したい場合は、次のように記述します。

  • Python2.7の場合
readstr = environ['wsgi.input'].read() # returns str object
  • Python3.5の場合
readbytes = environ['wsgi.input'].read() # returns bytes object readstr = readbytes.decode('utf-8') # returns str object

wsgi.errorsストリームは、アプリケーションエラーをサーバーに報告するために使用する必要があり、行は\nで終了する必要があります。 Webサーバーは、システムに応じて終了する別の行への変換を処理する必要があります。

アプリケーション呼び出し可能オブジェクトのstart_response引数

start_response引数は、2つの必須引数、つまりstatusheaders 、および1つのオプションの引数exc_infoを持つ呼び出し可能である必要があります。 本体の一部をWebサーバーに送り返す前に、アプリケーションから呼び出す必要があります。

この記事の冒頭の最初のアプリケーション例では、応答の本文をリストとして返したため、リストがいつ繰り返されるかを制御することはできません。 このため、リストを返す前にstart_responseを呼び出す必要がありました。

2番目の例では、応答本文の最初の(この場合はのみ)部分を生成する直前にstart_responseを呼び出しました。 どちらの方法もWSGI仕様の範囲内で有効です。

Webサーバー側からは、 start_responseの呼び出しは実際にはヘッダーをクライアントに送信するべきではありませんが、応答本文に空でないバイト文字列が少なくとも1つ存在するまで、クライアントに送り返すために遅延させます。 このアーキテクチャにより、アプリケーションの実行の最後の可能な瞬間まで、エラーを正しく報告できます。

start_responsestatus引数

start_responseコールバックに渡されるstatus引数は、HTTPステータスコードと説明で構成され、単一のスペースで区切られた文字列である必要があります。 有効な例は、 '200 OK'または'404 Not Found'です。

start_responseheaders引数

start_responseコールバックに渡されるheaders引数は、 tupleのPython listである必要があり、各タプルは(header_name, header_value)として構成されます。 各ヘッダーの名前と値は両方とも文字列である必要があります(Pythonのバージョンに関係なく)。 これは、WSGI仕様で実際に要求されているため、タイプが重要となるまれな例です。

header引数がどのように見えるかの有効な例を次に示します。

 response_body = json.dumps(data).encode('utf-8') headers = [('Content-Type', 'application/json'), ('Content-Length', str(len(response_body))]

HTTPヘッダーでは大文字と小文字が区別されません。また、WSGI準拠のWebサーバーを作成している場合は、これらのヘッダーをチェックするときに注意する必要があります。 また、アプリケーションによって提供されるヘッダーのリストは、網羅的であるとは想定されていません。 応答をクライアントに送り返す前に、必要なすべてのHTTPヘッダーが存在することを確認し、アプリケーションによって提供されていないヘッダーを入力するのはサーバーの責任です。

start_responseexc_info引数

start_responseコールバックは、エラー処理に使用される3番目の引数exc_infoをサポートする必要があります。 この引数の正しい使用法と実装は、実稼働Webサーバーとアプリケーションにとって最も重要ですが、この記事の範囲外です。

詳細については、WSGI仕様のこちらをご覧ください。

start_responseの戻り値– writeコールバック

下位互換性のために、WSGIを実装するWebサーバーはwrite呼び出し可能オブジェクトを返す必要があります。 このコールバックにより、アプリケーションは、イテレータを介してサーバーにデータを渡すのではなく、本文の応答データをクライアントに直接書き戻すことができます。

その存在にもかかわらず、これは非推奨のインターフェースであり、新しいアプリケーションはそれを使用しないようにする必要があります。

応答本文の生成

WSGIを実装するアプリケーションは、反復可能なオブジェクトを返すことによって応答本文を生成する必要があります。 ほとんどのアプリケーションでは、応答本体はそれほど大きくなく、サーバーのメモリに簡単に収まります。 その場合、それを送信する最も効率的な方法は、1つの要素を反復可能にして一度に送信することです。 全身をメモリにロードすることが不可能な特殊なケースでは、アプリケーションはこの反復可能なインターフェイスを介して部分的にそれを返す場合があります。

ここでは、Python2とPython3のWSGIにはわずかな違いしかありません。Python3では、応答本文はbytesオブジェクトで表されます。 Python 2では、これの正しいタイプはstrです。

UTF-8文字列をbytesまたはstrに変換するのは簡単な作業です。

  • Python 3.5:
 body = 'unicode stuff'.encode('utf-8')
  • Python 2.7:
 body = u'unicode stuff'.encode('utf-8')

Python 2のUnicodeとバイト文字列の処理について詳しく知りたい場合は、YouTubeにすばらしいチュートリアルがあります。

WSGIを実装するWebサーバーは、上記のように、下位互換性のためにwriteコールバックもサポートする必要があります。

Webサーバーなしでアプリケーションをテストする

このシンプルなインターフェイスを理解すれば、実際にサーバーを起動しなくても、アプリケーションをテストするためのスクリプトを簡単に作成できます。

この小さなスクリプトを例にとってみましょう。

 from io import BytesIO def get(app, path = '/', query = ''): response_status = [] response_headers = [] def start_response(status, headers): status = status.split(' ', 1) response_status.append((int(status[0]), status[1])) response_headers.append(dict(headers)) environ = { 'HTTP_ACCEPT': '*/*', 'HTTP_HOST': '127.0.0.1:8000', 'HTTP_USER_AGENT': 'TestAgent/1.0', 'PATH_INFO': path, 'QUERY_STRING': query, 'REQUEST_METHOD': 'GET', 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '8000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'SERVER_SOFTWARE': 'TestServer/1.0', 'wsgi.errors': BytesIO(b''), 'wsgi.input': BytesIO(b''), 'wsgi.multiprocess': False, 'wsgi.multithread': False, 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.version': (1, 0), } response_body = app(environ, start_response) merged_body = ''.join((x.decode('utf-8') for x in response_body)) if hasattr(response_body, 'close'): response_body.close() return {'status': response_status[0], 'headers': response_headers[0], 'body': merged_body}

このようにして、たとえば、いくつかのテストデータとモックモジュールをアプリに初期化し、それに応じて応答するかどうかをテストするためにGET呼び出しを行うことができます。 これは実際のWebサーバーではないことがわかりますが、アプリケーションにstart_responseコールバックと環境変数を含む辞書を提供することで、同等の方法でアプリとインターフェイスします。 リクエストの最後に、レスポンス本文のイテレータを使用し、そのすべてのコンテンツを含む文字列を返します。 同様のメソッド(または一般的なメソッド)を、さまざまなタイプのHTTPリクエストに対して作成できます。

要約

WSGIは、ほとんどすべてのPythonWebフレームワークの重要な部分です。

この記事では、WSGIがファイルのアップロードをどのように処理するかについては触れていません。これは、紹介記事には適さない、より「高度な」機能と見なすことができるためです。 詳細については、ファイル処理に関するPEP-3333のセクションをご覧ください。

この記事が、PythonがWebサーバーとどのように通信するかをよりよく理解するのに役立ち、開発者がこのインターフェイスを面白くて創造的な方法で使用できるようになることを願っています。

謝辞

この記事を手伝ってくれた編集者のNickMcCreaに感謝します。 彼の仕事のおかげで、元のテキストははるかに明確になり、いくつかのエラーは修正されませんでした。