2023-02-20

〇、前言

往期解讀

本期導(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 rTrue 表示 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),只需要做兩件事:

  1. 設(shè)置標(biāo)頭:Content-Type: application/json
  2. 將數(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ù)完成了這兩件事。

  1. 設(shè)置標(biāo)頭:mimetype='application/json'
  2. 轉(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)之前即可:

  1. 設(shè)置標(biāo)頭:Content-Type: application/json
  2. 將數(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_classdefault_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.

本文源碼:https://github.com/yyywang/flask-docs/blob/main/Flask%200.2%20%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB:%20HTTP%20%E6%B6%88%E6%81%AF%E5%9C%A8%20Flask%20%E4%B8%AD%E7%9A%84%E6%B5%81%E5%8A%A8%E4%B8%8E%E5%A4%84%E7%90%86.md

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容