【Python】Webpy 源码学习(一)
程序员文章站
2024-01-07 14:28:40
...
自己是个python新手,之前买了本<<python核心编程>>,但看了一半实在看不下去了(内容过于啰嗦,而且在关键点的地方又浅尝辄止),所以希望通过阅读一些简单的开源项目来快速提高python水平,最终让我发现了webpy这个好东西!
那么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:
执行代码:
在具体看WSGIServer代码之前,我们先看一幅图,这幅图概述了WSGIServer内部执行流程:
接下来我们看下代码,ps: 为了较清晰的梳理主干流程,我只列出核心代码段
之前我们说过HTTPServer中的request属性是一个线程池(这个线程池内部关联着一个消息队列),现在我们看看作者是如何实现一个线程池的:
刚才我们看到,WorkThread从消息队列中获取一个HTTPConnection对象,然后调用它的communicate方法,那这个communicate方法究竟做了些什么呢?
在我们具体看HTTPRequest.parse_request如何解析HTTP请求之前,我们先了解下HTTP协议. HTTP协议是一个文本行的协议,它通常由以下部分组成:
而HTTPRequest.parse_request方法就是把socket中的字节流,按照HTTP协议规范解析,并且从中提取信息(最终封装成一个env传递给webapp):
至此我们就分析完了HTTPRequest.parse_request方法如何解析HTTP请求,下面我们就接着看看HTTPRequest.respond如何响应请求:
在继续往下看代码之前,我们先简单思考下,为什么要有这个gateway,为什么这里不把请求直接交给webapp处理?
我自己觉得还是出于分层和代码复用性考虑。因为可能存在,或者需要支持很多web规范,目前我们使用的是wsgi规范,明天可能出来个ysgi,大后天可能还来个zsgi,如果按照当前的设计,我们只需要替换HTTPServer的gateway属性,而不用修改其他代码(类似JAVA概念中的DAO层),下面我们就来看看这个gateway的具体实现(回到本文最初,我们在Server中注册的gateway是WSGIGateway_10):
WSGI网关
WSGIGateway_10继承WSGIGateway类,并实现get_environ方法
好了,到这里我们已经把整个流程:从HTTPServer接受外部请求,到我们web应用处理这一过程已经大致说完,希望对各位有帮助。
那么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:
#/usr/bin/python import web.wsgiserver def my_wsgi_app(env, start_response): status = '200 OK' response_headers = [('Content-type','text/plain')] start_response(status, response_headers) return ['Hello world!'] server = web.wsgiserver.CherryPyWSGIServer(("127.0.0.1", 8080), my_wsgi_app); server.start()
执行代码:
在具体看WSGIServer代码之前,我们先看一幅图,这幅图概述了WSGIServer内部执行流程:
接下来我们看下代码,ps: 为了较清晰的梳理主干流程,我只列出核心代码段
# Webpy内置的WSGIServer class CherryPyWSGIServer(HTTPServer): 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) # 响应外部请求的webapp self.wsgi_app = wsgi_app # wsgi网关(http_request ->wsgi_gateway ->webpy/webapp) self.gateway = WSGIGateway_10 # wsgi_server监听地址 self.bind_addr = bind_addr # ... class HTTPServer(object): # 启动一个网络服务器 # 如果你阅读过<<Unix网络编程>>,那么对于后面这些代码将会再熟悉不过,唯一的区别一个是c, #一个是python def start(self): # 如果bind_addr是一个字符串(文件名),那么采用unix domain协议 if isinstance(self.bind_addr, basestring): try: os.unlink(self.bind_addr) except: pass info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)] else: # 否则采用TCP/IP协议 host, port = self.bind_addr try: info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE) except socket.gaierror: # ... # 循环测试 getaddrinfo函数返回值,直到有一个bind成功或是遍历完所有结果集 for res in info: af, socktype, proto, canonname, sa = res try: self.bind(af, socktype, proto) except socket.error: if self.socket: self.socket.close() self.socket = None continue break if not self.socket: raise socket.error(msg) # 此时socket 进入listening状态(可以用netstat命令查看) self.socket.listen(self.request_queue_size) # 启动线程池(这个线程池做些什么呢? 稍后会说) self.requests.start() self.ready = True while self.ready: # HTTPSever核心函数,用来接受外部请求(request) # 然后封装成一个HTTPConnection对象放入线程池中的消息队列里, # 接着线程会从消息队列中取出该对象并处理 self.tick() def bind(self, family, type, proto=0): # 创建socket self.socket = socket.socket(family, type, proto) # 设置socket选项(允许在TIME_WAIT状态下,bind相同的地址) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # socket bind self.socket.bind(self.bind_addr) # HTTPSever核心函数 def tick(self): try: # 接受一个TCP连接 s, addr = self.socket.accept() # 把外部连接封装成一个HTTPConnection对象 makefile = CP_fileobject conn = self.ConnectionClass(self, s, makefile) # 然后把该对象放入线程池中的消息队列里 self.requests.put(conn) except : # ...
之前我们说过HTTPServer中的request属性是一个线程池(这个线程池内部关联着一个消息队列),现在我们看看作者是如何实现一个线程池的:
class ThreadPool(object): def __init__(self, server, min=10, max=-1): # server实例 self.server = server # 线程池中线程数配置(最小值,最大值) self.min = min self.max = max # 线程池中的线程实例集合(list) self._threads = [] # 消息队列(Queue是一个线程安全队列) self._queue = Queue.Queue() # 编程技巧,用来简化代码,等价于: # def get(self) # return self._queue.get() self.get = self._queue.get # 启动线程池 def start(self): # 创建min个WorkThread并启动 for i in range(self.min): self._threads.append(WorkerThread(self.server)) for worker in self._threads: worker.start() # 把obj(通常是一个HTTPConnection对象)放入消息队列 def put(self, obj): self._queue.put(obj) # 在不超过允许创建线程的最大数下,增加amount个线程 def grow(self, amount): for i in range(amount): if self.max > 0 and len(self._threads) >= self.max: break worker = WorkerThread(self.server) self._threads.append(worker) worker.start() # kill掉amount个线程 def shrink(self, amount): # 1.kill掉已经不在运行的线程 for t in self._threads: if not t.isAlive(): self._threads.remove(t) amount -= 1 # 2.如果已经kill掉线程数小于amount,则在消息队列中放入线程退出标记对象_SHUTDOWNREQUEST # 当线程从消息队列中取到的不是一个HTTPConnection对象,而是一个_SHUTDOWNREQUEST,则退出运行 if amount > 0: for i in range(min(amount, len(self._threads) - self.min)): self._queue.put(_SHUTDOWNREQUEST) # 工作线程WorkThread class WorkerThread(threading.Thread): def __init__(self, server): self.ready = False self.server = server # ... threading.Thread.__init__(self) def run(self): # 线程被调度运行,ready状态位设置为True self.ready = True while True: # 尝试从消息队列中获取一个obj conn = self.server.requests.get() # 如果这个obj是一个“退出标记”对象,线程则退出运行 if conn is _SHUTDOWNREQUEST: return # 否则该obj是一个HTTPConnection对象,那么线程则处理该请求 self.conn = conn try: # 处理HTTPConnection conn.communicate() finally: conn.close()
刚才我们看到,WorkThread从消息队列中获取一个HTTPConnection对象,然后调用它的communicate方法,那这个communicate方法究竟做了些什么呢?
class HTTPConnection(object): RequestHandlerClass = HTTPRequest def __init__(self, server, sock, makefile=CP_fileobject): self.server = server self.socket = sock # 把socket对象包装成类File对象,使得对socket读写就像对File对象读写一样简单 self.rfile = makefile(sock, "rb", self.rbufsize) self.wfile = makefile(sock, "wb", self.wbufsize) def communicate(self): # 把HTTPConnection对象包装成一个HTTPRequest对象 req = self.RequestHandlerClass(self.server, self) # 解析HTTP请求 req.parse_request() # 响应HTTP请求 req.respond()
在我们具体看HTTPRequest.parse_request如何解析HTTP请求之前,我们先了解下HTTP协议. HTTP协议是一个文本行的协议,它通常由以下部分组成:
引用
请求行(请求方法 URI路径 HTTP协议版本)
请求头(譬如:User-Agent,Host等等)
空行
可选的数据实体
请求头(譬如:User-Agent,Host等等)
空行
可选的数据实体
而HTTPRequest.parse_request方法就是把socket中的字节流,按照HTTP协议规范解析,并且从中提取信息(最终封装成一个env传递给webapp):
def parse_request(self): self.rfile = SizeCheckWrapper(self.conn.rfile, self.server.max_request_header_size) # 读取请求行 self.read_request_line() # 读取请求头 success = self.read_request_headers() # ---------------------------------------------------------------- def read_request_line(self): # 从socket中读取一行数据 request_line = self.rfile.readline() # 按照HTTP协议规范,把request_line分割成请求方法(method),uri路径(uri),HTTP协议版本(req_protocol) method, uri, req_protocol = request_line.strip().split(" ", 2) self.uri = uri self.method = method scheme, authority, path = self.parse_request_uri(uri) # 获取uri请求参数 qs = '' if '?' in path: path, qs = path.split('?', 1) self.path = path # ---------------------------------------------------------------- def read_request_headers(self): # 读取请求头,inheaders是一个dict read_headers(self.rfile, self.inheaders) # ---------------------------------------------------------------- def read_headers(rfile, hdict=None): if hdict is None: hdict = {} while True: line = rfile.readline() # 把line按照":"分割成k, v,譬如 Host:baidu.com就被分割成Host和baidu.com两部分 k, v = line.split(":", 1) # 格式化分割后的 k = k.strip().title() v = v.strip() hname = k # HTTP协议中的有些请求头允许重复(譬如Accept等等),那么webpy就会把这些相同头的value用","连接起来 if k in comma_separated_headers: existing = hdict.get(hname) if existing: v = ", ".join((existing, v)) # 把请求头k, v存入hdict hdict[hname] = v return hdict
至此我们就分析完了HTTPRequest.parse_request方法如何解析HTTP请求,下面我们就接着看看HTTPRequest.respond如何响应请求:
def respond(self): # 把请求交给gateway响应 self.server.gateway(self).respond()
在继续往下看代码之前,我们先简单思考下,为什么要有这个gateway,为什么这里不把请求直接交给webapp处理?
我自己觉得还是出于分层和代码复用性考虑。因为可能存在,或者需要支持很多web规范,目前我们使用的是wsgi规范,明天可能出来个ysgi,大后天可能还来个zsgi,如果按照当前的设计,我们只需要替换HTTPServer的gateway属性,而不用修改其他代码(类似JAVA概念中的DAO层),下面我们就来看看这个gateway的具体实现(回到本文最初,我们在Server中注册的gateway是WSGIGateway_10):
WSGI网关
class WSGIGateway(Gateway): def __init__(self, req): self.req = req # HTTPRequest对象 self.env = self.get_environ() # 获取wsgi的环境变量(留给子类实现) def get_environ(self): raise NotImplemented def respond(self): # ----------------------------------- # 按照 WSGI 规范调用我们得 webapp/webpy # ----------------------------------- response = self.req.server.wsgi_app(self.env, self.start_response) # 把处理结果写回给客户端 for chunk in response: self.write(chunk) def start_response(self, status, headers, exc_info = None): self.req.status = status self.req.outheaders.extend(headers) return self.write def write(self, chunk): # 写http响应头 self.req.send_headers() # 写http响应体 self.req.write(chunk)
WSGIGateway_10继承WSGIGateway类,并实现get_environ方法
class WSGIGateway_10(WSGIGateway): def get_environ(self): # build WSGI环境变量(req中的这些属性,都是通过HTTPRequest.prase_request解析HTTP请求获得的) req = self.req env = { 'ACTUAL_SERVER_PROTOCOL': req.server.protocol, 'PATH_INFO': req.path, 'QUERY_STRING': req.qs, 'REMOTE_ADDR': req.conn.remote_addr or '', 'REMOTE_PORT': str(req.conn.remote_port or ''), 'REQUEST_METHOD': req.method, 'REQUEST_URI': req.uri, 'SCRIPT_NAME': '', 'SERVER_NAME': req.server.server_name, 'SERVER_PROTOCOL': req.request_protocol, 'SERVER_SOFTWARE': req.server.software, 'wsgi.errors': sys.stderr, 'wsgi.input': req.rfile, 'wsgi.multiprocess': False, 'wsgi.multithread': True, 'wsgi.run_once': False, 'wsgi.url_scheme': req.scheme, 'wsgi.version': (1, 0), } # ... # 请求头 for k, v in req.inheaders.iteritems(): env["HTTP_" + k.upper().replace("-", "_")] = v # ... return env
好了,到这里我们已经把整个流程:从HTTPServer接受外部请求,到我们web应用处理这一过程已经大致说完,希望对各位有帮助。