zoukankan      html  css  js  c++  java
  • [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中的内容是不一样的。

    ##

  • 相关阅读:
    摄像机镜头详细知识 镜头选型
    镜头Lens Image circle像圈的解释是什么意思
    IC封装的热特性
    接收灵敏度
    步进电机选型
    步进电机步距角、相数、转矩
    锂电池充电的原理
    通过反射了解集合泛型的本质
    用方法对象进行反射
    java反射获取方法名称,参数类型
  • 原文地址:https://www.cnblogs.com/leokale-zz/p/12402284.html
Copyright © 2011-2022 走看看