今天这篇文章,聊一下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-server
python-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
事件循环的风险,而用线程池处理就可以规避这种情况。