Flask request,g,session的实现原理

最近一直在研究Flask,由于gfirefly中提供的Http接口使用了Flask,以前都是写一些游戏中简单的操作,最近涉及到Flask的方面比较多,所以就认真研究了下。对Flask的request context和app context略有心得,所以和小伙伴们分享一下Flask的request原理。

在我们视图中要使用request时只需要from flask import request就可以了
很好奇在多线程的环境下,是如何保证request没有混乱的
在flask.globals.py中

[python]  view plain  copy
  1. def _lookup_req_object(name):  
  2.     top = _request_ctx_stack.top  
  3.     if top is None:  
  4.         raise RuntimeError('working outside of request context')  
  5.     return getattr(top, name)  
  6.   
  7. _request_ctx_stack = LocalStack()  
  8. request = LocalProxy(partial(_lookup_req_object, 'request'))  
  9. session = LocalProxy(partial(_lookup_req_object, 'session'))  
其实可以看到不管request还是session最后都是通过getattr(top, name)获取的,也就是说肯定有一个上下文

对象同时保持request和session。我们只要一处导入request,在任何视图函数中都可以使用request,

关键是每次的都是不同的request对象,说明获取request对象肯定是一个动态的操作,不然肯定都是相同的request。这里的魔法就是_lookup_req_object函数和LocalProxy组合完成的。

LocalProxy是werkzeug.local.py中定义的一个代理对象,它的作用就是将所有的请求都发给内部的_local对象

[python]  view plain  copy
  1. class LocalProxy(object):  
  2.     def __init__(self, local, name=None):  
  3.         #LocalProxy的代码被我给简化了,这里的local不一定就是local.py中定义的线程局部对象,也可以是任何可调用对象  
  4.         #在我们的request中传递的就是_lookup_req_object函数  
  5.         object.__setattr__(self'_LocalProxy__local', local)  
  6.         object.__setattr__(self'__name__', name)  
  7.   
  8.     def _get_current_object(self):  
  9.         #很明显,_lookup_req_object函数没有__release_local__  
  10.         if not hasattr(self.__local, '__release_local__'):  
  11.             return self.__local()  
  12.         try:  
  13.             return getattr(self.__local, self.__name__)  
  14.         except AttributeError:  
  15.             raise RuntimeError('no object bound to %s' % self.__name__)  
  16.       
  17.     def __getattr__(self, name):  
  18.         return getattr(self._get_current_object(), name)  
当我们调用request.method时会调用_lookup_req_object,对request的任何调用都是对_lookup_req_object返回对象的调用。
既然每次request都不同,要么调用top = _request_ctx_stack.top返回的top不同,要么top.request属性不同,在flask中每次返回的top是不一样的,所以request的各个属性都是变化的。
现在需要看看_request_ctx_stack = LocalStack(),LocalStack其实就是简单的模拟了堆栈的基本操作,push,top,pop,内部保存的线程本地变量是在多线程中request不混乱的关键。

[python]  view plain  copy
  1. class Local(object):  
  2.     __slots__ = ('__storage__''__ident_func__')  
  3.   
  4.     def __init__(self):  
  5.         object.__setattr__(self'__storage__', {})  
  6.         object.__setattr__(self'__ident_func__', get_ident)  
  7.     def __getattr__(self, name):  
  8.         return self.__storage__[self.__ident_func__()][name]  
     简单看一下Local的代码,__storage__为内部保存的自己,键就是thread.get_ident,也就是根据线程的标示符返回对应的值。
下面我们来看看整个交互过程,_request_ctx_stack堆栈是在哪里设置push的,push的应该是我们上面说的同时具有request和session属性的对象,那这家伙又到底是什么?

flask从app.run()开始

[python]  view plain  copy
  1. class Flask(_PackageBoundObject):  
  2.     def run(self, host=None, port=None, debug=None, **options):  
  3.         from werkzeug.serving import run_simple  
  4.         run_simple(host, port, self, **options)  
使用的是werkzeug的run_simple,根据wsgi规范,app是一个接口,并接受两个参数,即,application(environ, start_response)
在run_wsgi的run_wsgi我们可以清晰的看到调用过程

[python]  view plain  copy
  1. def run_wsgi(self):  
  2.     environ = self.make_environ()  
  3.   
  4.     def start_response(status, response_headers, exc_info=None):  
  5.         if exc_info:  
  6.             try:  
  7.                 if headers_sent:  
  8.                     reraise(*exc_info)  
  9.             finally:  
  10.                 exc_info = None  
  11.         elif headers_set:  
  12.             raise AssertionError('Headers already set')  
  13.         headers_set[:] = [status, response_headers]  
  14.         return write  
  15.   
  16.     def execute(app):  
  17.         application_iter = app(environ, start_response)  
  18.         #environ是为了给request传递请求的  
  19.         #start_response主要是增加响应头和状态码,最后需要werkzeug发送请求  
  20.         try:  
  21.             for data in application_iter: #根据wsgi规范,app返回的是一个序列  
  22.                 write(data) #发送结果  
  23.             if not headers_sent:  
  24.                 write(b'')  
  25.         finally:  
  26.             if hasattr(application_iter, 'close'):  
  27.                 application_iter.close()  
  28.             application_iter = None  
  29.   
  30.     try:  
  31.         execute(self.server.app)  
  32.     except (socket.error, socket.timeout) as e:  
  33.         pass  
flask中通过定义__call__方法适配wsgi规范
[python]  view plain  copy
  1. class Flask(_PackageBoundObject):  
  2.     def __call__(self, environ, start_response):  
  3.         """Shortcut for :attr:`wsgi_app`."""  
  4.         return self.wsgi_app(environ, start_response)  
  5.   
  6.     def wsgi_app(self, environ, start_response):  
  7.         ctx = self.request_context(environ)  
  8.         #这个ctx就是我们所说的同时有request,session属性的上下文  
  9.         ctx.push()  
  10.         error = None  
  11.         try:  
  12.             try:  
  13.                 response = self.full_dispatch_request()  
  14.             except Exception as e:  
  15.                 error = e  
  16.                 response = self.make_response(self.handle_exception(e))  
  17.             return response(environ, start_response)  
  18.         finally:  
  19.             if self.should_ignore_error(error):  
  20.                 error = None  
  21.             ctx.auto_pop(error)  
  22.   
  23.     def request_context(self, environ):  
  24.         return RequestContext(self, environ)  
哈哈,终于找到神秘人了,RequestContext是保持一个请求的上下文变量,之前我们_request_ctx_stack一直是空的,当一个请求来的时候调用ctx.push()将向_request_ctx_stack中push ctx。
我们看看ctx.push

[python]  view plain  copy
  1. class RequestContext(object):  
  2.     def __init__(self, app, environ, request=None):  
  3.         self.app = app  
  4.         if request is None:  
  5.             request = app.request_class(environ) #根据环境变量创建request  
  6.         self.request = request  
  7.         self.session = None  
  8.   
  9.     def push(self):  
  10.         _request_ctx_stack.push(self#将ctx push进 _request_ctx_stack  
  11.         # Open the session at the moment that the request context is  
  12.         # available. This allows a custom open_session method to use the  
  13.         # request context (e.g. code that access database information  
  14.         # stored on `g` instead of the appcontext).  
  15.         self.session = self.app.open_session(self.request)  
  16.         if self.session is None:  
  17.             self.session = self.app.make_null_session()  
  18.   
  19.     def pop(self, exc=None):  
  20.         rv = _request_ctx_stack.pop()  
  21.        
  22.     def auto_pop(self, exc):  
  23.         if self.request.environ.get('flask._preserve_context'or \  
  24.            (exc is not None and self.app.preserve_context_on_exception):  
  25.             self.preserved = True  
  26.             self._preserved_exc = exc  
  27.         else:  
  28.             self.pop(exc)  

我们看到ctx.push操作将ctx push到_request_ctx_stack,所以当我们调用request.method时将调用_lookup_req_object。 top此时就是ctx上下文对象,而getattr(top, "request")将返回ctx的request,而这个request就是在ctx的__init__中根据环境变量创建的。

哈哈,明白了吧,每一次调用视图函数操作之前,flask会把创建好的ctx放在线程Local中,当使用时根据线程id就可以拿到了。在wsgi_app的finally中会调用ctx.auto_pop(error),会根据情况判断是否清除放_request_ctx_stack中的ctx。

上面是我简化的代码,其实在RequestContext push中_app_ctx_stack = LocalStack()是None,也会把app push进去,对应的
app上下文对象为AppContext。

我们知道flask还有一个神秘的对象g,flask从0.10开始g是和app绑定在一起的(http://flask.pocoo.org/docs/0.10/api/#flask.g),g是AppContext的一个成员变量。虽然说g是和app绑定在一起的,但不同请求的AppContext是不同的,所以g还是不同。也就是说你不能再一个视图中设置g.name,然后再另一个视图中使用g.name,会提示AttributeError。

绑定要app的好处就是可以脱离request使用g,否则就要使用flask._app_ctx_stack.top代替g,

可参考http://dormousehole.readthedocs.org/en/latest/patterns/sqlite3.html


到这里,不知道你对RequestContext还有没有疑惑,这里有一点比较疑惑的是我们当前request只有一个,为什么要request要使用栈,而不是直接保存当前的request实例

其实这主要是为了多app共存考虑的,由于我们一般都只有一个app,所以栈顶存放的肯定是当前request对象,当如果是多个app,那么栈顶存放的是当前活跃的request,也就是说使用栈是为了获取当前的活跃request对象。

下面使用gtwisted模拟多个app,由于gtwisted使用了greenlet,所以多个app.run并不会阻塞,当然你也可以使用线程来模拟。

[python]  view plain  copy
  1. from flask import Flask, url_for  
  2. from gtwisted.core import reactor  
  3. app1 = Flask("app1")  
  4. app2 = Flask("app2")  
  5.  
  6. @app1.route("/index1")  
  7. def index1():  
  8.     return "app1"  
  9.  
  10. @app1.route("/home")  
  11. def home():  
  12.     return "app1home"  
  13.  
  14. @app2.route("/index2")  
  15. def index2():  
  16.     return "app2"  
  17.       
  18. # reactor.listenWSGI(8000, app1)  
  19. # reactor.listenWSGI(8001, app2)  
  20. # reactor.run()  
  21. with app1.test_request_context():  
  22.     print url_for('index1')  
  23.     with app2.test_request_context():  
  24.         print url_for('index2')  
  25.     print url_for('home')  

如果将中间三行去掉,可以通过浏览器127.0.0.1:8000/index1,127.0.0.1:80001/index2访问多个app

输出结果:


当app1.test_request_context()时,会将app1对应的context压栈,当前活跃app context是app1的,所以url_for使用的是app1的context,当with app2.test_request_context()时,会将app2对应的context压栈,当前活跃app context是app2的,当with过了作用域,将弹出app2的context,此时活跃的为app1的,这就是为什么context使用栈而不是实例变量。

到这里各位小伙伴们都明白了吧,flask以优雅的方式给我们提供了很大的便利,自己做了很多的工作,这就是当代活雷锋!!!


版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn

最近一直在研究Flask,由于gfirefly中提供的Http接口使用了Flask,以前都是写一些游戏中简单的操作,最近涉及到Flask的方面比较多,所以就认真研究了下。对Flask的request context和app context略有心得,所以和小伙伴们分享一下Flask的request原理。

在我们视图中要使用request时只需要from flask import request就可以了
很好奇在多线程的环境下,是如何保证request没有混乱的
在flask.globals.py中

[python]  view plain  copy
  1. def _lookup_req_object(name):  
  2.     top = _request_ctx_stack.top  
  3.     if top is None:  
  4.         raise RuntimeError('working outside of request context')  
  5.     return getattr(top, name)  
  6.   
  7. _request_ctx_stack = LocalStack()  
  8. request = LocalProxy(partial(_lookup_req_object, 'request'))  
  9. session = LocalProxy(partial(_lookup_req_object, 'session'))  
其实可以看到不管request还是session最后都是通过getattr(top, name)获取的,也就是说肯定有一个上下文

对象同时保持request和session。我们只要一处导入request,在任何视图函数中都可以使用request,

关键是每次的都是不同的request对象,说明获取request对象肯定是一个动态的操作,不然肯定都是相同的request。这里的魔法就是_lookup_req_object函数和LocalProxy组合完成的。

LocalProxy是werkzeug.local.py中定义的一个代理对象,它的作用就是将所有的请求都发给内部的_local对象

[python]  view plain  copy
  1. class LocalProxy(object):  
  2.     def __init__(self, local, name=None):  
  3.         #LocalProxy的代码被我给简化了,这里的local不一定就是local.py中定义的线程局部对象,也可以是任何可调用对象  
  4.         #在我们的request中传递的就是_lookup_req_object函数  
  5.         object.__setattr__(self'_LocalProxy__local', local)  
  6.         object.__setattr__(self'__name__', name)  
  7.   
  8.     def _get_current_object(self):  
  9.         #很明显,_lookup_req_object函数没有__release_local__  
  10.         if not hasattr(self.__local, '__release_local__'):  
  11.             return self.__local()  
  12.         try:  
  13.             return getattr(self.__local, self.__name__)  
  14.         except AttributeError:  
  15.             raise RuntimeError('no object bound to %s' % self.__name__)  
  16.       
  17.     def __getattr__(self, name):  
  18.         return getattr(self._get_current_object(), name)  
当我们调用request.method时会调用_lookup_req_object,对request的任何调用都是对_lookup_req_object返回对象的调用。
既然每次request都不同,要么调用top = _request_ctx_stack.top返回的top不同,要么top.request属性不同,在flask中每次返回的top是不一样的,所以request的各个属性都是变化的。
现在需要看看_request_ctx_stack = LocalStack(),LocalStack其实就是简单的模拟了堆栈的基本操作,push,top,pop,内部保存的线程本地变量是在多线程中request不混乱的关键。

[python]  view plain  copy
  1. class Local(object):  
  2.     __slots__ = ('__storage__''__ident_func__')  
  3.   
  4.     def __init__(self):  
  5.         object.__setattr__(self'__storage__', {})  
  6.         object.__setattr__(self'__ident_func__', get_ident)  
  7.     def __getattr__(self, name):  
  8.         return self.__storage__[self.__ident_func__()][name]  
     简单看一下Local的代码,__storage__为内部保存的自己,键就是thread.get_ident,也就是根据线程的标示符返回对应的值。
下面我们来看看整个交互过程,_request_ctx_stack堆栈是在哪里设置push的,push的应该是我们上面说的同时具有request和session属性的对象,那这家伙又到底是什么?

flask从app.run()开始

[python]  view plain  copy
  1. class Flask(_PackageBoundObject):  
  2.     def run(self, host=None, port=None, debug=None, **options):  
  3.         from werkzeug.serving import run_simple  
  4.         run_simple(host, port, self, **options)  
使用的是werkzeug的run_simple,根据wsgi规范,app是一个接口,并接受两个参数,即,application(environ, start_response)
在run_wsgi的run_wsgi我们可以清晰的看到调用过程

[python]  view plain  copy
  1. def run_wsgi(self):  
  2.     environ = self.make_environ()  
  3.   
  4.     def start_response(status, response_headers, exc_info=None):  
  5.         if exc_info:  
  6.             try:  
  7.                 if headers_sent:  
  8.                     reraise(*exc_info)  
  9.             finally:  
  10.                 exc_info = None  
  11.         elif headers_set:  
  12.             raise AssertionError('Headers already set')  
  13.         headers_set[:] = [status, response_headers]  
  14.         return write  
  15.   
  16.     def execute(app):  
  17.         application_iter = app(environ, start_response)  
  18.         #environ是为了给request传递请求的  
  19.         #start_response主要是增加响应头和状态码,最后需要werkzeug发送请求  
  20.         try:  
  21.             for data in application_iter: #根据wsgi规范,app返回的是一个序列  
  22.                 write(data) #发送结果  
  23.             if not headers_sent:  
  24.                 write(b'')  
  25.         finally:  
  26.             if hasattr(application_iter, 'close'):  
  27.                 application_iter.close()  
  28.             application_iter = None  
  29.   
  30.     try:  
  31.         execute(self.server.app)  
  32.     except (socket.error, socket.timeout) as e:  
  33.         pass  
flask中通过定义__call__方法适配wsgi规范
[python]  view plain  copy
  1. class Flask(_PackageBoundObject):  
  2.     def __call__(self, environ, start_response):  
  3.         """Shortcut for :attr:`wsgi_app`."""  
  4.         return self.wsgi_app(environ, start_response)  
  5.   
  6.     def wsgi_app(self, environ, start_response):  
  7.         ctx = self.request_context(environ)  
  8.         #这个ctx就是我们所说的同时有request,session属性的上下文  
  9.         ctx.push()  
  10.         error = None  
  11.         try:  
  12.             try:  
  13.                 response = self.full_dispatch_request()  
  14.             except Exception as e:  
  15.                 error = e  
  16.                 response = self.make_response(self.handle_exception(e))  
  17.             return response(environ, start_response)  
  18.         finally:  
  19.             if self.should_ignore_error(error):  
  20.                 error = None  
  21.             ctx.auto_pop(error)  
  22.   
  23.     def request_context(self, environ):  
  24.         return RequestContext(self, environ)  
哈哈,终于找到神秘人了,RequestContext是保持一个请求的上下文变量,之前我们_request_ctx_stack一直是空的,当一个请求来的时候调用ctx.push()将向_request_ctx_stack中push ctx。
我们看看ctx.push

[python]  view plain  copy
  1. class RequestContext(object):  
  2.     def __init__(self, app, environ, request=None):  
  3.         self.app = app  
  4.         if request is None:  
  5.             request = app.request_class(environ) #根据环境变量创建request  
  6.         self.request = request  
  7.         self.session = None  
  8.   
  9.     def push(self):  
  10.         _request_ctx_stack.push(self#将ctx push进 _request_ctx_stack  
  11.         # Open the session at the moment that the request context is  
  12.         # available. This allows a custom open_session method to use the  
  13.         # request context (e.g. code that access database information  
  14.         # stored on `g` instead of the appcontext).  
  15.         self.session = self.app.open_session(self.request)  
  16.         if self.session is None:  
  17.             self.session = self.app.make_null_session()  
  18.   
  19.     def pop(self, exc=None):  
  20.         rv = _request_ctx_stack.pop()  
  21.        
  22.     def auto_pop(self, exc):  
  23.         if self.request.environ.get('flask._preserve_context'or \  
  24.            (exc is not None and self.app.preserve_context_on_exception):  
  25.             self.preserved = True  
  26.             self._preserved_exc = exc  
  27.         else:  
  28.             self.pop(exc)  

我们看到ctx.push操作将ctx push到_request_ctx_stack,所以当我们调用request.method时将调用_lookup_req_object。 top此时就是ctx上下文对象,而getattr(top, "request")将返回ctx的request,而这个request就是在ctx的__init__中根据环境变量创建的。

哈哈,明白了吧,每一次调用视图函数操作之前,flask会把创建好的ctx放在线程Local中,当使用时根据线程id就可以拿到了。在wsgi_app的finally中会调用ctx.auto_pop(error),会根据情况判断是否清除放_request_ctx_stack中的ctx。

上面是我简化的代码,其实在RequestContext push中_app_ctx_stack = LocalStack()是None,也会把app push进去,对应的
app上下文对象为AppContext。

我们知道flask还有一个神秘的对象g,flask从0.10开始g是和app绑定在一起的(http://flask.pocoo.org/docs/0.10/api/#flask.g),g是AppContext的一个成员变量。虽然说g是和app绑定在一起的,但不同请求的AppContext是不同的,所以g还是不同。也就是说你不能再一个视图中设置g.name,然后再另一个视图中使用g.name,会提示AttributeError。

绑定要app的好处就是可以脱离request使用g,否则就要使用flask._app_ctx_stack.top代替g,

可参考http://dormousehole.readthedocs.org/en/latest/patterns/sqlite3.html


到这里,不知道你对RequestContext还有没有疑惑,这里有一点比较疑惑的是我们当前request只有一个,为什么要request要使用栈,而不是直接保存当前的request实例

其实这主要是为了多app共存考虑的,由于我们一般都只有一个app,所以栈顶存放的肯定是当前request对象,当如果是多个app,那么栈顶存放的是当前活跃的request,也就是说使用栈是为了获取当前的活跃request对象。

下面使用gtwisted模拟多个app,由于gtwisted使用了greenlet,所以多个app.run并不会阻塞,当然你也可以使用线程来模拟。

[python]  view plain  copy
  1. from flask import Flask, url_for  
  2. from gtwisted.core import reactor  
  3. app1 = Flask("app1")  
  4. app2 = Flask("app2")  
  5.  
  6. @app1.route("/index1")  
  7. def index1():  
  8.     return "app1"  
  9.  
  10. @app1.route("/home")  
  11. def home():  
  12.     return "app1home"  
  13.  
  14. @app2.route("/index2")  
  15. def index2():  
  16.     return "app2"  
  17.       
  18. # reactor.listenWSGI(8000, app1)  
  19. # reactor.listenWSGI(8001, app2)  
  20. # reactor.run()  
  21. with app1.test_request_context():  
  22.     print url_for('index1')  
  23.     with app2.test_request_context():  
  24.         print url_for('index2')  
  25.     print url_for('home')  

如果将中间三行去掉,可以通过浏览器127.0.0.1:8000/index1,127.0.0.1:80001/index2访问多个app

输出结果:


当app1.test_request_context()时,会将app1对应的context压栈,当前活跃app context是app1的,所以url_for使用的是app1的context,当with app2.test_request_context()时,会将app2对应的context压栈,当前活跃app context是app2的,当with过了作用域,将弹出app2的context,此时活跃的为app1的,这就是为什么context使用栈而不是实例变量。

到这里各位小伙伴们都明白了吧,flask以优雅的方式给我们提供了很大的便利,自己做了很多的工作,这就是当代活雷锋!!!


猜你喜欢

转载自blog.csdn.net/uuihoo/article/details/80196100
今日推荐