HTTP请求
一个标准的HTTP请求: http://reedo.cn/hello?name=jiuri。 ‘http://‘ 为协议字符串,指定要使用的协议 。 reedo.cn’为服务器地址、域名。 ‘/hello?name=jiuri’为要获取的资源路径、path。
- url后面?name=jiuri 部分是查询字符串,url中查询字符串用来向指定的资源传递参数。 查询字符串从问号?开始,以键值对形式写出,多个键值对之间用&分割。
报文
访问url时随之产生的向服务器发送的请求,请求发送的数据与返回请求发送的数据称为”报文message”。 请求时发送的数据成为请求报文(request message),服务器返回的数据成为返回报文(response message)。
请求报文
请求报文包括请求的方法、URL、协议版本、首部字段(header)、内容实体。
- 报文由报文首部(请求行:方法、URL、协议 和各种首部字段)和报文主体组成,两者由空行隔开。
- HTTP通过方法来区分不同请求类型,包括GET、POST等
- 报文首部包含了请求的各种信息和设置如客户端类型、缓存、语言等。
- 请求报文主体一般为空, 如果url有查询字段或提交了表单,那么报文主体会是查询字符串或表单数据。
Request对象
Flask的request对象封装了从客户端发来的请求报文,我们可以从request对象中获得请求报文中的所有数据。
通过request的属性获得url的各部分
属性 | |
---|---|
path | ‘/hello’ |
full_path | ‘hello?name=jiuri’ |
host | ‘reedo.cn’ |
host_url | |
base_url | |
url | |
url_root |
通过request提供的方法属性获得其他信息
属性/方法 | 说明 |
---|---|
args | werkzeug的ImmutableMultiDict对象,存储解析后的查询字符串,可通过字典方式获取键值。 |
blueprint | |
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, perse_from_data=False) | 获取请求中的数据,默认读取的为字节字符串。 |
get_json(self, force=False, silent=False, cache=True) | |
method | 请求的HTTP方法 |
ps: Werkzeug的MutliDict类是字典的子类,主要实现了一个键对应多个值的情况,比如一个文件上传字段可能接收多个文件,这时可通过getlist()方法获取文件的对象列表。 ImmutableMultiDic类继承了MultiDict类,但其值不可更改。
- 从request对象的类型为MultiDict或ImmutableMultiDic的属性(如files、form、args)中直接使用健作为索引获取数据(如request、args[‘name’])时,如果没有对应的键会返回400错误, 所以应该使用get()方法取数,如果没有对应的值返回None,get()的第二个参数可以设置默认值。 如request.args.get(‘name’, ‘Bob’)。
在Flask中处理请求
url是指向网络上的资源的地址,flask需要让url匹配到对应的视图函数,视图函数返回url对应的资源。
路由匹配
- 程序实例存储了一个路由表(app.url_map)来进行请求分发到对应的视图函数。 当产生请求后flask根据URL(path部分)尝试与这个表中的视图函数匹配,匹配成功则调用关联的视图函数。如果没有找到则返回404错误,
- 使用flask routes命令可以查看程序中定义的所有路由(这个列表与app.url_map解析得到)。 列表包括端点、HTTP方法(Methods,如GET、POST)、URL规则。
设置监听的HTTP方法
- 在app.route() 装饰器中使用methods参数传入一个包含HTTP方法的可迭代对象,可以设置监听的方法, 如果不设置默认监听GET方法。 通过定义方法列表可以实现同一个URL规则定义多个视图函数。
- 当请求的方法不符合要求时,会返回405响应错误。
@app.route('/hello', methods=['GET','POST'])
def function():
...
URL变量处理
url中包含变量, 变量部分默认为字符串,falsk提供了一些转换器可以在URL规则里使用。
- 转化器通过特定的规则指定,即:<转换器:变量名>。 把变量转换成对应的类型。 除了转换变量类型,还可以URL匹配,如果url中变量不是对应的类型则则返回404错误相应。
- Flask内置的URL变量转换器包括:String(默认值、不包含斜线的字符串)、Int(整形)、float、path(包含斜线的字符串,static路由的url规则中的filename变量就使用这个转换器)、any(匹配一系列给定值中的一个元素)、uuid。
#int使用
@app.route('/goback/<int:year>')
#any使用
@app.route('/color/<any(blue,red,white):color>')
或
colors = ['blue', 'white', 'red']
@app.route('color/<any(%s):color>') % str(colors)[1:-1]
请求钩子Hook
有时我们需要对请求进行预处理(preprocessing)和后处理(postprocesstin),这时可以使用Falsk提供的一些请求钩子(Hook), 可以用来注册请求处理的不同阶段执行的处理函数(或称回调函数Callback)
- 这些请求钩子使用装饰器实现,通过实例app调用。 当对一个函数附加了钩子的装饰器就会将这个函数处理成对应的处理函数, 在对应的请求情况下触发对应的函数。
- 每个钩子可以注册任意多个处理函数。
常用的请求钩子:
钩子 | 说明 |
---|---|
before_first_request | 注册一个函数,在处理第一个请求前运行。 # 一般用于第一次系统运行时初始化 |
before_request | 每次请求前运行。 # 记录最后在线时间, 是否登陆判断等。 |
after_request | 无异常情况下每个请求结束后运行 # 更改提交到数据库等。 或单元测试中请求之后关闭连接 |
teardown_request | 无论是否有异常,每次请求后运行,如果有异常则传入异常对象作为参数到注册的函数中 |
after_this_request | 在视图函数内注册一个函数,在这个请求结束后运行。 |
@app.before_request
def do_something():
pass #这些代码在每次请求前执行,如判断是否登陆
HTTP响应
客户端发出请求出发视图函数,获取返回值会作为响应主体,最后完成完整的响应,即响应报文。
响应报文
响应报文主要由协议版本、状态码(status code)、原因短语、响应首部和响应主体组成。
- 响应报文首部包含响应和服务器的信息,由Flask生成
- 响应报文主体内容为视图函数返回的内容。
在Flask中生成响应
- 响应在Flask中用Response对象表示, 响应报文中大部分内容由服务器处理, 多数情况我们只需处理返回主体内容,即视图函数返回值;
- 视图函数最多可返回由三个元素组成的元组:响应主体、状态码、首部字段。 多书情况除了响应主体,其他内容使用默认即可。
@app.route('/hello')
def hello():
...
return 'hello, flask' # 只返回响应主体,状态码默认为200
或
return 'hello , flask', 201 #指定不同状态码
或
return 'hello, flask', 302, {'location':'http://reedo.cn'} #302重定向,设置首部字段中重定向地址。
重定向
输入A地址,页面加载后时B地址,这种行为成为重定向(Redirect)
- 除了如上手动生成302响应,可以直接使用Flask提供的redirect()函数来生成重定向响应,重定向目标url为redirect()的第一个参数。
from flask import Flask, redirect
@app.route('/hello')
def hello():
...
return redirect('http://reedo.cn')
或
return redirect(url_for('index')) #如果需要重定向到其他视图,可以使用url_for()生成地址.
响应错误
- HTPP错误对应的异常类在Werkzeug的werkzeug.exceptions模块中定义,抛出这些异常即可返回对应的错误响应。 如果想手动返回错误响应,可以使用Flask提供的abort()函数, 在abort()函数中传入状态码即可返回对应的错误响应。
from flask import Flask, abort
@app.route('/404')
def not_found():
abort(404) #abort前不需要return, 一旦abort函数被调用,abort()之后的代码将不被执行。
响应格式
- 在HTTP响应中数据可以通过多种格式传输如:纯文本、HTML、XML、JSON 。 不同的响应数据格式需要设置不同的MIME类型,MIME类型在首部的Content-Type字段中定义。
- 多数情况下使用HTML,未设置是Flask默认也是html。 如果想使用其他的MIME类型,可通过Flask的make_response()方法生成相应对象,传入相应主体作为参数,然后使用相应对象的minetype属性设置MIME类型。
ps:MIMELEl类型(又称media type或content type)是一种用来标识文件类型的机制,它与文件扩展名相对应,可以让客户端区分不同的内容类型并执行不同的操作。, 一般的格式为“类型名/子类型名”,其中子类型名一般为文件扩展名。如HTML的MIME类型为’text/html’,png图片的MIME类型为”image/png”。
from flaks import make_response
@app.route('/foo')
def foo():
response = make_response('hello, flask')
response.mimetype='text/plain'
或直接设置首部字段,不过比mimetype属性麻烦
response.headers['Content-Type'] = 'text/plain;charset=utf-8'
return response
纯文本
MIME类型为:text/plain
HTML
MIME类型为:text/html
XML
XML是Extensible Markup Language(可扩展标记语言),被设计用来交换数据,弥补html的不足, XML中标签只用来定义数据,一般作为AJAX请求的响应格式或WEP API的响应格式
MIME类型为:application/xml
<?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Peter</to>
<from>Jane</from>
<heading>Reminder</heading>
<body>Don't forget the party!</body>
</note>
JSON
JSON指JavaScript Object Notation(javascript对象表示法),是一种流行的轻量数据交换格式,弥补了XML的体格打处理解析慢的不足;JOSN更轻量、简洁、易于阅读并且能和web默认的客户端语言javascript兼容。
- JSON结构基于“键值对的集合”和“有序的值列表”。
MIME类型为:application/json
{
"note":{
"to":"Peter",
"from":"Jane",
"heading":"Reminder",
"body":"don't forget the party"
}
}
- Flask中通过引用Python标准库中的json模块为程序提供了JSON支持, 可以支持从Flask中导入json对象,然后使用dump()方法将字典、列表或元组序列化(serialize)为JSON字符串,然后修改MIME类型,即可返回JSON响应。 ```python from flask import Flask, make_response, json
@app.route(‘/foo’) def foo(): data = { ‘name’:’Grely Li’, ‘gender’:’male’ } response = make_response(json.dump(data)) response.mimetype=’application/json’ return response
- Flask还提供了更方便的jsonify()函数,仅需要传入数据或参数,即可完成上述所有的序列化、转换为json字符串作为响应主体、生成响应对象并且设置正确的MIME类型。
- jsonify()函数可以接收多种形式的参数,包括普通参数、关键字参数,或者字典、列表、元组等。
- jsonify()默认生成200响应, 可以通过附加状态码来自定义响应类型。
```python
from flask import jsonify
@app.route('/foo')
def foo():
data = {
'name':'Grely Li',
'gender':'male'
}
return jsonfy(data)
Cookie
HTPP是无状态协议,也就是说再一个请求响应结束后服务器不会留下任何关于对方状态的信息,但是有些时候客户端的某些信息又必须被记住如登陆状态。为了解决这一问题就有了Cookie技术。 Cookie技术通过再请求和响应报文中添加Cookie数据来保存客户端的状态信息。
- Cookie是服务器为了存储某些数据(用用户信息)而保存在浏览器上的小型文本数据。 浏览器会在一定时间内保存它并在下次向服务器发送请求时附带这些数据,
- Cookie的内容存储在请求首部的Cookie字段中。
- 在Flask中, Cookie可以通过请求对象的cookies属性读取。
- 一般用来进行会员管理(如登陆状态)、保存用户个性信息(如语言、视频播放位置、网站主题等)以及记录和手机用户浏览数据。
添加Cookie
- 在Flask中,可以使用Response类提供的set_cookie()方法来在响应中添加cookie信息。 使用这个方法需要先用make_response()方法手动生成一个响应对象,传入响应主体作为参数,这个响应对象默认实例化内置的Response类。
Response类常用的属性和方法
方法/属性 | 说明 |
---|---|
headers | 一个Werlzeug的Headers对象,表示响应首部,可以向字典一样操作。 |
status | 状态码、文本类型 |
status_code | 状态码、整形 |
mimetype | MIME 类型 |
set_cookie() | 用来设置一个cookie |
此外Response类还同样拥有和Request类相同的get_json(), is_json()方法即json属性。
set_cookie()方法支持多个参数来设置Cookie的选项
属性 | 说明 |
---|---|
key | cookie的键(名称) |
value | cookie的值 |
max_age | cookie被保存的时间,单位为秒,默认在用户会话结束(即关闭浏览器)时过期 |
expires | 具体的过期时间,一个datetime对象或UNIX时间戳 |
path | 限定cookie只在给定的路径可用,默认整个域名 |
domain | 设置cookie可用的域名 |
secure | 如果设置为True,只有通过https才可以使用 |
httponly | 如果设置为True,禁止客户端JavaScript获取cookie |
from flask import Flask, make_response
@app.route('/set/<name>')
def set_cookie(name):
response = make_response(redirect(url_for('index')))
response.set_cookie('name':name) #会在生成的响应报文首部创建一个Set-Cookie:name=Grey; Path=/
return response
from flask import Flask, request
@app.route('/hello')
def hello():
name = request.args.get('name')
if name is None:
name = request.cookies.get('name':'Nokonw')
return 'hello %s' % name
Session
- session可以对cookie信息加密, 防止恶意串改
- session指用户会话(user session),又成为对话(dialogue).
- 在Flask中,session对象用来加密Cookie,默认情况它会把数据存储在浏览器上一个名为session的cookie里。
设置程序密钥
- session使用密钥对数据进行签名以及加密数据。 因此需要先设置一个密钥,密钥是一个具有一定复杂度和随机性的字符串。
- Flask中程序的密钥可以通过Flask.secret_key属性或配置变量SECRET_KEY设置。
app.secret_key = 'secret string'
或更安全的做法是把密钥写进系统环境变量 或 保存在.env文件中
SECRET_KEY= 'secret string'
#然后在程序脚本中使用os模块的getenv()获取
import os
app.secret_key = os.getenv('SECRET_KEY', 'secret string') #第二个参数非必须,没有获取到环境变量时使用。
模拟用户认证
from flask import redirect, session, url_for
@app.route('/login')
def login():
session['login_in'] = True #写入session
return redirect(url_for('index'))
- session可以像字典一样操作
- 当使用session对象添加cookie时,数据会使用程序的密钥对其进行签名。加密后的数据存储在名为session的cookie中。 session中的值可以看到但无法修改。因为session中的内容使用密钥进行签名,一旦数据被改,签名的值也会变化,这样在验证时就会失败。
验证
from flask import session, abort
@app.route('/hello')
def hello():
if 'login_in' not in session:
abort(403)
return "Welcome in!"
退出
from flask import session, redirect, url_for
@app.route('/logout')
def logout():
if 'login_in' in session:
session.pop('login_in')
return redirect('url_for('index')')
Flask上下文
上下文即程序运行环境的环境,包含了各种信息。 上下文包括程序上下文(application context)和请求上下文(request context)
上下文全局变量
- Flask中当每个请求产生后会自动激活当前请求的上下文,request被临时设置为全局可访问, 这样在请求处理的视图函数就可以获取当当前请求的信息。 而当每个请求结束后Flask就会销毁对应的请求上下文。 ps:这里的全局其实不是正真意义上的全局, 而是当前线程和请求中全局可访问。
Flask提供4个上下文全局变量; ps:这4个变量都是代理对象,即指向真实对象的代理。 通过全局变量可以实现??
变量名 | 上下文类型 | 说明 |
---|---|---|
current_app | 程序上下文 | 指向处理请求的当前程序实例 |
g | 程序上下文 | 用于存储全局数据,只在当前成球中可用,每次请求都会重设。 |
requesrt | 请求上下文 | 封装客户端发出的请求报文数据 |
session | 请求上下文 | 用于记住请求之间的数据,通过签名的cookie实现。 |
激活上下文
自动激活上下文情况
Flask自动计划程序上下文:以下情况:
- 使用flask run命令启动程序时
- 使用旧的app.run()方法启动程序时
- 执行使用@app.cli.command()装饰器注册的flask命令时
- 使用flask shell命令启动python shell时。
当请求进入时,flask会自动激活请求上下文,这时request、session变量可用使用。
手动激活上下文
只有激活上下文后对应的变量可才使用。 如未激活情况下使用这些变量可用使用手动激活上下文
1 程序上下文对象使用app.app_context()获取,使用with语句执行上下文操作。
2 程序上下文显示的使用push()方法激活上下文,使用po()方法销毁上下文
3 请求上下文通过test_request_context()方法临时创建
4 请求上下文同样可用使用push()和pop()方法显示的激活和销毁上下文。
上下文钩子
teardown_appcontext钩子:使用它注册的函数会在程序/请求上下文被销毁时调用。 如在每个请求接收后断开数据库连接:
@app.teardown_appcontext
def teardown_db(exception): #使用app.teardoen_appcontext()装饰器注册的回调函数需要接收异常对象作为参数;请求正常时参数值为None,函数返回值被忽略。
...
db.close()
HTTP进阶实践
重定向回上个页面
获取上一个页面URL
1 HTTP referer
- HTTP referer是用来记录请求发源地址的HTTP首部字段,即访问来源。
- 当用户在某个站点点击连接,浏览器向新连接所在的服务器发起请求,请求数据中包含的HTTP_REFERER字段记录了用户所在原站点URL。 在falsk中referer值可用通过请求对象的referer属性获取即request.referer
- 有些时候如用户输入地址或浏览器清除, referer字段可能为空值
def old_url():
return redirect(request.referer)
或
return redirect(request.referer or url_for('hello'))
2 查询参数
除了从referer参数获取,另一种更常见的方式是在url中手动加入包含当前页面url的查询参数,查询参数一般命名为next。 然后在处理视图内获取路径然后重新定向到对应的路径
from flask import request
@app.route('/foo')
def foo():
return '<a href='%s'>do some thing</a>' % url_for('do_someting', next=request.full_path)
# 用request.full_path获取当前页面完整路径然后加到next的查询参数传入到下一个页面视图。
@app.route('/do')
def do_something():
...
return redirect(request.args.get('next', url_for('index'))) #查询next参数并跳转到该地址
更完整的回跳逻辑
def redirect_back(default='index', **kwargs): #创建一个通用的函数
for target in request.args.get('next'), request.referer:
return redirect(target)
return redirect(url_for(default, **kwargs))
@app.route('/do_something')
def do_something():
...
return redirect_back() #在需要的地方调用回调函数即可
对URL安全进行验证
鉴于referer和next值容易被篡改, 因此需要对这些值进行验证,否则回形式开放重定向(Open Redirect)漏洞
- 确保url安全的关键就是判断url是否属于程序内部
from urllib.parse import urlparse, urljoin # python2需要从urllib导入
from flask import request
def is_safe_url(target): #函数接收目标地址作为参数
ref_url = urlparse(request.host_url) 从request.url获取主机地址url,使用urlparse解析地址
test_url = urlparse(urljoin(request.host_url, terget)) #使用urljoin函数将目标地址转为绝对url。使用urlparse解析地址。
return ref_url.netloc == test_url.netloc and test_url.scheme in ('http', 'https') #对两个主机地址进行验证
使用AJAX技术发送异步请求
AJAX发送异步请求可用局部更新页面内容,不用页面全部刷新,提升体验减少服务器资源浪费。
AJAX
AJAX是指异步Javascript和XML(Asynchronous Javascript and XML), 不是编程语言或通信协议,而是一系列技术的组合体。
- AJAX基于XMLHttpRequest可用让我们在不重载页面的情况下和服务器进行数据交换。 ps:XMLHttpRequest不仅支持http协议,还支持FILE、FTP协议。
- JavaScript和DOM,可用在接收到响应数据后局部更新页面
- XML是使用的数据交互格式。 ps:也可以用纯文本、HTML或JSON
使用jQuery发送Ajax请求
可以使用原生XMLHttpRequert实现AJAX,也可以使用如jQuery或其他JavaScript框架。
- jQuery是流程的JavaScript库,它包装了Javascript,可以更简单的方式编写JavaScript。并且提供了多个方法可以很方便的实现AJAX操作。
- jQUery处理了不同浏览器兼容问题。
下面使用jQuery的ajax()函数发送AJAX请求, ajax()函数参数如下:
参数 | 参数值类型及默认值 | 说明 |
---|---|---|
url | 字符串,默认为当前页地址 | 请求的地址 |
type | 字符串,默认为GET | 请求的方式即HTTP方法,如GET、POST、DELETE等 |
data | 字符串,无默认值 | 发送到服务器的数据,会被jQuery自动转换为查询字符串。 |
data Type | 字符串,默认由Jquery根据返回值自动判断 | 期待服务器返回的数据类型,如xml、html、script、json、jsonp、text |
contentType | 字符串,默认为’application/x-www-form-urlencoded; charset=UTF-8’ | 发送请求时使用的内容类型,即请求首部的Content-Type字段内容 |
complate | 函数,无默认 | 请求完成后调用的回调函数 |
success | 函数,无默认 | 请求成功后的调用的回调函数 |
error | 函数,无默认 | 请求失败后的调用的回调函数 |
返回请求的数据
对于处理ajax请求的视图函数,不会返回完整的html响应,一般回返回局部数据,类型如下
1 纯文本或局部HTML模板
- 纯文本用于替换页面中的文本值
- 局部HTML可以直接插入到页面中如评论列表
@app.route('/comments/<int:post_id>')
def get_comment(post_id):
...
return render_template('comments.html')
2 JSON数据
- json数据可以在js中直接操作
- jQuery中的ajax()方法的succes回调中,响应主体中的json字符串会被解析为json对象,可以直接获取并进行操作。
@app.route('/hello')
def hello():
return jsonify(username = 'Bob', sex='female')
- jQuery中的ajax()方法的succes回调中,响应主体中的json字符串会被解析为json对象,可以直接获取并进行操作。
3 空值
- 有时候接受到ajxa请求后并不需要返回内容如删除操作。 此时可以返回空值并将状态码指定为204(表示无内容)
@app.route('post/delete/<int:post_id>')
def delete_post(post_id):
...
return '' 204
异步加载长文章示例
from jinja2.utils import generate_lorem_ipsum
@app.route('/post')
def show_post():
post_body = generate_lorem_ipsum(n=2) #generate_lorem_ipsum()函数,生成随机字符组成的虚拟文章,n指定段落的数量、默认5
return '''
<h1>A long post</h1>
<div class="body">%s</div>
<button id="load">加载更多</button>
<script src=".......jquery-3.3.1.min.js"></script> #从cdn加载jQuery资源
<script type="text/javascript"> #开始写js代码
$(function() { #1 $是jQuery的简写,用来调用jQuery方法; 2 $(function(){...})函数,用来在页面DOM加载完成后执行代码,是$(document).ready(function(){...}) 的简写
$("$load").click(function() { # 1 $('#load') 成为选择器,括号中传入目标id、class等属性来定位到目标元素,将其创建为jQuery对象.; 2 为对象附加.click(function(){...}),为按钮添加一个点击事件处理函数。
$.ajax({ # $.ajax() 方法
url:'more',
type:'get',
success:function(data){ # ajxa成功回调函数
$('.body').append(data);
}
})
})
})
</script> % post_body
'''
@app.route('/more')
def more():
return generate_loem_ipsum(=1)
HTTP服务器端推送
通信模式包括客户端拉取(client pul)和服务器端主动推送(server push)
- 客户端拉取:客户端发出请求,服务器端才会返回响应。
- 有时候需要服务器主动推送如商品库存信息、文档协作、私信等。
实现服务器端推送的一系列技术被合成为HTTP Server Push(HTTP 服务器端推送),目前常用的技术
名称 | 说明 |
---|---|
传统轮询 | 特定时间间隔内,客户端使用Ajax技术不断向服务器发起HTTP请求,然后获取新的数据并更新页面 |
长轮询 | 和传统轮询类似,但如果服务器没有返回数据,就保持连接一直开启,直到有数据时才返回。取回数据后再次发送另一个请求 |
Server-Sent Events (SSE) | SSE通过html5中的EventSource API实现,SSE会在客户端和服务器端建立一个单向的通道,客户端监听来自服务器端的数据,服务器端可以在任意时间发送数据。 两者建立类似订阅/发布的通信模式。 |
- 轮询(polling)这类使用Ajax技术模拟服务器推送的方法实现比较简单,但是通常回增加服务器负担。而且会让客户端设备更耗电。
- SSE效率更高,除IE/Edge。SSE支持所有主流浏览器,但浏览器通常会限制标签页的连接数量。
- 除了推送技术,HTML5的API还包含一个WebSocket协议,它是基于TCP协议的全双工通信协议,它实时性更强,而且可以实现双向通信。