flask源码剖析--请求流程

  想了解这篇里面的内容,请先去了解我另外一篇博客Flask上下文  

 

  在了解flask之前,我们需要了解两个小知识点

  • 偏函数
import functools

def func(a1,a2):
    print(a1)
    print(a2)

#重新封装成一个 给前面参数加默认值 的函数
new_func = functools.partial(func, 666)
new_func(777)
  •  面向对象   对象 + 会执行__add__方法
class Foo(object):

    def __init__(self, num):
        self.num = num

    def __add__(self, other):
        data = self.num + other.num
        return Foo(data)

obj1 = Foo(11)
obj2 = Foo(22)
v = obj1 + obj2
print(v.num)
  •  拼接列表中的值
from itertools import chain

v1 = [11,22,33]
v2 = [44,55,66]

new = chain(v1, v2)
for item in new:
    print(item)


def f1(x):
    return x + 1

func1_list = [f1, lambda x:x-1]

def f2(x):
    return x + 10

new_func_list = chain([f2], func1_list)
for func in new_func_list:
    print(func)
  • 强制调用私有变量 _类名__私有变量名
class Foo(object):

    def __init__(self):
        self.name = 'alex'
        self.__age = 18

    def get_age(self):
        return self.__age

obj = Foo()
# print(obj.name)
# print(obj.get_age())
# 强制获取私有字段
print(obj._Foo__age)

 

  为什么要了解这个几个点呢?这边先按下不说,分析过程中,自然就明白了

 

  从哪里开始,那就要留心我们平时写代码了,flask程序运行起来主要app.run实现的

  进入run函数中,代码最终执行了run_simple(host, port, self, **options),而run_simple执行时,第三参数加括号执行,也就是会执行self(flask对象)__calll__方法

  __call__方法里,执行了self.wsgi_app(environ, start_response),而这里面的代码可以说是flask处理请求的核心代码

        ctx = self.request_context(environ)
        ctx.push()
        error = None
        try:
            try:
                response = self.full_dispatch_request()
            except Exception as e:
                error = e
                response = self.handle_exception(e)
            except:
                error = sys.exc_info()[1]
                raise
            return response(environ, start_response)
        finally:
            if self.should_ignore_error(error):
                error = None
            ctx.auto_pop(error)

 

 

请求到来时

  第一步

  ctx = self.request_context(environ) 函数里执行了RequestContext(self, environ)

  • 执行RequestContext类里的__init__方法
    def __init__(self, app, environ, request=None):
        self.app = app  #app为flask对象
        if request is None:
            request = app.request_class(environ)
        self.request = request  #请求相关
        self.url_adapter = app.create_url_adapter(self.request)  #url映射
        self.flashes = None  #闪现相关
        self.session = None  #session相关
  1. 注意此时self为RequestContext对象,也就是赋值给ctx变量的对象
  2. __init__方法主要把flask对象,生成请求对象并把请求和闪现,session...相关的东西封装在ctx里

 

  第二步

  • ctx.push() 执行RequestContext里的push方法,函数里最终执行了_request_ctx_stack.push(self)
  • 执行的是LocalStack的push方法,并把RequestContext(ctx)对象为参数传了进来
        rv = getattr(self._local, 'stack', None)
        if rv is None:
            self._local.stack = rv = []
        rv.append(obj)
        return r
  1.  刚开始进入stack是没有值的,所以会执行self._local.stack = rv = [],等同把空列表同时赋值给stack和rv
  2. self._local.stack = [],本质上会执行self._local对象里的__setattr__方法,也就是Local类的
   #其中name为stack,value为[]
    def __setattr__(self, name, value):
        ident = self.__ident_func__()  #线程或协程唯一标识
        storage = self.__storage__  #__init__方法赋值为{}
        try:
            storage[ident][name] = value
        except KeyError:
            storage[ident] = {name: value}

   3. 等同把RequestContext对象放到Local的这么一个字典中

storage = {
  唯一ID:{
       stack:[RequestContext对象,] 
    },
  唯一ID:{
       stack:[RequestContext对象,] 
    }    
}

 

请求结束时

  • 在wsgi_app函数中,在try代码中无非就是url映射,找到视图函数并执行,执行完后就需要到存储字典里把相关request数据移除掉,而在finally最终会执行这么一句代码ctx.auto_pop(error)--error有错误时就是错误信息没有就是None,也就是执行RequestContext类里的auto_pop方法
  • else中执行了self.pop(exc)  self为RequestContext对象,pop里最终执行_request_ctx_stack.pop(),而_request_ctx_stack则是LocalStack对象,也就是执行了LocalStack类的pop方法
        stack = getattr(self._local, 'stack', None)  #stack 类似这么个列表[RequestContext对象,]
        if stack is None:
            return None
        elif len(stack) == 1:
            release_local(self._local)
            return stack[-1]
        else:
            return stack.pop()  #列表pop进行删除

 

请求处理中

  • 找到视图函数并执行请求
  1. print(request),打印对象,会执行类里的__str__方法,那这个request是哪个类的呢,要知道这个,就需要知道在导入时,是从哪导的
from flask import Flask,request
  •  也就是执行了下面这句代码,也许你会觉得这个partial怎么似曾相识啊,这个就是偏函数,把request字符串当默认值传入,并且还是返回函数,所以这句代码就是实例化LocalProxy对象,并把一个偏函数传了进去,此时会去执行LocalProxy的__init__,所以说在导入的request对象,本质是LocalProxy对象
request = LocalProxy(partial(_lookup_req_object, 'request'))
  • 执行LocalProxy的__init__方法时,有这么一句代码,其中_LocalProxy__local是不是感觉有在哪见过,对没错,这就是强制调用私有变量,本质上就是做了__local = 偏函数,local是传进来的偏函数
object.__setattr__(self, '_LocalProxy__local', local)
  •  回到视图函数进行打印request则会执行LocalProxy里的__str__方法,也就是下面这句
    __str__ = lambda x: str(x._get_current_object())
  • _get_current_object函数里,进入if中执行__local加括号,也就是执行偏函数
        if not hasattr(self.__local, '__release_local__'):  #刚开始时 偏函数中肯定没辙玩意
            return self.__local()
        try:
            return getattr(self.__local, self.__name__)
        except AttributeError:
            raise RuntimeError('no object bound to %s' % self.__name__)
  •  而在_get_current_object主要做了这么一件事,提取偏函数并执行,也就是执行下面这个函数
def _lookup_req_object(name):  #此时的name为request字符串
    top = _request_ctx_stack.top   
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)
  • _request_ctx_stack.top,执行LocalStack的top,最终返回就是字典存储的RequestContext对象
        try:
            return self._local.stack[-1]
        except (AttributeError, IndexError):
            return None
  •  所以最终getattr(top, name),是去RequestContext中获取request数据的
  • request.method,执行LocalProxy的__getattr__方法
  • 还是执行_get_current_object方法,获取偏函数_lookup_req_object执行,提取request数据

 

上下文总结:

  threading.Local和Flask自定义Local对象

  --请求到来

    - ctx = 封装RequestContext(request, session)

    - ctx放到Local中

  --执行视图时

    - 导入request

    - print(request)      -->   LocalProxy对象的__str__

    - request.method   -->   LocalProxy对象的__getattr__
               - request + 1          -->  LocalProxy对象的__add__

      - 调用 _lookup_req_object函数:去local中将requestContext想获取到,再去requestContext中获取request或session

  -- 请求结束

    - ctx.auto_pop()

    - ctx从local中移除

 

  了解整个请求流程源码后,其实你也可以这么做

from flask.globals import _request_ctx_stack
from functools import partial

def _lookup_req_object(name):
    # name = request
    # top= ctx
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError('不存在')
    # return ctx.request
    return getattr(top, name)

class Foo(object):
    def __init__(self):
        self.xxx = 123
        self.ooo = 888

req = partial(_lookup_req_object,'xxx')
xxx = partial(_lookup_req_object,'ooo')

# 当前求刚进来时
_request_ctx_stack.push(Foo())

# 使用
# obj = _request_ctx_stack.top
# obj.xxx
v1 = req()
print(v1)
v2 = xxx()
print(v2)


# 请求终止,将local中的值移除
_request_ctx_stack.pop()

 

 

APP上下文

  由于flask版本的区别,有一部分数据被分离到APP上下文中,而其原理和请求上下文是一样的

  • 我们再次看到app.wsgi_app源码中这句ctx.push(),函数里会有这么几句
        app_ctx = _app_ctx_stack.top
        if app_ctx is None or app_ctx.app != self.app:
            app_ctx = self.app.app_context()
            app_ctx.push()
  •  这里self是RequestContext对象,即ctx,self.app就是封装在里面的flask对象,再看下app_context方法中干了啥
  • 返回的是一个AppContext,并把flask对象传入了进去,并执行AppContext的__init__方法,封装了flask对象给app,还封装了一个g对象,对于这个对象,你就可以理解为一个存储数据的字典
    def __init__(self, app):
        self.app = app
        self.url_adapter = app.create_url_adapter(None)
        self.g = app.app_ctx_globals_class()

   为了方便你理解这个g中主要存储什么数据,这里不妨举个例子:比如你在请求前有个数据要传递给视图函数使用,你会怎么搞了,你可能会想,直接通过request.xxx=yyy赋值就可以了,这样做是可以的,但是为了更好避免名字重了,你用这个g来进行存储,这里需要注意的是:它主要保存的是一个请求周期的值

from flask import Flask,request,g

app = Flask(__name__)

@app.before_request
def before():
    g.permission_code_list = ['list','add']


@app.route('/',methods=['GET',"POST"])
def index():
    print(g.permission_code_list)
    return "index"


if __name__ == '__main__':
    app.run()
  •  app_ctx.push()-->_app_ctx_stack.push(self),其中_app_ctx_stack又是一个LocalStack对象,执行它里面的push,会把app_ctx(AppContext对象)放到_app_ctx_stack下的local,这里会注意到:无论多少个线程,会创建请求上下文和APP上下文两个local对象进行存储数据
  • 上面是请求到来时,请求结束时,也会进行删除,和请求上下文是一样,可以通过ctx.auto_pop(error)一步一步看下去,最终会执行_app_ctx_stack.pop(),也就是LocalStack的pop
  • 请求过程中,比如打印g对象,还是执行LocalProxy的__str__方法,最终还是执行偏函数,不过这里偏函数换成了_lookup_app_object,也是去local中获取到AppContext对象,并获取到g对象

 

多APP应用

  我们已经学过了蓝图,url过来,经过app分发给蓝图进行处理

  接下来的内容,则可以通过不同的APP来处理不同的url

from werkzeug.wsgi import DispatcherMiddleware
from werkzeug.serving import run_simple
from flask import Flask, current_app

app1 = Flask('app01')

app2 = Flask('app02')



@app1.route('/index')
def index():
    return "app01"


@app2.route('/index2')
def index2():
    return "app2"

# http://www.oldboyedu.com/index
# http://www.oldboyedu.com/sec/index2
dm = DispatcherMiddleware(app1, {
    '/sec': app2,
})

if __name__ == "__main__":
    app2.__call__
    run_simple('localhost', 5000, dm)

   对上面这段代码的实现原理,看了源码后,你才会发现,原来如此简单

  • 先看到run_simple,最终会执行第三参数+(),也就是dm()
  • dm是一个DispatcherMiddleware对象,里面封装了起始app以及其他app和url的映射关系
    def __init__(self, app, mounts=None):
        self.app = app
        self.mounts = mounts or {}
  •  dm()会执行DispatcherMiddleware的__call__方法
    def __call__(self, environ, start_response):
        script = environ.get('PATH_INFO', '')  #获取url信息,比如/sec/index2
        path_info = ''
        while '/' in script:
            if script in self.mounts:  #判断当前url在不在映射关系里
                app = self.mounts[script]  #在就获取url对应的app对象,退出循环
                break
        #分割后 script='/sec' last_itme='index2' script, last_item = script.rsplit('/', 1) #如果不在映射关系里,从右进行分割一次 path_info = '/%s%s' % (last_item, path_info) #path_info="/index2" 用于去APP下面找视图函数 else: app = self.mounts.get(script, self.app) #如果都没有匹配到,那就获取 起始app #最后把处理好的路径信息重新封装到environ中
     original_script_name = environ.get('SCRIPT_NAME', '') environ['SCRIPT_NAME'] = original_script_name + script environ['PATH_INFO'] = path_info return app(environ, start_response)
  •  源码最后执行了app(environ, start_response),如果是/sec/index2,那么此时的app就是app2,是一个flask对象,加括号,会执行里面的__call__,从这里开始又和单app情况下执行流程是一样的

 

  最后,多app情况下,local下存储结构是咋样的呢?

  • 和单app的场景是一样的,同时通过线程和协程唯一标识进行存储的
  1. 在local下获取值和设置值的,不存在通过类似app唯一标识进行操作,只通过线程和协程唯一标识
  2. 在上述代码中,如果导入request,两个app是暴露在同一全局变量下的,request不对app进行区分
    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  #name  -->'stack'
        except KeyError:
            storage[ident] = {name: value}

 

  可能大家一直有个疑问,那就是local存储中存储APPcontent对象为什么要用栈呢?

    def push(self, obj):
        """Pushes a new item to the stack"""
        rv = getattr(self._local, 'stack', None)
        if rv is None:
            self._local.stack = rv = []
        rv.append(obj)
        return rv

   正常请求流程中,是不会出现栈里有两个对象的,但是测试在进行离线脚本测试时,就有可能有两个呢?

  想了解清楚,这里还是了解一下面向对象的一个知识点

class SQLHelper(object):

    def open(self):
        pass

    def fetch(self,sql):
        pass

    def close(self):
        pass

    def __enter__(self):
        self.open()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()


# obj = SQLHelper()
# obj.open()
# obj.fetch('select ....')
# obj.close()


with SQLHelper() as obj: # 自动调用类中的__enter__方法, obj就是__enter__返回值
    obj.fetch('xxxx')
    # 当执行完毕后,自动调用类 __exit__ 方法

   当测试脚本中,存在with嵌套时,栈中就可能有多个对象

from flask import Flask,current_app,globals,_app_ctx_stack

app1 = Flask('app01')
app1.debug = False # 用户/密码/邮箱
# app_ctx = AppContext(self):
# app_ctx.app
# app_ctx.g

app2 = Flask('app02')
app2.debug = True # 用户/密码/邮箱
# app_ctx = AppContext(self):
# app_ctx.app
# app_ctx.g



with app1.app_context():# __enter__方法 -> push -> app_ctx添加到_app_ctx_stack.local
    # {<greenlet.greenlet object at 0x00000000036E2340>: {'stack': [<flask.ctx.AppContext object at 0x00000000037CA438>]}}
    print(_app_ctx_stack._local.__storage__)
    print(current_app.config['DEBUG'])

    with app2.app_context():
        # {<greenlet.greenlet object at 0x00000000036E2340>: {'stack': [<flask.ctx.AppContext object at 0x00000000037CA438> ]}}
        print(_app_ctx_stack._local.__storage__)
        print(current_app.config['DEBUG'])

	#退出时,执行__exit__方法,执行pop,移除栈里最后的一个对象
    print(current_app.config['DEBUG'])

 

posted @ 2018-08-30 09:56  财经知识狂魔  阅读(292)  评论(0编辑  收藏  举报