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

我们之前的文章介绍了 ASGI 协议,并介绍了为什么拥有一个标准化的低级server/application接口是有用的,以及 Python 社区超越现有 WSGI 服务器并开始采用 ASGI 的一些动机。

在这篇文章中,我们将开始看看 ASGI 的构建快,并演示我们如何开始使用它们来编写 web 服务。

作为一个应用程序开发者,你通常不会在低级别的地方使用ASGI,因为框架通常会提供一个更高级别的接口来工作。

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

具体可以看ASGI3.0

1. ASGI应用

ASGI的结构是一对可调用的接口。

第一个API调用是一个常规函数调用,它是为了建立一个新的有状态上下文。

第二个API调用是一个异步调用,它提供了一对通信通道,服务器和客户端通过这个通道互相发送信息。

下面是基本结构的样子:

1
2
3
4
5
6
7
8
9
def asgi_application(scope):
# Perform any initial state setup.
...

async def asgi_instance(receive, send):
# This is where the application performs any actual network I/O.
...

return asgi_instance

上面的例子是ASGI2双重调用的风格,新的 ASGI3.0应用程序看起来是这样的:

1
2
async def application(scope, receive, send):
...

让我们来看看这些接口的参数:

1.1 Scope

一个信息字典,用来设置应用程序的状态。

ASGI可以用于各种接口,而不仅仅是HTTP,所以这个字典中最重要的键是 “type “键,它用来确定设置的是什么样的消息接口。

下面是一个简单的HTTP GET请求 https://www.example.org/的scoop的例子:

1
2
3
4
5
6
7
8
{
"type": "http",
"method": "GET",
"scheme": "https",
"server": ("www.example.org", 80),
"path": "/",
"headers": []
}

1.2 Send

一个接受单个消息参数并返回 None 的异步函数。在 HTTP 的情况下,这个消息通道用于发送 HTTP 响应。

有两种类型的 HTTP 响应消息: 一种用于初始化发送响应,另一种用于发送响应正文。

1
2
3
4
5
6
7
8
9
10
11
await send({
"type": "http.response.start",
"status": 200,
"headers": [
[b"content-type", b"text/plain"],
],
})
await send({
"type": "http.response.body",
"body": b"Hello, world!",
})

1.3 Receive

一个不带参数的异步函数,该函数返回一条消息。在使用HTTP的情况下,此消息传递通道用于使用HTTP请求正文。

1
2
3
4
5
6
7
8
# Consume the entire HTTP request body into `body`.
body = b''
more_body = True
while more_body:
message = await receive()
assert message["type"] == "http.request.body"
body += message.get("body", b"")
more_body = message.get("more_body", False)

2. 我们的第一个“ Hello,World!”应用程序

让我们把所有这些放在我们的第一个简单的 ASGI 应用程序中:

example.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async def app(scope, receive, send):
assert scope["type"] == "http"

await send({
"type": "http.response.start",
"status": 200,
"headers": [
[b"content-type", b"text/plain"],
],
})
await send({
"type": "http.response.body",
"body": b"Hello, World!",
})

你现在可以使用任何 ASGI 服务器运行该应用程序,包括 daphne、 uvicorn 或 hypercorn。

1
2
3
4
5
6
$ pip3 install uvicorn
[...]
$ uvicorn example:app
INFO: Started server process [30074]
INFO: Waiting for application startup.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

现在在你的网页浏览器中打开“ http://127.0.0.1:8000

也许现在还不是特别令人兴奋?尽管如此,它仍然是一整套功能的基础,而这些功能在 Python 现有的 WSGI 接口中是不可能实现的。

3. 构造 ASGI 应用程序的不同方法(适用于ASGI2)

有多种方法可以构建一个 ASGI 应用程序。

3.1 使用闭包将scope绑定到 ASGI 实例

1
2
3
4
5
6
7
def app(scope):
assert scope["type"] == "http"

async def asgi(receive, send):
...

return asgi

3.2 使用 functools.partial 将scope绑定到 ASGI 实例

1
2
3
4
5
6
7
8
9
import functools


async def asgi_instance(receive, send, scope):
...

def asgi_application(scope):
assert scope["type"] == "http"
return functools.partial(asgi_instance, scope=scope)

3.3 使用基于类的接口将scope绑定到一个 ASGI 实例

1
2
3
4
5
6
7
class ASGIApplication:
def __init__(self, scope):
assert scope["type"] == "http"
self.scope = scope

async def __call__(self, receive, send):
...

基于类的接口将在 ASGI 实现中非常常见,因为它实例化了一个对象,在单个请求/响应周期的生命周期中可以对其状态进行操作。

4. 更高层次的工作

虽然理解 ASGI 的工作原理很重要,但是你不希望大部分时间都在低层接口上工作。

Starlette 库提供了请求和响应类,可用于处理读取传入的HTTP请求和发送传出的响应的底层细节。

4.1 HTTP Requests

Request 类接受一个 ASGI scope,也可以选择receive通道,并在请求上显示一个更高级别的接口。

1
2
3
4
5
6
7
from starlette.requests import Request


async def app(scope, receive, send):
assert scope["type"] == "http"
request = Request(scope=scope, receive=receive)
...

request类使下列接口可用:

  • request.method - The HTTP method.
  • request.url - A string-like interface that also gives you access to the parsed components of the URL. eg request.url.path.
  • request.query_params - A multi-dict, containing the parsed URL query parameters.
  • request.headers - A case-insensitive multi-dict, containing the HTTP headers.
  • request.cookies - A dictionary of string values, representing all the cookie data included in the request.
  • async request.body() - An asynchronous method for returning the request body as bytes.
  • async request.form() - An asynchronous method for returning the request body parsed as HTML form data.
  • async request.json() - An asynchronous method for returning the request body parsed as JSON data.
  • async request.stream() - An asynchronous iterator for consuming the request stream chunk-by-chunk without reading everything into memory.

4.2 HTTP Responses

Starlette 包括各种处理发送回传出 HTTP 响应的 Response 类。

下面是一个同时使用请求和响应的示例:

example.py

1
2
3
4
5
6
7
8
9
10
11
12
from starlette.requests import Request
from starlette.responses import JSONResponse

async def app(scope, receive, send):
assert scope["type"] == "http"
request = Request(scope=scope, receive=receive)
response = JSONResponse({
"method": request.method,
"path": request.url.path,
"query_params": dict(request.query_params),
})
await response(scope, receive, send)

响应实例与其他任何 ASGI 实例呈现相同的接口。

要真正发送响应,你可以用同样的方式来称呼它:

await response(scope, receive, send)

这是一个很好的属性,因为它意味着我们可以像使用 ASGI 应用程序的后半部分一样使用响应实例。

运行我们的应用程序:

1
2
3
4
$ uvicorn example:app
INFO: Started server process [30074]
INFO: Waiting for application startup.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

5. 总结

我们已经掌握了第一个 ASGI“ Hello,World”应用程序。

虽然理解 ASGI 消息传递的基本原理很重要,但这不是我们开发的时候要花费时间的地方,因此我们也看到了如何开始将这些细节抽象到更高级别的请求/响应接口中。

我们讨论了以下术语,每当我们谈论使用ASGI的机制时,都需要这些术语:

  • ASGI Application - 满足 ASGI 接口的应用程序
  • Scope - 用于实例化 ASGI 应用程序的信息字典
  • Receive, Send - server/application消息传递发生的一对通道
  • Message - 通过接收或发送通道发送的信息字典

我们还开始使用Starlette软件包,该软件包为我们提供了在更高级别的界面上与ASGI一起使用所需的基本工具集。

本系列的下一篇文章中,我们将更详细地探讨ASGI HTTP消息传递。