2 Flask与HTTP

2.1 请求响应循环

以一个真实的URL为例:

http://helloflask.com/hello

当我们在浏览器中的地址栏中输入这个URL,然后按下Enter时,稍
等片刻,浏览器会显示一个问候页面。这背后到底发生了什么?你一定
可以猜想到,这背后也有一个类似我们第1章编写的程序运行着。它负
责接收用户的请求,并把对应的内容返回给客户端,显示在用户的浏览
器上。事实上,每一个Web应用都包含这种处理模式,即“请求-响应循
环(Request-Response Cycle)”:客户端发出请求,服务器端处理请求
并返回响应,如图所示。
HTTP请求
客户端(Client Side)是指用来提供给用户的与服务器通信的各种
软件。在本书中,客户端通常指Web浏览器(后面简称浏览器),比如
Chrome、Firefox、IE等;服务器端(Server Side)则指为用户提供服务
的服务器,也是我们的程序运行的地方。
Flask Web程序工作流程
当用户访问一个URL,浏览器便生成对应的HTTP请求,经由互联
网发送到对应的Web服务器。Web服务器接收请求,通过WSGI将HTTP
格式的请求数据转换成我们的Flask程序能够使用的Python数据。在程序
中,Flask根据请求的URL执行对应的视图函数,获取返回值生成响应。
响应依次经过WSGI转换生成HTTP响应,再经由Web服务器传递,最终
被发出请求的客户端接收。浏览器渲染响应中包含的HTML和CSS代
码,并执行JavaScript代码,最终把解析后的页面呈现在用户浏览器的窗
口中。

2.2 HTTP请求

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

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

URL组成部分

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

2.2.1 请求报文

当我们在浏览器中访问这个URL时,随之产生的是一个发
向http://helloflask.com所在服务器的请求。请求的实质是发送到服务器
上的一些数据,这种浏览器与服务器之间交互的数据被称为报文
(message),请求时浏览器发送的数据被称为请求报文(request
message),而服务器返回的数据被称为响应报文(response
message)。
请求报文由请求的方法、URL、协议版本、首部字段(header)以
及内容实体组成。

请求报文

2.2.2 Request对象

这个请求对象封装了从客户端发来的请求报文,我们能从它获取请求报文中的所有数据。
请求解析和响应封装实际上大部分是由Werkzeug完成的,Flask子类化Werkzeug的请求(Request)和响应(Response)对象并添加了和程序相关的特定功能。

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

Werkzeug的MutliDict类是字典的子类,它主要实现了同一个键对应
多个值的情况。比如一个文件上传字段可能会接收多个文件。这时就可
以通过getlist()方法来获取文件对象列表。而ImmutableMultiDict类继
承了MutliDict类,但其值不可更改。更多内容可访问Werkzeug相关数据
结构章节http://werkzeug.pocoo.org/docs/latest/datastructures/。

当你访问
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

需要注意的是,和普通的字典类型不同,当我们从request对象的类
型为MutliDict或ImmutableMultiDict的属性(比如files、form、args)中
直接使用键作为索引获取数据时(比如request.args[‘name’]),如果没有
对应的键,那么会返回HTTP 400错误响应(Bad Request,表示请求无
效),而不是抛出KeyError异常,如图2-4所示。为了避免这个错误,我
们应该使用get()方法获取数据,如果没有对应的值则返回None;
get()方法的第二个参数可以设置默认值,比如
requset.args.get(‘name’,‘Human’)。

如果开启了调试模式,那么会抛出BadRequestKeyError异常并显示
对应的错误堆栈信息,而不是常规的400响应。

2.2.3 在Flask中处理请求

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

1. 路由匹配

为了便于将请求分发到对应的视图函数,程序实例中存储了一个路
由表(app.url_map),其中定义了URL规则和视图函数的映射关系。当
请求发来后,Flask会根据请求报文中的URL(path部分)来尝试与这个
表中的所有URL规则进行匹配,调用匹配成功的视图函数。如果没有找
到匹配的URL规则,说明程序中没有处理这个URL的视图函数,Flask会
自动返回404错误响应(Not Found,表示资源未找到)。你可以尝试在
浏览器中访问http://localhost:5000/nothing,因为我们的程序中没有视图
函数负责处理这个URL,所以你会得到404响应,
4040错误详情
当请求的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添加的特殊路由,用来访问静态文件,
2. 设置监听的HTTP方法
在上一节通过flask routes命令打印出的路由列表可以看到,每一个
路由除了包含URL规则外,还设置了监听的HTTP方法。GET是最常用
的HTTP方法,所以视图函数默认监听的方法类型就是GET,HEAD、
OPTIONS方法的请求由Flask处理,而像DELETE、PUT等方法一般不会
在程序中实现,在后面我们构建Web API时才会用到这些方法。

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


```python
@app.route('/hello', methods=['GET', 'POST'])
def hello():
	return '<h1>Hello, Flask!</h1>'
```
当某个请求的方法不符合要求时,请求将无法被正常处理。比如,
在提交表单时通常使用POST方法,而如果提交的目标URL对应的视图
函数只允许GET方法,这时Flask会自动返回一个405错误响应(Method
Not Allowed,表示请求方法不允许),如图2-6所示。
![405错误响应](https://img-blog.csdnimg.cn/20200622144722945.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3N1eW9uZ3poaTE=,size_16,color_FFFFFF,t_70)
通过定义方法列表,我们可以为同一个URL规则定义多个视图函

数,分别处理不同HTTP方法的请求

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规则里使用,如表所示。
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错误响应。

在用法上唯一特别的是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>'

如果你想在any转换器中传入一个预先定义的列表,可以通过格式
化字符串的方式(使用%或是format()函数)来构建URL规则字符
串,比如:

colors = ['blue', 'white', 'red']
@app.route('/colors/<any(%s):color>' % str(colors)[1:-1])

2.2.4 请求钩子

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

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

假如我们创建了三个视图函数A、B、C,其中视图C使用了
after_this_request钩子,那么当请求A进入后,整个请求处理周期的请求
处理函数调用流程如图2-7所示。
请求处理函数调用示意图
下面是请求钩子的一些常见应用场景:

  1. before_first_request:在玩具程序中,运行程序前我们需要进行一
    些程序的初始化操作,比如创建数据库表,添加管理员用户。这些工作
    可以放到使用before_first_request装饰器注册的函数中。
  2. before_request:比如网站上要记录用户最后在线的时间,可以通
    过用户最后发送的请求时间来实现。为了避免在每个视图函数都添加更
    新在线时间的代码,我们可以仅在使用before_request钩子注册的函数中
    调用这段代码。
  3. after_request:我们经常在视图函数中进行数据库操作,比如更
    新、插入等,之后需要将更改提交到数据库中。提交更改的代码就可以
    放到after_request钩子注册的函数中。

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

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

2.3 HTTP响应

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

2.3.1 响应报文

响应报文主要由协议版本、状态码(status code)、原因短语
(reason phrase)、响应首部和响应主体组成。以发向localhost:
5000/hello的请求为例,服务器生成的响应报文示意如表所示。
响应报文
响应报文的首部包含一些关于响应和服务器的信息,这些内容由
Flask生成,而我们在视图函数中返回的内容即为响应报文中的主体内
容。浏览器接收到响应后,会把返回的响应主体解析并显示在浏览器窗
口上。
常见HTTP状态码
当关闭调试模式时,即FLASK_ENV使用默认值production,如果程
序出错,Flask会自动返回500错误响应;而调试模式下则会显示调试信
息和错误堆栈。

2.3.2 在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'}
1. 重定向

在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():
	...
2. 错误响应

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()函数中传入状态码即可返回对应的错误响应,代码清单
2-3中的视图函数返回404错误响应。
代码清单2-3 http/app.py:返回404错误响应

from flask import Flask, abort
...
@app.route('/404')
def not_found():
	abort(404)

abort()函数前不需要使用return语句,但一旦abort()函数被调
用,abort()函数之后的代码将不会被执行。

2.3.3 响应格式

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

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类型,比如:

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

也可以直接设置首部字段,比如response.headers[‘Content-
Type’]=‘text/xml;charset=utf-8’。但操作mimetype属性更加方便,而且不
用设置字符集(charset)选项。
常用的数据格式有纯文本、HTML、XML和JSON,下面我们分别
对这几种数据进行简单的介绍和分析。为了对不同的数据类型进行对
比,我们将会用不同的数据类型来表示一个便签的内容:Jane写给Peter
的一个提醒。

1. JSON

使用jsonify函数可以将前面的例子
简化为这种形式:

from flask import jsonify
@app.route('/foo')
def foo():
	return jsonify(name='Grey Li', gender='male')

jsonify()函数接收多种形式的参数。你既可以传入普通参数,也
可以传入关键字参数。如果你想要更直观一点,也可以像使用
dumps()方法一样传入字典、列表或元组,比如:

from flask import jsonify
@app.route('/foo')
def foo():
	return jsonify({name: 'Grey Li', gender: 'male'})

另外,jsonify()函数默认生成200响应,你也可以通过附加状态
码来自定义响应类型,比如:

@app.route('/foo')
def foo():
	return jsonify(message='Error!'), 500

Flask在获取请求中的JSON数据上也有很方便的解决方案,具体可
以参考我们在Request对象小节介绍的request.get_json()方法和 request.json属性。

2.3.4 Cookie

HTTP是无状态(stateless)协议。也就是说,在一次请求响应结束
后,服务器不会留下任何关于对方状态的信息。但是对于某些Web程序
来说,客户端的某些信息又必须被记住,比如用户的登录状态,这样才
可以根据用户的状态来返回不同的响应。为了解决这类问题,就有了
Cookie技术。Cookie技术通过在请求和响应报文中添加Cookie数据来保
存客户端的状态信息。
在Flask中,如果想要在响应中添加一个cookie,最方便的方法是使
用Response类提供的set_cookie()方法。要使用这个方法,我们需要先
使用make_response()方法手动生成一个响应对象,传入响应主体作为
参数。这个响应对象默认实例化内置的Response类。表2-10是内置的
Response类常用的属性和方法。
Response类的常用属性和方法
set_cookie()方法的参数
set_cookie()方法的参数
set_cookie视图用来设置cookie,它会将URL中的name变量的值设置
到名为name的cookie里,如代码所示。

from flask import Flask, make_response
...
@app.route('/set/<name>')
def set_cookie(name):
	response = make_response(redirect(url_for('hello')))
	response.set_cookie('name', name)
	return response

在这个make_response()函数中,我们传入的是使用redirect()
函数生成的重定向响应。set_cookie视图会在生成的响应报文首部中创
建一个Set-Cookie字段,即“Set-Cookie:name=Grey;Path=/”。
现在我们查看浏览器中的Cookie,就会看到多了一块名为name的
cookie,其值为我们设置的“Grey”,因为过期时间使用
默认值,所以会在浏览会话结束时(关闭浏览器)过期。
在Flask中,Cookie可以通过请求对象的cookies属性读取。在修改后
的hello视图中,如果没有从查询参数中获取到name的值,就从Cookie中
寻找:

from flask import Flask, request
@app.route('/')
@app.route('/hello')
def hello():
	name = request.args.get('name')
	if name is None:
		name = request.cookies.get('name', 'Human') # 从Cookie中获取name值
	return '<h1>Hello, %s</h1>' % name

2.3.5 sesion

Cookie在Web程序中发挥了很大的作用,其中最重要的功能是存储
用户的认证信息。我们先来看看基于浏览器的用户认证是如何实现的。
当我们使用浏览器登录某个社交网站时,会在登录表单中填写用户名和
密码,单击登录按钮后,这会向服务器发送一个包含认证数据的请求。
服务器接收请求后会查找对应的账户,然后验证密码是否匹配,如果匹
配,就在返回的响应中设置一个cookie,比如,“login_user:greyli”。

响应被浏览器接收后,cookie会被保存在浏览器中。当用户再次向
这个服务器发送请求时,根据请求附带的Cookie字段中的内容,服务器
上的程序就可以判断用户的认证状态,并识别出用户。

但是这会带来一个问题,在浏览器中手动添加和修改Cookie是很容
易的事,仅仅通过浏览器插件就可以实现。所以,如果直接把认证信息
以明文的方式存储在Cookie里,那么恶意用户就可以通过伪造cookie的
内容来获得对网站的权限,冒用别人的账户。为了避免这个问题,我们
需要对敏感的Cookie内容进行加密。方便的是,Flask提供了session对象
用来将Cookie数据加密储存。

  1. 设置程序密钥

    app.secret_key = 'secret string'
    import os
    # ...
    app.secret_key = os.getenv('SECRET_KEY', 'secret string')
    
  2. 模拟用户认证

    from flask import redirect, session, url_for
    @app.route('/login')
    def login():
    	 session['logged_in'] = True # 写入session
    	return redirect(url_for('hello'))
    

    当支持用户登录后,我们就可以根据用户的认证状态分别显示不同
    的内容。在login视图的最后,我们将程序重定向到hello视图,下面是修
    改后的hello视图:

    from flask import request, session
    @app.route('/')
    @app.route('/hello')
    def hello():
    	name = request.args.get('name')
    	if name is None:
    		name = request.cookies.get('name', 'Human')
    		response = '<h1>Hello, %s!</h1>' % name
    	# 根据用户认证状态返回不同的内容
    	 if 'logged_in' in session:
    		response += '[Authenticated]'
    	else:
    		response += '[Not Authenticated]'
    	return response
    

安利一门Python超级好课!
扫码下单输优惠码【csdnfxzs】再减5元,比官网还便宜!
超级好课

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值