Python Web开发中的WSGI协议简介

Python Web开发中,我们一般使用Flask、Django等web框架来开发应用程序,生产环境中将应用部署到Apache、Nginx等web服务器时,还需要uWSGI或者Gunicorn。一个完整的部署应该类似这样:

Web Server(Nginx、Apache) <-----> WSGI server(uWSGI、Gunicorn) <-----> App(Flask、Django)

要弄清这些概念之间的关系,就需要先理解WSGI协议。

WSGI是什么

WSGI的全称是Python Web Server Gateway Interface,WSGI不是web服务器,python模块,或者web框架以及其它任何软件,它只是一种规范,描述了web server如何与web application进行通信的规范。PEP-3333有关于WSGI的具体定义。

为什么需要WSGI

我们使用web框架进行web应用程序开发时,只专注于业务的实现,HTTP协议层面相关的事情交于web服务器来处理,那么,Web服务器和应用程序之间就要知道如何进行交互。有很多不同的规范来定义这些交互,最早的一个是CGI,后来出现了改进CGI性能的FasgCGI。Java有专用的Servlet规范,实现了Servlet API的Java web框架开发的应用可以在任何实现了Servlet API的web服务器上运行。WSGI的实现受Servlet的启发比较大。

WSGI的实现

在WSGI中有两种角色:一方称之为server或者gateway, 另一方称之为application或者framework。application可以提供一个可调用对象供server调用。server先收到用户的请求,然后调用application提供的可调用对象,调用的结果会被封装成HTTP响应后发送给客户端。

The Application/Framework Side

WSGI对application的要求有3个:

- 实现一个可调用对象

- 可调用对象接收两个参数,environ(一个dict,包含WSGI的环境信息)与start_response(一个响应请求的函数)

- 返回一个iterable可迭代对象

可调用对象可以是一个函数、类或者实现了__call__方法的类实例。
environ和start_response由server方提供。
environ是包含了环境信息的字典。
start_response也是一个callable,接受两个必须的参数,status(HTTP状态)和response_headers(响应消息的头),可调用对象返回前调用start_response。

下面是实现简application的代码:

HELLO_WORLD = b"Hello world!\n"

 

def simple_app(environ, start_response):

    """Simplest possible application object"""

    status = '200 OK'

    response_headers = [('Content-type', 'text/plain')]

    start_response(status, response_headers)

    return [HELLO_WORLD]

 

class AppClass:

 

    def __init__(self, environ, start_response):

        self.environ = environ

        self.start = start_response

 

    def __iter__(self):

        status = '200 OK'

        response_headers = [('Content-type', 'text/plain')]

        self.start(status, response_headers)

        yield HELLO_WORLD

代码分别用函数与类对application的可调用对象进行了实现。类实现中定义了__iter__方法,返回的类实例就变为了iterable可迭代对象。

我们也可以用定义了__call__方法的类实例做一下实现:

class AppClass:

 

    def __call__(self, environ, start_response):

        status = '200 OK'

        response_headers = [('Content-type', 'text/plain')]

        start_response(status, response_headers)

        return [HELLO_WORLD]

The Server/Gateway Side

server要做的是每次收到HTTP请求时,调用application可调用对象,pep文档代码:

import os, sys

 

enc, esc = sys.getfilesystemencoding(), 'surrogateescape'

 

def unicode_to_wsgi(u):

    # Convert an environment variable to a WSGI "bytes-as-unicode" string

    return u.encode(enc, esc).decode('iso-8859-1')

 

def wsgi_to_bytes(s):

    return s.encode('iso-8859-1')

 

def run_with_cgi(application):

    environ = {k: unicode_to_wsgi(v) for k,v in os.environ.items()}

    environ['wsgi.input']        = sys.stdin

    environ['wsgi.errors']      = sys.stderr

    environ['wsgi.version']      = (1, 0)

    environ['wsgi.multithread']  = False

    environ['wsgi.multiprocess'] = True

    environ['wsgi.run_once']    = True

 

    if environ.get('HTTPS', 'off') in ('on', '1'):

        environ['wsgi.url_scheme'] = 'https'

    else:

        environ['wsgi.url_scheme'] = 'http'

 

    headers_set = []

    headers_sent = []

 

    def write(data):

        out = sys.stdout

 

        if not headers_set:

             raise AssertionError("write() before start_response()")

 

        elif not headers_sent:

             # Before the first output, send the stored headers

             status, response_headers = headers_sent[:] = headers_set

             out.write(wsgi_to_bytes('Status: %s\r\n' % status))

             for header in response_headers:

                 out.write(wsgi_to_bytes('%s: %s\r\n' % header))

             out.write(wsgi_to_bytes('\r\n'))

 

        out.write(data)

        out.flush()

 

    def start_response(status, response_headers, exc_info=None):

        if exc_info:

            try:

                if headers_sent:

                    # Re-raise original exception if headers sent

                    raise exc_info[1].with_traceback(exc_info[2])

            finally:

                exc_info = None    # avoid dangling circular ref

        elif headers_set:

            raise AssertionError("Headers already set!")

 

        headers_set[:] = [status, response_headers]

 

        # Note: error checking on the headers should happen here,

        # *after* the headers are set.  That way, if an error

        # occurs, start_response can only be re-called with

        # exc_info set.

 

        return write

 

    result = application(environ, start_response)

    try:

        for data in result:

            if data:    # don't send headers until body appears

                write(data)

        if not headers_sent:

            write('')  # send headers now if body was empty

    finally:

        if hasattr(result, 'close'):

            result.close()

代码中server组装了environ,定义了start_response函数,将两个参数提供给application并且调用,最后输出HTTP响应的status、header和body:

if __name__ == '__main__':

    run_with_cgi(simple_app)

输出:

Status: 200 OK

Content-type: text/plain

 

Hello world!

environ

environ字典包含了一些CGI规范要求的数据,以及WSGI规范新增的数据,还可能包含一些操作系统的环境变量以及Web服务器相关的环境变量,具体见。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/5a18f0a52638df07849bb2b45c5090b7.html