Python 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]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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'  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#! /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  
1
2
127.0.0.1 - - [09/Sep/2015 23:39:09] "GET / HTTP/1.1" 200 5540  
end  
可迭代的响应


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




return [response_body]  
1
return [response_body]  
改成:




return response_body  
1
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'  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#! /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  
1
2
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>')  
1
2
3
4
5
6
7
8
9
10
11
12
#! /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;
1
2
3
4
5
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'  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#! /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]
''  
1
2
3
4
5
6
7
8
>>> "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'  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#! /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
1
2
3
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'  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#! /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的内容。


文/letiantian
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值