【python web学习】python web窥探

本文为参考网上一些博客翻译以及想法,自己写的一篇总结博文,可能有重复的地方,纯粹总结只用。
阅读之前可参考:

1、How to write a web framework in Python(作者anandology

web.py代码的两位维护者之一,另一位则是大名鼎鼎却英年早逝的AaronSwartz
2、Why so many Python web frameworks? 也是一篇很好的文章,也许它会让您对Python中Web框架的敬畏之心荡然

无存:-)

如果你打算用python进行网络开发的话,自己写的框架可以说是一种不受支持的想法,可能使用一个现成的Web框架(如DjangTornadoweb.pyPylons等)会是更合适的选择,毕竟都是大师级的作品。

一、一次最简单的web之旅

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""myweb.py"""
from wsgiref.simple_server import make_server, demo_app
httpd = make_server('', 8086, demo_app)
sa = httpd.socket.getsockname()
print 'http://{0}:{1}/'.format(*sa)
# Respond to requests until process is killed
httpd.serve_forever()

在命令运行之后

image_mark

打开浏览器:http://0.0.0.0:8086/

一行”Hello world!” 和 众多环境变量值。

image_mark

定位到simple_server.py文件,我们看到make_server函数和WSGIServer类

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
def make_server(
host, port, app, server_class=WSGIServer, handler_class=WSGIRequestHandler
):
"""Create a new WSGI server listening on `host` and `port` for `app`"""
server = server_class((host, port), handler_class)
server.set_app(app)
return server
class WSGIServer(HTTPServer):
"""BaseHTTPServer that implements the Python WSGI protocol"""
application = None
def server_bind(self):
"""Override server_bind to store the server name."""
HTTPServer.server_bind(self)
self.setup_environ()
def setup_environ(self):
# Set up base environment
env = self.base_environ = {}
env['SERVER_NAME'] = self.server_name
env['GATEWAY_INTERFACE'] = 'CGI/1.1'
env['SERVER_PORT'] = str(self.server_port)
env['REMOTE_HOST']=''
env['CONTENT_LENGTH']=''
env['SCRIPT_NAME'] = ''
def get_app(self):
return self.application
def set_app(self,application):
self.application = application

可以看到,我们运行python文件后启动的是WSGIServer类对象(继承于HTTPServer,子类有run函数,后文会详细讲一下),而demo_app是一个拥有特定格式:接受两个参数,一个列表return

对象的函数,抑或是类、类对象(见下文)。
很多时候,要简单写一个web框架,主要需要改动传入的app以及server。

二、app的修改

其中,可调用对象 包括 函数、方法、类 或者 具有call方法的 实例;environ 是一个字典对象,包括CGI风格的环境变量(CGI-style environment variables)和 WSGI必需的变

量(WSGI-required variables);start_response 是一个可调用对象,它接受两个常规参数(status,response_headers)和 一个 默认参数(exc_info);字符串迭代对象 可以是 字符

串列表、生成器函数 或者 具有iter方法的可迭代实例。更多细节参考Specification Details

The Application/Framework Side中给出了一个典型的application实现:

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""application.py"""
def simple_app(environ, start_response):
"""Simplest possible application object"""
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return ['Hello world!\n']

替换原来自带的demo_app,重新运行之

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""code.py"""
from wsgiref.simple_server import make_server
from application import my_app as app
if __name__ == '__main__':
httpd = make_server('', 8086, app)
sa = httpd.socket.getsockname()
print 'http://{0}:{1}/'.format(*sa)
#Respond to requests until process is killed
httpd.serve_forever()

这时就输出hello world!而没有环境变量。因为demo_app.py是这样的:

1
2
3
4
5
6
7
8
9
10
def demo_app(environ,start_response):
from StringIO import StringIO
stdout = StringIO()
print >;>;stdout, "Hello world!"
print >;>;stdout
h = environ.items(); h.sort()
for k,v in h:
print >;>;stdout, k,'=', repr(v)
start_response("200 OK", [('Content-Type','text/plain')])
return [stdout.getvalue()]

三、URL调度修改

之前的访问server都是基于host+port的形式,那要怎样实现url的分发呢。这需要对app进行修改才行。说到这里,就先将app从一个函数改为一个类吧,再做url区分处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""application.py"""
class my_app:
def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response
def __iter__(self):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
self.start(status, response_headers)
yield "Hello world!\n"

复习一下python类的语法,说说为什么可以这样写。开始的app可以这样用

1
list = simple_app(a,b)

现在也可以这样用

1
list = my_app(a,b)

注:其中参数来自init(),返回值来自iter()的return值(yield返回的就是一个可迭代对象),也许你会问,如果是传类对象的话呢?且看下下文。

再在return的函数即iter()中修改根据不同的path进行不同返回。

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 -*-
"""application.py"""
class my_app:
def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response
def __iter__(self):
path = self.environ['PATH_INFO'] #environ的作用看到了吧
if path == "/":
return self.GET_index()
elif path == "/hello":
return self.GET_hello()
else:
return self.notfound()
def GET_index(self):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
self.start(status, response_headers)
yield "Welcome!\n"
def GET_hello(self):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
self.start(status, response_headers)
yield "Hello world!\n"
def notfound(self):
status = '404 Not Found'
response_headers = [('Content-type', 'text/plain')]
self.start(status, response_headers)
yield "Not Found\n"

这时用浏览器就可以访问/,/hello,其他访问为Not Found。

四、重构

1、正则匹配URL

消除URL硬编码,增加URL调度的灵活性:

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""application.py"""
import re ##########修改点
class my_app:
urls = (
("/", "index"),
("/hello/(.*)", "hello"),
) ##########修改点,Django工程中url.py即视感
def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response
def __iter__(self): ##########修改点
path = self.environ['PATH_INFO']
method = self.environ['REQUEST_METHOD']
for pattern, name in self.urls:
m = re.match('^' + pattern + '$', path) #注意这里,url匹配函数名
if m:
#pass the matched groups as arguments to the function
args = m.groups()
funcname = method.upper() + '_' + name
if hasattr(self, funcname):
func = getattr(self, funcname)
return func(*args)
return self.notfound()
def GET_index(self):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
self.start(status, response_headers) #遵循调用start_response后再return iterObject
yield "Welcome!\n"
def GET_hello(self, name): ##########修改点
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
self.start(status, response_headers)
yield "Hello %s!\n" % name
def notfound(self):
status = '404 Not Found'
response_headers = [('Content-type', 'text/plain')]
self.start(status, response_headers)
yield "Not Found\n"

2、消除GET_*方法中的重复代码,并且允许它们返回字符串:

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""application.py"""
import re
class my_app:
urls = (
("/", "index"),
("/hello/(.*)", "hello"),
)
def __init__(self, environ, start_response): ##########修改点
self.environ = environ
self.start = start_response
self.status = '200 OK'
self._headers = []
def __iter__(self): ##########修改点
result = self.delegate() #利用这个函数先进行返回结果的收集
self.start(self.status, self._headers) #start_response
# 将返回值result(字符串 或者 字符串列表)转换为迭代对象
if isinstance(result, basestring):
return iter([result])
else:
return iter(result)
def delegate(self): ##########修改点
path = self.environ['PATH_INFO']
method = self.environ['REQUEST_METHOD']
for pattern, name in self.urls:
m = re.match('^' + pattern + '$', path)
if m:
# pass the matched groups as arguments to the function
args = m.groups()
funcname = method.upper() + '_' + name
if hasattr(self, funcname):
func = getattr(self, funcname)
return func(*args)
return self.notfound()
def header(self, name, value): ##########修改点
self._headers.append((name, value))
def GET_index(self): ##########修改点
self.header('Content-type', 'text/plain')
return "Welcome!\n"
def GET_hello(self, name): ##########修改点
self.header('Content-type', 'text/plain')
return "Hello %s!\n" % name
def notfound(self): ##########修改点
self.status = '404 Not Found'
self.header('Content-type', 'text/plain')
return "Not Found\n"

3、抽象出框架

为了将类my_app抽象成一个独立的框架,需要作出以下修改:

1、剥离出其中的具体处理细节:urls配置 和 GET_*方法(改成在多个类中实现相应的GET方法)

2、把方法header实现为类方法(classmethod),以方便外部作为功能函数调用

3、改用 具有call方法的 实例 来实现application(上文提到)

修改后的application.py(最终版本):

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""application.py"""
import re
class my_app:
"""my simple web framework"""
headers = []
def __init__(self, urls=(), fvars={}):
self._urls = urls
self._fvars = fvars
def __call__(self, environ, start_response):
self._status = '200 OK' # 默认状态OK
del self.headers[:] # 清空上一次的headers
result = self._delegate(environ)
start_response(self._status, self.headers)
# 将返回值result(字符串 或者 字符串列表)转换为迭代对象
if isinstance(result, basestring):
return iter([result])
else:
return iter(result)
def _delegate(self, environ):
path = environ['PATH_INFO']
method = environ['REQUEST_METHOD']
for pattern, name in self._urls:
m = re.match('^' + pattern + '$', path)
if m:
# pass the matched groups as arguments to the function
args = m.groups()
funcname = method.upper() # 方法名大写(如GET、POST)
klass = self._fvars.get(name) # 根据字符串名称查找类对象
if hasattr(klass, funcname):
func = getattr(klass, funcname)
return func(klass(), *args)
return self._notfound()
def _notfound(self):
self._status = '404 Not Found'
self.header('Content-type', 'text/plain')
return "Not Found\n"
@classmethod
def header(cls, name, value):
cls.headers.append((name, value))

到这里,基本上就算是小功告成了,但只是了解了怎么用那些子类,大篇幅还是讲怎么设计。窥探一下wsgiref.simple_server。

五、wsgiref原理介绍

1、概述

a.什么是WSGI, WSGI application, WSGI server, WSGI middleware.

WSGI是关于Python脚本与Web服务器交互的协议,wsgi将 web 组件分为三类: web服务器,web中间件,web应用程序。

image_mark

b.WSGI Server有哪些

比如 Django、CherryPy 都自带 WSGI server,主要是测试用途, 发布时则使用生产环境的 WSGI server,例如Apache,nginx等,而有些 WSGI 下的框架比如 pylons、bfg 等, 自己不实现 WSGI server。

wsgiref就是python自带的WSGI server。上面提到的app需要传入的两个参数application(environ, start_response),其实就是一个接口两个参数的集合体。一篇博文
这样说明:

wsgi server 基本工作流程:

1、服务器创建socket,监听端口,等待客户端连接。

2、当有请求来时,服务器解析客户端信息放到环境变量environ中,并调用绑定的handler来处理请求。

3、handler解析这个http请求,将请求信息例如method,path等放到environ中。

4、wsgi handler再将一些服务器端信息也放到environ中,最后服务器信息,客户端信息,本次请求信息全部都保存到了环境变量environ中。

5、wsgi handler 调用注册的wsgi app,并将environ和回调函数传给wsgi app

6、wsgi app 将reponse header/status/body 回传给wsgi handler

7、最终handler还是通过socket将response信息塞回给客户端。

2、组成(python2.7.8)

image_mark

image_mark

simple_server

这一模块实现了一个简单的 HTTP 服务器,并给出了一个简单的 demo,运行:

python simple_server.py

会启动这个demo,运行一次请求,并把这次请求中涉及到的环境变量在浏览器中显示出来。

handlers

simple_server模块将HTTP服务器分成了 Server 部分和Handler部分,前者负责接收请求,后者负责具体的处理, 其中Handler部分主要在handlers中实现。

headers

这一模块主要是为HTTP协议中header部分建立数据结构。

util

这一模块包含了一些工具函数,主要用于对环境变量,URL的处理。

validate

这一模块提供了一个验证工具,可以用于验证你的实现是否符合WSGI标准。

simple_server 模块主要有两部分内容,上面一到四的内容可以总结。

应用程序

函数demo_app是应用程序部分

服务器程序

服务器程序主要分成Server 和 Handler两部分,另外还有一个函数 make_server 用来生成一个服务器实例

各种继承关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# M:
# +------------+
# | BaseServer |
# +------------+
# |
# V
# +------------+
# | TCPServer |
# +------------+
# |
# V
# +------------+
# | HTTPServer |
# +------------+
# |
# V
# +------------+
# | WSGIServer |
# +------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# M:
# +--------------------+
# | BaseRequestHandler |
# +--------------------+
# |
# V
# +-----------------------+
# | StreamRequestHandler |
# +-----------------------+
# |
# V
# +------------------------+
# | BaseHTTPRequestHandler |
# +------------------------+
# |
# V
# +--------------------+
# | WSGIRequestHandler |
# +--------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# M:
# +-------------+
# | BaseHandler |
# +-------------+
# |
# V
# +----------------+
# | SimpleHandler |
# +----------------+
# |
# V
# +---------------+
# | ServerHandler |
# +---------------+

在调用make_server的时候,都发生了什么

image_mark

再这里,就不细讲handler的处理过程了,很多时候网络handler的研究需要看源码才能真正消化。

以下完全引用on_1y的一篇博文,该博文讲得很细,不过需要研究源码才能真正消化。可以先看doc:https://docs.python.org/2/library/wsgiref.html
再看源码:https://pypi.python.org/pypi/wsgiref

headers

image_mark

这个模块是对HTTP 响应部分的头部设立的数据结构,实现了一个类似Python 中 dict的数据结构。可以看出,它实现了一些函数来支持一些运算符,例如 len, setitem,

getitem, delitem, str, 另外,还实现了 dict 操作中的get, keys, values函数

util
image_mark

这个模块主要就是一些有用的函数,用于处理URL, 环境变量。


validate

image_mark

这个模块主要是检查你对WSGI的实现,是否满足标准,包含三个部分:

  • validator
  • Wrapper
  • Check

validator 调用后面两个部分来完成验证工作,可以看出Check部分对WSGI中规定的各个部分进行了检查。

Comments