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实现bitmap数据结构详解
Feb 17 Python
python根据路径导入模块的方法
Sep 30 Python
在Python中使用mongoengine操作MongoDB教程
Apr 24 Python
python访问抓取网页常用命令总结
Apr 11 Python
Python中单、双下划线的区别总结
Dec 01 Python
python3 flask实现文件上传功能
Mar 20 Python
python print输出延时,让其立刻输出的方法
Jan 07 Python
简单了解python的一些位运算技巧
Jul 13 Python
python生成requirements.txt的两种方法
Sep 18 Python
selenium+python实现自动登陆QQ邮箱并发送邮件功能
Dec 13 Python
Python利用命名空间解析XML文档
Aug 10 Python
详解pycharm的python包opencv(cv2)无代码提示问题的解决
Jan 29 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
40年前的这部特摄片恐龙特级克塞号80后的共同回忆
2020/03/08 日漫
ThinkPHP表单自动验证实例
2014/10/13 PHP
Yii2中datetime类的使用
2016/12/17 PHP
jQuery 源码分析笔记(7) Queue
2011/06/19 Javascript
jquery 之 $().hover(func1, funct2)使用方法
2012/06/14 Javascript
js禁止document element对象选中文本实现代码
2013/03/21 Javascript
解析jQuery与其它js(Prototype)库兼容共存
2013/07/04 Javascript
nodejs中操作mysql数据库示例
2014/12/20 NodeJs
javascript将DOM节点添加到文档的方法实例分析
2015/08/04 Javascript
javascript iframe跨域详解
2016/10/26 Javascript
关于Javascript中document.cookie的使用
2017/03/08 Javascript
使用JS实现图片轮播的实例(前后首尾相接)
2017/09/21 Javascript
详解ES6中的三种异步解决方案
2018/06/28 Javascript
AngularJs返回前一页面时刷新一次前面页面的方法
2018/10/09 Javascript
python 随机数生成的代码的详细分析
2011/05/15 Python
用Python代码来绘制彭罗斯点阵的教程
2015/04/03 Python
Python函数可变参数定义及其参数传递方式实例详解
2015/05/25 Python
Python文件读取的3种方法及路径转义
2015/06/21 Python
Django中login_required装饰器的深入介绍
2017/11/24 Python
Python图形绘制操作之正弦曲线实现方法分析
2017/12/25 Python
python dataframe astype 字段类型转换方法
2018/04/11 Python
详解Python3的TFTP文件传输
2018/06/26 Python
python实现词法分析器
2019/01/31 Python
通过实例了解Python str()和repr()的区别
2020/01/17 Python
python thrift 实现 单端口多服务的过程
2020/06/08 Python
Python xlrd模块导入过程及常用操作
2020/06/10 Python
HTML5各种头部meta标签的功能(推荐)
2017/03/13 HTML / CSS
什么是重载?CTS、CLS和CLR分别做何解释
2012/05/06 面试题
大学生求职简历的自我评价
2013/10/21 职场文书
消防器材管理制度
2014/01/28 职场文书
六查六看自查材料
2014/02/17 职场文书
党员2014两会学习心得体会
2014/03/17 职场文书
公司年会主持词
2014/03/22 职场文书
美术兴趣小组活动总结
2014/07/07 职场文书
三年级上册科学教学计划
2015/01/21 职场文书
十大最强电系宝可梦,阿尔宙斯电系之一,第七被称为雷神
2022/03/18 日漫