那么webpy是什么呢? 阅读它的源码我们又能学到什么呢?

简单说webpy就是一个开源的web应用框架(官方首页:http://webpy.org/

它的源代码非常整洁精干,学习它一方面可以让我们快速了解python语法(遇到看不懂的语法就去google),另一方面可以学习到python高级特性的使用(譬如反射,装饰器),而且在webpy中还内置了一个简单HTTP服务器(文档建议该服务器仅用于开发环境,生产环境应使用apache之类的),对于想简单了解下HTTP服务器实现的朋友来说,这个是再好不过的例子了(并且在这个服务器代码中,还可以学习到线程池,消息队列等技术),除此之外webpy还包括模板渲染引擎,DB框架等等,这里面的每一个部分都可以单独拿出来学习.

在JavaWeb开发中有Servlet规范,那么Python Web开发中有规范吗? 
答案就是:WSGI,它定义了服务器如何与你的webapp交互

关于WSGI规范,可以参看下面这个链接: 
http://ivory.idyll.org/articles/wsgi-intro/what-is-wsgi.html

现在我们利用webpy内置的WSGIServer,按照WSGI规范,写一个简单的webapp,eg:

  1. #/usr/bin/python
  2. import web.wsgiserver
  3. def my_wsgi_app(env, start_response):
  4. status = '200 OK'
  5. response_headers = [('Content-type','text/plain')]
  6. start_response(status, response_headers)
  7. return ['Hello world!']
  8. server = web.wsgiserver.CherryPyWSGIServer(("127.0.0.1", 8080), my_wsgi_app);
  9. server.start()

执行代码:

 
在具体看WSGIServer代码之前,我们先看一幅图,这幅图概述了WSGIServer内部执行流程:

接下来我们看下代码,ps: 为了较清晰的梳理主干流程,我只列出核心代码段

  1. # Webpy内置的WSGIServer
  2. class CherryPyWSGIServer(HTTPServer):
  3. def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None,
  4. max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5):
  5. # 线程池(用来处理外部请求,稍后详述)
  6. self.requests = ThreadPool(self, min=numthreads or 1, max=max)
  7. # 响应外部请求的webapp
  8. self.wsgi_app = wsgi_app
  9. # wsgi网关(http_request ->wsgi_gateway ->webpy/webapp)
  10. self.gateway = WSGIGateway_10
  11. # wsgi_server监听地址
  12. self.bind_addr = bind_addr
  13. # ...
  14. class HTTPServer(object):
  15. # 启动一个网络服务器
  16. # 如果你阅读过<<Unix网络编程>>,那么对于后面这些代码将会再熟悉不过,唯一的区别一个是c,
  17. #一个是python
  18. def start(self):
  19. # 如果bind_addr是一个字符串(文件名),那么采用unix domain协议
  20. if isinstance(self.bind_addr, basestring):
  21. try: os.unlink(self.bind_addr)
  22. except: pass
  23. info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)]
  24. else:
  25. # 否则采用TCP/IP协议
  26. host, port = self.bind_addr
  27. try:
  28. info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
  29. socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
  30. except socket.gaierror:
  31. # ...
  32. # 循环测试 getaddrinfo函数返回值,直到有一个bind成功或是遍历完所有结果集
  33. for res in info:
  34. af, socktype, proto, canonname, sa = res
  35. try:
  36. self.bind(af, socktype, proto)
  37. except socket.error:
  38. if self.socket:
  39. self.socket.close()
  40. self.socket = None
  41. continue
  42. break
  43. if not self.socket:
  44. raise socket.error(msg)
  45. # 此时socket 进入listening状态(可以用netstat命令查看)
  46. self.socket.listen(self.request_queue_size)
  47. # 启动线程池(这个线程池做些什么呢? 稍后会说)
  48. self.requests.start()
  49. self.ready = True
  50. while self.ready:
  51. # HTTPSever核心函数,用来接受外部请求(request)
  52. # 然后封装成一个HTTPConnection对象放入线程池中的消息队列里,
  53. # 接着线程会从消息队列中取出该对象并处理
  54. self.tick()
  55. def bind(self, family, type, proto=0):
  56. # 创建socket
  57. self.socket = socket.socket(family, type, proto)
  58. # 设置socket选项(允许在TIME_WAIT状态下,bind相同的地址)
  59. self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  60. # socket bind
  61. self.socket.bind(self.bind_addr)
  62. # HTTPSever核心函数
  63. def tick(self):
  64. try:
  65. # 接受一个TCP连接
  66. s, addr = self.socket.accept()
  67. # 把外部连接封装成一个HTTPConnection对象
  68. makefile = CP_fileobject
  69. conn = self.ConnectionClass(self, s, makefile)
  70. # 然后把该对象放入线程池中的消息队列里
  71. self.requests.put(conn)
  72. except :
  73. # ...

之前我们说过HTTPServer中的request属性是一个线程池(这个线程池内部关联着一个消息队列),现在我们看看作者是如何实现一个线程池的:

  1. class ThreadPool(object):
  2. def __init__(self, server, min=10, max=-1):
  3. # server实例
  4. self.server = server
  5. # 线程池中线程数配置(最小值,最大值)
  6. self.min = min
  7. self.max = max
  8. # 线程池中的线程实例集合(list)
  9. self._threads = []
  10. # 消息队列(Queue是一个线程安全队列)
  11. self._queue = Queue.Queue()
  12. # 编程技巧,用来简化代码,等价于:
  13. # def get(self)
  14. #    return self._queue.get()
  15. self.get = self._queue.get
  16. # 启动线程池
  17. def start(self):
  18. # 创建min个WorkThread并启动
  19. for i in range(self.min):
  20. self._threads.append(WorkerThread(self.server))
  21. for worker in self._threads:
  22. worker.start()
  23. # 把obj(通常是一个HTTPConnection对象)放入消息队列
  24. def put(self, obj):
  25. self._queue.put(obj)
  26. # 在不超过允许创建线程的最大数下,增加amount个线程
  27. def grow(self, amount):
  28. for i in range(amount):
  29. if self.max > 0 and len(self._threads) >= self.max:
  30. break
  31. worker = WorkerThread(self.server)
  32. self._threads.append(worker)
  33. worker.start()
  34. # kill掉amount个线程
  35. def shrink(self, amount):
  36. # 1.kill掉已经不在运行的线程
  37. for t in self._threads:
  38. if not t.isAlive():
  39. self._threads.remove(t)
  40. amount -= 1
  41. # 2.如果已经kill掉线程数小于amount,则在消息队列中放入线程退出标记对象_SHUTDOWNREQUEST
  42. # 当线程从消息队列中取到的不是一个HTTPConnection对象,而是一个_SHUTDOWNREQUEST,则退出运行
  43. if amount > 0:
  44. for i in range(min(amount, len(self._threads) - self.min)):
  45. self._queue.put(_SHUTDOWNREQUEST)
  46. # 工作线程WorkThread
  47. class WorkerThread(threading.Thread):
  48. def __init__(self, server):
  49. self.ready = False
  50. self.server = server
  51. # ...
  52. threading.Thread.__init__(self)
  53. def run(self):
  54. # 线程被调度运行,ready状态位设置为True
  55. self.ready = True
  56. while True:
  57. # 尝试从消息队列中获取一个obj
  58. conn = self.server.requests.get()
  59. # 如果这个obj是一个“退出标记”对象,线程则退出运行
  60. if conn is _SHUTDOWNREQUEST:
  61. return
  62. # 否则该obj是一个HTTPConnection对象,那么线程则处理该请求
  63. self.conn = conn
  64. try:
  65. # 处理HTTPConnection
  66. conn.communicate()
  67. finally:
  68. conn.close()

刚才我们看到,WorkThread从消息队列中获取一个HTTPConnection对象,然后调用它的communicate方法,那这个communicate方法究竟做了些什么呢?

  1. class HTTPConnection(object):
  2. RequestHandlerClass = HTTPRequest
  3. def __init__(self, server, sock, makefile=CP_fileobject):
  4. self.server = server
  5. self.socket = sock
  6. # 把socket对象包装成类File对象,使得对socket读写就像对File对象读写一样简单
  7. self.rfile = makefile(sock, "rb", self.rbufsize)
  8. self.wfile = makefile(sock, "wb", self.wbufsize)
  9. def communicate(self):
  10. # 把HTTPConnection对象包装成一个HTTPRequest对象
  11. req = self.RequestHandlerClass(self.server, self)
  12. # 解析HTTP请求
  13. req.parse_request()
  14. # 响应HTTP请求
  15. req.respond()

在我们具体看HTTPRequest.parse_request如何解析HTTP请求之前,我们先了解下HTTP协议. HTTP协议是一个文本行的协议,它通常由以下部分组成:

引用
请求行(请求方法 URI路径  HTTP协议版本) 
请求头(譬如:User-Agent,Host等等) 
空行 
可选的数据实体

而HTTPRequest.parse_request方法就是把socket中的字节流,按照HTTP协议规范解析,并且从中提取信息(最终封装成一个env传递给webapp):

  1. def parse_request(self):
  2. self.rfile = SizeCheckWrapper(self.conn.rfile,
  3. self.server.max_request_header_size)
  4. # 读取请求行
  5. self.read_request_line()
  6. # 读取请求头
  7. success = self.read_request_headers()
  8. # ----------------------------------------------------------------
  9. def read_request_line(self):
  10. # 从socket中读取一行数据
  11. request_line = self.rfile.readline()
  12. # 按照HTTP协议规范,把request_line分割成请求方法(method),uri路径(uri),HTTP协议版本(req_protocol)
  13. method, uri, req_protocol = request_line.strip().split(" ", 2)
  14. self.uri = uri
  15. self.method = method
  16. scheme, authority, path = self.parse_request_uri(uri)
  17. # 获取uri请求参数
  18. qs = ''
  19. if '?' in path:
  20. path, qs = path.split('?', 1)
  21. self.path = path
  22. # ----------------------------------------------------------------
  23. def read_request_headers(self):
  24. # 读取请求头,inheaders是一个dict
  25. read_headers(self.rfile, self.inheaders)
  26. # ----------------------------------------------------------------
  27. def read_headers(rfile, hdict=None):
  28. if hdict is None:
  29. hdict = {}
  30. while True:
  31. line = rfile.readline()
  32. # 把line按照":"分割成k, v,譬如 Host:baidu.com就被分割成Host和baidu.com两部分
  33. k, v = line.split(":", 1)
  34. # 格式化分割后的
  35. k = k.strip().title()
  36. v = v.strip()
  37. hname = k
  38. # HTTP协议中的有些请求头允许重复(譬如Accept等等),那么webpy就会把这些相同头的value用","连接起来
  39. if k in comma_separated_headers:
  40. existing = hdict.get(hname)
  41. if existing:
  42. v = ", ".join((existing, v))
  43. # 把请求头k, v存入hdict
  44. hdict[hname] = v
  45. return hdict

至此我们就分析完了HTTPRequest.parse_request方法如何解析HTTP请求,下面我们就接着看看HTTPRequest.respond如何响应请求:

  1. def respond(self):
  2. # 把请求交给gateway响应
  3. self.server.gateway(self).respond()

在继续往下看代码之前,我们先简单思考下,为什么要有这个gateway,为什么这里不把请求直接交给webapp处理? 
我自己觉得还是出于分层和代码复用性考虑。因为可能存在,或者需要支持很多web规范,目前我们使用的是wsgi规范,明天可能出来个ysgi,大后天可能还来个zsgi,如果按照当前的设计,我们只需要替换HTTPServer的gateway属性,而不用修改其他代码(类似JAVA概念中的DAO层),下面我们就来看看这个gateway的具体实现(回到本文最初,我们在Server中注册的gateway是WSGIGateway_10):

WSGI网关

  1. class WSGIGateway(Gateway):
  2. def __init__(self, req):
  3. self.req = req  # HTTPRequest对象
  4. self.env = self.get_environ()
  5. # 获取wsgi的环境变量(留给子类实现)
  6. def get_environ(self):
  7. raise NotImplemented
  8. def respond(self):
  9. # -----------------------------------
  10. # 按照 WSGI 规范调用我们得 webapp/webpy
  11. # -----------------------------------
  12. response = self.req.server.wsgi_app(self.env, self.start_response)
  13. # 把处理结果写回给客户端
  14. for chunk in response:
  15. self.write(chunk)
  16. def start_response(self, status, headers, exc_info = None):
  17. self.req.status = status
  18. self.req.outheaders.extend(headers)
  19. return self.write
  20. def write(self, chunk):
  21. # 写http响应头
  22. self.req.send_headers()
  23. # 写http响应体
  24. self.req.write(chunk)

WSGIGateway_10继承WSGIGateway类,并实现get_environ方法

    1. class WSGIGateway_10(WSGIGateway):
    2. def get_environ(self):
    3. # build WSGI环境变量(req中的这些属性,都是通过HTTPRequest.prase_request解析HTTP请求获得的)
    4. req = self.req
    5. env = {
    6. 'ACTUAL_SERVER_PROTOCOL': req.server.protocol,
    7. 'PATH_INFO': req.path,
    8. 'QUERY_STRING': req.qs,
    9. 'REMOTE_ADDR': req.conn.remote_addr or '',
    10. 'REMOTE_PORT': str(req.conn.remote_port or ''),
    11. 'REQUEST_METHOD': req.method,
    12. 'REQUEST_URI': req.uri,
    13. 'SCRIPT_NAME': '',
    14. 'SERVER_NAME': req.server.server_name,
    15. 'SERVER_PROTOCOL': req.request_protocol,
    16. 'SERVER_SOFTWARE': req.server.software,
    17. 'wsgi.errors': sys.stderr,
    18. 'wsgi.input': req.rfile,
    19. 'wsgi.multiprocess': False,
    20. 'wsgi.multithread': True,
    21. 'wsgi.run_once': False,
    22. 'wsgi.url_scheme': req.scheme,
    23. 'wsgi.version': (1, 0),
    24. }
    25. # ...
    26. # 请求头
    27. for k, v in req.inheaders.iteritems():
    28. env["HTTP_" + k.upper().replace("-", "_")] = v
    29. # ...
    30. return env

【Python】Webpy 源码学习的更多相关文章

  1. python SimpleHTTPServer源码学习

    SimpleHTTPServer.SimpleHTTPRequestHandler继承了BaseHTTPServer.BaseHTTPRequestHandler. 源码中主要实现了BaseHTTPS ...

  2. Python库源码学习1:Flask之app.run

    先列出app.run()实现的功能,我们以debug=True的情况下进行分析. 1. web服务器,处理http请求 2. 当代码修改后,重启服务器 那么app.run()是如何实现这两个功能的呢? ...

  3. python源码学习(一)——python的总体架构

    python源码学习(一)——python的总体架构 学习环境: 系统:ubuntu 12.04 STLpython版本:2.7既然要学习python的源码,首先我们要在电脑上安装python并且下载 ...

  4. python 协程库gevent学习--gevent源码学习(二)

    在进行gevent源码学习一分析之后,我还对两个比较核心的问题抱有疑问: 1. gevent.Greenlet.join()以及他的list版本joinall()的原理和使用. 2. 关于在使用mon ...

  5. 『TensorFlow』SSD源码学习_其一:论文及开源项目文档介绍

    一.论文介绍 读论文系列:Object Detection ECCV2016 SSD 一句话概括:SSD就是关于类别的多尺度RPN网络 基本思路: 基础网络后接多层feature map 多层feat ...

  6. 《python解释器源码剖析》第0章--python的架构与编译python

    本系列是以陈儒先生的<python源码剖析>为学习素材,所记录的学习内容.不同的是陈儒先生的<python源码剖析>所剖析的是python2.5,本系列对应的是python3. ...

  7. [阿里DIN] 从论文源码学习 之 embedding层如何自动更新

    [阿里DIN] 从论文源码学习 之 embedding层如何自动更新 目录 [阿里DIN] 从论文源码学习 之 embedding层如何自动更新 0x00 摘要 0x01 DIN源码 1.1 问题 1 ...

  8. Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结

    2017年的秋招彻底结束了,感觉Java上面的最常见的集合相关的问题就是hash--系列和一些常用并发集合和队列,堆等结合算法一起考察,不完全统计,本人经历:先后百度.唯品会.58同城.新浪微博.趣分 ...

  9. jQuery源码学习感想

    还记得去年(2015)九月份的时候,作为一个大四的学生去参加美团霸面,结果被美团技术总监教育了一番,那次问了我很多jQuery源码的知识点,以前虽然喜欢研究框架,但水平还不足够来研究jQuery源码, ...

随机推荐

  1. (转)scala apply方法 笔记

    在akka源码中有这样一个Cluster类. 使用方法是这样的:val cluster = Cluster(context.system); 作为scala菜鸟的我,并没有找到Cluster(syst ...

  2. (转)mysql的单向复制

    mysql的单向复制操作很简单:大概只需要二十分钟看完这篇文章就能搞定了.http://11837782.blog.51cto.com/11827782/1885967 为了提高主从服务器的健壮性,我 ...

  3. Jconsole

    Jconsole 1.1 简介以及连接 JConsole是一个基于JMX的GUI工具,用于连接正在运行的JVM,它是Java自带的简单性能监控工具.下面以对tomcat的监控为例,带领大家熟悉Jcon ...

  4. java线程总结(1/5)

    前言 闲来无事正值面试,看面试中有线程之问题,特此总结一番. 正文 一.线程和进程的区别:1.每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销.2.线程可以看成时轻量级的进程 ...

  5. redis学习笔记——客户端

    Redis服务器是典型的一对多服务器程序:一个服务器可以与多个客户端建立网络连接,每个客户端可以向服务器发送命令请求,而服务器则接收并处理客户端发送的命令请求,并向客户端返回命令回复. 对于每个与服务 ...

  6. java中的序列化和反序列化学习笔记

    须要序列化的Person类: package cn.itcast_07; import java.io.Serializable; /* * NotSerializableException:未序列化 ...

  7. 用filter:grayscale将图片过滤成灰色

    设置成百分之百直接过滤成灰色: img{filter:gray; filter:grayscale(100%); -0-filter:grayscale(100%); -moz-filter:gray ...

  8. 坑爹的Adprep32.exe

    今天看书的时候,看到用ADSI设置Universal Group Membership Caching,就想到用.Net Framework来实现,找了半天,才找到System.directoryse ...

  9. ECMAScript 6 | 新特性

    新特性概览 参考文章: http://www.cnblogs.com/Wayou/p/es6_new_features.html ——————————————————————————————————— ...

  10. Machine Learning:PageRank算法

    1. PageRank算法概述 PageRank,即网页排名,又称网页级别.Google左側排名或佩奇排名.         在谷歌主导互联网搜索之前, 多数搜索引擎採用的排序方法, 是以被搜索词语在 ...