zoukankan      html  css  js  c++  java
  • 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。

    下面是PEP-3333实现简application的代码:

    HELLO_WORLD = b"Hello world!
    "
    
    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
    ' % status))
                 for header in response_headers:
                     out.write(wsgi_to_bytes('%s: %s
    ' % header))
                 out.write(wsgi_to_bytes('
    '))
    
            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服务器相关的环境变量,具体见environ

    首先是CGI规范中要求的变量:
      - REQUEST_METHOD: HTTP请求方法,'GET', 'POST'等,不能为空
      - SCRIPT_NAME: HTTP请求path中的初始部分,用来确定对应哪一个application,当application对应于服务器的根,可以为空
      - PATH_INFO: path中剩余的部分,application要处理的部分,可以为空
      - QUERY_STRING: HTTP请求中的查询字符串,URL中?后面的内容
      - CONTENT_TYPE: HTTP headers中的Content-Type内容
      - CONTENT_LENGTH: HTTP headers中的Content-Length内容
      - SERVER_NAMESERVER_PORT: 服务器域名和端口,这两个值和前面的SCRIPT_NAME, PATH_INFO拼起来可以得到完整的URL路径
      - SERVER_PROTOCOL: HTTP协议版本,'HTTP/1.0'或'HTTP/1.1'
      - HTTP_Variables: 和HTTP请求中的headers对应,比如'User-Agent'写成'HTTP_USER_AGENT'的格式

    WSGI规范中还要求environ包含下列成员:
      - wsgi.version:一个元组(1, 0),表示WSGI版本1.0
      - wsgi.url_scheme:http或者https
      - wsgi.input:一个类文件的输入流,application可以通过这个获取HTTP请求的body
      - wsgi.errors:一个输出流,当应用程序出错时,可以将错误信息写入这里
      - wsgi.multithread:当application对象可能被多个线程同时调用时,这个值需要为True
      - wsgi.multiprocess:当application对象可能被多个进程同时调用时,这个值需要为True
      - wsgi.run_once:当server期望application对象在进程的生命周期内只被调用一次时,该值为True

    我们可以使用python官方库wsgiref实现的server看一下environ的具体内容:

    def demo_app(environ, start_response):
        from StringIO import StringIO
        stdout = StringIO()
        print >>stdout, "Hello world!"
        print >>stdout
        h = environ.items()
        h.sort()
        for k,v in h:
            print >>stdout, k,'=', repr(v)
        start_response("200 OK", [('Content-Type','text/plain')])
        return [stdout.getvalue()]
    
    if __name__ == '__main__':
        from wsgiref.simple_server import make_server
        httpd = make_server('', 8000, demo_app)
        sa = httpd.socket.getsockname()
        print "Serving HTTP on", sa[0], "port", sa[1], "..."
        import webbrowser
        webbrowser.open('http://localhost:8000/xyz?abc')
        httpd.handle_request()  # serve one request, then exit
        httpd.server_close()

    打开的网页会输出environ的具体内容。

    使用environ组装请求URL地址:

    from urllib.parse import quote
    url = environ['wsgi.url_scheme']+'://'
    
    if environ.get('HTTP_HOST'):
        url += environ['HTTP_HOST']
    else:
        url += environ['SERVER_NAME']
    
        if environ['wsgi.url_scheme'] == 'https':
            if environ['SERVER_PORT'] != '443':
               url += ':' + environ['SERVER_PORT']
        else:
            if environ['SERVER_PORT'] != '80':
               url += ':' + environ['SERVER_PORT']
    
    url += quote(environ.get('SCRIPT_NAME', ''))
    url += quote(environ.get('PATH_INFO', ''))
    if environ.get('QUERY_STRING'):
        url += '?' + environ['QUERY_STRING']

    start_resposne

    start_response是一个可调用对象,接收两个必选参数和一个可选参数:

      - status: 一个字符串,表示HTTP响应状态字符串,比如'200 OK'、'404 Not Found'
      - headers: 一个列表,包含有如下形式的元组:(header_name, header_value),用来表示HTTP响应的headers
      - exc_info(可选): 用于出错时,server需要返回给浏览器的信息

    start_response必须返回一个write(body_data)
    我们知道HTTP的响应需要包含status,headers和body,所以在application对象将body作为返回值return之前,需要先调用start_response,将status和headers的内容返回给server,这同时也是告诉server,application对象要开始返回body了。

    关系

    由此可见,server负责接收HTTP请求,根据请求数据组装environ,定义start_response函数,将这两个参数提供给application。application根据environ信息执行业务逻辑,将结果返回给server。响应中的status、headers由start_response函数返回给server,响应的body部分被包装成iterable作为application的返回值,server将这些信息组装为HTTP响应返回给请求方。

    在一个完整的部署中,uWSGI和Gunicorn是实现了WSGI的server,Django、Flask是实现了WSGI的application。两者结合起来其实就能实现访问功能。实际部署中还需要Nginx、Apache的原因是它有很多uWSGI没有支持的更好功能,比如处理静态资源,负载均衡等。Nginx、Apache一般都不会内置WSGI的支持,而是通过扩展来完成。比如Apache服务器,会通过扩展模块mod_wsgi来支持WSGI。Apache和mod_wsgi之间通过程序内部接口传递信息,mod_wsgi会实现WSGI的server端、进程管理以及对application的调用。Nginx上一般是用proxy的方式,用Nginx的协议将请求封装好,发送给应用服务器,比如uWSGI,uWSGI会实现WSGI的服务端、进程管理以及对application的调用。

    uWSGI与Gunicorn的比较,由链接可知: 

    在响应时间较短的应用中,uWSGI+django是个不错的组合(测试的结果来看有稍微那么一点优势),但是如果有部分阻塞请求 Gunicorn+gevent+django有非常好的效率, 如果阻塞请求比较多的话,还是用tornado重写吧。

    Middleware

    WSGI除了server和application两个角色外,还有middleware中间件,middleware运行在server和application中间,同时具备server和application的角色,对于server来说,它是一个application,对于application来说,它是一个server:

    from piglatin import piglatin
    
    class LatinIter:
    
        def __init__(self, result, transform_ok):
            if hasattr(result, 'close'):
                self.close = result.close
            self._next = iter(result).__next__
            self.transform_ok = transform_ok
    
        def __iter__(self):
            return self
    
        def __next__(self):
            if self.transform_ok:
                return piglatin(self._next())   # call must be byte-safe on Py3
            else:
                return self._next()
    
    class Latinator:
    
        # by default, don't transform output
        transform = False
    
        def __init__(self, application):
            self.application = application
    
        def __call__(self, environ, start_response):
    
            transform_ok = []
    
            def start_latin(status, response_headers, exc_info=None):
    
                # Reset ok flag, in case this is a repeat call
                del transform_ok[:]
    
                for name, value in response_headers:
                    if name.lower() == 'content-type' and value == 'text/plain':
                        transform_ok.append(True)
                        # Strip content-length if present, else it'll be wrong
                        response_headers = [(name, value)
                            for name, value in response_headers
                                if name.lower() != 'content-length'
                        ]
                        break
    
                write = start_response(status, response_headers, exc_info)
    
                if transform_ok:
                    def write_latin(data):
                        write(piglatin(data))   # call must be byte-safe on Py3
                    return write_latin
                else:
                    return write
    
            return LatinIter(self.application(environ, start_latin), transform_ok)
    
    
    from foo_app import foo_app
    run_with_cgi(Latinator(foo_app))

    可以看出,Latinator调用foo_app充当server角色,然后实例被run_with_cgi调用充当application角色。

    uWSGI、uwsgi与WSGI的区别

      - uwsgi:与WSGI一样是一种通信协议,是uWSGI服务器的独占协议,据说该协议是fastcgi协议的10倍快。

      - uWSGI:是一个web server,实现了WSGI协议、uwsgi协议、http协议等。

    Django中WSGI的实现

    每个Django项目中都有个wsgi.py文件,作为application是这样实现的:

    from django.core.wsgi import get_wsgi_application
    application = get_wsgi_application()

    源码:

    from django.core.handlers.wsgi import WSGIHandler
    
    def get_wsgi_application():
    
        django.setup(set_prefix=False)
        return WSGIHandler()

    WSGIHandler:

    class WSGIHandler(base.BaseHandler):
        request_class = WSGIRequest
    
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.load_middleware()
    
        def __call__(self, environ, start_response):
            set_script_prefix(get_script_name(environ))
            signals.request_started.send(sender=self.__class__, environ=environ)
            request = self.request_class(environ)
            response = self.get_response(request)
    
            response._handler_class = self.__class__
    
            status = '%d %s' % (response.status_code, response.reason_phrase)
            response_headers = [
                *response.items(),
                *(('Set-Cookie', c.output(header='')) for c in response.cookies.values()),
            ]
            start_response(status, response_headers)
            if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'):
                response = environ['wsgi.file_wrapper'](response.file_to_stream)
            return response

    application是一个定义了__call__方法的WSGIHandler类实例,首先加载中间件,然后根据environ生成请求request,根据请求生成响应response,status和response_headers由start_response处理,然后返回响应body。

    Django也自带了WSGI server,当然性能不够好,一般用于测试用途,运行runserver命令时,Django可以起一个本地WSGI server,django/core/servers/basehttp.py文件:

    def run(addr, port, wsgi_handler, ipv6=False, threading=False, server_cls=WSGIServer):
        server_address = (addr, port)
        if threading:
            httpd_cls = type('WSGIServer', (socketserver.ThreadingMixIn, server_cls), {})
        else:
            httpd_cls = server_cls
        httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6)
        if threading:
            httpd.daemon_threads = True
        httpd.set_app(wsgi_handler)
        httpd.serve_forever() 

    实现的WSGIServer,继承自wsgiref:

    class WSGIServer(simple_server.WSGIServer):
        """BaseHTTPServer that implements the Python WSGI protocol"""
    
        request_queue_size = 10
    
        def __init__(self, *args, ipv6=False, allow_reuse_address=True, **kwargs):
            if ipv6:
                self.address_family = socket.AF_INET6
            self.allow_reuse_address = allow_reuse_address
            super().__init__(*args, **kwargs)
    
        def handle_error(self, request, client_address):
            if is_broken_pipe_error():
                logger.info("- Broken pipe from %s
    ", client_address)
            else:
                super().handle_error(request, client_address)
    

    参考链接

      - pep-3333

      - WSGI简介

      - Python Web开发最难懂的WSGI协议,到底包含哪些内容

  • 相关阅读:
    fedora上部署ASP.NET——(卡带式电脑跑.NET WEB服务器)
    SQL Server 请求失败或服务未及时响应。有关详细信息,请参见事件日志或其它适合的错误日志
    8086CPU的出栈(pop)和入栈(push) 都是以字为单位进行的
    FTP 服务搭建后不能访问问题解决
    指定的 DSN 中,驱动程序和应用程序之间的体系结构不匹配
    Linux 安装MongoDB 并设置防火墙,使用远程客户端访问
    svn Please execute the 'Cleanup' command. 问题解决
    .net 操作MongoDB 基础
    oracle 使用绑定变量极大的提升性能
    尝试加载 Oracle 客户端库时引发 BadImageFormatException。如果在安装 32 位 Oracle 客户端组件的情况下以 64 位模式运行,将出现此问题。
  • 原文地址:https://www.cnblogs.com/linxiyue/p/10800020.html
Copyright © 2011-2022 走看看