Web框架的請求上下文
背景
最近在研究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/