今天这篇文章,聊一下python在web开发上的一些基础实现,阐述下自己理解中的WSGI、ASGI,以及拿uvicorn+FastAPI的组合举个ASGI应用的例子。
WSGI
python的web服务的诞生,其实追溯到一种机制,叫做WSGI,全称Web Server Gateway Interface。WSGI的提案来源于PEP-333,可以理解为一种python-web-server和python-web-app的接口通信标准。在这种场景下,python的web服务呈现以下的工作模式:
python-web-app,也就是web应用层,实现WSGI接口,用作web请求的handler- 用户向
python-web-server发送web请求 python-web-server,又称作WSGI Server,解析请求数据,整理当前session的环境信息python-web-server加载python-web-app,调用python-web-app实例的WSGI接口,处理请求python-web-app处理完请求,返回结果给到python-web-serverpython-web-server写回返回结果,给回用户
代码上是这样的表现,以官方提案的例子为例:
1 | import os, sys |
通过WSGI,就可以实现python-web-app和python-web-server的分离,这样无论什么python-web-app,只要实现了WSGI接口标准,就能够无缝移植到其它支持WSGI的python-web-server上。
ASGI
自python3推出异步IO实现asyncio之后,ASGI也应运而生。ASGI的目标和WSGI相同,但也有一些改进点,一方面是支持asyncio的机制,另一方面也能够解决WSGI难以支持WebSocket之类长连接模式的问题。要深入了解ASGI,可以参考这篇文档。
在ASGI标准下,python-web-app需要这样的接口实现:
1 | async def application(scope, receive, send): |
不论是receive到的还是send出去的event,都会包含一个type字段表示这个event的类型,一般type会有:
http.xxx:http连接、请求、返回相关websocket.xxx:websocket连接、请求、返回相关xxx.send/receive:收发消息相关lifespan.xxx:web服务生命周期相关
ASGI案例之uvicorn+FastAPI
为了更加直观感受ASGI的应用,本文也顺带以uvicorn加FastAPI的组合,通过源码实现来看ASGI是如何串联起python-web-server和python-web-app的。
在笔者封装的简易http-web-app框架start-fastapi中,就支持了通过uvicorn启动FastAPI应用。其中,main.py的uvicorn实例会加载app模块下的APP这一FastAPI实例,启动web-app应用。
1 | # ============ start-fastapi project ============ |
首先从uvicorn.run开始看起,其代码实现如下:
1 | # uvicorn/main.py |
默认会走Server实例的run方法,我们来看其中的实现:
1 | # uvicorn/server.py |
这里有两个重要步骤:
config.load:加载配置startup:启动服务器
首先看配置加载,里面会将app实例进行初始化:
1 | # uvicorn/config.py |
可以看到FastAPI的app实现里,定义了ASGI,并且也在uvicorn的config.load里被识别到了。FastAPI继承了Starlette,而Starlette本身即是支持ASGI的web框架,为python-web-app提供了路由、中间件相关的应用级底层支持。FastAPI实际是对Starlette的包装,相关handler、middleware的注册也是给到Starlette框架里面的。针对web-server发来的请求,FastAPI在设置一些环境信息后,最终也是交由Starlette底层处理。
之后回到uvicorn,看一下startup的实现:
1 | # uvicorn/server.py |
startup分两步:
- 初始化
lifespan - 定义
http-handler,通过asyncio.start_server启动http-server
在初始化lifespan过程中,uvicorn会发送lifespan.startup事件,这个事件就会被FastAPI-app的ASGI捕获到,最终层层往下,会走到Starlette的Router实例:
1 | # starlette/routing.py |
当Startlette的Router检测到lifespan事件时,就会走到lifespan逻辑,其中会看lifespan的当前阶段是否有对应的hook函数,有的话就执行。当前阶段是lifespan.startup,因此如果我们在FastAPI中定义了这个协程,就可以在startup阶段执行到:
1 | # register startup event |
lifespan.startup之后,就定义http-handler并绑到listen-server上。http-handler会解析请求数据,然后调用app的ASGI接口处理请求,大致是这样的链路:
1 | class H11Protocol(asyncio.Protocol): |
好比我们GET健康检查接口/api/v1/core/health,那么最终被FastAPI-app捕获到的请求数据里,scope长这样:
1 | scope = { |
根据这些信息,层层往下,就会又走到Starlette的路由逻辑:
1 | # starlette/routing.py |
由于我们在start-fastapi项目中,通过APIRouter定义了这个路由的handler,注册到了Starlette中:
1 | # ============ start-fastapi ============ |
那么/api/v1/core/health就会被完整匹配,走到对应路由实例的handle步骤:
1 | # starlette/routing.py |
由于我们对健康检查路由定义了GET方法,那么这个路由就支持处理。最终来到了FastAPI的run_endpoint_function方法,调用我们定义的Controller。由于我们是直接def health_check(),因此会走到loop.run_in_executor线程池方法,去执行Controller,然后返回结果。否则如果是async def定义的Controller的话,就直接await。
所以整个请求返回的链路就完成了,而且我们也会看到,针对需要耗时耗CPU的请求,尽量不要用async def定义FastAPI的Controller,否则会有阻塞整个asyncio事件循环的风险,而用线程池处理就可以规避这种情况。