浅谈flask源码之请求过程
Flask
Flask是什么?
Flask是一个使用Python编写的轻量级Web应用框架,让我们可以使用Python语言快速搭建Web服务,Flask也被称为"microframework",因为它使用简单的核心,用extension增加其他功能
为什么选择Flask?
我们先来看看python现在比较流行的web框架
- Flask
- Django
- Tornado
- Sanic
Flask:轻,组件间松耦合,自由、灵活,可扩展性强,第三方库的选择面广的同时也增加了组件间兼容问题
Django:Django相当于一个全家桶,几乎包括了所有web开发用到的模块(session管理、CSRF防伪造请求、Form表单处理、ORM数据库对象化、模板语言),但是相对应的会造成一个紧耦合的情况,对第三方插件不太友好
Tornado:底层通过eventloop来实现异步处理请求,处理效率高,学习难度大,处理稍有不慎很容易阻塞主进程导致不能正常提供服务,新版本也支持asyncio
Sanic:一个类Flask框架,但是底层使用uvloop进行异步处理,可以使用同步的方式编写异步代码,而且运行效率十分高效.
WSGI
先来看看维基百科对WSGI的定义
Web服务器网关接口(PythonWebServerGatewayInterface,缩写为WSGI)是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口.
何为网关,即从客户端发出的每个请求(数据包)第一个到达的地方,然后再根据路由进行转发处理.而对于服务端发送过来的消息,总是先通过网关层,然后再转发至客户端
那么可想而知,WSGI其实是作为一个网关接口,来接受Server传递过来的信息,然后通过这个接口调用后台app里的viewfunction进行响应.
先看一段有趣的对话:
Nginx:Hey,WSGI,我刚收到了一个请求,我需要你作些准备,然后由Flask来处理这个请求.
WSGI:OK,Nginx.我会设置好环境变量,然后将这个请求传递给Flask处理.
Flask:Thanks.WSGI给我一些时间,我将会把请求的响应返回给你.
WSGI:Alright,那我等你.
Flask:Okay,我完成了,这里是请求的响应结果,请求把结果传递给Nginx.
WSGI:Goodjob!Nginx,这里是响应结果,已经按照要求给你传递回来了.
Nginx:Cool,我收到了,我把响应结果返回给客户端.大家合作愉快~
对话里面可以清晰了解到WSGI、nginx、Flask三者的关系
下面来看看Flask中的wsgi接口(注意:每个进入Flask的请求都会调用Flask.__call__)
#摘自Flask源码app.py classFlask(_PackageBoundObject): #中间省略 def__call__(self,environ,start_response): returnself.wsgi_app(environ,start_response) defwsgi_app(self,environ,start_response): #environ:一个包含全部HTTP请求信息的字典,由WSGIServer解包HTTP请求生成 #start_response:WSGIServer提供的函数,调用可以发送响应的状态码和HTTP报文头, #函数在返回前必须调用一次. :paramenviron:AWSGIenvironment. :paramstart_response:Acallableacceptingastatuscode, alistofheaders,andanoptionalexceptioncontextto starttheresponse. #创建上下文 ctx=self.request_context(environ) error=None try: try: #把上下文压栈 ctx.push() #分发请求 response=self.full_dispatch_request() exceptExceptionase: error=e response=self.handle_exception(e) except: error=sys.exc_info()[1] raise #返回结果 returnresponse(environ,start_response) finally: ifself.should_ignore_error(error): error=None #上下文出栈 ctx.auto_pop(error)
wsgi_app中定义的就是Flask处理一个请求的基本流程,
1.创建上下文
2.把上下文入栈
3.分发请求
4.上下文出栈
5.返回结果
其中response=self.full_dispatch_request()请求分发的过程我们需要关注一下
#摘自Flask源码app.py classFlask(_PackageBoundObject): #中间省略 deffull_dispatch_request(self): self.try_trigger_before_first_request_functions() try: request_started.send(self) rv=self.preprocess_request() ifrvisNone: rv=self.dispatch_request() exceptExceptionase: rv=self.handle_user_exception(e) returnself.finalize_request(rv) defdispatch_request(self): req=_request_ctx_stack.top.request ifreq.routing_exceptionisnotNone: self.raise_routing_exception(req) rule=req.url_rule ifgetattr(rule,'provide_automatic_options',False)\ andreq.method=='OPTIONS': returnself.make_default_options_response() returnself.view_functions[rule.endpoint](**req.view_args) deffinalize_request(self,rv,from_error_handler=False): response=self.make_response(rv) try: response=self.process_response(response) request_finished.send(self,response=response) exceptException: ifnotfrom_error_handler: raise self.logger.exception('Requestfinalizingfailedwithan' 'errorwhilehandlinganerror') returnresponse
我们可以看到,请求分发的操作其实是由dispatch_request来完成的,而在请求进行分发的前后我们可以看到Flask进行了如下操作:
1.try_trigger_before_first_request_functions,首次处理请求前的操作,通过@before_first_request定义,可以进行数据库连接
2.preprocess_request,每次处理请求前进行的操作,通过@before_request来定义,可以拦截请求
3.process_response,每次正常处理请求后进行的操作,通过@after_request来定义,可以统计接口访问成功的数量
4.finalize_request,把视图函数的返回值转换成一个真正的响应对象
以上的这些是Flask提供给我们使用的钩子(hook),可以根据自身需求来定义,
而hook中还有@teardown_request,是在每次处理请求后执行(无论是否有异常),所以它是在上下文出栈的时候被调用
如果同时定义了四种钩子(hook),那么执行顺序应该是
graphLR
before_first_request-->before_request
before_request-->after_request
after_request-->teardown_request
在请求函数和钩子函数之间,一般通过全局变量g实现数据共享
现在的处理流程就变为:
1.创建上下文
2.上下文入栈
3.执行before_first_request操作(如果是第一次处理请求)
4.执行before_request操作
5.分发请求
6.执行after_request操作
7.执行teardown_request操作
8.上下文出栈
9.返回结果
其中3-7就是需要我们完成的部分.
如何使用Flask
上面我们知道,Flask处理请求的步骤,那么我们来试试
fromflaskimportFlask app=Flask(__name__) @app.before_first_request defbefore_first_request(): print('before_first_requestrun') @app.before_request defbefore_request(): print('before_requestrun') @app.after_request defafter_request(param): print('after_requestrun') returnparam @app.teardown_request defteardown_request(param): print('teardown_requestrun') @app.route('/') defhello_world(): return'HelloWorld!' if__name__=='__main__': app.run()
当运行flask进程时,访问127.0.0.1:5000,程序输出,正好认证了我们之前说的执行顺序.
before_first_requestrun
before_requestrun
after_requestrun
teardown_requestrun
127.0.0.1--[03/May/201818:42:52]"GET/HTTP/1.1"200-
路由分发
看了上面的代码,我们可能还是会有疑问,为什么我们的请求就会跑到helloworld函数去处理呢?我们先来普及几个知识点:
- url:客户端访问的网址
- view_func:即我们写的视图函数
- rule:定义的匹配路由的地址
- url_map:存放着rule与endpoint的映射关系
- endpoint:可以看作为每个view_func的ID
- view_functions:一个字典,以endpoint为key,view_func为value
添加路由的方法:
1.@app.route
2.add_url_rule
我们先来看看@app.route干了什么事情
#摘自Flask源码app.py classFlask(_PackageBoundObject): #中间省略 defroute(self,rule,**options): defdecorator(f): endpoint=options.pop('endpoint',None) self.add_url_rule(rule,endpoint,f,**options) returnf returndecorator
我们可以看到,route函数是一个装饰器,它在执行时会先获取endpoint,然后再通过调用add_url_rule来添加路由,也就是说所有添加路由的操作其实都是通过add_url_rule来完成的.下面我们再来看看add_url_rule.
#摘自Flask源码app.py classFlask(_PackageBoundObject): #中间省略 #定义view_functions self.view_functions={} #定义url_map self.url_map=Map() defadd_url_rule(self,rule,endpoint=None,view_func=None, provide_automatic_options=None,**options): #创建rule rule=self.url_rule_class(rule,methods=methods,**options) rule.provide_automatic_options=provide_automatic_options #把rule添加到url_map self.url_map.add(rule) ifview_funcisnotNone: old_func=self.view_functions.get(endpoint) ifold_funcisnotNoneandold_func!=view_func: raiseAssertionError('Viewfunctionmappingisoverwritingan' 'existingendpointfunction:%s'%endpoint) #把view_func添加到view_functions字典 self.view_functions[endpoint]=view_func
可以看到,当我们添加路由时,会生成一个rule,并把它存放到url_map里头,然后把view_func与其对应的endpoint存到字典.
当一个请求进入时,Flask会先根据用户访问的Url到url_map里边根据rule来获取到endpoint,然后再利用view_functions获取endpoint在里边所对应的视图函数
graphLR
url1-->url_map
url2-->url_map
url3-->url_map
urln-->url_map
url_map-->endpoint
endpoint-->view_functions
上下文管理
下面我们再来看看之前一直忽略的上下文,什么是上下文呢?
上下文即语境、语意,是一句话中的语境,也就是语言环境.一句莫名其妙的话出现会让人不理解什么意思,如果有语言环境的说明,则会更好,这就是语境对语意的影响.而对应到程序里往往就是程序中需要共享的信息,保存着程序运行或交互中需要保持或传递的信息.
Flask中有两种上下文分别为:应用上下文(AppContext)和请求上下文(RequestContext).按照上面提到的我们很容易就联想到:应用上下文就是保存着应用运行或交互中需要保持或传递的信息,如当前应用的应用名,当前应用注册了什么路由,又有什么视图函数等.而请求上下文就保存着处理请求过程中需要保持或传递的信息,如这次请求的url是什么,参数又是什么,请求的method又是什么等.
我们只需要在需要用到这些信息的时候把它从上下文中取出来即可.而上下文是有生命周期的,不是所有时候都能获取到.
上下文生命周期:
- RequestContext:生命周期在处理一次请求期间,请求处理完成后生命周期也就结束了.
- AppContext:生命周期最长,只要当前应用还在运行,就一直存在.(应用未运行前并不存在)
那么上下文是在什么时候创建的呢?我们又要如何创建上下文:刚才我们提到,在wsgi_app处理请求的时候就会先创建上下文,那个上下文其实是请求上下文,那应用上下文呢?
#摘自Flask源码ctx.py classRequestContext(object): #中间省略 defpush(self): top=_request_ctx_stack.top iftopisnotNoneandtop.preserved: top.pop(top._preserved_exc) #获取应用上下文 app_ctx=_app_ctx_stack.top #判断应用上下文是否存在并与当前应用一致 ifapp_ctxisNoneorapp_ctx.app!=self.app: #创建应用上下文并入栈 app_ctx=self.app.app_context() app_ctx.push() self._implicit_app_ctx_stack.append(app_ctx) else: self._implicit_app_ctx_stack.append(None) ifhasattr(sys,'exc_clear'): sys.exc_clear() #把请求上下文入栈 _request_ctx_stack.push(self)
我们知道当有请求进入时,Flask会自动帮我们来创建请求上下文.而通过上述代码我们可以看到,在创建请求上下文时会有一个判断操作,如果应用上下文为空或与当前应用不匹配,那么会重新创建一个应用上下文.所以说一般情况下并不需要我们手动去创建,当然如果需要,你也可以显式调用app_context与request_context来创建应用上下文与请求上下文.
那么我们应该如何使用上下文呢?
fromflaskimportFlask,request,g,current_app app=Flask(__name__) @app.before_request defbefore_request(): print'before_requestrun' g.name="Tom" @app.after_request defafter_request(response): print'after_requestrun' print(g.name) returnresponse @app.route('/') defindex(): print(request.url) g.name='Cat' print(current_app.name) if__name__=='__main__': app.run()
访问127.0.0.1:5000时程序输出
before_requestrun
http://127.0.0.1:5000/
flask_run
after_requestrun
Cat
127.0.0.1--[04/May/201818:05:13]"GET/HTTP/1.1"200-
代码里边应用到的current_app和g都属于应用上下文对象,而request就是请求上下文.
- current_app表示当前运行程序文件的程序实例
- g:处理请求时用作临时存储的对象.每次请求都会重设这个变量生命周期同RequestContext
- request代表的是当前的请求
那么随之而来的问题是:这些上下文的作用域是什么?
线程有个叫做ThreadLocal的类,也就是通常实现线程隔离的类.而werkzeug自己实现了它的线程隔离类:werkzeug.local.Local.而LocalStack就是用Local实现的.
这个我们可以通过globals.py可以看到
#摘自Flask源码globals.py fromfunctoolsimportpartial fromwerkzeug.localimportLocalStack,LocalProxy _request_ctx_stack=LocalStack() _app_ctx_stack=LocalStack() current_app=LocalProxy(_find_app) request=LocalProxy(partial(_lookup_req_object,'request')) session=LocalProxy(partial(_lookup_req_object,'session')) g=LocalProxy(partial(_lookup_app_object,'g'))
_lookup_app_object思就是说,对于不同的线程,它们访问这两个对象看到的结果是不一样的、完全隔离的.Flask通过这样的方式来隔离每个请求.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。