python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

Python DNS 路由器 中央處理器 設計 幽默程序員 2019-07-04
python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享
python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享
python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享
python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

用戶應該能夠定義幾個異步函數返回字符串或響應對象,然後用表示路由的字符串與這些函數配對,最後通過一個函數調用(start_server)開始處理請求。

有了這些設計後,我需要編碼來實現這些抽象:

  • 一個可以接受TCP連接和進度的異步函數。
  • 將原始文本解析成某種抽象的容器。
  • 某種機制,可以確定每個請求,哪個函數應該被調用。
  • 將上面所有的集合在一起並提供一個簡單的接口給開發者。
python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

模擬異步連接

為了滿足限制(約束),每個HTTP請求是一個單獨的 TCP 連接。這會導致請求處理變慢,因為建立多個TCP連接(DNS查詢消耗,TCP三次握手消耗,慢啟動等)會有相對高的消耗,但是這樣更容易模擬。對於這個任務,我選擇了asyncio傳輸協議之上的高級的asyncio-stream模塊。我推薦從標準庫(stdlib)中籤出這段代碼,因為閱讀這段代碼會有樂趣。

HTTPConnection的實例處理多個任務。它使用asyncio.StreamReader對象以增量方式從TCP連接中讀取數據並將其存儲在緩存中。每次讀操作後,它試圖解析數據(無論是否在緩存中)並生成一個請求(Request)對象。一旦它接收整個請求,它生成一個回覆並通過asyncio.StreamWriter對象發送回客戶端。它還處理兩個更多的任務︰超時連接和錯誤處理。

你可以在這裡查看這個類的完整代碼。我將分開介紹代碼的每個部分,為了簡潔起見,我刪除了描述部分:

python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享
python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享
python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

所有代碼都是包在一個try-expect代碼塊中,在解析請求或響應期間拋出的任何異常都會被捕獲到,並且一個錯誤響應會發送回客戶端。

請求在一個while循環中會一直被讀取,當遇到以下情況會停止:當解析器設置self.request.finished = True時,或客戶端關閉了連接(self._reader.at_eof()方法返回True時)。代碼嘗試在每次循環迭代中從StreamReader中讀取數據,並通過調用self.process_data(data)逐步擴展self.request。每次循環讀取任何數據的時候,連接超時計時器會被重置。

代碼中有一個錯誤,你能找到嗎?我稍後會提到。我同樣注意到這個循環有可能消耗掉所有CPU資源,因為沒有東西可以讀的話,self._reader.read返回b""對象,意味著會不斷的循環,卻什麼也不做。一個可能的解決方案是以非阻塞方式等待一點時間:await asyncio.sleep(0.1)。我會在的確有必要的時候對其進行優化。

還記得我在上一段的開始提到的錯誤嗎?self._reset_conn_timeout方法僅在數據從StreamReader讀取時被調用。這種設置方式意味直接第一個字節到來時超時才會開始。如果一個客戶端同服務器建立了連接但並不發送任何數據,那麼就決不會超時。這可能會耗盡系統資源並引起服務的拒絕訪問。解決方法是隻需在 init方法中調用self._reset_conn_timeout。

在收到請求時或當連接斷開時,代碼流就會走到if-else代碼塊中。這部分代碼塊會判斷是否已經收到的所有數據並完成解析請求,如果是?那麼好,產生響應,並將其發送回客戶端。如果否?請求有錯誤發生,拋出異常。最後,調用self.close_connection做一些清理工作。

然析請求的代碼是在self.process_data方法中。這段代碼非常短且簡單,易於測試。

python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

每次調用將數據積累到self._buffer中並且使用self.http_parser嘗試解析任何已存放在緩衝區中的數據。值得在這裡指出這段代碼展示了一種“依賴注入”的模式。如果你記得init這個初始化函數,你應該知道我傳進來一個http_server對象。在這種情況下,http_parser對象是diy_framework包中的一個模塊,它有一個parse_into函數,這個函數接受一個Request對象和一個bytearray作為參數。這很有用,原因有兩個。首先,這段代碼很容易擴展。比如某人想通過一個不同的解析器來使用HTTPConnection,沒問題,只要將這個解析器作為參數傳遞就可以了。其次,它使測試更加容易,因為http_parser不是硬編碼的,因此我們可以用假數據來代替會變得非常容易。

下一個有趣部分是響應方法:

python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

在這裡,HTTPConnection實例使用一個來自HTTPServer的路由(router)對象來獲取一個生成響應的對象。一個路由可以是任何一個具有get_handler方法的對象,這個方法接受一個字符串參數,並且返回一個可調用的對象或拋出NotFoundException異常。可調用對象是用來處理請求和生成響應。處理程序由使用這個框架的用戶來寫,就像上面用例中概括的那樣,應返回字符串或響應對象。響應對象提供給我們一個友好的接口,因此簡單的if代碼塊確保,無論處理程序返回什麼,這段代碼最終返回一個統一的響應對象。

接下來,賦值給self._writer的StreamWriter實例被調用,將字節流字符串發送回客戶端。在函數返回前,它等待await self._writer.drain,這就保證了所有數據已被髮送到客戶端。這將確保當仍有未發送的數據在緩衝區中時,對self._writer.close的調用不會發生。

HTTPConnection類有兩個有趣的地方:一個關閉連接的方法和一組處理超時機制的方法。首先,關閉一個連接是通過下面這個小函數來完成的:

python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

任何時候一個連接將被關閉時,代碼首先要做的就是取消超時,將它從事件循環中清除掉。

超時機制是一套三個相關的函數︰一個函數,發送錯誤信息給客戶端並且關閉連接的超時器;一個函數,取消當前的超時器;一個函數,調度超時功能。前兩個是簡單的,出於完整性考慮我添加了它們,我將著重解釋下第三個:_reset_conn_timeout。

python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

每次_reset_conn_timeout被調用,它首先取消任何以前賦值給self._conn_timeout設置的asyncio.Handle對象。然後,使用BaseEventLoop.call_later函數,計劃在超時數秒後運行_conn_timeout_close函數。如果你記得handle_request函數的內容,你就會知道每次接收任何數據時,就會調用此函數。在將來,這將取消任何現有的超時並且重新設置_conn_timeout_close函數的超時秒數。只要有數據到來,這個循環就不斷重置超時回調。如果在超時秒數內沒接收到任何數據,_conn_timeout_close最終將被調用。

創建連接

有些事情不得不創建HTTPConnection對象並且要處理好這個對象。這項任務委託給HTTPServer類,該類是一個非常簡單的容器,可以幫助存儲一些配置(解析器、路由器和事件循環實例),然後使用該配置來創建HTTPConnection的實例:

python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

HTTPServer 的每個實例監聽在一個端口上。它有一個異步的handle_connection方法,這個方法用來創建HTTPConnection實例並且在事件循環中調試執行它們。這個方法傳遞給asyncio.start_server作為一個回調函數:每次TCP連接開始的時候被調用(StreamReader和StreamWriter作為參數)。

python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

這構成了應用程序工作原理的核心:asyncio.start_server接受TCP連接,並在預配置的 HTTPServer 對象上調用一個方法。此方法處理單個連接的所有邏輯:獲取請求、解析、生成響應併發送回客戶端,最後關閉連接。它側重於IO邏輯、解析和產生響應。

非核心IO東西我們先不管。

解析請求

這個微型框架的用戶被寵壞了,他們不想使用字節流。他們想要一個更高層次的抽象——一種處理請求(requests)更方便的方式。這個微型框架包含了一個簡單的HTTP解析器將字節流轉換為請求對象。

這些請求對象是看起來像這樣的容器:

python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

它擁有開發者需要的所有,接收來自客戶端很容易理解的包中的數據。那麼,除了cookies外,身份驗證是至關重要的,我將這部分留在第2部分。

每個HTTP請求包含某些所需的部分——比如請求的路徑或請求的方法。它還包含某些可選的部分,比如body體,header頭部,或URL參數。此外,由於REST的流行,URL,省略URL參數,還可能包含部分信息。比如,"/users/1/edit"包含了用戶的id。

每個請求的各個部分必須被識別,解析並被賦值給一個請求(Request)對象的各個屬性。HTTP/1.1是文本協議,簡化了過程(HTTP/2是二進制協議-相當有趣)。

http_parser模塊中是一組函數,因為解析器不需要跟蹤狀態。相反,調用代碼需要管理一個請求(Request)對象並將其和bytearray(包含請求的原始字節)一起傳遞到parse_into函數中。為此,解析器修改請求對象以及bytearray緩衝區內容。請求對象獲取越來越多數據,而ytearray緩衝區逐漸變空。

Http_parser模塊的核心功能是在parse_into函數中:

python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

正如你在上面代碼中看到的那樣,我將解析過程劃分為三個部分:解析請求行(請求行有這樣的格式:GET /resource HTTP/1.1),解析頭部和解析請求body體。

請求行包含的 HTTP 方法和 URL。URL按序還包含更多的信息:路徑、url參數和開發人員自定義的url參數。解析出方法和URL很容易——重要的是適當地分割字符串。urlparse.parse函數用於解析URL參數。開發人員定義的url參數使用正則表達式提取。

接下來是HTTP頭部。這些都是鍵/值對形式的簡單文本。問題在於,有可能多個頭部有相同名稱但值不同。一個需要注意的header頭部是Content-Length,它指定了body體的長度(不是整個請求,僅僅是body體),對於決定是否解析body體來說是重要的。

最後,解析器根據HTTP方法和頭部來決定是否解析請求的body體。

路由

在某種意義上,路由是框架和用戶間連接的橋,用戶用合適的方法創建一個路由(Router)對象,這個對象由路徑/函數對組成,然後將路由對象賦值給App對象。這個App對象按順序調用get_handler函數生成一個響應。總之,路由負責兩件事情——存儲路徑/函數對和返回所要求的一對。

在路由(Router)類中有兩個方法允許最終開發人員增加路由:add_routes和add_route。由於add_routes是在add_route上的一層封裝,我將略過它,將注意力集中在 add_route上。

python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

使用Router.build_route_regexp類方法首先將一個路由(一個字符串,類似"/cars/{id}")編譯成一個已編譯的正則表達式對象。這些已編譯正則表達式對象匹配請求路徑和提取指定路由的開發人員定義的URL參數。下一步如果有相同的路由存在,則會拋出異常,最終,路由/處理函數對被加入到一個簡單的字典中——self.routes。

這裡展示了Router如何編譯路由(routes)的:

python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

該方法使用正則表達式來替換命名的正則表達式組"(?P<variable>...)"中"{variable}"出現的位置。然後在結果字符串的開頭和結尾加上^和$標記,最後編譯正則表達式對象。

存儲一條路由僅僅成功了一半,這裡是如何獲得一條路由:

python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

一旦App對象獲得一個請求對象,就會獲知URL的路徑部分(比如,/users/15/edit)。然後需要一個匹配函數生成響應或404錯誤。get_handler將路徑作為參數,循環遍歷路由,在每個路由上調用Router.match_path類方法來檢測是否有已編譯的正則對象匹配請求的路徑。如果存在,則調用HandleWrapper來包裝這個路由函數。path_params字典包含了路徑變量(比如,/users/15/edit中的"15")或如果路由不指定任何變量的話則為空。最後返回包裝過的路由函數給App對象。

如果代碼迭代遍歷整個路由,沒有找到匹配路徑的,函數拋出NotFoundException異常。

這個Route.match類方法簡單:

python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

使用正則對象的匹配方法檢測路由與路徑是否匹配。如果不匹配則返回None。

最後,我們使用HandleWrapper類。它唯一的工作就是包裝一個異步函數,存儲path_params字典,通過handle方法對外提供一個統一的接口。

python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

整合所有

框架的最後部分是將所有部分集合在一起——App類。

App類旨在收集所有的配置詳細信息。App對象使用它的單個方法start_server,使用一些配置數據創建HTTPServer實例,然後將其傳遞給函數asyncio.start_server,這裡查看。每一個進來的TCP連接,asyncio.start_server函數調用HTTPServer對象的handle_connection方法。

python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

吸取的經驗教訓

如果你看了整個源碼,你會注意到如果不算上測試代碼的話,整個框架大致320行代碼(算上測試代碼,大約540行代碼)。確實令我感到驚訝,如此少的代碼滿足如此多的功能。當然,這個框架還沒提供一些有用的功能比如模板,身份驗證或數據庫訪問,不過,這些工作會非常有趣:)。這同樣給了我一些想法關於其他框架,比如Django或Tornado是如何工作在一個一般水平的並且我能夠快速調試它們。

這也是我以TDD方式(測試驅動開發(Test-Driven Development))做的第一個項目,過程是愉快和高效的。編寫測試首先迫使我思考設計和架構而不僅僅是將能工作的代碼粘合起來。不要誤會我的意思,有很多情況,後者的方法是首選的,但是,如果你對不可維護的代碼很重視,你和其他人在未來的數週或數月可以很好的工作,那麼TDD確實是你需要的。

我研究一些東西像清晰架構和依賴注入,很明顯,路由(Router)類是一個更高級別的接近“核心”的抽象(實體?),然而,像http_parser或App是處在邊緣上的,它們要麼處理極小的字符串,要麼是字節流,要麼是中級IO。然而,TDD迫使我去單獨思考每個小部分,這讓我問自己這樣的問題:是否這些方法調用的組合是可理解的?是否類名準確的反映了我正在解決的問題?是否容易區分我代碼中不同級別的抽象?

來吧,寫一個小的框架,充滿樂趣 !

最後,想學習Python的小夥伴們!

請關注+私信回覆:“資料”就可以拿到一份我為大家準備的Python學習資料!

python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

pytyhon學習資料

python初學者必看!零基礎搭建一個Web框架,附全套教程視頻分享

python學習資料

相關推薦

推薦中...