python內(nèi)置HTTP Server如何實(shí)現(xiàn)及原理解析
應(yīng)用案例
from http.server import HTTPServer, BaseHTTPRequestHandler
IP = '127.0.0.1'
PORT = 8000
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
message = "Hello, World!"
self.wfile.write(bytes(message, "utf8"))
with HTTPServer((IP, PORT), Handler) as httpd:
print("serving at port", PORT)
httpd.serve_forever()以上是使用內(nèi)置模塊 http.server 實(shí)現(xiàn)的一個(gè)最簡單的 http 服務(wù)器,能處理 http GET 請求。
python 內(nèi)置的 http server 主要集中在兩個(gè)代碼文件上,分別是 socketserver.py 和 http/server.py。socketserver.py 提供 socket 通信能力的 Server 封裝并預(yù)留了用戶自定義請求處理的接口;http/server.py 基于前者做進(jìn)一步封裝,用得比較多的是 HTTP 的封裝。
從開頭的例子出發(fā)閱讀代碼(python 3.10.1),大致梳理出以下代碼結(jié)構(gòu),圖畫得很隨意無規(guī)范可言,只是為了更具象化解釋。
問題一:實(shí)現(xiàn)一個(gè) HTTP 服務(wù)器大致需要什么要素
先看圖 1,左邊 BaseServer 一列是類,從上往下是父類到子類;右邊 server_forever() 一列是方法,從上往下是逐步深入的調(diào)用鏈。
從父類到子類 主線流程
+----------------+ +------------------+
| | | |
| BaseServer +--------------------->| serve_forever() |
| | | |
+--------+-------+ +--------=+--------+
| |
| |
| |
V V
+----------------+ +----------------------------+
| | | |
| TCPServer | | _handle_request_noblock() |
| | | |
+--------+-------+ +-------------+--------------+
| |
+-----------+------------+ |
| | |
V V V
+----------------+ +----------------+ +------------------+
| | | | | |
| HTTPServer | | UDPServer | | process_request()|
| | | | | |
+----------------+ +----------------+ +---------+--------+
|
|
|
V
+------------------+
| |
| finish_request() |
| |
+------------------+
圖 1
例子中使用了 HTTPServer 這個(gè)類,字面意思,這個(gè)類就是一個(gè) HTTP 服務(wù)器,順著繼承鏈看到 HTTPServer 是 TCPServer 的子類,符合 HTTP 報(bào)文是基于 TCP 協(xié)議傳輸?shù)恼J(rèn)知,HTTPServer 類其實(shí)沒什么內(nèi)容,代碼如下:
class HTTPServer(socketserver.TCPServer):
allow_reuse_address = 1 # Seems to make sense in testing environment
def server_bind(self):
"""Override server_bind to store the server name."""
socketserver.TCPServer.server_bind(self)
host, port = self.server_address[:2]
self.server_name = socket.getfqdn(host)
self.server_port = portTCPServer 的源碼實(shí)現(xiàn)得益于父類的預(yù)留接口,只需要 TCP socket 走一遍 bind、listen、accept、close 流程(子類 UDPServer 同理)。
重點(diǎn)關(guān)注 BaseServer,這里是網(wǎng)絡(luò)請求處理核心流程的實(shí)現(xiàn),文章最開頭的例子中 serve_forever() 這個(gè)入口方法就是在此類被實(shí)現(xiàn),我在源碼上加了些簡單的注釋:
def serve_forever(self, poll_interval=0.5):
"""Handle one request at a time until shutdown.
Polls for shutdown every poll_interval seconds. Ignores
self.timeout. If you need to do periodic tasks, do them in
another thread.
"""
self.__is_shut_down.clear()
try:
# XXX: Consider using another file descriptor or connecting to the
# socket to wake this up instead of polling. Polling reduces our
# responsiveness to a shutdown request and wastes cpu at all other
# times. with _ServerSelector() as selector:
selector.register(self, selectors.EVENT_READ) # 注冊Server描述符并監(jiān)聽I/O讀事件
while not self.__shutdown_request:
ready = selector.select(poll_interval) # 超時(shí)時(shí)長poll_interval避免長時(shí)間阻塞,在while循環(huán)下實(shí)現(xiàn)輪詢
# bpo-35017: shutdown() called during select(), exit immediately.
if self.__shutdown_request:
break
if ready:
self._handle_request_noblock() # 請求過來,I/O讀事件準(zhǔn)備好,開始處理請求
self.service_actions()
finally:
self.__shutdown_request = False
self.__is_shut_down.set()從 _handle_request_noblock() 中看到,一個(gè)網(wǎng)絡(luò)請求的處理流程無非就是 verify_request()、process_request()、shoutdown_request() 加上些許異常處理邏輯,比較簡明。在 finish_request() 中出現(xiàn) RequestHandlerClass 的類對(duì)象創(chuàng)建,這里其實(shí)就是用戶自定義的 RequestHandler(在 BaseServer 的 __int__() 中被初始化)。源碼如下,較好理解:
def _handle_request_noblock(self):
"""Handle one request, without blocking.
I assume that selector.select() has returned that the socket is
readable before this function was called, so there should be no risk of
blocking in get_request().
"""
try:
request, client_address = self.get_request()
except OSError:
return
if self.verify_request(request, client_address): # 從這里開始就是網(wǎng)絡(luò)請求的處理流程
try:
self.process_request(request, client_address)
except Exception:
self.handle_error(request, client_address)
self.shutdown_request(request)
except:
self.shutdown_request(request)
raise
else:
self.shutdown_request(request)
def process_request(self, request, client_address):
"""Call finish_request.
Overridden by ForkingMixIn and ThreadingMixIn.
"""
self.finish_request(request, client_address)
self.shutdown_request(request)
def finish_request(self, request, client_address):
"""Finish one request by instantiating RequestHandlerClass."""
self.RequestHandlerClass(request, client_address, self)
def shutdown_request(self, request):
"""Called to shutdown and close an individual request."""
self.close_request(request)小結(jié):要實(shí)現(xiàn)一個(gè) HTTP 服務(wù)器,需要包含 TCP socket 實(shí)現(xiàn),網(wǎng)絡(luò)請求流程大致抽象為 verify_request()、process_request()、shoutdown_request() 。如果考慮支持用戶自定義請求處理,還需要預(yù)留接口提供擴(kuò)展性。當(dāng)然如何要支持處理 HTTP 協(xié)議,還需要具備解析 HTTP 報(bào)文的能力,下文繼續(xù)探討。
問題二:python 內(nèi)置的 HTTP Server 是怎么實(shí)現(xiàn)的
前文介紹了內(nèi)置一個(gè)網(wǎng)絡(luò)請求的處理流程(等價(jià)于 HTTP Server 的運(yùn)行流程),一定程度上解釋了本節(jié)的問題,但欠缺一點(diǎn)細(xì)節(jié),沒有體現(xiàn) HTTP 報(bào)文的解析邏輯在哪里實(shí)現(xiàn)。其實(shí)內(nèi)置的 HTTP Server 的把 HTTP 協(xié)議解析的工作解耦出去,單獨(dú)做成 BaseHTTPRequestHandler 類,這樣允許用戶自行實(shí)現(xiàn)任意應(yīng)用層的協(xié)議解析工作,參考下面圖 2:
+----------------------+ +----------------+
| | | |
| BaseRequestHandler +------->| __init__() |
| | | |
+-----------+----------+ +----------------+
|
|
| +----------------+
| | |
V +--->| setup() |
+----------------------+ | | |
| | | +----------------+
| StreamRequestHandler +---+
| | |
+-----------+----------+ | +----------------+
| | | |
| +----> finish() |
V | |
+------------------------+ +----------------------+ +----------------+
| | | |
|SimpleHTTPRequestHandler|<---+BaseHTTPRequestHandler|
| | | |
+------------------------+ +-----------+----------+
|
|
|
V
+------------------+
| |
| handler() |
| |
+---------+--------+ +----------------+
| | |
| +--->| parse_request()|
| | | |
V | +----------------+
+----------------------+ |
| | |
| handler_one_request()+---+
| | | +----------------+
+----------------------+ | | |
+--->| do_XXX() |
| |
+----------------+
圖 2
圖 2 中,但凡帶括號(hào)的都是方法,不帶括號(hào)的是類,從上往下也是父類到子類。本著代碼最大化復(fù)用的原則,父類 BaseRequestHandler 的 __init__() 中將工作流程確定下來,分別是 setup()、handler()、finish() 的先后調(diào)用順序。setup() 和 finish() 在子類 StreamRequestHandler 被實(shí)現(xiàn),最后在 BaseHTTPRequestHandler 類中實(shí)現(xiàn) HTTP 協(xié)議解析功能,以及用 HTTP method 來決定調(diào)用哪個(gè)用戶自定義的 do_XXX() 方法,如 do_GET()、do_POST() 等。代碼如下:
class BaseRequestHandler:
"""Base class for request handler classes.
......
"""
def __init__(self, request, client_address, server):
self.request = request
self.client_address = client_address
self.server = server
self.setup()
try:
self.handle()
finally:
self.finish()
def setup(self):
pass
def handle(self):
pass
def finish(self):
pass
class StreamRequestHandler(BaseRequestHandler):
"""Define self.rfile and self.wfile for stream sockets."""
# 省略代碼
def setup(self):
# 設(shè)置鏈接超時(shí)時(shí)長、nagle算法、讀寫緩沖區(qū)
self.connection = self.request
if self.timeout is not None:
self.connection.settimeout(self.timeout)
if self.disable_nagle_algorithm:
self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)
self.rfile = self.connection.makefile('rb', self.rbufsize)
if self.wbufsize == 0:
self.wfile = _SocketWriter(self.connection)
else:
self.wfile = self.connection.makefile('wb', self.wbufsize)
def finish(self):
if not self.wfile.closed:
try:
self.wfile.flush()
except socket.error:
# A final socket error may have occurred here, such as
# the local error ECONNABORTED.
pass
self.wfile.close()
self.rfile.close()HTTP 協(xié)議解析關(guān)注 parse_request() 方法,由于代碼較多不單獨(dú)貼過來,思路如下:
- 解析 HTTP 協(xié)議版本號(hào),確定版本解析是否支持(1.1 <= version < 2.0)
- 獲取 HTTP method
- 解析 HTTP header 解析完 HTTP 協(xié)議后,根據(jù)所獲取的 HTTP method,調(diào)用用戶自定義的對(duì)應(yīng)方法,至此結(jié)束。
總結(jié)
python 內(nèi)置的 HTTP Server 實(shí)現(xiàn)比較簡潔,功能相對(duì)簡單。如果要自行從零實(shí)現(xiàn)一個(gè) HTTP Server,設(shè)計(jì)上參考 python 的實(shí)現(xiàn),應(yīng)該具備以下要素:
- TCP socket 通信
- HTTP 協(xié)議的報(bào)文解析
- 用戶自定義的 RequestHandler 調(diào)用(設(shè)計(jì)上需要引入拓展)
相關(guān)文章
Python+Selenium實(shí)現(xiàn)短視頻熱點(diǎn)爬取
隨著短視頻的大火,不僅可以給人們帶來娛樂,還有熱點(diǎn)新聞時(shí)事以及各種知識(shí),刷短視頻也逐漸成為了日常生活的一部分。本文將通過Pyhton依托Selenium來爬取短視頻熱點(diǎn),需要的可以參考一下2022-04-04
python判斷字符串編碼的簡單實(shí)現(xiàn)方法(使用chardet)
這篇文章主要介紹了python判斷字符串編碼的簡單實(shí)現(xiàn)方法,涉及chardet模塊的安裝與簡單使用方法,需要的朋友可以參考下2016-07-07
LyScript實(shí)現(xiàn)Hook改寫MessageBox的方法詳解
LyScript可實(shí)現(xiàn)自定義匯編指令的替換功能。用戶可自行編寫匯編指令,將程序中特定的通用函數(shù)進(jìn)行功能改寫與轉(zhuǎn)向操作,此功能原理是簡單的Hook操作。本文將詳細(xì)介紹Hook改寫MessageBox的方法,感興趣的可以了解一下2022-09-09
基于Python繪制一個(gè)會(huì)動(dòng)的3D立體粽子
下周就要到端午節(jié)了,所以本文小編就來和大家分享一個(gè)有趣的Python項(xiàng)目——繪制會(huì)動(dòng)的3D立體粽子,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2023-06-06
Python使用sys.path查看當(dāng)前的模塊搜索路徑
sys.path 是 Python 中的一個(gè)列表,它用于存儲(chǔ)模塊搜索路徑,當(dāng)你使用 import 語句導(dǎo)入一個(gè)模塊時(shí),Python 會(huì)按照 sys.path 列表中的路徑順序來查找這個(gè)模塊,本文給大家介紹了Python使用sys.path查看當(dāng)前的模塊搜索路徑,需要的朋友可以參考下2025-02-02

