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

創(chuàng)新互聯(lián)是專業(yè)的黃埔網(wǎng)站建設(shè)公司,黃埔接單;提供成都網(wǎng)站建設(shè)、網(wǎng)站設(shè)計(jì),網(wǎng)頁(yè)設(shè)計(jì),網(wǎng)站設(shè)計(jì),建網(wǎng)站,PHP網(wǎng)站建設(shè)等專業(yè)做網(wǎng)站服務(wù);采用PHP框架,可快速的進(jìn)行黃埔網(wǎng)站開(kāi)發(fā)網(wǎng)頁(yè)制作和功能擴(kuò)展;專業(yè)做搜索引擎喜愛(ài)的網(wǎng)站,專業(yè)的做網(wǎng)站團(tuán)隊(duì),希望更多企業(yè)前來(lái)合作!
還有那些只會(huì)用 Web 框架的同學(xué),是否無(wú)數(shù)次打開(kāi)框架的源碼,想學(xué)習(xí)提高卻無(wú)從下手?
今天我們就抽絲剝繭、去繁存簡(jiǎn),用一個(gè)文件,實(shí)現(xiàn)一個(gè)迷你 Web 框架,從而把其核心技術(shù)部分清晰地講解清楚,配套的源碼均已開(kāi)源。
GitHub 地址:https://github.com/521xueweihan/OneFile
在線查看:https://hellogithub.com/onefile/
如果你覺(jué)得我做的這件事對(duì)你有幫助,就請(qǐng)給我一個(gè) Star,多多轉(zhuǎn)發(fā)讓更多人受益。
閑言少敘,下面就開(kāi)始我們今天的提高之旅。
一、介紹原理
說(shuō)到 Web 不得不提的就是網(wǎng)絡(luò)協(xié)議,如果我們從 OSI 七層網(wǎng)絡(luò)模型開(kāi)始,我敢斷定看完的絕對(duì)不超過(guò)三成!
所以今天我們就直接聊最上面的一層,也就是 Web 框架接觸最多的 HTTP 應(yīng)用層,至于 TCP/IP 部分會(huì)在聊 socket 的時(shí)候粗略帶過(guò)。期間我會(huì)刻意打碼非必要講解技術(shù)的細(xì)枝末節(jié),切斷遠(yuǎn)離本期主題的技術(shù)話題,一個(gè)文件只講一個(gè)技術(shù)點(diǎn)!絕不拖堂請(qǐng)大家放心閱讀。
首先讓我們先回憶下,平常瀏覽網(wǎng)站的流程。
如果我們把在網(wǎng)上沖浪,比做在一間教室聽(tīng)課,那么老師就是服務(wù)器(server),學(xué)生就是客戶端(client)。當(dāng)同學(xué)有問(wèn)題的時(shí)候會(huì)先舉手(請(qǐng)求建立 TCP),老師發(fā)現(xiàn)學(xué)生的提問(wèn)請(qǐng)求,同意學(xué)生回答問(wèn)題后,學(xué)生起立提出問(wèn)題(發(fā)送請(qǐng)求),如果老師承諾會(huì)給提問(wèn)的學(xué)生加課堂表現(xiàn)分,那么提問(wèn)的時(shí)候就需要有個(gè)高效的提問(wèn)方式(請(qǐng)求格式),即:
- 先報(bào)學(xué)號(hào)
- 再提問(wèn)題
師接收到學(xué)生的提問(wèn)后就可以立即回答問(wèn)題(返回響應(yīng))無(wú)需再問(wèn)學(xué)號(hào),回答格式(響應(yīng)格式)如下:
- 回答問(wèn)題
- 根據(jù)學(xué)號(hào)加分!
有了約定好的提問(wèn)格式(協(xié)議),就可以省去老師每次詢問(wèn)學(xué)生的學(xué)號(hào),即高效又嚴(yán)謹(jǐn)。最后,老師回答完問(wèn)題讓學(xué)生坐下(關(guān)閉連接)。
其實(shí),我們?cè)诰W(wǎng)絡(luò)上通信流程也大致如此:
只不過(guò)機(jī)器執(zhí)行起來(lái)更加嚴(yán)格,大家都是遵循某種協(xié)議來(lái)開(kāi)發(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ù)情況下要處理的事情,所以我們要寫(xiě)的 Web 框架也就是處理兩件事,即:
- 處理連接(socket)
- 處理請(qǐng)求(request)
一定要記住:連接和請(qǐng)求是兩個(gè)東西,建立起連接才能發(fā)送請(qǐng)求。
而想要建立連接發(fā)起通信,就需要通過(guò) socket 來(lái)實(shí)現(xiàn)(建立連接),socket 可以理解為兩個(gè)虛擬的本子(文件句柄),通信的雙方人手一個(gè),它既能讀也能寫(xiě),只要把傳輸?shù)膬?nèi)容寫(xiě)到本子上(處理請(qǐng)求),對(duì)方就可以看到了。
下面我把 Web 框架分為兩部分進(jìn)行講解,所有代碼將采用簡(jiǎn)單易懂的 Python3 進(jìn)行實(shí)現(xiàn)。
二、編寫(xiě) Web 框架
代碼+注釋一共 457 行,請(qǐng)放心絕對(duì)簡(jiǎn)單易懂。
2.1 處理連接(HTTPServer)
這里需要簡(jiǎn)單聊一下 socket 這個(gè)東西,在編程語(yǔ)言層面它就是一個(gè)類庫(kù),負(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)的原因。
最后,上面所說(shuō)的服務(wù)器其實(shí)就是一臺(tái)性能好一點(diǎn)、一直開(kāi)著的電腦,而客戶端就是瀏覽器、手機(jī)、電腦,它們都有 socket 這個(gè)東西(操作系統(tǒng)級(jí)別的一個(gè)進(jìn)程)。
如果上面這段話沒(méi)有看懂也不礙事,能看懂下面的圖就行,得搞明白 socket 處理連接的步驟和流程,才能編寫(xiě) Web 框架處理連接的部分。
下面分別展示基于 socket 編寫(xiě)的 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)聽(tīng)TCP,1代表:操作系統(tǒng)可以掛起(未處理請(qǐng)求時(shí)等待狀態(tài))的最大連接數(shù)量。該值至少為1
print('監(jiān)聽(tīng)端口:', PORT)
while 1:
conn, _ = s.accept() # 開(kāi)始被動(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ù)器接收回來(lái)的數(shù)據(jù)
運(yùn)行效果如下:
結(jié)合上面的代碼,可以更加容易理解 socket 建立通信的流程:
- socket:創(chuàng)建socket
- bind:綁定端口號(hào)
- listen:開(kāi)始監(jiān)聽(tīng)
- accept:接收請(qǐng)求
- recv:接收數(shù)據(jù)
- close:關(guān)閉連接
所以,Web 框架中處理連接的 HTTPServer 類要做的事情就呼之欲出了。即:一開(kāi)始在 __init__方法中創(chuàng)建 socket,接著綁定端口(server_bind)然后開(kāi)始監(jiān)聽(tīng)端口(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()
# 開(kāi)始監(jiān)聽(tīng)端口
self.server_activate()
通過(guò)傳入的 RequestHandlerClass 參數(shù)可以看出,處理請(qǐng)求與建立連接是分開(kāi)處理。
下面就要開(kāi)始啟動(dòng)服務(wù)接收請(qǐng)求了,也就是 HTTPServer 的啟動(dòng)方法 serve_forever,這里包含了接收請(qǐng)求、接收數(shù)據(jù)、開(kāi)始處理請(qǐng)求、結(jié)束請(qǐng)求的全過(guò)程。
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)求了,通過(guò) 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)兩端通信嗎?通過(guò)兩個(gè)可讀寫(xiě)的“虛擬本子”。
再加上還要保證通信的高效和嚴(yán)謹(jǐn),就需要有對(duì)應(yīng)的“通信格式”。
所以,處理請(qǐng)求只需要三步走:
setup:初始化兩個(gè)本子
- 讀請(qǐng)求的文件句柄(rfile)
- 寫(xiě)響應(yīng)的文件句柄(wfile)
handle:讀取并解析請(qǐng)求、處理請(qǐng)求、構(gòu)造響應(yīng)并寫(xiě)入
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 # 接收來(lái)的請(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) # 寫(xiě)響應(yīng)的本子
def handle(self):
# 根據(jù) HTTP 協(xié)議,解析請(qǐng)求
# 具體的處理邏輯,即業(yè)務(wù)邏輯
# 構(gòu)造響應(yīng)并寫(xiě)入本子
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ù)代碼(處理邏輯)分開(kāi)。
在解析 HTTP 之前,需要先看一個(gè)實(shí)際的 HTTP 請(qǐng)求,當(dāng)我打開(kāi) hellogithub.com 網(wǎng)站首頁(yè)的時(shí)候,瀏覽器發(fā)送的 HTTP 請(qǐng)求如下:
整理歸納可得 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):
# --- 開(kāi)始解析 --- #
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)求頭了,下面開(kāi)始解析
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ī)則不一樣這里不做展開(kāi)講解
self.headers = self.parse_headers() # 解析請(qǐng)求頭也是處理字符串,但更為復(fù)雜標(biāo)準(zhǔn)庫(kù)有工具函數(shù)這里略過(guò)
# --- 業(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')) # 寫(xiě)入響應(yīng) body,即:頁(yè)面內(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}
"""
# 寫(xiě)響應(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()
這里通過(guò)繼承 Web 框架的 HTTPRequestHandler 實(shí)現(xiàn)的子類 RequestHandler 重寫(xiě) do_get 方法,實(shí)現(xiàn)業(yè)務(wù)代碼和框架的分離。這樣保證了框架的靈活性和解耦。
接下來(lái)服務(wù)毫無(wú)意外地運(yùn)行起來(lái)了,效果如下:
本文中涉及 Web 框架的代碼,為方便閱讀都經(jīng)過(guò)了簡(jiǎn)化。如果想要獲取完整可運(yùn)行的代碼,可前往 GitHub 地址獲取:
https://github.com/521xueweihan/OneFile/blob/main/src/python/web-server.py
該框架并不包含 Web 框架應(yīng)有的豐富功能,旨在通過(guò)最簡(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í)路徑:
- Python3 的 HTTPServer、BaseHTTPRequestHandler
- bottle:?jiǎn)挝募?、無(wú)三方依賴、持續(xù)更新,可用于生產(chǎn)環(huán)境的開(kāi)源 Web 框架:
- werkzeug -> flask
- starlette -> uvicorn -> fastapi
有的時(shí)候閱讀框架源碼不是為了寫(xiě)一個(gè)新的框架,而是向前輩學(xué)習(xí)和靠攏。
最后
新的技術(shù)總是學(xué)不完的,掌握核心的技術(shù)原理,不僅可以在接受新的知識(shí)時(shí)快人一步,還可以在排查問(wèn)題時(shí)一針見(jiàn)血。
新聞標(biāo)題:用一個(gè)文件,實(shí)現(xiàn)迷你Web框架(建議收藏)
分享URL:http://www.dlmjj.cn/article/djoihsh.html


咨詢
建站咨詢
