[Python自学] Flask框架 (4) (Request&Session上下文管理、redis保存session、App&g上下文管理)
一、上下文管理理论基础
1.线程数据隔离
多线程访问一个数据:当内存中存在一个数据,多个线程都可以对其进行修改,如果要保证数据的一致性,则需要对其进行加锁。
多线程操作自己的数据:当需要每个线程都只能操作自己的数据,而该数据需要放置到一个全局的空间(例如全局变量)。则需要对其进行数据隔离,即线程只能访问自己存储的数据。
在threading模块中,我们可以使用threading.local来实现线程之间的数据隔离:
import threading import time # 定义一个threading.local对象 obj = threading.local def run(index): # 写入xxx=index obj.xxx = index # 10个线程 for i in range(10): t = threading.Thread(target=run, agrs=(i,)) t.start()
虽然每个线程都对obj写入了一个名为"xxx"的变量,值为自己的index。但是threading.local对象为各个线程做了数据隔离。
他的原理是,为每一个线程都开辟一块内存空间,实际上就是利用一个字典,将线程的唯一表示作为键key,线程存入的值放到该键对应的value中。如下所示:
# threading.local使用一个字典来保存各个线程的数据 { 1233: {'xxx': 0}, # 第0个线程的tid为1233,存入的值放在对应的字典中 1234: {'xxx': 1}, 1235: {'xxx': 2}, 1236: {'xxx': 3}, 1237: {'xxx': 4}, 1238: {'xxx': 5}, 1239: {'xxx': 6}, 1240: {'xxx': 7}, 1241: {'xxx': 8}, 1242: {'xxx': 9}, }
2.用字典实现一个threading.local类
import threading class Local(object): DIC = {} # DIC字典用于存放各线程的数据,通过key来隔离 # 从DIC中线程tid对应的字典中获取值 def __getattr__(self, item): tid = threading.get_ident() if tid in self.DIC: return self.DIC[tid].get(item) else: return None # 设置一个值,类似obj.xxx = 1 def __setattr__(self, key, value): # 获取该线程的tid tid = threading.get_ident() # 如果DIC中存在键为tid的数据 if tid in self.DIC: self.DIC[tid][key] = value else: self.DIC[tid] = {key: value} # 创建一个Local对象 obj = Local() def run(index): # 使用Local对象保存各线程的数据 obj.xxx = index # 开启10个线程 for i in range(10): t = threading.Thread(target=run, args=(i,)) t.start() # 打印最后的值 print( obj.DIC) # {2852: {'xxx': 0}, 13520: {'xxx': 1}, 10756: {'xxx': 2}, 7488: {'xxx': 3}, 8484: {'xxx': 4}, 13924: {'xxx': 5}, 10668: {'xxx': 6}, 10252: {'xxx': 7}, 11348: {'xxx': 8}, 10736: {'xxx': 9}}
3.将Local支持协程
我们实现的Local类,达到了和threading.local一样的效果,可以隔离线程的数据。但是我们如果使用的是协程,则需要对其进行扩展。
import threading try: import greenlet # 使用协程时,将协程获取唯一标识的方法赋值给get_ident get_ident = greenlet.getcurrent print("使用协程") except Exception as e: print("使用线程") # 没有使用协程时,将线程获取唯一标识的方法赋值给get_ident get_ident = threading.get_ident class Local(object): DIC = {} # DIC字典用于存放各线程的数据,通过key来隔离 # 从DIC中线程tid对应的字典中获取值 def __getattr__(self, item): tid = get_ident() if tid in self.DIC: return self.DIC[tid].get(item) else: return None # 设置一个值,类似obj.xxx = 1 def __setattr__(self, key, value): # 获取该线程的tid tid = get_ident() # 如果DIC中存在键为tid的数据 if tid in self.DIC: self.DIC[tid][key] = value else: self.DIC[tid] = {key: value} # 创建一个Local对象 obj = Local() def run(index): # 使用Local对象保存各线程的数据 obj.xxx = index # 开启10个线程 for i in range(10): t = threading.Thread(target=run, args=(i,)) t.start() # 打印最后的值 print(obj.DIC)
要扩展支持协程,其实很简单,就是让唯一标识从线程的唯一标识替换为协程的唯一标识。
4.线程、协程数据隔离和Flask的关系
虽然threading.local和Flask没有直接的关系,但是Flask中实现了一套利用此原理的数据隔离机制。类似我们第3.节中实现的支持线程和协程的版本。
在Flask中,请求相关的数据和session等数据都是通过上下文管理的。我们可以将上下文看成一个全局的数据存放点。而我们需要对其数据进行隔离,因为Flask有可能底层会使用多线程、协程等方式来运行。
多个线程或协程会同时接受来自用户的请求,而如果不进行数据隔离,则可能数据会相互覆盖,从而导致数据错误。
二、阅读Flask上下文源码所需的一些小知识点
1.偏函数
偏函数就是利用functools.partial帮我们为某个函数传入一些固定的参数:
import functools # 原本的函数 def add(a, b): return a + b # 使用functools.partial将add函数变为偏函数 new_func = functools.partial(add, 100) # 调用偏函数,只需要传递剩下的参数 res = new_func(1) print(res) # 打印结果为101
2.类的继承关系
类的继承关系可以参考:https://www.cnblogs.com/leokale-zz/p/8472560.html 中的第7节《多继承》和第8节《新式类和经典类的区别》。
我们调用父类方法,通常有两种方式:
class Foo(Base_1, Base_2): def func(self): # 方式一,使用super调用 super(Foo,self).func() # 如果在类内部调用父类方法,可以省略super的参数。即super().func() # 方式二,直接使用父类名调用 Base_1.func(self)
第二种方式很直接,指定父类名调用,如果父类没有对应的方法,则会报错。
而第一种方式super是安装继承顺序来逐级查找被调用的方法的。
例如以下代码:
class Base_1(object): def func(self): print("Base_1.func") class Base_2(object): def func(self): print("Base_2.func") class Foo(Base_1, Base_2): def func(self): # 方式一,使用super调用 super(Foo, self).func() # 打印 Base_1.func # 方式二,直接使用父类名调用 Base_2.func(self) # 打印 Base_2.func if __name__ == '__main__': f = Foo() f.func()
Foo类继承于Base_1和Base_2类,而Base_1和Base_2类继承自object。
我们执行f.func(),可以得到打印结果super执行了Base_1的func方法。
而当我们将Base_1的func方法去掉后(只有Base_2才有func方法):
class Base_1(object): pass class Base_2(object): def func(self): print("Base_2.func") class Foo(Base_1, Base_2): def func(self): # 方式一,使用super调用 super(Foo, self).func() # 打印 Base_2.func # 方式二,直接使用父类名调用 Base_2.func(self) # 打印 Base_2.func if __name__ == '__main__': f = Foo() f.func()
此时,super找到了Base_2的func方法。
所以,super可以执行父类的方法,但不一定会执行父类的方法。当Base_1和Base_2都没有func方法时,super还回去object类中查找func方法,结果是找不到,则报错。
我们总结一下super的查找顺序:
总结:super不是找父类,而是从左往右在多个父类中查找,如果找到了,则直接调用(前提是方法名和参数都一致,如果参数不一致,也要报错),不往后面继续查找,如果直到object都还没找到,则报错。
3.__getattr__、__setattr__以及__delattr__
在python的类中,有两个很重要的特殊方法__getattr__(self,item)和__setattr__(self,key,value):
class Foo(object): def __getattr__(self, item): print(item) def __setattr__(self, key, value): print(key, value)
这两个方法是在使用该类对象进行"."操作的时候会被调用。例如:
if __name__ == '__main__': obj = Foo() obj.name = 'Alex' # __setattr__被调用,打印name Alex obj.age # __getattr__被调用,打印age
注意,我们平时在使用对象的"."操作时,一般不会定义这两个方法。所以默认情况下,都会找到object类的__setattr__和__getattr__来执行,而object类的这两个方法默认的功能就是设置属性和获取属性的值。
如果我们在自己定义的类中重写了这两个方法,那么就可以自己设置"."操作的行为。
考虑以下特殊场景:(构造函数的属性初始化也会触发__setattr__方法)
class Foo(object): def __init__(self): self.storage = {} def __getattr__(self, item): print(item) def __setattr__(self, key, value): print(key, value) if __name__ == '__main__': obj = Foo() obj.name = 'Alex'
在该类的构造方法中,我们初始化了一个对象属性storage。那么按照我们前面所叙述的__setattr__的触发机制。这里应该打印以下信息:
storage {}
name Alex
即,构造函数中的self.storage = {}也会触发__setattr__方法。因为self代表Foo的对象obj(由__new__(Foo)产生),然后使用"."操作设置了storage。
__delattr__:
del obj.name
__delattr__也一样,使用del删除指定的属性,但只能使用"."操作。
注意:__setattr__、__getattr__、__delattr__应该和__setitem__、__getitem__、__delitem__区分。__xxxitem__是使用字典形式操作,例如obj['name'] = Alex,del obj['name']。
具体可以参考:[Python自学] day-7 (静态方法、类方法、属性方法、类的其他、类的来源、反射、异常处理、socket) 中《类的其他内容-7节》
4.基于列表实现栈
python中使用列表实现栈结构,很简单:
class Stack(object): def __init__(self): self._list = [] def push(self, x): self._list.append(x) def pop(self): return self._list.pop() def top(self): if self._list: return self._list[-1] else: return None if __name__ == '__main__': s = Stack() s.push('Alex') # 从栈顶压入"Alex" s.push('Leo') # 从栈顶压入"Leo" print(s.pop()) # 从栈顶弹出"Leo" print(s.pop()) # 从栈顶弹出"Alex" print(s.top()) # s中已经没有数据,打印None
三、源码中的Local类(数据隔离)
在前面的第一章《上下文管理理论基础》中的第3节,我们实现了一个Local类,用于线程、协程的数据隔离。
Flask中其实也是这种实现方式,我们来看看源码是怎么样的:
try: # 使用使用协程,则使用getcurrent作为唯一标识符获取方法 from greenlet import getcurrent as get_ident except ImportError: # 如果使用线程,则使用get_ident作为唯一标识符获取方法 try: from thread import get_ident except ImportError: from _thread import get_ident class Local(object): # 用于限定向外暴露的属性 __slots__ = ("__storage__", "__ident_func__") # 构造函数,由于我们要在这个类中重写object的__setattr__方法,所以构造函数初始化属性直接调用object原始的__setattr__方法 # __storage__私有属性用于存放每个线程或协程的数据 # __ident_func__私有属性用于存放获取唯一标识符的方法(线程为get_ident,协程为getcurrent) def __init__(self): object.__setattr__(self, "__storage__", {}) object.__setattr__(self, "__ident_func__", get_ident) # 返回__storage__的迭代器 def __iter__(self): return iter(self.__storage__.items()) # def __call__(self, proxy): # """Create a proxy for a name.""" # return LocalProxy(self, proxy) # 释放整个线程或协程对应的数据 def __release_local__(self): self.__storage__.pop(self.__ident_func__(), None) # 获取数据(对应各自线程或协程) def __getattr__(self, name): try: return self.__storage__[self.__ident_func__()][name] except KeyError: raise AttributeError(name) # 设置数据(对应各自线程或协程) def __setattr__(self, name, value): ident = self.__ident_func__() storage = self.__storage__ try: storage[ident][name] = value except KeyError: storage[ident] = {name: value} # 删除线程或协程自己的数据 def __delattr__(self, name): try: del self.__storage__[self.__ident_func__()][name] except KeyError: raise AttributeError(name)
注释掉的部分,我们不用关心。特别注意标黄的部分。
我们可以发现,Flask源码中的Local类,和我们自己实现的Local类几乎相同。我们判断字典中是否存在键使用的是if else,而源码中使用的是异常捕获(可以学习借鉴)。
四、源码中的LocalStack类(利用Local实现数据隔离的栈结构)
在Local类中,我们可以使用obj.attr的方式来往字典中存放值(线程或协程隔离的)。那么,我们可以在该字典中维持一个栈:
if __name__ == '__main__': obj = Local() obj.stack = [] obj.stack.append('Alex') obj.stack.append('Leo') print(obj.stack.pop()) print(obj.stack.pop())
虽然这样可以操作字典中的stack,但不是很方便。在Flask源码中实现了一个代理类LocalStack来操作字典中的stack。源码如下:
# 操作stack结构的代理类 class LocalStack(object): # 构造函数,创建一个Local实例(里面有一个字典__storage__) def __init__(self): self._local = Local() # 释放一个线程对应的数据 def __release_local__(self): self._local.__release_local__() # 获取线程或协程唯一标识符的方法 @property def __ident_func__(self): return self._local.__ident_func__ # 设置获取唯一标识符的方法 @__ident_func__.setter def __ident_func__(self, value): object.__setattr__(self._local, "__ident_func__", value) # def __call__(self): # def _lookup(): # rv = self.top # if rv is None: # raise RuntimeError("object unbound") # return rv # # return LocalProxy(_lookup) # 往字典中的stack键对应的栈中放数据(对应调用线程) def push(self, obj): rv = getattr(self._local, "stack", None) # 如果没有stack栈 if rv is None: # 设置一个空栈,rv和self._local.stack都指向该空列表 self._local.stack = rv = [] # 往栈中放数据 rv.append(obj) return rv # 从栈顶弹出数据 def pop(self): stack = getattr(self._local, "stack", None) # 如果栈不存在,返回None if stack is None: return None elif len(stack) == 1: # 如果栈中只有一个数据,则释放该线程或协程对应的栈 release_local(self._local) # 这里等价于self._local.__release_local__() # 将栈的唯一一个数据返回 return stack[-1] else: return stack.pop() # 获取栈顶元素,如果没有则返回None @property def top(self): """The topmost item on the stack. If the stack is empty, `None` is returned. """ try: return self._local.stack[-1] except (AttributeError, IndexError): return None
同样的,不用关心注释掉的部分。
我们可以看到LocalStack类在构造函数中初始化了一个Local对象,在该对象中的字典里,每个线程或协程都对应一个空间。而这个空间里只有一个元素,就是一个栈"stack"。
而LocalStack所提供的操作,例如push、pop、top等都是针对这个栈的。
所以LocalStack提供了一个线程或协程隔离的栈结构存储空间。
五、源码中对request和session的存储
在第4章中,我们了解了Flask源码对LocalStack的实现,知道其利用LocalStack对线程或协程进行数据隔离,并在其中使用栈结构来保存数据。
那么,我们看看LocalStack是如何存储和读取用户的上下文的。
1.模拟RequestContext
参考[Python自学] Flask框架 (3) (路由、CBV、自定义正则动态路由、请求处理流程、蓝图)中的请求处理流程章节,我们知道,当用户请求到达后,Flask将用户的请求信息和session都封装到了一个RequestContext类的对象中(叫做ctx变量)。
假设简单实现一个RequestContext类,模拟源码中的RequestContext类:
# 模拟一个RequestContext类,其中包含用户请求和session class RequestContext(object): def __init__(self): self.request = 'my request' self.session = 'my session'
2.仿照源码实现ctx的存储和读取
# 模拟一个RequestContext类,其中包含用户请求和session class RequestContext(object): def __init__(self): self.request = 'my request' self.session = 'my session' if __name__ == '__main__': # 创建保存上下文实例的栈(支持数据隔离) _request_ctx_stack = LocalStack() # 当用户请求到达时,request和session被封装到RequestContext中 # 将封装好的RequestContext对象保存到栈中 _request_ctx_stack.push(RequestContext()) # 根据参数,取栈中上下文里的request或session def _lookup_req_object(arg): ctx = _request_ctx_stack.top return getattr(ctx, arg) import functools # 通过functools.partial将其封装成两个偏函数,方便使用(源码中的request和session还包了一层LocalProxy类,可以看后面LocalProxy的章节) request = functools.partial(_lookup_req_object, 'request') session = functools.partial(_lookup_req_object, 'session') # 通过request和sesison获取上下文中的数据 print(request()) print(session())
3.源码中ctx存储的过程
1)请求到达时,服务器调用Flask的__call__方法,然后在其中调用wsgi_app方法:
def __call__(self, environ, start_response): return self.wsgi_app(environ, start_response)
2)在wsgi_app方法中实例化ctx,
def wsgi_app(self, environ, start_response): # 实例化RequestContext,在其中封装Request对象,并将session初始化为空 ctx = self.request_context(environ) error = None try: try: # 调用ctx的push方法 ctx.push() ... ...
3)ctx.push()中,将自己压入栈
def push(self): ... ... # 将自己(ctx对象)压入栈_request_ctx_stack _request_ctx_stack.push(self) ... ...
4)_request_ctx_stack是LocalStack的实例,该实例全局定义的
# globals.py # context locals _request_ctx_stack = LocalStack()
4.ctx存储过程图
5.session的存储过程
总体来说,由于session和request都被封装在ctx对象中(RequestContext类的对象)。所以存储的过程是一样的。
但是session和request的不同点在于,数据获取的时机不同。
对于request:我们在wsgi_app()函数中,创建ctx对象的时候,就将environ作为参数传递进去,environ被封装成Request对象,然后放在ctx对象中。
对于session:wsgi_app()函数创建好ctx的时候,session只是被初始化为空,我们看RequestContext的构造函数源码:
class RequestContext(object): def __init__(self, app, environ, request=None, session=None): self.app = app if request is None: request = app.request_class(environ) self.request = request self.url_adapter = None try: self.url_adapter = app.create_url_adapter(self.request) except HTTPException as e: self.request.routing_exception = e self.flashes = None self.session = session ... ...
在ctx.push()中,ctx被压入到栈之后,对session进行了赋值:
def push(self): ... ... _request_ctx_stack.push(self) ... ... if self.session is None: session_interface = self.app.session_interface self.session = session_interface.open_session(self.app, self.request) if self.session is None: self.session = session_interface.make_null_session(self.app) ...
源码中的app.session_interface实际上是 SecureCookieSessionInterface类,session_interface是其一个对象,然后调用其中的open_session来对session进行赋值。
通过open_session源码,可以大致了解session的赋值过程:
def open_session(self, app, request): s = self.get_signing_serializer(app) if s is None: return None # 从cookie中获取名为session的数据(默认名为session) val = request.cookies.get(app.session_cookie_name) # 如果没有session,则session为空的SecureCookieSession对象 if not val: return self.session_class() max_age = total_seconds(app.permanent_session_lifetime) try: # 对拿到的数据做反序列化 data = s.loads(val, max_age=max_age) return self.session_class(data) except BadSignature: return self.session_class()
六、源码中的LocalProxy
1.LocalProxy类工作流程
在第五章的2.仿照源码实现ctx的存储和读取 中可以看到,我们使用functools.partial制造了两个偏函数,用于从ctx中获取request和session。如下代码:
request = functools.partial(_lookup_req_object, 'request') session = functools.partial(_lookup_req_object, 'session') # 通过request和sesison获取上下文中的数据 print(request()) print(session())
这是我们仿造源码实现的功能。真正的源码还对偏函数进行了一层封装,即使用LocalProxy类,
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"))
其中的current_app和g我们将在后面的章节中讨论。
LocalPorxy的源码如下:
class LocalProxy(object): # 构造函数,将request或session偏函数传入,即local参数 def __init__(self, local, name=None): # 将其保存到self.__local中 object.__setattr__(self, "_LocalProxy__local", local) ... def _get_current_object(self): # 返回self.__local(),即返回request或session对象 if not hasattr(self.__local, "__release_local__"): return self.__local() ... # 当我们使用request.xxx的时候就会从request对象中帮我们取值 def __getattr__(self, name): if name == "__members__": return dir(self._get_current_object()) return getattr(self._get_current_object(), name) # 当我们使用request['xxx']取值时,帮我们从request对象中取值 __getitem__ = lambda x, i: x._get_current_object()[i] ... ...
从源码中,我们可以看到,self.__local(self._LocalProxy__local)就是request和session的偏函数,所以self.__local()得到的就是request和session对象。
LocalProxy类中实现了__getattr__、__setattr__、__getitem__、__setitem__等方法,用于帮我们从request和session中取值。
所以,综上所述,LocalProxy就是帮我们从Request和Session对象中取值的中间代理,只是为了让我们取值更加方便。
2.修改后的ctx存储过程图
图中多了LocalProxy部分和偏函数部分。
在偏函数中,通过_lookup_req_object获取request和session对应的对象(ctx=_request_ctx_stack.top())。
在LocalProxy中通过__getattr__等特殊方法来获取request和session对象中的值。
七、flask-session组件
在第五章对session工作流程的解析中,我们知道从加密cookie中获取session,以及将session写到加密cookie中。都是使用的一个叫做SecureCookieSessionInterface的类。
那么,我们如果想将session存储到redis数据库中,则只需要使用其他的类来替换SecureCookieSessionInterface类即可。
1.安装flask-session
pip install flask-session
2.导入flask-session
from flask_session import Session
以前的老版本可能是:
from flask.ext.session import Session
3.使用flask-session
# 在Flask的全局配置中指定使用redis来保存session app.config['SESSION_TYPE'] = 'redis' # 然后将app对象传递给Session,在里面原本的app.session_interface = SecureCookieSessionInterface()会被替换为RedisSessionInterface() Session(app)
我们解析以下Session类的源码:
import os # 导入Session支持的各种Interface,例如redis、memcache、文件系统、MongoDB、SQL from .sessions import NullSessionInterface, RedisSessionInterface, \ MemcachedSessionInterface, FileSystemSessionInterface, \ MongoDBSessionInterface, SqlAlchemySessionInterface class Session(object): # 构造函数,app对象被传入 def __init__(self, app=None): self.app = app # 如果app不为空 if app is not None: self.init_app(app) # 调用init_app(app) def init_app(self, app): # 在这里替换app.session_interface=SecureCookieSessionInterface() app.session_interface = self._get_interface(app) # 在这个方法中,根据我们指定的SESSION_TYPE来返回对应的Interface类 def _get_interface(self, app): # 从全局配置复制一份 config = app.config.copy() # 注意,这里都是使用的setdefault,即配置不存在才设置 config.setdefault('SESSION_TYPE', 'null') config.setdefault('SESSION_PERMANENT', True) config.setdefault('SESSION_USE_SIGNER', False) config.setdefault('SESSION_KEY_PREFIX', 'session:') config.setdefault('SESSION_REDIS', None) config.setdefault('SESSION_MEMCACHED', None) config.setdefault('SESSION_FILE_DIR', os.path.join(os.getcwd(), 'flask_session')) config.setdefault('SESSION_FILE_THRESHOLD', 500) config.setdefault('SESSION_FILE_MODE', 384) config.setdefault('SESSION_MONGODB', None) config.setdefault('SESSION_MONGODB_DB', 'flask_session') config.setdefault('SESSION_MONGODB_COLLECT', 'sessions') config.setdefault('SESSION_SQLALCHEMY', None) config.setdefault('SESSION_SQLALCHEMY_TABLE', 'sessions') # 如果我们在全局配置中设置了SESSION_TYPE为redis,则返回RedisSessionInterface类 if config['SESSION_TYPE'] == 'redis': session_interface = RedisSessionInterface( config['SESSION_REDIS'], config['SESSION_KEY_PREFIX'], config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT']) # 如果指定使用Memcached,则返回MemcachedSessioninterface类 elif config['SESSION_TYPE'] == 'memcached': session_interface = MemcachedSessionInterface( config['SESSION_MEMCACHED'], config['SESSION_KEY_PREFIX'], config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT']) # 如果指定文件系统,则返回FileSystemSessionInterface类 elif config['SESSION_TYPE'] == 'filesystem': session_interface = FileSystemSessionInterface( config['SESSION_FILE_DIR'], config['SESSION_FILE_THRESHOLD'], config['SESSION_FILE_MODE'], config['SESSION_KEY_PREFIX'], config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT']) # Mongodb,返回MongoDBSessionInterface类 elif config['SESSION_TYPE'] == 'mongodb': session_interface = MongoDBSessionInterface( config['SESSION_MONGODB'], config['SESSION_MONGODB_DB'], config['SESSION_MONGODB_COLLECT'], config['SESSION_KEY_PREFIX'], config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT']) # SqlAlchemy,返回SqlAlchemySessionInterface类 elif config['SESSION_TYPE'] == 'sqlalchemy': session_interface = SqlAlchemySessionInterface( app, config['SESSION_SQLALCHEMY'], config['SESSION_SQLALCHEMY_TABLE'], config['SESSION_KEY_PREFIX'], config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT']) else: # 如果都不符合,则返回NullSessionInterface类 session_interface = NullSessionInterface() return session_interface
我们可以看到,最核心的过程就是根据全局配置中指定的SESSION_TYPE类返回对应的Interface类,来代替默认的加密cookie。
4.redis的配置
如果我们在全局配置中指定了使用redis,那么肯定需要给Flask指定redis的IP、端口等信息。
我们首先观察RedisSessionInterface类构造函数的源码:
def __init__(self, redis, key_prefix, use_signer=False, permanent=True): if redis is None: from redis import Redis redis = Redis() self.redis = redis self.key_prefix = key_prefix self.use_signer = use_signer self.permanent = permanent
可以看到,RedisSessionInterface接收4个参数,第一个参数是redis实例,第二个参数是session存到redis中名字的前缀。第三个参数是否使用加密盐(对sessionid进行加密),第四个参数
此时,我们再看Session类源码中,使用RedisSessionInterface的时候传入的四个参数对应的全局配置项:
if config['SESSION_TYPE'] == 'redis': session_interface = RedisSessionInterface( config['SESSION_REDIS'], config['SESSION_KEY_PREFIX'], config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT'])
从上述代码可以看出全局配置项SESSION_REDIS应该配置一个redis实例,SESSION_KEY_PREFIX应该配置一个前缀字符串。
所以,我们应该这样配置使用redis:
from flask import Flask from flask_session import Session import redis
app = Flask(__name__) app.config['SESSION_TYPE'] = 'redis' app.config['SESSION_REDIS'] = redis.Redis(host='111.111.111.111',port=6379,password='123456') Session(app) if __name__ == '__main__': app.run()
5.如何从redis中读取session
我们查看RedisSessionInterface类中的open_session方法源码:
def open_session(self, app, request): # 先从cookie中去拿session的id sid = request.cookies.get(app.session_cookie_name) # 如果sid为空(例如第一次请求) if not sid: # 生成一个sid,格式是uuid4 sid = self._generate_sid() # 返回一个空的session return self.session_class(sid=sid, permanent=self.permanent) # 是否使用加密盐 if self.use_signer: # 获取app中设置的加密盐字符串 signer = self._get_signer(app) if signer is None: return None try: # 解密 sid_as_bytes = signer.unsign(sid) sid = sid_as_bytes.decode() except BadSignature: sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) if not PY2 and not isinstance(sid, text_type): sid = sid.decode('utf-8', 'strict') # 根据获取到的sid加上前缀,从redis中获取session的值 val = self.redis.get(self.key_prefix + sid) # 如果值不为空,则发反序列化,然后返回RedisSession对象(其中包含session数据和sid) if val is not None: try: data = self.serializer.loads(val) return self.session_class(data, sid=sid) except: return self.session_class(sid=sid, permanent=self.permanent) return self.session_class(sid=sid, permanent=self.permanent)
如果是第一次请求,则cookie没有带sessionid,所以会新建一个随机字符串(uuid4)作为sessionid,并且创建一个空的session对象。
如果是第二次请求,则cookie中带着sessionid,则从cookie中获取该sessionid。如果使用了加密盐,则使用盐解密。然后得到解密后的sid,加上我们指定的前缀字符串作为key,从redis中获取对应的session数据。如果获取到数据,则反序列化,并将其封装成session对象返回。
6.如何将session保存到redis
当用户请求处理过程中,对session进行了修改(例如保存了一个值在session中)。请求处理完毕后,在返回响应之前,会在RedisSessionInterface类中的save_session方法中将修改后的session保存到redis中,并且将sessionid设置到cookie中。
我们看一下save_session方法的源码:
def save_session(self, app, session, response): domain = self.get_cookie_domain(app) path = self.get_cookie_path(app) # 删除session if not session: if session.modified: self.redis.delete(self.key_prefix + session.sid) response.delete_cookie(app.session_cookie_name, domain=domain, path=path) return httponly = self.get_cookie_httponly(app) secure = self.get_cookie_secure(app) expires = self.get_expiration_time(app, session) val = self.serializer.dumps(dict(session)) # 给存放在redis中的session加上默认超时时间(31天) self.redis.setex(name=self.key_prefix + session.sid, value=val, time=total_seconds(app.permanent_session_lifetime)) # 如果使用加密盐 if self.use_signer: # 加密 session_id = self._get_signer(app).sign(want_bytes(session.sid)) else: # 否则不加密 session_id = session.sid # 将session的id写到响应的cookie中 response.set_cookie(app.session_cookie_name, session_id, expires=expires, httponly=httponly, domain=domain, path=path, secure=secure)
当参数中session为空时,执行删除session操作,从redis中删除对应session数据,主要用于当用户退出登录时。
当session不为空时,首先序列化session字典(对象.__dict__转换为字典)。然后将其存入redis,并且设置超时时间为默认的31天(可在全局配置中修改)。
如果使用了加密盐,则对sid进行加密,然后设置到response的cookie中,返回给用户。
八、app和g上下文
在前面的章节,我们了解了request+session形成的上下文管理流程。流程中使用了一个Local、一个LocalStack、两个LocalProxy,其中Local用于维护一个线程或协程隔离的字典,用于存放按线程或协程唯一标识符作为键的数据。
然后,LocalStack用于在Local的字典中维护一个栈结构,每个线程或协程对应一个栈,而用户的请求和session组成的ctx(上下文实例)就存放在这个栈中。我们通过LocalStack将请求对应的ctx压入和弹出,并且通过LocalProxy来方便的获取ctx中包含的Request和Session对象中的值。
LocalProxy为Flask用户提供的方便的request和session操作接口。
1.app和g的上下文
除了我们已经了解的request+session上下文,Flask中还有一组上下文管理流程,他们也使用一个Local、一个LocalStack以及两个LocalProxy。该上下文用于管理Flask的对象app以及g。
我们首先看以下全局对象的源码(globals.py):
# context locals # request+session的LocalStack,其中维护了一个Local对象 _request_ctx_stack = LocalStack() # app和g的LocalStack,其中也维护了一个Local对象 _app_ctx_stack = LocalStack() # app使用的LocalProxy current_app = LocalProxy(_find_app) request = LocalProxy(partial(_lookup_req_object, "request")) session = LocalProxy(partial(_lookup_req_object, "session")) # g使用的LocalProxy g = LocalProxy(partial(_lookup_app_object, "g"))
2.app和g上下文流程
app和g的上下文存储流程和request、session的存储流程相似。
request和session的上下文是在app.wsgi_app方法中创建的,然后调用了ctx.push将其压入对应的LocalStack。
但是app和g的上下文是在ctx.push方法中创建的,而且先于ctx压入LocalStack之前被压入自己对应的LocalStack。
源码:
def push(self): ... ... # 这里先从app上下文管理的LocalStack中去获取app_ctx app_ctx = _app_ctx_stack.top # 如果获取的app_ctx为空或者其中的app不是当前app if app_ctx is None or app_ctx.app != self.app: # 则新创建一个AppContext对象。app_ctx = AppContext()对象 app_ctx = self.app.app_context() # _app_ctx_stack.push(self) 将self,即app_ctx放入_app_ctx_stack(LocalStack) app_ctx.push() self._implicit_app_ctx_stack.append(app_ctx) else: self._implicit_app_ctx_stack.append(None) if hasattr(sys, "exc_clear"): sys.exc_clear() # 然后将ctx放入_request_ctx_stack(另一个LocalStack) _request_ctx_stack.push(self) ... ...
源码中,蓝色部分是ctx压入request、session对应的LocalStack。
黄色部分是app_ctx压入app、g对应的LocalStack。
3.使用app和g
如何使用保存在LocalStack中的app和g,这和使用request以及session是一样的,直接导入使用即可(他们是在globals.py中定义的全局变量)。
# 直接导入并使用 from flask import Flask,current_app,g
其中current_app就是app,current_app和g都对应一个LocalProxy对象。但是使用的话,像操作app本身一样操作即可。
4.g是什么
由于g和session的处理流程很相似,我们可以对他们进行对比:
1)session中的值是每次请求到才从加密cookie或redis中获取的。而g并不获取值。
2)响应返回的时候,session中的值被重新写入cookie或redis,然后session被销毁。当然g也会被销毁。
3)session是线程隔离的。g也是线程隔离的。
我们可以得出结论。g和session实际上的生命周期是一样的,都是一个请求的生命周期。
g和全局变量的对比:
1)普通的全局变量时在程序启动时就定义的,可以在任何时候任何地方直接使用。
2)g保存在全局变量Local对象中,但是其被线程唯一标识符所隔离,并且根据请求--->响应的周期进行创建和销毁。
所以得出结论,g只是针对某个请求的生命周期中的全局变量。在这个请求的生命周期内,可以在不同的地方存入和取出值。
5.g有什么用
当在一个请求的生命周期中,我们可以用g作为存放公共变量的地方。
例如使用g可以仿造出一个session:
@lg.before_request def before(): print('before') g.session = {} g.session['name'] = 'Leo' # 使用蓝图来调用装饰器(而不是使用app) @lg.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'GET': print(g.session.get('name')) return "login"
因为before函数和login函数都在一个请求生命周期。但是对于每个请求,g中的内容是不一样的。
##