2023-02-20

〇、前言

往期解读

本期导读

Flask 0.2 提供了快捷生成 JSON 响应的函数:jsonify,如何实现的呢?网络中的字节流数据如何传递到 Flask,Flask 又是如何生成字节流数据返回给客户端的?我们从服务器接收到 HTTP 消息说起。

一、服务器接收到 HTTP 消息

HTTP 消息是“一问一答”的形式,客户端发问(请求),服务端回答(响应)。先有客户端还是先有服务端?

去小卖铺买冰淇淋,如果老板不在店里,在店里喊:“老板,来个冰淇淋”,老板能有回复吗?不能,因为老板没有在接收消息,必须要老板在线,处于接收消息的状态,我们发出的“请求”,才能得到“响应”。客户端与服务端也是同样的道理,必须先在服务端监听请求,然后才能收到客户端发送的请求。

客户端发送的 HTTP 消息有目标地址:主机:端口,是基于 TCP/IP 协议传输的,socket 把 TCP/IP 层复杂的操作抽象封装为了几个简单的接口,供应用层调用实现程序在网络中的通信。

在 UNIX 操作系统中,socket 就是一个文件,在服务端调用 socket 接口监听 TCP 请求,实际上是创建了一个可读的文件,当文件中有数据被写入时,就收到了客户端发来的请求。

Flask 渐进式源码解读: 0.1》中说到,执行 serve_forever 即可启动 Flask 服务,来详细看看:

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) 相当于执行 select.select([self], [], [], poll_),这是一个系统调用,返回值是三个列表,包含已就绪对象,若返回值 r 非空,表示已成功创建可读的 socket 文件,即启动了 socket 服务,开始监听请求。

if self in r:
    self._handle_request_noblock()

if self in rTrue 表示 socket 监听服务已启动,服务启动后会不断执行 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() 中调用 self.get_request()self.get_request() 调用 socket.accept(),这个函数被动接受 TCP 客户端的连接,等待连接的到来(阻塞式,如果没有连接到来,程序会停留在这个地方,直到请求到来才会往后执行)。

二、HTTP 消息在 Flask 中的流动

接收到请求后,调用 self.process_request() -> self.finish_request() -> self.RequestHandlerClass(request, client_address, self),在 Flask 渐进式源码解读: 0.1,二、Flask 如何收到请求? 中分析过,调用 self.RequestHandlerClass(request, client_address, self),后续会调用 WSGIRequestHandler.__init__() -> BaseRequestHandler.__init__() -> self.handle() -> Flask.__call__(),这将请求传递至 Flask 处理。

BaseRequsetHandler.__init__() 中调用 self.setup(),这个方法被 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)

其创建了两个 io 流:self.rfile 用于读,self.wfile 用于写。socket 接收到的请求数据,可以从 self.rfile 中读到,socket 要返回给客户端的响应数据,存储在 self.wfile 中,调用 self.wfile.flush() 就会向客户端发送数据。

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__() 返回一个可迭代对象,后续迭代,将数据写入 io 流(write(data))并发送给客户端。

三、Flask 生成 JSON 格式响应

在响应中,Content-Type 标头告诉客户端实际返回的内容的内容类型。要生成 JSON 格式响应,只需要做两件事:

  1. 设置标头:Content-Type: application/json
  2. 将数据转换为 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 函数完成了这两件事。

  1. 设置标头:mimetype='application/json'
  2. 转换格式:json.dumps(dict(*args, **kwargs), indent=None if request.is_xhr else 2)

以下例子,在路由函数中,将数据传入 jsonify 并返回即可生成 JSON 格式响应。

@app.route('/_get_current_user')
def get_current_user():
    return jsonify(username=g.user.username,
                    email=g.user.email,
                    id=g.user.id)

"""返回的 JSON 格式响应
{
    "username": "admin",
    "email": "admin@localhost",
    "id": 42
}
"""

四、更快捷的 JSON 化数据

JSON 是前后端数据交互最流行的方式之一,如果整个 Flask 项目 API 返回数据都是 JSON 格式,每个视图函数最后都调用一次 jsonify 函数显的很冗余。能否不调用 jsonify 而直接返回 Python 原生数据类型/自定义数据类型(比如 SQLAlchemy 的 Model 类型)就能生成 JSON 化响应?来尝试对 Flask 做一些框架层面的修改。

要实现以上目标,只需把以下两个操作添加到 Flask 生成响应之前即可:

  1. 设置标头:Content-Type: application/json
  2. 将数据转换为 JSON 格式的字符串

4.1 设置标头:Content-Type: application/json

生成响应时标头 Content-Type 的值默认取 Flask.response_classdefault_mimetype 属性的值,因此只需要继承响应基类并覆写 default_mimetype = 'application/json' ,将 Flask 响应类替换掉即可。实现如下:

from werkzeug import Response as ResponseBase


class JSONResponse(ResponseBase):
    default_mimetype = 'application/json'


Flask.response_class = JSONResponse

4.2 将数据转换为 JSON 格式的字符串

视图函数返回值转换为响应体对象是在 Flask.make_response 方法中实现的,将 Flask.make_response 覆写,判断接收到的参数是否需要进行 JSON 格式化,如果需要则转换为 JSON 格式。例如自动将 dict, list 格式数据转化为 JSON 格式响应,实现如下:

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)

视图函数直接返回 dict, list 即可生成 JSON 格式响应,不需再调用 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'}

以上修改实现见:https://github.com/yyywang/flask-backend-clean-architecture

版本:python 2.7, werkzeug==0.6.1, Flask==0.2

参考文献:

[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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341

推荐阅读更多精彩内容