python wsgirefソース解析


python web開発においてhttpが要求する処理の流れは、一般的にweb-browser,web-server,wsgiとweb-apprationの四つの段階で、bottleによるweb-apprationを学習しました。http.serverも勉強しました。python 3のソースコードの中で持参したwsgirefの倉庫を完成したら、最後の一環であるwsgiをつなぎ合わせることができます。本論文は次のいくつかの部分に分けられます。
  • wsgiに関する概念
  • cgi例
  • wsgirefソース
  • wsgi小结
  • 小技
  • wsgiに関する概念
    CGI
    CGI(Common Gateway Interface)汎用ゲートウェイインターフェース。1993年に米国NCSAによって発明された。簡単で使いやすい、言葉に関係ないという特徴があります。今日はCGIを直接使ってプログラミングする人が少なくなりましたが、Apache、IIS、NgixなどのWebサーバーが主流です。

    CGIはインタフェース規範を提供して、アプリケーションを提供して、普通は各種のシナリオ言語で、たとえばperl、php、pythonなどはウェブサービスを拡張して、サービスを動態化させます。
    WSGI
    WSGI(Web Server Gateway Interface)ウェブサービスゲートウェイインターフェース。は、ウェブサービスとウェブアプリケーションとの間のインターフェース仕様であり、PEP 333で提案されている。

    wsgiはアプリケーションとウェブサービスの間を結合させ、アプリケーションは規範に従うだけで、様々なウェブサービスの配置で実行することができる。例えば、上の図では、flashk/djangoに基づいて実現されたアプリケーションは、gnicorn展開を使用しても良いし、nginx+uwsgi配置を使用しても良い。
    ASGI
    ASGI(Aynchronous Server Gateway Interface)非同期サーバゲートウェイインターフェース。ASGIは、非同期機能を持つPython Webサーバを目指し、フレームワークとアプリケーション間で標準インターフェースを提供する。ASGIはWSGI後方互換性の実現と複数のサーバとアプリケーションのフレームを持っています。

    wsgiでは、要求応答モデルを使用して、各要求は同期して1つの応答を得ることができる。ASGIでは、要求の応答は非同期的に実現され、一般的にはwebsocketプロトコルに用いられる。asgiの内容は、非同期的な実現に関連しているので、本論文では多く紹介しない。
    cgiの例
    単純な概念の理解は難しいです。例に合わせて勉強します。まずCGIから始めます。
    httpモジュールは簡単なファイルディレクトリサービスを提供します。
    
    python3 -m http.server
    Serving HTTP on :: port 8000 (http://[::]:8000/) ...
    
    このサービスは静的な展示機能だけで、私達はcgiを利用して動的機能を拡張できます。
    cgiスクリプト
    cgi-binディレクトリを作成します。これはCGIに約束されたディレクトリ名です。そしてハロー.pyを作成します。コードは以下の通りです。
    
    #!/usr/bin/env python
    
    import time
    import sqlite3
    import os
    
    DB_FILE = "guests.db"
    
    def init_db():
    	pass #       
    
    def update_total(ts):
    	pass #       
    
    print('<html>')
    print('<head>')
    print('<meta charset="utf-8">')
    print('<title>Hello Word!</title>')
    print('</head>')
    print('<body>')
    print('<h2>Hello Python!</h2>')
    if not os.path.exists(DB_FILE):
    	init_db()
    total = update_total(time.time())
    print(f'total guest: {total}!')	
    print('</body>')
    print('</html>')
    
    コードを簡潔にするために、db操作部分の具体的な実現を省略しました。スクリプトに実行可能な権限が必要です。
    ソースここです
    chmod 755ハロー.py
    
    ./hello.py
    <html>
    <head>
    <meta charset="utf-8">
    <title>Hello Word!</title>
    </head>
    <body>
    <h2>Hello Python!</h2>
    total guest: 4!
    </body>
    </html>
    
    http.server中のcgiサービスを起動します。
    
    python -m http.server --cgi
    
    後ろの--cgiパラメータに注意して、サービスにcgi-handlerを使わせます。起動後にcurlを使ってアクセスします。
    
    curl -v http://127.0.0.1:8000/cgi-bin/hello.py
    * Trying 127.0.0.1...
    * TCP_NODELAY set
    * Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
    > GET /cgi-bin/hello.py HTTP/1.1
    > Host: 127.0.0.1:8000
    > User-Agent: curl/7.64.1
    > Accept: */*
    >
    * HTTP 1.0, assume close after body
    < HTTP/1.0 200 Script output follows
    < Server: SimpleHTTP/0.6 Python/3.8.5
    < Date: Sun, 31 Jan 2021 13:09:29 GMT
    < <html>
    < <head>
    < <meta charset="utf-8">
    < <title>Hello Word!</title>
    < </head>
    < <body>
    < <h2>Hello Python!</h2>
    < total guest: 5! #    
    < </body>
    < </html>
    * Closing connection 0
    
    ハロー.pyが正しく実行され、訪問者数+1が見られます。データはdbに記憶されているので、サービスの再開はまだ有効です。
    cgiサービスの実現
    cgiの実現は主に以下のコードです。
    
    # http.server
    
    class CGIHTTPRequestHandler(SimpleHTTPRequestHandler):
    
     def run_cgi(self):
      import subprocess
      cmdline = [scriptfile]
      if self.is_python(scriptfile):
       interp = sys.executable
       cmdline = [interp, '-u'] + cmdline
      if '=' not in query:
       cmdline.append(query)
    
      try:
       nbytes = int(length)
      except (TypeError, ValueError):
       nbytes = 0
      p = subprocess.Popen(cmdline,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            env = env
            )
      if self.command.lower() == "post" and nbytes > 0:
       data = self.rfile.read(nbytes)
      # throw away additional data [see bug #427345]
      while select.select([self.rfile._sock], [], [], 0)[0]:
       if not self.rfile._sock.recv(1):
        break
      stdout, stderr = p.communicate(data)
      self.wfile.write(stdout)
      p.stderr.close()
      p.stdout.close()
      status = p.returncode
    
    cgiの実現は:
  • subprocess.Popenを使用して、新しいプロセスを実行してスクリプト
  • を実行します。
  • リダイレクトスクリプトの出力は現在のsocketのwfile、つまりhttp要求のリターンになります。
  • コードも検証しました。なぜhello.pyに実行可能な権限を与える必要がありますか?
    例から分かるように、http.serverはhttpサービスの提供に専念し、ap.pyは業務機能に専念し、両者はcgiで接続する。
    wsgiref
    wsgirefは、pythonが持参するwsgiの実現のための参考(reference)であり、主なコード構造:
    ファイル
    説明
    handles.py
    wsgi実現
    headers.py
    http-headerを管理する
    シンプル_server.py
    wsgiのhttpサービスをサポートします。
    util.py&validator.py
    ツールと検証
    WSGIServerのコード:
    
    class WSGIServer(HTTPServer):
    
     """BaseHTTPServer that implements the Python WSGI protocol"""
    
     application = None
    
     def server_bind(self):
      """Override server_bind to store the server name."""
      HTTPServer.server_bind(self)
      self.setup_environ()
    
     def setup_environ(self): #        
      # Set up base environment
      env = self.base_environ = {}
      env['SERVER_NAME'] = self.server_name
      env['GATEWAY_INTERFACE'] = 'CGI/1.1'
      env['SERVER_PORT'] = str(self.server_port)
      env['REMOTE_HOST']=''
      env['CONTENT_LENGTH']=''
      env['SCRIPT_NAME'] = ''
    
     def get_app(self):
      return self.application
    
     def set_app(self,application): #   application class,   class
      self.application = application
    
    WSGIServerは複雑ではなく、http-serverから引き継ぎ、appplicationの注入を受けてweb-serverとwe-appplicationを接続します。接続後の動作は古い決まりで、HTTPRequest Handlerに任せて実現します。同時に、wsgiサービスはenvの動作を準備して、いくつかのwsgiの環境変数を約束しました。
    
    class WSGIRequestHandler(BaseHTTPRequestHandler):
    
     server_version = "WSGIServer/" + __version__
    
     def get_environ(self):
      pass
    
     def handle(self):
      """Handle a single HTTP request"""
    
      self.raw_requestline = self.rfile.readline(65537)
      if len(self.raw_requestline) > 65536:
       ...
       self.send_error(414)
       return
    
      if not self.parse_request(): # An error code has been sent, just exit
       return
    
      handler = ServerHandler(
       self.rfile, self.wfile, self.get_stderr(), self.get_environ(),
       multithread=False,
      ) #       handler
      handler.request_handler = self  
      handler.run(self.server.get_app()) #   application  
    
    WSGIRequest Handlerはhandlerをカバーし、httpプロトコルを処理しました。requestの後、また4つの動作をしました。
  • environ
  • を作成します。
  • Server Handlerオブジェクトを作成する
  • は、appオブジェクト
  • を作成する。
  • 実行アプリ
  • environ処理は主にhttp要求のheader情報をwsgi-serverの環境変数に付随する。
    
    def get_environ(self):
     env = self.server.base_environ.copy() # wsgi-server     
     env['SERVER_PROTOCOL'] = self.request_version
     env['SERVER_SOFTWARE'] = self.server_version
     env['REQUEST_METHOD'] = self.command
     
     ...
     
     host = self.address_string()
     if host != self.client_address[0]:
      env['REMOTE_HOST'] = host
     env['REMOTE_ADDR'] = self.client_address[0]
    
     if self.headers.get('content-type') is None:
      env['CONTENT_TYPE'] = self.headers.get_content_type()
     else:
      env['CONTENT_TYPE'] = self.headers['content-type']
    
     length = self.headers.get('content-length')
     if length:
      env['CONTENT_LENGTH'] = length
    
     for k, v in self.headers.items():
      k=k.replace('-','_').upper(); v=v.strip()
      if k in env:
       continue     # skip content length, type,etc.
      if 'HTTP_'+k in env:
       env['HTTP_'+k] += ','+v  # comma-separate multiple headers
      else:
       env['HTTP_'+k] = v
     return env
    
    Server Handlerオブジェクトの作成、入出力/エラー、および環境変数情報を受け付けます。
    
    class ServerHandler(BaseHandler):
    
     def __init__(self,stdin,stdout,stderr,environ,
      multithread=True, multiprocess=False
     ):
      self.stdin = stdin
      self.stdout = stdout
      self.stderr = stderr
      self.base_env = environ
      self.wsgi_multithread = multithread
      self.wsgi_multiprocess = multiprocess
     ...
    
    ポイントはServerhandlerのrun関数です。
    
    class BaseHandler:
     def run(self, application):
     """Invoke the application"""
     # Note to self: don't move the close()! Asynchronous servers shouldn't
     # call close() from finish_response(), so if you close() anywhere but
     # the double-error branch here, you'll break asynchronous servers by
     # prematurely closing. Async servers must return from 'run()' without
     # closing if there might still be output to iterate over.
      ...
      self.setup_environ()
      self.result = application(self.environ, self.start_response)
      self.finish_response()
      ...
    
    キーの3つのステップ:
  • セットアップ_environ環境変数の構築を継続する
  • http要求をapplicationで処理するための戻り値
  • http応答を完了しました。
  • セットアップenvironはenvに対してさらに包装を行い、要求のin/errorが付いています。これによってenvを使ってhttpに対して読み書きを要求することができます。
    
    def setup_environ(self):
     """Set up the environment for one request"""
    
     env = self.environ = self.os_environ.copy()
     self.add_cgi_vars() #      self.environ.update(self.base_env)
    
     env['wsgi.input']  = self.get_stdin() #     stdout
     env['wsgi.errors']  = self.get_stderr()
     env['wsgi.version']  = self.wsgi_version
     env['wsgi.run_once']  = self.wsgi_run_once
     env['wsgi.url_scheme'] = self.get_scheme()
     env['wsgi.multithread'] = self.wsgi_multithread
     env['wsgi.multiprocess'] = self.wsgi_multiprocess
    
     if self.wsgi_file_wrapper is not None:
      env['wsgi.file_wrapper'] = self.wsgi_file_wrapper
    
     if self.origin_server and self.server_software:
      env.setdefault('SERVER_SOFTWARE',self.server_software)
    
    envの処理手順は、3ステップ:1)serverの実行情報2を付加して要求されたhttpヘッダ(プロトコル情報)3)に追加要求されたストリーム情報として理解できる。envは、httpが要求するすべてのコンテキスト環境という言い方に変えられます。
    アプリはまた、フィードバック関数start_を受信します。レスポンスは主にhttpプロトコルの仕様に従い、レスポンス状態とレスポンスを生成します。header:
    
    def start_response(self, status, headers,exc_info=None):
     """'start_response()' callable as specified by PEP 3333"""
    
     self.status = status
     self.headers = self.headers_class(headers)
     status = self._convert_string_type(status, "Status")
     assert len(status)>=4,"Status must be at least 4 characters"
     assert status[:3].isdigit(), "Status message must begin w/3-digit code"
     assert status[3]==" ", "Status message must have a space after code"
    
     return self.write
    
    アプリケーションの要求に対する処理:
    
    def demo_app(environ,start_response):
     from io import StringIO
     stdout = StringIO()
     print("Hello world!", file=stdout)
     print(file=stdout)
     # http     
     h = sorted(environ.items())
     for k,v in h:
      print(k,'=',repr(v), file=stdout)
     #     http_status, response_headers
     start_response("200 OK", [('Content-Type','text/plain; charset=utf-8')])
     #       response_body
     return [stdout.getvalue().encode("utf-8")]
    
    レスポンスはまだServer Handlerによって書き込まれています。
    
    def finish_response(self):
     if not self.result_is_file() or not self.sendfile():
      for data in self.result:
       self.write(data)
      self.finish_content()
    
    以下の命令を使ってこの流れをテストできます。
    
    python -m wsgiref.simple_server
    Serving HTTP on 0.0.0.0 port 8000 ...
    127.0.0.1 - - [31/Jan/2021 21:43:05] "GET /xyz?abc HTTP/1.1" 200 3338
    
    wsgiまとめ
    簡単で簡単な小さい結び目のwsgiの実現。http要求の処理フローweb-browser<->web-server<->wsgi<->web-appicationでは、階層的な思想が具現されており、それぞれの層が異なることをする:
  • web-serverはhttp/tcpプロトコルを処理して、スレッド/プロセスのスケジュールなどの下の階は
  • を実現します。
  • wsgiはhttpの要求を受けて、appliaitonの処理要求を呼び出して、応答
  • を完成します。
  • appicationは、上位業務ロジック
  • を処理する。
    テクニック
    wsgirefコードの中にも様々な小さい技術があります。勉強したらコードをもっとpythonicにします。
    環境変数はこのように設定されています。
    
    def setup_environ(self):
     # Set up base environment
     env = self.base_environ = {}
     env['SERVER_NAME'] = self.server_name
     env['GATEWAY_INTERFACE'] = 'CGI/1.1'
     ...
    
    前は大体こう書きました。
    
    def setup_environ(self):
     self.base_environ = {}
     self.base_environ['SERVER_NAME'] = self.server_name
     self.base_environ['GATEWAY_INTERFACE'] = 'CGI/1.1'
    
    対照的に、前の書き方はもっと簡潔であることが分かります。
    例えばストリームの継続書込み:
    
    def _write(self,data):
     result = self.stdout.write(data)
     if result is None or result == len(data):
      return
     from warnings import warn
     warn("SimpleHandler.stdout.write() should not do partial writes",
      DeprecationWarning)
     while True:
      data = data[result:] #      ,    
      if not data:
       break
      result = self.stdout.write(data)
    
    例えばheaderの処理は、配列を辞書として使っています。
    
    class Headers:
     """Manage a collection of HTTP response headers"""
    
     def __init__(self, headers=None):
      headers = headers if headers is not None else []
      self._headers = headers #         
     
     def __setitem__(self, name, val):
      """Set the value of a header."""
      del self[name]
      self._headers.append(
       (self._convert_string_type(name), self._convert_string_type(val)))
    
     ....
    
     def __getitem__(self,name):
      """Get the first header value for 'name'
    
      Return None if the header is missing instead of raising an exception.
    
      Note that if the header appeared multiple times, the first exactly which
      occurrence gets returned is undefined. Use getall() to get all
      the values matching a header field name.
      """
      return self.get(name)
    
     def get(self,name,default=None):
      """Get the first header value for 'name', or return 'default'"""
      name = self._convert_string_type(name.lower())
      for k,v in self._headers:
       if k.lower()==name:
        return v
      return default
    
    このようなContent-Type: application/javascript; charset=utf-8の値は、以下のように使用されてもよい。
    
    if self.headers.get('content-type') is None:
     env['CONTENT_TYPE'] = self.headers.get_content_type()
    else:
     env['CONTENT_TYPE'] = self.headers['content-type']
    辞書ではなく行列を使うのはなぜですか?私の推測では、headerの特性はデータの読み操作が多いからです。
    以上がpython wsgirefソースの解析の詳細です。python wsgirefソースに関する詳細については、他の関連記事に注目してください。