【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: html

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


執行代碼: 

 
在具體看WSGIServer代碼以前,咱們先看一幅圖,這幅圖概述了WSGIServer內部執行流程: 



接下來咱們看下代碼,ps: 爲了較清晰的梳理主幹流程,我只列出核心代碼段 python

Python代碼   收藏代碼
  1. # Webpy內置的WSGIServer  
  2. class CherryPyWSGIServer(HTTPServer):  
  3.   
  4.     def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None,  
  5.                  max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5):  
  6.         # 線程池(用來處理外部請求,稍後詳述)  
  7.         self.requests = ThreadPool(self, min=numthreads or 1, max=max)  
  8.         # 響應外部請求的webapp  
  9.         self.wsgi_app = wsgi_app  
  10.         # wsgi網關(http_request ->wsgi_gateway ->webpy/webapp)  
  11.         self.gateway = WSGIGateway_10  
  12.         # wsgi_server監聽地址  
  13.         self.bind_addr = bind_addr  
  14.     # ...  
  15.   
  16. class HTTPServer(object):  
  17.     # 啓動一個網絡服務器  
  18.     # 若是你閱讀過<<Unix網絡編程>>,那麼對於後面這些代碼將會再熟悉不過,惟一的區別一個是c,  
  19.     #一個是python  
  20.     def start(self):  
  21.   
  22.         # 若是bind_addr是一個字符串(文件名),那麼採用unix domain協議  
  23.         if isinstance(self.bind_addr, basestring):  
  24.             try: os.unlink(self.bind_addr)  
  25.             except: pass  
  26.             info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)]  
  27.         else:  
  28.             # 不然採用TCP/IP協議  
  29.             host, port = self.bind_addr  
  30.             try:  
  31.                 info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,   
  32.                                             socket.SOCK_STREAM, 0, socket.AI_PASSIVE)  
  33.             except socket.gaierror:  
  34.                 # ...  
  35.           
  36.         # 循環測試 getaddrinfo函數返回值,直到有一個bind成功或是遍歷完全部結果集  
  37.         for res in info:  
  38.             af, socktype, proto, canonname, sa = res  
  39.             try:  
  40.                 self.bind(af, socktype, proto)  
  41.             except socket.error:  
  42.                 if self.socket:  
  43.                     self.socket.close()  
  44.                 self.socket = None  
  45.                 continue  
  46.             break  
  47.         if not self.socket:  
  48.             raise socket.error(msg)  
  49.           
  50.         # 此時socket 進入listening狀態(能夠用netstat命令查看)  
  51.         self.socket.listen(self.request_queue_size)  
  52.           
  53.         # 啓動線程池(這個線程池作些什麼呢? 稍後會說)  
  54.         self.requests.start()  
  55.           
  56.         self.ready = True  
  57.         while self.ready:  
  58.             # HTTPSever核心函數,用來接受外部請求(request)  
  59.             # 而後封裝成一個HTTPConnection對象放入線程池中的消息隊列裏,  
  60.             # 接着線程會從消息隊列中取出該對象並處理  
  61.             self.tick()  
  62.               
  63.     def bind(self, family, type, proto=0):  
  64.         # 建立socket  
  65.         self.socket = socket.socket(family, type, proto)  
  66.         # 設置socket選項(容許在TIME_WAIT狀態下,bind相同的地址)  
  67.         self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  
  68.         # socket bind  
  69.         self.socket.bind(self.bind_addr)  
  70.       
  71.     # HTTPSever核心函數  
  72.     def tick(self):  
  73.         try:  
  74.             # 接受一個TCP鏈接  
  75.             s, addr = self.socket.accept()  
  76.   
  77.             # 把外部鏈接封裝成一個HTTPConnection對象  
  78.             makefile = CP_fileobject  
  79.             conn = self.ConnectionClass(self, s, makefile)  
  80.             # 而後把該對象放入線程池中的消息隊列裏  
  81.             self.requests.put(conn)  
  82.         except :  
  83.             # ...  


以前咱們說過HTTPServer中的request屬性是一個線程池(這個線程池內部關聯着一個消息隊列),如今咱們看看做者是如何實現一個線程池的: web

Python代碼   收藏代碼
  1. class ThreadPool(object):  
  2.       
  3.     def __init__(self, server, min=10, max=-1):  
  4.         # server實例  
  5.         self.server = server  
  6.         # 線程池中線程數配置(最小值,最大值)  
  7.         self.min = min  
  8.         self.max = max  
  9.         # 線程池中的線程實例集合(list)  
  10.         self._threads = []  
  11.         # 消息隊列(Queue是一個線程安全隊列)  
  12.         self._queue = Queue.Queue()  
  13.         # 編程技巧,用來簡化代碼,等價於:  
  14.         # def get(self)  
  15.         #    return self._queue.get()  
  16.         self.get = self._queue.get  
  17.       
  18.     # 啓動線程池  
  19.     def start(self):  
  20.         # 建立min個WorkThread並啓動  
  21.         for i in range(self.min):  
  22.             self._threads.append(WorkerThread(self.server))  
  23.         for worker in self._threads:  
  24.             worker.start()  
  25.       
  26.     # 把obj(一般是一個HTTPConnection對象)放入消息隊列  
  27.     def put(self, obj):  
  28.         self._queue.put(obj)  
  29.   
  30.     # 在不超過容許建立線程的最大數下,增長amount個線程  
  31.     def grow(self, amount):  
  32.         for i in range(amount):  
  33.             if self.max > and len(self._threads) >= self.max:  
  34.                 break  
  35.             worker = WorkerThread(self.server)  
  36.             self._threads.append(worker)  
  37.             worker.start()  
  38.       
  39.     # kill掉amount個線程  
  40.     def shrink(self, amount):  
  41.         # 1.kill掉已經不在運行的線程  
  42.         for t in self._threads:  
  43.             if not t.isAlive():  
  44.                 self._threads.remove(t)  
  45.                 amount -= 1  
  46.   
  47.         # 2.若是已經kill掉線程數小於amount,則在消息隊列中放入線程退出標記對象_SHUTDOWNREQUEST  
  48.         # 當線程從消息隊列中取到的不是一個HTTPConnection對象,而是一個_SHUTDOWNREQUEST,則退出運行  
  49.         if amount > 0:  
  50.             for i in range(min(amount, len(self._threads) - self.min)):  
  51.                 self._queue.put(_SHUTDOWNREQUEST)  
  52.   
  53. # 工做線程WorkThread  
  54. class WorkerThread(threading.Thread):  
  55.   
  56.     def __init__(self, server):  
  57.         self.ready = False  
  58.         self.server = server  
  59.         # ...  
  60.         threading.Thread.__init__(self)  
  61.       
  62.     def run(self):  
  63.          # 線程被調度運行,ready狀態位設置爲True  
  64.         self.ready = True  
  65.         while True:  
  66.             # 嘗試從消息隊列中獲取一個obj  
  67.             conn = self.server.requests.get()  
  68.   
  69.             # 若是這個obj是一個「退出標記」對象,線程則退出運行  
  70.             if conn is _SHUTDOWNREQUEST:  
  71.                 return  
  72.             # 不然該obj是一個HTTPConnection對象,那麼線程則處理該請求  
  73.             self.conn = conn  
  74.   
  75.             try:  
  76.                 # 處理HTTPConnection  
  77.                 conn.communicate()  
  78.             finally:  
  79.                 conn.close()  


剛纔咱們看到,WorkThread從消息隊列中獲取一個HTTPConnection對象,而後調用它的communicate方法,那這個communicate方法究竟作了些什麼呢? apache

Python代碼   收藏代碼
  1. class HTTPConnection(object):  
  2.      
  3.     RequestHandlerClass = HTTPRequest  
  4.       
  5.     def __init__(self, server, sock, makefile=CP_fileobject):  
  6.         self.server = server  
  7.         self.socket = sock  
  8.         # 把socket對象包裝成類File對象,使得對socket讀寫就像對File對象讀寫同樣簡單  
  9.         self.rfile = makefile(sock, "rb", self.rbufsize)  
  10.         self.wfile = makefile(sock, "wb", self.wbufsize)  
  11.       
  12.     def communicate(self):  
  13.         # 把HTTPConnection對象包裝成一個HTTPRequest對象  
  14.         req = self.RequestHandlerClass(self.server, self)  
  15.         # 解析HTTP請求  
  16.         req.parse_request()  
  17.         # 響應HTTP請求  
  18.         req.respond()  

     
在咱們具體看HTTPRequest.parse_request如何解析HTTP請求以前,咱們先了解下HTTP協議. HTTP協議是一個文本行的協議,它一般由如下部分組成: 編程

引用
請求行(請求方法 URI路徑  HTTP協議版本) 
請求頭(譬如:User-Agent,Host等等) 
空行 
可選的數據實體


 


而HTTPRequest.parse_request方法就是把socket中的字節流,按照HTTP協議規範解析,而且從中提取信息(最終封裝成一個env傳遞給webapp): 
 安全

Python代碼   收藏代碼
  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.   # ----------------------------------------------------------------  
  10.   def read_request_line(self):  
  11.       # 從socket中讀取一行數據  
  12.       request_line = self.rfile.readline()  
  13.         
  14.       # 按照HTTP協議規範,把request_line分割成請求方法(method),uri路徑(uri),HTTP協議版本(req_protocol)  
  15.       method, uri, req_protocol = request_line.strip().split(" ", 2)  
  16.       self.uri = uri  
  17.       self.method = method  
  18.         
  19.       scheme, authority, path = self.parse_request_uri(uri)  
  20.       # 獲取uri請求參數  
  21.       qs = ''  
  22.       if '?' in path:  
  23.           path, qs = path.split('?', 1)  
  24.       self.path = path  
  25.   
  26.   # ----------------------------------------------------------------  
  27.   def read_request_headers(self):  
  28.       # 讀取請求頭,inheaders是一個dict  
  29.       read_headers(self.rfile, self.inheaders)  
  30.   
  31.   # ----------------------------------------------------------------  
  32.   def read_headers(rfile, hdict=None):  
  33.       if hdict is None:  
  34.           hdict = {}  
  35.         
  36.       while True:  
  37.           line = rfile.readline()  
  38.           # 把line按照":"分割成k, v,譬如 Host:baidu.com就被分割成Host和baidu.com兩部分  
  39.           k, v = line.split(":", 1)  
  40.           # 格式化分割後的     
  41.           k = k.strip().title()  
  42.           v = v.strip()  
  43.           hname = k  
  44.             
  45.           # HTTP協議中的有些請求頭容許重複(譬如Accept等等),那麼webpy就會把這些相同頭的value用","鏈接起來  
  46.           if k in comma_separated_headers:  
  47.               existing = hdict.get(hname)  
  48.               if existing:  
  49.                   v = ", ".join((existing, v))  
  50.           # 把請求頭k, v存入hdict  
  51.           hdict[hname] = v  
  52.         
  53.       return hdict  


至此咱們就分析完了HTTPRequest.parse_request方法如何解析HTTP請求,下面咱們就接着看看HTTPRequest.respond如何響應請求: 
   服務器

Python代碼   收藏代碼
  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網關 網絡

Python代碼   收藏代碼
  1. class WSGIGateway(Gateway):  
  2.     def __init__(self, req):  
  3.         self.req = req  # HTTPRequest對象  
  4.         self.env = self.get_environ()  
  5.       
  6.     # 獲取wsgi的環境變量(留給子類實現)  
  7.     def get_environ(self):  
  8.         raise NotImplemented  
  9.       
  10.     def respond(self):  
  11.         # -----------------------------------  
  12.         # 按照 WSGI 規範調用咱們得 webapp/webpy  
  13.         # -----------------------------------  
  14.         response = self.req.server.wsgi_app(self.env, self.start_response)  
  15.   
  16.         # 把處理結果寫回給客戶端  
  17.         for chunk in response:  
  18.             self.write(chunk)  
  19.       
  20.     def start_response(self, status, headers, exc_info = None):  
  21.         self.req.status = status  
  22.         self.req.outheaders.extend(headers)  
  23.           
  24.         return self.write  
  25.       
  26.     def write(self, chunk):  
  27.         # 寫http響應頭  
  28.         self.req.send_headers()  
  29.         # 寫http響應體  
  30.         self.req.write(chunk)  


WSGIGateway_10繼承WSGIGateway類,並實現get_environ方法 app

Python代碼   收藏代碼
    1. class WSGIGateway_10(WSGIGateway):  
    2.       
    3.     def get_environ(self):  
    4.         # build WSGI環境變量(req中的這些屬性,都是經過HTTPRequest.prase_request解析HTTP請求得到的)  
    5.         req = self.req  
    6.         env = {  
    7.             'ACTUAL_SERVER_PROTOCOL': req.server.protocol,  
    8.             'PATH_INFO': req.path,  
    9.             'QUERY_STRING': req.qs,  
    10.             'REMOTE_ADDR': req.conn.remote_addr or '',  
    11.             'REMOTE_PORT': str(req.conn.remote_port or ''),  
    12.             'REQUEST_METHOD': req.method,  
    13.             'REQUEST_URI': req.uri,  
    14.             'SCRIPT_NAME': '',  
    15.             'SERVER_NAME': req.server.server_name,  
    16.             'SERVER_PROTOCOL': req.request_protocol,  
    17.             'SERVER_SOFTWARE': req.server.software,  
    18.             'wsgi.errors': sys.stderr,  
    19.             'wsgi.input': req.rfile,  
    20.             'wsgi.multiprocess': False,  
    21.             'wsgi.multithread': True,  
    22.             'wsgi.run_once': False,  
    23.             'wsgi.url_scheme': req.scheme,  
    24.             'wsgi.version': (1, 0),  
    25.             }  
    26.         # ...  
    27.   
    28.         # 請求頭  
    29.         for k, v in req.inheaders.iteritems():  
    30.             env["HTTP_" + k.upper().replace("-", "_")] = v  
    31.           
    32.         # ...  
    33.         return env  
相關文章
相關標籤/搜索