diadia

興味があることをやってみる。自分のメモを残しておきます。

gunicornについて

テーマ アプリケーション・サーバー間やりとり

PythonではアプリケーションとWebサーバのインタフェースとして PEP3333 で定義された WSGI(Web Server Gateway Interface)という仕様が広く利用されているらしくgunicornについて全く分かっていないので少し理解しておきたい。

gunicornやuWSGIはWSGIサーバと呼ばれる

WSGIサーバの種類 - gunicorn - uWSGI

gunicornはpythonで動いているものなのか??? 答えはyes リポジトリを見ればpythonで書かれていることを確認できる。

gunicornのリポジトリ https://github.com/benoitc/gunicorn/tree/5d0c7783008d4df87d579a576d19182c4a95caf7/gunicorn

ドキュメント:
https://docs.gunicorn.org/en/stable/run.html

$ gunicorn [OPTIONS] APP_MODULE

wsgiの仕様にあったpythonのコードであれば、WSGIサーバを動かすことができることがわかった。

  1. WSGIのアプリケーションは、2つの引数を持った呼び出し可能なオブジェクトである。
  2. 第2引数として渡されたオブジェクトを呼び出し、HTTPステータスコードとヘッダ情報を渡す。
  3. レスポンスボディとしてバイト文字列をyieldするiterableなオブジェクトを返す。

参考: https://c-bata.link/webframework-in-python/wsgi.html

wsgiの基準では第一引数は辞書型オブジェクトという決まりがあるらしい。 このオブジェクトにurlやHTTPメソッドを格納して利用することができる.

WSGIの説明で分かりやすいもの

では、WSGIとは何でしょうか?WSGI(ウィズギー)とは、PythonでWebアプリケーションとWebサーバーを接続する際に考案されたインターフェース定義です。 その昔、PythonのWebフレームワークが急増したことにより、Webアプリ開発者にある不都合が生じました。 フレームワークごとに、サーバーと接続するためのインターフェースが異なっていたため、同一のアプリケーションであっても、接続できるサーバーが制限される(あるサーバーには接続できるのに、別のサーバーには接続できない)という事態が生じてしまったのです。 そのため、インターフェースを統一して、どのフレームワークでも同じように各種サーバーに接続できるようにしよう、ということで生まれたのがWSGIでした。 WSGIアプリケーションの定義として、 WSGIアプリケーションは、呼び出し可能なオブジェクトとして定義する。このオブジェクトが呼び出される際、第一引数に環境変数が渡され、第二引数にステータスコードとレスポンスヘッダを受け取る呼び出し可能なオブジェクトが渡される。 第二引数に渡されたオブジェクトを呼び出して、ステータスコードとレスポンスヘッダ情報を渡す。 戻り値として、バイト文字列をyieldするiterableなオブジェクトを返す。 といったものがあります。

https://qiita.com/sti320a/items/828d7bceabea5f363ad1

# ファイル名はhello.py

def application(env, start_response):
    start_response('200 OK', [('Content-type', 'text/plain; charset=utf-8')])
    return [b'Hello World']

gunicornを起動(gunicorn -w 1 hello:application)してlocalhost:8000にアクセスすると、ブラウザにHello Worldが表示される。

以下の場合も試してみた。

def application(change1, change2):
    print('change1 :,'change1)
    print('change2',change2)
    change2('200 OK', [('Content-type', 'text/plain; charset=utf-8')])
    return [b'Hello World']

すると同じように動いた。gunicornではひいてはwsgi仕様では、第一引数と第二引数があり、それらを使って関数さえ書いていればブラウザに表示できることがわかった。
change1, change2を標準出力した結果を残しておく。

change1:  {'wsgi.errors': <gunicorn.http.wsgi.WSGIErrorsWrapper object at 0x10ff16460>, 'wsgi.version': (1, 0), 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'wsgi.file_wrapper': <class 'gunicorn.http.wsgi.FileWrapper'>, 'wsgi.input_terminated': True, 'SERVER_SOFTWARE': 'gunicorn/20.0.4', 'wsgi.input': <gunicorn.http.body.Body object at 0x10ff165b0>, 'gunicorn.socket': <socket.socket fd=9, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8000), raddr=('127.0.0.1', 50644)>, 'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'RAW_URI': '/', 'SERVER_PROTOCOL': 'HTTP/1.1', 'HTTP_HOST': 'localhost:8000', 'HTTP_CONNECTION': 'keep-alive', 'HTTP_CACHE_CONTROL': 'max-age=0', 'HTTP_DNT': '1', 'HTTP_UPGRADE_INSECURE_REQUESTS': '1', 'HTTP_USER_AGENT': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36', 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'HTTP_SEC_FETCH_SITE': 'none', 'HTTP_SEC_FETCH_MODE': 'navigate', 'HTTP_SEC_FETCH_USER': '?1', 'HTTP_SEC_FETCH_DEST': 'document', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br', 'HTTP_ACCEPT_LANGUAGE': 'ja-JP,ja;q=0.9,es-ES;q=0.8,es;q=0.7,en-US;q=0.6,en;q=0.5', 'HTTP_COOKIE': 'csrftoken=LD89SVbaQvSe6eBpG8yp7zseGHi1yJpaFephs4Bi4SuAgkNFutL027VJmgasezv6; sessionid=n77unsdykcmss7i86rjb7rpziegzro79', 'wsgi.url_scheme': 'http', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '50644', 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '8000', 'PATH_INFO': '/', 'SCRIPT_NAME': ''}
start_response <bound method Response.start_response of <gunicorn.http.wsgi.Response object at 0x10ff16490>>
change1 :  {'wsgi.errors': <gunicorn.http.wsgi.WSGIErrorsWrapper object at 0x10ff16550>, 'wsgi.version': (1, 0), 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'wsgi.file_wrapper': <class 'gunicorn.http.wsgi.FileWrapper'>, 'wsgi.input_terminated': True, 'SERVER_SOFTWARE': 'gunicorn/20.0.4', 'wsgi.input': <gunicorn.http.body.Body object at 0x10ff16490>, 'gunicorn.socket': <socket.socket fd=9, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8000), raddr=('127.0.0.1', 50645)>, 'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'RAW_URI': '/favicon.ico', 'SERVER_PROTOCOL': 'HTTP/1.1', 'HTTP_HOST': 'localhost:8000', 'HTTP_CONNECTION': 'keep-alive', 'HTTP_USER_AGENT': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36', 'HTTP_DNT': '1', 'HTTP_ACCEPT': 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8', 'HTTP_SEC_FETCH_SITE': 'same-origin', 'HTTP_SEC_FETCH_MODE': 'no-cors', 'HTTP_SEC_FETCH_DEST': 'image', 'HTTP_REFERER': 'http://localhost:8000/', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br', 'HTTP_ACCEPT_LANGUAGE': 'ja-JP,ja;q=0.9,es-ES;q=0.8,es;q=0.7,en-US;q=0.6,en;q=0.5', 'HTTP_COOKIE': 'csrftoken=LD89SVbaQvSe6eBpG8yp7zseGHi1yJpaFephs4Bi4SuAgkNFutL027VJmgasezv6; sessionid=n77unsdykcmss7i86rjb7rpziegzro79', 'wsgi.url_scheme': 'http', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '50645', 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '8000', 'PATH_INFO': '/favicon.ico', 'SCRIPT_NAME': ''}
change2 <bound method Response.start_response of <gunicorn.http.wsgi.Response object at 0x10ff165e0>>

よく分からないけど二回出力されていることを確認した。

第一引数には何が格納されているか分かりづらいので以下のようにして出力をみた。

def application(env, start_response):
    print('env : ', env)
    print('env.keys() : ', env.keys())
    print('start_response', start_response)
    start_response('200 OK', [('Content-type', 'text/plain; charset=utf-8')])
    return [b'Hello World']
env.keys() :  dict_keys(['wsgi.errors', 'wsgi.version', 'wsgi.multithread', 'wsgi.multiprocess', 'wsgi.run_once', 'wsgi.file_wrapper', 'wsgi.input_terminated', 'SERVER_SOFTWARE', 'wsgi.input', 'gunicorn.socket', 'REQUEST_METHOD', 'QUERY_STRING', 'RAW_URI', 'SERVER_PROTOCOL', 'HTTP_HOST', 'HTTP_CONNECTION', 'HTTP_USER_AGENT', 'HTTP_DNT', 'HTTP_ACCEPT', 'HTTP_SEC_FETCH_SITE', 'HTTP_SEC_FETCH_MODE', 'HTTP_SEC_FETCH_DEST', 'HTTP_REFERER', 'HTTP_ACCEPT_ENCODING', 'HTTP_ACCEPT_LANGUAGE', 'HTTP_COOKIE', 'wsgi.url_scheme', 'REMOTE_ADDR', 'REMOTE_PORT', 'SERVER_NAME', 'SERVER_PORT', 'PATH_INFO', 'SCRIPT_NAME'])

次にこれで実行してみた。なお、'http://localhost:8000/sjfof'をブラウザに入力している。

def application(env, start_response):
    #print('env : ', env)
    #print('env.keys() : ', env.keys())
    #print('start_response', start_response)
    print('================start_response前 始まり========================')
    print('env["REQUEST_METHOD"] : ', env["REQUEST_METHOD"])
    print('env["QUERY_STRING"] : ', env["QUERY_STRING"])
    print('env["RAW_URI"] : ', env["RAW_URI"])
    print('env["HTTP_HOST"] : ', env["HTTP_HOST"])
    print('================start_response前 終わり========================')
    start_response('200 OK', [('Content-type', 'text/plain; charset=utf-8')])
    print('================start_response後 始まり========================')
    print('env["REQUEST_METHOD"] : ', env["REQUEST_METHOD"])
    print('env["QUERY_STRING"] : ', env["QUERY_STRING"])
    print('env["RAW_URI"] : ', env["RAW_URI"])
    print('env["HTTP_HOST"] : ', env["HTTP_HOST"])
    print('================start_response後 終わり========================')
    return [b'Hello World']
================start_response前 始まり========================
env["REQUEST_METHOD"] :  GET
env["QUERY_STRING"] :  
env["RAW_URI"] :  /sjfof
env["HTTP_HOST"] :  localhost:8000
================start_response前 終わり========================
================start_response後 始まり========================
env["REQUEST_METHOD"] :  GET
env["QUERY_STRING"] :  
env["RAW_URI"] :  /sjfof
env["HTTP_HOST"] :  localhost:8000
================start_response後 終わり========================
================start_response前 始まり========================
env["REQUEST_METHOD"] :  GET
env["QUERY_STRING"] :  
env["RAW_URI"] :  /favicon.ico
env["HTTP_HOST"] :  localhost:8000
================start_response前 終わり========================
================start_response後 始まり========================
env["REQUEST_METHOD"] :  GET
env["QUERY_STRING"] :  
env["RAW_URI"] :  /favicon.ico
env["HTTP_HOST"] :  localhost:8000
================start_response後 終わり========================

'http://localhost:8000/sjfof?mykey=myvalue&second=value2'をブラウザに入力している。

================start_response前 始まり========================
env["REQUEST_METHOD"] :  GET
env["QUERY_STRING"] :  mykey=myvalue&second=value2
env["RAW_URI"] :  /sjfof?mykey=myvalue&second=value2
env["HTTP_HOST"] :  localhost:8000
================start_response前 終わり========================
================start_response後 始まり========================
env["REQUEST_METHOD"] :  GET
env["QUERY_STRING"] :  mykey=myvalue&second=value2
env["RAW_URI"] :  /sjfof?mykey=myvalue&second=value2
env["HTTP_HOST"] :  localhost:8000
================start_response後 終わり========================
================start_response前 始まり========================
env["REQUEST_METHOD"] :  GET
env["QUERY_STRING"] :  
env["RAW_URI"] :  /favicon.ico
env["HTTP_HOST"] :  localhost:8000
================start_response前 終わり========================
================start_response後 始まり========================
env["REQUEST_METHOD"] :  GET
env["QUERY_STRING"] :  
env["RAW_URI"] :  /favicon.ico
env["HTTP_HOST"] :  localhost:8000
================start_response後 終わり========================

この結果からどうやら二回標準出力は2回繰り返される。そして第一引数この場合はenvは1回目と2回目で内容が変化することだけわかった. それかfaviconuriにアクセスするために二種類のリクエスト情報が作られたと考える方が正しいかもしれない。これは進めていけばいずれわかることだろう。

ここまでわかったこと(後日分かったことも追加している)

第一引数キー
REQUEST_METHOD GET、POST等のリクエストメソッド
QUERY_STRING ?以降の内容
RAW_URI ブラウザに入力したURIもしくはkeep aliveで追加でアクセスするURIな気がする
HTTP_HOST 接続先のホスト
PATH_INFO RAW_URIからQUERY_STRING部分を削除した、いわゆるPATH値が格納される

また、ブラウザに異なるURIを入力してもenv['RAW_URI']にURIが格納されるだけで、実際に起動している 関数(この場合はhello.py application関数)が起動するだけである。したがってapplication関数内でRAW_URIの内容によって表示する条件分岐を作ればリソースのアドレス可能性を担保できるようになるのではないか??