〇、前言
往期解讀
本期導(dǎo)讀
Flask 0.2 提供了快捷生成 JSON 響應(yīng)的函數(shù):jsonify,如何實(shí)現(xiàn)的呢?網(wǎng)絡(luò)中的字節(jié)流數(shù)據(jù)如何傳遞到 Flask,F(xiàn)lask 又是如何生成字節(jié)流數(shù)據(jù)返回給客戶端的?我們從服務(wù)器接收到 HTTP 消息說(shuō)起。
一、服務(wù)器接收到 HTTP 消息
HTTP 消息是“一問(wèn)一答”的形式,客戶端發(fā)問(wèn)(請(qǐng)求),服務(wù)端回答(響應(yīng))。先有客戶端還是先有服務(wù)端?
去小賣鋪買冰淇淋,如果老板不在店里,在店里喊:“老板,來(lái)個(gè)冰淇淋”,老板能有回復(fù)嗎?不能,因?yàn)槔习鍥](méi)有在接收消息,必須要老板在線,處于接收消息的狀態(tài),我們發(fā)出的“請(qǐng)求”,才能得到“響應(yīng)”??蛻舳伺c服務(wù)端也是同樣的道理,必須先在服務(wù)端監(jiān)聽(tīng)請(qǐng)求,然后才能收到客戶端發(fā)送的請(qǐng)求。
客戶端發(fā)送的 HTTP 消息有目標(biāo)地址:主機(jī):端口,是基于 TCP/IP 協(xié)議傳輸?shù)?,socket 把 TCP/IP 層復(fù)雜的操作抽象封裝為了幾個(gè)簡(jiǎn)單的接口,供應(yīng)用層調(diào)用實(shí)現(xiàn)程序在網(wǎng)絡(luò)中的通信。
在 UNIX 操作系統(tǒng)中,socket 就是一個(gè)文件,在服務(wù)端調(diào)用 socket 接口監(jiān)聽(tīng) TCP 請(qǐng)求,實(shí)際上是創(chuàng)建了一個(gè)可讀的文件,當(dāng)文件中有數(shù)據(jù)被寫入時(shí),就收到了客戶端發(fā)來(lái)的請(qǐng)求。
《Flask 漸進(jìn)式源碼解讀: 0.1》中說(shuō)到,執(zhí)行 serve_forever 即可啟動(dòng) Flask 服務(wù),來(lái)詳細(xì)看看:
def _eintr_retry(func, *args):
while True:
try:
return func(*args)
except (OSError, select.error) as e:
if e.args[0] != errno.EINTR:
raise
class BaseServer:
def serve_forever(self, poll_interval=0.5):
self.__is_shut_down.clear()
try:
while not self.__shutdown_request:
r, w, e = _eintr_retry(select.select, [self], [], [],
poll_interval)
if self.__shutdown_request:
break
if self in r:
self._handle_request_noblock()
finally:
self.__shutdown_request = False
self.__is_shut_down.set()
r, w, e = _eintr_retry(select.select, [self], [], [], poll_interval) 相當(dāng)于執(zhí)行 select.select([self], [], [], poll_),這是一個(gè)系統(tǒng)調(diào)用,返回值是三個(gè)列表,包含已就緒對(duì)象,若返回值 r 非空,表示已成功創(chuàng)建可讀的 socket 文件,即啟動(dòng)了 socket 服務(wù),開(kāi)始監(jiān)聽(tīng)請(qǐng)求。
if self in r:
self._handle_request_noblock()
if self in r 為 True 表示 socket 監(jiān)聽(tīng)服務(wù)已啟動(dòng),服務(wù)啟動(dòng)后會(huì)不斷執(zhí)行 self._handle_request_noblock()。
def _handle_request_noblock(self):
try:
request, client_address = self.get_request()
except socket.error:
return
if self.verify_request(request, client_address):
try:
self.process_request(request, client_address)
except:
self.handle_error(request, client_address)
self.shutdown_request(request)
else:
self.shutdown_request(request)
class TCPServer(BaseServer):
def get_request(self):
return self.socket.accept()
def process_request(self, request, client_address):
self.finish_request(request, client_address)
self.shutdown_request(request)
def finish_request(self, request, client_address):
self.RequestHandlerClass(request, client_address, self)
self._handle_request_noblock() 中調(diào)用 self.get_request(),self.get_request() 調(diào)用 socket.accept(),這個(gè)函數(shù)被動(dòng)接受 TCP 客戶端的連接,等待連接的到來(lái)(阻塞式,如果沒(méi)有連接到來(lái),程序會(huì)停留在這個(gè)地方,直到請(qǐng)求到來(lái)才會(huì)往后執(zhí)行)。
二、HTTP 消息在 Flask 中的流動(dòng)
接收到請(qǐng)求后,調(diào)用 self.process_request() -> self.finish_request() -> self.RequestHandlerClass(request, client_address, self),在 Flask 漸進(jìn)式源碼解讀: 0.1,二、Flask 如何收到請(qǐng)求? 中分析過(guò),調(diào)用 self.RequestHandlerClass(request, client_address, self),后續(xù)會(huì)調(diào)用 WSGIRequestHandler.__init__() -> BaseRequestHandler.__init__() -> self.handle() -> Flask.__call__(),這將請(qǐng)求傳遞至 Flask 處理。
BaseRequsetHandler.__init__() 中調(diào)用 self.setup(),這個(gè)方法被 StreamRequestHandler 覆寫:
class StreamRequestHandler(BaseRequestHandler):
rbufsize = -1
wbufsize = 0
timeout = None
disable_nagle_algorithm = False
def setup(self):
self.connection = self.request
if self.timeout is not None:
self.connection.settimeout(self.timeout)
if self.disable_nagle_algorithm:
self.connection.setsockopt(socket.IPPROTO_TCP,
socket.TCP_NODELAY, True)
self.rfile = self.connection.makefile('rb', self.rbufsize)
self.wfile = self.connection.makefile('wb', self.wbufsize)
其創(chuàng)建了兩個(gè) io 流:self.rfile 用于讀,self.wfile 用于寫。socket 接收到的請(qǐng)求數(shù)據(jù),可以從 self.rfile 中讀到,socket 要返回給客戶端的響應(yīng)數(shù)據(jù),存儲(chǔ)在 self.wfile 中,調(diào)用 self.wfile.flush() 就會(huì)向客戶端發(fā)送數(shù)據(jù)。
class WSGIRequestHandler(BaseHTTPRequestHandler, object):
def run_wsgi(self):
app = self.server.app
environ = self.make_environ()
def write(data):
...
self.wfile.write(data)
self.wfile.flush()
def execute(app):
application_iter = app(environ, start_response)
try:
for data in application_iter:
write(data)
# make sure the headers are sent
if not headers_sent:
write('')
finally:
if hasattr(application_iter, 'close'):
application_iter.close()
application_iter = None
Flask.__call__() 返回一個(gè)可迭代對(duì)象,后續(xù)迭代,將數(shù)據(jù)寫入 io 流(write(data))并發(fā)送給客戶端。
三、Flask 生成 JSON 格式響應(yīng)
在響應(yīng)中,Content-Type 標(biāo)頭告訴客戶端實(shí)際返回的內(nèi)容的內(nèi)容類型。要生成 JSON 格式響應(yīng),只需要做兩件事:
- 設(shè)置標(biāo)頭:
Content-Type: application/json - 將數(shù)據(jù)轉(zhuǎn)換為 JSON 格式的字符串
def jsonify(*args, **kwargs):
return current_app.response_class(json.dumps(dict(*args, **kwargs),
indent=None if request.is_xhr else 2), mimetype='application/json')
jsonify 函數(shù)完成了這兩件事。
- 設(shè)置標(biāo)頭:
mimetype='application/json' - 轉(zhuǎn)換格式:
json.dumps(dict(*args, **kwargs), indent=None if request.is_xhr else 2)
以下例子,在路由函數(shù)中,將數(shù)據(jù)傳入 jsonify 并返回即可生成 JSON 格式響應(yīng)。
@app.route('/_get_current_user')
def get_current_user():
return jsonify(username=g.user.username,
email=g.user.email,
id=g.user.id)
"""返回的 JSON 格式響應(yīng)
{
"username": "admin",
"email": "admin@localhost",
"id": 42
}
"""
四、更快捷的 JSON 化數(shù)據(jù)
JSON 是前后端數(shù)據(jù)交互最流行的方式之一,如果整個(gè) Flask 項(xiàng)目 API 返回?cái)?shù)據(jù)都是 JSON 格式,每個(gè)視圖函數(shù)最后都調(diào)用一次 jsonify 函數(shù)顯的很冗余。能否不調(diào)用 jsonify 而直接返回 Python 原生數(shù)據(jù)類型/自定義數(shù)據(jù)類型(比如 SQLAlchemy 的 Model 類型)就能生成 JSON 化響應(yīng)?來(lái)嘗試對(duì) Flask 做一些框架層面的修改。
要實(shí)現(xiàn)以上目標(biāo),只需把以下兩個(gè)操作添加到 Flask 生成響應(yīng)之前即可:
- 設(shè)置標(biāo)頭:
Content-Type: application/json - 將數(shù)據(jù)轉(zhuǎn)換為 JSON 格式的字符串
4.1 設(shè)置標(biāo)頭:Content-Type: application/json
生成響應(yīng)時(shí)標(biāo)頭 Content-Type 的值默認(rèn)取 Flask.response_class 中 default_mimetype 屬性的值,因此只需要繼承響應(yīng)基類并覆寫 default_mimetype = 'application/json' ,將 Flask 響應(yīng)類替換掉即可。實(shí)現(xiàn)如下:
from werkzeug import Response as ResponseBase
class JSONResponse(ResponseBase):
default_mimetype = 'application/json'
Flask.response_class = JSONResponse
4.2 將數(shù)據(jù)轉(zhuǎn)換為 JSON 格式的字符串
視圖函數(shù)返回值轉(zhuǎn)換為響應(yīng)體對(duì)象是在 Flask.make_response 方法中實(shí)現(xiàn)的,將 Flask.make_response 覆寫,判斷接收到的參數(shù)是否需要進(jìn)行 JSON 格式化,如果需要?jiǎng)t轉(zhuǎn)換為 JSON 格式。例如自動(dòng)將 dict, list 格式數(shù)據(jù)轉(zhuǎn)化為 JSON 格式響應(yīng),實(shí)現(xiàn)如下:
from flask import Flask as FlaskBase
class Flask(FlaskBase):
def make_response(self, rv):
if isinstance(rv, (dict, list)):
rv = json.dumps(rv)
return FlaskBase.make_response(self, rv)
視圖函數(shù)直接返回 dict, list 即可生成 JSON 格式響應(yīng),不需再調(diào)用 jsonify 處理,示例:
@app.route('/json/list')
def test_json():
# return jsonify([1, 2, 3])
return [1, 2, 3]
@app.route('/json/dict')
def test_json():
# return jsonify({'hello': 'world', 'name': 'huaiyue'})
return {'hello': 'world', 'name': 'huaiyue'}
以上修改實(shí)現(xiàn)見(jiàn):https://github.com/yyywang/flask-backend-clean-architecture
版本:
python 2.7, werkzeug==0.6.1, Flask==0.2參考文獻(xiàn):
[1] Flask changes. (n.d). Retrieved February 19, 2023, from https://flask.palletsprojects.com/en/2.2.x/changes/#version-0-2.
[2] MDN Web Docs. (n.d). Retrieved February 20, 2023, from https://developer.mozilla.org/zh-CN/docs/Web/HTTP.