cherrypy3应用框架结构分析:
最近在学习python和zope,本来希望可以比较详细的分析一下zope的设计的,但是由于很久都没有写这么正式的文字了,写了一部分以后觉得有点乱,总决得组织不好,只好先找个简单的框架作为练手,于是写了这篇关于cherrypy3的分析。
cherrypy3是一个纯python的web开发框架,它的使用非常简单,而且也提供了很多开发者可以使用的配置。
1、cherrpypy3模块划分(系统能工作的最简单模型)
a.服务器监听模块 _cpserver,可以同时管理多个server
b.响应数据组织模型 _cptree, 使用字典组织响应模型,同时每一个节点均可以
进行配置
c.响应和请求的数据抽象 _cprequest Request/Respose
d.WSGI规范的实现 _cpWSGI/_cpWSGIserver
2、系统启动流程
__init__.py 中cherrpy3创建了server对象(只是一个设置了host和port的数据结构)、log对象(使用了python标准模块logging)、 tree对象(一个映射脚本名到WSGI响应的字典),使用threading local 实现线程的request、response的独立访问。
这些对象创建后,系统已经具备了数据管理的功能,但是并没有响应任何请求的能力。
一个典型的应用(这里指动态响应,静态映射事实上就是一个hook,在这个hook中屏蔽了正常的handler),就是由开发人员写一个响应对象(比如 类、函数,反正是一切可以call的,同时存在expose为true属性的对象)然后注册到响应的tree中,想想,这其实是很有unix/linux 味道,就连注册的函数也叫mount,注册的时候,cherrypy创建一个WSGI Application(为了遵从WSGI规范)与它对应(注意:这里要求的expose属性纯粹是作者的规定,如果我要写一个框架,我也会有很多自己的 规范的^_^),到了这里,系统还只是具备了响应的数据维护,而没有接受请求的能力。
接着开发者调用quickstart(这里指cherrypy.server.quickstart()),如果使用的是 cherrypy.quickstart,它内部会依次调用 tree.mount/server.star/engine.start由于quickstart内部调用的自身的server信息,所以一般来说,它 只适用于只有一个server的时候,这时候可以直接通过server.socket_port、server.socket_host来设定 server的信息(如果在调用quickstart的时候没有提供server实例,也没有提供server的模块名,那么默认是创建 WSGIServer,这就是前面提到的tree节点创建的是WSGI Application,到这里为止,还没有看到这个WSGI Server 和WSGI Application 是怎样联系起来,它们是怎样知道对方的存在的,聪明的你肯定想到是全局量,就是__init__.py 中的几个对象)。
系统资源(sockets)的分配并不在创建WSGIServer这一步,_cpwsgi中的WSGIServer是继承 _cpwsgiserver中的CherryPyWSGIServer(绕这么一圈cherrypy所说是为了独立_cpwsgiserver的功能,我 们先不管这个,反正一般的设计思路就是先实现功能,然后开始尽可能的独立没个模块,软件工程上美其名曰:降低耦合性,最恨这些创造拗口名词的人,这使我联 想起马克思主义哲学——把大家都懂的东西用陌生的名词包装,使很多人都不懂,以证明高深莫测)。
在WSGIServer的__init__函数中调用了CherryPyWSGIServer初始化,为了独立CherryPyWSGIServer,必 不可少的在它的__init__函数中提供了一堆参数,实在有点过分了(虽然有的参数有默认值),同时在这里可以看到cherrypy使用 cherry.tree.items(),提供整个服务结构的数据结构。而在CherryPyWSGIServer中就通过 self.mount_points这个list来维护相应的信息([(script_name, WSGI_app),(...)]),同时CherryPyServer会对mount_point进行排序,以优先匹配最长的路径(这也是很合理的)。
好了,到了这一步,其实一切资源还是没有真正分配,真是见鬼。
在一切都初始化完成后,下一步好戏要上演了:
cherrypy.server.quickstart函数中调用->self.start(),惊天地,泣鬼神的事情要发生了: self.start函数根据self.httpservers(一个字典:httpserver(一个数据结构,和实际server关系其实不是很大, 如:CherryPyWSGIServer)-->(host,port)),调用self._start_http,不看不知道,一看吓一跳,原 来没有个httpserver是单独使用一个线程处理的(Threading),这就是cherrypy所谓的支持任服务器数的本质。
我们继续看看self._start_http_thread做了些什么工作,它调用httpserver.start()函数,把启动的任务交给 httpserver本身,这也无可厚非,毕竟cherrypy也不知道这些server到底要怎么启动,另一方面也说明了只要实现了start接口,任 何东西都可以作为httpserver,哪怕它只是一个在stdout输出hello,world就退出的程序。
好了,看来一切的事情将会发生在每个httpserver中,那我们来看看CherryPyWSGIServer是怎么做的(以后我们来看看其他的Web框架如:zope/django/Karrigell/***又是怎样实现HTTP服务的)。
httpserver.start做的事情很简单,就是绑定一个设置的端口,然后在那里无限循环,等待请求的到来这个是主线思路,为了能够处 理并发的请求,无可避免地引入了多线程(当然,在zope中是使用IO服用机制的),所以在start里cherrypy创建了多个workthread (又人为的引入了一个类),这些workThread就在那边不断的循环等待用户请求的到来。
说到这里又被聪明的你发现了,既然一个httpserver是一个单独的线程,而这个单独的线程又产生多个workThread,那么 workThread与httpserver thread是怎么通讯的呢?主线程在那边accept来accept去的,workthread怎么知道什么时候有任务到啊?而这多个 workthread又是怎么同步的呢?万一它们发生抢活干的事情,这个世界就不和谐了。
我想大家也许都会想到使用互斥量来访问共享资源,cherrypy也不例外,不过正如python的口头禅一样:“我就是库多,其他没什么”, cherrypy中使用了异步Queue这个类,它就帮我们处理了多线程互斥的问题,而不需要我们自己Lock->Use->unLock。 就是这么简单,cherrypy已经可以接受外界的请求了。
恩,如果单单实启动一个监听服务,用不多于10行代码就可以了,这么麻烦的启动流程就是为了传说中的高扩展和高定制。。。。。。
好,废话少说(其实已经说了不少了^_^),现在才是正场,前面是广告。
一个HTTP Server的性能是在于它对请求的处理能力和处理时间:
当浏览器发送一个http请求到服务器的时候,它的过程大致如下:域名解析->建立TCP socket->打包数据(说白了就是发送一个字符串,为了传送一些二进制文件,有时会对发送内容进行编码或者压缩gzip)->等待服务器 的响应->根据服务器返回的信息(最主要是要根据响应报文的header信息对后续的内容进行解码,输出),从客户端的整个处理流程可以看到,一个 http请求是在一个socket中完成的,就是非常传统的停止等待协议,同时也可以看到,为了使发送内容的能够被接受方理解,除了内容本身外,还要附带 一些描述信息,request header 和response header就是这些描述信息,这么看来HTTP协议也就是这么一回事,它具备了任何一种C/S结构协议的特点:描述+内容,只不过它比较牛,成了国际标 准,美其名曰:B/S结构。
转入正题:
cherrypy的httpserver在进入无限循环socket.accetp()后,当客户端请求到时,httpserver主线程得到 accept的socket,然后进行数据的读入并将它映射到HTTPRequest对象,接着将这个对象加入到Queue中,接着这个Request就 会等待workThread的处理了。主线程就会继续accept下一个请求。这个就是httpserver主线程的无限循环的主要工作。
那我们现在看看,cherrypy是怎样把浏览器提交的数据映射为Request对象的:(写到这里,突然又感觉到很浓的unix/linux味道)将浏 览器提交的数据映射为request是最基本的思路,另外在cherrypy中接收到Client的请求时是先生成一个HTTPConnection对象 表示逻辑连接,并把这个对象加入到等待处理的队列中,workthread就可以响应每个请求了),这个HTTPConnection的主要用途就是完成 输入数据的读取,并按照WSGI(WSGI规范在后面会提要性的总结一下的,现在把它理解为两个接口定义就好了)的规范,同时 HTTPConnection还支持http和https,果然考虑周到(我们现在先从简单的http开始),在HTTPConnection的 __init__中主要是进行envrion的 WSGI.input/WSGI.url_schema/REMOTE_PORT/REMOTE_ADDR/REMOVE_HOST等的设置,接着就是调 用HTTPConnection的communicate对Client发送的数据进行Mime type的处理,这里还包含了错误信息的处理,如果发现错误,那么就会对Client进行直接的响应,而不是加入大队列中等待workthread接手处 理。(可以预示,这个会是一个瓶颈,如果Client上传一个很大的文件的话,到底是不是这样的呢?还HTTPConenction只是处理最基本的文件 头部分呢?)
我们继续往下看:
communicate主要做两件事:
1、根据输入初始化HTTPRequest对象,req = self.RequestHandlerClass(self)req.parse_request(),这个调用没有传参数,说明在它使用了对象创建时 使用的HTTPConnection对象。呵呵,不得不说,这里很多部分的相关程度很高啊^_^
2、调用req.respond()进行响应,现在我们来看看workthread到底做了些什么东西,它到底做了些什么东西??
communicate函数的两个任务是怎么完成的,它们到底做了些什么事情(为了实现对于单独的路径进行参数配置
在cherrypy中到处可以看到dict.copy(),dict.update(),在代码风格上实在不怎么好看):
首先看看parse_request():
1、根据HTTP协议RFC2616,parse_request首先根据浏览器提交的信息更新envrion中的REQUEST_METHOD和wsgi.url_scheme,
接着就是从url分析http请求,这个直接影响到这个是否是一个有效的http请求:
http://localhost:8080/helloworld/test/app/show%2Fhello?name=hello%2Fworld&user=admin
('http', 'localhost:8080', /helloworld/test/app/show%2Fhello','',', '')
&n