Python WSGI的深入理解


Posted in Python onAugust 01, 2018

前言

本文主要介绍的是Python WSGI相关内容,主要来自以下网址:

  • What is WSGI?
  • WSGI Tutorial
  • An Introduction to the Python Web Server Gateway Interface (WSGI)

可以看成一次简单粗暴的翻译。

什么是WSGI

WSGI的全称是Web Server Gateway Interface,这是一个规范,描述了web server如何与web application交互、web application如何处理请求。该规范的具体描述在PEP 3333。注意,WSGI既要实现web server,也要实现web application。

实现了WSGI的模块/库有wsgiref(python内置)、werkzeug.serving、twisted.web等,具体可见Servers which support WSGI。

当前运行在WSGI之上的web框架有Bottle、Flask、Django等,具体可见Frameworks that run on WSGI。

WSGI server所做的工作仅仅是将从客户端收到的请求传递给WSGI application,然后将WSGI application的返回值作为响应传给客户端。WSGI applications 可以是栈式的,这个栈的中间部分叫做中间件,两端是必须要实现的application和server。

WSGI教程

这部分内容主要来自WSGI Tutorial。

WSGI application接口

WSGI application接口应该实现为一个可调用对象,例如函数、方法、类、含__call__方法的实例。这个可调用对象可以接收2个参数:

  • 一个字典,该字典可以包含了客户端请求的信息以及其他信息,可以认为是请求上下文,一般叫做environment(编码中多简写为environ、env);
  • 一个用于发送HTTP响应状态(HTTP status )、响应头(HTTP headers)的回调函数。

同时,可调用对象的返回值是响应正文(response body),响应正文是可迭代的、并包含了多个字符串。

WSGI application结构如下:

def application (environ, start_response):

 response_body = 'Request method: %s' % environ['REQUEST_METHOD']

 # HTTP响应状态
 status = '200 OK'

 # HTTP响应头,注意格式
 response_headers = [
  ('Content-Type', 'text/plain'),
  ('Content-Length', str(len(response_body)))
 ]

 # 将响应状态和响应头交给WSGI server
 start_response(status, response_headers)

 # 返回响应正文
 return [response_body]

Environment

下面的程序可以将environment字典的内容返回给客户端(environment.py):

# ! /usr/bin/env python
# -*- coding: utf-8 -*- 

# 导入python内置的WSGI server
from wsgiref.simple_server import make_server

def application (environ, start_response):

 response_body = [
  '%s: %s' % (key, value) for key, value in sorted(environ.items())
 ]
 response_body = '\n'.join(response_body) # 由于下面将Content-Type设置为text/plain,所以`\n`在浏览器中会起到换行的作用

 status = '200 OK'
 response_headers = [
  ('Content-Type', 'text/plain'),
  ('Content-Length', str(len(response_body)))
 ]
 start_response(status, response_headers)

 return [response_body]

# 实例化WSGI server
httpd = make_server (
 '127.0.0.1', 
 8051, # port
 application # WSGI application,此处就是一个函数
)

# handle_request函数只能处理一次请求,之后就在控制台`print 'end'`了
httpd.handle_request()

print 'end'

浏览器(或者curl、wget等)访问http://127.0.0.1:8051/,可以看到environment的内容。

另外,浏览器请求一次后,environment.py就结束了,程序在终端中输出内容如下:

127.0.0.1 - - [09/Sep/2015 23:39:09] "GET / HTTP/1.1" 200 5540
end

可迭代的响应

如果把上面的可调用对象application的返回值:

return [response_body]

改成:

return response_body

这会导致WSGI程序的响应变慢。原因是字符串response_body也是可迭代的,它的每一次迭代只能得到1 byte的数据量,这也意味着每一次只向客户端发送1 byte的数据,直到发送完毕为止。所以,推荐使用return [response_body]。

如果可迭代响应含有多个字符串,那么Content-Length应该是这些字符串长度之和:

# ! /usr/bin/env python
# -*- coding: utf-8 -*- 

from wsgiref.simple_server import make_server

def application(environ, start_response):

 response_body = [
  '%s: %s' % (key, value) for key, value in sorted(environ.items())
 ]
 response_body = '\n'.join(response_body)

 response_body = [
  'The Beggining\n',
  '*' * 30 + '\n',
  response_body,
  '\n' + '*' * 30 ,
  '\nThe End'
 ]

 # 求Content-Length
 content_length = sum([len(s) for s in response_body])

 status = '200 OK'
 response_headers = [
  ('Content-Type', 'text/plain'),
  ('Content-Length', str(content_length))
 ]

 start_response(status, response_headers)
 return response_body

httpd = make_server('localhost', 8051, application)
httpd.handle_request()

print 'end'

解析GET请求

运行environment.py,在浏览器中访问http://localhost:8051/?age=10&hobbies=software&hobbies=tunning,可以在响应的内容中找到:

QUERY_STRING: age=10&hobbies=software&hobbies=tunning
REQUEST_METHOD: GET

cgi.parse_qs()函数可以很方便的处理QUERY_STRING,同时需要cgi.escape()处理特殊字符以防止脚本注入,下面是个例子:

# ! /usr/bin/env python
# -*- coding: utf-8 -*- 
from cgi import parse_qs, escape

QUERY_STRING = 'age=10&hobbies=software&hobbies=tunning'
d = parse_qs(QUERY_STRING)
print d.get('age', [''])[0] # ['']是默认值,如果在QUERY_STRING中没找到age则返回默认值
print d.get('hobbies', [])
print d.get('name', ['unknown'])

print 10 * '*'
print escape('<script>alert(123);</script>')

输出如下:

10
['software', 'tunning']
['unknown']
**********
<script>alert(123);</script>

然后,我们可以写一个基本的处理GET请求的动态网页了:

# ! /usr/bin/env python
# -*- coding: utf-8 -*- 

from wsgiref.simple_server import make_server
from cgi import parse_qs, escape

# html中form的method是get,action是当前页面
html = """
<html>
<body>
 <form method="get" action="">
  <p>
   Age: <input type="text" name="age" value="%(age)s">
  </p>
  <p>
   Hobbies:
   <input
    name="hobbies" type="checkbox" value="software"
    %(checked-software)s
   > Software
   <input
    name="hobbies" type="checkbox" value="tunning"
    %(checked-tunning)s
   > Auto Tunning
  </p>
  <p>
   <input type="submit" value="Submit">
  </p>
 </form>
 <p>
  Age: %(age)s<br>
  Hobbies: %(hobbies)s
 </p>
</body>
</html>
"""

def application (environ, start_response):

 # 解析QUERY_STRING
 d = parse_qs(environ['QUERY_STRING'])

 age = d.get('age', [''])[0] # 返回age对应的值
 hobbies = d.get('hobbies', []) # 以list形式返回所有的hobbies

 # 防止脚本注入
 age = escape(age)
 hobbies = [escape(hobby) for hobby in hobbies]

 response_body = html % { 
  'checked-software': ('', 'checked')['software' in hobbies],
  'checked-tunning': ('', 'checked')['tunning' in hobbies],
  'age': age or 'Empty',
  'hobbies': ', '.join(hobbies or ['No Hobbies?'])
 }

 status = '200 OK'

 # 这次的content type是text/html
 response_headers = [
  ('Content-Type', 'text/html'),
  ('Content-Length', str(len(response_body)))
 ]

 start_response(status, response_headers)
 return [response_body]

httpd = make_server('localhost', 8051, application)

# 能够一直处理请求
httpd.serve_forever()

print 'end'

启动程序,在浏览器中访问http://localhost:8051/、http://localhost:8051/?age=10&hobbies=software&hobbies=tunning感受一下~

这个程序会一直运行,可以使用快捷键Ctrl-C终止它。

这段代码涉及两个我个人之前没用过的小技巧:

>>> "Age: %(age)s" % {'age':12}
'Age: 12'
>>> 
>>> hobbies = ['software']
>>> ('', 'checked')['software' in hobbies]
'checked'
>>> ('', 'checked')['tunning' in hobbies]
''

解析POST请求

对于POST请求,查询字符串(query string)是放在HTTP请求正文(request body)中的,而不是放在URL中。请求正文在environment字典变量中键wsgi.input对应的值中,这是一个类似file的变量,这个值是一个。The PEP 3333 指出,请求头中CONTENT_LENGTH字段表示正文的大小,但是可能为空、或者不存在,所以读取请求正文时候要用try/except。

下面是一个可以处理POST请求的动态网站:

# ! /usr/bin/env python
# -*- coding: utf-8 -*- 

from wsgiref.simple_server import make_server
from cgi import parse_qs, escape

# html中form的method是post
html = """
<html>
<body>
 <form method="post" action="">
  <p>
   Age: <input type="text" name="age" value="%(age)s">
  </p>
  <p>
   Hobbies:
   <input
    name="hobbies" type="checkbox" value="software"
    %(checked-software)s
   > Software
   <input
    name="hobbies" type="checkbox" value="tunning"
    %(checked-tunning)s
   > Auto Tunning
  </p>
  <p>
   <input type="submit" value="Submit">
  </p>
 </form>
 <p>
  Age: %(age)s<br>
  Hobbies: %(hobbies)s
 </p>
</body>
</html>
"""

def application(environ, start_response):

 # CONTENT_LENGTH 可能为空,或者没有
 try:
  request_body_size = int(environ.get('CONTENT_LENGTH', 0))
 except (ValueError):
  request_body_size = 0

 request_body = environ['wsgi.input'].read(request_body_size)
 d = parse_qs(request_body)

 # 获取数据
 age = d.get('age', [''])[0] 
 hobbies = d.get('hobbies', []) 

 # 转义,防止脚本注入
 age = escape(age)
 hobbies = [escape(hobby) for hobby in hobbies]

 response_body = html % { 
  'checked-software': ('', 'checked')['software' in hobbies],
  'checked-tunning': ('', 'checked')['tunning' in hobbies],
  'age': age or 'Empty',
  'hobbies': ', '.join(hobbies or ['No Hobbies?'])
 }

 status = '200 OK'

 response_headers = [
  ('Content-Type', 'text/html'),
  ('Content-Length', str(len(response_body)))
 ]

 start_response(status, response_headers)
 return [response_body]

httpd = make_server('localhost', 8051, application)

httpd.serve_forever()

print 'end'

Python WSGI入门

这段内容参考自An Introduction to the Python Web Server Gateway Interface (WSGI) 。

Web server

WSGI server就是一个web server,其处理一个HTTP请求的逻辑如下:

iterable = app(environ, start_response)
for data in iterable:
 # send data to client

app即WSGI application,environ即上文中的environment。可调用对象app返回一个可迭代的值,WSGI server获得这个值后将数据发送给客户端。

Web framework/app

即WSGI application。

中间件(Middleware)

中间件位于WSGI server和WSGI application之间,所以

一个示例

该示例中使用了中间件。

# ! /usr/bin/env python
# -*- coding: utf-8 -*- 

from wsgiref.simple_server import make_server

def application(environ, start_response):

 response_body = 'hello world!'

 status = '200 OK'

 response_headers = [
  ('Content-Type', 'text/plain'),
  ('Content-Length', str(len(response_body)))
 ]

 start_response(status, response_headers)
 return [response_body]

# 中间件
class Upperware:
 def __init__(self, app):
  self.wrapped_app = app

 def __call__(self, environ, start_response):
  for data in self.wrapped_app(environ, start_response):
  yield data.upper()

wrapped_app = Upperware(application)

httpd = make_server('localhost', 8051, wrapped_app)

httpd.serve_forever()

print 'end'

然后

有了这些基础知识,就可以打造一个web框架了。感兴趣的话,可以阅读一下Bottle、Flask等的源码。

在Learn about WSGI还有更多关于WSGI的内容。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
python 实现堆排序算法代码
Jun 05 Python
python基于pygame实现响应游戏中事件的方法(附源码)
Nov 11 Python
使用Python的Django和layim实现即时通讯的方法
May 25 Python
Python实现繁?转为简体的方法示例
Dec 18 Python
5分钟 Pipenv 上手指南
Dec 20 Python
在Pycharm中自动添加时间日期作者等信息的方法
Jan 16 Python
python实现动态创建类的方法分析
Jun 25 Python
python selenium循环登陆网站的实现
Nov 04 Python
python 解决flask 图片在线浏览或者直接下载的问题
Jan 09 Python
selenium WebDriverWait类等待机制的实现
Mar 18 Python
Python如何将字符串转换为日期
Jul 31 Python
解决python和pycharm安装gmpy2 出现ERROR的问题
Aug 28 Python
Django进阶之CSRF的解决
Aug 01 #Python
python3利用venv配置虚拟环境及过程中的小问题小结
Aug 01 #Python
mvc框架打造笔记之wsgi协议的优缺点以及接口实现
Aug 01 #Python
python爬虫自动创建文件夹的功能
Aug 01 #Python
浅谈关于Python3中venv虚拟环境
Aug 01 #Python
python Web开发你要理解的WSGI &amp; uwsgi详解
Aug 01 #Python
Django教程笔记之中间件middleware详解
Aug 01 #Python
You might like
php的一个简单加密解密代码
2014/01/14 PHP
PHP调用.NET的WebService 简单实例
2015/03/27 PHP
PHP实现统计在线人数功能示例
2016/10/15 PHP
PHP实现文件上传下载实例
2016/10/18 PHP
php断点续传之文件分割合并详解
2016/12/13 PHP
Yii框架弹出窗口组件CJuiDialog用法分析
2017/01/07 PHP
php实现base64图片上传方式实例代码
2017/02/22 PHP
PHP7使用ODBC连接SQL Server2008 R2数据库示例【基于thinkPHP5.1框架】
2019/05/06 PHP
使用js获取QueryString的方法小结
2010/02/28 Javascript
js日历功能对象
2012/01/12 Javascript
P3P Header解决Cookie跨域的问题
2013/03/12 Javascript
从数据结构的角度分析 for each in 比 for in 快的多
2013/07/07 Javascript
javascript 处理null及null值示例
2014/06/09 Javascript
Extjs实现下拉菜单效果
2016/04/01 Javascript
JS图片定时翻滚效果实现方法
2016/06/21 Javascript
快速解决js中window.location.href不工作的问题
2016/11/02 Javascript
基于jQuery实现滚动刷新效果
2017/01/09 Javascript
Javascript中字符串和数字的操作方法整理
2017/01/22 Javascript
JavaScript方法_动力节点Java学院整理
2017/06/28 Javascript
详解如何用typescript开发koa2的二三事
2018/11/13 Javascript
JavaScript实现横版菜单栏
2020/03/17 Javascript
详解vue高级特性
2020/06/09 Javascript
Java 生成随机字符的示例代码
2021/01/13 Javascript
python实现梯度下降法
2020/03/24 Python
python和php哪个更适合写爬虫
2020/06/22 Python
如何利用python之wxpy模块玩转微信
2020/08/17 Python
python实现视频压缩功能
2020/12/18 Python
CSS3教程:background-clip和background-origin
2008/10/17 HTML / CSS
纯css3实现鼠标经过图片显示描述的动画效果
2014/09/01 HTML / CSS
美国大码时尚女装购物网站:ELOQUII
2017/12/28 全球购物
最新大学毕业求职简历的自我评价
2013/10/18 职场文书
2014年“世界无车日”活动方案
2014/09/21 职场文书
2015年审计人员工作总结
2015/05/26 职场文书
2015最新婚礼司仪主持词
2015/06/30 职场文书
Vue elementUI表单嵌套表格并对每行进行校验详解
2022/02/18 Vue.js
电脑关机速度很慢怎么办 提升电脑关机速度设置教程
2022/04/08 数码科技