自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從零構(gòu)建一個簡單的Python框架

開發(fā) 前端
這篇文章旨在通過對設(shè)計和實現(xiàn)過程一步一步的闡述告訴讀者,我在完成一個小型的服務(wù)器和框架之后學(xué)到了什么。你可以在這個代碼倉庫中找到這個項目的完整代碼。我希望這篇文章可以鼓勵更多的人來嘗試,因為這確實很有趣。它讓我知道了 web 應(yīng)用是如何工作的,而且這比我想的要容易的多!

[[171560]]

為什么你想要自己構(gòu)建一個 web 框架呢?我想,原因有以下幾點:

  • 你有一個新奇的想法,覺得將會取代其他的框架
  • 你想要獲得一些名氣
  • 你遇到的問題很獨特,以至于現(xiàn)有的框架不太合適
  • 你對 web 框架是如何工作的很感興趣,因為你想要成為一位更好的 web 開發(fā)者。

接下來的筆墨將著重于最后一點。這篇文章旨在通過對設(shè)計和實現(xiàn)過程一步一步的闡述告訴讀者,我在完成一個小型的服務(wù)器和框架之后學(xué)到了什么。你可以在這個代碼倉庫中找到這個項目的完整代碼。

我希望這篇文章可以鼓勵更多的人來嘗試,因為這確實很有趣。它讓我知道了 web 應(yīng)用是如何工作的,而且這比我想的要容易的多!

范圍

框架可以處理請求-響應(yīng)周期、身份認證、數(shù)據(jù)庫訪問、模板生成等部分工作。Web 開發(fā)者使用框架是因為,大多數(shù)的 web 應(yīng)用擁有大量相同的功能,而對每個項目都重新實現(xiàn)同樣的功能意義不大。

比較大的的框架如 Rails 和 Django 實現(xiàn)了高層次的抽象,或者說“自備電池”(“batteries-included”,這是 Python 的口號之一,意即所有功能都自足。)。而實現(xiàn)所有的這些功能可能要花費數(shù)千小時,因此在這個項目上,我們重點完成其中的一小部分。在開始寫代碼前,我先列舉一下所需的功能以及限制。

功能:

  • 處理 HTTP 的 GET 和 POST 請求。你可以在這篇 wiki 中對 HTTP 有個大致的了解。
  • 實現(xiàn)異步操作(我喜歡 Python 3 的 asyncio 模塊)。
  • 簡單的路由邏輯以及參數(shù)擷取。
  • 像其他微型框架一樣,提供一個簡單的用戶級 API 。
  • 支持身份認證,因為學(xué)會這個很酷啊(微笑)。

限制:

  • 將只支持 HTTP 1.1 的一個小子集,不支持傳輸編碼(transfer-encoding)、HTTP 認證(http-auth)、內(nèi)容編碼(content-encoding)(如 gzip)以及持久化連接等功能。
  • 不支持對響應(yīng)內(nèi)容的 MIME 判斷 - 用戶需要手動指定。
  • 不支持 WSGI - 僅能處理簡單的 TCP 連接。
  • 不支持?jǐn)?shù)據(jù)庫。

我覺得一個小的用例可以讓上述內(nèi)容更加具體,也可以用來演示這個框架的 API:

  1. from diy_framework import App, Router 
  2. from diy_framework.http_utils import Response 
  3. # GET simple route 
  4. async def home(r): 
  5.     rsp = Response() 
  6.     rsp.set_header('Content-Type''text/html'
  7.     rsp.body = '<html><body><b>test</b></body></html>' 
  8.     return rsp 
  9. # GET route + params 
  10. async def welcome(r, name): 
  11.     return "Welcome {}".format(name
  12. # POST route + body param 
  13. async def parse_form(r): 
  14.     if r.method == 'GET'
  15.         return 'form' 
  16.     else
  17.         name = r.body.get('name''')[0] 
  18.         password = r.body.get('password''')[0] 
  19.        return "{0}:{1}".format(namepassword
  20. # application = router + http server 
  21. router = Router() 
  22. router.add_routes({ 
  23.     r'/welcome/{name}': welcome, 
  24.     r'/': home, 
  25.     r'/login': parse_form,}) 
  26. app = App(router) 
  27. app.start_server() 

' 用戶需要定義一些能夠返回字符串或 Response 對象的異步函數(shù),然后將這些函數(shù)與表示路由的字符串配對,最后通過一個函數(shù)調(diào)用(start_server)開始處理請求。

完成設(shè)計之后,我將它抽象為幾個我需要編碼的部分:

  • 接受 TCP 連接以及調(diào)度一個異步函數(shù)來處理這些連接的部分
  • 將原始文本解析成某種抽象容器的部分
  • 對于每個請求,用來決定調(diào)用哪個函數(shù)的部分
  • 將上述部分集中到一起,并為開發(fā)者提供一個簡單接口的部分

我先編寫一些測試,這些測試被用來描述每個部分的功能。幾次重構(gòu)后,整個設(shè)計被分成若干部分,每個部分之間是相對解耦的。這樣就非常好,因為每個部分可以被獨立地研究學(xué)習(xí)。以下是我上文列出的抽象的具體體現(xiàn):

  • 一個 HTTPServer 對象,需要一個 Router 對象和一個 http_parser 模塊,并使用它們來初始化。
  • HTTPConnection 對象,每一個對象表示一個單獨的客戶端 HTTP 連接,并且處理其請求-響應(yīng)周期:使用 http_parser 模塊將收到的字節(jié)流解析為一個 Request 對象;使用一個 Router 實例尋找并調(diào)用正確的函數(shù)來生成一個響應(yīng);最后將這個響應(yīng)發(fā)送回客戶端。
  • 一對 Request 和 Response 對象為用戶提供了一種友好的方式,來處理實質(zhì)上是字節(jié)流的字符串。用戶不需要知道正確的消息格式和分隔符是怎樣的。
  • 一個包含“路由:函數(shù)”對應(yīng)關(guān)系的 Router 對象。它提供一個添加配對的方法,可以根據(jù) URL 路徑查找到相應(yīng)的函數(shù)。
  • 最后,一個 App 對象。它包含配置信息,并使用它們實例化一個 HTTPServer 實例。

讓我們從 HTTPConnection 開始來講解各個部分。

模擬異步連接

為了滿足上述約束條件,每一個 HTTP 請求都是一個單獨的 TCP 連接。這使得處理請求的速度變慢了,因為建立多個 TCP 連接需要相對高的花銷(DNS 查詢,TCP 三次握手,慢啟動等等的花銷),不過這樣更加容易模擬。對于這一任務(wù),我選擇相對高級的 asyncio-stream 模塊,它建立在 asyncio 的傳輸和協(xié)議的基礎(chǔ)之上。我強烈推薦你讀一讀標(biāo)準(zhǔn)庫中的相應(yīng)代碼,很有意思!

一個 HTTPConnection 的實例能夠處理多個任務(wù)。首先,它使用 asyncio.StreamReader 對象以增量的方式從 TCP 連接中讀取數(shù)據(jù),并存儲在緩存中。每一個讀取操作完成后,它會嘗試解析緩存中的數(shù)據(jù),并生成一個 Request 對象。一旦收到了這個完整的請求,它就生成一個回復(fù),并通過 asyncio.StreamWriter 對象發(fā)送回客戶端。當(dāng)然,它還有兩個任務(wù):超時連接以及錯誤處理。

你可以在這里瀏覽這個類的完整代碼。我將分別介紹代碼的每一部分。為了簡單起見,我移除了代碼文檔。

  1. class HTTPConnection(object): 
  2.     def init(self, http_server, reader, writer): 
  3.         self.router = http_server.router 
  4.         self.http_parser = http_server.http_parser 
  5.         self.loop = http_server.loop 
  6.         self._reader = reader 
  7.         self._writer = writer 
  8.         self._buffer = bytearray() 
  9.         self._conn_timeout = None 
  10.         self.request = Request() 

這個 init 方法沒啥意思,它僅僅是收集了一些對象以供后面使用。它存儲了一個 router 對象、一個 http_parser 對象以及 loop 對象,分別用來生成響應(yīng)、解析請求以及在事件循環(huán)中調(diào)度任務(wù)。

然后,它存儲了代表一個 TCP 連接的讀寫對,和一個充當(dāng)原始字節(jié)緩沖區(qū)的空字節(jié)數(shù)組。_conn_timeout 存儲了一個 asyncio.Handle 的實例,用來管理超時邏輯。最后,它還存儲了 Request 對象的一個單一實例。

下面的代碼是用來接受和發(fā)送數(shù)據(jù)的核心功能:

  1. async def handle_request(self): 
  2.     try: 
  3.         while not self.request.finished and not self._reader.at_eof(): 
  4.             data = await self._reader.read(1024) 
  5.             if data: 
  6.                 self._reset_conn_timeout() 
  7.                 await self.process_data(data) 
  8.         if self.request.finished: 
  9.             await self.reply() 
  10.         elif self._reader.at_eof(): 
  11.             raise BadRequestException() 
  12.     except (NotFoundException, 
  13.             BadRequestException) as e: 
  14.         self.error_reply(e.code, body=Response.reason_phrases[e.code]) 
  15.     except Exception as e: 
  16.         self.error_reply(500, body=Response.reason_phrases[500]) 
  17.     self.close_connection() 

所有內(nèi)容被包含在 try-except 代碼塊中,這樣在解析請求或響應(yīng)期間拋出的異??梢员徊东@到,然后一個錯誤響應(yīng)會發(fā)送回客戶端。

在 while 循環(huán)中不斷讀取請求,直到解析器將 self.request.finished 設(shè)置為 True ,或者客戶端關(guān)閉連接所觸發(fā)的信號使得 self._reader_at_eof() 函數(shù)返回值為 True 為止。這段代碼嘗試在每次循環(huán)迭代中從 StreamReader 中讀取數(shù)據(jù),并通過調(diào)用 self.process_data(data) 函數(shù)以增量方式生成 self.request。每次循環(huán)讀取數(shù)據(jù)時,連接超時計數(shù)器被重置。

這兒有個錯誤,你發(fā)現(xiàn)了嗎?稍后我們會再討論這個。需要注意的是,這個循環(huán)可能會耗盡 CPU 資源,因為如果沒有讀取到東西 self._reader.read() 函數(shù)將會返回一個空的字節(jié)對象 b''。這就意味著循環(huán)將會不斷運行,卻什么也不做。一個可能的解決方法是,用非阻塞的方式等待一小段時間:await asyncio.sleep(0.1)。我們暫且不對它做優(yōu)化。

還記得上一段我提到的那個錯誤嗎?只有從 StreamReader 讀取數(shù)據(jù)時,self._reset_conn_timeout() 函數(shù)才會被調(diào)用。這就意味著,直到第一個字節(jié)到達時,timeout 才被初始化。如果有一個客戶端建立了與服務(wù)器的連接卻不發(fā)送任何數(shù)據(jù),那就永遠不會超時。這可能被用來消耗系統(tǒng)資源,從而導(dǎo)致拒絕服務(wù)式攻擊(DoS)。修復(fù)方法就是在 init 函數(shù)中調(diào)用 self._reset_conn_timeout() 函數(shù)。

當(dāng)請求接受完成或連接中斷時,程序?qū)⑦\行到 if-else 代碼塊。這部分代碼會判斷解析器收到完整的數(shù)據(jù)后是否完成了解析。如果是,好,生成一個回復(fù)并發(fā)送回客戶端。如果不是,那么請求信息可能有錯誤,拋出一個異常!最后,我們調(diào)用 self.close_connection 執(zhí)行清理工作。

解析請求的部分在 self.process_data 方法中。這個方法非常簡短,也易于測試:

  1. async def process_data(self, data): 
  2.     self._buffer.extend(data) 
  3.     self._buffer = self.http_parser.parse_into( 
  4.         self.request, self._buffer) 

每一次調(diào)用都將數(shù)據(jù)累積到 self._buffer 中,然后試著用 self.http_parser 來解析已經(jīng)收集的數(shù)據(jù)。這里需要指出的是,這段代碼展示了一種稱為依賴注入(Dependency Injection)的模式。如果你還記得 init 函數(shù)的話,應(yīng)該知道我們傳入了一個包含 http_parser 對象的 http_server 對象。在這個例子里,http_parser 對象是 diy_framework 包中的一個模塊。不過它也可以是任何含有 parse_into 函數(shù)的類,這個 parse_into 函數(shù)接受一個 Request 對象以及字節(jié)數(shù)組作為參數(shù)。這很有用,原因有二:一是,這意味著這段代碼更易擴展。如果有人想通過一個不同的解析器來使用 HTTPConnection,沒問題,只需將它作為參數(shù)傳入即可。二是,這使得測試更加容易,因為 http_parser 不是硬編碼的,所以使用虛假數(shù)據(jù)或者 mock 對象來替代是很容易的。

下一段有趣的部分就是 reply 方法了:

  1. async def reply(self): 
  2.     request = self.request 
  3.     handler = self.router.get_handler(request.path) 
  4.     response = await handler.handle(request) 
  5.     if not isinstance(response, Response): 
  6.         response = Response(code=200, body=response) 
  7.     self._writer.write(response.to_bytes()) 
  8.     await self._writer.drain() 

這里,一個 HTTPConnection 的實例使用了 HTTPServer 中的 router 對象來得到一個生成響應(yīng)的對象。一個路由可以是任何一個擁有 get_handler 方法的對象,這個方法接收一個字符串作為參數(shù),返回一個可調(diào)用的對象或者拋出 NotFoundException 異常。而這個可調(diào)用的對象被用來處理請求以及生成響應(yīng)。處理程序由框架的使用者編寫,如上文所說的那樣,應(yīng)該返回字符串或者 Response 對象。Response 對象提供了一個友好的接口,因此這個簡單的 if 語句保證了無論處理程序返回什么,代碼最終都得到一個統(tǒng)一的 Response 對象。

接下來,被賦值給 self._writer 的 StreamWriter 實例被調(diào)用,將字節(jié)字符串發(fā)送回客戶端。函數(shù)返回前,程序在 await self._writer.drain() 處等待,以確保所有的數(shù)據(jù)被發(fā)送給客戶端。只要緩存中還有未發(fā)送的數(shù)據(jù),self._writer.close() 方法就不會執(zhí)行。

HTTPConnection 類還有兩個更加有趣的部分:一個用于關(guān)閉連接的方法,以及一組用來處理超時機制的方法。首先,關(guān)閉一條連接由下面這個小函數(shù)完成:

  1. def close_connection(self): 
  2.     self._cancel_conn_timeout() 
  3.     self._writer.close() 

每當(dāng)一條連接將被關(guān)閉時,這段代碼首先取消超時,然后把連接從事件循環(huán)中清除。

超時機制由三個相關(guān)的函數(shù)組成:第一個函數(shù)在超時后給客戶端發(fā)送錯誤消息并關(guān)閉連接;第二個函數(shù)用于取消當(dāng)前的超時;第三個函數(shù)調(diào)度超時功能。前兩個函數(shù)比較簡單,我將詳細解釋第三個函數(shù) _reset_cpmm_timeout() 。

  1. def _conn_timeout_close(self): 
  2.     self.error_reply(500, 'timeout'
  3.     self.close_connection() 
  4. def _cancel_conn_timeout(self): 
  5.     if self._conn_timeout: 
  6.         self._conn_timeout.cancel() 
  7. def _reset_conn_timeout(self, timeout=TIMEOUT): 
  8.     self._cancel_conn_timeout() 
  9.     self._conn_timeout = self.loop.call_later( 
  10.         timeout, self._conn_timeout_close) 

每當(dāng) _reset_conn_timeout 函數(shù)被調(diào)用時,它會先取消之前所有賦值給 self._conn_timeout 的 asyncio.Handle 對象。然后,使用 BaseEventLoop.call_later 函數(shù)讓 _conn_timeout_close 函數(shù)在超時數(shù)秒(timeout)后執(zhí)行。如果你還記得 handle_request 函數(shù)的內(nèi)容,就知道每當(dāng)接收到數(shù)據(jù)時,這個函數(shù)就會被調(diào)用。這就取消了當(dāng)前的超時并且重新安排 _conn_timeout_close 函數(shù)在超時數(shù)秒(timeout)后執(zhí)行。只要接收到數(shù)據(jù),這個循環(huán)就會不斷地重置超時回調(diào)。如果在超時時間內(nèi)沒有接收到數(shù)據(jù),最后函數(shù) _conn_timeout_close 就會被調(diào)用。

創(chuàng)建連接

我們需要創(chuàng)建 HTTPConnection 對象,并且正確地使用它們。這一任務(wù)由 HTTPServer 類完成。HTTPServer 類是一個簡單的容器,可以存儲著一些配置信息(解析器,路由和事件循環(huán)實例),并使用這些配置來創(chuàng)建 HTTPConnection 實例:

  1. class HTTPServer(object): 
  2.     def init(self, router, http_parser, loop): 
  3.         self.router = router 
  4.         self.http_parser = http_parser 
  5.         self.loop = loop 
  6.     async def handle_connection(self, reader, writer): 
  7.         connection = HTTPConnection(self, reader, writer) 
  8.         asyncio.ensure_future(connection.handle_request(), loop=self.loop) 

HTTPServer 的每一個實例能夠監(jiān)聽一個端口。它有一個 handle_connection 的異步方法來創(chuàng)建 HTTPConnection 的實例,并安排它們在事件循環(huán)中運行。這個方法被傳遞給 asyncio.start_server 作為一個回調(diào)函數(shù)。也就是說,每當(dāng)一個 TCP 連接初始化時(以 StreamReader 和 StreamWriter 為參數(shù)),它就會被調(diào)用。

  1. self._server = HTTPServer(self.router, self.http_parser, self.loop) 
  2. self._connection_handler = asyncio.start_server( 
  3.      self._server.handle_connection, 
  4.      host=self.host, 
  5.      port=self.port, 
  6.      reuse_address=True
  7.      reuse_port=True
  8.      loop=self.loop) 

這就是構(gòu)成整個應(yīng)用程序工作原理的核心:asyncio.start_server 接受 TCP 連接,然后在一個預(yù)配置的 HTTPServer 對象上調(diào)用一個方法。這個方法將處理一條 TCP 連接的所有邏輯:讀取、解析、生成響應(yīng)并發(fā)送回客戶端、以及關(guān)閉連接。它的重點是 IO 邏輯、解析和生成響應(yīng)。

講解了核心的 IO 部分,讓我們繼續(xù)。

解析請求

這個微型框架的使用者被寵壞了,不愿意和字節(jié)打交道。它們想要一個更高層次的抽象 —— 一種更加簡單的方法來處理請求。這個微型框架就包含了一個簡單的 HTTP 解析器,能夠?qū)⒆止?jié)流轉(zhuǎn)化為 Request 對象。

這些 Request 對象是像這樣的容器:

  1. class Request(object): 
  2.     def init(self): 
  3.         self.method = None 
  4.         self.path = None 
  5.         self.query_params = {} 
  6.         self.path_params = {} 
  7.         self.headers = {} 
  8.         self.body = None 
  9.         self.body_raw = None 
  10.         self.finished = False 

它包含了所有需要的數(shù)據(jù),可以用一種容易理解的方法從客戶端接受數(shù)據(jù)。哦,不包括 cookie ,它對身份認證是非常重要的,我會將它留在第二部分。

每一個 HTTP 請求都包含了一些必需的內(nèi)容,如請求路徑和請求方法。它們也包含了一些可選的內(nèi)容,如請求體、請求頭,或是 URL 參數(shù)。隨著 REST 的流行,除了 URL 參數(shù),URL 本身會包含一些信息。比如,"/user/1/edit" 包含了用戶的 id 。

一個請求的每個部分都必須被識別、解析,并正確地賦值給 Request 對象的對應(yīng)屬性。HTTP/1.1 是一個文本協(xié)議,事實上這簡化了很多東西。(HTTP/2 是一個二進制協(xié)議,這又是另一種樂趣了)

解析器不需要跟蹤狀態(tài),因此 http_parser 模塊其實就是一組函數(shù)。調(diào)用函數(shù)需要用到 Request 對象,并將它連同一個包含原始請求信息的字節(jié)數(shù)組傳遞給 parse_into 函數(shù)。然后解析器會修改 Request 對象以及充當(dāng)緩存的字節(jié)數(shù)組。字節(jié)數(shù)組的信息被逐漸地解析到 request 對象中。

http_parser 模塊的核心功能就是下面這個 parse_into 函數(shù):

  1. def parse_into(request, buffer): 
  2.     _buffer = buffer[:] 
  3.     if not request.method and can_parse_request_line(_buffer): 
  4.         (request.method, request.path, 
  5.          request.query_params) = parse_request_line(_buffer) 
  6.         remove_request_line(_buffer) 
  7.     if not request.headers and can_parse_headers(_buffer): 
  8.         request.headers = parse_headers(_buffer) 
  9.         if not has_body(request.headers): 
  10.             request.finished = True 
  11.         remove_intro(_buffer) 
  12.     if not request.finished and can_parse_body(request.headers, _buffer): 
  13.         request.body_raw, request.body = parse_body(request.headers, _buffer) 
  14.         clear_buffer(_buffer) 
  15.         request.finished = True 
  16.     return _buffer 

從上面的代碼中可以看到,我把解析的過程分為三個部分:解析請求行(這行像這樣:GET /resource HTTP/1.1),解析請求頭以及解析請求體。

請求行包含了 HTTP 請求方法以及 URL 地址。而 URL 地址則包含了更多的信息:路徑、url 參數(shù)和開發(fā)者自定義的 url 參數(shù)。解析請求方法和 URL 還是很容易的 - 合適地分割字符串就好了。函數(shù) urlparse.parse 可以用來解析 URL 參數(shù)。開發(fā)者自定義的 URL 參數(shù)可以通過正則表達式來解析。

接下來是 HTTP 頭部。它們是一行行由鍵值對組成的簡單文本。問題在于,可能有多個 HTTP 頭有相同的名字,卻有不同的值。一個值得關(guān)注的 HTTP 頭部是 Content-Length,它描述了請求體的字節(jié)長度(不是整個請求,僅僅是請求體)。這對于決定是否解析請求體有很重要的作用。

最后,解析器根據(jù) HTTP 方法和頭部來決定是否解析請求體。

路由!

在某種意義上,路由就像是連接框架和用戶的橋梁,用戶用合適的方法創(chuàng)建 Router 對象并為其設(shè)置路徑/函數(shù)對,然后將它賦值給 App 對象。而 App 對象依次調(diào)用 get_handler 函數(shù)生成相應(yīng)的回調(diào)函數(shù)。簡單來說,路由就負責(zé)兩件事,一是存儲路徑/函數(shù)對,二是返回需要的路徑/函數(shù)對

Router 類中有兩個允許最終開發(fā)者添加路由的方法,分別是 add_routes 和 add_route。因為 add_routes 就是 add_route 函數(shù)的一層封裝,我們將主要講解 add_route 函數(shù):

  1. def add_route(self, path, handler): 
  2.     compiled_route = self.class.build_route_regexp(path) 
  3.     if compiled_route not in self.routes: 
  4.         self.routes[compiled_route] = handler 
  5.     else
  6.         raise DuplicateRoute 

首先,這個函數(shù)使用 Router.build_router_regexp 的類方法,將一條路由規(guī)則(如 '/cars/{id}' 這樣的字符串),“編譯”到一個已編譯的正則表達式對象。這些已編譯的正則表達式用來匹配請求路徑,以及解析開發(fā)者自定義的 URL 參數(shù)。如果已經(jīng)存在一個相同的路由,程序就會拋出一個異常。最后,這個路由/處理程序?qū)Ρ惶砑拥揭粋€簡單的字典self.routes中。

下面展示 Router 是如何“編譯”路由的:

  1. @classmethod 
  2. def build_route_regexp(cls, regexp_str): 
  3.     ""
  4.     Turns a string into a compiled regular expression. Parses '{}' into 
  5.     named groups ie. '/path/{variable}' is turned into 
  6.     '/path/(?P<variable>[a-zA-Z0-9_-]+)'
  7.     :param regexp_str: a string representing a URL path. 
  8.     :return: a compiled regular expression. 
  9.     ""
  10.     def named_groups(matchobj): 
  11.         return '(?P<{0}>[a-zA-Z0-9_-]+)'.format(matchobj.group(1)) 
  12.     re_str = re.sub(r'{([a-zA-Z0-9_-]+)}', named_groups, regexp_str) 
  13.     re_str = ''.join(('^', re_str, '$',)) 
  14.     return re.compile(re_str) 

這個方法使用正則表達式將所有出現(xiàn)的 {variable} 替換為 (?P<variable>)。然后在字符串頭尾分別添加 ^ 和 $ 標(biāo)記,最后編譯正則表達式對象。

完成了路由存儲僅成功了一半,下面是如何得到路由對應(yīng)的函數(shù):

  1. def get_handler(self, path): 
  2.     logger.debug('Getting handler for: {0}'.format(path)) 
  3.     for route, handler in self.routes.items(): 
  4.         path_params = self.class.match_path(route, path) 
  5.         if path_params is not None: 
  6.             logger.debug('Got handler for: {0}'.format(path)) 
  7.             wrapped_handler = HandlerWrapper(handler, path_params) 
  8.             return wrapped_handler 
  9.     raise NotFoundException() 

一旦 App 對象獲得一個 Request 對象,也就獲得了 URL 的路徑部分(如 /users/15/edit)。然后,我們需要匹配函數(shù)來生成一個響應(yīng)或者 404 錯誤。get_handler 函數(shù)將路徑作為參數(shù),循環(huán)遍歷路由,對每條路由調(diào)用 Router.match_path 類方法檢查是否有已編譯的正則對象與這個請求路徑匹配。如果存在,我們就調(diào)用 HandleWrapper 來包裝路由對應(yīng)的函數(shù)。path_params 字典包含了路徑變量(如 '/users/15/edit' 中的 '15'),若路由沒有指定變量,字典就為空。最后,我們將包裝好的函數(shù)返回給 App 對象。

如果遍歷了所有的路由都找不到與路徑匹配的,函數(shù)就會拋出 NotFoundException 異常。

這個 Route.match 類方法挺簡單:

  1. def match_path(cls, route, path): 
  2.     match = route.match(path) 
  3.     try: 
  4.         return match.groupdict() 
  5.     except AttributeError: 
  6.         return None 

它使用正則對象的 match 方法來檢查路由是否與路徑匹配。若果不匹配,則返回 None 。

最后,我們有 HandleWraapper 類。它的唯一任務(wù)就是封裝一個異步函數(shù),存儲 path_params 字典,并通過 handle 方法對外提供一個統(tǒng)一的接口。

  1. class HandlerWrapper(object): 
  2.     def init(self, handler, path_params): 
  3.         self.handler = handler 
  4.         self.path_params = path_params 
  5.         self.request = None 
  6.     async def handle(self, request): 
  7.         return await self.handler(request, **self.path_params) 

組合到一起

框架的最后部分就是用 App 類把所有的部分聯(lián)系起來。

App 類用于集中所有的配置細節(jié)。一個 App 對象通過其 start_server 方法,使用一些配置數(shù)據(jù)創(chuàng)建一個 HTTPServer 的實例,然后將它傳遞給 asyncio.start_server 函數(shù)。asyncio.start_server 函數(shù)會對每一個 TCP 連接調(diào)用 HTTPServer 對象的 handle_connection 方法。 

  1. def start_server(self): 
  2.     if not self._server: 
  3.         self.loop = asyncio.get_event_loop() 
  4.         self._server = HTTPServer(self.router, self.http_parser, self.loop) 
  5.         self._connection_handler = asyncio.start_server( 
  6.             self._server.handle_connection, 
  7.             host=self.host, 
  8.             port=self.port, 
  9.             reuse_address=True
  10.             reuse_port=True
  11.             loop=self.loop) 
  12.         logger.info('Starting server on {0}:{1}'.format( 
  13.             self.host, self.port)) 
  14.         self.loop.run_until_complete(self._connection_handler) 
  15.         try: 
  16.             self.loop.run_forever() 
  17.         except KeyboardInterrupt: 
  18.             logger.info('Got signal, killing server'
  19.         except DiyFrameworkException as e: 
  20.             logger.error('Critical framework failure:'
  21.             logger.error(e.traceback) 
  22.         finally: 
  23.             self.loop.close() 
  24.     else
  25.         logger.info('Server already started - {0}'.format(self)) 

總結(jié)

如果你查看源碼,就會發(fā)現(xiàn)所有的代碼僅 320 余行(包括測試代碼的話共 540 余行)。這么少的代碼實現(xiàn)了這么多的功能,讓我有點驚訝。這個框架沒有提供模板、身份認證以及數(shù)據(jù)庫訪問等功能(這些內(nèi)容也很有趣哦)。這也讓我知道,像 Django 和 Tornado 這樣的框架是如何工作的,而且我能夠快速地調(diào)試它們了。

這也是我按照測試驅(qū)動開發(fā)完成的第一個項目,整個過程有趣而有意義。先編寫測試用例迫使我思考設(shè)計和架構(gòu),而不僅僅是把代碼放到一起,讓它們可以運行。不要誤解我的意思,有很多時候,后者的方式更好。不過如果你想給確保這些不怎么維護的代碼在之后的幾周甚至幾個月依然工作,那么測試驅(qū)動開發(fā)正是你需要的。

我研究了下整潔架構(gòu)以及依賴注入模式,這些充分體現(xiàn)在 Router 類是如何作為一個更高層次的抽象的(實體?)。Router 類是比較接近核心的,像 http_parser 和 App 的內(nèi)容比較邊緣化,因為它們只是完成了極小的字符串和字節(jié)流、或是中層 IO 的工作。測試驅(qū)動開發(fā)(TDD)迫使我獨立思考每個小部分,這使我問自己這樣的問題:方法調(diào)用的組合是否易于理解?類名是否準(zhǔn)確地反映了我正在解決的問題?我的代碼中是否很容易區(qū)分出不同的抽象層?

責(zé)任編輯:龐桂玉 來源: Linux中國
相關(guān)推薦

2020-11-09 06:38:00

ninja構(gòu)建方式構(gòu)建系統(tǒng)

2017-06-08 15:53:38

PythonWeb框架

2017-05-08 14:27:49

PHP框架函數(shù)框架

2020-09-24 11:46:03

Promise

2016-09-21 12:54:10

CAAS系統(tǒng)鏡像

2020-09-29 15:08:47

Go UI框架開發(fā)

2018-11-08 13:53:15

Flink程序環(huán)境

2019-05-14 12:30:07

PythonPygame游戲框架

2018-03-08 16:22:39

FlutterAndroid代碼

2013-01-14 09:44:58

JavaScriptJSJS框架

2025-01-03 09:00:00

代碼C++gTest

2019-04-24 15:06:37

Http服務(wù)器協(xié)議

2022-06-20 09:01:56

Plasmo開源

2019-06-10 15:00:27

node命令行前端

2021-08-04 05:49:40

數(shù)據(jù)庫數(shù)時序數(shù)據(jù)庫技術(shù)

2021-06-30 07:19:36

網(wǎng)絡(luò)安全

2021-07-29 10:46:56

Python內(nèi)置庫代碼

2021-08-27 09:48:18

Pythonitertools代碼

2020-11-30 13:16:29

Python編程語言

2017-11-23 08:30:26

編程Python擲骰子游戲
點贊
收藏

51CTO技術(shù)棧公眾號