本文基于《Flask web 开发实战》第 2 章《Flask 与 HTTP》删减改写而来, 作为该书的样章分享.
HTTP(Hypertext Transfer Protocol, 超文本传输协议)定义了服务器和客户端之间信息交流的格式和传递方式, 它是万维网 (World Wide Web) 中数据交换的基础. 在这篇文章中, 我们会以 HTTP 协议定义的请求响应循环流程作为框架, 了解 Flask 处理请求和响应的各种方式.
附注 HTTP 的详细定义在 RFC 7231~7235 中可以看到. RFC(Request For Comment, 请求评议)是一系列关于互联网标准和信息的文件, 可以将其理解为互联网 (Internet) 的设计文档. 完整的 RFC 列表可以在这里看到: https://tools.ietf.org/rfc/.
本章的示例程序在 helloflask 仓库 https://github.com/greyli/helloflask 的 helloflask/demos/http 目录下, 你可以通过下面的操作来运行程序:
- $ Git clone https://github.com/greyli/helloflask
- $ cd helloflask
- $ pipenv install
- $ pipenv shell
- $ cd demos/http
$ flask run
请求响应循环
为了更贴近现实, 我们以一个真实的 URL 为例:
http://helloflask.com/hello
当我们在浏览器中的地址栏中输入这个 URL, 然后按下 Enter, 稍等片刻, 浏览器会显示一个问候页面. 这背后到底发生了什么? 你一定可以猜想到, 这背后也有一个类似我们第 1 章编写的程序运行着. 它负责接收用户的请求, 并把对应的内容返回给客户端, 显示在用户的浏览器上. 事实上, 每一个 Web 应用都包含这种处理模式, 即 "请求 - 响应循环(Request-Response Cycle)": 客户端发出请求, 服务器处理请求并返回响应, 如下图所示:
附注 客户端 (Client Side) 是指用来提供给用户的与服务器通信的各种软件. 在本书中, 客户端通常指 Web 浏览器 (后面简称浏览器), 比如 Chrome,Firefox,IE 等; 服务器端(Server Side) 则指为用户提供服务的服务器, 也是我们的程序运行的地方.
这是每一个 Web 程序的基本工作模式, 如果再进一步, 这个模式又包含着更多的工作单元,
下图展示了一个 Flask 程序工作的实际流程:
从上图可以看出, HTTP 在整个流程中起到了至关重要的作用, 它是客户端和服务器端之间沟通的桥梁.
当用户访问一个 URL, 浏览器便生成对应的 HTTP 请求, 经由互联网发送到对应的 Web 服务器. Web 服务器接收请求, 通过 WSGI 将 HTTP 格式的请求数据转换成我们的 Flask 程序能够使用的 Python 数据. 在程序中, Flask 根据请求的 URL 执行对应的视图函数, 获取返回值生成响应. 响应依次经过 WSGI 转换生成 HTTP 响应, 再经由 Web 服务器传递, 最终被发出请求的客户端接收. 浏览器渲染响应中包含的 html 和 CSS 代码, 并执行 JavaScript 代码, 最终把解析后的页面呈现在用户浏览器的窗口中.
提示 关于 WSGI 的更多细节, 我们会在第 16 章进行详细介绍.
提示 这里的服务器指的是处理请求和响应的 Web 服务器, 比如我们上一章介绍的开发服务器, 而不是指物理层面上的服务器主机.
HTTP 请求
URL 是一个请求的起源. 不论服务器是运行在美国洛杉矶, 还是运行在我们自己的电脑上, 当我们输入指向服务器所在地址的 URL, 都会向服务器发送一个 HTTP 请求. 一个标准的 URL 由很多部分组成, 以下面这个 URL 为例:
http://helloflask.com/hello?name=Grey
这个 URL 的各个组成部分如下表所示:
附注 这个 URL 后面的? name=Grey 部分是查询字符串(query string).URL 中的查询字符串用来向指定的资源传递参数. 查询字符串从问号? 开始, 以键值对的形式写出, 多个键值对之间使用 & 分隔.
请求报文
当我们在浏览器中访问这个 URL 时, 随之产生的是一个发向 http://helloflask.com 所在服务器的请求. 请求的实质是发送到服务器上的一些数据, 这种浏览器与服务器之间交互的数据被称为报文(message), 请求时浏览器发送的数据被称为请求报文(request message), 而服务器返回的数据被称为响应报文(response message).
请求报文由请求的方法, URL, 协议版本, 首部字段 (header) 以及内容实体组成. 前面的请求产生的请求报文示意如下表所示:
如果你想看看真实的 HTTP 报文, 可以在浏览器中向任意一个有效的 URL 发起请求, 然后在浏览器的开发者工具 (F12) 里的 Network 标签中看到 URL 对应资源加载的所有请求列表, 点击任一个请求条目即可看到报文信息, 下图是使用 Chrome 访问本地的示例程序的示例:
报文由报文首部和报文主体组成, 两者由空行分隔, 请求报文的主体一般为空. 如果 URL 中包含查询字符串, 或是提交了表单, 那么报文主体将会是查询字符串和表单数据.
HTTP 通过方法来区分不同的请求类型. 比如, 当你直接访问一个页面时, 请求的方法是 GET; 当你在某个页面填写了表单并提交时, 请求方法则通常为 POST. 下表是常见的几种 HTTP 方法类型:
报文首部包含了请求的各种信息和设置, 比如客户端的类型, 是否设置缓存, 语言偏好等等.
附注 HTTP 中可用的首部字段列表可以在 https://www.iana.org/assignments/message-headers/message-headers.xhtml 看到. 请求方法的详细列表和说明可以在 RFC 7231 中看到.
如果运行了示例程序, 那么当你在浏览器中访问 http://127.0.0.1:5000/hello, 开发服务器会在命令行中输出一条记录日志, 其中包含请求的主要信息:
127.0.0.1 - - [02/Aug/2017 09:51:37] "GET /hello HTTP/1.1" 200 -
Request 对象
现在该让 Flask 的请求对象 request 出场了, 这个请求对象封装了从客户端发来的请求报文, 我们能从它获取请求报文中的所有数据.
注意 请求解析和响应封装实际上大部分是由 Werkzeug 完成的, Flask 子类化 Werkzeug 的请求 (Request) 和响应 (Response) 对象并添加了和程序相关的特定功能. 在这里为了方便理解, 我们先略过不谈. 在第 16 章, 我们会详细了解 Flask 的工作原理.
和上一节一样, 我们先从 URL 说起. 假设请求的 URL 是 http://helloflask.com/hello?name=Grey , 当 Flask 接收到请求后, 请求对象会提供多个属性来获取 URL 的各个部分, 常用的属性如下表所示:
除了 URL, 请求报文中的其他信息都可以通过 request 对象提供的属性和方法获取, 其中常用的部分如下表所示:
提示 Werkzeug 的 MutliDict 类是字典的子类, 它主要实现了同一个键对应多个值的情况. 比如一个文件上传字段可能会接收多个文件. 这时就可以通过 getlist()方法来获取文件对象列表. 而 ImmutableMultiDict 类继承了 MutliDict 类, 但其值不可更改. 具体访问 Werkzeug 文档相关数据结构章节 werkzeug.pocoo.org/docs/latest....
在我们的示例程序中实现了同样的功能. 当你访问 http://localhost:5000/hello?name=Grey, 页面加载后会显示 "Hello, Grey!". 这说明处理这个 URL 的视图函数从查询字符串中获取了查询参数 name 的值, 如下所示:
- from flask import Flask, request
- App = Flask(__name__)
- @App.route('/hello')
- def hello():
- name = request.args.get('name', 'Flask') # 获取查询参数 name 的值
- return '<h1>Hello, %s!</h1>' % name # 插入到返回值中
注意 上面的示例代码包含安全漏洞, 在现实中我们要避免直接将用户传入的数据直接作为响应返回, 在本章的末尾我们将介绍包括这个漏洞在内的 Web 常见安全漏洞的具体细节和防范措施.
需要注意的是, 和普通的字典类型不同, 当我们从 request 对象中类型为 MutliDict 或 ImmutableMultiDict 的属性 (比如 files,form,args) 中直接使用键作为索引获取数据时 (比如 request.args['name']), 如果没有对应的键, 那么会返回 HTTP 400 错误响应(Bad Request, 表示请求无效), 而不是抛出 KeyError 异常, 如下图所示. 为了避免这个错误, 我们应该使用 get() 方法获取数据, 如果没有对应的值则返回 None;get()方法的第二个参数可以设置默认值, 比如 requset.args.get('name', 'Human').
提示 如果开启了调试模式, 那么会抛出 BadRequestKeyError 异常并显示对应的错误堆栈信息, 而不是常规的 400 响应.
在 Flask 中处理请求
URL 是指向网络上资源的地址. 在 Flask 中, 我们需要让请求的 URL 匹配对应的视图函数, 视图函数返回值就是 URL 对应的资源.
路由匹配
为了便于将请求分发到对应的视图函数, 程序实例中存储了一个路由表 (App.url_map), 其中定义了 URL 规则和视图函数的映射关系. 当请求发来后, Flask 会根据请求报文中的 URL(path 部分) 来尝试与这个表中的所有的 URL 规则进行匹配, 调用匹配成功的视图函数. 如果没有找到匹配的 URL 规则, 说明程序中没有处理这个 URL 的视图函数, Flask 会自动返回 404 错误响应(Not Found, 表示资源未找到). 你可以尝试在浏览器中访问 http://localhost:5000/nothing, 因为我们的程序中没有视图函数负责处理这个 URL, 所以你会得到 404 响应, 如下图所示:
如果你经常上网, 那么肯定会对这个错误代码相当熟悉, 它表示请求的资源没有找到. 和前面提及的 400 错误响应一样, 这类错误代码被称为 HTTP 状态码, 用来表示响应的状态, 具体会在下面详细讨论.
当请求的 URL 与某个视图函数的 URL 规则匹配成功时, 对应的视图函数就会被调用. 使用 flask routes 命令可以查看程序中定义的所有路由, 这个列表由 App.url_map 解析得到:
- $ flask routes
- Endpoint Methods Rule
- -------- ------- -----------------------
- hello GET /hello
- go_back GET /goback/<int:age>
- hi GET /hi
- ...
static GET /static/<path:filename >
在输出的文本中, 我们可以看到每个路由对应的端点 (Endpoint),HTTP 方法(Methods) 和 URL 规则(Rule), 其中 static 端点是 Flask 添加的特殊路由, 用来访问静态文件, 具体我们会在第 3 章学习.
设置监听的 HTTP 方法
在上一节通过 flask routes 命令打印出的路由列表可以看到, 每一个路由除了包含 URL 规则外, 还设置了监听的 HTTP 方法. GET 是最常用的 HTTP 方法, 所以视图函数的默认监听的方法类型就是 GET,HEAD,OPTIONS 方法的请求由 Flask 处理, 而像 DELETE,PUT 等方法一般不会在程序中实现, 在后面我们构建 Web API 时才会用到这些方法.
我们可以在 App.route()装饰器中使用 methods 参数传入一个包含监听的 HTTP 方法的可迭代对象. 比如, 下面的视图函数同时监听 GET 请求和 POST 请求:
- @App.route('/hello', methods=['GET', 'POST'])
- def hello():
return '<h1>Hello, Flask!</h1>'
当某个请求的方法不符合要求时, 请求将无法被正常处理. 比如, 在提交表单时通常使用 POST 方法, 而如果提交的目标 URL 对应的视图函数只允许 GET 方法, 这时 Flask 会自动返回一个 405 错误响应(Method Not Allowed, 表示请求方法不允许), 如下图所示:
通过定义方法列表, 我们可以为同一个 URL 规则定义多个视图函数, 分别处理不同 HTTP 方法的请求, 我们在本书第二部分构建 Web API 时会用到这个特性.
3. URL 处理
从前面的路由列表中可以看到, 除了 / hello, 这个程序还包含许多 URL 规则, 比如和 go_back 端点对应的 / goback/<int:year>. 现在请尝试访问 http://localhost:5000/goback/34, 在 URL 中加入一个数字作为时光倒流的年数, 你会发现加载后的页面中有通过传入的年数计算出的年份:"Welcome to 1984!". 仔细观察一下, 你会发现 URL 规则中的变量部分有一些特别,<int:year > 表示为 year 变量添加了一个 int 转换器, Flask 在解析这个 URL 变量时会将其转换为整型. URL 中的变量部分默认类型为字符串, 但 Flask 提供了一些转换器可以在 URL 规则里使用, 如下表所示:
转换器通过特定的规则指定, 即 "<转换器: 变量名>".<int:year > 把 year 的值转换为整数, 因此我们可以在视图函数中直接对 year 变量进行数学计算:
- @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 错误响应. 比如, 你可以尝试访问 http://localhost:5000/goback/tang.
在用法上唯一特别的是 any 转换器, 你需要在转换器后添加括号来给出可选值, 即 "<any(value1, value2, ...): 变量名>", 比如:
- @App.route('/colors/<any(blue, white, red):color>')
- def three_colors(color):
return '<p>Love is patient and kind. Love is not jealous or boastful or proud or rude.</p>'
当你在浏览器中访问 http://localhost:5000/colors/<color > 时, 如果将 < color > 部分替换为 any 转换器中设置的可选值以外的任意字符, 均会获得 404 错误响应.
如果你想在 any 转换器中传入一个预先定义的列表, 可以通过格式化字符串的方式 (使用 % 或是 format() 函数)来构建 URL 规则字符串, 比如:
- colors = ['blue', 'white', 'red']
- @App.route('/colors/<any(%s):color>' % str(colors)[1:-1])
...
HTTP 响应
在 Flask 程序中, 客户端发出的请求触发相应的视图函数, 获取返回值会作为响应的主体, 最后生成完整的响应, 即响应报文.
响应报文
响应报文主要由协议版本, 状态码(status code), 原因短语(reason phrase), 响应首部和响应主体组成. 以发向 localhost:5000/hello 的请求为例, 服务器生成的响应报文示意如下表所示:
响应报文的首部包含了一些关于响应和服务器的信息, 这些内容由 Flask 生成, 而我们在视图函数中返回的内容即为响应报文中的主体内容. 浏览器接受到响应后, 会把返回的响应主体解析并显示在浏览器窗口上.
HTTP 状态码用来表示请求处理的结果, 下表是常见的几种状态码和相应的原因短语:
提示 当关闭调试模式时, 即 FLASK_ENV 使用默认值 production, 如果程序出错, Flask 会自动返回 500 错误响应; 而调试模式下则会显示调试信息和错误堆栈.
附注 响应状态码的详细列表和说明可以在 RFC 7231 中看到.
在 Flask 中生成响应
响应在 Flask 中使用 Response 对象表示, 响应报文中的大部分内容由服务器处理, 大多数情况下, 我们只负责返回主体内容.
根据我们在请求一节介绍的内容, Flask 会先判断是否可以找到与请求 URL 相匹配的路由, 如果没有则返回 404 响应. 如果找到, 则调用对应的视图函数, 视图函数的返回值构成了响应报文的主体内容, 正确返回时状态码默认为 200.Flask 会调用 make_response()方法将视图函数返回值转换为响应对象.
完整的说, 视图函数可以返回最多由三个元素组成的元组: 响应主体, 状态码, 首部字段. 其中首部字段可以为字典, 或是两元素元组组成的列表.
比如, 普通的响应可以只包含主体内容:
- @App.route('/hello')
- def hello():
- ...
return '<h1>Hello, Flask!</h1>'
默认的状态码为 200, 下面指定了不同的状态码:
- @App.route('/hello')
- def hello():
- ...
return '<h1>Hello, Flask!</h1>', 201
有时你会想附加或修改某个首部字段. 比如, 要生成状态码为 3XX 的重定向响应, 需要将首部中的 Location 字段设置为重定向的目标 URL:
- @App.route('/hello')
- def hello():
- ...
return '', 302, {'Location':'http://www.example.com'}
现在访问 http://localhost:5000/hello, 会重定向到 www.example.com. 在多数情况下, 除了响应主体, 其他部分我们通常只需要使用默认值即可.
重定向
如果你访问 http://localhost:5000/hi, 你会发现页面加载后地址栏中的 URL 变为了 http://localhost:5000/hello. 这种行为被称为重定向(Redirect), 你可以理解为网页跳转. 在上一节的示例中, 状态码为 302 的重定向响应的主体为空, 首部中需要将 Location 字段设为重定向的目标 URL, 浏览器接受到重定向响应后会向 Location 字段中的目标 URL 发起新的 GET 请求, 整个流程下图所示:
在 Web 程序中, 我们经常需要进行重定向. 比如, 当某个用户在没有经过认证的情况下访问需要登录后才能访问的资源, 程序通常会重定向到登录页面.
对于重定向这一类特殊响应, Flask 提供了一些辅助函数. 除了像前面那样手动生成 302 响应, 我们可以使用 Flask 提供的 redirect()函数来生成重定向响应, 重定向的目标 URL 作为第一个参数. 前面的例子可以简化为:
- from flask import Flask, redirect
- ...
- @App.route('/hello')
- def hello():
return redirect('http://www.example.com')
提示 使用 redirect()函数时, 默认的状态码为 302, 即临时重定向. 如果你想修改状态码, 可以在 redirect()函数中作为第二个参数或使用 code 关键字传入.
如果要在程序内重定向到其他视图, 那么只需在 redirect()函数中使用 url_for()函数生成目标 URL 即可, 如下所示:
- from flask import Flask, redirect, url_for
- ...
- @App.route('/hi')
- def hi():
- ...
- return redierct(url_for('hello')) # 重定向到 / hello
- @App.route('/hello')
- def hello():
...
错误响应
如果你访问 http://localhost:5000/brew/coffee, 你会获得一个 418 错误响应(I'm a teapot), 如下图所示:
附注 418 错误响应由 IETF(Internet Engineering Task Force, 互联网工程任务组)在 1998 年愚人节发布的 HTCPCP(Hyper Text Coffee Pot Control Protocol, 超文本咖啡壶控制协议)中定义(玩笑), 当一个控制茶壶的 HTCPCP 收到 BREW 或 POST 指令要求其煮咖啡时应当回传此错误.
大多数情况下, Flask 会自动处理常见的错误响应. HTTP 错误对应的异常类在 Werkzeug 的 werkzeug.exceptions 模块中定义, 抛出这些异常即可返回对应的错误响应. 如果你想手动返回错误响应, 更方便的方法是使用 Flask 提供的 abort()函数.
在 abort()函数中传入状态码即可返回对应的错误响应, 下面的视图函数返回 404 错误响应:
- from flask import Flask, abort
- ...
- @App.route('/404')
- def not_found():
abort(404)
提示 abort()函数前不需要使用 return 语句, 但一旦 abort()函数被调用, abort()函数之后的代码将不会被执行.
附注 虽然我们有必要返回正确的状态码, 但这并不是必须的. 比如, 当某个用户没有权限访问某个资源时, 返回 404 错误要比 403 错误更加友好.
本文基于《Flask Web 开发实战》第 2 章《Flask 与 HTTP》删减改写而来, 完整的章节目录请访问本书主页 (http://helloflask.com/book) 查看.
来源: https://juejin.im/post/5c00adcae51d455616193645