浅析WSGI和Flask

一、初识WSGI:一个接口

PythonWeb服务器网关接口(Python Web Server Gateway Interface,缩写为WSGI)是Python应用程序或框架和Web服务器之间的一种接口,已经被广泛接受, 它已基本达成它的可移植性方面的目标。2003年PEP333协议定义了WSGI,目前协议最新的版本是PEP3333(https://www.python.org/dev/peps/pep-3333/),已经替代了原有的PEP333协议,那就从协议入手来了解WSGI协议及其原理。

在协议的abstract中,描述了WSGI的目的:
This document specifies a proposed standard interface between web servers and Python web applications or frameworks, to promote web application portability across a variety of web servers.

WSGI定义的是一个标准接口,用于web服务器和应用/框架之间的交互。早期web应用开发的框架选择多,不统一,对于开发者来说难以选择,而且和服务器端框架强耦合,于是将JAVA的servlet API模式引入了进来,在这个时期又诞生了好几种API,而经过时代更迭,WSGI这种简单易用的API最终得到广泛认可,目前主流的Python web框架都是基于WSGI。关于WSGI,协议中反复强调,只是定义了一个接口,不管是server还是app,都需要开发者自行设计,它存在的目的,下面这句话说得非常明了: the goal of WSGI is to facilitate easy interconnection of existing servers and applications or frameworks, not to create a new web framework.

二、2+2=?

下面就来认识下这个接口,The WSGI interface has two sides: the “server” or “gateway” side, and the “application” or “framework” side. 既然是接口,有两面是毋庸置疑的,只是下面这句话比较拗口:The server side invokes a callable object that is provided by the application side.

看文字比较困难,但是看函数示例,就是一目了然,run_with_cgi是server端的接口函数,传入的参数也是一个函数,并且是由应用端(application)提供的。

def run_with_cgi(application):
    ....

当Client端发起一个HTTP请求的时候,这个函数将被触发,(应该是某种中断去触发的,没有去考证),进而开始一次报文的处理,最终返回一个response给客户,展示到客户的浏览器上。协议举了一个实际的简单的CGI网关的代码实例,源码就不贴出来了,大家可以在协议上面去看,这里把主要的步骤分析下:

def run_with_cgi(application):
    #1
    environ = ......   

    #2 处理的是HTTP响应包中的包头,以及返回的状态码
    def start_response(status, response_headers, exc_info=None): 
    ...

    #3 调用application
    result = application(environ, start_response)

    #4 处理结果
    def write(data):
    .......
    try:
        for data in result:
                write(data)
    finally:
            result.close()

1、预置环境参数,这个需要设置好送给application

2、定义一个闭包的start_response函数,这个非常重要,是真正的server和application之间的接口,不管你是什么server什么框架,函数名称必须叫start_response

3、调用application,这个就是应用端提供的回调函数,在服 务端,完全不知道它的实现形式,可以是一个函数,或者是一个自己实 现call方法的类实例,由应用端的开发者决定。这句话的重点在 于,必须把environ和start_response作为参数传入,在 application中,得到status, response_headers后,需要调 用start_response来完成处理。这里的调用关系比较绕,看起来是 服务器端的中的run_with_cgi调应用端的application, application又来调run_with_cgi中的闭包函数 start_response,感觉非常凌乱。但实际上,start_response 和run_with_cgi关系并不大,我们完全可以把它拎出来作为一个单 独的函数,来看待这个调用关系,就是run_with_cgi- >application->start_response三个函数的单线流程,只是因 为它们处于不同的层次,函数只能用回调的方式来传递。文字描述容易 绕晕,下面有图解,看下箭头就知道怎么回事了。

4、处理result。站在server端来看,调用application所产生的效果有两个,第一个就是第三点讲到的,start_response被回调了,处理了状态和响应头。第二个就是这里的result,result里面装的是响应体,其中重要的东西就是被渲染后的网页内容。这时候,把状态、响应头、响应体包装一下,组成一个response,返回给客户端,处理的结果就呈现在客户的网页上面了,服务器端的处理也就大功告成。当然代码示例中,又给出了一个write的函数,实质上就是把结果写入内存,包不包成函数并不是关键因素。

下面再来看看application的处理:

def application(environ, start_response):
    '''1、获取status, response_headers,通过回调
    start_response送给服务器端'''
    status, response_headers = process(environ)
    start_response(status, response_headers) 
    '''通过调用server端的start_response函数,将状态码和报文头交给server端处理.'''

    '''2、获取响应内容,也就是渲染的结果等等,通过return 的方式返回给服务器'''
    data = render()
    return data

通过以上的分析,可以把一次获取request到返回response的过程总结为“两次回调,两次处理”,两次回调是run调用application,以及application调用start_response;两次处理是处理status, response_headers,以及处理响应体,及本段的标题2+2。当然实际的处理步骤很多,但可以把这些细节囊括在其中。下面画了张图来总结其调用关系,而基于这种低耦合度的调用关系,不管是现有框架,还是新开发的,都能够很容易地套用进来,2+2的模式能够产生的多少可能性?不知道…
这里写图片描述

三、异曲同工——中间件的提出

定义了WSGI,可以轻松地实现服务器端和应用端的对接,然而在应用端,一些处理是统一而且与我们的业务逻辑并没有关系,比如URL的路由,在一个进程中实现多个应用的并发处理,返回内容的预处理,负载均衡…..

因此可以考虑写一部分代码,来实现这些统一的处理,让开发者更专注于自己的业务逻辑,于是中间件便应运而生。这部分代码处在服务器和业务应用之间,站在服务器的角度,它是被划分在了WSGI的应用端,而在业务应用的角度上,它又像是一个提供了统一功能的服务器。PEP3333下面这段话比较有意思:

A user who desires to incorporate middleware into an application simply provides the middleware component to the server, as if it were an application, and configures the middleware component to invoke the application, as if the middleware component were a server.

其中用到了虚拟语气,as if it were an application,as if the middleware component were a server.说明它既不是应用程序,也不是服务器,它就叫中间件,它位于WSGI的应用端,为更上层的web开发提供基础服务。

对于中间件的要求,协议用下面这段话来描述:For the most part, middleware must conform to the restrictions and requirements of both the server and application sides of WSGI. In some cases, however, requirements for middleware are more stringent than for a “pure” server or application, and these points will be noted in the specification.

第一句话的英文翻译:中间件必须同时满足WSGI标准中,服务器和应用两端的限制和要求。对于这句话的解读,首先,服务器并不知道应用端长什么样,它只认识接口,因此和服务器交互必须遵守WSGI。但是另外一方面,中间件和上层应用之间,是没有这个限制的。而现实情况也是这样,目前的web开发和框架是强耦合的,我们写的基于flask的代码在django上是跑不起来的。

中间件和上层的接口方式并没有限制,就可以任意发挥了,当然PEP3333给出的例子,还是延续了2+2的模式,一张图说话,代码就自己看了。为了突出调用关系,图中的数据处理就没有写了。另外协议中还提到,中间这一层可以是一个middleware stack,比如你觉得app还可以拆分,就可以自己再写个middleware_2/middleware_3……,随你天马行空,和上下定义好调用关系就可以。
这里写图片描述

四、Flask的解读

PEP3333后面定义了很多的细则,暂时没有时间看了,而且细则也并不影响对它的理解。于是接下来就分析下flask是怎样的一个中间件,来满足WSGI,并且为我们的web开发提供帮助的。

在这个架构中,服务器端使用的是werkzeug,同样是一个支持WSGI的服务器框架,Flask我们接触比较多,也就是前面PEP3333协议中定义的中间件,而app就是我们自己写的代码,处理页面渲染,数据库操作之类的动作。

1、服务器加载

在flask中,当调用app.run()的时候,服务器就加载起来了,我们可以看到实际上它是调用的werkzeug的run_simple,说明服务的启动是由我们自己的app来控制的,但是最终的进程是跑在了服务器端,也符合对服务器的定义:
这里写图片描述

另外注意在调用的时候,第三个参数是self,也就是flask把自己这个对象传给了服务器,此时WSGI中的application,就是flask的这个对象!

最终在werkzueg中的BaseWSGIServer中,进行了这样的赋值。
这里写图片描述

2、响应的触发和调用

在WSGIRequestHandler这个类中,有一个handle_one_request,这个应该是被HTTP请求中断触发的,它最终会调用run_wsgi这个函数,也就对应到WSGI协议中,服务器端的run函数。

下面来看看这个函数,可以看到很多熟悉的身影,首先,调用之前保存的self.server.app(environ, start_response),flask翻过来回调start_response,处理status和header,最后app的返回值是application_iter,是个可迭代对象,说明支持多个返回结果,最终write一下返回结果。整个过程和之前WSGI的流程一模一样。
这里写图片描述

3、Flask的WSGI接口

Flask类中,定义了自己的call方法,因此可以保证Werkzeug可以调用到,这点在协议中也讲到了:The term “object” should not be misconstrued as requiring an actual object instance: a function, method, class, or instance with a call method are all acceptable for use as an application object.

通过调用关系,最终是通过full_dispatch_request中得到了一个response,来调实现WSGI模型中application的真正调用。其实到此为止,flask和Werkzeug(middleware和server)的接口就已经明了了。里面具体的实现,都是flask和上层app的操作,或者就是它自身的事情。
这里写图片描述

4、和app的联系

下面来看看full_dispatch_request,preprocess_request是做的url路由解析的预处理,这个就不看了,重点关注下dispatch_request和finalize_request,前者是渲染处理,是flask这个中间件和上层app发生关联的地方,后者是response对象的封装,是中间件的内部动作,即前文讲到的统一操作。
这里写图片描述

dispatch_request中,首先把rule解析出来,就是http://127.0.0.1:5000/login中的login,然后在view_function列表中找到对应的视图函数对象,并带上(**req.view_args)来调用这个函数。而view_function中,存放的就是我们通过@app.route装饰器注册进来的视图函数,用过flask的地球人都知道,接下来的事情就是渲染,数据库操作之类的app应用程序,这里就不讲了。至此,flask和app(middleware和app)之间的关联,也通过view_functions和放在里面的视图函数建立起来了。
这里写图片描述

5、返回响应

在finalize_request中,会生成一个response对象,这个对象最终是继承子werkzueg的BaseResponse,看来仅仅靠一个WSGI,服务器和应用端的完全解耦还是做不到,不过协议里面的描述用的也只是loosely-coupled,松散的耦合,描述还是很精准的。
这里写图片描述

在werkzueg的BaseResponse中,提供了call方法,status,headers,start_response的调用,应有尽有,不过这个东东是在flask中创建对象并且被调用,而提供工具的又是自己,感觉怪怪的,就像自己有碗筷不用,要让别人用自己的碗筷喂自己吃饭…..
这里写图片描述

而后,返回response给服务器,再发送出去,一次响应完美结束。最后来张图,展现下整个过程吧:
这里写图片描述

第一次在CSDN上面发博客,发现写得貌似有点太冗长了…….

展开阅读全文

没有更多推荐了,返回首页