文章首发于:
火线Zone社区(https://zone.huoxian.cn/)
最近在研究web框架时,对"请求上下文"这个基础概念有了更多的了解,因此记录一下,包括以下内容:
-
"请求上下文"是什么?
-
web框架(flask和gin)实现"请求上下文"的区别?
-
"线程私有数据"是什么?
"请求上下文"是什么?
根据 Go语言动手写Web框架 - Gee第二天 上下文Context[1] 和 Context:请求控制器,让每个请求都在掌控之中[2] 两篇文章,可以知道从"框架开发者"的角度看,"请求上下文"包括:
* 请求对象:包括请求方法、路径、请求头等内容
* 响应对象:可以用来返回http响应
* 工具函数:可以用来更方便地操作"请求对象"和"响应对象"
那么web框架怎么让"框架的使用者"拿到"请求上下文"呢?
"框架的使用者怎么"拿到"请求上下文"?
flask框架中请求上下文是一个全局变量,而gin框架中请求上下文被当作参数传递。
根据flask文档[3]知道request对象包含有请求信息,可以如下获取
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文档[4]知道gin.Context实例c中包含有请求信息,可以如下获取
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框架实现"请求上下文"有一些区别:
* gin框架中"框架使用者"需要把"请求上下文"当作参数,显示地传递
* flask框架中"框架使用者"只需要request这个全局变量,就能获得"请求上下文"
于是就有两个问题:
* flask的request是个全局变量,那"基于多线程实现"的服务端同时收到多个请求时,request怎么能代表当前线程处理的请求呢?
* flask似乎对"框架使用者"来说更方便,毕竟不需要多传递一个参数。那为什么gin框架不也这么设计呢?
第一个问题其实涉及到"线程私有数据"的概念
是什么?
举个例子,下面代码中新线程看不到主线程的mydata变量,因为mydata是"主线程"和"新线程"的私有数据"
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是怎么实现的?
从源码[5]中可以看到localdict是实际存放数据的对象,每个线程对应一个localdict。
线程在读写"线程私有数据"时,会找到自己的localdict。
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是实现"协程私有数据"。
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[6]中可以看到request是werkzeug库的Local类型。
_request_ctx_stack = LocalStack()
...
request: "Request" = LocalProxy(partial(_lookup_req_object, "request")) # type: ignore
而从werkzeug/local.py源码[7]可以看出来werkzeug库的Local是基于contextvars.ContextVar实现的
class Local:
...
def __init__(self) -> None:
object.__setattr__(self, "_storage", ContextVar("local_storage"))
所以,flask并没有用threading.local,而是werkzeug库的Local类型。也因此在"多线程"或者"多协程"环境下,flask的request全局变量能够代表到当前线程或者协程处理的请求。
web框架让"框架使用者"拿到"请求对象"有两种方式,包括"参数传递"、"全局变量"。
实现"全局变量"这种方式时,因为web服务可能是在多线程或者多协程的环境,所以需要每个线程或者协程使用"全局变量"时互不干扰,就涉及到"线程私有数据"的概念。
SpringWeb中在使用"RequestContextHolder.getRequestAttributes()静态方法"获取请求时,也是类似的业务逻辑。
[1]Go语言动手写Web框架 - Gee第二天 上下Context:
https://geektutu.com/post/gee-day2.html
[2]Context:请求控制器,让每个请求都在掌控之中:
https://time.geekbang.org/column/article/418283
[3]flask文档:
https://flask.palletsprojects.com/en/2.1.x/quickstart/#accessing-request-data
[4]gin文档:
https://pkg.go.dev/github.com/gin-gonic/gin#section-readme
[5]源码:
https://github.com/python/cpython/blob/main/Lib/_threading_local.py
[6]flask/globals.py:
https://github.com/pallets/flask/blob/main/src/flask/globals.py
[7]werkzeug/local.py源码:
https://github.com/pallets/werkzeug/blob/main/src/werkzeug/local.py
[8]flask 源码解析:上下文:
https://cizixs.com/2017/01/13/flask-insight-context/
【火线Zone云安全社区群】
进群可以与技术大佬互相交流
进群有机会免费领取节假日礼品
进群可以免费观看技术分享直播
识别二维码回复【社区群】进群
【火线zone社区周激励】
2022.4.11~ 2022.4.17公告
【相关精选文章】
火线Zone是[火线安全平台]运营的云安全社区,内容涵盖云计算、云安全、漏洞分析、攻防等热门主题,研究讨论云安全相关技术,助力所有云上用户实现全面的安全防护。欢迎具备分享和探索精神的云上用户加入火线Zone社区,共建一个云安全优质社区!
如需转载火线Zone公众号内的文章请联系火线小助手:hxanquan(微信)
// 火线Zone //
微信号 : huoxian_zone
点击阅读原文,加入社区,共建一个有技术氛围的优质社区!
原文始发于微信公众号(火线Zone):Web框架的请求上下文
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论