Skip to content

Flask与HTTP

HTTP(Hypertext Transfer Protocol,超文本传输协议)定义了服务器和客户端之间信息交流的格式和传递方式,它是万维网(World Wide Web)中数据交换的基础

请求响应循环

以一个真实的 URL 为例:

http://helloflask.com/hello

当我们在浏览器中的地址栏中输入这个 URL,然后按下 Enter 时,稍等片刻,浏览器会显式一个问候页面。
这背后到底发生了什么?你一定可以猜想到,这背后有一个程序运行着。
它负责接收用户的请求,并把对应的内容返回给客户端,显示在用户的浏览器上。
事实上,每一个 Web 应用都包含这种处理模式,即“请求-响应循环(Request-Response Cycle)”: 客户端发出请求,服务器端处理请求并返回响应

这是每一个 Web 程序的基本工作模式,如果再进异步,这个模式又包含着更多的工作单元

例如一个 Flask 程序工作的实际流程:

当用户访问一个 URL,浏览器便生成对应的 HTTP 请求,经由互联网发送到对应的 Web 服务器。
Web 服务器接收请求,通过 WSGI 将 HTTP 格式的请求数据转换成我们的 Flask 程序能够使用的 Python 数据。
在程序中,Flask 根据请求的 URL 执行对应的视图函数,获取返回值生成响应。
响应依次经过 WSGI 转换生成 HTTP 响应,再经由 Web 服务器传递,最终被发出请求的客户端接收。
浏览器渲染响应中包含的 HTML 和 CSS 代码,并执行JavaScript 代码,最终把解析后的页面呈现在用户浏览器的窗口中

HTTP 请求

URL 是一个请求的起源。
不论服务器是运行在美国洛杉矶,还是运行在我们自己的电脑上,当我们输入指向服务器所在地址的 URL,都会向服务器发送一个 HTTP 请求。
一个标准的 URL 由多部分组成,以下面这个 URL 为例:

http://helloflask.com/hello?name=Grey

这个 URL 的各个部分如表所示:

信息 说明
http:// 协议字符串,指定要使用的协议
helloflask.com 服务器的地址(域名)
/hello?name=Grey 要获取的资源路径(path),类似 UNIX 的文件目录结构

注:
这个 URL 后面的 ?name=Grey 部分是查询字符串(query string)。
URL 中的查询字符串用来向指定的资源传递参数。查询字符串从问号 ? 开始,以键值对的形式写出,多个键值对之间使用 & 分隔

请求报文

当我们在浏览器中访问这个 URL 时,随之产生的是一个发向 http://helloflask.com 所在服务器的请求。
请求的实质是发送到服务器上的一些数据,这种浏览器与服务器之间交互的数据被称为报文(message),请求时浏览器发送的数据被称为请求报文(request message),而服务器返回的数据被称为响应报文(response message)。

请求报文由请求的方法、URL、协议版本、首部字段(header)以及内容实体组成。
前面的请求产生的请求报文示意如表所示:

组成说明 请求报文内容
报文首部: 请求行(方法、URL、协议) GET /hello HTTP/1.1
报文首部: 各种首部字段 Host: helloflask.com Connection: keep-alive Cache-Control: max-age=0 User-Agent: Mozilla/5.0(Windows NT6.1;Win64;x64) AppleWebKit/537.36(KHTML,like Gecko) Chrome/59.0.3071.104 Safari/537.36...
空行
报文主体 name=Grey

如果想看真实的 HTTP 报文,可以在浏览器中向任意一个有效的 URL 发起请求,然后在浏览器的开发者工具(F12)里的 Network 标签中看到 URL 对应资源加载的所有请求列表,单击任一个请求条目即可看到报文信息

报文由报文首部和报文主体组成,两者由空行分隔,请求报文的主体一般为空。
如果 URL 中包含查询字符串,或是提交了表单,那么报文主体将会是查询字符串和表单数据。

HTTP 通过方法来区分不同的请求类型。
比如,当你直接访问一个页面时,请求的方法时 GET;当你在某个页面填写了表单并提交时,请求方法则通常为 POST。
下表时常见的几种 HTTP 方法类型

方法 说明
GET 获取资源
POST 创建或更新资源
PUT 创建或替换资源
DELETE 删除资源
HEAD 获得报文首部
OPTIONS 询问支持的方法

Request 对象

注:
请求解析和响应封装实际上大部分时由 Werkzeug 完成的,Flask 子类化 Werkzeug 的请求(Request)和响应(Response)对象并添加了和程序相关的特定功能。

假设请求的 URL 是 http://helloflask.com/hello?name=Grey,当 Flask 接收到请求后,请求对象会提供多个属性来获取 URL 的各个部分,常用的属性如表所示:

属性
path '/hello'
full_path '/hello?name=Grey'
host 'helloflask.com'
host_url 'http://helloflask.com/'
base_url 'http://helloflask.com/hello'
url 'http://helloflask.com/hello?name=Grey'
url_root 'http://helloflask.com/'

除了 URL,请求报文中的其他信息都可以通过 request 对象提供的属性和方法获取,其中常用的部分如表所示:

属性/方法 说明
args Werkzeug 的 ImmutableMultiDict 对象。存储解析后的查询字符串,可通过字典方式获取键值。如果你想获取未解析的原生查询字符串,可以使用 query_string 属性
blueprint 当前蓝本的名称
cookies 一个包含所有随请求提交的 cookies 的字典
data 包含字符串形式的请求数据
endpoint 与当前请求相匹配的端点值
files Werkzeug 的 MultiDict 对象,包含所有上传文件,可以使用字典的形式获取文件。使用的键为文件 input 标签中的 name 属性值,对应的值为 Werkzeug 的 FileStorage 对象,可以调用 save() 方法并传入保存路径来保存文件
form Werkzeug 的 ImmutableMultiDict 对象。与 files 类似,包含解析后的表单数据。表单字段值通过 input 标签的 name 属性值作为键获取
values Werkzeug 的 CombinedMultiDict 对象,结合了 args 和 form 属性的值
get_data(cache=True, as_text=False, parse_from_data=False) 获取请求中的数据,默认读取为字节字符串(bytestring),将 as_text 设为 True 则返回值将是解码后的 unicode 字符串
get_json(self, force=False, silent=False, cache=True) 作为 JSON 解析并返回数据,如果 MIME 类型不是 JSON,返回 None(除非 force 设为 True): 解析出错则跑出 Werkzeug 提供的 BadRequest 异常(如果未开启调试模式,则返回 400 错误响应),如果 silent 设为 True 则返回 None;cache 设置是否缓存解析后的 JSON 数据
headers 一个 Werkzeug 的 EnvironHeaders 对象,包含首部字段,可以以字典的形式操作
is_json 通过 MIME 类型判断是否为 JSON 数据,返回布尔值
json 包含解析后的 JSON 数据,内部调用 get_json(),可通过字典的方式获取键值
method 请求的 HTTP 方法
referer 请求发起的源 URL,即 referer
scheme 请求的 URL 模式(http 或 https)
user_agent 用户代理(User Agent, UA), 包含了用户的客户端类型,操作系统类型等信息

提示:
Werkzeug 的 MultiDict 类是字典的子类,它主要实现了同一个键对应多个值的情况。
比如一个文件上传字段可能会接收多个文件。这时就可以通过 getlist() 方法来获取文件对象列表。
而 ImmutableMultiDict 类继承了 MultiDict 类,但其值不可更改。

在 Flask 中处理请求

URL 是指向网络上资源的地址。
在 Flask 中,我们需要让请求的 URL 匹配对应的视图函数,视图函数返回值就是 URL 对应的资源。

路由匹配

为了便于将请求分发到对应的视图函数,程序实例中存储了一个路由表(app.url_map),其中定义了 URL 规则和视图函数的映射关系。
当请求发来后,Flask 会根据请求报文中的 URL(path 部分)来尝试与这个表中的所有 URL 规则进行匹配,调用匹配成功的视图函数。
如果没有找到匹配的 URL 规则,说明程序中没有处理这个 URL 的视图函数,Flask 会自动返回 404 错误响应(Not Found,表示资源未找到)。

当请求的 URL 与某个视图函数的 URL 规则匹配成功时,对应的视图函数就会被调用。
使用 flask routes 命令可以查看程序中定义的所有路由,这个例诶包由 app.url_map 解析得到

1
2
3
4
5
flask routes
Endpoint  Methods  Rule
--------  -------  -----------------------
download  GET      /download/<path:path>
static    GET      /static/<path:filename>

在输出的文本中,我们可以看到每个路由对应的端点(Endpoint)、HTTP 方法(Methods)和 URL 规则(Rule),其中 static 端点是 Flask 添加的特殊路由,用来访问静态文件

设置监听的 HTTP 方法

通过 flask routes 命令打印出的路由列表可以看到,每一个路由除了包含 URL 规则外,还设置了监听的 HTTP 方法。
GET 是最常用的 HTTP 方法,所以视图函数默认监听的方法类型就是 GET,HEAD、OPTIONS 方法的请求由 Flask 处理,而像 DELETE、PUT 等方法一般不会在程序中实现

我们可以在 app.route() 装饰器中使用 methods 参数传入一个包含监听的 HTTP 方法的可迭代对象。
比如,下面的视图函数同时监听 GET 请求和 POST 请求:

1
2
3
@app.route('/hello', methods=['GET', 'POST'])
def hello():
    return '<h1>Hello, Flask!</h1>'

当某个请求的方法不符合要求时,请求将无法被正常处理。
比如,在提交表单时通常使用 POST 方法,而如果提交的目标 URL 对应的视图函数只允许 GET 方法,这时 Flask 会自动返回一个 405 错误响应(Method Not Allowed,表示请求方法不允许)

通过定义方法列表,我们可以为同一个 URL 规则定义多个视图函数,分别处理不同 HTTP 方法的请求

URL 处理

<int: year> 表示为 year 变量添加了一个 int 转换器,Flask 在解析这个 URL 变量时会将其转换为整型。
URL 中的变量部分默认类型为字符串,但 Flask 提供了一些转换器可以在 URL 规则里使用

Flask 内置的 URL 变量转换器:

转换器 说明
string 不包含斜线的字符串(默认值)
int 整型
float 浮点数
path 包含斜线的字符串。static 路由的 URL 规则中的 filename 变量就使用了这个转换器
any 匹配一系列给定值中的一个元素
uuid UUID 字符串

转换器通过特定的规则指定,即 "<转换器: 变量名>"。
<int: year> 把 year 的值转换为整数,因此我们可以在视图函数中直接对 year 变量进行数学计算:

1
2
3
@app.route('/goback/<int:year>')
def go_back(year):
    return '<p>Welcome to %d!</p>' % (2018-year)

默认的行为不仅仅时转换变量类型,还包括 URL 匹配。
在这个例子中,如果不使用转换器,默认 year 变量会被转换成字符串,为了能够在 Python 中计算天数,我们需要使用 int() 函数将 year 变量转换成整型。
但是如果用户输入的时英文字母,就会出现转换错误,抛出 ValueError 异常,我们还需要手动验证;
使用了转换器后,如果 URL 中传入的变量不是数字,那么会直接返回 404 错误响应。

在用法上唯一特别的时 any 转换器,你需要在转换器后添加括号来给出可选值,即 "<any(value1, value2, ...): 变量名>",比如:

1
2
3
@app.route('/colors/<any(blue,white,red):color>')
def three_colors(color):
    return '<p>Love is patient and kind. Lover is not jelous or boastful or proud or rude.</p>'

请求钩子

有时我么你需要对请求进行预处理(preprocessing)和后处理(postprocessing),这时可以使用 Flask 提供的一些请求钩子(Hook),它们可以用来注册在请求处理的不同截断执行的处理函数(或称为回调函数,即 Callback)。
这些请求钩子使用装饰器实现,通过程序实例 app 调用,用法很简单: 以 before_request 钩子(请求之前)为例,当你对一个函数附加了 app.before_request 装饰器后,就会将这个函数注册为 before_request 处理函数,每次执行请求前都会触发所有 before_request 处理函数。
Flask 默认实现的五种请求钩子如表所示:

钩子 说明
before_first_request 注册一个函数,在处理第一个请求前运行
before_request 注册一个函数,在处理每个请求前运行
after_request 注册一个函数,如果没有未处理的异常抛出,会在每个请求结束后运行
teardown_request 注册一个函数,即使有未处理的异常抛出,会在每个请求结束后运行。如果发生异常,会传入异常对象作为参数到注册的函数中
after_this_request 在视图函数内注册一个函数,会在这个请求结束后运行

这些钩子使用起来和 app.route() 装饰器基本相同,每个钩子可以注册任意多个处理函数,函数名并不是必须和钩子名称相同,下面是一个基本示例:

1
2
3
@app.before_request
def do_something():
    pass  # 这里的代码会在每个请求处理前执行

假设我们创建了三个视图函数 A、B、C,其中视图 C 使用了 after_this_request 钩子,那么当请求 A 进入后,整个请求处理周期的请求处理函数调用流程如下图所示:

请求处理函数调用示意图

下面是请求钩子的一些常见应用场景:

before_first_request:
运行程序前我们需要进行一些程序的初始化操作,比如创建数据库表,添加管理员用户。
这些工作可以放到使用 before_first_request 装饰器注册的函数中。

before_request:
比如网站上要记录用户最后在线的时间,可以通过用户最后发送的请求时间来实现。
为了避免在每个视图函数都添加更新在线时间的代码,我们可以仅在使用 befoer_request 钩子注册的函数中调用这段代码。

after_request:
我们经常在视图函数中进行数据库操作,比如更新、插入等,之后需要将更改提交到数据库中。
提交更改的代码就可以放到 after_request 钩子注册的函数中。

另一种常见的应用是建立数据库连接,通常会有多个视图函数需要建立和关闭数据库连接,这些操作基本相同。
一个理想的解决方法是在请求之前(before_request)建立连接,在请求之后(teardown_request)关闭连接。
通过在使用相应的请求钩子注册的函数中添加代码就可以实现。
这很像单元测试中的 setUp() 方法和 tearDown() 方法。

注:
after_request 钩子和 after_this_request 钩子必须接收一个响应类对象作为参数,并且返回同一个或更新后的响应对象。

HTTP 响应

在 Flask 程序中,客户端发出的请求触发相应的视图函数,获取返回值会作为响应的主体,最后生成完整的响应,即响应报文

响应报文

响应报文主要由协议版本、状态码(status code)、原因短语(reson pharse)、响应首部和响应主体组成。
以发向 localhost:5000/hello 的请求为例,服务器生成的响应报文示意如表所示:

组成说明 响应报文内容
报文首部: 状态行(协议、状态码、原因短语) HTTP/1.1 200 OK
报文首部: 各种首部字段 Content-Type: text/html; charset=utf-8 Countent-Length: 22 Server: Werkzeug/0.12.2 Python/2.7.13 Date: Thu. 03 Aug 2017 05:05:54 GMT...
空行
报文主体 <h1>Hello. Human!</h1>

响应报文的首部包含一些关于响应的服务器的信息,这些内容由 Flask 生成,而我们在视图函数中返回的内容即为响应报文中的主体内容。
浏览器接收到响应后,会把返回的响应主体解析并显示在浏览器窗口上。

在 Flask 中生成响应

响应在 Flask 中使用 Response 对象表示,响应报文中的大部分内容由服务器处理,大多数情况下,我们只负责返回主体内容。

Flask 会先判断是否可以找到与请求 URL 相匹配的路由,如果没有则返回 404 响应。
如果找到,则调用对应的视图函数,视图函数的返回值构成了响应报文的主体内容,正确返回时状态码默认为 200。
Flask 会调用 Make_response() 方法将视图函数返回值转换为响应对象。

完整地说,视图函数可以返回最多由三个元素组成的元组: 响应主体、状态码、首部字段。
其中首部字段可以为字典,或是两元素元组组成的列表:

比如,普通的响应可以只包含主体内容:

1
2
3
4
@app.route('/hello')
def hello():
    ...
    return '<h1>Hello, Flask!</h1>'

默认的状态码为 200,下面指定了不同的状态码:

1
2
3
4
@app.route('/hello')
def hello():
    ...
    return '<h1>Hello, Flask!</h1>', 201

有时你会想附加或修改某个首部字段。
比如,要生成状态码为 3XX 的重定向响应,需要将首部中的 Location 字段设置为重定向的目标 URL:

1
2
3
4
@app.route('/hello')
def hello():
    ...
    return '<h1>Hello, Flask!</h1>', 302, {'Location': 'http://www.example.com'}

响应格式

在 HTTP 响应中,数据可以通过多种格式传输。
大多数情况下,我们会使用 HTML 格式,这也是 Flask 中的默认设置。在特定的情况下,我们也会使用其他格式。
不同的响应数据格式需要设置不同的 MIME 类型,MIME 类型在首部的 Content-Type 字段中定义,以默认的 HTML 类型为例:

1
Content-Type: text/html; charset=utf-8

附注:
MIME 类型(又称为 media type 或 content type)是一种用来表示同意文件类型的机制,它与文件扩展名相对应,可以让客户端区分不同的内容类型,并执行不同的操作。
一般的格式为 “类型名/子类型名”,其中的子类型名一般为文件扩展名。
比如,HTML 的 MIME 类型为 "text/html",png 图片的 MIME 类型为 "image/png"。

完整的标准 MIME 类型列表可以在这里看到:

https://www.iana.org/assignments/media-types/media-types.xhtml

如果你想使用其他 MIME 类型,可以通过 Flask 提供的 make_response() 方法生成响应对象,传入响应的主体作为参数,然后使用响应对象的 mimetype 属性设置 MIME 类型,比如

1
2
3
4
5
6
7
from flask import make_response

@app.route('/foo')
def roo():
    response = make_response('Hello, World!')
    response.mimetype = 'text/plain'
    return response

你也可以直接设置首部字段,比如 response.headers['Content-Type']='text/xml;charset=utf-8'
但操作 mimetype 属性更加方便,而且不用设置字符集(charset)选项。

HTTP 是无状态(stateless)协议。也就是说,在一次请求响应结束后,服务器不会留下任何关于对方状态的信息。
但是对于某些 Web 程序来说,客户端的某些信息又必须被记住,比如用户的登录状态,这样才可以根据用户的状态来返回不同的响应。
为了解决这类问题,就有了 Cookie 技术。
Cookie 技术通过在请求和响应报文中添加 Cookie 数据来保存客户端的状态信息。