1+1=10

扬长避短 vs 取长补短

Python ASGI入门笔记

接前面WSGI入门笔记,继续了解一下ASGI。

ASGI:Asynchronous Server Gateway Interface 异步服务器网关接口。ASGI是WSGI在异步方向的延续?为具有异步功能的Python Web服务器、框架 和 应用之间提供一个标准接口。

不同于WSGI由PEP规范;ASGI由Django团队提出,用于开发Django Channels,以便于支持异步应用,并支持websocket协议和HTTP/2等。

ASGI 3.0是2019年3月4日发布的,本文只关注3.0。

简介

根据ASGI协议,ASGI分为两部分:

  • Protocol Server
  • Application

我们主要关注Application端

例子

一个简单ASGI 3.0的application:

async def app(scope, receive, send):
    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!',
    })

再命令行使用uvicorn来执行它

uvicorn myapp:app

而后通过浏览器访问 127.0.0.1:8000 地址即可

也可以直接写代码,然后运行程序:

if __name__ == '__main__':
    from uvicorn import run
    run(app)

概念

不同于WSGI,一个ASGI连接有两个部分:

  • connection scope:代表一个客户的连接,在连接断开前始终存在
  • event:表示当连接(connection)有事情发生时发送到 Application 中的信息,以及由 Application 发送给 Server 的消息(其中包括传送给 Client 的的数据)。

接口参数

看一看例子中出现的三个参数

scope

一个信息字典,由于ASGI可用于包括HTTP在内的多种协议,这个字典中最重要的键是type。它应该代表连接生命期。

在上面的例子中,我们可以加上一句

async def app(scope, receive, send):
    print(scope)
    ...

使用firefox访问页面127.0.0.1:8000时,控制台输出:

{
    'type': 'http', 
    'asgi': {'version': '3.0', 'spec_version': '2.3'},
    'http_version': '1.1',
    'server': ('127.0.0.1', 8000),
    'client': ('127.0.0.1', 62321),
    'scheme': 'http',
    'method': 'GET',
    'root_path': '',
    'path': '/',
    'raw_path': b'/',
    'query_string': b'',
    'headers': [
        (b'host', b'127.0.0.1:8000'),
        (b'user-agent', b'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0'),
        (b'accept', b'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'),
        (b'accept-language', b'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2'),
        (b'accept-encoding', b'gzip, deflate, br'),
        (b'connection', b'keep-alive'),
        (b'upgrade-insecure-requests', b'1'),
        (b'sec-fetch-dest', b'document'),
        (b'sec-fetch-mode', b'navigate'),
        (b'sec-fetch-site', b'none'),
        (b'sec-fetch-user', b'?1')
        ],
    'state': {}
}

send

接受单个消息参数并返回None的异步可调用对象。在HTTP情况下,用于发送HTTP响应。

如例子所示,有两种类型HTTP响应消息:

  • 一种用于初始化发送响应 http.response.start
  • 一种用于发送响应正文 http.response.body

这和WSGI的拆成的start_reponse和返回值一致的:

#WSGI
def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    return [b'<h1>Hello World</h1>']

receive

和send一样,receive也是一个异步的可调用对象,允许运行时被await挂起。

不带参数,返回一条消息。在HTTP情况下,用于返回HTTP请求正文。

async def app(scope, receive, send):
    recv = await receive()
    print (recv)
    ...

控制台输出结果:

{'type': 'http.request', 'body': b'', 'more_body': False}

WebSocket

一个包含websocket的例子

例子

html = b'''
<!DOCTYPE HTML>
<html>
  <head>
    <script>
      var socket = new WebSocket("ws://127.0.0.1:8000");
      socket.onopen = function(event) {
        socket.send("Hello from websocket client!");
      };
      socket.onmessage = function(event) {
        sss = document.createElement("p");
        sss.innerHTML = event.data;
        document.body.appendChild(sss);
      };
    </script>
  </head>
  <body>
    WebSocket Example
  </body>
</html>
'''

async def app(scope, receive, send):
    if scope['type'] == 'http':
        await send({
            'type': 'http.response.start',
            'status': 200,
            'headers': [
                [b'content-type', b'text/html'],
            ],
        })
        await send({
            'type': 'http.response.body',
            'body': html,
        })
    elif scope['type'] == 'websocket':
        await send({
            'type': 'websocket.accept',
        })
        while True:
            event = await receive()
            if event['type'] == 'websocket.receive':
                await send({
                    'type': 'websocket.send',
                    'text': event['text'],
                })
            elif event['type'] == 'websocket.disconnect':
                break

运行前,需要安装支持websocket的ASGI服务器,比如 uvicorn[standard],

pip install 'uvicorn[standard]'

Starlette

Starlette是一个轻量的ASGI框架、工具集,是在Python下创建异步web服务的理想选择。

pip install starlette

使用它的话,一个ASGI程序就可以变成:

from starlette.responses import PlainTextResponse

async def app(scope, receive, send):
    response = PlainTextResponse('Hello, world!')
    await response(scope, receive, send)

再进一步

使用路由机制:

from starlette.applications import Starlette
from starlette.responses import JSONResponse, PlainTextResponse
from starlette.routing import Route


async def home(request):
    return JSONResponse({'hello': 'world'})

async def debao(request):
    return PlainTextResponse('Hello Debao')


app = Starlette(debug=True, routes=[
    Route('/', home),
    Route('/debao', debao),
])

对于json数据,在firefox浏览器看到页面(JSON,原始数据,头):

starlette-json-page

ASGI服务器

可用于生产的ASGI服务器

  • daphne:最初的ASGI服务器,最初为Django channels开发
  • uvicorn:高性能的ASGI服务器,基于uvloop和httptools实现
  • hypercorn:ASGI服务器,支持HTTP/2推送等

参考

  • https://asgi.readthedocs.io/en/latest/introduction.html
  • https://www.aeracode.org/2019/03/20/asgi-30/
  • https://en.wikipedia.org/wiki/Asynchronous_Server_Gateway_Interface
  • https://peps.python.org/pep-3333/
  • https://github.com/florimondmanca/awesome-asgi
  • https://florimond.dev/en/posts/2019/08/introduction-to-asgi-async-python-web
  • https://www.starlette.io

Python web

Comments