Python WSGI的深入理解

作者:樂天笔记 时间:2021-04-20 21:48:25 

前言

本文主要介绍的是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']
**********
&lt;script&gt;alert(123);&lt;/script&gt;

然后,我们可以写一个基本的处理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的内容。

来源:https://www.letiantian.me/2015-09-10-understand-python-wsgi/

标签:python,wsgi
0
投稿

猜你喜欢

  • python3中编码获取网页的实例方法

    2023-07-17 23:31:47
  • 基于Python实现图像文字识别OCR工具

    2022-10-19 04:44:42
  • Oracle建立二进制文件索引的方法

    2010-07-18 13:29:00
  • Python 中的集合和字典

    2021-03-18 22:53:30
  • python空值判断方式(if xxx和if xxx is None的区别及说明)

    2022-04-01 20:18:42
  • 网页常用特效整理:中级篇

    2013-07-15 13:43:32
  • 几款Python编译器比较与推荐(小结)

    2022-07-21 01:31:56
  • 使用JScript遍历Request表单参数集合

    2011-02-26 11:08:00
  • 关于python pygame游戏进行声音添加的技巧

    2023-06-25 16:12:30
  • python线程、进程和协程详解

    2023-03-02 14:00:39
  • Python使用Phantomjs截屏网页的方法

    2022-06-17 15:47:14
  • Python 爬虫学习笔记之正则表达式

    2021-07-02 01:50:55
  • python3使用pandas获取股票数据的方法

    2023-01-04 15:01:15
  • 对python 多线程中的守护线程与join的用法详解

    2021-08-11 10:56:51
  • 客户体验

    2008-12-11 13:58:00
  • python绘制字符画视频的示例代码

    2023-11-09 16:21:46
  • python3安装speech语音模块的方法

    2023-03-24 12:09:32
  • python实现斐波那契递归函数的方法

    2022-03-22 19:54:49
  • 在python带权重的列表中随机取值的方法

    2022-05-09 01:44:25
  • python通过定义一个类实例作为ftp回调方法

    2021-08-16 14:12:03
  • asp之家 网络编程 m.aspxhome.com