本文为参考网上一些博客翻译以及想法,自己写的一篇总结博文,可能有重复的地方,纯粹总结只用。
阅读之前可参考:
1、How to write a web framework in Python(作者anandology是
web.py代码的两位维护者之一,另一位则是大名鼎鼎却英年早逝的AaronSwartz )
2、Why so many Python web frameworks? 也是一篇很好的文章,也许它会让您对Python中Web框架的敬畏之心荡然
无存:-)
如果你打算用python进行网络开发的话,自己写的框架可以说是一种不受支持的想法,可能使用一个现成的Web框架(如Djang、Tornado 、web.py 、Pylons等)会是更合适的选择,毕竟都是大师级的作品。
一、一次最简单的web之旅
1 2 3 4 5 6 7 8 9 10 11
| """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) httpd.serve_forever()
|
在命令运行之后
打开浏览器:http://0.0.0.0:8086/
一行”Hello world!” 和 众多环境变量值。
定位到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): 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
| """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
| """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) 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
| """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可以这样用
现在也可以这样用
注:其中参数来自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
| """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'] 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
| """application.py""" import re class my_app: urls = ( ("/", "index"), ("/hello/(.*)", "hello"), ) 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) if m: 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) 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
| """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) 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: 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
| """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' del self.headers[:] result = self._delegate(environ) start_response(self._status, self.headers) 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: args = m.groups() funcname = method.upper() 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应用程序。
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)
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
|
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
|
在调用make_server的时候,都发生了什么
再这里,就不细讲handler的处理过程了,很多时候网络handler的研究需要看源码才能真正消化。
以下完全引用on_1y的一篇博文,该博文讲得很细,不过需要研究源码才能真正消化。可以先看doc:https://docs.python.org/2/library/wsgiref.html
再看源码:https://pypi.python.org/pypi/wsgiref
headers
这个模块是对HTTP 响应部分的头部设立的数据结构,实现了一个类似Python 中 dict的数据结构。可以看出,它实现了一些函数来支持一些运算符,例如 len, setitem,
getitem, delitem, str, 另外,还实现了 dict 操作中的get, keys, values函数
util
这个模块主要就是一些有用的函数,用于处理URL, 环境变量。
validate
这个模块主要是检查你对WSGI的实现,是否满足标准,包含三个部分:
validator 调用后面两个部分来完成验证工作,可以看出Check部分对WSGI中规定的各个部分进行了检查。