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

- 你有一個(gè)新奇的想法,覺(jué)得將會(huì)取代其他的框架
- 你想要獲得一些名氣
- 你遇到的問(wèn)題很獨(dú)特,以至于現(xiàn)有的框架不太合適
- 你對(duì) web 框架是如何工作的很感興趣,因?yàn)槟阆胍蔀橐晃桓玫?web 開(kāi)發(fā)者。
接下來(lái)的筆墨將著重于最后一點(diǎn)。這篇文章旨在通過(guò)對(duì)設(shè)計(jì)和實(shí)現(xiàn)過(guò)程一步一步的闡述告訴讀者,我在完成一個(gè)小型的服務(wù)器和框架之后學(xué)到了什么。你可以在這個(gè)代碼倉(cāng)庫(kù)中找到這個(gè)項(xiàng)目的完整代碼。
我希望這篇文章可以鼓勵(lì)更多的人來(lái)嘗試,因?yàn)檫@確實(shí)很有趣。它讓我知道了 web 應(yīng)用是如何工作的,而且這比我想的要容易的多!
范圍
框架可以處理請(qǐng)求-響應(yīng)周期、身份認(rèn)證、數(shù)據(jù)庫(kù)訪問(wèn)、模板生成等部分工作。Web 開(kāi)發(fā)者使用框架是因?yàn)?,大多?shù)的 web 應(yīng)用擁有大量相同的功能,而對(duì)每個(gè)項(xiàng)目都重新實(shí)現(xiàn)同樣的功能意義不大。
比較大的的框架如 Rails 和 Django 實(shí)現(xiàn)了高層次的抽象,或者說(shuō)“自備電池”(“batteries-included”,這是 Python 的口號(hào)之一,意即所有功能都自足。)。而實(shí)現(xiàn)所有的這些功能可能要花費(fèi)數(shù)千小時(shí),因此在這個(gè)項(xiàng)目上,我們重點(diǎn)完成其中的一小部分。在開(kāi)始寫代碼前,我先列舉一下所需的功能以及限制。
功能:
- 處理 HTTP 的 GET 和 POST 請(qǐng)求。你可以在這篇 wiki 中對(duì) HTTP 有個(gè)大致的了解。
- 實(shí)現(xiàn)異步操作(我喜歡 Python 3 的 asyncio 模塊)。
- 簡(jiǎn)單的路由邏輯以及參數(shù)擷取。
- 像其他微型框架一樣,提供一個(gè)簡(jiǎn)單的用戶級(jí) API 。
- 支持身份認(rèn)證,因?yàn)閷W(xué)會(huì)這個(gè)很酷啊(微笑)。
限制:
- 將只支持 HTTP 1.1 的一個(gè)小子集,不支持傳輸編碼(transfer-encoding)、HTTP 認(rèn)證(http-auth)、內(nèi)容編碼(content-encoding)(如 gzip)以及持久化連接等功能。
- 不支持對(duì)響應(yīng)內(nèi)容的 MIME 判斷 - 用戶需要手動(dòng)指定。
- 不支持 WSGI - 僅能處理簡(jiǎn)單的 TCP 連接。
- 不支持?jǐn)?shù)據(jù)庫(kù)。
我覺(jué)得一個(gè)小的用例可以讓上述內(nèi)容更加具體,也可以用來(lái)演示這個(gè)框架的 API:
- from diy_framework import App, Router
- from diy_framework.http_utils import Response
- # GET simple route
- async def home(r):
- rsp = Response()
- rsp.set_header('Content-Type', 'text/html')
- rsp.body = 'test'
- return rsp
- # GET route + params
- async def welcome(r, name):
- return "Welcome {}".format(name)
- # POST route + body param
- async def parse_form(r):
- if r.method == 'GET':
- return 'form'
- else:
- name = r.body.get('name', '')[0]
- password = r.body.get('password', '')[0]
- return "{0}:{1}".format(name, password)
- # application = router + http server
- router = Router()
- router.add_routes({
- r'/welcome/{name}': welcome,
- r'/': home,
- r'/login': parse_form,})
- app = App(router)
- app.start_server()
' 用戶需要定義一些能夠返回字符串或 Response 對(duì)象的異步函數(shù),然后將這些函數(shù)與表示路由的字符串配對(duì),最后通過(guò)一個(gè)函數(shù)調(diào)用(start_server)開(kāi)始處理請(qǐng)求。
完成設(shè)計(jì)之后,我將它抽象為幾個(gè)我需要編碼的部分:
- 接受 TCP 連接以及調(diào)度一個(gè)異步函數(shù)來(lái)處理這些連接的部分
- 將原始文本解析成某種抽象容器的部分
- 對(duì)于每個(gè)請(qǐng)求,用來(lái)決定調(diào)用哪個(gè)函數(shù)的部分
- 將上述部分集中到一起,并為開(kāi)發(fā)者提供一個(gè)簡(jiǎn)單接口的部分
我先編寫一些測(cè)試,這些測(cè)試被用來(lái)描述每個(gè)部分的功能。幾次重構(gòu)后,整個(gè)設(shè)計(jì)被分成若干部分,每個(gè)部分之間是相對(duì)解耦的。這樣就非常好,因?yàn)槊總€(gè)部分可以被獨(dú)立地研究學(xué)習(xí)。以下是我上文列出的抽象的具體體現(xiàn):
- 一個(gè) HTTPServer 對(duì)象,需要一個(gè) Router 對(duì)象和一個(gè) http_parser 模塊,并使用它們來(lái)初始化。
- HTTPConnection 對(duì)象,每一個(gè)對(duì)象表示一個(gè)單獨(dú)的客戶端 HTTP 連接,并且處理其請(qǐng)求-響應(yīng)周期:使用 http_parser 模塊將收到的字節(jié)流解析為一個(gè) Request 對(duì)象;使用一個(gè) Router 實(shí)例尋找并調(diào)用正確的函數(shù)來(lái)生成一個(gè)響應(yīng);最后將這個(gè)響應(yīng)發(fā)送回客戶端。
- 一對(duì) Request 和 Response 對(duì)象為用戶提供了一種友好的方式,來(lái)處理實(shí)質(zhì)上是字節(jié)流的字符串。用戶不需要知道正確的消息格式和分隔符是怎樣的。
- 一個(gè)包含“路由:函數(shù)”對(duì)應(yīng)關(guān)系的 Router 對(duì)象。它提供一個(gè)添加配對(duì)的方法,可以根據(jù) URL 路徑查找到相應(yīng)的函數(shù)。
- 最后,一個(gè) App 對(duì)象。它包含配置信息,并使用它們實(shí)例化一個(gè) HTTPServer 實(shí)例。
讓我們從 HTTPConnection 開(kāi)始來(lái)講解各個(gè)部分。
模擬異步連接
為了滿足上述約束條件,每一個(gè) HTTP 請(qǐng)求都是一個(gè)單獨(dú)的 TCP 連接。這使得處理請(qǐng)求的速度變慢了,因?yàn)榻⒍鄠€(gè) TCP 連接需要相對(duì)高的花銷(DNS 查詢,TCP 三次握手,慢啟動(dòng)等等的花銷),不過(guò)這樣更加容易模擬。對(duì)于這一任務(wù),我選擇相對(duì)高級(jí)的 asyncio-stream 模塊,它建立在 asyncio 的傳輸和協(xié)議的基礎(chǔ)之上。我強(qiáng)烈推薦你讀一讀標(biāo)準(zhǔn)庫(kù)中的相應(yīng)代碼,很有意思!
一個(gè) HTTPConnection 的實(shí)例能夠處理多個(gè)任務(wù)。首先,它使用 asyncio.StreamReader 對(duì)象以增量的方式從 TCP 連接中讀取數(shù)據(jù),并存儲(chǔ)在緩存中。每一個(gè)讀取操作完成后,它會(huì)嘗試解析緩存中的數(shù)據(jù),并生成一個(gè) Request 對(duì)象。一旦收到了這個(gè)完整的請(qǐng)求,它就生成一個(gè)回復(fù),并通過(guò) asyncio.StreamWriter 對(duì)象發(fā)送回客戶端。當(dāng)然,它還有兩個(gè)任務(wù):超時(shí)連接以及錯(cuò)誤處理。
你可以在這里瀏覽這個(gè)類的完整代碼。我將分別介紹代碼的每一部分。為了簡(jiǎn)單起見(jiàn),我移除了代碼文檔。
- class HTTPConnection(object):
- def init(self, http_server, reader, writer):
- self.router = http_server.router
- self.http_parser = http_server.http_parser
- self.loop = http_server.loop
- self._reader = reader
- self._writer = writer
- self._buffer = bytearray()
- self._conn_timeout = None
- self.request = Request()
這個(gè) init 方法沒(méi)啥意思,它僅僅是收集了一些對(duì)象以供后面使用。它存儲(chǔ)了一個(gè) router 對(duì)象、一個(gè) http_parser 對(duì)象以及 loop 對(duì)象,分別用來(lái)生成響應(yīng)、解析請(qǐng)求以及在事件循環(huán)中調(diào)度任務(wù)。
然后,它存儲(chǔ)了代表一個(gè) TCP 連接的讀寫對(duì),和一個(gè)充當(dāng)原始字節(jié)緩沖區(qū)的空字節(jié)數(shù)組。_conn_timeout 存儲(chǔ)了一個(gè) asyncio.Handle 的實(shí)例,用來(lái)管理超時(shí)邏輯。最后,它還存儲(chǔ)了 Request 對(duì)象的一個(gè)單一實(shí)例。
下面的代碼是用來(lái)接受和發(fā)送數(shù)據(jù)的核心功能:
- async def handle_request(self):
- try:
- while not self.request.finished and not self._reader.at_eof():
- data = await self._reader.read(1024)
- if data:
- self._reset_conn_timeout()
- await self.process_data(data)
- if self.request.finished:
- await self.reply()
- elif self._reader.at_eof():
- raise BadRequestException()
- except (NotFoundException,
- BadRequestException) as e:
- self.error_reply(e.code, body=Response.reason_phrases[e.code])
- except Exception as e:
- self.error_reply(500, body=Response.reason_phrases[500])
- self.close_connection()
所有內(nèi)容被包含在 try-except 代碼塊中,這樣在解析請(qǐng)求或響應(yīng)期間拋出的異??梢员徊东@到,然后一個(gè)錯(cuò)誤響應(yīng)會(huì)發(fā)送回客戶端。
在 while 循環(huán)中不斷讀取請(qǐng)求,直到解析器將 self.request.finished 設(shè)置為 True ,或者客戶端關(guān)閉連接所觸發(fā)的信號(hào)使得 self._reader_at_eof() 函數(shù)返回值為 True 為止。這段代碼嘗試在每次循環(huán)迭代中從 StreamReader 中讀取數(shù)據(jù),并通過(guò)調(diào)用 self.process_data(data) 函數(shù)以增量方式生成 self.request。每次循環(huán)讀取數(shù)據(jù)時(shí),連接超時(shí)計(jì)數(shù)器被重置。
這兒有個(gè)錯(cuò)誤,你發(fā)現(xiàn)了嗎?稍后我們會(huì)再討論這個(gè)。需要注意的是,這個(gè)循環(huán)可能會(huì)耗盡 CPU 資源,因?yàn)槿绻麤](méi)有讀取到東西 self._reader.read() 函數(shù)將會(huì)返回一個(gè)空的字節(jié)對(duì)象 b''。這就意味著循環(huán)將會(huì)不斷運(yùn)行,卻什么也不做。一個(gè)可能的解決方法是,用非阻塞的方式等待一小段時(shí)間:await asyncio.sleep(0.1)。我們暫且不對(duì)它做優(yōu)化。
還記得上一段我提到的那個(gè)錯(cuò)誤嗎?只有從 StreamReader 讀取數(shù)據(jù)時(shí),self._reset_conn_timeout() 函數(shù)才會(huì)被調(diào)用。這就意味著,直到第一個(gè)字節(jié)到達(dá)時(shí),timeout 才被初始化。如果有一個(gè)客戶端建立了與服務(wù)器的連接卻不發(fā)送任何數(shù)據(jù),那就永遠(yuǎn)不會(huì)超時(shí)。這可能被用來(lái)消耗系統(tǒng)資源,從而導(dǎo)致拒絕服務(wù)式攻擊(DoS)。修復(fù)方法就是在 init 函數(shù)中調(diào)用 self._reset_conn_timeout() 函數(shù)。
當(dāng)請(qǐng)求接受完成或連接中斷時(shí),程序?qū)⑦\(yùn)行到 if-else 代碼塊。這部分代碼會(huì)判斷解析器收到完整的數(shù)據(jù)后是否完成了解析。如果是,好,生成一個(gè)回復(fù)并發(fā)送回客戶端。如果不是,那么請(qǐng)求信息可能有錯(cuò)誤,拋出一個(gè)異常!最后,我們調(diào)用 self.close_connection 執(zhí)行清理工作。
解析請(qǐng)求的部分在 self.process_data 方法中。這個(gè)方法非常簡(jiǎn)短,也易于測(cè)試:
- async def process_data(self, data):
- self._buffer.extend(data)
- self._buffer = self.http_parser.parse_into(
- self.request, self._buffer)
每一次調(diào)用都將數(shù)據(jù)累積到 self._buffer 中,然后試著用 self.http_parser 來(lái)解析已經(jīng)收集的數(shù)據(jù)。這里需要指出的是,這段代碼展示了一種稱為依賴注入(Dependency Injection)的模式。如果你還記得 init 函數(shù)的話,應(yīng)該知道我們傳入了一個(gè)包含 http_parser 對(duì)象的 http_server 對(duì)象。在這個(gè)例子里,http_parser 對(duì)象是 diy_framework 包中的一個(gè)模塊。不過(guò)它也可以是任何含有 parse_into 函數(shù)的類,這個(gè) parse_into 函數(shù)接受一個(gè) Request 對(duì)象以及字節(jié)數(shù)組作為參數(shù)。這很有用,原因有二:一是,這意味著這段代碼更易擴(kuò)展。如果有人想通過(guò)一個(gè)不同的解析器來(lái)使用 HTTPConnection,沒(méi)問(wèn)題,只需將它作為參數(shù)傳入即可。二是,這使得測(cè)試更加容易,因?yàn)?http_parser 不是硬編碼的,所以使用虛假數(shù)據(jù)或者 mock 對(duì)象來(lái)替代是很容易的。
下一段有趣的部分就是 reply 方法了:
- async def reply(self):
- request = self.request
- handler = self.router.get_handler(request.path)
- response = await handler.handle(request)
- if not isinstance(response, Response):
- response = Response(code=200, body=response)
- self._writer.write(response.to_bytes())
- await self._writer.drain()
這里,一個(gè) HTTPConnection 的實(shí)例使用了 HTTPServer 中的 router 對(duì)象來(lái)得到一個(gè)生成響應(yīng)的對(duì)象。一個(gè)路由可以是任何一個(gè)擁有 get_handler 方法的對(duì)象,這個(gè)方法接收一個(gè)字符串作為參數(shù),返回一個(gè)可調(diào)用的對(duì)象或者拋出 NotFoundException 異常。而這個(gè)可調(diào)用的對(duì)象被用來(lái)處理請(qǐng)求以及生成響應(yīng)。處理程序由框架的使用者編寫,如上文所說(shuō)的那樣,應(yīng)該返回字符串或者 Response 對(duì)象。Response 對(duì)象提供了一個(gè)友好的接口,因此這個(gè)簡(jiǎn)單的 if 語(yǔ)句保證了無(wú)論處理程序返回什么,代碼最終都得到一個(gè)統(tǒng)一的 Response 對(duì)象。
接下來(lái),被賦值給 self._writer 的 StreamWriter 實(shí)例被調(diào)用,將字節(jié)字符串發(fā)送回客戶端。函數(shù)返回前,程序在 await self._writer.drain() 處等待,以確保所有的數(shù)據(jù)被發(fā)送給客戶端。只要緩存中還有未發(fā)送的數(shù)據(jù),self._writer.close() 方法就不會(huì)執(zhí)行。
HTTPConnection 類還有兩個(gè)更加有趣的部分:一個(gè)用于關(guān)閉連接的方法,以及一組用來(lái)處理超時(shí)機(jī)制的方法。首先,關(guān)閉一條連接由下面這個(gè)小函數(shù)完成:
- def close_connection(self):
- self._cancel_conn_timeout()
- self._writer.close()
每當(dāng)一條連接將被關(guān)閉時(shí),這段代碼首先取消超時(shí),然后把連接從事件循環(huán)中清除。
超時(shí)機(jī)制由三個(gè)相關(guān)的函數(shù)組成:第一個(gè)函數(shù)在超時(shí)后給客戶端發(fā)送錯(cuò)誤消息并關(guān)閉連接;第二個(gè)函數(shù)用于取消當(dāng)前的超時(shí);第三個(gè)函數(shù)調(diào)度超時(shí)功能。前兩個(gè)函數(shù)比較簡(jiǎn)單,我將詳細(xì)解釋第三個(gè)函數(shù) _reset_cpmm_timeout() 。
- def _conn_timeout_close(self):
- self.error_reply(500, 'timeout')
- self.close_connection()
- def _cancel_conn_timeout(self):
- if self._conn_timeout:
- self._conn_timeout.cancel()
- def _reset_conn_timeout(self, timeout=TIMEOUT):
- self._cancel_conn_timeout()
- self._conn_timeout = self.loop.call_later(
- timeout, self._conn_timeout_close)
每當(dāng) _reset_conn_timeout 函數(shù)被調(diào)用時(shí),它會(huì)先取消之前所有賦值給 self._conn_timeout 的 asyncio.Handle 對(duì)象。然后,使用 BaseEventLoop.call_later 函數(shù)讓 _conn_timeout_close 函數(shù)在超時(shí)數(shù)秒(timeout)后執(zhí)行。如果你還記得 handle_request 函數(shù)的內(nèi)容,就知道每當(dāng)接收到數(shù)據(jù)時(shí),這個(gè)函數(shù)就會(huì)被調(diào)用。這就取消了當(dāng)前的超時(shí)并且重新安排 _conn_timeout_close 函數(shù)在超時(shí)數(shù)秒(timeout)后執(zhí)行。只要接收到數(shù)據(jù),這個(gè)循環(huán)就會(huì)不斷地重置超時(shí)回調(diào)。如果在超時(shí)時(shí)間內(nèi)沒(méi)有接收到數(shù)據(jù),最后函數(shù) _conn_timeout_close 就會(huì)被調(diào)用。
創(chuàng)建連接
我們需要?jiǎng)?chuàng)建 HTTPConnection 對(duì)象,并且正確地使用它們。這一任務(wù)由 HTTPServer 類完成。HTTPServer 類是一個(gè)簡(jiǎn)單的容器,可以存儲(chǔ)著一些配置信息(解析器,路由和事件循環(huán)實(shí)例),并使用這些配置來(lái)創(chuàng)建 HTTPConnection 實(shí)例:
- class HTTPServer(object):
- def init(self, router, http_parser, loop):
- self.router = router
- self.http_parser = http_parser
- self.loop = loop
- async def handle_connection(self, reader, writer):
- connection = HTTPConnection(self, reader, writer)
- asyncio.ensure_future(connection.handle_request(), loop=self.loop)
HTTPServer 的每一個(gè)實(shí)例能夠監(jiān)聽(tīng)一個(gè)端口。它有一個(gè) handle_connection 的異步方法來(lái)創(chuàng)建 HTTPConnection 的實(shí)例,并安排它們?cè)谑录h(huán)中運(yùn)行。這個(gè)方法被傳遞給 asyncio.start_server 作為一個(gè)回調(diào)函數(shù)。也就是說(shuō),每當(dāng)一個(gè) TCP 連接初始化時(shí)(以 StreamReader 和 StreamWriter 為參數(shù)),它就會(huì)被調(diào)用。
- self._server = HTTPServer(self.router, self.http_parser, self.loop)
- self._connection_handler = asyncio.start_server(
- self._server.handle_connection,
- host=self.host,
- port=self.port,
- reuse_address=True,
- reuse_port=True,
- loop=self.loop)
這就是構(gòu)成整個(gè)應(yīng)用程序工作原理的核心:asyncio.start_server 接受 TCP 連接,然后在一個(gè)預(yù)配置的 HTTPServer 對(duì)象上調(diào)用一個(gè)方法。這個(gè)方法將處理一條 TCP 連接的所有邏輯:讀取、解析、生成響應(yīng)并發(fā)送回客戶端、以及關(guān)閉連接。它的重點(diǎn)是 IO 邏輯、解析和生成響應(yīng)。
講解了核心的 IO 部分,讓我們繼續(xù)。
解析請(qǐng)求
這個(gè)微型框架的使用者被寵壞了,不愿意和字節(jié)打交道。它們想要一個(gè)更高層次的抽象 —— 一種更加簡(jiǎn)單的方法來(lái)處理請(qǐng)求。這個(gè)微型框架就包含了一個(gè)簡(jiǎn)單的 HTTP 解析器,能夠?qū)⒆止?jié)流轉(zhuǎn)化為 Request 對(duì)象。
這些 Request 對(duì)象是像這樣的容器:
- class Request(object):
- def init(self):
- self.method = None
- self.path = None
- self.query_params = {}
- self.path_params = {}
- self.headers = {}
- self.body = None
- self.body_raw = None
- self.finished = False
它包含了所有需要的數(shù)據(jù),可以用一種容易理解的方法從客戶端接受數(shù)據(jù)。哦,不包括 cookie ,它對(duì)身份認(rèn)證是非常重要的,我會(huì)將它留在第二部分。
每一個(gè) HTTP 請(qǐng)求都包含了一些必需的內(nèi)容,如請(qǐng)求路徑和請(qǐng)求方法。它們也包含了一些可選的內(nèi)容,如請(qǐng)求體、請(qǐng)求頭,或是 URL 參數(shù)。隨著 REST 的流行,除了 URL 參數(shù),URL 本身會(huì)包含一些信息。比如,"/user/1/edit" 包含了用戶的 id 。
一個(gè)請(qǐng)求的每個(gè)部分都必須被識(shí)別、解析,并正確地賦值給 Request 對(duì)象的對(duì)應(yīng)屬性。HTTP/1.1 是一個(gè)文本協(xié)議,事實(shí)上這簡(jiǎn)化了很多東西。(HTTP/2 是一個(gè)二進(jìn)制協(xié)議,這又是另一種樂(lè)趣了)
解析器不需要跟蹤狀態(tài),因此 http_parser 模塊其實(shí)就是一組函數(shù)。調(diào)用函數(shù)需要用到 Request 對(duì)象,并將它連同一個(gè)包含原始請(qǐng)求信息的字節(jié)數(shù)組傳遞給 parse_into 函數(shù)。然后解析器會(huì)修改 Request 對(duì)象以及充當(dāng)緩存的字節(jié)數(shù)組。字節(jié)數(shù)組的信息被逐漸地解析到 request 對(duì)象中。
http_parser 模塊的核心功能就是下面這個(gè) parse_into 函數(shù):
- def parse_into(request, buffer):
- _buffer = buffer[:]
- if not request.method and can_parse_request_line(_buffer):
- (request.method, request.path,
- request.query_params) = parse_request_line(_buffer)
- remove_request_line(_buffer)
- if not request.headers and can_parse_headers(_buffer):
- request.headers = parse_headers(_buffer)
- if not has_body(request.headers):
- request.finished = True
- remove_intro(_buffer)
- if not request.finished and can_parse_body(request.headers, _buffer):
- request.body_raw, request.body = parse_body(request.headers, _buffer)
- clear_buffer(_buffer)
- request.finished = True
- return _buffer
從上面的代碼中可以看到,我把解析的過(guò)程分為三個(gè)部分:解析請(qǐng)求行(這行像這樣:GET /resource HTTP/1.1),解析請(qǐng)求頭以及解析請(qǐng)求體。
請(qǐng)求行包含了 HTTP 請(qǐng)求方法以及 URL 地址。而 URL 地址則包含了更多的信息:路徑、url 參數(shù)和開(kāi)發(fā)者自定義的 url 參數(shù)。解析請(qǐng)求方法和 URL 還是很容易的 - 合適地分割字符串就好了。函數(shù) urlparse.parse 可以用來(lái)解析 URL 參數(shù)。開(kāi)發(fā)者自定義的 URL 參數(shù)可以通過(guò)正則表達(dá)式來(lái)解析。
接下來(lái)是 HTTP 頭部。它們是一行行由鍵值對(duì)組成的簡(jiǎn)單文本。問(wèn)題在于,可能有多個(gè) HTTP 頭有相同的名字,卻有不同的值。一個(gè)值得關(guān)注的 HTTP 頭部是 Content-Length,它描述了請(qǐng)求體的字節(jié)長(zhǎng)度(不是整個(gè)請(qǐng)求,僅僅是請(qǐng)求體)。這對(duì)于決定是否解析請(qǐng)求體有很重要的作用。
最后,解析器根據(jù) HTTP 方法和頭部來(lái)決定是否解析請(qǐng)求體。
路由!
在某種意義上,路由就像是連接框架和用戶的橋梁,用戶用合適的方法創(chuàng)建 Router 對(duì)象并為其設(shè)置路徑/函數(shù)對(duì),然后將它賦值給 App 對(duì)象。而 App 對(duì)象依次調(diào)用 get_handler 函數(shù)生成相應(yīng)的回調(diào)函數(shù)。簡(jiǎn)單來(lái)說(shuō),路由就負(fù)責(zé)兩件事,一是存儲(chǔ)路徑/函數(shù)對(duì),二是返回需要的路徑/函數(shù)對(duì)
Router 類中有兩個(gè)允許最終開(kāi)發(fā)者添加路由的方法,分別是 add_routes 和 add_route。因?yàn)?add_routes 就是 add_route 函數(shù)的一層封裝,我們將主要講解 add_route 函數(shù):
- def add_route(self, path, handler):
- compiled_route = self.class.build_route_regexp(path)
- if compiled_route not in self.routes:
- self.routes[compiled_route] = handler
- else:
- raise DuplicateRoute
首先,這個(gè)函數(shù)使用 Router.build_router_regexp 的類方法,將一條路由規(guī)則(如 '/cars/{id}' 這樣的字符串),“編譯”到一個(gè)已編譯的正則表達(dá)式對(duì)象。這些已編譯的正則表達(dá)式用來(lái)匹配請(qǐng)求路徑,以及解析開(kāi)發(fā)者自定義的 URL 參數(shù)。如果已經(jīng)存在一個(gè)相同的路由,程序就會(huì)拋出一個(gè)異常。最后,這個(gè)路由/處理程序?qū)Ρ惶砑拥揭粋€(gè)簡(jiǎn)單的字典self.routes中。
下面展示 Router 是如何“編譯”路由的:
- @classmethod
- def build_route_regexp(cls, regexp_str):
- """
- Turns a string into a compiled regular expression. Parses '{}' into
- named groups ie. '/path/{variable}' is turned into
- '/path/(?P
[a-zA-Z0-9_-]+)'. - :param regexp_str: a string representing a URL path.
- :return: a compiled regular expression.
- """
- def named_groups(matchobj):
- return '(?P<{0}>[a-zA-Z0-9_-]+)'.format(matchobj.group(1))
- re_str = re.sub(r'{([a-zA-Z0-9_-]+)}', named_groups, regexp_str)
- re_str = ''.join(('^', re_str, '$',))
- return re.compile(re_str)
這個(gè)方法使用正則表達(dá)式將所有出現(xiàn)的 {variable} 替換為 (?P
完成了路由存儲(chǔ)僅成功了一半,下面是如何得到路由對(duì)應(yīng)的函數(shù):
- def get_handler(self, path):
- logger.debug('Getting handler for: {0}'.format(path))
- for route, handler in self.routes.items():
- path_params = self.class.match_path(route, path)
- if path_params is not None:
- logger.debug('Got handler for: {0}'.format(path))
- wrapped_handler = HandlerWrapper(handler, path_params)
- return wrapped_handler
- raise NotFoundException()
一旦 App 對(duì)象獲得一個(gè) Request 對(duì)象,也就獲得了 URL 的路徑部分(如 /users/15/edit)。然后,我們需要匹配函數(shù)來(lái)生成一個(gè)響應(yīng)或者 404 錯(cuò)誤。get_handler 函數(shù)將路徑作為參數(shù),循環(huán)遍歷路由,對(duì)每條路由調(diào)用 Router.match_path 類方法檢查是否有已編譯的正則對(duì)象與這個(gè)請(qǐng)求路徑匹配。如果存在,我們就調(diào)用 HandleWrapper 來(lái)包裝路由對(duì)應(yīng)的函數(shù)。path_params 字典包含了路徑變量(如 '/users/15/edit' 中的 '15'),若路由沒(méi)有指定變量,字典就為空。最后,我們將包裝好的函數(shù)返回給 App 對(duì)象。
如果遍歷了所有的路由都找不到與路徑匹配的,函數(shù)就會(huì)拋出 NotFoundException 異常。
這個(gè) Route.match 類方法挺簡(jiǎn)單:
- def match_path(cls, route, path):
- match = route.match(path)
- try:
- return match.groupdict()
- except AttributeError:
- return None
它使用正則對(duì)象的 match 方法來(lái)檢查路由是否與路徑匹配。若果不匹配,則返回 None 。
最后,我們有 HandleWraapper 類。它的唯一任務(wù)就是封裝一個(gè)異步函數(shù),存儲(chǔ) path_params 字典,并通過(guò) handle 方法對(duì)外提供一個(gè)統(tǒng)一的接口。
- class HandlerWrapper(object):
- def init(self, handler, path_params):
- self.handler = handler
- self.path_params = path_params
- self.request = None
- async def handle(self, request):
- return await self.handler(request, **self.path_params)
組合到一起
框架的最后部分就是用 App 類把所有的部分聯(lián)系起來(lái)。
App 類用于集中所有的配置細(xì)節(jié)。一個(gè) App 對(duì)象通過(guò)其 start_server 方法,使用一些配置數(shù)據(jù)創(chuàng)建一個(gè) HTTPServer 的實(shí)例,然后將它傳遞給 asyncio.start_server 函數(shù)。asyncio.start_server 函數(shù)會(huì)對(duì)每一個(gè) TCP 連接調(diào)用 HTTPServer 對(duì)象的 handle_connection 方法。
- def start_server(self):
- if not self._server:
- self.loop = asyncio.get_event_loop()
- self._server = HTTPServer(self.router, self.http_parser, self.loop)
- self._connection_handler = asyncio.start_server(
- self._server.handle_connection,
- host=self.host,
- port=self.port,
- reuse_address=True,
- reuse_port=True,
- loop=self.loop)
- logger.info('Starting server on {0}:{1}'.format(
- self.host, self.port))
- self.loop.run_until_complete(self._connection_handler)
- try:
- self.loop.run_forever()
- except KeyboardInterrupt:
- logger.info('Got signal, killing server')
- except DiyFrameworkException as e:
- logger.error('Critical framework failure:')
- logger.error(e.traceback)
- finally:
- self.loop.close()
- else:
- logger.info('Server already started - {0}'.format(self))
總結(jié)
如果你查看源碼,就會(huì)發(fā)現(xiàn)所有的代碼僅 320 余行(包括測(cè)試代碼的話共 540 余行)。這么少的代碼實(shí)現(xiàn)了這么多的功能,讓我有點(diǎn)驚訝。這個(gè)框架沒(méi)有提供模板、身份認(rèn)證以及數(shù)據(jù)庫(kù)訪問(wèn)等功能(這些內(nèi)容也很有趣哦)。這也讓我知道,像 Django 和 Tornado 這樣的框架是如何工作的,而且我能夠快速地調(diào)試它們了。
這也是我按照測(cè)試驅(qū)動(dòng)開(kāi)發(fā)完成的第一個(gè)項(xiàng)目,整個(gè)過(guò)程有趣而有意義。先編寫測(cè)試用例迫使我思考設(shè)計(jì)和架構(gòu),而不僅僅是把代碼放到一起,讓它們可以運(yùn)行。不要誤解我的意思,有很多時(shí)候,后者的方式更好。不過(guò)如果你想給確保這些不怎么維護(hù)的代碼在之后的幾周甚至幾個(gè)月依然工作,那么測(cè)試驅(qū)動(dòng)開(kāi)發(fā)正是你需要的。
我研究了下整潔架構(gòu)以及依賴注入模式,這些充分體現(xiàn)在 Router 類是如何作為一個(gè)更高層次的抽象的(實(shí)體?)。Router 類是比較接近核心的,像 http_parser 和 App 的內(nèi)容比較邊緣化,因?yàn)樗鼈冎皇峭瓿闪藰O小的字符串和字節(jié)流、或是中層 IO 的工作。測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(TDD)迫使我獨(dú)立思考每個(gè)小部分,這使我問(wèn)自己這樣的問(wèn)題:方法調(diào)用的組合是否易于理解?類名是否準(zhǔn)確地反映了我正在解決的問(wèn)題?我的代碼中是否很容易區(qū)分出不同的抽象層?
網(wǎng)頁(yè)名稱:從零構(gòu)建一個(gè)簡(jiǎn)單的Python框架
本文URL:http://www.dlmjj.cn/article/djigsie.html


咨詢
建站咨詢
