请求

Introduction

使用python的包requests进行http/https的处理. 其中, 建立在TCP/IP协议之上的HTTP需要符合如下的规范:

1
2
3
4
<method> <request-URI> <http-verison>
<http request headers>

<body>

注意, 一般而言request-URI中存储的为URI,而并非URL, 可以通过headers中的HOST头来组成完成的URL请求头中的各类参数告知server不同的信息:

  • 请求的信息类型, 例如表单application/x-www-form-urlencoded
  • 请求的长度
  • 客户端能够接受的响应类型

具体的各个字段的含义会在下文逐一说明, 对于一些特殊的设计较多知识的分类会以单章的形式呈现.

content-length

在requests的POST请求中, 传入data的值如果是unicode, 则会出现字符串长度被截断的现在发生, 因为在代码requests.models.prepare_body中, 使用len进行长度计算, 此时会出现问题.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# coding:utf8
import requests


headers = {
'Content-Type': 'text/plain;charset=UTF-8'
}


page_url = 'https://baidu.com/post'
sjhm = '13958236863'
zjhm = '330381198411193319'
xm = u'章'
dlmm = 'Csz193319'
dlm = 'Csz193319'
verify_code = '0943'
payload = '&zjhm=%s&xm=%s&certify_type=0&sjhm=%s&dlmm=%s&yzm=%s&account=%s' % (
zjhm, xm, sjhm, dlmm, verify_code, dlm)

print type(payload)
print len(payload)
# 进入代码: requests/models/prepare_body函数中, 使用len进行长度计算
rsp = requests.post(page_url, data=payload, headers=headers, verify=False)
if rsp:
print rsp
else:
print 'Failed'

长度计算实例:

1
2
len('狂想') == 6
len(u'狂想') == 2

Content Type

Intro

对于POST方法, 在传输数据时需要指明格式以便server能够正确的解析请求数据. 这里主要介绍集中常见的Content-Type并给出响应的python例子进行说明, 对于不同的格式, 请求体中的内容也是按照不同的格式进行编码.

注意, 一般而言, 服务端语言如PHP, Python, Java, 关联的框架在解析HTTP时会默认根据请求头Content-Type解析数据并存储, 开发者仅仅按照约定或者Request对象中提取需要的数据即可.
当然, 返回的特定格式的Response, 则需要在Response Header中指明, 以便Browser能够识别解析, 这是一个反向的过程.

表单

application/x-www-form-urlencoded, 最常见的表单提交方式, 浏览器原生的<form>表单格式, 在JS中使用Ajax方式提交数据时默认就使用application/x-www-form-urlencoded;charset=utf-8来进行提交.

GET方法的表单提交包:

1
2
3
4
5
6
7
8
9
GET /api/v2/monitor/upload?a=1&b=kuang HTTP/1.1
Content-Type: application/x-www-form-urlencoded
cache-control: no-cache
Postman-Token: 2f9bfff5-8031-44ab-b538-afeab5788cc1
User-Agent: PostmanRuntime/7.2.0
Accept: */*
Host: 0.0.0.0:5000
accept-encoding: gzip, deflate
Connection: keep-alive

此时在服务端需要根据如下代码进行解析

1
2
3
4
5
6
7
8
9
10
11
from flask import request
params = request.args
if params:
for key, value in params.items():
print('Key:{} Value:{}'.format(key, value))
````

输出如下:
```txt
Key:a Value:1
Key:b Value:kuang

POST方法提交表单:

1
2
3
4
5
6
7
8
9
10
11
12
POST /api/v2/monitor/upload HTTP/1.1
Content-Type: application/x-www-form-urlencoded
cache-control: no-cache
Postman-Token: 0a3cad9c-4168-40f8-baca-93517605b04a
User-Agent: PostmanRuntime/7.2.0
Accept: */*
Host: 0.0.0.0:5000
accept-encoding: gzip, deflate
content-length: 19
Connection: keep-alive

a=1&b=kuang&c=value&c=value2

此时的服务器解析代码:

1
2
3
4
5
from urlparse import parse_qs
data = request.get_data()
if data:
print(parse_qs(data))
return ResponseUtil.response(data={})

输出如下, 注意, 此时的输出有一点特殊

1
2
# 为何使用列表, 就是为了防止有相同key存在的情况
{'a': ['1'], 'c': ['value', 'value2'], 'b': ['kuang']}

表单文件

最常见的表单文件提交方式, 浏览器使用`
`提交表单时, 需要指明`enctype`字段为`multipart/form-data`, 以便告知服务器提交的是一个二进制形式的文件.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

对于如下POST请求:

```txt
POST /api/v2/monitor/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=--------------------------074797537532569367946334
cache-control: no-cache
Postman-Token: d01932ed-994d-495d-b548-8549602d6a5d
User-Agent: PostmanRuntime/7.2.0
Accept: */*
Host: 0.0.0.0:5000
accept-encoding: gzip, deflate
content-length: 75115
Connection: keep-alive

----------------------------074797537532569367946334
Content-Disposition: form-data; name="file1"; filename="0马来头像2.jpg"
Content-Type: image/jpeg

此处是超级二进制字节乱码

----------------------------074797537532569367946334
Content-Disposition: form-data; name="text2"

text msg
----------------------------074797537532569367946334
Content-Disposition: form-data; name="file3"; filename="test.txt"
Content-Type: text/plain

I am a file

----------------------------074797537532569367946334--

下面先讲解一下表单文件的请求体中各个关键部分.

  • boundary: 用于分割不同的文件之间, 这个非常重要
  • Content-Type: 注意不是Header的Content-Type, 如果是字符串则无需指定
  • Content-Disposition: 指明该部分上传文件的名称, 键值.

此时的服务器解析代码:

1
2
3
4
5
6
# 提取非字符串类型
for key, value in request.files.items():
print('Key:', key, ' Value:', value)
# 字符串类型
for key, value in request.form.items():
print('Key:', key, ' Value:', value)

输出如下:

1
2
3
Key: file3  Value: <FileStorage: u'test.txt' ('text/plain')>
Key: file1 Value: <FileStorage: u'0\u9a6c\u6765\u5934\u50cf2.jpg' ('image/jpeg')>
Key: text2 Value: text msg

json

application/json, 这是目前Restful API中最通用的方式了, 基于前后端分离, 以JSON格式返回给前端,无缝的利用JS来发送和解析数据. JSON能够简单的表达一个复杂的结构化数据(列表, 字典等).

请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
POST /api/v2/monitor/upload HTTP/1.1
Content-Type: application/json
cache-control: no-cache
Postman-Token: dc72001d-0f3a-48c1-9978-0f0534652a22
User-Agent: PostmanRuntime/7.2.0
Accept: */*
Host: 0.0.0.0:5000
accept-encoding: gzip, deflate
content-length: 289
Connection: keep-alive

{
"ip": "127.0.0.1",
"monitors": [
{
"type": "error",
"occur_time": "2018-07-17 08:00:00",
"country": "ds160",
"task_id": "Taskid123456"
},
{
"type": "error",
"occur_time": "2018-07-19 08:00:00",
"country": "singapore",
"task_id": "Taskid123456898"
}
]
}

此时的服务器解析代码:

1
2
3
4
import json
data = request.get_data()
if data:
print(json.loads(data))

输出如下:

1
{u'ip': u'127.0.0.1', u'monitors': [{u'country': u'ds160', u'type': u'error', u'task_id': u'Taskid123456', u'occur_time': u'2018-07-17 08:00:00'}, {u'country': u'singapore', u'type': u'error', u'task_id': u'Taskid123456898', u'occur_time': u'2018-07-19 08:00:00'}]}

xml

application/xml, 一种通用的XML包传输格式, 常常见于JAVA生态中, 相比json, 如果不是JAVA生态, 建议使用application/json替代.

请求包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /api/v2/monitor/upload HTTP/1.1
Content-Type: application/xml
cache-control: no-cache
Postman-Token: 1c9fe700-74f9-4565-b1cc-1dbaef618352
User-Agent: PostmanRuntime/7.2.0
Accept: */*
Host: 0.0.0.0:5000
accept-encoding: gzip, deflate
content-length: 287
Connection: keep-alive

<?xml version="1.0" encoding="UTF-8"?>
<Request service="RouteValidService" lang="zh-CN">
<Body>
<WaybillRoute id="135179" mailno="444001877884" orderid="QIAO-20180104026" acceptTime="2018-07-26 09:39:40" acceptAddress="深圳市" remark="备注" opCode="50"/>
</Body>
</Request>

服务器解析代码:

1
2
3
4
import xml.dom.minidom
resp_xml = xml.dom.minidom.parseString(request.get_data())
request_node = resp_xml.getElementsByTagName('Request')[0]
print(request_node)

输出如下: <DOM Element: Request at 0x102a32b90>

另外常见的XML请求格式还有: text/xml, 例如在XML-RPC作为调用方式, 传递函数方法以及参数

1
2
3
4
5
6
7
8
9
10
11
12
POST http://www.example.com HTTP/1.1 
Content-Type: text/xml

<?xml version="1.0"?>
<methodCall>
<methodName>examples.getStateName</methodName>
<params>
<param>
<value><i4>41</i4></value>
</param>
</params>
</methodCall>

缓存

操作

  1. 动作F5/ctrl + R, cmd+R: 刷新页面, 并在请求头加上cache-control: max-age=0, 强行发起缓存验证, 但保留If-Modified-Since/If-Non-Match 请求头, 页面其他相关联的请求(js, css, 图片等)不做任何特殊处理.
1
2
3
Cache-Control: max-age=0
If-Modified-Since: Mon, 27 Feb 2023 02:56:33 GMT
If-None-Match: "63fc1be1-6b2"

实际上此时js, css等资源也会请求一次, 不过请求头不会携带cache-control: max-age=0, 响应状态码一般为304, 实际并未返回任何数据(正常一个文件2.1KB, 304返回的响应数据只有222B).

  1. 动作CTRL + SHIFT + R, CMD+SHIFT+R: 强制刷新, 在请求头加上cache-control: no-cache; pragma: no-cache, 同时去掉If-Modified-Since/If-Non-Match请求头, 其中no-cache表示本地缓存失效需要重新进行验证, 此时所有资源的请求都会一视同仁.

头部说明

  1. 强缓存: Cache-Control, Expires, 其中Expires返回一个固定的过期时间戳, 它有如下缺点:
  • 服务器需要频繁的更改过期时间戳, 若是突然忘了更新过期时间戳, 则会造成浪涌或雪崩现象
  • 过期时间戳受到本地时间的影响, 实际上cacheControl中的maxAge也会收到本地时间影响, 但其通过频繁的更新解决了该问题

Cache-Control中的maxAge解决了Expires上述的问题, 其可选项说明如下:

  • no-cacheno-store: 前者会缓存资源, 但是每次请求都需要重新协商验证, 后者不使用缓存, 注意区别
  • public: 客户端和服务器均可缓存
  • private: 客户端可缓存
  • max-age: 缓存保质期, 一个相对时间
  1. 协商缓存: Last-Modified/If-Modified-Since, etag/If-None-Match, 协商缓存仅仅在cache-control: max-age=0; 或者cache-control: no-cache的情况发生

无效缓存

另外, 若在chrome使用https://IP:PORT的方式进行访问, 发现强缓存并不生效, 但是在firefox中却是可以的, 此时进行如下操作:

  • a. 修改本地hosts, 添加一个域名, 映射到IP
  • b. 通过https://domain:PORT的方式进行访问, 此时在chrome中发现强缓存生效, 不知道具体原因

最后找到原因, 对于self-signed SSL Certificates, chrome会忽略所有的缓存配置, 见下文参考.

状态码

任何一个 HTTP 请求, Server会按照 HTTP 协议返回指定的状态码, 用以响应该请求,其整体分类如下:

1**: 额外信息, 表示服务器收到请求, 需要请求者继续执行后续操作
2**: 成功, 表示服务器成功接收请求并处理
3**: 重定向, 表示需要进一步的请求才能完成最初的目的
4**: 客户端错误, 表示服务器接收到一个错误的请求
5**: 服务器错误, 表示服务器在处理请求的过程中出现错误, 可能由代码认为抛出

1XX

  • 100: continue, 表示继续请求
  • 101: switching protocols, 表示切换协议, 服务器根据Client的请求需要切换到高级协议

2XX

  • 200: OK
  • 201: Created, 成功请求并创建了新的资源
  • 202: Accepted: 已接收请求, 但是为处理完成
  • 203: Non-Authoritative: 非授权信息, 请求成功, 但是返回一个副本
  • 204: No-content, 无内容
  • 205: Reset Content, 重置内容, 服务器返回此码, 告知浏览器清除表单域
  • 206: 部分内容, 成功处理了部分请求

3XX

  • 300: Multiple Choices, 请求的资源可能有多个位置
  • 301: Move Permanently, 永久移动
  • 302: Found, 临时移动
  • 303: See other, 查看其它地址
  • 304: Not Modified, 未修改
  • 305: Use Proxy, 表示使用的资源必须通过代理访问
  • 306: Unused
  • 307: Temporary Redirect: 临时重定向

其中301和302的区别:

  • 301: 临时资源移动
  • 302: 持久性资源移动

303/307提出原因: 用于细化302请求, 处理非GET/HEAD请求, 在HTTP1.1中使用. 非 GET/HEAD 请求: 理论上, 对于 POST 请求, 如果资源被Moved Temporarily, 则需要浏览器做出一定的操作, 让用户重新确认进行跳转, 但是实际上没有做, 浏览器直接 GET 请求.

4XX

  • 400: Bad Request, 请求错误, Server无法理解
  • 401: Unauthorized, 请求要求用户进行认证
  • 402: Payment Required, 保留状态码
  • 403: Forbidden, 服务器拒绝执行
  • 404: Not found, 服务器无法找到资源
  • 405: Method not allowed, 请求的方法被服务器禁止
  • 406: Not Acceptable, 服务器无法根据客户端的内容特性完成请求
  • 407: Proxy Authentication, 请求要求代理的身份认证
  • 408: Request Timeout, 服务器等待客户端发送时间过程, 超时, Request body过大
  • 409: Conflict, 处理冲突, 例如已经存在相同的唯一信息
  • 410: Gone, 请求的资源已经丢失, 以前存在
  • 411: Length Required: 无法处理没有Content-length的请求
  • 412: Precondition Failed: 客户端请求信息的先决条件有问题
  • 413: Request Entity Too Large: 请求实体过大, 服务器无法处理
  • 414: Request URI too large, 请求的 URL 过长
  • 415: Unsupported media type: 不支持的媒体格式
  • 416: Requested range not satisfiable: 客户端请求的范围无效
  • 417: Expectation Failed: 服务器无法满足Expect的请求头信息

5XX

  • 500: 内部错误
  • 501: Not implemented, 无法处理请求
  • 502: Bad Gateway, 充当网关或者代理的服务器, 从远端服务器接收到一个无效请求
  • 503: Service Unavailable, 由于系统超载, 服务器暂时无法处理
  • 504: Gateway timeout, 充当网关或者代理的武器, 从远端服务器接收请求时超时
  • 505: Http version: 服务器不支持的 HTTP 版本

参考