webpy源码简单分析 1. 框架代码结构 2. 数据处理流程 3. 具体代码分析 4. 总结

文章目录

到现在断断续续学习python也将近半个月了,总觉得书本(《Python基础教程.第二版》)上的项目有些过于理想化,可以用来巩固书本前面章节的语法知识(而我基本没怎么看前面的语法~),了解python的一些标准库,就是过于单调。

好在开源的python的项目众多,而且搭建学习环境简单迅速。

考虑到那些大而且知名的项目往往经过高度的发展,过多的版本迭代,封装、抽象的过于复杂,了解其整个的架构设计,类层次结构,调用栈都不是短期之功,与我快速学习python的目的不符,所以最终定下来web.py这个半学术型的项目,该项目短小精悍,又符合我服务器开发的职业身份,适合现在的我阅读,而且了解这个后,我今后用其写点小东西也方便快捷。

web.py是一款轻量级的Python web开发框架,简单、高效、学习成本低,代码量少,以至于特别适合作为python web开发的入门框架。

官方站点:http://webpy.org/

1. 框架代码结构

我学习的对象是webpy-webpy-0.38版本,其大致目录结构如下(仅保留必要的源码部分):

webpy-webpy-0.38
│  code.py
├─docs     
├─experimental    
├─templates
│      index.html    
├─test    
├─tools      
└─web
    │  application.py
    │  browser.py
    │  db.py
    │  debugerror.py
    │  form.py
    │  http.py
    │  httpserver.py
    │  net.py
    │  python23.py
    │  session.py
    │  template.py
    │  test.py
    │  tree.txt
    │  utils.py
    │  webapi.py
    │  webopenid.py
    │  wsgi.py
    │  __init__.py
    ├─contrib    
    └─wsgiserver
            ssl_builtin.py
            ssl_pyopenssl.py
            __init__.py

对于不关心框架源码的开发者而言,其需要关注的是根目录下的app入口code.py文件以及templates文件夹下的的网页模板文件。

而本文,关注的是web.py框架源码主要集中的web文件夹,文件不多,但其中有我需要学习的很多东西。几个需要关注的文件:

  • application.py 实现开发者的应用启动,应用响应最终的回调处理
  • db.py 数据库的简单封装,根据应用开发的需求,可用可不用,上层业务使用
  • form.py 表单处理,上层业务使用
  • session.py cookies机制搞得session管理吧,上层业务使用
  • template.py 网页模板的实现,上层业务使用
  • httpserver.py 顾名思义了,http服务器,但看源码,并不具体实现监听和处理请求,根据不同需求可定制不同处理模式。
  • wsgi.py 几种不同的符合wsgi规范的服务器运行模式
  • webapi.py wsgi规范接口的一些包装
  • wsgiserver 该文件夹下为独立程序运行的http服务器实现,整个框架的最底层,端口监听,http请求处理的实现

2. 数据处理流程

先看看web.py框架从接受客户端请求到返回这个机制的流程:

+---------------------------------+
|                                 |
|          application            |
|                                 |
+---------------------------------+
|                                 |
|       WSGIGateway(Gateway)      |
|                                 |
+---------------------------------+
|                                 |
|  CherryPyWSGIServer(HTTPServer) |
|                                 |
+-----------+--------+------------+
            ^        |
            |        |
            |        |
            |        v
     +------+--------+-------+
     |                       |
     |   client or browser   |
     |                       |
     +-----------------------+

3. 具体代码分析

那么,跟着上图的这个流程,咱们来看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class (HTTPServer):
wsgi_version = (1, 0)
def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None,
max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5):
self.requests = ThreadPool(self, min=numthreads or 1, max=max)
self.wsgi_app = wsgi_app
self.gateway = wsgi_gateways[self.wsgi_version]
self.bind_addr = bind_addr
if not server_name:
server_name = socket.gethostname()
self.server_name = server_name
self.request_queue_size = request_queue_size
self.timeout = timeout
self.shutdown_timeout = shutdown_timeout
self.clear_stats()
...

CherryPyWSGIServer继承自HTTPServer,他的主要工作是初始化了http请求的requests线程池,确定最终响应的app,然后确定网关和监听地址,之后socket设置、端口绑定、监听都继承自父类处理。

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
class ThreadPool(object):
def __init__(self, server, min=10, max=-1):
self.server = server
self.min = min
self.max = max
self._threads = []
self._queue = Queue.Queue()
self.get = self._queue.get
...
class WorkerThread(threading.Thread):
...
def run(self):
self.server.stats['Worker Threads'][self.getName()] = self.stats
try:
self.ready = True
while True:
conn = self.server.requests.get()
if conn is _SHUTDOWNREQUEST:
return
self.conn = conn
if self.server.stats['Enabled']:
self.start_time = time.time()
try:
conn.communicate()
finally:
conn.close()
if self.server.stats['Enabled']:
self.requests_seen += self.conn.requests_seen
self.bytes_read += self.conn.rfile.bytes_read
self.bytes_written += self.conn.wfile.bytes_written
self.work_time += time.time() - self.start_time
self.start_time = None
self.conn = None
except (KeyboardInterrupt, SystemExit), exc:
self.server.interrupt = exc

httpserver中初始化的线程池ThreadPool内含有一个线程安全消息队列,由Queue.Queue()构造,而ThreadPool内的线程都为WorkerThread。该队列内缓存那些刚到的http连接对象,然后由数个WorkerThread线程持续不断的去消息队列中取出http连接对象来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class HTTPConnection(object):
...
def communicate(self):
request_seen = False
try:
while True:
req = None
req = self.RequestHandlerClass(self.server, self)
req.parse_request()
if self.server.stats['Enabled']:
self.requests_seen += 1
if not req.ready:
return
request_seen = True
req.respond()
if req.close_connection:
return
...

这便是之前放进队列中的http连接对象,省去了其他部分,主要看communicate()函数,HTTPConnection对象被RequestHandlerClass转换成HTTPRequest对象之后,解析req.parse_request(),最后响应req.respond()。
至于解析,http协议本身都是文本,熟悉服务器开发的应该都了解,不明白网上也有很多资料,这里就不展开说明解析函数了。
看看HTTPRequest对象的响应函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def respond(self):
"""Call the gateway and write its iterable output."""
mrbs = self.server.max_request_body_size
if self.chunked_read:
self.rfile = ChunkedRFile(self.conn.rfile, mrbs)
else:
cl = int(self.inheaders.get("Content-Length", 0))
if mrbs and mrbs < cl:
if not self.sent_headers:
self.simple_response("413 Request Entity Too Large",
"The entity sent with the request exceeds the maximum "
"allowed bytes.")
return
self.rfile = KnownLengthRFile(self.conn.rfile, cl)
self.server.gateway(self).respond()
if (self.ready and not self.sent_headers):
self.sent_headers = True
self.send_headers()
if self.chunked_write:
self.conn.wfile.sendall("0rnrn")

呵呵,看着一大串,其实也就一行self.server.gateway(self).respond(),就是说响应交给网关WSGIGateway(Gateway)处理了,也就是上图中第二层了。呼,终于往上走了一层!

最早CherryPyWSGIServer(HTTPServer)初始化时self.gateway = wsgi_gateways[self.wsgi_version]来初始化的网关,看wsgi_gateways字典,发现WSGIGateway_10是真正使用的网关,而WSGIGateway_10继承WSGIGateway类,只实现get_environ方法而已,也就是真响应在这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class WSGIGateway(Gateway):
...
def respond(self):
response = self.req.server.wsgi_app(self.env, self.start_response)
try:
for chunk in response:
if chunk:
if isinstance(chunk, unicode):
chunk = chunk.encode('ISO-8859-1')
self.write(chunk)
finally:
if hasattr(response, "close"):
response.close()
...

这里最终交给了早已初始化好的APP处理,wsgi_app(self.env, self.start_response),

交给app后WSGIGateway(Gateway)层接受请求部分完成,

application层根据具体业务逻辑,路由指定web页处理,然后返回出response对象,

WSGIGateway(Gateway)将response对象写入预先保持的HTTPRequest对象持有由socket构造的可写二进制文件描述符中。

至此,一次客户端http请求的过程完全结束!

4. 总结

web.py的这种3层的设计,我从上到下姑且称为应用层代理层网络层(注:这里并不对应OSI网络模型)。

这种设计很好的隔离了业务逻辑和网络通信底层,上层应用的开发,完全无需理会底层的变动,根据文档说明,web.py可以很容易的切换到第三方http服务器(如Apache)处理网络底层。

这一切都源于代理层的网关将底层实现隔离,切换只需要更改更改HTTPServer的gateway属性而已。


版权声明:

本文由greedcoder创作、发表并维护,

采用自由分享-保留署名-非商用-禁止演绎4.0(CC BY-NC-ND 4.0)国际许可协议进行许可.

版权有一点,侵权不一定究!

本文永久链接:https://greedcoder.github.io/2016/09/04/webpy-source-analysis/