<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          用一個(gè)文件,實(shí)現(xiàn)迷你 Web 框架(建議收藏)

          共 8136字,需瀏覽 17分鐘

           ·

          2022-04-01 10:27

          8f4a75fe3d4801998c2619866de58d97.webp

          當(dāng)下網(wǎng)絡(luò)就如同空氣一樣在我們的周圍,它以無數(shù)種方式改變著我們的生活,但要說網(wǎng)絡(luò)的核心技術(shù)變化甚微。

          隨著開源文化的蓬勃發(fā)展,誕生了諸多優(yōu)秀的開源 Web 框架,讓我們的開發(fā)變得輕松。但同時(shí)也讓我們不敢停下學(xué)習(xí)新框架的腳步,其實(shí)萬變不離其宗,只要理解了 Web 框架的核心技術(shù)部分,當(dāng)有一個(gè)新的框架出來的時(shí)候,基礎(chǔ)部分大同小異只需要重點(diǎn)了解:它有哪些特點(diǎn),用到了哪些技術(shù)解決了什么痛點(diǎn)?這樣接受和理解起新技術(shù)來會(huì)更加得心應(yīng)手,不至于疲于奔命。

          還有那些只會(huì)用 Web 框架的同學(xué),是否無數(shù)次打開框架的源碼,想學(xué)習(xí)提高卻無從下手?

          今天我們就抽絲剝繭、去繁存簡(jiǎn),用一個(gè)文件,實(shí)現(xiàn)一個(gè)迷你?Web 框架,從而把其核心技術(shù)部分清晰地講解清楚,配套的源碼均已開源。

          GitHub 地址:https://github.com/521xueweihan/OneFile


          在線查看:https://hellogithub.com/onefile/

          如果你覺得我做的這件事對(duì)你有幫助,就請(qǐng)給我一個(gè) ?Star,多多轉(zhuǎn)發(fā)讓更多人受益。

          閑言少敘,下面就開始我們今天的提高之旅。

          一、介紹原理

          說到 Web 不得不提的就是網(wǎng)絡(luò)協(xié)議,如果我們從 OSI 七層網(wǎng)絡(luò)模型開始,我敢斷定看完的絕對(duì)不超過三成!

          所以今天我們就直接聊最上面的一層,也就是 Web 框架接觸最多的 HTTP 應(yīng)用層,至于 TCP/IP 部分會(huì)在聊 socket 的時(shí)候粗略帶過。期間我會(huì)刻意打碼非必要講解技術(shù)的細(xì)枝末節(jié),切斷遠(yuǎn)離本期主題的技術(shù)話題,一個(gè)文件只講一個(gè)技術(shù)點(diǎn)!絕不拖堂請(qǐng)大家放心閱讀。

          首先讓我們先回憶下,平常瀏覽網(wǎng)站的流程。

          如果我們把在網(wǎng)上沖浪,比做在一間教室聽課,那么老師就是服務(wù)器(server),學(xué)生就是客戶端(client)。當(dāng)同學(xué)有問題的時(shí)候會(huì)先舉手(請(qǐng)求建立 TCP),老師發(fā)現(xiàn)學(xué)生的提問請(qǐng)求,同意學(xué)生回答問題后,學(xué)生起立提出問題(發(fā)送請(qǐng)求),如果老師承諾會(huì)給提問的學(xué)生加課堂表現(xiàn)分,那么提問的時(shí)候就需要有個(gè)高效的提問方式(請(qǐng)求格式),即:

          • 先報(bào)學(xué)號(hào)
          • 再提問題

          師接收到學(xué)生的提問后就可以立即回答問題(返回響應(yīng))無需再問學(xué)號(hào),回答格式(響應(yīng)格式)如下:

          • 回答問題
          • 根據(jù)學(xué)號(hào)加分!

          有了約定好的提問格式(協(xié)議),就可以省去老師每次詢問學(xué)生的學(xué)號(hào),即高效又嚴(yán)謹(jǐn)。最后,老師回答完問題讓學(xué)生坐下(關(guān)閉連接)。

          其實(shí),我們?cè)诰W(wǎng)絡(luò)上通信流程也大致如此:

          0bb3658709e593ecaa671e6d340e66de.webp

          只不過機(jī)器執(zhí)行起來更加嚴(yán)格,大家都是遵循某種協(xié)議來開發(fā)軟件,這樣就可以實(shí)現(xiàn)在某種協(xié)議下進(jìn)行通信,而這種網(wǎng)絡(luò)通信協(xié)議就叫做 HTTP(超文本傳輸協(xié)議)。

          而我們要做的 Web 框架就是處理上面的流程:建立連接、接收請(qǐng)求、解析請(qǐng)求、處理請(qǐng)求、返回請(qǐng)求。

          原理部分就聊這么多,目前你只需要記住網(wǎng)絡(luò)上通信分為兩大步:建立連接(用于通信)和處理請(qǐng)求。

          所謂框架就是處理大多數(shù)情況下要處理的事情,所以我們要寫的 Web 框架也就是處理兩件事,即:

          • 處理連接(socket)
          • 處理請(qǐng)求(request)

          一定要記住:連接和請(qǐng)求是兩個(gè)東西,建立起連接才能發(fā)送請(qǐng)求。

          而想要建立連接發(fā)起通信,就需要通過 socket 來實(shí)現(xiàn)(建立連接),socket 可以理解為兩個(gè)虛擬的本子(文件句柄),通信的雙方人手一個(gè),它既能讀也能寫,只要把傳輸?shù)膬?nèi)容寫到本子上(處理請(qǐng)求),對(duì)方就可以看到了。

          537aed31af3470e02a8a09dca20ed2f6.webp

          下面我把 Web 框架分為兩部分進(jìn)行講解,所有代碼將采用簡(jiǎn)單易懂的 Python3 進(jìn)行實(shí)現(xiàn)。

          二、編寫?Web 框架

          04ce9039c9452bf6f41509e3e62949b2.webp

          代碼+注釋一共 457 行,請(qǐng)放心絕對(duì)簡(jiǎn)單易懂。

          2.1 處理連接(HTTPServer)

          這里需要簡(jiǎn)單聊一下 socket 這個(gè)東西,在編程語言層面它就是一個(gè)類庫,負(fù)責(zé)搞定連接建立網(wǎng)絡(luò)通信。但本質(zhì)上是系統(tǒng)級(jí)別提供通信的進(jìn)程,而一臺(tái)電腦可以建立多條通信線路,所以每一個(gè)端口號(hào)后面都是一個(gè) socket 進(jìn)程,它們相互獨(dú)立、互不干涉,這也是為什么我們?cè)趩?dòng)服務(wù)的時(shí)候要指定端口號(hào)的原因。

          最后,上面所說的服務(wù)器其實(shí)就是一臺(tái)性能好一點(diǎn)、一直開著的電腦,而客戶端就是瀏覽器、手機(jī)、電腦,它們都有 socket 這個(gè)東西(操作系統(tǒng)級(jí)別的一個(gè)進(jìn)程)。

          如果上面這段話沒有看懂也不礙事,能看懂下面的圖就行,得搞明白 socket 處理連接的步驟和流程,才能編寫 Web 框架處理連接的部分。

          123b991d31d7910b6ba948c592b9f3ab.webp

          下面分別展示基于 socket 編寫的 server.py 和 client.py 代碼。

          #?coding:?utf-8
          #?服務(wù)器端代碼(server.py)
          import?socket

          print('我是服務(wù)端!')
          HOST?=?''
          PORT?=?50007
          s?=?socket.socket(socket.AF_INET,?socket.SOCK_STREAM)??#?創(chuàng)建?TCP?socket?對(duì)象
          s.setsockopt(socket.SOL_SOCKET,?socket.SO_REUSEADDR,?1)??#?重啟時(shí)釋放端口
          s.bind((HOST,?PORT))??#?綁定地址
          s.listen(1)??#?監(jiān)聽TCP,1代表:操作系統(tǒng)可以掛起(未處理請(qǐng)求時(shí)等待狀態(tài))的最大連接數(shù)量。該值至少為1
          print('監(jiān)聽端口:',?PORT)
          while?1:
          ????conn,?_?=?s.accept()??#?開始被動(dòng)接受TCP客戶端的連接。
          ????data?=?conn.recv(1024)??#?接收TCP數(shù)據(jù),1024表示緩沖區(qū)的大小
          ????print('接收到:',?repr(data))
          ????conn.sendall(b'Hi,?'+data)??#?給客戶端發(fā)送數(shù)據(jù)
          ????conn.close()

          因?yàn)?HTTP 是建立在相對(duì)可靠的 TCP 協(xié)議上,所以這里創(chuàng)建的是 TCP socket 對(duì)象。

          #?coding:?utf-8
          #?客戶端代碼(client.py)
          import?socket

          print('我是客戶端!')
          HOST?=?'localhost'????#?服務(wù)器的IP
          PORT?=?50007??????????????#?需要連接的服務(wù)器的端口
          s?=?socket.socket(socket.AF_INET,?socket.SOCK_STREAM)
          s.connect((HOST,?PORT))
          print("發(fā)送'HelloGitHub'")
          s.sendall(b'HelloGitHub')??#?發(fā)送‘HelloGitHub’給服務(wù)器
          data?=?s.recv(1024)
          s.close()
          print('接收到',?repr(data))??#?打印從服務(wù)器接收回來的數(shù)據(jù)

          運(yùn)行效果如下:

          7ed024af93e829b6e43f8b6a800a1c0f.webp

          結(jié)合上面的代碼,可以更加容易理解 socket 建立通信的流程:

          1. socket:創(chuàng)建socket
          2. bind:綁定端口號(hào)
          3. listen:開始監(jiān)聽
          4. accept:接收請(qǐng)求
          5. recv:接收數(shù)據(jù)
          6. close:關(guān)閉連接

          所以,Web 框架中處理連接的 HTTPServer 類要做的事情就呼之欲出了。即:一開始在 __init__方法中創(chuàng)建 socket,接著綁定端口(server_bind)然后開始監(jiān)聽端口(server_activate)

          #?處理連接進(jìn)行數(shù)據(jù)通信
          class?HTTPServer(object):
          ????def?__init__(self,?server_address,?RequestHandlerClass):
          ????????self.server_address?=?server_address?#?服務(wù)器地址
          ????????self.RequestHandlerClass?=?RequestHandlerClass?#?處理請(qǐng)求的類

          ????????#?創(chuàng)建?TCP?Socket
          ????????self.socket?=?socket.socket(socket.AF_INET,
          ????????????????????????????????????socket.SOCK_STREAM)
          ????????#?綁定?socket?和端口
          ????????self.server_bind()
          ????????#?開始監(jiān)聽端口
          ????????self.server_activate()

          通過傳入的 RequestHandlerClass 參數(shù)可以看出,處理請(qǐng)求與建立連接是分開處理。

          下面就要開始啟動(dòng)服務(wù)接收請(qǐng)求了,也就是 HTTPServer 的啟動(dòng)方法 serve_forever,這里包含了接收請(qǐng)求、接收數(shù)據(jù)、開始處理請(qǐng)求、結(jié)束請(qǐng)求的全過程。

          def?serve_forever(self):
          ????while?True:
          ????????ready?=?selector.select(poll_interval)
          ????????#?當(dāng)客戶端請(qǐng)求的數(shù)據(jù)到位,則執(zhí)行下一步
          ????????if?ready:
          ????????????#?有準(zhǔn)備好的可讀文件句柄,則與客戶端的鏈接建立完畢
          ????????????request,?client_address?=?self.socket.accept()
          ????????????#?可以進(jìn)行下面的處理請(qǐng)求了,通過?RequestHandlerClass?處理請(qǐng)求和連接獨(dú)立
          ????????????self.RequestHandlerClass(request,?client_address,?self)
          ????????????#?關(guān)閉連接
          ????????????self.socket.close()

          如此循環(huán)下去,就是 HTTPServer 處理連接、建立起 HTTP 連接的全部代碼,就這?對(duì)!是不是很簡(jiǎn)單?

          代碼中的 RequestHandlerClass 形參是處理請(qǐng)求的類,下面將深入講解其對(duì)應(yīng)的 HTTPRequestHandler 是如何處理 HTTP 請(qǐng)求。

          2.2 處理請(qǐng)求(HTTPRequestHandler)

          還記得上面介紹的 socket 如何實(shí)現(xiàn)兩端通信嗎?通過兩個(gè)可讀寫的“虛擬本子”。

          再加上還要保證通信的高效和嚴(yán)謹(jǐn),就需要有對(duì)應(yīng)的“通信格式”。

          所以,處理請(qǐng)求只需要三步走:

          1. setup:初始化兩個(gè)本子
          • 讀請(qǐng)求的文件句柄(rfile)
          • 寫響應(yīng)的文件句柄(wfile)
          handle:讀取并解析請(qǐng)求、處理請(qǐng)求、構(gòu)造響應(yīng)并寫入finish:返回響應(yīng),銷毀兩個(gè)本子釋放資源,然后塵歸塵土歸土,等待下個(gè)請(qǐng)求

          對(duì)應(yīng)的代碼:

          #?處理請(qǐng)求
          class?HTTPRequestHandler(object):
          ????def?__init__(self,?request,?client_address,?server):
          ????????self.request?=?request?#?接收來的請(qǐng)求(socket)
          ????????#?1、初始化兩個(gè)本子
          ????????self.setup()
          ????????try:
          ????????????#?2、讀取、解析、處理請(qǐng)求,構(gòu)造響應(yīng)
          ????????????self.handle()
          ????????finally:
          ????????????#?3、返回響應(yīng),釋放資源
          ????????????self.finish()
          ????
          ????def?setup(self):
          ????????self.rfile?=?self.request.makefile('rb',?-1)?#?讀請(qǐng)求的本子
          ????????self.wfile?=?self.request.makefile('wb',?0)?#?寫響應(yīng)的本子
          ????def?handle(self):
          ????????#?根據(jù)?HTTP?協(xié)議,解析請(qǐng)求
          ????????#?具體的處理邏輯,即業(yè)務(wù)邏輯
          ????????#?構(gòu)造響應(yīng)并寫入本子
          ????def?finish(self):
          ????????#?返回響應(yīng)
          ????????self.wfile.flush()
          ????????#?關(guān)閉請(qǐng)求和響應(yīng)的句柄,釋放資源
          ????????self.wfile.close()
          ????????self.rfile.close()

          以上就是處理請(qǐng)求的整體流程,下面將詳細(xì)介紹 handle 如何解析 HTTP 請(qǐng)求和構(gòu)造 HTTP 響應(yīng),以及如何實(shí)現(xiàn)把框架和具體的業(yè)務(wù)代碼(處理邏輯)分開。

          在解析 HTTP 之前,需要先看一個(gè)實(shí)際的 HTTP 請(qǐng)求,當(dāng)我打開 hellogithub.com 網(wǎng)站首頁的時(shí)候,瀏覽器發(fā)送的 HTTP 請(qǐng)求如下:

          77aebf92fb7e0383fca91a10fbf1f949.webp

          整理歸納可得 HTTP 請(qǐng)求格式,如下:

          {HTTP?method}?{PATH}?{HTTP?version}\r\n
          {header?field?name}:{field?value}\r\n
          ...
          \r\n
          {request?body}

          得到了請(qǐng)求格式,那么 handle 解析請(qǐng)求的方法也就有了。

          def?handle(self):
          ????#?---?開始解析?---?#
          ????self.raw_requestline?=?self.rfile.readline(65537)?#?讀取請(qǐng)求第一行數(shù)據(jù),即請(qǐng)求頭
          ????requestline?=?str(self.raw_requestline,?'iso-8859-1')?#?轉(zhuǎn)碼
          ????requestline?=?requestline.rstrip('\r\n')?#?去換行和空白行
          ????#?就可以得到?"GET?/?HTTP/1.1"?請(qǐng)求頭了,下面開始解析
          ????self.command,?self.path,?self.request_version?=?requestline.split()?
          ????#?根據(jù)空格分割字符串,可得到("GET",?"/",?"HTTP/1.1")
          ????#?command?對(duì)應(yīng)的是?HTTP?method,path?對(duì)應(yīng)的是請(qǐng)求路徑
          ????#?request_version?對(duì)應(yīng)?HTTP?版本,不同版本解析規(guī)則不一樣這里不做展開講解
          ????self.headers?=?self.parse_headers()?#?解析請(qǐng)求頭也是處理字符串,但更為復(fù)雜標(biāo)準(zhǔn)庫有工具函數(shù)這里略過
          ????#?---?業(yè)務(wù)邏輯?---?#
          ????#?do_HTTP_method?對(duì)應(yīng)到具體的處理函數(shù)
          ????mname?=?('do_'?+?self.command).lower()
          ????method?=?getattr(self,?mname)
          ????#?調(diào)用對(duì)應(yīng)的處理方法
          ????method()
          ????#?---?返回響應(yīng)?---?#
          ????self.wfile.flush()

          def?do_GET(self):
          ????#?根據(jù)?path?區(qū)別處理
          ????if?self.path?==?'/':
          ????????self.send_response(200)??#?status?code
          ????????#?加入響應(yīng)?header
          ????????self.send_header("Content-Type",?"text/html;?charset=utf-8")
          ????????self.send_header("Content-Length",?str(len(content)))
          ????????self.end_headers()?#?結(jié)束頭部分,即:'\r\n'
          ????????self.wfile.write(content.encode('utf-8'))?#?寫入響應(yīng) body,即:頁面內(nèi)容

          def?send_response(self,?code,?message=None):
          ????#?響應(yīng)體格式
          ????"""
          ????{HTTP?version}?{status?code}?{status?phrase}\r\n
          ????{header?field?name}:{field?value}\r\n
          ????...
          ????\r\n
          ????{response?body}
          ????"""

          ????#?寫響應(yīng)頭行
          ????self.wfile.write("%s?%d?%s\r\n"?%?("HTTP/1.1",?code,?message))
          ????#?加入響應(yīng)?header
          ????self.send_header('Server',?"HG/Python?")
          ????self.send_header('Date',?self.date_time_string())

          以上就是 handle 處理請(qǐng)求和返回響應(yīng)的核心代碼片段了,至此 HTTPRequestHandler 全部?jī)?nèi)容均已講解完畢,下面將演示運(yùn)行效果。

          2.3 運(yùn)行

          class?RequestHandler(HTTPRequestHandler):
          ????#?處理?GET?請(qǐng)求
          ????def?do_get(self):
          ????????#?根據(jù)?path?對(duì)應(yīng)到具體的處理方法
          ????????if?self.path?==?'/':
          ????????????self.handle_index()
          ????????elif?self.path.startswith('/favicon'):
          ????????????self.handle_favicon()
          ????????else:
          ????????????self.send_error(404)

          if?__name__?==?'__main__':
          ????server?=?HTTPServer(('',?8080),?RequestHandler)
          ????#?啟動(dòng)服務(wù)
          ????server.serve_forever()

          這里通過繼承 Web 框架的 HTTPRequestHandler 實(shí)現(xiàn)的子類 RequestHandler 重寫 do_get 方法,實(shí)現(xiàn)業(yè)務(wù)代碼和框架的分離。這樣保證了框架的靈活性和解耦。

          接下來服務(wù)毫無意外地運(yùn)行起來了,效果如下:

          3352d92053e0dfaa7c2abcd4eebf8e08.webp

          本文中涉及 Web 框架的代碼,為方便閱讀都經(jīng)過了簡(jiǎn)化。如果想要獲取完整可運(yùn)行的代碼,可前往 GitHub 地址獲?。?/p>

          https://github.com/521xueweihan/OneFile/blob/main/src/python/web-server.py

          該框并不包含 Web 框架應(yīng)有的豐富功能,旨在通過最簡(jiǎn)單的代碼,實(shí)現(xiàn)一個(gè)迷你 Web 框架,讓不了解基本 Web 框架結(jié)構(gòu)的同學(xué),得以一探究竟。

          如果本文的內(nèi)容勾起了你對(duì) Web 框架的興趣,你還想更加深入的了解更加全面、適用于生產(chǎn)環(huán)境、代碼和結(jié)構(gòu)同樣的簡(jiǎn)潔的 Web 框架。我建議的學(xué)習(xí)路徑:

          1. Python3 的 HTTPServer、BaseHTTPRequestHandler
          2. bottle:?jiǎn)挝募?、無三方依賴、持續(xù)更新,可用于生產(chǎn)環(huán)境的開源 Web 框架:
          • 地址:https://github.com/bottlepy/bottle
          werkzeug -> flaskstarlette -> uvicorn -> fastapi

          有的時(shí)候閱讀框架源碼不是為了寫一個(gè)新的框架,而是向前輩學(xué)習(xí)和靠攏。

          最后

          新的技術(shù)總是學(xué)不完的,掌握核心的技術(shù)原理,不僅可以在接受新的知識(shí)時(shí)快人一步,還可以在排查問題時(shí)一針見血。

          不知道這種一個(gè)文件講解一個(gè)技術(shù)點(diǎn),力求通過簡(jiǎn)單的文字和精簡(jiǎn)的代碼描述原理,期間抹去了細(xì)枝末節(jié)的技術(shù)專注于一門技術(shù),最后給出完整可運(yùn)行的開源代碼的文章,是否符合你的胃口?

          本文是我對(duì)新的系列一種嘗試,接受任何指點(diǎn)和批評(píng)。

          如果你喜歡此類文章,就請(qǐng)點(diǎn)贊給我一點(diǎn)鼓勵(lì),還可以留言提建議或者“點(diǎn)餐”。

          不要想你為開源做了什么,你只需要清楚你為自己做了什么。

          OneFile 期待你的加入,輕點(diǎn){閱讀原文}貢獻(xiàn)一份力量。

          - END -

          ?? 關(guān)注「HelloGitHub」第一時(shí)間收到更新??

          瀏覽 37
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  aⅴ亚洲高清 | 91干网站| 一级黄色网络免费看 | 91 黄网站在线观看 | 污污在线无码 |