Python WSGI笔记
Python WSGI笔记
本文转载自花了两个星期,我终于把 WSGI 整明白了
问1:一个HTTP请求到达对应的 application 处理函数要经过怎样的过程?
问2:如何不通过流行的 web 框架来写一个简单的web服务?
一个HTTP请求的过程可以分为两个阶段,第一阶段是从客户端到WSGI Server,第二阶段是从WSGI Server 到 WSGI Application。
主要内容:
- WSGI是什么,因何而生?
- HTTP请求是如何到应用程序的?
- 实现一个简单的WSGI Server
- 实现"高并发"的WSGI Server
- 第一次路由:PasteDeploy
- PasteDeploy 使用说明
- webob.dec.wsgify装饰器
- 第二次路由:中间件 routes 路由
1.WSGI是什么?因何而生?
WSGI是 Web Server Gateway Interface 的缩写。介于 web server 和 应用 server 之间的接口规范。
它是 Python 应用程序 (application) 或 框架(如 Django) 和 Web 服务器之间的一种接口,已经被广泛接受。
它是一种协议,一种规范,其是在 PEP 3333 提出的。这个协议旨在解决众多 web 框架和 web server软件的兼容问题。有了 WSGI,你不用再因为你使用的 web 框架而去选择特定的 web server软件。
常见的web应用框架有:Django、Flask等。
常用的web服务器软件有:uWSGI、Gunicorn等。
那这个 WSGI 协议内容是什么呢?知乎上有人将 PEP 3333 翻译成中文,写得非常好,我将这段协议的内容搬运过来。
为了解PEP 333的读者准备的前言
PEP 3333是PEP 333的升级版本,进行了略微修改以提高在 Python 3下的可用性。同时,合并了几点长期存在的、实际的修改。
规范概述
WSGI 接口有服务端和应用端两部分,服务端也可以叫网关端,应用端也叫框架端。服务端调用一个由应用端提供的可调用对象。如何提供这个对象,由服务端决定。例如某些服务器或者网管需要应用的部署者写一段脚本,以创建服务器或者网关的实例,并且为这个实例提供一个应用实例。另一些服务器或者网关则可能使用配置文件或其他方法以指定应用实例应该从哪里导入或获取。
除了这些比较纯的服务器/网关 和 应用/框架,我们还可以创建实现了此规范的中间件组件。这个中间件对服务器来说像是应用,对它所包含的应用来说像是服务器。中间件可以用来提供扩展API、内容转换、导航等其他有用的功能。
纵观整个规范,术语[可调用对象]可能代表函数、方法、类或者实现了 __ call __ 方法的实例。这取决于服务器、网关或者应用选择哪种实现技术。相反地,调用这个可调用对象的服务器、网关或者应用不能依赖提供给它的是哪种可调用对象。可调用对象仅仅是被调用,不会内省自己。
字符串类型
一般来说,HTTP处理的是字节,这意味着WSGI规范也要处理字节。然而,这些字节内容往往有某种文本解释。在Python中,字符串是处理文本最方便的方式。
但是在很多Python版本和实现中,字符串是Unicode,不是字节。这就需要小心平衡在HTTP的上下文中如何正确转换字节和文本,并提供有用的API。尤其是需要支持Python实现中不同str类型的转换代码。
因此 WSGI 定义了两种字符串:
- 原生字符串(总是使用 str 来实现)用于 请求/响应 的头部(headers) 和 元数据(metadata)
- 字节字符串(在 Python3 中用 bytes实现,其他版本中用 str 实现)用于请求/响应的数据部分(如 POST/PUT的输入数据,HTML 的输出内容等)。
一句话:当你在本文档中看到 string 时,它代表[原生]字符串,例如一个 str 类型的对象,不管它内部实现上用的是字节还是 Unicode。当你看到 bytestring,应该视作一个在 python3 中的 bytes对象,在 python2 中的 str 对象。
应用/框架端
应用对象(application object)就是一个简单的接收两个参数的可调用对象。 不要混淆术语"object"就真的是一个对象实例。Python 中的函数、方法、类、实现了__ call __的实例都是可以接受的。应用对象必须可以被多次调用,因为实际上所有服务器/网关(除了 CGI 网关)都会重复地调用它。
WSGI 对于 application 对象有如下三点要求:
- 必须是一个可调用的对象
- 接收两个必选参数 environ、start_response;
- 返回值是可迭代对象,用来表示 http body。
下面是两个应用对象(application object)的示例。一个是函数(function),一个是类(class):
HELLO_WORLD = b'Hello world!/n'def simple_app(environ, start_response):"""最简单的应用对象"""status = '200 OK'response_headers = [('Content-type', 'text/plain')]start_response(status, response_headers)return [HELLO_WORLD]class APPClass:"""产生相同的输出,但是用类实现。"""def __init__(self, environ, start_response):self.environ = environself.start = start_responsedef __iter__(self):status = '200 OK'response_headers = [('Content-type', 'text/plain')]self.start(status, response_headers)yield HELLO_WORLD
服务器/网关端
服务器或者网关每次从 HTTP 客户端收到一个请求,就调用一次应用对象。
中间件:可以与两端交互的组件
中间件就是一个简单对象:既可以作为服务端角色,响应应用对象;也可以作为应用对象,与服务器交互。除此之外,还有一些其他功能:
- 重写environ,然后基于 URL,将请求对象路由给不同的应用对象。
- 支持多个应用或者框架顺序地运行于同一个进程中。
- 通过转发请求和响应,支持负载均衡和远程处理。
- 支持对内容做后处理(postprocessing),比如处理一个 XSL 样式表文件。
中间件的灵魂是:对 WSGI 接口的服务器/网关端 和 应用/框架端是透明的,不需要其他条件。
希望将中间件合并进应用的用户,将这个中间件传递给服务器即可,就好像这个中间件是一个应用对象;或者让中间件去调用应用对象,好像这个中间件就是服务器。当然,被中间件包装(wrap)的应用对象,实际上可能是另一个包装了另一个应用的中间件,以此类推,就创建了一个中间件栈(middleware stack)。
最重要的,中间件必须同时满足服务端和应用端的限制和条件。然而,在有些情况下,中间件需要的条件比单纯的服务端或者应用端更严格,这些点会在下面予以说明。
HTTP请求是如何到应用程序的?
当客户端发出一个 HTTP 请求后,是如何转到我们的应用程序处理并返回的呢?
关于这个过程,细节的点这里没法细讲,只能讲个大概。
根据其架构组成的不同将这个过程的实现分为两种:
1、两级结构
在这种结构里,uWSGI作为服务器,它用到了HTTP协议以及wsgi协议,flask应用作为application,实现了wsgi协议。当有客户端发来请求,uWSGI接收请求,调用flask app得到响应,之后响应给客户端。
这里说一点,通常来说,Flask等web框架会自己附带一个wsgi服务器(这就是flask应用可以直接启动的原因),但是这只是在开发阶段用到的,在生产环境是不够用的,所以用到了 uWSGI 这个性能高的wsgi服务器。
2、三级结构
这种结构里,uWSGI作为中间件,它用到了uWSGI协议(与 nginx 通信),wsgi协议(调用Flask app)。当有客户端发来请求,nginx先做处理(静态资源是 nginx 的强项),无法处理的请求(uWSGI),最后的响应也是ngin回复给客户端的。多了一层反向代理有什么好处?
提高web server性能(uWSGI处理静态资源不如nginx;nginx会在收到一个完整的http请求后再转发给uWSGI);
nginx可以做负载均衡(前提是有多个服务器),保护了实际的web服务器(客户端是在和nginx交互而不是uWSGI);
uWSGI
uWSGI 是一个实现了 wsgi、uwsgi、http协议的服务器。
它有两种模式,http模式对应上面的两级结构,socket模式对应上面的三层结构。
3、实现一个简单的WSGI Server
# hello.py
# 函数式 WSGI 服务器调用的对象是 app
def app(environ, start_response): # 提供两个必须变量start_response('200 OK', [('Content-Type', 'text/html')]) # 调用start_response发送头部body = ''for i, j in environ.items():body += '<p>' + (str(i) + ':::::' + str(j) + '<br><p>')return [body.encode('utf-8')] # 返回byte的元组表示 response body
确实很简单,我们再来看看别的方法实现的app。
# 实现__call__方法的类的实例变量 WSGI 服务器调用的对象是 app1()
class app1:def __call__(self, environ, start_response):start_response('200 OK', [('Content-Type', 'text/html;charset=utf-8')])body = ''for i, j in environ.items():body += '<p>' + (str(i) + ':::::' + str(j) + '<br><p>')return [body.encode('utf-8')]
# 迭代法 WSGI服务器调用的对象是app2
class app2:def __init__(self, environ, start_response):self.environ = environself.start_response = start_responsedef __iter__(self):self.start_response('200 OK', [('Content-Type', 'text/html;charset=utf-8')])body = ''for i, j in self.environ.items():body += '<p>' + (str(i) + ':::::' + str(j) + '<br><p>')yield body.encode('utf-8')
这三种写法不同,但效果是一致的。
看起来很简单,那么怎么调用呢?这两个参数又是怎么提供呢?答案在下边。
WSGI服务器
在上面的架构图里,不知道你发现没有,有个库叫做 wsgiref
, 它是 Python 自带的一个 wsgi 服务器模块。
从其名字上就可以看出,它是用纯 Python 编写的WSGI服务器的参考实现。所谓"参考实现"是指该实现完全符合WSGI标准,但是不考虑任何运行效率,仅供开发和测试使用。
有了 wsgiref 这个模块,你就可以很快速的启动一个 wsgi server。
# ser.py 同 hello.py 同一层
from wsgiref.simple_server import make_server
from hello import app# 创建一个服务器,第三个参数是处理函数
# 监听端口及绑定的ip一级请求来时使用的app
httpd = make_server('0.0.0.0', 8000, app)
print('Serving HTTP on port 8000...')
# 开始不断监听HTTP请求
httpd.serve_forever()
当你运行这段代码后,就会开启一个 wsgi server,监听 0.0.0.0:8000
,并接收请求。
打开浏览器,键入http://127.0.0.1:8000,就能看到我们app的效果了。
第一次路由:PasteDeploy
安装:
使用PasteDeploy部署,需要安装:
- pip install PasteDeploy
- pip install Paste
上面我们提到 WSGI Server 的创建要传入一个 Application,用来处理接收到的请求,对于一个由多个 app 的项目。
比如,你有一个个人网站提供了如下几个模块:
/blog # 博客 app
/wiki # wiki app
如何根据 请求的 url 地址,将请求转发到对应的 application 上呢?
答案是,使用 PasteDeploy 这个库。
PasteDeploy 到底是做什么的呢?
根据 官方文档 的说明,翻译如下:
- PasteDeploy 是用来寻找和配置WSGI应用和服务的系统。PasteDeploy给开发者提供了一个简单的函数loadapp,通过这个函数,可以从一个配置文件或者Python egg中加载一个WSGI应用。
使用PasteDeploy的其中一个重要意义在于,系统管理员可以安装和管理WSGI应用,而无需掌握与Python与WSGI相关知识。
由于 PasteDeploy 原来是属于 Paste 的,现在独立出来了,但是安装的时候还是会安装到 paste 目录(site-packages\paste\deploy)下。
PasteDeploy 使用说明
考虑到很多人事第一次接触 PasteDeploy,所以这里结合网上博客做了以下总结。对你入门会有帮助。
掌握 PasteDeploy,你只要按照以下三个步骤逐个完成即可:
- 配置 PasteDeploy使用的ini文件;
- 定义WSGI应用;
- 通过loadapp函数加载WSGI应用;
第一步:写 paste.ini 文件
在写之前,咱得知道 ini 文件的格式吧。
首先,像下面这样一个段叫做 section
。
[type:name]
key = value
...
其上的 type,主要有如下几种:
composite
(组合):多个 app 的路由分发;[composite:main] user = egg:Paste#urlmap / = home /blog = blog /wiki = wiki
app (应用):指明 WSGI 应用的路径;
[app:home] paste.app_factory = example:Home.factory
pipeline(管道):给一个 app 绑定多个过滤器;将多个filter和最后一个 WSGI 应用串联起来。
[pipeline:main] pipeline = filter1 filter2 filter3 myapp[filter:filter1] ...[filter:filter2] ...[app:myapp] ...
filter(过滤器):以 app 作为唯一参数的函数,并返回一个 “过滤” 后的app。通过键值 next 可以指定需要将请求传递给谁。next 指定的可以是一个普通的 WSGI应用,也可以是另一个过滤器。虽然名称上是过滤器,但是功能上不局限于过滤功能,可以是其它功能,例如日志功能,即将认为重要的请求数据记录下来。
[app-filter:filter_name] use = egg:... next = next_app[app:next_app] ...
对 ini 文件有了一定的了解后,就可以看懂下面这个 ini 配置文件了
[composite:main]
use = egg:Paste#urlmap
/blog = blog
/wiki = wiki[app:blog]
paste.app_factory = example:Blog.factory[app:wiki]
paste.app_factory = example:Wiki.factory
第二步是定义一个符合 WSGI 规范的 application 对象。
符合 WSGI 规范的 application 对象,可以有多种形式,函数、方法、类、实例对象,这里仅以实例对象为例(需要实现 __ call __ 方法),做一个演示。
import os
from paste import deploy
from wsgiref.simple_server import make_serverclass Blog(object):def __init__(self):print('Init Blog.')def __call__(self, environ, start_response):status_code = '200 OK'response_headers = [('Content-Type', 'text/plain')]response_body = "This is Blog's response body.".encode('utf-8')start_response(status_code, response_headers)return [response_body]@classmethoddef factory(cls, global_conf, **kwargs):print('Blog factory.')return Blog()
最后,第三步是使用 loadapp 函数加载 WSGI 应用。
loadapp 是 PasteDeploy 提供的一个函数,使用它可以很方便地从第一步的ini配置文件里加载 app;
loadapp 函数可以接收两个实参:
- WSGI 对于 application 对象有如下三点要求;
- URI:“config:<配置文件的全路径>”
conf_path = os.path.abspath('paste.ini')# 加载 app
applications = deploy.loadapp('config:{}'.format(conf_path) 'main')
applications 是 URLMap 对象。
完善并整合第二步和第三步的内容,写成一个 python 文件(wsgi_server.py)。
内容如下:
import os
from paste import deploy
from wsgiref.simple_server import make_serverclass Blog(object):def __init__(self):print('Init Blog.')def __call__(self, environ, start_response):status_code = '200 OK'response_headers = [('Content-Type', 'text/plain')]response_body = "This is Blog's response body.".encode('utf-8')start_response(status_code, response_headers)return [response_body]@classmethoddef factory(cls, global_conf, **kwargs):print('Blog factory.')return Blog()class Wiki(object):def __init__(self):print("Init Wiki.")def __call__(self, environ, start_response):status_code = '200 OK'response_headers = [('Content-Type', 'text/plain')]response_body = "This is Wiki's response body.".encode('utf-8')start_response(status_code, response_headers)return [response_body]@classmethoddef factory(cls, global_conf, **kwargs):print('Wiki factory.')return Wiki()if __name__ == "__main__":app = "main"port = 22800conf_path = os.path.abspath('paste.ini')# 加载 appapplications = deploy.loadapp('config:{}'.format(conf_path), app)server = make_server('localhost', port, applications)print('Started web server at port {}'.format(port))server.serve_forever()
一切都准备好后,在终端执行 python wsgi_server.py
来启动 web server;
如果像上图一样一切正常,那么打开浏览器
- 访问http://127.0.0.1:8000/blog,应该显示:This is Blog’s response body.
- 访问http://127.0.0.1:8000/wiki,应该显示:This is Wiki’s response body.。
注意:urlmap对url的大小写是敏感的,例如如果访问http://127.0.0.1:8000/BLOG,在url映射中未能找到大写的BLOG。
到此,你学会了使用 PasteDeploy 的简单使用。
本文转载自花了两个星期,我终于把 WSGI 整明白了
Python WSGI笔记相关推荐
- Python学习笔记:web开发3
前言 最近在学习深度学习,已经跑出了几个模型,但Pyhton的基础不够扎实,因此,开始补习Python了,大家都推荐廖雪峰的课程,因此,开始了学习,但光学有没有用,还要和大家讨论一下,因此,写下这些帖 ...
- Python学习笔记:web开发2
前言 最近在学习深度学习,已经跑出了几个模型,但Pyhton的基础不够扎实,因此,开始补习Python了,大家都推荐廖雪峰的课程,因此,开始了学习,但光学有没有用,还要和大家讨论一下,因此,写下这些帖 ...
- Python学习笔记--10.Django框架快速入门之后台管理admin(书籍管理系统)
Python学习笔记--10.Django框架快速入门之后台管理 一.Django框架介绍 二.创建第一个Django项目 三.应用的创建和使用 四.项目的数据库模型 ORM对象关系映射 sqlite ...
- python学习笔记_week22
python学习笔记_week22 note 知识点概要- Session- CSRF- Model操作- Form验证(ModelForm)- 中间件- 缓存- 信号 内容详细: 1. Sessio ...
- 字节跳动大佬的Python自学笔记.pdf
1. 字节跳动大佬的Python自学笔记 这是我的一个朋友自学资料包,通过这个资料包自学拿到了字节跳动的Offer, 下面是他之前入门学习Python时候的学习资料,非常全面,从Python基础.到w ...
- [python教程入门学习]python学习笔记(CMD执行文件并传入参数)
本文章向大家介绍python学习笔记(CMD执行文件并传入参数),主要包括python学习笔记(CMD执行文件并传入参数)使用实例.应用技巧.基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋 ...
- python学习笔记之编写readConfig读写配置文件
python学习笔记之编写readConfig读写配置文件_weixin_34055910的博客-CSDN博客
- Python学习笔记(十一)
Python学习笔记(十一): 生成器,迭代器回顾 模块 作业-计算器 1. 生成器,迭代器回顾 1. 列表生成式:[x for x in range(10)] 2. 生成器 (generator o ...
- Python学习笔记一简介及安装配置
Python学习笔记一 参考教程:廖雪峰官网https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e54 ...
最新文章
- android读取工程目录下的文件,Android编程实现读取工程中的txt文件功能
- 报错:ModuleNotFoundError: No module named ‘cv_bridge‘,以及在ROS是如何安装cv_bridge库包
- 一些著名的大公司JAVA面试题目
- 【STM32】GPIO功能复用
- IP地址不是唯一的吗?为什么路由器的IP地址都是这样的呢?
- C语言函数不能返回局部变量的地址
- php网站的编辑器,5款适合PHP使用的HTML编辑器推荐
- mysql_install_db is deprecated_MySQL5.7源码安装问题汇总
- 编辑服务器上的文件,Sublime Text编辑远程Linux服务器上的文件
- 朋友圈句句刺痛人心的唯美句子有哪些
- vue从入门到开发--2-基本结构
- Jetty 和tomcat 比较研究初探
- HTML5与CSS3权威指南.pdf7
- 解决Failed to connect to raw.githubusercontent.com port 443的办法
- android studio的sha1,[原]Android Studio查询SHA1的方法
- java 使用vue_简单使用vue-cli
- NetBeans的下载、安装
- echarts折线颜色渐变
- Docker------网络
- java生成有序的序号,java生成序号