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è)最簡(jiǎn)單的 http 服務(wù)器,能處理 http GET 請(qǐng)求。
python 內(nèi)置的 http server 主要集中在兩個(gè)代碼文件上,分別是 socketserver.py
和 http/server.py
。socketserver.py
提供 socket 通信能力的 Server 封裝并預(yù)留了用戶自定義請(qǐng)求處理的接口;http/server.py
基于前者做進(jìn)一步封裝,用得比較多的是 HTTP 的封裝。
從開頭的例子出發(fā)閱讀代碼(python 3.10.1),大致梳理出以下代碼結(jié)構(gòu),圖畫得很隨意無(wú)規(guī)范可言,只是為了更具象化解釋。
問(wèn)題一:實(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í)沒(méi)什么內(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 = port
TCPServer
的源碼實(shí)現(xiàn)得益于父類的預(yù)留接口,只需要 TCP socket 走一遍 bind
、listen
、accept
、close
流程(子類 UDPServer
同理)。
重點(diǎn)關(guān)注 BaseServer
,這里是網(wǎng)絡(luò)請(qǐng)求處理核心流程的實(shí)現(xiàn),文章最開頭的例子中 serve_forever()
這個(gè)入口方法就是在此類被實(shí)現(xiàn),我在源碼上加了些簡(jiǎ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) # 注冊(cè)Server描述符并監(jiān)聽I(yíng)/O讀事件 while not self.__shutdown_request: ready = selector.select(poll_interval) # 超時(shí)時(shí)長(zhǎng)poll_interval避免長(zhǎng)時(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() # 請(qǐng)求過(guò)來(lái),I/O讀事件準(zhǔn)備好,開始處理請(qǐng)求 self.service_actions() finally: self.__shutdown_request = False self.__is_shut_down.set()
從 _handle_request_noblock()
中看到,一個(gè)網(wǎng)絡(luò)請(qǐng)求的處理流程無(wú)非就是 verify_request()
、process_request()
、shoutdown_request()
加上些許異常處理邏輯,比較簡(jiǎn)明。在 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ò)請(qǐng)求的處理流程 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ò)請(qǐng)求流程大致抽象為 verify_request()
、process_request()
、shoutdown_request()
。如果考慮支持用戶自定義請(qǐng)求處理,還需要預(yù)留接口提供擴(kuò)展性。當(dāng)然如何要支持處理 HTTP 協(xié)議,還需要具備解析 HTTP 報(bào)文的能力,下文繼續(xù)探討。
問(wèn)題二:python 內(nèi)置的 HTTP Server 是怎么實(shí)現(xiàn)的
前文介紹了內(nèi)置一個(gè)網(wǎng)絡(luò)請(qǐng)求的處理流程(等價(jià)于 HTTP Server 的運(yùn)行流程),一定程度上解釋了本節(jié)的問(wèn)題,但欠缺一點(diǎn)細(xì)節(jié),沒(méi)有體現(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__()
中將工作流程確定下來(lái),分別是 setup()
、handler()
、finish()
的先后調(diào)用順序。setup()
和 finish()
在子類 StreamRequestHandler
被實(shí)現(xiàn),最后在 BaseHTTPRequestHandler
類中實(shí)現(xiàn) HTTP 協(xié)議解析功能,以及用 HTTP method 來(lái)決定調(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í)長(zhǎng)、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ú)貼過(guò)來(lái),思路如下:
- 解析 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)比較簡(jiǎn)潔,功能相對(duì)簡(jiǎn)單。如果要自行從零實(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)爬取
隨著短視頻的大火,不僅可以給人們帶來(lái)娛樂(lè),還有熱點(diǎn)新聞時(shí)事以及各種知識(shí),刷短視頻也逐漸成為了日常生活的一部分。本文將通過(guò)Pyhton依托Selenium來(lái)爬取短視頻熱點(diǎn),需要的可以參考一下2022-04-04python判斷字符串編碼的簡(jiǎn)單實(shí)現(xiàn)方法(使用chardet)
這篇文章主要介紹了python判斷字符串編碼的簡(jiǎn)單實(shí)現(xiàn)方法,涉及chardet模塊的安裝與簡(jiǎn)單使用方法,需要的朋友可以參考下2016-07-07LyScript實(shí)現(xiàn)Hook改寫MessageBox的方法詳解
LyScript可實(shí)現(xiàn)自定義匯編指令的替換功能。用戶可自行編寫匯編指令,將程序中特定的通用函數(shù)進(jìn)行功能改寫與轉(zhuǎn)向操作,此功能原理是簡(jiǎn)單的Hook操作。本文將詳細(xì)介紹Hook改寫MessageBox的方法,感興趣的可以了解一下2022-09-09python爬蟲庫(kù)scrapy簡(jiǎn)單使用實(shí)例詳解
這篇文章主要介紹了python爬蟲庫(kù)scrapy簡(jiǎn)單使用實(shí)例詳解,需要的朋友可以參考下2020-02-02基于Python繪制一個(gè)會(huì)動(dòng)的3D立體粽子
下周就要到端午節(jié)了,所以本文小編就來(lái)和大家分享一個(gè)有趣的Python項(xiàng)目——繪制會(huì)動(dòng)的3D立體粽子,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2023-06-06Python使用sys.path查看當(dāng)前的模塊搜索路徑
sys.path 是 Python 中的一個(gè)列表,它用于存儲(chǔ)模塊搜索路徑,當(dāng)你使用 import 語(yǔ)句導(dǎo)入一個(gè)模塊時(shí),Python 會(huì)按照 sys.path 列表中的路徑順序來(lái)查找這個(gè)模塊,本文給大家介紹了Python使用sys.path查看當(dāng)前的模塊搜索路徑,需要的朋友可以參考下2025-02-02