cherrypy3应用框架结构分析

 

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','',', '')
   
    根据得到的path信息'/helloworld/test/app/show%2Fhello'搜索server中注册的Application,当然 是从最长的路径开始匹配,如果不是完全匹配的话,还要更新enrivron的PATH_INFO信息,这些在后面的处理中还要转化为*args参数。如果 找不到和它匹配的路径,那么直接应答self.simple_response('404 Not Found'), 虽然这个应答很简单,但是它还是遵守WSGI规范的接着就是解析HTTP的版本,envrion['SERVER_PROTOCOL']、envrion ['SERVER_NAME']等等,同时在cherrypy中引入了一个非WSGI的环境变量envrion ['ACTUAL_SERVER_PROTOCOL'],  因为在处理请求的时候envrion['SERVER_PROTOCOL']是取request和server的版本中较低的值的,增加这个属性只是为了 使Application可以更加了解server的状态而已,实际上没有什么很重要的东西,不过这个还是有一个rfc与之对应的,详细的情况 rfc2145。
   
    上面处理的只是Client提交信息的第一行,由于要遵从WSGI规范,所以做了这么多的处理,如果不遵守WSGI规范的话,上面的处理可以简化很多,甚至至可以使用email标准模块一句代码搞定。
   
    接着就是处理request header的信息,cherrypy中使用了mimetools这个标准的模块(在python中已经建议使用email模块进行替换了, cherrpy中还在使用)进行处理,我想这个主要是应为mimetools的Message只是处理http header,而不处理其他东西,这样的话,如果在发送的http stream中包含了一个很大的上传的文件,cherrpy有机会使用cgi FieldStorage来把上传的文件保存为临时文件,以节省占用的内存。
   
    使用mimetools取得了header后,当然要更新到envrion中了,接着就是设置Connection参数,表示是否保持连接(大家可以看 到,如果客户端使用keep-alive参数,从workthread开始会一直地读取客户端地数据),这个设计中一个HTTPConnection可以 处理多个HTTP请求,这个就是HTTP/1.1的所谓连接重用,当然在httpserver accept得到socket生成这个对象的时候就设置了timeout为的就是通过timeout exception来表示是一个连接已经中断,对应的工作线程workthread能够处理下一个请求了,程序中的默认值是1秒。
   
    处理完Connection问题后,接着就是处理'Transfer-Encoding',cherrypy3.0 中目前只支持'chunked' 编码,这种编码方式就是通过自标识(长度或分割符)的形式提供header信息,只是HTTP的一种扩展的处理方式而已,它的目的还是提供header信 息,不过它有一个好处,就是可以预先得到一个数据的长度,从而可以进行缓冲。    
    parse_request()最后要做的就是判断'Content-Length',对于GET模式,Client是不需要提供Content- length的,但是对于POST/PUT模式,由于cherrypy中使用了cgi模块的FieldStorage类(而不是把所有的request header都交给cgi处理,这个是为了遵守WSGI规范所导致的,
    从这个类的源码中可以看到它依赖于一个内容解析的长度,所以cherrypy也验证这个值(如果自己实现,或者改写这个模块的话,可以去掉这个检查,不过 这样不能保证接受到的数据的完整性),如果header中存在'Transfer-Encoding',由于cherrypy缓冲了整个 chuncked,(如果chuncked中包含了关于分界符的描述,那么也许FieldStorage可以从中正确区分各种信息,但是cherrypy 没有做这个判断,我想是应为这些信息一般不会包含在chuncked中吧)。   
    到此为止,request请求header处理基本完成,除了通过enctype="multipart/form-data"提交的数据,这个是在后面通过cgi.FieldStorage处理。
   
  2、我们继续看workthread的下一个处理流程:request.response。
   respond函数的第一句就是WSGI规范调用,self.WSGI_app(self.environ, self.start_response),WSGI规范就是从这里开始的,接着就是非常典型的yield模型:
   for line in response:
    self.write(line)    
   if hasattr(response, 'close'):
    respose.close()    
   if(self.ready and not self.sent_headers and not self.connection.server.interrupt);
    self.sent_heaaders = True
    self.send_headers()
   .....
    def write(self, d):
        if not self.sent_headers:
            self.sent_headers = True
            self.send_headers()
        self.connection.wfile.write(d)
        self.connection.wfile.flush()
    
这 里把代码列出来是要说明两个问题,如果response产生的是一个空的string,那么输出会留在最后,如果response产生的是一个非空 iterator,那么header会在收到第一个非空的string后,这是为了在真正输出之前Application或middleware有机会对 条件做进一步的验证,到这里为止,整个响应的过程(对于WSGI规范中对于Web服务器的规定)已经完成了。

   看来cherrypy的Web服务器部分做的事情并不是很多,它的介绍中的很多特性、功能其实都是WSGI Application 的扩展而已。

   既然这样,我们下面就看看WSGI Application到底又做了些什么。

   还记得吗,WSGI Application是在创建响应结构树tree()对象mount上去的,一个WSGI httpserver是在初始化的时候通过tree.items()建立scirpt_name 到处理Application对象的list,然后在parse_request的时候得到相应的WSGI Application,也就是说归根结底响应就是从Application开始的。恩,现在我们就看看这个WSGI Application到底做了些什么。

   在mount的时候需要提供两个参数,一个是开发者处理的请求的对象,就是那个包含expose属性的方法的"可调用对象"。在 parse_request的时候得到的其实是一个注册的路径,那么这个路径的处理者到底是谁呢?这个搜索的任务其实是WSGI Application根据environ['PATH_INFO']得到的。另外一个参数就是注册的处理路径,它在搜索响应对象的时候使用。下面我们看 看WSGI Application(其实就是一个wrapper)是怎么工作的。

   由于Application对象事实上是一个可调用对象(定义了__call__接口)当HTTPRequest调用respond的时候,传递了 envrion参数和一个start_response函数对象,在envrion参数中包含了关于请求的信息,其中一个决定了调用函数的是 envrion['PATH_INFO']的环境变量,Application在__call__接口中直接调用自身的WSGIapp函数,而 WSGIapp函数还只是一个wrapper,真正进行工作的是一个全局函数wsgi_handler, 除了environ和start_response函数外,它还带了一个指向Application对象的参数,通过这个参数,这个全局函数可以访问关于 这个WSGI Application的所有信息, 下面我们一起看看wsgi_handler到底做了些什么工作。(注意,现在讨论的已经是WSGI规范中关于Application部分的内容,和 Server已经关系不大了)。

在继续讨论WSGI_handler前,我们还有一个问题没有解决,那就是在整个cherrypy的主线程的处理流程中,在进行 cherrypy.server.start()/cherrypy.server.quickstart()后(在调用中已经等待了各个 httpserver启动完成),主线程没有退出(当然它不能退出啦),而是进行一个调用cherrypy.engine.start(),现在我们先来 看一看这个engine到底是负责什么工作的,从前面的讨论看到,server的功能运行好像已经很完整了,除了还需要一个整体监控的线程以外,没有什么 东西需要的了,莫非这个engine就是一个监控的线程?不管它的功能是什么它至少要满足两个条件:

        1、正常情况下,不能退出(否则其他子线程自动退出,服务器不工作了) 
        2、不能占用很多资源,不然其他线程就没得混了。

现在我们就看看这个engine到底负责什么任务:
engine对象在__init__.py中就创建了,所以import cherrypy就能使用,从它的初始化函数就可以猜到它的确是一个监控性质的对象,但是具体是怎么处理的呢?从它的初始化函数中看到有很多list,看 起来是一些钩子,现在只能这么猜测了,而且它的初始化也没有做什么事情。下面我们看看start又是做了些什么:
    果然不出聪明的你所料: self.on_start_engine_list存放的是一些启动钩子,但是你又发现了,从来没有看到过这个 on_start_engine_list初始化,好,就让我们猜一下,也许就是传说中的config作用,还是必须由开发者手工调用注册相应的函数呢, 不过现在先放一下。接着,start函数中启动一个threading.Timer(freq, self.monitor),很明显,这个是一个线程的Timer,用于监控其他部分的操作。最后主线程就进入一个无限的循环,一方面等待可能的用户中 断,另一方面监控,以便随时进行自动重新装载资源。threading.Timer其实就是定时进行检测服务的请求是否操时而已。这里检查 self.severing中的request和response对象。现在我们已经知道了engine到底做了些什么事情,简单的说就是监控 serving队列中的response对象有没有超时,同时负责自动重新装载(这些对象怎么加进去的现放一下,后面将会说明的)。

    现在回到前面的wsgi_handler中,我们再看看WSGI_handler做些什么事情,我晕,wsgi_handler调用 cherrypy.engine.request()函数来生成一个_cprequest.Request对象,注意,这个Request对象是 Application对于Client请求的封装,和一开始的时候Server生成的HTTPRequest不一样,这也难怪,因为 Application本来就独立于Server,它们之间只是通过envrion来传递参数,当然需要各自对数据进行封装了。在request函数中还 通过threading的local设置了线程本地变量request和respose,同时把response加入了监控的serving队列(这个就 是监控线程要检测的对象了)。还有一个有趣的地方是_cprequest.Response对象的创建没有任何的参数,这里就有一个疑问了,这个 response又是做什么的呢,它又怎么知道它自己对应的是哪一个request?

    先不要急,我们先看看Request对象负责什么东西,它的实现有什么特点。

我们先想想它应会有什么功能:
    1、获得header信息?这么获得呢?外面传进去的初始化函数的参数只有localhost、remotehost、

      httpmode、server_protocol
    2、根据请求提供响应,但是不是生成了“独立的response”对象了吗?
    3、错误处理,什么样的情况才是错误呢?
    4、对于PUT、POST方式的客户端请求,接收并处理剩下的接受内容

我就想到这么多了,现在看看是不是这样的。

__init__很简单,就是几个变量的赋值,同时copy一下类的默认配置(这里有一个引起我注意的东西,就是nanmspaces)在说明文档 上说,可以用这种方式注册对应的处理函数),就这样一个Application的Request对象生成了,还没有体现出来我们对它的作用的猜测。

   回到wsgi_handler。晕,原来我们猜测的request的作用是在这里通过直接给request对象增加属性来实现的:


request.login = env('LOGON_USER') or env('REMOTE_USER') or None   
request.multithread = environ['WSGI.multithread']
request.multiprocess = environ['WSGI.multiprocess']
request.WSGI_environ = environ
request.app = app

接着就是进行真正的响应request.run,这里有些地方要注意的:
1、在run调用中提供了完整的调用路径env('SCRIPT_NAME', '') + env('PATH_INFO', ''),前面分析知道
   响应对象是由Applicaiton自己决定的,也就是说必须由Application重新解析请求,从而得到响应的函数
   和位置参数信息
2、 run调用中还单独提供了environ['WSGI.input'],这也说明了这个参数的重要性,对于PUT、POST调用方式这样的参数,我们可以 猜想这个可能是解析输入参数的调用,因为在request对象的初始化函数并没有做实际上的工作。正如我们猜测的,在Request.run里进行了 Client请求的重新组装、HTTP协议版本的对比、Cookie的生成Header信息的记录(和Server做了一些重复的工作,这个是WSGI规 范所不能避免的,也造成了一定性能的下降)这里最重要的是进行script_name和path_info的重新计算(path_info)这两个是为了 划分具体的响应函数的必要信息(说些题外话,由于cherrypy每次都是动态的寻找响应函数,这会造成一定性能的下降,如果引入zope中的 interface机制,可以在服务器启动的时候就建立起映射关系dict,这样可以加速查找的过程,我改写了一些代码
后在我自己的笔记本上测可以上到600+ r/ps)。

    现在可以正式响应用户请求了->self.respond(),这个函数是最耗时的操作

   晕,(源代码)真多try。。。。。。

   第一个try是为了响应中断,第二个中断是为了释放资源,第三个try是为了用raise的方式实现goto的功能,这个我喜欢^_^。

respond的主要做了这么几件事:
1、解析header,这里的解析其实最主要就是要处理HTTPRedirects??我不是很明白 这是什么意思??我觉得这个只是为了在其他的Server提供的信息header信息中处理mime的编码问题,如:RFC2047等说白了就是“杞人忧 天”的做法,对于cherrypy来说在Server处理请求的时候就使用了mimetool来过滤了这个也增加了cherrypy的响应时间。
  
2、 提取资源,这里有提取资源的算法??包括静态文件和动态响应对象?(cherrypy还复制了一份钩子)晕,get_resource这个名字实在让我产 生错觉--》打开一个文件,果然是unix的味道,everything is file, everything is resource, 其实get_resource是把请求发送到真正的处理函数的,在这个函数里是通过self.dispatch来进行 请求调度的,现在先不看这个 dispatch怎么实现调度,我们先看看这里一个不起眼的语句:
 nodeconf = self.app.config.get(trail, {})
一个简单的语句可以联想两件事:

      1、这个config是什么东西,什么时候初始化,如果初始化,它的结构是什么样子的呢?
      2、开发者可以自己定义一个关于这个节点的配置(包括:发布器、参数、处理方式等)这个果然是强啊。
   正如cherrypy所说的,绝大部分的资源都是通过动态的加载到一个request的属性当中,例如:request.config = {*****},不过比较搞的是,在Application中只有一个方式可以修改这个app.config,那就是调用app.merge,而这个 merge又是调用_cpconfig.merge,也就是说,一切的“cherrypy”提供的配置都是从_cpconfig模块开始的。在这里有了这 个整体上的认识就可以了。
   
   现在看看这个Dispatch是怎么工作的:
   Dispatch是一个可调用对象,在__call__中它是通过threading local来得到当前的request对象,然后根据提供的path_info创建一个handler, 寻找处理的开发者提供的函数是由 find_handler()完成的,find_handler是怎样做到的呢:
   find_handler通过threading.local获得request,注意:这个request是application的request抽 象而不是Server的HTTPRequest对象,再从request得到开发者注册(mount)时提供的可调用对象这里有一个配置选项, find_handler首先检查root是否具'_cp_config'属性(事实上这是一个字典,包含兼容的配置信息),如果有,那么把它的信息更新 到这个节点的配置上,如果'/'的配置在app.config中,那么更新这个节点的配置信息,这里使用相对路径'/'作为判断,使得用户的这个类可以应 用到任何的路径下。 find_handler的寻路算法其实很冗肿,同时它是多次更新一个节点的配置,算法如下:
    根据但前节点是否包含了下一级的路径对象(注意:在这里算法并没有检查对应的对象是否具有expose属性)如果存在对应的属性,那么更新对应节点的 config信息,这个config可以包含了回调单独的配置信息,有则更新。同时检查app中实现关于这个节点的config信息,这样就实现了一个路 径信息的历遍算法。同时在这个算法中,可以看到关于路径的配置信息和可调用对象的配置是分离的,也就是说即使没有对应的可调用对象也可以实现响应的配置, 还有一个好玩的东西就是:可以通过嵌套的类或者方法封装整个站点目录结构(不过这个方式比较怪)。但是这也有好处就是可以通过upvalue机制访问上一 个节点的信息(写到这里,我突然想起了zope的acquire机制,好像就是这个味道,难道它就是这样实现的?这里先卖个关子,有兴趣的读者可以看看我 的另一篇blog:“zope设计体系分析”,呵呵,这个还在写)

   路径信息已经分割已经完成了,那么下一步就是从这些信息中找到最好的匹配,或者是default,注意index是可以省略的。我们留意一下这个细节上的 东西,为了使用最长的匹配方式,cherrypy是从最长的路径开始匹配的,在搜索中如果可调用对象为None,那么直接跳到下一个分割的信息项(但是划 分的时候为什么遇到None的时候不直接中止呢,真是晕,这是一个效率问题)寻找最匹配的响应算法是这样子的:
   假设提交的是:http://localhost:8080/hello/world/show?name=hellowworld
   首先处理的'default',由于在生成候选路径的时候已经认为的增加了一个'index'元素,所以最先检测的是
   /hello/world/show/index/default->
   /hello/world/show/index->
   /hello/world/show/default->
   /hello/world/show(匹配)
  
匹配算法得到最佳的处理对象后通过set_conf()内建函数产生所有的路径配置信息,这里有一个比较特别的地方
那 就是对于'tools.staticdir.dir'配置是每次都会用最长的路径(这里是绝对路径)来替代,原因也许是出于这样的考虑,在用户使用 'tools.staticdir.dir'配置时时在类的内部定以的,它只能用相对的路径表示,但是当类挂载到(mount)整个应用中的时候(尤其是 对于虚拟路径来说),它的相对路径也许是不对的,而静态文件的处理是完全依赖于绝对路径,所以作出了这样的修改。当然目前只是猜测,等到我们一起看 config机制的时候,真相将会大白^_^。
  
说到这里,其实只是找到了响应的对象(如果有),还没有进行参数解析和传递,不过 find_handler返回了一个路径的list实际上里面放的就是路径参数。根据find_handler得到的结果,cherrypy把这些东西封 装成一个LateParamPageHandle()对象,注意;这里有一个很有趣的名字late,这暗示到目前为止还是没有调用这个处理函数,但是到底 要等到什么时候才会调用呢?而在这段时间里又会发生什么事会阻止这个调用的发生?

总的来说在find_handler这个函数产生了一个handler,并加到request的handler属性中。

   让我们继续往下看,现在我们已经看到一切都已经准备好了(可以随时调用request.handler得到响应的输出),对了,参数还没有准备好。我们回 到request.respond函数继续看:get_resource后调用的是self.configure,那么这个configure又是什么东 西,完成什么功能?看configure自己的说明说,是要融合self.config(在get_resource中生成)和self.toolmap 的配置。

  configure函数首先做的是从前面生成的config中划分出第一个名空间:"tools.a.b"->(tools, a.b),如果tools在self.namespaces中的话,那么就调用响应的名空间处理函数,其实也就是相当于钩子的功能,只是把这种钩子用名空 间的方式处理,这种调用是以(a.b, self.config['tools.a.b'])作为参数的,这样可以进一步区分调用的对象,而且这种调用会在每一个请求中完成,其实这种形式的钩 子,我唯一想到的用的地方就是中间件,但是它并不遵从WSGI规范,也就是说这个是cherrypy设计上的一个特色.

   在这个函数里终于要牵涉到cherrypy中推崇备至的tools配置系统,现在先略去它的细节(后面再来看),在这里使用cherrypy.tool这 个是在__init__.py中赋值的一个默认的ToolBox,具体的初始化在_cptools.py中,现在先跳过。如果不深入到tools的机制, 那么这个configure函数也就做了两件很简单的事:

      1、根据配置,注册对应的(cherrypy 名空间钩子)

      2、启动注册的Tool(应为在cherrypy中所有的动态配置都是以一个独立的Tool负责的,所以启动Tool,实际上就是启动一个过滤钩子,我没有使用过其他版本的cherrypy,但是从它的介绍文档看到以前的版本是静态配置策略的)。

   在完成了这个configure后,我们回到respond函数中,接着,cherrypy启动hook机制,也就是传说中的HookMap, self.hooks.run('on_start_resource')但是对于开发者来说可以在什么地方注册一个自己的hook呢?看来只有在一个负 责响应的类的定义的时候进行了,难道可以在一个tool的setup中完成??应为从逻辑上来看tool的setup是在启动钩子之前,而tool的配置 是可以通过在类或者请求的响应者中定义的。cherrypy也是这么设计的,而且它比我想得周到,它直接提供了一个'hook'的namespaces, 这样的话,开发者就可以在响应的最小的局部定义自己的hook函数来验证一些事情,当然,在响应的过程中也是可以验证的(这种情况适合开发新的系统,但是 对于要改进或者扩展一个现有的系统,那么这种hook机制就会很有用,因为在配置中会很容易的看到验证的顺序,同时也可以分离验证的细节)。这个 config是在路径划分的时候生成的,它是根据app.config来的,
这个app.config是在mount的时候提供的,同时在划分路径的时候它会合并全局的配置和Application的配置。

   好,现在回归到respond的主流程,configure函数后进行的是self.hooks.run('on_start_resource'),然 后是检查request中是否需要处理request_body,对于PUT和POST方式的请求,Client是会提供request_body的,聪 明的你肯定记得在WSGIServer中只是处理了header部分,而没有处理request_body,现在就需要处理这个东西了,当然,这个首先需 要检查body的大小了。从这里也看到,到现在为止,还是不能提供足够的参数调用handler,例如:request_body中提供了上传的文件或者 是form的其他变量,这也体现了所谓的"Late"。
   接着我们看看这是怎么处理request_body的:
   正如我们前面所说的,这里对request_body的处理使用的是python的标准模块cgi中的FieldStorage,cgi的 FieldStorage对于大文件的处理是通过产生临时磁盘文件的,而不是全部放在内存中,在cherrypy的FieldStorage是通过继承 cgi.FieldStorage来得到对应的处理功能,同时覆盖了三个函数read_lines_to_eof、 read_lines_to_outerboundary、skip_lines这些覆盖的函数没有做过多的事情它们只有一个地方和 cgiFieldStorage不一样,就是每次read/readline的时候设定了一个最大值(1<<16), cgi.FieldStorage会返回
一个用属性标识的对象,来表征解析得到的参数。

   现在我们看看这个标准的cgi.FieldStorage是怎么工作的,其实cgi.FileStorage的工作原理很简单,它根据读入的(也可以说是 传入的,因为大部分是传入的,但同时也有读入的)Client request header信息,生成多个与它对应的request_body抽象与它对应,这个抽象就是FieldStorage,一个FieldStorage可以 包含很多个下级FieldStorage,和MiniFiledStorage,从而形成树形的结构,最后得到的信息保存方式可以通过这么几个属性得到: field_storage_obj.list--->保存了很多的FieldStorage对象,这个FieldStorage实际上是针对文件 上传设计的,然后兼容了很多其他的field,这个从它的属性说明中就能反映出来,这里简单地讲一下它地设计思路:一个以mutiple part形式上传的Client request body是以特定的outerboundary作为边界,FieldStorage的处理方式就是读取这个分界的内容,然后本地缓冲这些内容,当发现长度 超出了允许的最大长度时生成一个临时文件与它对应(也就是将原来的StringIO流转变为文件)这个就是cgi.FieldStorage最吸引人的地 方,我想也是这个原因cherrypy还在使用mimetools与cgi模块配合,FieldStorage在处理提交数据的时候对于每一个 boundary分割的元素都用独立的FiledStorage来保存的,只是在给用户返回的时候才将相同的名称对应的值转化为数组,而没有使用 email标准模块的原因,到现在为止我没有细看email模块,只是试用了几个功能,发现email好像是全部内存缓冲数据的。

    搞清楚了cgi.FieldStorage的功能(其实cherrypy中只是使用了它的部分功能,因为从cherrypy调用FieldStorage 的时候设定了METHOD为POST就可以看出来了),再回到respond函数继续往下看,在process_body后,就是 self.hooks.run('before_handler')。等了这么久,终于等到真正的开发者提供的handler调用 cherrypy.response.body = self.handler(),这里有一个比较神奇的地方,就是PageHandler封装了处理函数的
调用,它传递参数的方式是通过描述符的方 式在request.params中提取的,这个就是所谓的延迟调用的实质。当然处理的输出会以属性的方式连接到 cherry.response.body上。接着就是self.hooks.run('before_finalize'),这个 self.hooks.run('on_end_resource')是在finally中调用的,为的就是保证资源的释放(对于有共享池的应用这个是很 重要的)

   现在看看respond的最后一个调用是:cherrypy.response.finalize(),这个调用使人看起来莫名奇妙,一时不知道到底有什 么作用,话又说回来在cherrypy中很多时候都是直接通过cherrypy.request/cherrypy.response这样的访问,耦合性 真是比较大啊。。。。。。

   现在我们看看这个response.finalize()做了些什么东西:
   对比于request的九曲十三弯的调用流程,response的处理是在是出奇的简单,在初始化函数中它生成了简单的SimpleCookie,如果开 发者要使用这个东西,那么他需要在相应的处理函数中通过cherrypy.response.cookie的方式使用,其实这个就是所谓的封装。还有什么 更简单的呢,然后这个response.finalize就是把响应的header和cookie全部转换为一个list-->((key, value),(key,value)....)就是这个样子,可以不费力气的想象,最后输出的时候就是把这个东西连接成string(当然包括 cookie),那么就好了,当然这里还有一个隐含的作用就是把刚才修改的response.body属性整理为一个字符串,这里也隐式地限制了开发者提 供的返回值可以是一个字符串,也可以是一个string的list这个就是这种转换的代码:
 newbody = ''.join([chunk for chunk in self.body])

说到这里,一个请求的处理逻辑已经结束了,现在就是回到request.run中,是怎么把处理的结果返回到WSGIServer的: request.run直接把cherrypy.response返回到WSGI_handler, 对于无异常的流程,WSGI_handler调用WSGI server提供的start_response来通知WSGI服务器准发送响应,接下去的事情不用多说了,肯定就是返回一个generator啦。

到这里为止,cherrypy的整个处理流程的主干已经一目了然了。

下面,我们还要了解些什么呢?对了,还有config模块和tool模块的工作方式,可以说这些额外的模块才是一个框架的成功与否的关键,因为一个 HTTP服务器对于标准的实现流程基本相同的(以后我们看看其他框架(如:django、zope等)的实现就知道了),而与这个框架绑在一起的功能模块 的性能优劣程度直接决定它的可用性与易用性。

   我们看看config的实现,其实config是和tool和namespaces是联合起来使用的,那我们先看看config的使用: _cpconfig文件其实很简洁,这个config类只是简单地继承了dict, 但是有一个很特别地地方就是它覆盖了__setitem__函数,为的是要在更新dict的时候调用该名空间的处理函数,事实上这个server、 log、engine这三个名空间处理函数也只是简单地把对应属性加到该对象上。也就是说,_config.py做地事情其实也是非常的简单,它只是把配 置文件的信息对出来,在需要使用的时候以字典的形式组装给用户。同时它还提供了对这些字典信息合并的功能,注意,只有使用config.update时才 会触发对应的名空间处理函数,同时这里还有一个额外的功能,就是会将以文件形式提供的配置文件名记录下来,当系统重启的时候,会自动地加载这些配置文件, 这里有一个风险,就是如果以配置文件加载地话,必须把配置目录的路径写全否则不能进行正确的配置。

   这是通过配置文件或者由程序动态提供配置的方式,也就是说,必须在程序中手工地调用config.update,注意,这个config可以是一个普通的 字典又或者是普通的字典,如果是普通的字典,同时开发者又需要触发相应的名空间处理函数的话,那么必须由开发者处理这些细节。另外一种方法是在定义的 WSGI Application中定义_cp_config = {'tools.gzip.on': True},同样它也是一个字典,在ini配置文件中,是需要指出路径的,如;[/path/to/config],但是由于是在处理函数上进行配置,所 以可以省略路径,直接写配置的信息就可以了(省了一层字典,就是这样),在cherrypy处理请求的过程中会自动地加载这些config并作为相应的环 境参数(前面的分析中也看到了,一个request最后会有一个request.config属性表示它的配置,而且这个配置是覆盖性质的,也就是说,越 明确的路径配置会取代相对不明确的配置信息) 

   在_cpconfig中我看不到预想中的影响实际运行的操作,而只是简单的更新字典,而只有使用cherrypy.config进行全局参数update 的时候才会进行响应名空间的函数调用,也就是是说在寻路分割的时候配置信息的更新只是非常平常的字典更新操作,而且request和response都是 通过threading.local实现的线程局部变量,它的配置不会影响到其他线程的参数变量(环境变量)

   现在只有从_cptool上找可能存在的影响操作流程的操作了,有了前面的猜测和对cherrypy设计思路的了解,我们很快就会发现我们需要的信息,这 里以static的处理为例说明一下,其他的tool的实现思路基本相似(选择static作为例子,是因为它包含了很多特性)。static实际上是一 个HandlerTool,它是注册在
'beforehandler'钩子下的,如果在某一个mount的路进下进行了_cp_config={ 'tools.staticdir.on': True,}的配置,那么在寻路分割的时候就会把这个配置加入到对应request.config中,然后在调用request.configure的时 候就会检查"on"属性是否为True, 如果是,那么就会把整个staticdir tool安装到'before_handler'钩子
上, 然后在下一步就会调用这个钩子,注意了,在handlertool钩子处理的过程中,它会根据钩子处理函数的返回值动态地决定是否进行原来的处理函数的调 用,也就是说即使这个目录mount的时候就进行了staticdir.on 的设置,但是路径解析还是会进行的(这个也是一个时间的浪费)。

   好了,在cherrypy的最后一个问题是度于不同的request些tool是怎么区分的呢,答案就Tool类的_setup()函数是把对应的钩子函 数挂接到cherrypy.request.hooks变量上的,还只得把,cherrypy.request是一个线程local来的,这样就可以完成 线程(request)独立了。

   讲到这里,整个cherrypy的工作流程也基本上清楚了,内部的数据流向和操作调度结构也基本分析完了,我想有了这样的分析过程,自己也可以写一个比较完整的HTTP服务了,同时也知道了需要注意的细节。

   其实这篇介绍的主要目的是为了说明一个用python实现的HTTP框架其实不是很复杂,只要动手的话,每个人都可以实现自己的HTTP应用框架。

3、WSGI规范简介:

   a、目的: 为了协调现存的python web框架的融合,说白了,就是一个写好的应用,在进行可以尽可能少的改动下在另一个web Server下发布
  
   b、主要概念:
      WSGI SERVER:面向浏览器的应用程序
      WSGI APPLICTION:处理用户逻辑的可调用对象
      WSGI MIDDLEWARE:位于SERVER和APPLICATION之间的可调用对象(这中东西一般是在改动APPLICARION的发布SERVER的时 候引入,为的是调整新的SERVER和APPLICATION的差异,可以把它看成一个WAPPER)
                 
   c、规范协议:(这里只简单地说说它定义的调用流程,详细说明参考PEP333)为了方便大家对照英文看,下面就直接用英文画图了
    
 CLIENT                    SERVER                          APPLICATION
         socket              |                               
  ------------------------>accept                                   
                             |
                        read and parse header 
                       
                             |
                       init envrion dict    
                             |       
                       call application ----------------->callable fucntion
                      (environ, start_response)                  |
                                         ^              process header from envrion
                                         |                        |
                                         |---------------------call start_response
                               (bodys)                            |
                              |<----------------------------return a iter or string list
                      for body_block in bodys:
                         send data to Client
   <---------------------------|
  
  
  
对了,由于session经常使用,这里看看一下cherrypy的section是怎么实现的,这里以FileSession为代表把:
Sessction 也是一个Tool那么它也必须注册到hook中,这里有一点特别的是,它注册了3个hook,同时它注册的函数是sessions.py中的共用函数,这 几个函数都是wrapper,它们封装了session的继承关系,session的处理(继承流程和调用流程是cheerypy中类的继承机制用的比较 灵活的地方,也就是说它通过了基类的统一接口实现了不同层次的session功能封装),这里有一个特点,就是它的seesion的过期处理是在load 第一个session的时候启动一个Timer Thread实现的。同样的,为了实现session的线程独立,它也是通过threading.local实现session的访问。

   cherrypy中用了很多关于hook的封装,这使得阅读者不易理解,这里讲一下它的参数传递过程,是我们有一个清晰的概念,参数的开始源自进行路径分 割的时候得到的request.config(这个config包含了全局的配置,单路径的配置等),然后在request.configure函数调用 的时候配置要注册的hook(其实这里大部分就是tool的注册),在生成Hook对象的时候,根据以点分割的串建立字典参数列表:例如: 'tools.session.storage_type':'File'
那么在调用sessions.init的时候它的其中一个参数就是storage_type = 'File'。这个就是cherrypy的tool/hook机制。

 

(本blog信息均为原创,装载请注明出处^_^)

你可能感兴趣的:(cherrypy3应用框架结构分析)