第5章 ASGI ASGI(Asynchronous Server Gateway Interface)为异步服务网关接口,秉承WSGI统一网关接口原则,在异步服务、框架和应用之间提供一个标准接口,同时兼容WSGI。 学习一个新知识之前,我们首先需要明白它为什么会出现,那么为什么ASGI和WSGI这类的统一网关接口会出现呢? Python Web开发大致可以分为两大层,协议层(HTTP逻辑层,后简称协议层)和应用层,协议层主要根据HTTP协议解码与编码数据,应用层主要实现具体的业务逻辑,分层的好处是应用层可以根据需要很方便地替换协议层,要做到这一点,只需为协议层和应用层设计一个统一的接口,这就是WSGI与ASGI。 有了这个统一的接口,协议层不仅仅只有Python才能实现,其他的语言例如C语言如果按这个接口去实现协议层,那么也可以和Python应用层对接,所以出现的WSGI可让Python运行在Apache服务器上,这样可以让Apache强大的静态文件请求处理功能发挥作用,避免Python去处理静态请求,从而大大提高整个网站的运行速度。 6min 5.1WSGI WSGI可以与传统阻塞型IO服务器配合使用。 在这里需要明确一点,传统阻塞型IO服务器就一定是低效的吗?当然不是,我们已知异步IO编程模型的目的是为了合理利用IO资源,而传统的阻塞型IO编程也可以通过复杂的方案实现合理利用IO资源,例如优秀的Apache服务器。当然最新的Apache服务器也采用了异步IO编程模型,毕竟是操作系统已经实现了的功能,直接使用能够让项目的维 护成本降低。 图51项目结构 在传统阻塞型IO编程模型中,通常会把Python应用服务与Apache服务器结合使用,仅把与业务逻辑处理相关的任务交给Python语言去完成,而其他资源的处理(例如: 静态文件请求处理、代理等)任务交给强大的Apache服务器去完成,从而实现负载均衡,提高项目整体的运行效率。 接下来我们以搭建一个Apache + WSGI的Docker环境为例演示如何在项目中使用WSGI。项目结构如图51所示。 先从编写Docker构建脚本开始介绍,我们计划使用的基础镜像是httpd:2.4。因为需要重新编译mod_wsgi模块,所以需要下载庞大的C/C++编译环境,而官方镜像服务器在国外,下载速度较慢,所以这里使用清华的Debian镜像。 首先需要准备一个sources.list文件,其内容如下: #第5章/apache_with_wsgi/web_env/sources.list deb http://mirrors.tuna.tsinghua.edu.cn/debian/ buster main contrib non-free #deb-src https://mirrors.tuna.tsinghua.edu.cn/debian/ buster main contrib #non-free deb http://mirrors.tuna.tsinghua.edu.cn/debian/ buster-updates main contrib non-free #deb-src https://mirrors.tuna.tsinghua.edu.cn/debian/ buster-updates main #contrib non-free deb http://mirrors.tuna.tsinghua.edu.cn/debian/ buster-backports main contrib non-free #deb-src https://mirrors.tuna.tsinghua.edu.cn/debian/ buster-backports #main contrib non-free deb http://mirrors.tuna.tsinghua.edu.cn/debian-security buster/updates main contrib non-free #deb-src https://mirrors.tuna.tsinghua.edu.cn/debian-security #buster/updates main contrib non-free mod_wsgi的源码可从https://GitHub.com/GrahamDumpleton/mod_wsgi下载,在这里使用的mod_wsgi4.7.1,如果使用其他版本,需要注意修改相应的目录。Dockerfile文件的内容如下: #第5章/apache_with_wsgi/web_env/Dockerfile FROM httpd:2.4.41 RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak #替换软件源为清华镜像站 COPY sources.list /etc/apt/ #更新软件源 RUN apt-get update #安装Python运行环境及C语言编译器用以编译WSGI RUN apt-get install -y python3 python3-dev gcc g++make #将WSGI源码复制到镜像中 COPY mod_wsgi-4.7.1.tar.gz /opt #切换镜像中的工作目录 WORKDIR /opt #解压wsgi源码包 RUN tar -xzvf ./mod_wsgi-4.7.1.tar.gz #切换工作目录到WSGI源码目录 WORKDIR /opt/mod_wsgi-4.7.1 #配置编译环境 RUN ./configure --with-apxs=/usr/local/apache2/bin/apxs \ --with-python=/usr/bin/python3 #编译并安装 RUN make&&make install #这里对Apache服务器的配置文件进行修改,添加WSGI相关的配置项 #包括加载WSGI动态库、配置脚本文件、配置脚本文件所在目录的权限 RUN echo 'LoadModule wsgi_module modules/mod_wsgi.so' >>\ /usr/local/apache2/conf/httpd.conf RUN echo 'WSGIScriptAlias /app /opt/server/wsgi_app.py' >>\ /usr/local/apache2/conf/httpd.conf RUN echo '<Directory /opt/server/>' >>\ /usr/local/apache2/conf/httpd.conf RUN echo ' Require all granted' >> \ /usr/local/apache2/conf/httpd.conf RUN echo '</Directory>' >> /usr/local/apache2/conf/httpd.conf 脚本文件wsgi_app.py 的内容如下: """ 第5章/apache_with_wsgi/server/wsgi_app.py """ def application(environ, start_response): status = '200 OK' output = b'Hello World!' response_headers = [ ('Content-type', 'text/plain'), ('Content-Length', str(len(output))) ] start_response(status, response_headers) return [output] 这是一个最简单的WSGI脚本,其中声明了一个 application 函数,当有前端请求时,该函数会被调用以处理请求,这个示例中向前端返回'Hello World!'。 对应的 dockercompose.yml 文件内容如下: #第5章/apache_with_wsgi/docker-compose.yml version: "3" services: web: build: ./web_env tty: true ports: - "80:80" volumes: - "./server:/opt/server" 图52Apache集成WSGI示例页面 在终端里执行命令 dockercompose upd 以构建并启动该服务器,待服务器启动完成后,通过浏览器访问http://127.0.0.1/app,显示效果如图52所示。 用同样的方式,还可以集成第三方框架如web.py、web2py、Django等以提高开发效率。但这些都已经是过时的技术了,讲解WSGI是为了让读者拥有维护旧项目的能力,并对ASGI有一个初步的认知,也因为目前Apache和Nginx平台均未出现ASGI模块,无法演示ASGI模块的编译与配置,但构建ASGI与构建WSGI类似,所以只能以构建WSGI环境来演示构建过程。 8min 5.2ASGI ASGI是根据统一接口的思想重新设计的新标准,你可能会有疑问,为什么不直接升级WSGI而去创造新的标准呢? WSGI是基于HTTP短连接的网关接口,一次调用请求必须尽快处理完毕并返回结果,这种模式并不适用于长连接,例如HTML 5新标准中的技术SSE(ServerSent Events)和WebSocket,WSGI及传统阻塞型IO编程模型并不擅长处理这类请求。就算强行升级WSGI以支持异步IO,可是如果配套的技术(如Apache服务器)没有提供相应的支持也是没有意义的。既然Python异步IO编程模型已经走在了前面,那就制定一个全新的标准ASGI以最优雅的方式支持并使用最新的技术。 ASGI接口是一个异步函数,它要求传入3个参数,分别为 scope、receive和send,示例代码如下: async def app(scope, receive, send): pass 其中scope是一个字典(dict),包括连接相关的信息,图53所示是一个请求中的scope所包括信息的断点调试截图。 图53断点截图 receive是一个异步函数,用于读取前端发来的信息,一条读取到的信息结构如下: { 'type': 'http.request', 'body': b"", 'more_body': False } 该信息中包括3个字段,分别为类型(type)、内容(body)和是否还有更多内容(more_body),其中通过type可以用来判断该信息是什么类型,如HTTP请求、生命周期、WebSocket请求等,body是该信息中包括的数据,此数据采用二进制格式,more_body指明当前数据是否已经发送完毕,如果发送完毕,则more_body的值为False,这样便可以用来分段传输大文件。 send也是一个异步函数,用于向前端发送信息,所发送的信息结构与从前端接收的信息结构类似。一个向前端发送简单信息的示例代码如下: """ 第5章/asgi_app/simple_asgi.py """ async def app(scope, receive, send): #向前端发送HTTP协议头,包括了HTTP状态与协议头 await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/html'], ] }) #向前端发送数据,如果数据庞大,则可以分段发送 await send({ 'type': 'http.response.body', 'body': b"Hello World", 'more_body': False }) 除了常规数据通信外,ASGI 还规定了生命周期管理接口,可以用于侦听服务器的启动与关闭。在实际开发工作中,这非常有用,可以用来执行初始化工作与收尾工作,生命周期管理的运用代码如下: """ 第5章/asgi_app/asgi_lc.py """ async def app(scope, receive, send): request_type = scope['type'] if request_type == 'lifespan': while True: message = await receive() if message['type'] == 'lifespan.startup': await send({'type': 'lifespan.startup.complete'}) elif message['type'] == 'lifespan.shutdown': await send({'type': 'lifespan.shutdown.complete'}) break 当scpoe['type']的类型是lifespan时,意味着该请求的类型是生命周期,该请求会在服务器启动之初发生,接下来应该实现对生命周期的管理。 通过无限循环不断侦听请求状态的变化,当读到message['type']是lifespan.startup时执行初始化操作,在操作完成后向前端(协议层)发送lifespan.startup.complete信息,协议层可理解为服务器已经启动完成,可以正常接受浏览器请求了。 当读到message['type']是lifespan.shutdown时,意味服务要关闭,可能是由于服务器管理员执行了关闭指令,那么在这里就需要执行收尾工作,例如释放相应资源等。在收尾完成后向协议层发送lifespan.shutdown.complete信息,表明此时协议层可以放心地关闭服务器。 一个完整的基于ASGI的 Hello World 示例代码如下: """ 第5章/asgi_app/asgi.py """ async def app(scope, receive, send): request_type = scope['type'] if request_type == 'http': await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/html'], ] }) await send({ 'type': 'http.response.body', 'body': b"Hello World", 'more_body': False }) elif request_type == 'lifespan': while True: message = await receive() if message['type'] == 'lifespan.startup': await send({'type': 'lifespan.startup.complete'}) elif message['type'] == 'lifespan.shutdown': await send({'type': 'lifespan.shutdown.complete'}) break else: raise NotImplementedError() 2min 5.3Uvicorn Uvicorn是ASGI的一个协议层实现,一个轻量级的ASGI服务器, 基于uvloop和httptools实现,运行速度极快。 图54ASGI项目文件结构 uvloop是一个高效的基于异步IO的事件循环框架,底层实现由libuv承载。libuv是一个使用C语言开发的支持高并发的异步IO库,由Node.js的作者开发,作为Node.js的底层IO库实现,如今已经发展得相当成熟稳定。 要使用Uvicorn需要先通过命令pip install uvicorn安装该依赖项,项目结构如图54所示。 其中 asgi.py 文件即第5章/asgi_app/asgi.py。 接下来在终端输入uvicorn asgi:app以启动该服务器,效果如图55所示。 图55启动ASGI服务器 图56页面访问结果 服务器启动后,可使用浏览器通过http://127.0.0.1:8000访问该站点,结果如图56所示。 为了向用户提供更加安全的服务,现代网站都需要支持HTTPS,Uvicorn也提供了对HTTPS的支持,使用起来也相当方便。 首先准备好HTTPS证书文件,如图57所示。 图57证书文件所在目录 接下来通过命令uvicornsslcertfile ./ssl/cert.pemsslkeyfile ./ssl/cert.key asgi:app来启动该服务器,如图58所示。 图58以HTTPS方式启动服务器 容器化对应的 Dockerfile 文件内容如下: #第5章/asgi_app/Dockerfile FROM python:3-slim RUN pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple uvicorn 容器化对应的dockercompose.yml内容如下: #第5章/asgi_app/docker-compose.yml version: "3" services: web: build: . restart: always tty: true ports: - "8000:8000" volumes: - ".:/opt/" working_dir: "/opt/" command: uvicorn --host 0.0.0.0 asgi:app 5.4Daphne Daphne是一个功能强大的ASGI协议层实现,支持HTTP、HTTP2和WebSocket协议,由Django团队开发,用于支持DjangoChannels(Channels是一个由Django团队开发的可让Django支持WebSocket、HTTP长连接和其他异步编程的框架)的功能。 如果要使用 Daphne,需要先通过命令pip install daphne进行安装,然后通过命令daphne asgi:app启动服务器,如图59所示。 图59启动Daphne服务器 此时可以使用浏览器通过http://127.0.0.1:8000进行访问,结果如图510所示。 Daphne也支持HTTPS,需要先准备好HTTPS证书,如图511所示。 图510页面访问结果 图511证书文件所在目录 接下来用命令daphnee ssl:8443:privateKey=./ssl/cert.key:certKey=./ssl/cert.pem asgi:app启动服务器,如图512所示。 图512启动Daphne HTTPS服务器 此时可以使用浏览器通过https://127.0.0.1:8443进行访问,结果如图513所示。 图513访问HTTPS页面 容器化对应的Dockerfile文件内容如下: #第5章/daphne_asgi_app/Dockerfile FROM python:3-slim #因为安装Daphne的过程中可能需要编译原生代码,所以需要安装GCC编译器 RUN apt update&&apt install -y gcc make RUN pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple daphne 容器化对应的dockercompose.yml文件内容如下: #第5章/daphne_asgi_app/docker-compose.yml version: "3" services: web: build: . restart: always tty: true ports: - "8000:8000" volumes: - ".:/opt/" working_dir: "/opt/" command: daphne -b 0.0.0.0 asgi:app 5.5Django搭配ASGI Django是一个非常强大的Python Web开发框架,也是目前Python语言使用最广泛的Web开发框架,很多人在入门Python Web开发中所学习的第一个技术框架就是Django,此框架具有易学、易用、功能强大、文档优秀、技术支持完整等优点,这些优点使Django在近十几年来一直都是Python语言中最受欢迎的Web开发框架。 随着ASGI技术的快速发展,Django率先对ASGI提供了支持,目前官方已经发行了支持ASGI的稳定版本框架。 与AIOHTTP不同,Django对于技术的分层非常明确,Django属于应用层实现,协议层可以自由切换。AIOHTTP同时实现了协议层与应用层,并将所有的技术混合在一起,虽然使用起来相当方便,但是如果你期望后期随着技术的迁移和项目架构的改动而更换协议层进行实现,将会变得非常麻烦。而Django框架是单纯的应用层实现,无须担心协议层的技术实现,可以通过WSGI技术将协议层实现交给Apache服务器或者Nginx服务器实现,也可以通过ASGI技术将协议层实现交给Uvicorn或者Daphne等实现。 接下来我们一步一步来演示如何将Django部署在ASGI服务器上。 使用PyCharm创建一个新项目,名为django_proj,并基于Python 3.8创建一个运行环境,如图514所示。 图514创建项目 项目创建完成后通过命令pip install Django安装Django,如图515所示。 图515安装Django 通过命令djangoadmin startproject web创建一个Django项目,如图516所示。 图516创建Django项目 通过命令pip install uvicorn安装Uvicorn 环境,如图517所示。 图517安装Uvicorn 使用命令cd web将工作目录切换到 web目录,如图518所示。 图518切换工作目录 使用命令uvicorn web.asgi:application启动服务器,如图519所示。 图519启动Django服务器 此时可以使用浏览器通过地址http://127.0.0.1:8000访问,效果如图520所示。 图520Django站点首页 5.6Quart Quart是一个基于asyncio的Python Web开发框架,提供了一种简单的在Web开发中使用asyncio的方式。 这里需要说明一下,为什么我们要了解这么多框架,因为目前Python asyncio生态正处于发展阶段,各种框架如雨后春笋般出现,呈现百家争鸣的状态。对于开发者来讲,不能押宝于任何一个框架,只能尝试学习更多的框架,才能在各种框架之间有一个实际的对比能力,在进行技术选型的时候能够更加准确,而不至于给项目后期带来不利影响。同时更应该去学习和掌握框架实现技术,从而拥有解决框架本身问题的能力,或者直接在项目中采用自己所开发的框架,以杜绝依赖第三方技术所带来的不稳定因素。 使用Quart之前需要先使用命令pip install quart安装该依赖项,之后创建一个文件名为server.py的文件,实现一个最简页面请求处理功能,代码如下: """ 第5章/quart_app/server.py """ from quart import Quart #创建一个Quart应用 app = Quart(__name__) #处理对站点根路径的请求 @app.route('/') async def hello(): #向前端返回字符串 return 'hello' app.run() 通过命令python server.py启动该服务器,如图521所示。 图521启动Quart服务器 此时可以通过地址http://127.0.0.1:5000访问该站点,如图522所示。 图522页面访问结果 此时可以发现,Quart是可以独立运行的,就像AIOHTTP一样,这是因为Quart的依赖项中有Hypercorn(一个ASGI协议层实现,与Uvicorn、Daphne是同类型竞品,用法类似),在安装Quart时会自动安装Hypercorn,而当使用app.run函数启动服务器时,会默认使用Hypercorn来启动服务器。 也可以使用单独的Hypercorn去启动服务器,只需删除app.run()这行代码,源码如下: """ 第5章/quart_app/asgi.py """ from quart import Quart #创建一个Quart应用 app = Quart(__name__) #处理对站点根路径的请求 @app.route('/') async def hello(): #向前端返回字符串 return 'hello' 接下来通过命令hypercorn server:app启动服务器,如图523所示。 图523使用hypercorn命令启动服务器 在安装Uvicorn之后也可以使用命令uvicorn server:app启动服务器,如图524所示。 图524使用uvicorn命令启动服务器 同样在安装Daphne之后也可以使用命令daphne server:app启动服务器,如图525所示。 图525使用daphne命令启动服务器 5.7Starlette Starlette是一个轻量级的高性能应用层框架,构建于ASGI之上,框架本身不带ASGI协议层,所以运行时需要手动安装协议层依赖项,例如uvicorn。 要使用Starlette,需要先用命令pip install starlette安装该框架,如图526所示。 图526安装Starlette 创建一个名为server.py的文件,在其中输入Hello World示例代码如下: """ 第5章/starlette_app/server.py """ from starlette.applications import Starlette from starlette.responses import HTMLResponse from starlette.routing import Route async def homepage(request): return HTMLResponse("Hello World") app = Starlette(debug=True, routes=[ Route('/', homepage), ]) 若要运行该服务器,需要先使用命令pip install uvicorn安装Uvicorn,也可以尝试使用其他的协议层实现,如Daphne或者Hypercorn。在安装Uvicorn之后需要使用命令uvicorn server:app启动服务器,如图527所示。 图527启动Starlette服务器