web框架的请求上下文

背景

最近在研究web框架时,对”请求上下文”这个基础概念有了更多的了解,因此记录一下,包括以下内容:

  • “请求上下文”是什么?
  • web框架(flask和gin)实现”请求上下文”的区别?
  • “线程私有数据”是什么?

学习过程

  • “请求上下文”是什么?

    根据 Go语言动手写Web框架 - Gee第二天 上下文ContextContext:请求控制器,让每个请求都在掌控之中 两篇文章,可以知道从”框架开发者”的角度看,”请求上下文”包括:

    1
    2
    3
    * 请求对象:包括请求方法、路径、请求头等内容
    * 响应对象:可以用来返回http响应
    * 工具函数:可以用来更方便地操作"请求对象"和"响应对象"

    那么web框架怎么让”框架的使用者”拿到”请求上下文”呢?

  • “框架的使用者怎么”拿到”请求上下文”?

    flask框架中请求上下文是一个全局变量,而gin框架中请求上下文被当作参数传递。

    根据flask文档知道request对象包含有请求信息,可以如下获取

    1
    2
    3
    4
    5
    6
    7
    8
    from flask import request

    @app.route('/login', methods=['POST', 'GET'])
    def login():
    ...
    if request.method == 'POST':
    if valid_login(request.form['username'],
    request.form['password']):

    根据gin文档知道gin.Context实例c中包含有请求信息,可以如下获取

    1
    2
    3
    4
    5
    6
    7
    8
     router := gin.Default()

    router.GET("/welcome", func(c *gin.Context) {
    firstname := c.DefaultQuery("firstname", "Guest")
    lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")

    c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
    })

    从上面的使用方法可以看出来,flask和gin框架实现”请求上下文”有一些区别:

    1
    2
    * gin框架中"框架使用者"需要把"请求上下文"当作参数,显示地传递
    * flask框架中"框架使用者"只需要request这个全局变量,就能获得"请求上下文"

    于是就有两个问题:

    1
    2
    * flask的request是个全局变量,那"基于多线程实现"的服务端同时收到多个请求时,request怎么能代表当前线程处理的请求呢?
    * flask似乎对"框架使用者"来说更方便,毕竟不需要多传递一个参数。那为什么gin框架不也这么设计呢?

    第一个问题其实涉及到”线程私有数据”的概念

线程私有数据

  • 是什么?

    举个例子,下面代码中新线程看不到主线程的mydata变量,因为mydata是”主线程”和”新线程”的私有数据”

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import threading
    from threading import local

    mydata = local()
    mydata.number = 42


    def f():
    if getattr(mydata, "number", None) is not None:
    print(mydata.number) # 这里会打印42吗?


    thread = threading.Thread(target=f)
    thread.start()
    thread.join()
  • threading.local是怎么实现的?

    源码中可以看到localdict是实际存放数据的对象,每个线程对应一个localdict。

    线程在读写”线程私有数据”时,会找到自己的localdict。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class _localimpl:
    ...

    def get_dict(self):
    """Return the dict for the current thread. Raises KeyError if none
    defined."""
    thread = current_thread()
    return self.dicts[id(thread)][1] # id(thread)是当前线程对象内存地址,每个线程应该是唯一的

    def create_dict(self):
    """Create a new dict for the current thread, and return it."""
    localdict = {}
    key = self.key
    thread = current_thread()
    idt = id(thread) # id(thread)是当前线程对象内存地址,每个线程应该是唯一的
    ...
    self.dicts[idt] = wrthread, localdict
    return localdict

    from threading import current_thread, RLock

    那flask框架是用了threading.local吗?

  • flask框架用了threading.local吗?

    先说结论:flask的request对象不是基于”threading.local”,而是”contextvars.ContextVar”,后者可以实现”协程私有数据”

    下面代码运行结果中,task1函数不会打印hello,可以看出来ContextVar是实现”协程私有数据”。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    from greenlet import greenlet
    from contextvars import ContextVar
    from greenlet import getcurrent as get_ident

    var = ContextVar("var")
    var.set("hello")


    def p(s):
    print(s, get_ident())

    try:
    print(var.get())
    except LookupError:
    pass


    def task1():
    p("task1") # 不会打印hello
    # gr2.switch()


    # 测试ContextVar能否支持"协程私有数据"
    p("main")
    gr1 = greenlet(task1)
    gr1.switch()

    # 测试ContextVar能否支持"线程私有数据",结论是支持
    # import threading
    # p("main")
    # thread = threading.Thread(target=task1)
    # thread.start()
    # thread.join()

    flask/globals.py中可以看到request是werkzeug库的Local类型

    1
    2
    3
    _request_ctx_stack = LocalStack()
    ...
    request: "Request" = LocalProxy(partial(_lookup_req_object, "request")) # type: ignore

    而从werkzeug/local.py源码 可以看出来werkzeug库的Local是基于contextvars.ContextVar实现的

    1
    2
    3
    4
    5
    class Local:
    ...

    def __init__(self) -> None:
    object.__setattr__(self, "_storage", ContextVar("local_storage")) # 基于contextvars.ContextVar

    所以,flask并没有用threading.local,而是werkzeug库的Local类型。也因此在”多线程”或者”多协程”环境下,flask的request全局变量能够代表到当前线程或者协程处理的请求。

总结

web框架让”框架使用者”拿到”请求对象”有两种方式,包括”参数传递”、”全局变量”。

实现”全局变量”这种方式时,因为web服务可能是在多线程或者多协程的环境,所以需要每个线程或者协程使用”全局变量”时互不干扰,就涉及到”线程私有数据”的概念。

SpringWeb中在使用”RequestContextHolder.getRequestAttributes()静态方法”获取请求时,也是类似的业务逻辑。

参考

Go语言动手写Web框架 - Gee第二天 上下文Context
flask 源码解析:上下文