Skip to content

HTTP Client

HTTP 是一种用作获取诸如 HTML 文档这类资源的文件传输协议。同时他也是一种 C/S 协议,即客户端发出请求(request),由服务器接受来应答返回响应(response)

一个 HTTP 请求库分为两个方面: 首先构建请求,然后等待服务器应答来获取响应。其中 aiohttp 提供了异步的 HTTP 请求支持。这意味着他能够异步处理服务器返回的响应。

报文

HTTP 协议通过 TCP/IP 来传输,其中传输的都是字节流。

请求报文

一个标准的请求包括几个方面:

  1. HTTP 方法: 其中最为常用的就是 GET 和 POST 方法
  2. 要获取的资源路径: 也就是我们所说的 URL
  3. HTTP 协议以及版本号
  4. 请求头: 其中包含了要告知服务器的元信息
  5. 请求体: 像 POST 这样的方法需要将一些资源发送给服务器就需要添加请求体

上面的内容会以 HTTP 报文的形式发送给服务器:

Text Only
POST /search HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-silverlight, application/x-shockwave-flash, */*
Referer: <a href="http://www.google.cn/">http://www.google.cn/</a>
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; TheWorld)
Host: <a href="http://www.google.cn">www.google.cn</a>
Connection: Keep-Alive
Cookie: PREF=ID=80a06da87be9ae3c:U=f7167333e2c3b714:NW=1:TM=1261551909:LM=1261551917:S=ybYcq2wpfefs4V9g; NID=31=ojj8d-IygaEtSxLgaJmqSjVhCspkviJrB6omjamNrSm8lZhKy_yMfO2M4QMRKcH1g0iQv9u-2hfBW7bUFwVh7pGaRUb0RnHcJU37y-FxlRugatx63JLv7CWMD6UB_O_r

hl=zh-CN&source=hp&q=domety

Tips

所有的请求库说白了就是方便的以编程的形式构建 HTTP 报文的过程

响应报文

服务器将应答发回给客户端:

Text Only
HTTP/1.1 200 OK
Date: Sat, 31 Dec 2005 23:59:59 GMT
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 122

<html>
<head>
<title>Wrox Homepage</title>
</head>
<body>
<!-- body goes here -->
</body>
</html>

他包括:

  1. HTTP 协议以及版本号
  2. 响应状态码和信息: 300 - OK
  3. 响应头: 其中包含了要告知客户端的元信息
  4. 响应体: 呈现给用户的内容

使用 aiohttp 构建请求

一个标准的在 aiohttp 构建请求的方式如下:

Python
import aiohttp
import asyncio

async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get('http://httpbin.org/get') as resp:
            print(resp.status)

asyncio.run(main())

ClientSession

ClientSession是 aiohttp 中所有客户端 API 的心脏和主要入口点。会话的核心是他维护了一个连接池(默认 100 个,注意是链接 100 个不同 host 的网站,而不是当个 host 100 个连接),并且会话中对同一个 host 的请求会共享 cookie。

发起请求

当我们持有了 ClientSession 后可以通过 session.getsession.post 这样的形式来发起对应请求方法的请求,如果想要对同一个站点发起多个请求,也可以简单的通过下面的方式实现:

Python
async with aiohttp.ClientSession('http://httpbin.org') as session:
    async with session.get('/get'):
        pass
    async with session.post('/post', data=b'data'):
        pass
    async with session.put('/put', data=b'data'):
        pass

Note

session.get 等方法是对内部的 client.request("GET") 的封装

Tips

一定不要为每一个请求都创建一个会话,一个会话里面维护了连接池、Keep-Alive 等能够提高性能

在 URL 中传递参数

如果 URL 需要添加参数,需要构建 http://xxx.org/get?key1=val1&key2=val2 这样的形式,并且如果包含非法字符还需要将他们编码。 aiohttp 提供了更加方便的形式来处理他们:

Python
async with aiohttp.ClientSession() as session:
    params = {'key1': 'value1', 'key2': 'value2'}
    async with session.get('http://httpbin.org/get', params=params) as resp:
        expect = 'http://httpbin.org/get?key1=value1&key2=value2'
        assert str(resp.url) == expect

构建请求头

我们可以向请求添加 HTTP 请求头来定制请求。有两种传递形式:

  1. ClientSession 的 headers 参数可以为所有使用该会话的请求共享
  2. 特定于请求的 headers 参数
Python
# 所有使用该会话的请求共享该 headers
headers={"Authorization": "Basic bG9naW46cGFzcw=="}
async with aiohttp.ClientSession(headers=headers) as session:
    async with session.get("http://httpbin.org/headers") as r:
        json_body = await r.json()
        assert json_body['headers']['Authorization'] == \
            'Basic bG9naW46cGFzcw=='

    # 特定于指定请求的 headers 参数
    url = 'http://example.com/image'
    payload = b'GIF89a\x01\x00\x01\x00\x00\xff\x00,\x00\x00'
          b'\x00\x00\x01\x00\x01\x00\x00\x02\x00;'
    headers = {'content-type': 'image/gif'}
    await session.post(url,
                       data=payload,
                       headers=headers)

请求体

通常构造 post 请求需要添加请求体,则由 data 参数来指定。需要注意的时 data 的值由请求头中的 Content-Type 决定

他对应到 HTML 中就是 form 元素的 enctype 属性的值:

HTML
<!-- method="post" 才能指定 enctype -->
<form action="/foo" method="post" enctype="multipart/form-data">
  <input type="text" name="description" value="一些文本" />
  <input type="file" name="myFile" />
  <button type="submit">提交</button>
</form>

Tips

实际上还有其他类型,具体要看后端服务器提供的接口。不过 HTML 表单仅仅支持三种类型

Content-Type: application/x-www-form-urlencoded

这个是最简单也是默认的 post 请求体。他的报文如下所示:

Text Only
POST /test HTTP/1.1
Host: foo.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 27

field1=value1&field2=value2

在 aiohttp 中的构造方式就是:

Python
payload = {'key1': 'value1', 'key2': 'value2'}
async with session.post('http://foo.example/test', data=payload) as resp:
    print(await resp.text())

Tips

aiohttp 会自动为我们在请求头中添加对应的 Content-Type

Content-Type: multipart/form-data

在一些需要上传文件的地方可能会用到这种形式,他的报文如下所示:

Text Only
POST /foo HTTP/1.1
Content-Length: 68137
Content-Type: multipart/form-data; boundary=---------------------------974767299852498929531610575

-----------------------------974767299852498929531610575
Content-Disposition: form-data; name="description"

一些文本
-----------------------------974767299852498929531610575
Content-Disposition: form-data; name="myFile"; filename="foo.txt"
Content-Type: text/plain

(上传文件 foo.txt 的内容)
-----------------------------974767299852498929531610575--

要构建这类编码可以通过:

Python
url = 'http://httpbin.org/post'
files = {'file': open('foo.txt', 'rb')}

await session.post(url, data=files)

# 也可以显示设置
# 上面实际上是下面的便捷方法
url = 'http://httpbin.org/post'
data = aiohttp.FormData()
data.add_field('file',
               open('report.xls', 'rb'),
               filename='report.xls',
               content_type='application/vnd.ms-excel')

await session.post(url, data=data)

对于比较大的文件也可以使用流式上传:

Python
with open('massive-body', 'rb') as f:
   await session.post('http://httpbin.org/post', data=f)

Tips

同样 aiohttp 会自动为我们在请求头中添加对应的 Content-Type

Content-Type: application/json

这个并不是 form 的官方支持标准,但是 JSON 作为主流的数据共享方式,越来越多的服务器支持这种类型了,甚至大多数请求库都提供了 json 参数来接收他:

Python
async with session.post(url, json={'example': 'test'}) as resp:
    pass

超时

默认情况下如果请求超过 300s 没有响应将超时,可以通过 timeout 参数来覆盖这个值。并且由于异步的特殊性,超时需要使用 ClientTimeout 来构造,它包含几个参数:

  1. total: 整个操作(建立连接、发送请求和读取响应)的最大秒数
  2. connect: 建立连接或等待连接池中的空闲连接的最大秒数
  3. sock_connect: 与服务器建立连接的最大秒数
  4. sock_read: 从服务器读取新数据部分之间允许的最大秒数
  5. ceil_threshold: 触发超时上限的阈值,默认是 5
Python
timeout = aiohttp.ClientTimeout(total=60)
timeout_post = aiohttp.ClientTimeout(total=100)

async with aiohttp.ClientSession(timeout=timeout) as session:
    # 使用 session 的
    async with session.get(url) as resp:
        pass
    # 覆盖默认的
    async with session.post(url, timeout=timeout_post) as resp:
        pass

cookies

重定向

通过 allow_redirects=Truemax_redirects=10 来设置重定向,默认是开启的最大重定向数是 10 次。

代理

通过 proxy 来指定代理:

Python
async with aiohttp.ClientSession() as session:
    async with session.get("http://python.org", proxy="http://proxy.com") as resp:
        print(resp.status)

Tips

aiohttps 只支持普通的 HTTP 代理,注意这并不影响之后访问 https 协议网站

解析响应

向服务器发起请求后,服务器会应答并返回响应报文,整个过程是一个黑箱操作,aiohttp 会将响应报文包装成ClientResponse 对象返回供用户使用。

获取响应正文

响应最核心的就是获取响应的正文:

Python
async with session.get('https://api.github.com/events') as resp:
    # 最标准的获取方式
    # 用于返回以文本形式传输的内容
    content = await resp.text(encoding='utf-8')

async with session.get('https://api.github.com/image.png') as resp:
    # 如果非文本请求,使用 read() 来返回二进制
    # 实际上对于文本内容 resp.read().encode('utf-8') 也可以
    img = await resp.read()

async with session.get('https://api.github.com/test.json') as resp:
    # 对于响应体中 Content-Type: application/json 的也能够直接通过 json 返回
    # 他等价于 json.loads(resp.text())
    content = await resp.json()

上面的三个方法都会讲整个数据加载到内存中来返回,这对于非常大的文件来说非常消耗性能,因此 aiohttp 也提供了流式访问的接口:

Python
async with session.get('https://api.github.com/events') as resp:
    # resp.content 就是开启数据流
    await resp.content.read(10)

# 如果想要流式保存文件,可以使用 iter_chunked 接口:

with open(filename, 'wb') as fd:
    async for chunk in resp.content.iter_chunked(chunk_size):
        fd.write(chunk)

Tips

注意一旦启用了流即调用了 resp.content 那么 read、json 和 text 方法就无法使用了,即使流中的数据没有消耗完毕

响应状态码

通过 resp.statusresp.reason 来获取响应的状态码和对应的状态信息:

Python
async with session.get('https://api.github.com/events') as resp:
    # 他们都不是异步的
    print(resp.status) # 20
    print(resp.reason) # "OK"

还有一个 resp.ok 属性他会在 resp.status < 400 时返回 True 表示请求成功。

重定向历史

如果请求被重定向,整个重定向的调用链被保存在 resp.history: [] 中:

Python
resp = await session.get('http://example.com/some/redirect/')
assert resp.status == 200
assert resp.url = URL('http://example.com/some/other/url/')
assert len(resp.history) == 1
assert resp.history[0].status == 301
assert resp.history[0].url = URL('http://example.com/some/redirect/')

Tips

如果没有发生重定向或 allow_redirects = False 那么 resp.history == []

参考