ASGI翻译系列(三):使用 ASGI 和 HTTP

在上一篇文章中,我们研究了 ASGI 接口的基本结构。现在我们将进一步了解 HTTP 请求的消息结构,并看看我们如何使用 Starlette 包提供的一些数据结构来处理 ASGI 中的 HTTP 请求。

在任何 ASGI 应用程序中发生的第一件事情是,它用“scope”字典来实例化,该字典提供有关传入请求的一些初始信息。

下面是范围字典如何查找简单 HTTP 请求的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> scope = {
"type": "http",
"http_version": "1.1",
"method": "GET",
"scheme": "https",
"path": "/",
"query_string": b"search=red+blue&maximum_price=20",
"headers": [
(b"host", b"www.example.org"),
(b"accept", b"application/json")
],
"client": ("134.56.78.4", 1453),
"server": ("www.example.org", 443)
}

scope字典与 WSGI 的“environ”字典非常相似。事实上,ASGI 规范描述了如何在两者之间进行映射.。

1
2
3
4
5
6
7
8
9
10
11
12
13
environ = {
"REQUEST_METHOD": "GET",
"SCRIPT_NAME": "",
"PATH_INFO": "/",
"QUERY_STRING": "search=red+blue&maximum_price=20",
"SERVER_NAME": "www.example.org",
"SERVER_PORT": 443,
"REMOTE_HOST": "134.56.78.4",
"REMOTE_PORT": 1453,
"SERVER_PROTOCOL": "HTTP/1.1",
"HTTP_HOST": "www.example.org",
"HTTP_ACCEPT": "application/json",
}

译者注:原文代码的使用的ASGI2协议,当时接口采用的是双重调用,在2019年3月20日,ASGI3.0发布,改进了调用风格,虽然向下兼容,但是原文的例子有些还是没法用了,所以我这边做了更新统一采用ASGI3写法并适当进行增删改。

具体可以看ASGI3.0

1. scope的类型

ASGI 和 WSGI 之间的一个根本区别是 ASGI 是一个更通用的消息传递接口,而 WSGI 是严格的请求/响应接口。

在任何 ASGI 的scope中必须始终存在的一个元素是“ type”键,它用于指示协议类型。

1
2
3
scope = {
"type": "http", # Deal with an incoming HTTP request.
...

2. Request URL

可以根据 scheme server pathquery _ string 中包含的信息构造传入请求的完整 URL。

由于我们通常不需要使用原始的ASGI信息进行工作,因此拥有一些可重用的工具对我们很有用,我们可以使用这些工具为我们提取一些细节。

Starlette框架被特意设计为零依赖库,因此可以将其用作各种 ASGI 应用程序或其他更高级别框架的基础。

它提供了一组用于 ASGI 的基本数据结构。例如:

1
2
3
4
5
6
>>> from starlette.datastructures import URL
>>> url = URL(scope=scope)
>>> url
URL('https://www.example.org/?search=red+blue&maximum_price=20')
>>> str(url)
'https://www.example.org/?search=red+blue&maximum_price=20'

通过使用URL实例,你可以像使用标准库的urlparse一样检查URL上的各个组成部分:

1
2
>>> url.scheme
'https'

你也可以修改 URL 的组件,并返回一个新的实例:

1
2
>>> url.replace(hostname='www.example.com')
URL('https://www.example.com/?search=red&maximum_price=20')

3. Request headers

ASGI 中的 HTTP 头被授权为一个字节对列表,表示头的名称和值。由于 HTTP 标头是不区分大小写的,因此 ASGI 强制要求按字节分类的标头表示必须严格采用小写格式。

HTTP 标头还可以包含具有相同标头名称的多个实例,例如与 Set-Cookie一起使用。

Starlette 提供了一个不变的、不区分大小写的、多 dict 来处理来自 ASGI 的 HTTP 请求头。

1
2
3
4
>>> from starlette.datastructures import Headers
>>> headers = Headers(scope=scope)
>>> headers
Headers({'host': 'www.example.org', 'accept': 'application/json'})

实例化 header 数据结构是一种廉价的操作,与直接迭代逐字节对相比,它为检查请求 header 提供了一个更方便的接口。

1
2
>>> headers['Accept']
'application/json'

对于响应头,Starlette 提供了一个 MutableHeaders 数据结构,这个数据结构是等效的,但可以设置或删除头值。

4. Request query params

我们需要处理的 URL 的一个特殊部分是请求查询参数,例如? search = red + blue & maximum _ price = 20

为了处理这些问题,可以使用 QueryParams 类,它在参数上提供了一个不变的多字典实现。

1
2
3
4
5
6
>>> from starlette.datastructures import QueryParams
>>> params = QueryParams(scope=scope)
>>> params
QueryParams(query_string='search=red+blue&maximum_price=20')
>>> params['search']
'red blue'

5. Request body

有关传入请求的大部分信息都存储在“scope”中,并在 ASGI 应用程序实例化时显示。但是,对于请求主体,这是不可能的。

为了访问请求体,我们必须从“ receive”通道获取消息流。以下是我们如何在流中获取单个消息的方法:

1
message = await receive()

以下是 HTTP 请求正文消息结构的一个示例:

1
2
3
4
5
{
'type': 'http.request.body',
'body': b'{"example": "Some JSON data"}',
'more_body': False
}

如果你直接使用 ASGI,请按照以下模式使用HTTP请求正文的整个流:

1
2
3
4
5
6
body = b''
more_body = True
while more_body:
message = await receive()
body += message.get('body', b'')
more_body = message.get('more_body', False)

Starlette提供了一种轻量级的方式来在请求界面中同时封装“scope”和“receive”通道,从而提供了更简单的方法来访问请求主体:

1
2
request = Request(scope, receive=receive)
body = await request.body()

或者,要获取JSON解析的表示形式,请执行以下操作:

1
2
request = Request(scope, receive=receive)
body = await request.json()

对于大型请求,你可能不希望一次就把整个请求体消耗到内存中。

从 Python 3.6开始,异步生成器语法得到了支持,它允许我们提供一个很好的简单的API来流式处理请求体…

1
2
3
4
request = Request(scope, receive=receive)

async for chunk in request.stream():
... # Do something with "chunk"

我们可以将其与请求解析结合起来,以便优雅地处理表单数据。

在处理 HTTP 多部分请求时,您通常希望确保请求解析能够将任何文件上传内容流到磁盘上的临时文件中,而不必首先将所有数据加载到内存中。

1
2
3
4
request = Request(scope, receive=receive)

# Any upload files in the request body will be streamed into temporary files.
form = await request.form()

6. 总结

我们已经了解了如何为HTTP请求构造ASGI的“scope”消息,以及消息主体是如何通过 “receive “通道进行流式传输的。

我们还使用了Starlette提供的一些基本数据结构,以便在更高层次上使用ASGI。