gitbook鏈接:用python帶你進(jìn)入AI中的深度學(xué)習(xí)技術(shù)領(lǐng)域https://www.gitbook.com/book/scrappyzhang/python_to_deeplearn/details
github鏈接:https://github.com/ScrappyZhang/python_web_Crawler_DA_ML_DL
9 基于socket的靜態(tài)web服務(wù)器
前面8章我們學(xué)習(xí)了網(wǎng)絡(luò)編程最基礎(chǔ)的概念和在python中的使用方法碌燕。為了更充分的體現(xiàn)這些基礎(chǔ)概念的重要性罚舱,本節(jié)綜合所有基礎(chǔ)知識(shí)帶大家一同使用學(xué)習(xí)到的知識(shí)來(lái)實(shí)踐編寫一個(gè)靜態(tài)web服務(wù)器——python web server,簡(jiǎn)稱PWB监氢。
9.1 顯示固定頁(yè)面的web服務(wù)器
這一節(jié)為了更好的理解靜態(tài)web服務(wù)器的搭建過(guò)程,我們先來(lái)搭建一個(gè)只能顯示固定頁(yè)面內(nèi)容的web服務(wù)器衅码。如何實(shí)現(xiàn)呢拯刁?通過(guò)面向?qū)ο蟮乃季S,我們可以先創(chuàng)建一個(gè)web服務(wù)器類HTTPServer肆良,然后實(shí)例化調(diào)用運(yùn)行即可筛璧。一般情況下web服務(wù)器都是一直在運(yùn)行并處理客戶端請(qǐng)求逸绎,因此我們的這個(gè)web服務(wù)器類除了初始化__init__方法外惹恃,還需要一個(gè)永久運(yùn)行監(jiān)聽(tīng)客戶端(瀏覽器)連接的方法serve_forever和一個(gè)連接成功后向客戶端(瀏覽器)發(fā)送響應(yīng)數(shù)據(jù)的方法handlerequest。代碼框架如下:
import socket
SERVER_ADDR = (HOST, PORT) = '', 8888 # 服務(wù)器地址
VERSION = 1.0 # web服務(wù)器版本號(hào)
class HTTPServer():
def __init__(self, server_address):
"""初始化服務(wù)器TCP套接字"""
pass
def serve_forever(self):
"""永久運(yùn)行監(jiān)聽(tīng)接收連接"""
pass
def handlerequest(self, client_socket):
"""客戶端請(qǐng)求處理棺牧,發(fā)送響應(yīng)數(shù)據(jù)"""
pass
def run():
"""運(yùn)行服務(wù)器"""
pwb = HTTPServer(SERVER_ADDR)
print('web server:PWB %s on port %d...\n' % (VERSION, PORT))
pwb.serve_forever()
if __name__ == '__main__':
run()
運(yùn)行一下巫糙,可以看到終端輸出一下信息提示web服務(wù)器在8888端口運(yùn)行,由于它并沒(méi)有任何功能颊乘,所以僅僅是提示信息参淹。
web server:PWB 1.0 on port 8888...
接下來(lái)我們需要做的就是將代碼框架中的空缺部分一一填滿。
根據(jù)TCP服務(wù)器的搭建過(guò)程乏悄,我們知道浙值,HTTPServer類的初始化方法中應(yīng)當(dāng)實(shí)現(xiàn)這些功能:TCP服務(wù)套接字的創(chuàng)建、地址綁定并啟動(dòng)監(jiān)聽(tīng)檩小。
def __init__(self, server_address):
"""初始化服務(wù)器TCP套接字"""
self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.tcp_socket.bind(server_address)
self.tcp_socket.listen(128)
永久運(yùn)行監(jiān)聽(tīng)接收連接的方法serve_forevet主要功能是實(shí)現(xiàn)連接上客戶端开呐,并調(diào)用客戶端請(qǐng)求處理方法self.handlerequest。
def serve_forever(self):
"""永久運(yùn)行監(jiān)聽(tīng)接收連接"""
while True:
client_socket, client_address = self.tcp_socket.accept()
print(client_address, '向服務(wù)器發(fā)起了請(qǐng)求')
self.handlerequest(client_socket)
客戶端請(qǐng)求處理方法handlerequest則需要獲取客戶端(瀏覽器)發(fā)送來(lái)的請(qǐng)求數(shù)據(jù),并經(jīng)過(guò)解析后向?yàn)g覽器發(fā)送一組包括響應(yīng)頭和響應(yīng)體的數(shù)據(jù)筐付。這里由于我們是返回一個(gè)固定的內(nèi)容hello PWB卵惦,因此我們只要接收到了瀏覽器的請(qǐng)求,便可以發(fā)送固定的響應(yīng)成功頭和固定的響應(yīng)體hello PWB給瀏覽器瓦戚。
def handlerequest(self, client_socket):
"""客戶端請(qǐng)求處理沮尿,發(fā)送響應(yīng)數(shù)據(jù)"""
# 收取瀏覽器請(qǐng)求信息,并在服務(wù)器端打印顯示
request_data = client_socket.recv(2048).decode('utf-8')
request_header_lines = request_data.splitlines()
for line in request_header_lines:
print(line)
# 響應(yīng)信息:本例顯示固定內(nèi)容:響應(yīng)成功 + hello PWB
resp_headers = "HTTP/1.1 200 OK\r\n" # 200代表響應(yīng)成功并找到資源
resp_headers += "Server: PWB" + str(VERSION) + '\r\n' # 告訴瀏覽器服務(wù)器
resp_headers += '\r\n' # 空行隔開(kāi)body
resp_body = 'hello PWB\r\n' # 固定的顯示內(nèi)容
resp_data = resp_headers + resp_body
# 發(fā)送相應(yīng)數(shù)據(jù)至瀏覽器
client_socket.send(resp_data.encode('utf-8'))
client_socket.close() # HTTP短連接较解,請(qǐng)求完即關(guān)閉TCP連接
這樣子畜疾,我們的代碼框架就根據(jù)需求功能填充完畢了。
完整代碼如下:
'''net09_web_PWB1.py'''
import socket
SERVER_ADDR = (HOST, PORT) = '', 8888 # 服務(wù)器地址
VERSION = 1.0 # web服務(wù)器版本號(hào)
class HTTPServer():
def __init__(self, server_address):
"""初始化服務(wù)器TCP套接字"""
self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.tcp_socket.bind(server_address)
self.tcp_socket.listen(128)
def serve_forever(self):
"""永久運(yùn)行監(jiān)聽(tīng)接收連接"""
while True:
client_socket, client_address = self.tcp_socket.accept()
print(client_address, '向服務(wù)器發(fā)起了請(qǐng)求')
self.handlerequest(client_socket)
def handlerequest(self, client_socket):
"""客戶端請(qǐng)求處理印衔,發(fā)送響應(yīng)數(shù)據(jù)"""
# 收取瀏覽器請(qǐng)求信息庸疾,并在服務(wù)器端打印顯示
request_data = client_socket.recv(2048).decode('utf-8')
request_header_lines = request_data.splitlines()
for line in request_header_lines:
print(line)
# 響應(yīng)信息:本例顯示固定內(nèi)容:響應(yīng)成功 + hello PWB
resp_headers = "HTTP/1.1 200 OK\r\n" # 200代表響應(yīng)成功并找到資源
resp_headers += "Server: PWB" + str(VERSION) + '\r\n' # 告訴瀏覽器服務(wù)器
resp_headers += '\r\n' # 空行隔開(kāi)body
resp_body = 'hello PWB' # 固定的顯示內(nèi)容
resp_data = resp_headers + resp_body
# 發(fā)送相應(yīng)數(shù)據(jù)至瀏覽器
client_socket.send(resp_data.encode('utf-8'))
client_socket.close() # HTTP短連接,請(qǐng)求完即關(guān)閉TCP連接
def run():
"""運(yùn)行服務(wù)器"""
pwb = HTTPServer(SERVER_ADDR)
print('web server:PWB %s on port %d...\n' % (VERSION, PORT))
pwb.serve_forever()
if __name__ == '__main__':
run()
運(yùn)行服務(wù)器程序当编,并在瀏覽器中輸入localhost:8888來(lái)訪問(wèn)web服務(wù)器届慈,我們可以看到顯示的內(nèi)容為hello PWB。打開(kāi)檢查忿偷,我們可以對(duì)比查看瀏覽器中顯示的各項(xiàng)請(qǐng)求響應(yīng)信息和我們web服務(wù)器打印顯示的請(qǐng)求信息金顿,完全一致。
由于我們的請(qǐng)求處理中無(wú)論接收到什么信息鲤桥,都是向?yàn)g覽器發(fā)送hello PWB揍拆,因此當(dāng)我們輸入localhost:8888/11時(shí)也是顯示hello PWB。這就是固定內(nèi)容顯示的web服務(wù)器茶凳。
細(xì)心的讀者可能會(huì)發(fā)現(xiàn)嫂拴,瀏覽器還請(qǐng)求了一個(gè)名叫favicon.ico的文件。這個(gè)文件一般為網(wǎng)站標(biāo)識(shí)圖片贮喧,即網(wǎng)頁(yè)標(biāo)簽頁(yè)頂部顯示的圖片筒狠,如下圖所示。
9.2 顯示指定需要內(nèi)容的web服務(wù)器
一個(gè)WEB服務(wù)器不可能只顯示一個(gè)頁(yè)面箱沦,肯定有特定需求的特定頁(yè)面顯示辩恼,怎么實(shí)現(xiàn)呢?谓形。讀者在上一節(jié)很清楚的看到GET 后的信息類似我們常見(jiàn)的資源文件夾那樣的路徑一樣灶伊。是的,這就是靜態(tài)資源的路徑寒跳。我們可以通過(guò)判斷這里來(lái)識(shí)別瀏覽器請(qǐng)求的是哪個(gè)靜態(tài)資源聘萨。那我們就需要在HTTPServer類中的handlerequest方法修改代碼來(lái)解析瀏覽器請(qǐng)求頭并返回不同的響應(yīng)體。
注:在講解演示之前童太,我們?cè)诖a同文件夾目錄下創(chuàng)建了一個(gè)static文件夾米辐,里面有baidu.html碾牌、oschina.html和gitee.html三個(gè)html文件。它們分別是百度儡循、開(kāi)源中國(guó)和碼云三個(gè)網(wǎng)站的首頁(yè)源碼舶吗。讀者可以通過(guò)我們之前的并發(fā)下載網(wǎng)頁(yè)的源碼經(jīng)過(guò)適當(dāng)調(diào)整來(lái)下載生成這三個(gè)html文件;也可以直接在瀏覽器中分別打開(kāi)相應(yīng)的主頁(yè)择膝,將網(wǎng)頁(yè)源碼拷貝在本地建立三個(gè)html文件誓琼。本節(jié)將講解根據(jù)瀏覽器輸入U(xiǎn)RL的不同來(lái)請(qǐng)求獲取這三個(gè)html,就像訪問(wèn)真正的三個(gè)網(wǎng)站一樣肴捉。
首先我們需要解析請(qǐng)求信息腹侣。請(qǐng)求信息的第一行g(shù)et內(nèi)容為請(qǐng)求文件的路徑,通過(guò)正則表達(dá)式便可以將其抓取出來(lái)齿穗。代碼如下:
pattern = r'[^/]+(/[^ ]*)'
request_html_name = re.match(pattern, request_header_lines[0]).group(1)
一般web服務(wù)器都會(huì)在靜態(tài)資源URL的基礎(chǔ)之上來(lái)添加一些路徑來(lái)實(shí)現(xiàn)避免被用戶訪問(wèn)到不該訪問(wèn)的內(nèi)容傲隶。后面我們會(huì)看到本例中URL為localhost:8888/baidu.html,實(shí)際上調(diào)用的是./static/baidu.html文件窃页。假如我們作為web服務(wù)器放一個(gè)非常機(jī)密的文件password.txt跺株,這種常見(jiàn)的文件名稱和類型,很容易會(huì)被黑客等直接通過(guò)localhost:8888/password.txt獲取到并造成數(shù)據(jù)泄露脖卖。所以一般web服務(wù)器內(nèi)部都會(huì)對(duì)靜態(tài)資源路徑有一套編碼解碼機(jī)制來(lái)加密乒省。這里我們假設(shè)我們的默認(rèn)頁(yè)面就是baidu.html。即localhost:8888畦木、localhost:8888/袖扛、localhost:8888/baidu.html顯示的內(nèi)容一致。
if request_html_name == '/':
request_html_name = STATIC_PATH + 'baidu.html'
else:
request_html_name = STATIC_PATH + request_html_name
至此十籍,我們就已經(jīng)獲悉了用戶請(qǐng)求的具體文件情況蛆封,我們只需根據(jù)相應(yīng)文件名來(lái)讀取相應(yīng)文件并設(shè)置為響應(yīng)體發(fā)送給瀏覽器即可。
html_file = open(request_html_name, 'rb')
resp_body = html_file.read() # 顯示內(nèi)容為讀取的文件內(nèi)容
html_file.close()
完整的代碼如下:
'''net09_web_PWB2.py'''
import socket
import re
SERVER_ADDR = (HOST, PORT) = '', 8888 # 服務(wù)器地址
VERSION = 2.0 # web服務(wù)器版本號(hào)
STATIC_PATH = './static/'
class HTTPServer():
def __init__(self, server_address):
"""初始化服務(wù)器TCP套接字"""
self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.tcp_socket.bind(server_address)
self.tcp_socket.listen(128)
def serve_forever(self):
"""永久運(yùn)行監(jiān)聽(tīng)接收連接"""
while True:
client_socket, client_address = self.tcp_socket.accept()
print(client_address, '向服務(wù)器發(fā)起了請(qǐng)求')
self.handlerequest(client_socket)
def handlerequest(self, client_socket):
"""客戶端請(qǐng)求處理勾栗,發(fā)送響應(yīng)數(shù)據(jù)"""
# 收取瀏覽器請(qǐng)求頭惨篱,并在服務(wù)器端打印顯示
request_data = client_socket.recv(2048).decode('utf-8')
request_header_lines = request_data.splitlines()
# print(request_header_lines[0]) # 第一行為請(qǐng)求頭信息
# 解析請(qǐng)求頭,獲取具體請(qǐng)求信息
pattern = r'[^/]+(/[^ ]*)'
request_html_name = re.match(pattern, request_header_lines[0]).group(1)
# 根據(jù)解析到的內(nèi)容補(bǔ)全將要讀取文件的路徑
if request_html_name == '/':
request_html_name = STATIC_PATH + 'baidu.html'
else:
request_html_name = STATIC_PATH + request_html_name
# 根據(jù)文件情況來(lái)返回相應(yīng)的信息
try:
html_file = open(request_html_name, 'rb')
except FileNotFoundError:
# 文件不存在械姻,則返回文件不存在妒蛇,并返回狀態(tài)碼404
resp_headers = 'HTTP/1.1 404 not found\r\n'
resp_headers += "Server: PWB" + str(VERSION) + '\r\n'
resp_headers += '\r\n'
resp_body = '==== 404 file not found===='.encode('utf-8')
else:
# 文件存在,則讀取文件內(nèi)容楷拳,并返回狀態(tài)碼200
resp_headers = "HTTP/1.1 200 OK\r\n" # 200代表響應(yīng)成功并找到資源
resp_headers += "Server: PWB" + str(VERSION) + '\r\n' # 告訴瀏覽器服務(wù)器
resp_headers += '\r\n' # 空行隔開(kāi)body
resp_body = html_file.read() # 顯示內(nèi)容為讀取的文件內(nèi)容
html_file.close()
finally:
resp_data = resp_headers.encode('utf-8') + resp_body # 結(jié)合響應(yīng)頭和響應(yīng)體
# 發(fā)送相應(yīng)數(shù)據(jù)至瀏覽器
client_socket.send(resp_data)
client_socket.close() # HTTP短連接,請(qǐng)求完即關(guān)閉TCP連接
def run():
"""運(yùn)行服務(wù)器"""
pwb = HTTPServer(SERVER_ADDR)
print('web server:PWB %s on port %d...\n' % (VERSION, PORT))
pwb.serve_forever()
if __name__ == '__main__':
run()
可以看到吏奸,當(dāng)輸入localhost:8888時(shí)欢揖,返回的是我們本地的baidu.html 。同樣可以在瀏覽器里看到我們?cè)O(shè)置的web服務(wù)器版本PWB2.0了奋蔚。
當(dāng)輸入localhost:8888/gitee.html時(shí)她混,返回的是我們本地的gitee.html烈钞。
當(dāng)輸入localhost:8888/22時(shí),返回的是404錯(cuò)誤信息坤按。
9.3 多線程毯欣、多進(jìn)程、協(xié)程實(shí)現(xiàn)web服務(wù)器
和第7章的斗魚(yú)照片爬取類似臭脓,我們只需要修改TCP連接客戶服務(wù)套接字的啟動(dòng)方式就可以實(shí)現(xiàn)9.2節(jié)的內(nèi)容了酗钞,也就是修改HTTPServer類的serve_forver方法。限于篇幅来累,本節(jié)只列舉被修改的部分砚作,完整源代碼請(qǐng)參考net09_web_PWB3.py、net09_web_PWB4.py和net09_web_PWB5.py
9.3.1 多線程web服務(wù)器
def serve_forever(self):
"""永久運(yùn)行監(jiān)聽(tīng)接收連接"""
while True:
client_socket, client_address = self.tcp_socket.accept()
print(client_address, '向服務(wù)器發(fā)起了請(qǐng)求')
td1 = threading.Thread(target=self.handlerequest, args=(client_socket,))
td1.start()
9.3.2 多進(jìn)程web服務(wù)器
def serve_forever(self):
"""永久運(yùn)行監(jiān)聽(tīng)接收連接"""
while True:
client_socket, client_address = self.tcp_socket.accept()
print(client_address, '向服務(wù)器發(fā)起了請(qǐng)求')
processes = multiprocessing.Process(target=self.handlerequest, args=(client_socket,))
processes.start()
client_socket.close()
9.3.3 協(xié)程web服務(wù)器
def serve_forever(self):
"""永久運(yùn)行監(jiān)聽(tīng)接收連接"""
while True:
client_socket, client_address = self.tcp_socket.accept()
print(client_address, '向服務(wù)器發(fā)起了請(qǐng)求')
gevent.spawn(self.handlerequest, client_socket)
9.4 拓展epoll非阻塞IO的web服務(wù)器
在9.3節(jié)嘹锁,我們?cè)趕erve_forever方法中通過(guò)while True來(lái)實(shí)現(xiàn)死循環(huán)運(yùn)行實(shí)現(xiàn)并發(fā)web服務(wù)器葫录,不會(huì)出錯(cuò)是因?yàn)槲覀兊姆?wù)端套接字默認(rèn)是阻塞型的,即無(wú)連接時(shí)领猾,會(huì)停在accept()那里米同。這樣的工作模式在海量連接工作時(shí)效率很低。
要向工作效率高摔竿,就可以I/O多路復(fù)用窍霞。多路IO好處就在于單個(gè)process就可以同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的IO。對(duì)于多路IO模型在很多操作系統(tǒng)上都有一些實(shí)現(xiàn)拯坟,比如多數(shù)BSD平臺(tái)都支持的kqueue()但金,或者Windows支持的IOCP,而Linux 平臺(tái)(內(nèi)核版本2.5+)上就有一個(gè)著名的模型epoll郁季。在epoll模型中 由操作系統(tǒng)負(fù)責(zé)監(jiān)聽(tīng)的所有socket冷溃,當(dāng)某個(gè)socket有數(shù)據(jù)到達(dá)了,操作系統(tǒng)就通知用戶進(jìn)程梦裂。這樣比 在用戶層面實(shí)現(xiàn)的 for 似枕、while不斷死循環(huán)去檢測(cè)每個(gè)socket的通信狀態(tài) 的代碼高效太多 ,也稱輪詢年柠。如果用戶層面的輪詢 凿歼, 某短時(shí)間段內(nèi)沒(méi)有用戶訪問(wèn),則CPU執(zhí)行流程屬于無(wú)用功(while死循環(huán)中) 產(chǎn)出效率低 浪費(fèi)資源冗恨。并且執(zhí)行效率和輪詢的socket數(shù)量息息相關(guān)答憔, 一般在1024-2048左右。隨著需要輪詢的數(shù)量增加掀抹,輪詢效率越低虐拓。
I/O 多路復(fù)用的特點(diǎn):
通過(guò)一種機(jī)制使一個(gè)進(jìn)程能同時(shí)等待多個(gè)文件描述符,而這些文件描述符(套接字描述符)其中的任意一個(gè)進(jìn)入讀就緒狀態(tài)傲武,epoll()函數(shù)就可以返回蓉驹。所以, I/O多路復(fù)用城榛,本質(zhì)上在 任何時(shí)候 監(jiān)視任務(wù)都是單進(jìn)程(單線程)模式 進(jìn)行工作,它之所以能提高效率是因?yàn)閟elect / epoll 把進(jìn)來(lái)的socket放到他們的 '監(jiān)視' 列表里面态兴,當(dāng)任何socket有可讀可寫數(shù)據(jù)立馬處理狠持,那如果select\epoll 手里同時(shí)檢測(cè)著很多socket, 一有動(dòng)靜馬上返回給進(jìn)程處理瞻润,比一個(gè)一個(gè)socket過(guò)來(lái),恢復(fù)阻塞等待前的情況開(kāi)始執(zhí)行處理請(qǐng)求 效率高喘垂。
要I/O多路復(fù)用就需要把服務(wù)套接字設(shè)置為非阻塞,但是會(huì)出錯(cuò)敢订。怎么辦呢王污?
self.tcp_socket.setblocking(False) # 將套接字設(shè)置為非阻塞模式
epoll是Linux內(nèi)核為處理大批量文件描述符而作了改進(jìn)的poll,是Linux下多路復(fù)用IO接口select/poll的增強(qiáng)版本楚午,它能顯著提高程序在大量并發(fā)連接中只有少量活躍的情況下的系統(tǒng)CPU利用率昭齐。另一點(diǎn)原因就是獲取事件的時(shí)候,它無(wú)須遍歷整個(gè)被偵聽(tīng)的描述符集矾柜,只要遍歷那些被內(nèi)核IO事件異步喚醒而加入Ready隊(duì)列的描述符集合就行了阱驾。
python中有個(gè)select模塊可以實(shí)現(xiàn)epoll。一般的使用方式為:
- 創(chuàng)建epoll對(duì)象
self.epoll = select.epoll() # 創(chuàng)建一個(gè)epoll對(duì)象
- 注冊(cè)事件文件描述符怪蔑,監(jiān)聽(tīng)epoll.poll列表并執(zhí)行相應(yīng)操作
self.epoll.register(self.tcp_socket.fileno(), select.EPOLLIN | select.EPOLLET) # 將服務(wù)套接字注冊(cè)
self.epoll.register(new_client_socket.fileno(), select.EPOLLIN | select.EPOLLET)
EPOLLIN (可讀)
EPOLLOUT (可寫)
EPOLLET (ET模式)EPOLLLT(LT模式)
LT模式:當(dāng)epoll檢測(cè)到描述符事件發(fā)生并將此事件通知應(yīng)用程序里覆,應(yīng)用程序可以不立即處理該事件。下次調(diào)用epoll時(shí)缆瓣,
會(huì)再次響應(yīng)應(yīng)用程序并通知此事件喧枷。
ET模式:當(dāng)epoll檢測(cè)到描述符事件發(fā)生并將此事件通知應(yīng)用程序,應(yīng)用程序必須立即處理該事件弓坞。
如果不處理隧甚,下次調(diào)用epoll時(shí),不會(huì)再次響應(yīng)應(yīng)用程序并通知此事件
epoll_list = self.epoll.poll()
for fd, events in epoll_list:
do something
**單進(jìn)程單線程采用epoll實(shí)現(xiàn)靜態(tài)web服務(wù)器完整代碼**
"""linux平臺(tái) epoll 實(shí)現(xiàn)web服務(wù)器"""
'''net09_web_PWB6.py'''
import socket
import select
import re
SERVER_ADDR = (HOST, PORT) = '', 8888 # 服務(wù)器地址
VERSION = 6.0 # web服務(wù)器版本號(hào)
STATIC_PATH = './static/'
class HTTPServer():
def __init__(self, server_address):
"""初始化服務(wù)器TCP套接字"""
self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.tcp_socket.bind(server_address)
self.tcp_socket.listen(128)
self.tcp_socket.setblocking(False) # 將套接字設(shè)置為非阻塞模式
self.epoll = select.epoll() # 創(chuàng)建一個(gè)epoll對(duì)象
self.epoll.register(self.tcp_socket.fileno(), select.EPOLLIN | select.EPOLLET) # 將服務(wù)套接字注冊(cè)
self.connections = {}
self.addresses = {}
def serve_forever(self):
"""永久運(yùn)行監(jiān)聽(tīng)接收連接"""
while True:
epoll_list = self.epoll.poll()
for fd, events in epoll_list:
if fd == self.tcp_socket.fileno():
new_client_socket, new_client_address = self.tcp_socket.accept()
print(new_client_address, '向服務(wù)器發(fā)起了請(qǐng)求')
self.connections[new_client_socket.fileno()] = new_client_socket # 存入客戶連接事件文件描述符
self.addresses[new_client_socket.fileno()] = new_client_address
# 向epoll中則側(cè)新socket的可讀事件
self.epoll.register(new_client_socket.fileno(), select.EPOLLIN | select.EPOLLET)
elif events == select.EPOLLIN:
self.handlerequest(fd)
def handlerequest(self, fd):
"""客戶端請(qǐng)求處理渡冻,發(fā)送響應(yīng)數(shù)據(jù)"""
# 收取瀏覽器請(qǐng)求信息戚扳,并在服務(wù)器端打印顯示
request_data = self.connections[fd].recv(2048).decode('utf-8')
# 數(shù)據(jù)為空代表客戶端關(guān)閉了連接,則在監(jiān)聽(tīng)中除去響應(yīng)的文件描述符
if not request_data:
self.epoll.unregister(fd)
self.connections[fd].close() # server側(cè)主動(dòng)關(guān)閉連接fd
print("%s---offline---" % str(self.addresses[fd]))
del self.connections[fd]
del self.addresses[fd]
# 若存在數(shù)據(jù)族吻,則按照正常的事件處理
else:
request_header_lines = request_data.splitlines()
# for line in request_header_lines:
# print(line)
# 解析請(qǐng)求頭帽借,獲取具體請(qǐng)求信息
pattern = r'[^/]+(/[^ ]*)'
request_html_name = re.match(pattern, request_header_lines[0]).group(1)
# 根據(jù)解析到的內(nèi)容補(bǔ)全將要讀取文件的路徑
if request_html_name == '/':
request_html_name = STATIC_PATH + 'baidu.html'
else:
request_html_name = STATIC_PATH + request_html_name
# 根據(jù)文件情況來(lái)返回相應(yīng)的信息
try:
html_file = open(request_html_name, 'rb')
except FileNotFoundError:
# 文件不存在,則返回文件不存在超歌,并返回狀態(tài)碼404
resp_headers = 'HTTP/1.1 404 not found\r\n'
resp_headers += "Server: PWB" + str(VERSION) + '\r\n'
resp_headers += '\r\n'
resp_body = '==== 404 file not found===='.encode('utf-8')
else:
# 文件存在砍艾,則讀取文件內(nèi)容,并返回狀態(tài)碼200
resp_headers = "HTTP/1.1 200 OK\r\n" # 200代表響應(yīng)成功并找到資源
resp_headers += "Server: PWB" + str(VERSION) + '\r\n' # 告訴瀏覽器服務(wù)器
resp_headers += '\r\n' # 空行隔開(kāi)body
resp_body = html_file.read() # 顯示內(nèi)容為讀取的文件內(nèi)容
html_file.close()
finally:
resp_data = resp_headers.encode('utf-8') + resp_body # 結(jié)合響應(yīng)頭和響應(yīng)體
# 發(fā)送相應(yīng)數(shù)據(jù)至瀏覽器
self.connections[fd].send(resp_data)
# HTTP短連接握础,請(qǐng)求完即關(guān)閉TCP連接辐董,并除去相應(yīng)事件的文件描述符
self.epoll.unregister(fd)
self.connections[fd].close() # server側(cè)主動(dòng)關(guān)閉連接fd
print("%s--web請(qǐng)求響應(yīng)完畢-offline---" % str(self.addresses[fd]))
del self.connections[fd]
del self.addresses[fd]
def run():
"""運(yùn)行服務(wù)器"""
pwb = HTTPServer(SERVER_ADDR)
print('web server:PWB %s on port %d...\n' % (VERSION, PORT))
pwb.serve_forever()
if __name__ == '__main__':
run()
當(dāng)然也可以像9.3節(jié)那樣多線程 方式,一個(gè)連接過(guò)來(lái)開(kāi)一個(gè) 線程處理禀综,這樣消耗的內(nèi)存和線程 切換頁(yè)會(huì)耗掉更多的系統(tǒng)資源简烘。所以, 我們可以結(jié)合I/O多路復(fù)用和多線程 來(lái)提高性能并發(fā),I/O復(fù)用負(fù)責(zé)提高接受socket的通知效率定枷,收到請(qǐng)求后孤澎,交給 多線程 來(lái)處理邏輯。完整源碼見(jiàn)net09_web_PWB7.py欠窒。
'''net09_web_PWB7.py'''
def serve_forever(self):
"""永久運(yùn)行監(jiān)聽(tīng)接收連接"""
while True:
epoll_list = self.epoll.poll()
for fd, events in epoll_list:
if fd == self.tcp_socket.fileno():
new_client_socket, new_client_address = self.tcp_socket.accept()
print(new_client_address, '向服務(wù)器發(fā)起了請(qǐng)求')
self.connections[new_client_socket.fileno()] = new_client_socket # 存入客戶連接事件文件描述符
self.addresses[new_client_socket.fileno()] = new_client_address
# 向epoll中則側(cè)新socket的可讀事件
self.epoll.register(new_client_socket.fileno(), select.EPOLLIN | select.EPOLLET)
elif events == select.EPOLLIN:
td = threading.Thread(target=self.handlerequest, args=(fd,))
td.start()