美文网首页
Flask 源码之WSGI

Flask 源码之WSGI

作者: will_7c3c | 来源:发表于2023-02-22 13:26 被阅读0次

    WSGI

    WSGI是python容器服务器和python web app通信协议标准

    server负责去进行http层的处理,外部看来,server接受client端请求

    返回http response

    而中间处理过程则是调用web app

    WSGI就是调用标准,规定了app暴露的接口标准和返回标准以及服务器传参标准

    这样一来不同的app和server之间能够相互统一交互

    其中对app的要求是

    1. callable
    2. 结果iterable
    3. 接受server的 两个参数 : environ环境参数, start_response 生成标准http包头的函数

    callable好理解,实际上的app可以理解为server处理http request 的逻辑处理函数,所以要求一定是可以被服务器调用的,需要暴露调用接口

    iterable 要和start_response结合起来理解, iterable 的结果 我们将其用data简写 data[], 实际上这个data[]是http body, server 不停迭代,data[] 然后传输内容

    和start_response相结合,start_response也是一个callable,接受两个必须的参数,status(HTTP状态)和response_headers(响应消息的头).实际上返回的就是http header

    在wsgi app 内部, return data[]之前要先调用,但是不会立刻返回包头给 server, 而一定要等到迭代 data[]到第一个非空对象以后才会传

    换句话说,如果这时候迭代data[]出错了,你先传过去包头状态里写的是200那不就完犊子了
    所以一定要先等到迭代到第一个非空对象以后,这个头才会被传过去

    这里表述有问题,是一定在传data[]给server之前,先传start_response,也就是先传头,但是这时候server没有直接把这个header发给client,而是等body内容
    body一定不能为空,不能只给一个头

    这里start_response还有一个可选参数也就是exc_info,当处理请求的过程遇到错误时,这个参数会被设置,同时调用 start_response.如果这时候headers 还在 start_response内没出来呢,可以用这个参数直接去设置header 头, 也就是把status code改为其他错误代码
    如果已经输出到server了,则需要raise 一个 error 让 服务器 去处理,跳出应用.

    为了避免循环引用,start_response实现时需要保证 exc_info在函数调用后不再包含引用。 也就是说start_response用完 exc_info后,需要保证执行一句

    exc_info = None
    

    释放掉引用.

    下面来一个示例简陋的WSGI程序

    def application(environ, start_response): 
        status = '200 OK' 
        output = 'World!' 
        response_headers = [('Content-type', 'text/plain'), 
                            ('Content-Length', str(12)] 
        write = start_response(status, response_headers) 
        write('Hello ') 
        return [output]
    

    关于这个write,我看了一下start_response的返回对象也就是理论上的header,实际也是一个callable,
    形式为write(body_data)
    不过理论上,下面这么写应该更合理

    def application(environ, start_response): 
        status = '200 OK' 
        output = 'Hello,World!' 
        response_headers = [('Content-type', 'text/plain'), 
                            ('Content-Length', str(12)] 
        start_response(status, response_headers) 
        return [output]
    

    output就是http body
    startresponse就是header,这样子更易接受
    以上参考:
    wsgiref 源代码分析 --start_response()

    进一步理解WSGI对app端的定义

    1. callable
    2. return iterable
    3. 接受environ,start_response

    callable一定是function么? 类或是对象实现了callable不也可以?
    iterable 一定要是[],{}这种基本数据结构么?实现iter不也可以?

    至于接收参数就更简单了

    所以从wsgi规则上,我们可以定义出的不止是app可以是对象,或是直接是类如下

    # 1. 可调用对象是一个函数
    def application(environ, start_response):
    
       response_body = 'The request method was %s' % environ['REQUEST_METHOD']
    
       # HTTP response code and message
       status = '200 OK'
    
       # 应答的头部是一个列表,每对键值都必须是一个 tuple。
       response_headers = [('Content-Type', 'text/plain'),
                           ('Content-Length', str(len(response_body)))]
    
       # 调用服务器程序提供的 start_response,填入两个参数
       start_response(status, response_headers)
    
       # 返回必须是 iterable
       return [response_body]    
    
    # 2. 可调用对象是一个类
    class AppClass:
        """这里的可调用对象就是 AppClass 这个类,调用它就能生成可以迭代的结果。
            使用方法类似于: 
            for result in AppClass(env, start_response):
                 do_somthing(result)
        """
    
        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!\n"
    
    # 3. 可调用对象是一个实例 
    class AppClass:
        """这里的可调用对象就是 AppClass 的实例,使用方法类似于: 
            app = AppClass()
            for result in app(environ, start_response):
                 do_somthing(result)
        """
    
        def __init__(self):
            pass
    
        def __call__(self, environ, start_response):
            status = '200 OK'
            response_headers = [('Content-type', 'text/plain')]
            self.start(status, response_headers)
            yield "Hello world!\n"
    

    严格意义讲,这个标准是允许嵌套的,可以更进一步

    也就是server调用一个app,但是app继续往下调用app.
    从结果来看,中间这个app是一个中间件,对服务器来说是app,对app来说是服务器.

    PEP333给这个嵌套就定义为中间件,并给出了假设的场景

    • Routing a request to different application objects based on the target URL, after rewriting the environ accordingly.
    • Allowing multiple applications or frameworks to run side by side in the same process
    • Load balancing and remote processing, by forwarding requests and responses over a network
    • Perform content postprocessing, such as applying XSL stylesheets

    直接翻译一下

    • 根据 url 把请求给到不同的客户端程序(url routing),在把environ 改写以后
    • 允许多个客户端程序/web 框架同时运行,就是把接到的同一个请求传递给多个程序。
    • 负载均衡和远程处理:把请求在网络上传输
    • 应答的过滤处理

    PEP333直接给了一个中间件使用例子,但是不是那么多直观
    大概意思是先实现了一个迭代器类LatinIter,然后实现了一个类Latinator,在这个类中实现了callable,设置了对environ和start_response的处理
    并且指定了接下来要调用的可能的self.app

    最后实验调用这个Lationtor作为中间件,处理foo_app
    服务器环境是cgi

    PEP333
    这里面同时演示说明了,处理response头的过程中如果出意外了该怎么办.

    from piglatin import piglatin
    
    class LatinIter:
    
        """Transform iterated output to piglatin, if it's okay to do so
    
        Note that the "okayness" can change until the application yields
        its first non-empty string, so 'transform_ok' has to be a mutable
        truth value.
        """
    
        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())
            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))
                    return write_latin
                else:
                    return write
    
            return LatinIter(self.application(environ, start_latin), transform_ok)
    
    
    # Run foo_app under a Latinator's control, using the example CGI gateway
    from foo_app import foo_app
    run_with_cgi(Latinator(foo_app))
    

    我觉得上面这个例子不够直观,同时验证了好几条,我们看下面这个例子就是将路由设置为中间件,更容易理解

    class Router(object):
        def __init__(self):
            self.path_info = {}
        def route(self, environ, start_response):
            application = self.path_info[environ['PATH_INFO']]
            return application(environ, start_response)
        def __call__(self, path):
            def wrapper(application):
                self.path_info[path] = application
            return wrapper
    
    router = Router()
    
    ## 上面是中间件router,实际是一个wsgi app
    
    #here is the application
    @router('/hello')    #调用 route 实例,把函数注册到 paht_info 字典
    def hello(environ, start_response):
        status = '200 OK'
        output = 'Hello'
        response_headers = [('Content-type', 'text/plain'),
                            ('Content-Length', str(len(output)))]
        write = start_response(status, response_headers)
        return [output]
    
    @router('/world')
    def world(environ, start_response):
        status = '200 OK'
        output = 'World!'
        response_headers = [('Content-type', 'text/plain'),
                            ('Content-Length', str(len(output)))]
        write = start_response(status, response_headers)
        return [output]
    
    #here run the application
    result = router.route(environ, start_response)
    for value in result: 
        write(value)
    

    以上来自博客python wsgi简介
    我觉得更容易理解一些
    这里还有一点,我看到这个路由实现的时候惊呆了,怀疑是否flask里的路由也是这么实现的,hh但不是,
    那么为什么不这么做呢?
    这就是一个问题了,我需要问问老师,
    效率问题么?还是这么一来解耦太厉害了?

    但确实前后端分离中,后端只负责开发RESTfulAPI的话,连路由都不用写的,只需要处理数据库和暴露API给前段就好,node和vue自然会处理好路由.

    我们下一步开始分析流程

    相关文章

      网友评论

          本文标题:Flask 源码之WSGI

          本文链接:https://www.haomeiwen.com/subject/lmjakdtx.html