所谓 ASGI

请注意:本文编写于 ,其中某些信息可能已经失去时效性。

前言

本文的主体内容大部分来自对 ASGI Documentation 原文的翻译,其余部分为本人对原文的理解,在整理过程中我没有刻意地区分翻译的部分和我个人理解的部分,这两部分内容被糅杂在一起形成了本文。因此,请不要带着「本文的内容是百分之百正确」的想法阅读。如果文中的某些内容让你产生疑惑,你可以给我留言与我讨论或者对比 ASGI Documentation 的原文加以确认。

本人在之前整理过一篇 何为 WSGI 是对 PEP 333PEP 3333 的翻译和整理,感兴趣的话可以结合起来一起阅读。

字典参照

注:仅代表本人理解,在涉及到这些单词的地方均使用英文单词。

英文 中文 解释
ASGI 异步服务器网关接口
WSGI Web 服务器网关接口
Server 服务器 Web 软件中面向 Client 提供具体服务的部分
Application 应用(应用框架) Web 软件中面向 Server 提供具体应用服务的部分
Connection 连接 一个满足某种协议的 Socket 连接
Event 事件 连接中发生的事件的抽象
scope \ 存放连接细节的容器
send 发送器 应用发送事件消息的工具
receive 接收器 应用接受事件消息的工具

何为 ASGI

ASGI (Asynchronous Server Gateway Interface) is a spiritual successor to WSGI, intended to provide a standard interface between async-capable Python web servers, frameworks, and applications.

ASGI(异步服务器网关接口)是 WSGI 的精神续作,目的是为具有异步功能的 Python Web 服务器、框架和应用之间提供一个标准接口。

from ASGI Documentation

ASGI vs WSGI:宏观

  1. ASGI 并非由某个具体 PEP 提出,而是由 Django 开源团队提出并维护的一套对 WSGI 向下兼容的精神续作;
  2. 在异步、长连接盛行的今天,WSGI 所描述的由单一同步可调用对象组成的 Web Application 已经无法平滑地满足现在的开发需求。

ASGI 简介

一个 ASGI Application 是一个单一异步可调用对象,它包含三个参数:

  1. scope:一个存放着 Connection 细节的 Python 字典;
  2. send:一个异步可调用对象,使得 Appliction 可以发送 Event 消息到 Client;
  3. receive:一个异步可调用对象,能够让 Application 从 Client 接收 Event 消息。

在这种结构下,ASGI 下的每个 Application 不仅能同时处理多个 incoming Event 和 outcoming Event,在协程的加持下 Application 还可以做更多的事情,例如监听像 Redis 队列一样的外部的 trigger Event。

下面是一个最简单的 ASGI Application 模型:

1
2
3
4
async def application(scope, receive, send):
event = await receive()
...
await send({"tpye": "websocket.send", ...})

所有的 Event 都是通过一个特定格式的 Python 字典被 send 和 receive 进行处理,正是这些 Event formats 构成了 ASGI 标准的基础,使得 Application 能够适应不同的 Servers。

所有的 Event formats 都有一个用来决定自身结构的 type 键值对,下面就是一个 ASGI 中 HTTP 协议发送请求的 Event format 示例:

1
2
3
4
5
{
"type": "http.request",
"body": b"Hello World",
"more_body": False,
}

下面则是一个 ASGI 中通过 WebSocket 协议发送消息的 Event format:

1
2
3
4
{
"type": "websocket.send",
"text": "Hello world!",
}

与 WSGI 的兼容性

ASGI 被设计成 WSGI 的超集,并且给出了一个两者之间的转换方式,允许 WSGI Application 通过一个 translation wrapper 在 ASGI Server 中运行。

一个线程池可以在 async event loop 之外运行同步的 WSGI Applications。

注:这里的原话是 A threadpool can be used to run the synchronous WSGI applications away from the async event loop.,不太清楚是想表示一种并列还是异或关系。

ASGI 规范(V 3.0)

摘要

本规范提出了一种 Network Protocol Servers(尤其是 Web Servers) 和 Python Applications 之间通讯的标准接口,旨在允许通过一套协议同时处理多种常见协议,例如:HTTP、HTTP/2、和 WebSocket。

本规范期望通过固定一套 API,满足 Server 与 Server 之间和 Server 与 Application 之间的交互需要。每一个被支持的协议都有一个子规范,子规范描述了如何将该协议编码或解码为 Event 消息。

提出理由

WSGI 规范自推出以来一直运行良好,它的出现使得 Python Framework 和 Web Server 之间的对接变得更加灵活和简便。然而,它的设计过分依赖于 HTTP 风格的 Request/Response 循环,但现在越来越多不遵循这种交互模式的协议正在网络编程时被普遍使用,其中最明显的就是 WebSocket。

ASGI 便在这种情况下应运而生,它试图维护一个简单 Application API,同时提供一套抽象方法,允许来自不同 Appliction 线程或进程的数据在任何时刻被发送和接受。

基于「将不同协议转换为 Python 兼容且异步友好的消息集」的原则,ASGI 可以概括为两部分:

  • 用于构建 Servers 的标准化通信接口;
  • 一套支持不同协议的标准消息格式。

ASGI 的核心目的是提供一种方法在能够处理 HTTP/2 和 WebSocket 协议的同时正常处理 HTTP 协议。然而要实现这个功能就必然意味着需要一个简单快捷的方式来支持现有的 WSGI Servers 和 Applications。因为目前大部分的 Python Web 服务都依赖于 WSGI。这部分内容被放在了 HTTP 子协议

概述

ASGI 由两个不同的组件组成:

  • Protocol Server:用于终止 sockets 并将其转换为 connections 和相关 event messages;
  • Application:寄生在 Protocol Server 中,会在每个 connection 中被调用一次并在调用时处理相关 event messages,在必要时将处理好的相关 event messages 返回。

与 WSGI 相同的是,Server 会在内部处理 Application 并以标准化的格式向其分派请求。不同的是,ASGI 中的 Application 是异步的可调用对象,而不是简单的可调用对象,它通过接收和发送异步事件与 Server 进行通信,而不是接收单一的输入流并返回单一的可迭代对象。

ASGI Applications 必须以 async/await 兼容的协程程序运行,及兼容 asyncio。如果需要使用同步代码可以在主线程自由的使用线程或其他进程。

与 WSGI 的另一个不同之处是,ASGI 的 connection 有两个独立的部分:

  • Connection Scope:代表与用户进行的 Protocol Connection,它会一直存在直到 Connection 关闭;
  • Events:它表示当 Connection 有事情发生时发送到 Application 中的信息,以及由 Application 发送给 Server 的消息(其中包括传送给 Client 的的数据)。

Application 通过一个 Connection Spoce 和两个异步的可调用对象被调用的阻塞(await)来接收和返回事件消息。所有的这些都发生在异步事件循环(async event loop)中。

每一次对 Application 可调用对象的调用都会映射到一个传入的 socket 或 Connection,并会延续该 Connection 的寿命,如果有 cleanup 则 Connection 会持续更长时间。

一些协议可能不使用传统的 sockets,ASGI 在规定这些协议时会定义 Scope 合适过期以及合适关闭。

规范细节

Connection Scope | 连接范围

用户对 ASGI Application 的每个请求都会对应一个 Connection 并引发对 Application 可调用对象的调用,来完整地处理此 Connection。对特定 Connection 信息的描述和生命周期的记录被称为 Connection Scope。

第一个被传入 Application 可调用对象的参数就是一个存放特定 Connection 信息的 Scope 字典。

例如,在 HTTP 协议下,Connection Scope 仅持续一个请求,但是这个被传递的 Scope 包含了大多数钱请求数据(除了 HTTP Request Body,因为这部分内容是通过事件流传输的)。

但是,在 WebSocket 协议下,只要 socket 被接通 Connection Scope 就会维持下去。并且 Scope 会传递包含 WebSocket path 在内的信息,不过像消息等细节内容则是作为 Events 传递的。

一些协议可能会给定一个信息非常有限的 Scope,因为它们封装了类似握手之类的内容。但是每个协议的定义都必须包含 Connection Spoce 的持续时间和你将在这个 Scope 参数中获取那些信息。

根据协议细节不同,Applications 在与 Client 通讯之前可能需要等待一个初始的启动信息。

Events | 事件

ASGI 将协议分解为一系列 Application 必须接收和反应的的 Events,以及 Application 可能在响应中发送的 Events。对于 HTTP 来说,就是简单的按顺序接收两个 Events:http.requesthttp.disconnect 并且发送相应的 Event 消息。而对于像 WebSocket 之类的协议,它可能更多的会是:先接收一个 websocket.connect,再发送一个 websocket.send,在接收一个 websocket.receive 最后接收一个 websocket.disconnect

每个 Event 都是一个带有 top-level type key 的字典,它包含一个关于消息类型的 Unicode 字符串。用户可以自由的创造属于他们自己的消息类型并且在高级 Events Applications 实例之间发送它们,例如:一个了解 Application 可能会通过一个 mychat.message 的 user tpye 发送聊天信息。Applications 应该能够处理一个关于 Events 的混合集合,它们一些来自 Incoming Client Connection,一些来自 Application 的其他部分。

Events 中的信息可以通过网络发送,因此,它们需要被序列化,所以只能以以下类型进行传播:

  • Byte strings | 二进制字符串
  • Unicode strings | Unicode 字符串
  • Integers (within the signed 64-bit range) | 整数
  • Floating point numbers (within the IEEE 754 double precision range; no Nan or infinities) | 浮点数
  • Lists (tuples should be encoded as lists) | 列表(元组应该被转换为列表)
  • Dicts (keys must be Unicode strings) | 字典(键必须是 Unicode 字符串)
  • Booleans | 布尔值
  • None

Applications | 应用

在 3.0 版本中,Application 格式改变为使用一个单一的可调用对象,而不是之前的双可调用对象。双可调用的写法在后面的 [Legacy Application](#Legacy Application) 中有所记载。服务器可以使用 asgiref.compatibility 库轻松地实现对它的支持,并且应该尽可能支持它。

ASGI Application 应该是一个单一的异步可调用对象:

1
coroutine application(scope, receive, send)
  • scope:存放 Connection Scope 信息,是一个至少包含指定传入协议的的字典;
  • receive:一个能够在 Event 字典 available 时产出一个新 Event 字典的异步可调用对象;
  • send:一个异步可调用对象,接收一个 Event 字典作为位置参数,并在发送完成或 Connection 关闭时 return。

Application 会在每个 Connection 中被调用一次。Conection 的定义以及其生命周期由协议规范决定。例如,对于 HTTP 来说一个 Connection 就是一次请求,而对于 WebSocket 来说一个 Connection 是一个 WebSocket 连接。

你发送和接收的 Scope 和 Event 消息的格式都是由 Application 协议之一定义的。Scope 必须是一个字典。scope["type"] 必然存在,可以用它来判断那个协议被传入。scope["asgi"] 也会以字典的形式存在,其中 scope["asgi"]["version"] 代表了 Server 支持的 ASGI 的版本。如果这个值不存在,则默认为 "2.0"

也可能有一些特殊的版本信息存放在 scope["asgi"]["spec_version"]。这样做能够允许各个协议规范进行增强而不影响整个 ASGI 版本。

在本规范的具体协议子规范中给出了详细的 Scope 和 Event 消息的格式,它们就类似于 WSGI 的 environ 字典中的 keys 的规范

Legacy Application | Application 的历史版本

ASGI v2.0 Application 被定义成一个可调用对象:

1
application(scope)

它能够返回另一个异步的可调用对象:

1
coroutine application_instance(receive, send)

其中 scope, receive 和 send 的含义与新版本一致,但注意:第一个可调用对象是同步的。

第一个可调用对象会在 Connection 开始时被调用,然后第二个可调用对象紧接着会被调用或阻塞。

这种书写风格在 v3.0 中已经被淘汰了,使用两个可调用对象的布局方案被认为是没必要的。现在它们已经成为旧时代的遗物被用来支持一些仍旧以这种风格编写的程序。

asgiref.compatibility 模块中有一个兼容性套件,你可以用它来检测旧版风格的应用程序,并将其无缝切换为新版但可调用对象的风格。虽然现在这种遗留的风格仍被支持,但它终究会随着时间的推移而被放弃,所以请尽可能使用新版的风格进行代码开发。

Protocol Specifications | 协议规范

具体的协议规范描述了在指定协议下 Scope 和 Event 消息格式的标准化规范。

所有协议中的 Scope 和 Event 消息中都存在的一个键就是 type,它的值代表了 Scope 或 Event 消息的类型(协议类型)。

在 Scope 中,type 的值必须是一个 Unicode 字符串,例如 "http""webscoket,具体参考相关协议子规范的规定。

在消息中,type 应该被命名为 protocol.message_type 其中 protocol 与 Scope 中的 type 相匹配,message_type 由协议子规范定义。消息类型值的示例包括:http.requestwebsocket.send

注意:Application 应该主动拒绝任何未被定义(无法理解)的协议,并给出一个任意类型的异常。如果不这么做,可能会导致服务器认为你支持一个你并不支持的协议,这在于 Lifespan 协议一起使用时可能会产生混淆,因为 Server 会等你主动启动它。

当前支持的协议子规范有:

Middleware | 中间件

ASGI 同样拥有中间件这个概念,中间件同时拥有 Server 和 Application 的功能,能够接收一个 Scope,也能发送或接收异步可调用对象,可以对其内部进行修改并执行内部的 Application。

当中间件修改 Scope 的时候,它应该在改变 Scope 并将其传递给 Application 之前制作一个备份,以防止 Scope 的改变向上层泄漏。我们在构建中间件时不能想当然的认为中间件发送的 Scope 就是最终版本,因为这中间可能有其他的中间件阻挡。正因如此,不要在中间件中保持对 Scope(包括副本)的引用,更不要尝试在 ASGI app 的外围去改变它。修改 Scope 最好的时机就是将控制权交给子 Application(中间件中的 Application) 之前。

Error Handling | 异常处理

如果 Server 接收到一个错误的 Event 字典,例如:包含一个未知类型的、缺少 Event type 必要键的或者对象有错误 Python 类型(例如 HTTP 头信息的 Unicode 字符串),这种情况下应该引发一个异常,从异步可调用对象到 Application。

如果一个 Application 从 receive 接收到一个无效的事件字典,也应该有引发一个异常。

但如果是字典中存在额外的键,则不应该引发异常。这就允许在后续过程中对协议规范进行非破坏性的升级或定制。

Server 可以自由地处理运行在其中的 Application 抛出的异常——记录到控制台,发送到 syslog 或其他自定义操作——但一旦发生异常 Server 就必须终止 Application 实例和其中的相关 Connection。

注意在 Connection 关闭后 Server 接收到的 message 不被视为错误,在这种情况下 send 异步可调用对象应该充当一个 no-op。

Extra Coroutins | 额外的协程

Frameworks 或 Applications 可能会希望在为每个 Application 启动协同程序外运行额外的协同程序。但由于在 Python 3.7 中无法将额外的程序设置为实例的协同程序的父级,Application 应该确保所有在 Application 运行时启动的协程与 Application 对应的协程同时关闭或在此之前关闭。

任何在 Application 对应协程关闭后仍在运行的协程都不能保证它能够被顺利执行完毕,因为它可能在任意时间被强制退出。

Extensions | 协议拓展

在有些情况下,我们可能想在核心 ASGI 规范之外提供特定的拓展,或在某个规范推出之前对其进行测试。

为了应对这些场景,ASGI 定义了一种常见的拓展模式——可以对协议规范进行选择性补充,Server 可以利用这个机制使 Application 获得更多的功能。

这一切都是通过 scope 词典的 extensions 条目实现的,它本身对应的也是一个字典。Extensions 字典中的 Unicode 字符串名称是由 Server 和 Application 共同约定而成的。

如果 Server 想要支持一个拓展,需要在 extensions 字典中增加一个条目,并且增加的条目的值也应该是一个字典。Server 可以用这个字典中提供任对 scope 的拓展信息,如果 extension 仅用来表明 Server 的 send 可调用对象允许额外 events,只需要给相关 extension 条目附上一个空字典的值就可以了。

假设一个提供共 HTTP 服务的 Server 希望提供一个允许某个新 event 被返回的拓展,这个事件会导致刷新操作系统级别的网络发送缓冲区,那就可以通过一下格式进行拓展:

1
2
3
4
5
6
7
8
scope = {
"type": "http",
"method": "GET",
...
"extensions": {
"fullflush": {},
},
}

当某个 Application 接收到包含此 scope 的请求后,它就能在调用指定可调用对象时触发相关自定义事件(本例中就是 http.fullflush)。

Strings and Unicode

在本文档以及所有相关子规范中:
byte string:Python3 中的 bytes 类型
Unicode string:Python3 中的 str 类型

本文档永远不会使用 string 来模糊两者之间的表达,在涉及这两个概念的地方都使用了 str 和 bytes 作为区分。

本文档中所有字典的键(包括 scopes 和 events 中的字典)都是 Unicode string。

HTTP Protocol

HTTP 格式涵盖了 HTTP/1.0 HTTP/1.1 HTTP/2,HTTP/2 的变动主要集中在传输层。支持 HTTP/2 的 ASGI Server 应该为同一个 HTTP/2 上的不同请求生成不同的 scopes,并且能够将来自同一个流的响应正确地复用。HTTP 版本会在 scope 中以字符串的形式存在。

处理HTTP 协议中具有相同名字的 header 字段是很复杂的。RFC 7230 规定:在处理任何可重复出现的 header 字段时,都视作只发送一次该 header 字段并将所有的值使用逗号连接。

但同时 RFC 7239 和 RFC 6265 也明确指出,这一规则不适用于 HTTP Cookie 相关的 header 字段(Cookie 和 Set-Cookie)。Cookie header 字段只能由 user-agent 发送一次,但是 Set-Cookie header 字段可以重复出现并且不能使用逗号连接。

ASGI 协议的抉择是将请求和响应头分为两组 [name, value] 列表并且不对传入的值做任何其他处理。

ASGI Server 需要在向 ASGI Application 发出请求时通过一个值为 http 的 tpye 字段表明使用的协议。

HTTP Connection Scope

HTTP Connection 拥有一个唯一的 Request Connection Scope,也就是说,ASGI Application 会在请求开始时被调用,并持续到特定的请求结束后,即使底层的 Socket 仍然处于开启状态并且持续有请求进入。

Connection Scope Message 包括:

名称 类型 描述
type Unicode string “http”
asgi[“version”] Unicode string 使用的 ASGI 规范版本
asgi[“spec_version”] Unicode string Server 支持的 ASGI HTTP 规范版本,可选值:”2.0”,”2.1”,”2.2” 或 “2.3”,默认”2.0”
http_version Unicode string 可选值:”1.0”,”1.1” 或 “2”
method Unicode string HTTP 方法名称,大写
scheme Unicode string URL 协议,可选值:”http” 或 “https”,默认为 “http”
path Unicode string HTTP 请求目标,不包括任何查询字符串,由百分号编码和 UTF-8 字节序解码成字符
raw_path byte string 原始的未被修改的 HTTP path,来自 Web Server,可能缺省,默认为 None
query_string byte string url 中 ? 后的部分,百分号编码
root_path Unicode string Application 被绑定到的根路径,和 WSGI 中的 SCRIIPT_PATH 一致,默认为 “”
headers Iterable[[byte string, byte string]] 一个由 [name, value] 两个子项组成的可迭代对象,name 应尽可能是小写
client Iterable[Unicode string, int] 一个 [host, port] 可迭代对象,默认为 None
server Iterable[Unicode string, Optional[int]] 可以是 [host, port] 可迭代对象,也可以是 [path, None],其中 path 是 unix 套接字路径,缺省为 None

ASGI Server 应该负责处理所有入站和出站的分块传输编码。当一个带有 chunked encoded body 的请求通过 ASGI Server 时,它应该自动去掉请求的分块以 plain body bytes 的形式提供给 ASGI Application。当一个没有 Content-Length 的响应被提供给 ASGI Server 时,它可以按照合适的方式进行 chunked。

Request - receive event

由 ASGI Server 发送给 ASGI Application 以标识一个入站请求。关于这个请求的大部分信息都在对应的 Connection Scope 内。

Receive 中的 body message 是一种传输大量大的入站 HTTP body 块的方式,并且是判断何时执行「实际处理请求的代码」的触发器(因此不应该在仅有一个 Connection Scope 打开时就触发「实际处理请求的代码」)。

注意:如果请求发送时附带了 Transfer-Encoding: chunked 头,ASGI Server 需要负责处理这种编码。http.request message 应该只包含每个 chunk 的解码信息。

Request Receive Event Message 包含:

名称 类型 描述
type Unicode String “http.request”
body Byte String 请求主体,默认为 b””,如果设置了 more_body = True,则将其视为 body chunk 链的一部分,并与后续的 chunks 进行关联
more_body Bool 标志着是否还有额外的 body 内容,如果是 True 表示还有 body 内容,ASGI Application 需要等待,知道有一个为 False 的 chunk 到达

Response Start - send event

由 ASGI Application 发送给 ASGI Server,用于标识开始向 Web Client 发送响应。在此之后需要紧跟至少一个 response content message。ASGI Server 在接收到至少一个 Response Body 之前不得向 Web Client 发送响应。

ASGI Application 可能会在消息中发送一个 Transfer-Encoding header,但是 ASGI Server 必须忽略它。ASGI Server 需要自己处理 Transfer-Encoding,如果应用程序呈现的响应没有设置 Content-Length,可以选择使用 Transfer-Encoding: chunked

Response Start Send Event Message 包含:

名称 类型 描述
type Unicode String “http.response.start”
status int HTTP 状态码
headers Iterable[[byte string, byte string]] 一个 [name, vlaue] 迭代器,必须与 HTTP Response 中的顺序一致且 Header names 都必须是小写的。默认是 []。不能存在 Pseudo headers(HTTP/2 和 HTTP/3 中的)

Response Body - send event

由 ASGI Application 发送给 ASGI Server,用于继续向 Web Client 发送响应。ASGI Server 必须在 send 返回前将传递给它的全部数据传输到发送缓冲区。如果 more_body 被设置为 False,这个 Connection 将被关闭。

Response Body Send Event Message 包含:

名称 类型 描述
type Unicode String “http.response.body”
body byte string
more_body bool

Disconnect - receive event

由 ASGI Server 发送给 ASIG Application,在 HTTP Connection 关闭或在响应被发送后调用。主要适用于长轮询(long-polling),如果连接被提前关闭,希望触发某些清理代码时。

Disconnect Receive Event Message 包含:

名称 类型 描述
type Unicode String “http.disconnect”

WebSocket Protocol

WebSockets 与 HTTP 存在某些一致的细节:它们都有 path 和 headers,但是也有一些独特的状态。同样,大部分状态都在 Scope 中,只要 socket 存在它们就会一直存在。

WebSocket 协议服务器(后简称:ASGI Server)应该自行处理 PING/PONG 消息,并在必要时发送 PING 消息以确保 Connection 是有活性的。

ASGI Server 应该自行处理 message fragmentation,并且将完整的消息传递给 ASGI Application。

ASGI Server 需要在向 ASGI Application 发出请求时通过一个值为 websocket 的 tpye 字段表明使用的协议。

WebSocket Connection Scope

WebSocket Connection Scope 应该与 socket 共存,如果连接中断,socket 应该被同时关闭,反之亦然。

WebSocket Connection Scope Message 包含:

名称 类型 描述
type Unicode String “websocket”
asgi[“version”] Unicode String 使用的 ASGI 规范版本
asgi[“spec_version”] Unicode String Server 支持的 ASGI HTTP 规范版本,可选值:”2.0”,”2.1”,”2.2” 或 “2.3”,默认”2.0”
http_version Unicode String “1.1” 或 “2” 默认是 “1.1”
scheme Unicode String URL 协议,可选值:”ws” 或 “wss”,默认为 “ws”
path Unicode string HTTP 请求目标,不包括任何查询字符串,由百分号编码和 UTF-8 字节序解码成字符
raw_path byte string 原始的未被修改的 HTTP path,来自 Web Server,可能缺省,默认为 None
query_string byte string url 中 ? 后的部分,百分号编码
root_path Unicode string Application 被绑定到的根路径,和 WSGI 中的 SCRIIPT_PATH 一致,默认为 “”
headers Iterable[[byte string, byte string]] 一个由 [name, value] 两个子项组成的可迭代对象,name 应尽可能是小写
client Iterable[Unicode string, int] 一个 [host, port] 可迭代对象,默认为 None
server Iterable[Unicode string, Optional[int]] 可以是 [host, port] 可迭代对象,也可以是 [path, None],其中 path 是 unix 套接字路径,缺省为 None
subprotocols Iterable[Unicode string] 客户端公布的子协议,默认是 []

Connect - receive event

此事件是在 Web Client 打开一个 connection 并即将完成 WebSocket 握手的时候,由 ASGI Server 发送给 ASGI Application 的。

本事件的消息体必须被一个 Accept 事件消息或一个 Close 事件消息响应,在对应 socket 将要传递 websocket.receive 事件消息之前。ASGI Server 必须在 WebSocket 握手阶段发送本事件消息,并且在得到回复之前不能完成握手,如果 connection 被拒绝,则返回 HTTP 403。

Connect Receive Event Message 包括:

名称 类型 描述
type Unicode String “websocket.connect”

Accept - send event

此事件是在 ASGI Application 期望接受一个 incoming connection 时由 ASGI Application 向 ASGI Server 发送的。

Accept Send Event Message 包括:

名称 类型 描述
type Unicode String “websocket.accept”
subprotocol Unicode String ASGI Server 期望接受的子协议,可选值,默认为 None
headers Iterable[[byte string, byte string]] 一个 [name, value] 的可迭代对象。更多描述请看原文。

Receive - receive event

此事件是在收到来自 Web Client 的数据消息时由 ASGI Server 发送给 ASGI Application 的。

Receive Event Message 包括:

名称 类型 描述
type Unicode String “websocket.receive”
bytes Byte String message content,binary 模式,可选项,默认为 None
text Unicode String message content, text 模式,可选项,默认为 None

bytes 或 text 至少要存在一个,也可以两个都存在。

Send - send event

由 ASGI Application 发送给 ASGI Server,为 Web Client 发送一条数据信息。

Send Event Message 包括:

名称 类型 描述
type Unicode String “websocket.send”
bytes Byte String 同上
text Unicode String 同上

bytes 或 text 至少要存在一个,也可以两个都存在。

Disconnect - reveive event

此事件是在任何一个与 Web Client 的链接断开时(包括 Web Client 关闭连接、ASGI Server 关闭连接或 socket 丢失)由 ASGI Server 发送给 ASGI Application 的。

Disconnect Reveive Event Message 包括:

名称 类型 描述
type Unicode String “websocket.disconnect”
code int websocket close code

Close - send event

由 ASGI Application 发送给 ASGI Server 告知 connection 关闭。

如果在 socket 被接受之前发送,ASGI Server 必须以 HTTP 403 错误代码关闭 connection,并且不完成 WebSocket 握手,这在某些浏览器上可能表现为不同的 WebSocket 错误代码(例如 1006,异常关闭)。

如果在 socket 被接受后发送,ASGI Server 必须通过传递 message 关闭 socket(默认 code 是 1000)。

Close Send Event Message 包括:

名称 类型 描述
type Unicode String “websocket.close”
code int websocket close code,可选项,默认为 1000
reason Unicode String close 原因,可选项,默认为空字符串

WSGI Compatibility | WSGI 兼容性

HTTP 子协议的设计有一部分是为了确保它能够与 WSGI 规范保持一致,以降低兼容两种规范的难度,并且使得 ASGI Server 搭配 WSGI Application 成为可能。

WSGI Application 是同步的,必须运行在线程池中才能被驱动,否则它的 runtime 就会映射到 HTTP Connection Scope 的 lifetime 上。

WSGI 的 environ 变量中的各种特殊 key 几乎都可以直接映射到 HTTP Connection Scope 上:

WSGI environ ASGI HTTP Scope
REQUEST_METHOD method
SCRIPT_NAME root_path
PATH_INFO 从 root 中剥离 root_path 获得
QUERY_STRING query_string
CONTENT_TYPE 从 headers 中剥离
CONTENT_LENGTH 从 headers 中剥离
SERVER_NAME and SERVER_PORT server
REMOTE_HOST/REMOTE_ADDR and REMOTE_PORT client
SERVER_PROTOCOL http_version
wsgi.url_scheme scheme
wsgi.input 一个基于 http.request 的 StringIO
wsgi.errors directed by the wrapper as needed

WSGI 中的 start_response 可调用对象与 http.response.start 类似:

  • status argument 变成了 status,并且去掉了 reason phrase。
  • response_headers 映射到了 headers 上。

WSGI start_response

从 WSGI Application 中产生的内容映射到了 http.response.body 的 message 中。

WSGI encoding differences | WSGI 编码异同

WSGI 规范(如 PEP 3333 所定义)规定:所有发送或来自 WSGI Server 的 strings 必须是 str 类型,但值包括 ISO-8859-1(“lantin-1”) 范围内的 codepoints。这是因为它最迟是为 Python2 以及不同的 set of string types 设计的。

ASGI 的 HTTP 和 WebSocket 子规范将 Scope 字典的每个条目指定为 byte string 或 Unicode String 的其中一种。HTTP 作为一个早期协议,在编码制定上存在一些不完善的地方,所以在何处使用 Unicode 何处使用 byte 并没有明确的说明。

  • path:URL 可以同时拥有 percent-encoded 或 UTF-8 编码。这部分的解码行为通常是在底层服务器或传播过程中的服务器进行的,因此这个字段被设置成了 Unicode String,由 UTF-8 和 percent 编码同时构成。
  • headers:这些是由 Web Client 或 Server 发送的确切字节序的字节字符串。虽然现在的 HTTP 标准规定 headers 应该是 ASCII 码,但是旧的标准并没有规定,而是允许尽可能多的编码选择。Framework 或 Application 应该按照他们认为合适的方式进行 headers 的解码。
  • query_string:不像 path,它不会收到 Server 的干扰,因此使用 raw byte sring 呈现,使用 percent-encode 编码。
  • root_path:为了与 path 匹配同样使用 Unicode String。

Lifespan Protocol

Lifesapn ASGI 子规范概括了如何在 ASGI Application 中传递像 startup 或 shutdown 之类的 lifespan events。

Lifespan message 允许 ASGI Application 在一个运行中的事件循环的上下文中初始化或停止。这方面的一个例子是创建一个连接池,随后关闭连接池释放连接。

Lifespan 应该在处理请求的每个事件循环中执行一次。在多进程环境中,每个进程都会有 Lifespan event。重要的是,lifespan 和 request 是在同一个事件循环中运行的,以确保像数据库连接池这样的对象不会在循环中被移除或共享。

一个 lifespan 实现举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async def app(scope, receive, send):
if scope["type"] == "lifespan":
while True:
message = await receive()
if message["type"] == "lifespan.startup":
# Do some startup here
await send({"type": "lifespan.startup.complete"})
elif message["type"] == "lifespan.shutdown":
# Do some shutdown here
await send({"type": "lifespan.shutdown.complete"})
return
else:
# Handle other
pass

Scope

Lifespan Scope 会持续存在直到事件循环结束。

Lifespan Scope Message 包含:

名称 类型 描述
type Unicode String “lifespan”
asgi[“version”] Unicode String ASGI 协议版本
asgi[“spec_version”] Unicode String 子协议版本,默认 “1.0”

如果在调用带有 lifespan.startup 消息的 Application 或处理 type 是 lifespan 的 Scope 时抛出了异常,ASGI Server 需要继续执行但不 send any lifespan events。

这允许兼容不支持 lifespan 的 ASGI Application。如果需要记录在 lifespan 启动过程中发生的错误并阻止 ASGI Server 的启动过程,可以通过发送 lifespan.startup.filed 来实现。

Startup - reveive event

此事件是在 ASGI Server 准备好处理 startup 和 receive connection 但还没开始处理的时候,由 ASGI Server 发送给 ASGI Application 的。

Startup Receive Event Message 包括:

名称 类型 描述
type Unicode String “lifespan.startup”

Startup Complete - send event

此事件是在 ASGI Application 处理完 startup 后,由 ASGI Application 发送给 ASGI Server 的。ASGI Server 在开始处理 connection 之前必须等待这个事件

Startup Complete Send Event Message 包括:

名称 类型 描述
type Unicode String “lifespan.startup.complete”

Startup Failed - send event

此事件是在 ASGI Application 未能完成 startup 时,由 ASGI Application 发送给 ASGI Server 的。ASGI Server 应该在接收到事件消息后记录或打印消息所提供的内容,然后退出。

Startup Failed Send Event Message 包括:

名称 类型 描述
type Unicode String “lifespan.startup.failed”
message Unicode String 默认为 “”

Shutdown - reveive event

此事件是在 ASGI Server 停止接受 connection 并关闭所有正在处理的 connection 后,由 ASGI Server 发送给 ASGI Application 的。

Shutdown Reveive Event 包括:

名称 类型 描述
type Unicode String “lifespan.shutdown”

Shutdown Complete - send event

此事件是在 ASGI Application 完成 cleanup 之后,由 ASGI Application 发送给 ASGI Server 的。ASGI Server 在终止前必须等待此事件消息。

名称 类型 描述
type Unicode String “lifespan.shutdown.complete”

Shutdown Failed - send event

此事件是在 ASGI Application 未能正常处理 cleanup 之后,由 ASGI Application 发送给 ASGI Server 的。ASGI Server 应该在接收到事件消息后记录或打印消息所提供的内容,然后退出。

名称 类型 描述
type Unicode String “lifespan.shutdown.failed”
message Unicode String 默认 “”

ASGI TLS Extension

本规范概述了如何在 ASGI connection scope 对象中传递 TLS(或 SSL)connection 信息。

The Base Protocol | 基础协议

TSL 是无法单独使用的,它总是包裹着另一个协议。因此,此规范并非用来规定如何单独使用 TSL 的,它必须作为一个其他 ASGI 子协议的拓展来使用。与 TSL 搭配的其他 ASGI 子协议被称为基础协议。

对于 HTTP-over-TLS(HTTPS) 来说,需要联合使用 TLS 子规范和 ASGI HTTP 子规范,其中基础协议是 ASGI HTTP 子规范。

对于 WebSockets-over-TLS(wss:// protocol) 来说,需要联合使用 TLS 子规范和 ASGI WebSocket 子规范,其中基础协议是 ASGI WebSocket 子规范。

在搭配基础协议使用此拓展协议时需要注意:基础协议中必须定义 Connection Scope 以确保它最多包含一个 TLS 连接,否则,就不能使用此拓展协议。

When to use this extension

此拓展仅限于在 TLS Connection 中使用。

对于非 TLS Connection,ASGI Server 禁止提供此拓展。

ASGI Application 可以在 Connection Scope 的 extensions 字典中检查是否存在 tls 拓展。如果存在,说明 ASGI Server 支持此拓展,并且 Connection 是建立在 TLS 上的。如果不存在,说明 ASGI Server 不支持这个拓展或 Connection 不是建立在 TLS 上的。

TLS Connection Scope

基础协议的 Connection Scope 中包含一个 extensions 键值对,它的值是一个字典。在该字典中的 tls 键值即是 TLS Connection Scope:

名称 类型 描述
server_cert Unicode String or None ASGI Server 在建立 TLS 连接时发送的 x509 证书的 PEM 编码版本。一些 ASGI Server 的实现可能无法提供这一点(例如,如果 TLS 是由一个单独的代理或负载平衡服务器终止的),在这种情况下应该是 None。必须存在的。
client_cert_chain Iterable[Unicode string] 每个字符串都是一个 PEM 编码的 x509 证书。第一个证书是客户端证书。任何后续证书都是客户端发送的证书链的一部分,每个证书都会签署前面的证书。如果客户端没有提供证书,那么它就是一个空的可迭代对象。可选的,如果缺失默认为空的可迭代对象。
client_cert_name Unicode String or None 客户端证书主题的 x509 标识名(DN),按照 RFC 4514 中定义进行编码的单一字符串。如果客户端没有提供证书,则为 None。如果 client_cert_chain 被提供且不是空可迭代对象,那么这个字段必须要被提供,并且必须包含与 client_cert_chain[0] 一致的信息。可选的,如缺失默认为 None。
client_cert_error Unicode String or None 如果提供了客户端证书并验证成功,或没有提供客户端证书都为 None。如果提供了客户端证书但验证失败,则是一个非空字符串,包含一个错误信息或错误代码,表明验证失败的原因。大多数 Web Server 会在客户端证书验证失败后直接拒绝而非设置这个值。可选的,默认为 None。
tls_version Integer or None 正在使用的 TLS 版本。这是在 TLS 规范中定义的版本号之一,是一个无符号整数。常见值包括:0x0303 for TLS 1.2 或 0x0304 for TLS 1.3,如果 TLS 没有被使用则设置为 None。必须存在。
cipher_suite Integer or None 正在使用的 TLS cipher suite。一个 16 位无符号证书,按照网络字节序对相关 RFC 中规定的一对 8 位证书进行编码。一些 Web Server 无法提供这个功能,这种情况下,设置为 None。必须存在。

Events

所有的 Event 都是基于基础协议的。

Rational(Informative)

已有实现

Server

Daphne

Stable / http://github.com/django/daphne

当前 ASGI 的参考 Server 实现(亲儿子),使用 Twisted 编写,作为 Django Channel 项目组的一部分。支持 HTTP/1, HTTP/2 和 WebSockets。

Uvicorn

Stable / https://www.uvicorn.org/

一个基于 uvloophttptools 的 ASGI Server。支持 HTTP/1 和 WebSockets。

Hypercorn

Beta / https://pgjones.gitlab.io/hypercorn/index.html

一个基于 sans-io hyper, h11, h2wsproto 库的 ASGI Server。支持 HTTP/1, HTTP/2 和 WebSockets。

Application Frameworks

Django/Channels

Stable / http://channels.readthedocs.io

Channels 是 Django 项目的一部分,旨在为 Django 提供异步支持能力,是 ASGI 项目的发起者。为 Django 整合了处理 HTTP,WebSocket 以及任何满足 ASGI-native 代码实现协议的能力。

FastAPI

Beta / https://github.com/tiangolo/fastapi

FastPI 是一个在 Starlette 框架基础上进一步封装的 ASGI Web 框架,它整合了标准 Python 类型注释、OpenAPI、JSON Schema、OAuth 等特性。支持 HTTP 和 WebSocket 协议。

Quart

Beta / https://github.com/pgjones/quart

Quart 是一个 Python ASGI 微框架。它专注于使用最简单的 asyncio 特性为 Web 应用提供异步能力,常被用在 Flask apps 中。支持 HTTP 协议。

Sanic

Beta / https://sanicframework.org

Sanic 是一个灵活的框架,它既能够作为 ASGI Server 使用也能够作为 ASGI Application 使用。支持 HTTP 和 WebSockets 协议。

Starlette

Beta / https://github.com/encode/starlette

Starlette 是一个提供了编写基础但强大请求或响应对象的极简的 ASGI 库。支持 HTTP 和 WebSockets 协议。

rpc.py

Beta / https://github.com/abersheeran/rpc.py

一个易于使用的强大的 RPC 框架。RPC Server 基于 WSGI 和 ASGI,Client 基于 httpx。支持同步、异步、同步生成器和异步生成器,并提供可选的类型注释和 OpenAPI 文档生成特性。

Tools

a2wsgi

Stable / https://github.com/abersheeran/a2wsgi

将一个 WSGI Application 转换为一个 ASGI Application 的工具。纯 Python 实现,仅依赖于原生库。

后记

我在整理 已有实现章节 的时候发现,encode 这个 Github Group 有点强,Django-REST-Framework, Starlette, Uvicorn, httpx 都是出自这一个组织。

参考

  1. ASGI Documentation