注意:该文档依然在更新中,在开发过程中可能发生较大变化。
- 摘要
- 依据
- 总览
- 频道和消息
- 处理协议
- 扩展
- 群组
- 线性化
- 容量
- 规范详述
- 频道语义
- 持久化
- 消息格式
- HTTP
- 请求
- 请求体块(Request Body Chunk)
- 响应
- 响应块(Response Chunk)
- 服务器推送
- 断开连接
- WebSocket
- 建立连接(Connection)
- 接收消息(Receive)
- 断开连接 (Disconnection)
- 发送、关闭
- 协议格式指南
- 近似全局排序
- 字符串和 Unicode
- WSGI 兼容性
- 可能要做的
- References
摘要
该文档旨在描述一个介于网络协议服务和 Python 应用之间的标准接口,能够处理多种通用协议类型,包括 HTTP、HTTP2 和 WebSocket。
它的目的是在 WSGI 上进行拓展,并最终取代它。在设计上还是包含了 WSGI 到 ASGI 以及 ASGI 到 WSGI 的转换器,目的是为了使 HTTP 协议的编写更为容易。
依据
WSGI 规范自诞生以来应用广泛,在作为 Python 框架和 web 服务的选择上拥有非常好的灵活性。但,因为是针对 HTTP 风格的请求响应模型做的设计,加上越来越多不遵循这种模式的协议逐渐成为 web 编程的标准之一,比如说,WebSocket。所以需要新的改变。
ASGI 尝试保持在一个简单的应用接口的前提下,提供允许数据能够在任意时候、被任意应用进程发送和接受的抽象。
它同样描绘了一个新的,兼容 HTTP 请求响应以及 WebSocket 数据帧的序列格式。允许这些协议能通过网络或本地 socket 进行传输,以及让不同的协议被分配到不同的进程进行处理。
总览
ASGI 由三个不同的组件构成:协议服务、频道层(channel layer)、应用代码。频道层是这个实现中最重要的部分,它能同时对协议服务和应用提供接口。
一个频道层对协议服务、应用服务提供一个 send
的可调用方法,该方法接受 channel name
、message dict
以及一个 receive_many
方法作为参数。receive_many
方法接受 channel name
的 list
作为参数,返回指定频道的下一条可用的消息。
所以,相较于在 WSGI 上,我们将协议服务直接指向应用,在 ASGI 里,我们将协议服务和应用同时指向一个频道层的实例。它的目的是让应用服务和协议服务总是运行在不同的进程或者线程中,并通过频道层进行通信。
尽管提议的名字中含有『异步』二字,但 ASGI 并不是只针对异步的解决方案,例如 asyncio
、twisted
或者 gevent
。相反,receive_many
方法能在同步和异步之间切换。这种方式使得应用可以选择适合他们当前运行环境的最佳方案。后续的改进可以提供拥有协作版本(cooperative versions)的 receive_many
方法的拓展。
该文档中对协议服务和应用的区分主要是为了明晰他们要扮演的角色,同时也为了更便于描述概念。这两者之间并没有代码层面的区分,而且完全有可能发生一个进程既是协议服务又是应用的情况。最终大多数的实际操作将遵循这种模式也是期望之中的。
其实是可以增加一个 WSGI 类型的抽象,提供一个接受 channel, message, send_func
参数的方法。但这样在许多的场景里是很受限制的,同时也没有覆盖到如何指定监听的频道名称。这一块就交由框架来实现吧。
频道和消息
在 ASGI 栈里面的所有通信都是通过在频道里发送消息进行的。所有的消息必须是一个 dict
,并且为了保证可序列化,只允许包含以下类型数据:
- 字节串
- Unicode
- 整型(非
long
) - 列表
- 字典(键必须是 Unicode)
- 布尔
- None
频道的 ID 只能由 ASCII 字母、数字及.
、-
、_
,以及一个可选的前缀字符构成(见下文),注意,得是 unicode 类型的。
频道是一个先进先出队列,队列里的项最多被传输一次。它允许多位写入者和多位读取者,当仅有一位读取者时,需要读取每一个写入的消息。实现绝对不能将一条消息传输多次或传输给一位以上读取者,为了保证这一限制,必要时必须清空(drop)所有信息。
为了能够支持可拓展的网络架构,多端读取的频道(例如 http.request
频道,监听每一个应用进程)和单一读取的频道(例如一个 http.response!ABCDE
频道,绑定在一个终端 socket 上)会区别处理。
单端读取频道(Single-reader channel) ,命名中需要包含一个感叹号(!),这么做可以让频道层知道它要将数据通过不同的方式路由给这些频道,以保证数据能到达需要它的痛的奥;这些频道几乎总是与外界来的连接进行绑定。感叹号(!
)总是跟在主频道名称(如 `http.response`)后面,后面跟着每个终端/随机的片段。频道层可以通过取感叹号后面的部分进行路由,如果需要的话,也可以选择忽略,如果它们不需要使用不同的路由规则。
信息如果在一个频道里超过设定时间未读会被过期掉。这个设定时间推荐为一分钟,当然最佳的设定还是取决于频道层和它部署的方式。
消息的大小最大限定为 1 MB;如果要传输的数据大于这个上限,则需要分块(参见 HTTP 请求的内容体处理方式)。所有的频道层都必须遵循此限制。
处理协议
ASGI 信息主要是两个东西,内部应用事件(例如,一个频道可能被用作将之前上传的视频的缩略图存储如队列),以及来自或发往已连接的终端的协议事件。
如此,这份规范概括性地描述了 ASGI 消息是如何对 HTTP 和 WebSocket 进行编码的。这允许 ASGI web 服务器能够和其他 ASGI web 应用进行通信,同理,其他基于共同协议规范实现的服务器和应用也在此列。建议其他变得普遍的协议也应该为自己增补一个标准格式。
消息格式是这份规范的关键,没有它们,协议服务(protocol server)和 web 应用虽然可以进行通信,但不能理解通信的内容。从这个意义上说,消息格式等同于 WSGI 中的 environ
字典里的键(keys)。
设计模式是,大多数协议将共享一些频道,用于接收数据(例如,http.request
, websocket.connect
和 websocket.receive
),但将用独立的频道进行发送数据给每一个终端(例如 http.response!kj2daj23
)。这允许发送来的数据被分发到一个应用服务的集群,于此同时,响应消息(responses)被路由给持有终端的 socket 的单独的协议服务。
有一些协议没有唯一 socket 连接的概念,例如,一个短信网关协议服务可能只有 sms.receive
和 sms.send
,协议服务集群将从 sms.send
获取信息,然后根据信息里的属性(例如电话号码)将他们路由给电话网络。
扩展
对于一些基础的应用代码和几乎全部的协议服务代码不必须的功能,放进了拓展里,这让一些不需要全部特性的应用可以有轻型的频道层。
这里定义了三种拓展:groups
拓展,下面会详细说明;flush
拓展,让测试和开发更加容易;statistics
拓展,为频道层提供全局的和单频道的统计功能。
有可能会增加更多拓展,这些可能会用一份单独的规范进行说明,也可能在本规范的更新版本里增加。
如果应用代码需要一个拓展,它应该尽快对它进行检查,如果没有提供就报错。框架应该鼓励对拓展的可选使用,应尝试在进程启动时进行『拓展没找到』错误的处理,而不应该留到消息处理时。
群组
虽然基础频道模型已足够处理基础应用需求,很多异步消息传递的高级用法需要在一个事件发生的时候立即通知很多用户。例如,对于一个实时博客来说,发表一个条新的消息时,每一个用户都应该立即收到一个长轮询响应或 WebSocket 包。
这个概念本来没什么必要在 ASGI 规范中进行阐述, 之所以涵盖它是为了让频道层的实现有一个大的性能提升——在应对群组发送这样的场景上。可选的方案是 send_many
方法可能需要传入数以万计的目标频道名称参数。然而,群组特性依然是可选的,它以频道层的 supports_groups
属性的形式存在着。
所以,在 ASGI 里有一个简单的群组概念,主要起到在频道间进行广播、多点传送的作用。多个频道可加入一个群组,发往这个群组的消息将发送给这个群组里的所有频道。频道可以从群组里手动移除(例如,基于一个断开连接的事件),同时,频道层也会周期性地将群组里的『老』频道进行垃圾回收。
因为垃圾回收取决于频道层的内部实现,这里就不对其进行说明了。推荐的做法是,当一个指定进程的频道内的消息过期了,频道层就把该频道从所有所在的群组里移除,因为这完全可以断定这个频道的收听者(译者注:绑定的那个进程)消失了,也就没有继续在群组里呆着的必要了。
群组功能的实现是可选的。如果协议服务需要它而它并未被提供,则需要进行报错并退出运行。理论上讲,协议服务不应该需要用到群组。
线性化
ASGI 的设计是为了达成一个无共享架构(Shared Nothing Architecture)。消息可以被任意一个组运行着应用代码的线程、进程或机器进行处理。
这,当然,意味着一个应用的不同拷贝可以同时处理消息,那些消息甚至是来自同一个终端的,最差情况下,一个终端的两个数据包可以被乱序处理,如果其中一个服务慢于另一个的话。
这对 WSGI 这类规范来说也同样是问题——一个用户可以同时对同一个网站打开两个标签页,以及对不同的服务同时发起请求——但这里讨论的新规范(ASGI)的性质决定了,冲突将更容易产生。
如何解决这个问题将留给框架和应用,当然,外界也已经有了类似数据库事务的解决方案,而且,绝大多数的应用是不需要处理这种问题的。如果一个协议真的要求进来的数据包是按序的,这些数据包应该被标上包序号(WebSocket 就是这么干的)。
单接收者和指定进程的频道,像那些被用作响应终端的频道,并不会被这个问题所扰单个接收者肯定总是根据频道进来的消息顺序进行消息处理呀。
容量
为了提供背压,频道层里的每一个频道都得有一个容量上限,定义着这个层期望的值。(推荐做法是由用户通过传入关键词参数给频道层的构造方法进行制定,此外,如果想对单个频道进行指定,传入频道名称或名称前缀。)
频道达到或超出容量上限,尝试对该频道调用 send()
将抛出 ChannelFull
异常。接下来发送方该怎么处理这个异常取决于其所在的上下文。举个例子,一个尝试发送响应体的 Web 应用程序很可能是要等待频道腾出空间再进行发送;而一个尝试发送请求的 HTTP 接口服务会直接丢弃掉请求并返回一个 503 错误。
对一个群组发送消息永远不会引发 ChannelFull
异常,相反,遵循 ASGI 最多传递一次的策略,如果达到容量上限,它会悄悄地丢弃掉消息。
规范详述
频道层(channel layer)必须提供拥有以下属性的对象(所有方法参数都是可选的):
-
send(channel, message)
,该方法接受两个参数:channel
频道是发送对象,unicode string 类型,message
消息,是一个序列化的dict
。 -
receive_many(channels, block=False)
,该方法接受一个由 unicode string 类型的频道名称组成的列表作为参数,如果有消息,返回(channel, message)
,否则返回(None, None)
。如果block
为True
,它将等到一个内置超时器超时或者一个消息到来才返回;如果block
为False
,它将立即返回。忽略block
而直接返回或者等待一会再返回,都是合理的;block
意味着这个调用可以在返回之前花任意久的时间,并非是说它必须得等到一条消息才返回。 -
new_channel(pattern)
,该方法接受一个 unicode string 类型的 pattern,返回一个新的(也即不重复)有效的频道名称。名称的构造方式是在pattern
里的!
或?
字符后面添加一个 unicode string 类型的字符串,并且检查该名称在当前频道层里是否已经存在。pattern
必须以!
或?
结尾,否则这个方法一定会报错。如果是!
,则意味着它是一个指定进程的频道,new_channel
只能在通过receive_many
进行消息读取的频道层里被调用。也因此,其他频道层在该频道是无法接收消息的。 -
MessageTooLarge
,当一个发送操作因编码后的消息体积超出频道的大小限制而失败时,将抛出该异常。 -
ChannelFull
,当一个发送操作因目标频道超出容量上限而失败时,将抛出该异常。 -
extensions
,一个 unicode string 类型的列表,装着该层提供的所有扩展,如果不提供扩展,则为空。在这份文档里定义的名称包括groups
、flush
和statistics
。
实现了 groups
扩展的频道层需要另外再提供:
-
group_add(group, channel)
,该方法接受channel
作为一个频道,将它添加进group
群组里。这两个参数都是 unicode string 类型。如果该频道已经在群组里,该方法也会正常返回。 -
group_discard(group, channel)
,该方法将channel
频道从group
群组里移除。如果该频道不再群组内,则不做任何操作。 -
send_group(group, message)
,该方法接受两个可选参数:unicode string 类型的群组,和序列化的dict
类型的消息。该方法可能会抛出MessageTooLarge
异常,但不会抛出ChannelFull
异常。 -
group_expiry
,一个整数类型的单位为秒的时间变量,规定了在group_add
调用后,群组关系将会维系多久。(详见下面 持久化)
实现了 statistics
扩展的频道层需要另外再提供:
-
global_statistics()
, 该方法返回一个由零或多个 unicode string 类型的键组成的dict
:-
count
,当前所有频道内等待着的消息总数
-
-
channel_statistics(channel)
,该方法返回一个由零或多个 unicode string 类型的键组成的dict
:-
length
,当前频道内等待着的消息总数 -
age
,频道内的消息等待最久的时间,单位为秒 -
per_second
,过去一秒钟内处理的消息数量
-
实现了 flush
扩展的频道层需要另外再提供:
-
flush()
,该方法将频道层重置为初始状态(没有消息也没有群组(如果群组扩展实现了的话))。该方法必须是阻塞的,直到系统被清理完成。而且当频道层被分发,它对所有终端来说都应该是空的。
频道语义
频道必须满足:
- 如果是单接收者或指定进程的频道,则通过一个单接收者和写入者完美维持消息的顺序。
- 永不二次传递一条消息。
- 消息发送永不阻塞(虽然可能会引发
ChannelFull
或MessageTooLarge
异常)。 - 当编码是 JSON 时,能处理至少 1MB 大小的消息。(实现上可以使用更好的编码方法或压缩方式,只要能达到大小上的要求)
- 最大名称长度至少是 100 字节。
它们应该尽可能地在所有情况下保持有序,但在分布式情况下,完美的全局有序显然是不可能的。
它们需要保证在正常情况下至少 99.99% 的消息传递成功率。实现上最好是有一个『弹性测试』模式,在这个模式下故意丢失更多的消息,让开发者能测试他们的代码能否应对这种情况。
持久化
频道层不需要长期保留数据;群组关系只需要持续一个连接的长度即可,消息有一个过期时间,通常来说是几分钟。
就是说,如果一个频道服务器瞬间宕机失去了所有的数据,持续的 socket 连接将继续传递进来的数据并发送新的数据,但将失去它们所有的群组关系和进行中的信息。
为了避免这些因半路中断的 socket 引起的一系列令人讨厌的错误,如果协议服务发现频道层已经不工作或丢失了数据,它们应退出并重新启动,断开所有现有的连接,并让客户端重新连接以解决问题。
如果频道层实现了 groups
扩展,它必须维持群组关系,直到成员频道有一个因未被消费而过期了的消息,在这之后,它将在任意时候丢弃群组关系。如果一个频道紧接着有了一个成功的消息传递,频道层则需在另一条消息过期之前,都不能丢弃群组关系。
群组关系有一个可配置的超时,默认值是 86400 秒(一天),在超时后,频道层必须要丢弃掉群组关系。频道层的 group_expiry
属性就是群组关系的超时时间。
协议服务必须对对每一个基于连接的协议指定一个可配置的超时时间,超时后即断开连接。如果频道层提供了 group_expiry
,则默认值为 group_expiry
。这使得旧的群组关系得以安全的清理——群组到期后,原有连接一定都已经或在接下来几秒钟内断开。
建议终端开发者将超时设置的更低一点,数小时或数分钟,为了更好的协议设计和测试。即时 ASGI 对业务逻辑重启和协议服务重启做了分离,你也应该迁移协议服务。确保你的代码能够应对这种情况是十分重要的。
消息格式
这里描述该规范支持的协议的标准消息格式。所有的消息都是 dicts
,所有的键都是必须的,除非另有定义(如果键未指定则使用默认),键是 unicode string 类型。
所有协议都通用的键是 reply_channel
,一种表示客户端特定的频道发送响应的方法。一般鼓励协议有一个消息类型和一个应答频道类型以确保排序。
对每一个连接来说,reply_channel
应该是唯一的。如果一个协议能由任意一个服务发送一个响应,例如,一个理论上的 SMS 协议,它不应将 reply_channel
作为消息的属性,而应作为一个单独的顶级输出频道的属性。
消息将和它们期望的频道通过名称进行绑定。如果一个频道名称可以变化,例如响应频道,变化的部分将由 !
字符呈现,如 http.response!
,这也符合 new_channel
方法的参数格式。
并没有一个标签可以告知消息类型,消息类型可以从接收它们的频道名称中看出来。发送给同一个频道的两种消息,例如 HTTP 响应和响应块(chunks),要靠它们包含的字段进行区分。
HTTP
HTTP 协议包含 HTTP/1.0, HTTP/1.1 和 HTTP/2,HTTP/2 中最大的变化是对传输层的改进。协议服务对同一连接上的不同请求赋予不同的回复频道,并将这些响应正确地复合回去。HTTP 版本可以在请求消息里找到。
HTTP/2 服务器推送响应是包含在内的,但必须先于主响应之前被发送,并且应用们在发送他们之前必须检查 http_version = 2;如果协议服务器或没有服务器推送能力的连接接收到这些,就必须丢弃掉它们。
HTTP 中的同名多头字段是挺复杂的。 RFC 7230 规定,对于可以多次出现的头字段,将该头字段的所有值以逗号拼接起来一次性发送是完全等效的。
然而,RFC 7230 和 RFC 6265 清楚地声明,这条规则并不适用于 HTTP Cookies(Cookie
和 Set-Cookie
)使用的各种头。Cookie
头只能由用户代理发送一次,但是 Set-Cookie
头则可能出现多次,且不能用逗号拼接。基于这一点,我们可以将请求 headers
声明为一个 dict
类型,但响应 headers
必须以元祖列表发送,按照 WSGI 的要求。
请求
对于进入协议服务器的每个请求触发一次。
频道:http.request
键:
-
reply_channel
:响应和服务器推送的频道名称,以http.response!
开头。 -
http_version
: Unicode string 类型,1.0
、1.1
或2
。 -
method
: Unicode string 类型的 HTTP 方法名,全部大写。 -
scheme
: Unicode string 类型的 URL 协议名称(例如http
或https
)可选(但不可以是空值)默认是"http"
。 -
path
: Unicode string 类型的 HTTP URL 请求路径,用百分号转义编码和 UTF-8 字节序列解码成字符。 -
query_string
: Unicode string 类型的 URL 中?
后边的部分,和path
一样已经解码过,可选,默认是""
。 -
root_path
: Unicode string 类型,指定应用所在的根路径,和 WSGI 中 的SCRIPT_NAME
一致,可选,默认为""
。 -
headers
:[name, value]
列表,name
是字节串类型的头名称,value
是字节串类型的头值。 顺序应该从原始的 HTTP 请求被保留,可能会有重复,而且必须保留在接收到的消息中。头名称必须小写。 -
body
: 字节串类型的请求体,可选,默认是""
。如果设置了more_body
,将作为请求体的开头,用于拼接接下来的数据块。 -
more_body
: 包含请求体块消息(Request Body Chunk)的单接收者的名称(含?
)。可选,默认是None
。数据块将拼接到body
后边,如果body
设置了的话。频道的出现意味着至少有一条请求体块消息待阅,在那些消息里,由more_content
键储存着更多的待消费的内容。 -
client
:[host, port]
列表,host
是 unicode string 类型的远程主机的 IPv4 或 IPv6 地址,port
整数类型的主机端口。可选,默认是None
。 -
server
:[host, port]
列表,host
是 unicode string 类型的服务器监听地址,port
是整数类型的端口。可选,默认是None
。
请求体块(Request Body Chunk)
必须在响应初始化后触发。
频道:http.request.body?
键:
-
content
:字节串类型的 HTTP 内容体,将被拼接到之前接收到的请求里面的content
和body
。如果closed
为 False,则该键必须要有。 -
closed
:如果客户端过早断开连接,则为 True。如果接收到该值,则丢弃掉 HTTP 请求的处理。可选,默认是False
。 -
more_content
:布尔类型,用于表示接下来是否有更多的内容要进来(作为请求体块消息的一部分)。如果为False
,请求将被视作完成,当前频道上的其余消息将被忽略。可选,默认是False
。
响应
在服务器推送后、响应块(Response Chunk)之前触发。
频道:http.response!
键:
-
status
:整数类型的 HTTP 状态码。 -
headers
:[name, value]
列表,name
是字节串类型的头名称,value
是字节串类型的头值。顺序应该从原始的 HTTP 请求被保留。头名称必须小写。 -
content
:字节串类型的 HTTP 内容体。可选,默认为空。 -
more_content
:布尔类型,用于表示接下来是否有更多的内容要进来(作为请求体块消息的一部分)。如果为False
,请求将被视作完成,当前频道上的其余消息将被忽略。可选,默认是False
。
响应块(Response Chunk)
必须在响应初始化之后触发。
频道:http.response!
键:
-
content
:字节串类型的 HTTP 内容体,将被拼接到之前接收到的请求里面的content
。 -
more_content
:布尔类型,用于表示接下来是否有更多的内容要进来(作为响应块消息的一部分)。如果为False
,响应将被视作完成并关闭,当前频道上的其余消息将被忽略。可选,默认是False
。
服务器推送
必须在响应或响应块消息之前被触发。
当服务器收到该消息时,它必须将服务器推送的请求字段中的请求消息当做从网络中新接收到的 HTTP 请求。服务器可以,如果它想,用它所有的内部逻辑来处理这个请求(例如,服务器可能想用缓存来响应该请求)。无论如何,如果服务器自己不能满足该请求,它必须要创建一个新的 http.response!
频道给应用来发送响应消息,将这个频道放进消息的 reply_channel
字段,然后将请求在 http.request
频道里发送回应用。
这种做法减少了应用为处理推送响应所需要掌握的技能:它们在应用看来和不同的 HTTP 请求并没有什么两样,唯一的不同是应用自身发送的请求而已。
如果远程那端不支持服务器推送,要不就是因为它不是 HTTP/2,要不就是 SETTINGS_ENABLE_PUSH 是 0,对这样的消息,服务器啥都不用做。
频道:http.response!
键:
-
request
:一条请求消息。body
、body_channel
、reply_channel
字段一定不能有:服务器推送的请求是不允许内容体(body)的,应用也不应该创建回复频道。
断开连接
当 HTTP 连接被关闭后会触发。这主要是对长轮询有用。你可能会将响应频道加入一个群组或其他频道组,数据一进来立即回复。
频道:http.disconnect
键:
-
reply_channel
:响应会被发送到的频道名称。消息一经发送便不再有效,该频道上的所有消息都会被丢弃。
WebSocket
WebSocket 和 HTTP 有一些共通的地方——它们都有请求路径和头——但也有更多不同的状态。路径和头信息仅仅会在建立连接的消息里被发送。需要在后续的消息里查找这两个信息的应用则需要自己将它们存储到缓存或数据库。
WebSocket 协议服务器应该自己处理 PING/PONG 请求,为了确保连接是活着的,发送 PING 帧是十分必要的。
建立连接(Connection)
当客户端初始创建了一个连接并完成了 WebSocket 握手后触发。
频道:websocket.connect
键:
-
reply_channel
:用于发送数据的频道名称,以websocket.send!
开头。 -
scheme
:Unicode string 类型的 URL 协议名称(如ws
或wss
)。可选(但不能为空),默认是ws
。 -
path
:Unicode string 类型的 HTTP URL 请求路径,已经被 URL 解码。 -
query_string
: Unicode string 类型的 URL 中?
后边的部分,和path
一样已经解码过,可选,默认为空。 -
root_path
: Unicode string 类型,指定应用所在的根路径,和 WSGI 中 的SCRIPT_NAME
一致,可选,默认为空。 -
headers
:[name, value]
列表,name
是字节串类型的头名称,value
是字节串类型的头值。 顺序应该从原始的 HTTP 请求被保留,可能会有重复,而且必须保留在接收到的消息中。头名称必须小写。 -
client
:[host, port]
列表,host
是 unicode string 类型的远程主机的 IPv4 或 IPv6 地址,port
整数类型的主机端口。可选,默认是None
。 -
server
:[host, port]
列表,host
是 unicode string 类型的服务器监听地址,port
是整数类型的端口。可选,默认是None
。 -
order
:整数类型的值0
(译者注:WebSocket 流里帧的顺序)。
接收消息(Receive)
当从客户端收到一个数据帧时触发。
频道:websocket.receive
键:
-
reply_channel
:用于发送数据的频道名称,以websocket.send!
开头。 -
path
:在connect
中的路径,用于简化应用路由。 -
bytes
:如果处于字节模式,则是字节串类型的帧内容,否则为None
。 -
text
:如果处于文本模式,则是 Unicode string 类型的帧内容,否则为None
。 -
order
:WebSocket 流里的帧的顺序,从 1 开始(connect
是 0)。
bytes
和 text
二选一。
断开连接 (Disconnection)
当下列情况之一发生时触发:连接丢失、客户端关闭连接、服务器关闭连接或 socket 丢失。
频道:websocket.disconnect
键:
-
replay_channel
:用于发送数据的频道名称,以websocket.send!
开头。但并不能在这个时候使用,仅用于辨识连接。 -
path
:在connect
中的路径,用于简化应用路由。 - 相对于进来的数据帧
websocket.receive
的order
值。
发送、关闭
发送一个数据帧到客户端或者从服务端关闭当前连接。
频道:websocket.send!
键:
-
bytes
:如果处于字节模式,则是字节串类型的帧内容,否则为None
。 -
text
:如果处于文本模式,则是 Unicode string 类型的帧内容,否则为None
。 -
close
:布尔类型,用于说明在发送完数据后是否应该关闭连接。可选,默认是False
。
bytes
和 text
二选一。如果两者同时提供了,协议服务器将忽略整个消息。
协议格式指南
用于协议的消息格式应遵循以下规则,除非能提供一个性能或实现上的理由:
-
reply_channel
对于每一个逻辑上的连接都应该是唯一的,而非逻辑上的终端。 - 如果协议具有服务端状态,应当将状态包含在协议服务内部,不应要求消息消费方使用一个外部状态存储机制。
- 如果协议有低等级的内容协商、保持活跃等特性,应再协议服务器内部处理它们,不应将它们放入 ASGI 消息内。
- 如果保证了顺序,并且对给定的连接不实用特性的频道(如 HTTP 对 body 数据的处理),ASGI 消息应包含一个
order
字段(从 0 开始的索引)用来保存协议服务器接收到的消息顺序。该顺序应涵盖客户端发出的所有消息类型,举例来说,一个建立连接的消息应在位置0
,头两个数据帧则在1
和2
。 - 如果协议基于数据电报,一个数据电报等同于一个 ASGI 消息(除非电报体积是一个问题)
近似全局排序
由于保证全局(跨频道)消息排序对很多实现来说都是不切实际的,它们应该努力避免繁忙的频道抢占安静的频道。
例如,想象两个频道,busy
频道一秒钟 1000 条消息,而 quiet
频道一秒钟仅有一条消息。有一个消费方运行着 receive_many(['busy', 'quiet'])
,一秒钟可以处理 200 条消息。
在一个简化的基于 for 循环的实现里,如果频道层总是先检查 busy
,由于 busy
频道总是有消息待消费,所以就算 quiet
频道里的消息是跟 busy
频道同时发出的,消费方永远不能从 quiet
频道取得消息。
为了解决这个问题,简单的做法是每一次取消息的时候打乱频道的顺序。或者存在更好的做法,但不管用什么方法,都应该避免上述场景——因为其中一个频道过于繁忙导致其他渠道的消息得不到消费。
字符串和 Unicode
在这份文档中, 字节字符串 指的是 Python 2 里的 str
以及 Python 3 里的 bytes
。如果有潜在的实现使得这种类型也支持 Unicode 码点,那么它们的值应当保证在 8 字节长度以内。
Unicode 字符串 指的是 Python 2 里的 unicode
以及 Python 3 里的 str
。这份文档讨论的字符串都是这两种类型之一。
一些序列化工具,例如 json
,不能区别字节字符串和 Unicode 字符串,这些应当有一个逻辑处理,将一个类型装入另一个类型里边(例如,将字节字符串以带特殊字符前缀的 base64 unicode 字符串编码,像 U+FFFF)。
频道和群组名称永远都是 unicode 字符串,更进一步,它们仅能使用以下字符:
- ASCII 字母
- 数字
0 ~ 9
- 连字符
-
- 下划线
_
- 句点
.
- 问号
?
(仅用于描画单接收者的频道名称,每一个名称仅包含一个) - 叹号
!
(仅用于描画指定进程的频道名称,每一个名称仅包含一个)
WSGI 兼容性
这份规范中对于 HTTP 部分的设计,一部分是为了保证和 WSGI 规范的兼容。保证两者的应用的通用性,让 WSGI 服务器或应用都能直接用于 ASGI。
通用性主要体现在两方面:
- 从 WSGI 服务到 ASGI:一个 WSGI 应用可以这么写,将
environ
转变成一条请求消息,在http.request
频道发送,然后在生成的响应频道里等待一条响应消息。这么做虽然有使用一整个 WSGI 线程拉取一个频道的缺点,但如果请求频道里没有积压的话还不至于造成很大的性能损失。而且运行纯 ASGI web 应用的进程内适配器也会工作良好。 - 从 ASGI 到 WSGI 应用:需要有一个容器进程监听
http.request
频道,将进来的请求消息解码成符合 WSGI 规范的environ
dict,将每一个返回的内容块打包成响应(Response)或响应块消息(Response Chunk Message)(如果产生了不止一个返回)
下面是一个 WSGI environ
到请求消息的对应关系:
-
REQUEST_METHOD
就是method
键 -
SCRIPT_NAME
就是root_path
-
PATH_INFO
可以从path
和root_path
推断出来 -
QUERY_STRING
就是query_string
-
CONTENT_TYPE
可以从headers
提取出来 -
CONTENT_LENGTH
可以从headers
提取出来 -
SERVER_NAME
和SERVER_PORT
在server
里 -
REMOTE_HOST
/REMOTE_ADDR
和REMOTE_PORT
在client
里 -
SERVER_PROTOCOL
编码在http_version
里 -
wsgi.url_scheme
就是scheme
-
wsgi.input
是body
的 StringIO 形式 -
wsgi.errors
在需要的时候被容器层指定
start_response
方法相当于响应(Response):
-
status
参数还是status
参数 -
response_headers
对应headers
甚至可能可以将请求体块(Request Body Chunks)映射成一种允许以数据流形式发送体数据的方式。尽管对于许多应用来说,在调用 WSGI 应用之前简单地将全部体数据缓存进内存足够方便,也足够了。
可能要做的
- 也许可以将
http_version
替换成supports_server_push
? - 因为异步、协程是非阻塞的,它们并不能很方便地用于实现
receive_many
,可能用 Asyncio 扩展来提供receive_many_yeild
? - 考虑扩展能允许检测频道层的清空、重启事件并提示协议服务器重启?
References
- ASGI draft