在Python實現web服務器入門學習多進程、多線程實現并發HTTP服務器中,我們知道可以分別通過多進程、多線程的方式實現并發服務器,那么,是否可以通過單進程單線程的程序實現類似功能呢?
實際上,在Python多任務學習分別通過yield關鍵字、greenlet以及gevent實現多任務中,我們知道gevent可以通過協程的方式實現多任務,且相較于yield關鍵字和greenlet而言,gevent屏蔽了很多實現細節,使用起來簡單方便。
一、gevent實現并發HTTP服務器
下面代碼以gevent實現并發HTTP服務器(即一種單進程、單線程、非阻塞的方式):
from gevent import monkey
import gevent
import socket
import re
monkey.patch_all()
def serve_client(new_client_socket):
"""為這個客戶端返回數據"""
# 6.接收瀏覽器發送過來的http請求
request = new_client_socket.recv(1024).decode("utf-8")
# 7.將請求報文分割成字符串列表
request_lines = request.splitlines()
print(request_lines)
# 8.通過正則表達式提取瀏覽器請求的文件名
file_name = None
ret = re.match(r"^[^/]+(/[^ ]*)", request_lines[0])
if ret:
file_name = ret.group(1)
print("file_name:", file_name)
if file_name == "/":
file_name = "/index.html"
# 9.返回http格式的應答數據給瀏覽器
try:
f = open("./Charisma" + file_name, "rb")
except Exception:
response = "HTTP/1.1 404 NOT FOUND\r\n"
response += "\r\n"
response += "-----file not found-----"
new_client_socket.send(response.encode("utf-8"))
else:
# 9.1 讀取發送給瀏覽器的數據-->body
html_content = f.read()
f.close()
# 9.2 準備發送給瀏覽器的數據-->header
response = "HTTP/1.1 200 OK\r\n"
response += "\r\n"
# 將response header發送給瀏覽器--先以utf-8格式編碼
new_client_socket.send(response.encode("utf-8"))
# 將response body發送給瀏覽器--直接是以字節形式發送
new_client_socket.send(html_content)
# 10. 關閉此次服務的套接字
new_client_socket.close()
def main():
"""用來完成程序整體控制"""
# 1.創建套接字
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 通過設定套接字選項解決[Errno 98]錯誤
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 2.綁定端口
tcp_server_socket.bind(("", 7899))
# 3.變為監聽套接字
tcp_server_socket.listen(128)
while True:
# 4.等待新客戶端連接
new_client_socket, client_addr = tcp_server_socket.accept()
# 5.為連接上的客戶端服務
# 創建一個greenlet并不會導致其立即得到切換執行,
# 還需要在其父greenlet(在哪個程序控制流中創建該greenlet,
# 則這個程序控制流就是父greenlet)中遇到正確的阻塞延時類操作或調用greenlet對象的join()方法
#(此處不需要使用join()函數,因為主程序由于死循環的緣故不會在greenlet執行結束前退出)
greenlet = gevent.spawn(serve_client, new_client_socket)
# 關閉監聽套接字
tcp_server_socket.close()
if __name__ == "__main__":
main()
至此,本文和Python實現web服務器入門學習筆記(3)——多進程、多線程實現并發HTTP服務器給出了三種實現并發HTTP服務器的方式,對比之后,可以發現:
對于進程、線程實現方式,服務器都是在客戶端數量大于1時開辟新的程序執行控制流(分別叫子進程、子線程),且程序控制流之間一般相對獨立,不會因為一個的阻塞而導致其他程序控制流無法執行,以避免在單進程且單線程的程序中,先建立請求的客戶端因各種原因引起程序阻塞而導致其他客戶端的請求得不到執行。如:上述通過進程和線程實現并發服務器程序中的accept()、recv()方法均為阻塞類操作。
對于協程實現方式,服務器為實現并發,雖然也會開辟新的程序執行控制流(這里叫greenlet,程序執行控制流可以承擔很多身份,比如:進程、線程、greenlet,關于greenlet的詳細說明,具體請見Python多任務學習筆記(10)——分別通過yield關鍵字、greenlet以及gevent實現多任務),但是這些程序執行控制流之間通過確定的時序作相互間切換實現并發,切換時機為程序中所有延時阻塞類操作的地方,指定程序控制流切換執行時機的方式有兩種:
程序規模很小時,對于所有延時阻塞類操作,如:time.sleep(),socket.accept(),socket.recv(),使用gevent模塊中的同名操作做替換,如:gevent.sleep(),gevent.accept(),gevent.recv();
程序規模很大,且多處使用了涉及延時阻塞類操作時,不用挨個做模塊替換,通過gevent.monkey模塊中的patch_all()函數改變所有的阻塞類操作的行為,使得每當程序遇到阻塞類操作則切換至其他greenlet。
對于協程實現方式,在主程序執行控制流中,通過gevent.spawn()這一類方法創建一個greenlet之后,主程序執行控制流自動成為該greenlet的父greenlet,如果程序中僅存在這兩個greenlet,則程序也會在遇到正確的阻塞延時類操作時,在二者之間切換執行,請比較下面兩段代碼:
1. 程序未正確指定阻塞延時類操作:
import gevent
import time
def foo():
print('Explicit context switch to foo!')
gevent.sleep(0.0)
print('Explicit context switch to foo again!')
def main():
greenlet = gevent.spawn(foo)
print('Explicit execution in main!')
time.sleep(0.0)
print('Explicit context switch to main again!')
time.sleep(0.0)
print("The end of main!")
# 確保主程序(即主greenlet)等待子greenlet執行完畢之后才退出
greenlet.join()
if __name__ == '__main__':
main()
上述代碼的運行結果為:
Explicit execution in main!
Explicit context switch to main again!
The end of main!
Explicit context switch to foo!
Explicit context switch to foo again!
2. 程序正確指定了阻塞延時類操作:
import gevent
def foo():
print('Explicit context switch to foo!')
gevent.sleep(0.0)
print('Explicit context switch to foo again!')
def main():
greenlet = gevent.spawn(foo)
print('Explicit execution in main!')
gevent.sleep(0.0)
print('Explicit context switch to main again!')
gevent.sleep(0.0)
# greenlet.join()
print("The end of main!")
if __name__ == '__main__':
main()
上述代碼運行結果為:
Explicit execution in main!
Explicit context switch to foo!
Explicit context switch to main again!
Explicit context switch to foo again!
The end of main!
對比上述兩段代碼,我們知道:
創建一個greenlet并不會導致其立即得到切換執行,還需要在其父greenlet(在哪個程序控制流中創建該greenlet,則這個程序控制流就是父greenlet)中遇到正確的阻塞延時類操作或調用greenlet對象的join()方法;
即使不調用greenlet對象的join()方法,只要使用正確的阻塞延時類操作,程序依然可以按照期望的順序執行完畢。
二、單進程單線程非阻塞實現并發原理
實際上,從單進程、單線程、非阻塞這幾個關鍵字就可以發現,要想通過單進程、單線程實現并發,首要是要解決單進程、單線程的程序可能面對的程序阻塞問題,因為這一般會無謂地耗費時間。鄭州哪個人流醫院好 http://www.csyhjlyy.com/
那么,自然地,我們會想到:是否可以讓原本阻塞的操作不阻塞?答案是肯定的:對于socket對象中的accept()、recv()等方法,其原本都是阻塞類操作,可以通過調用socket對象的setblocking()方法設置其為非阻塞模式。
然而,問題在于:在將socket對象設置為非阻塞模式的情況下,在調用其accept()、recv()方法時,如果未能立刻正確返回,則程序會拋出異常。故此時需要進行異常捕捉和處理,保證程序不被中斷。
基于上述討論,下面代碼簡單演示了單進程單線程非阻塞實現并發的原理:
import socket
import time
def initialize(port):
# 1.創建服務器端TCP協議socket
tcp_server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2.綁定本地IP和端口
tcp_server_sock.bind(("", port))
# 3.設置套接字為監聽狀態
tcp_server_sock.listen(128)
# 4.設置套接字為非阻塞狀態
tcp_server_sock.setblocking(False)
return tcp_server_sock
def non_blocking_serve(tcp_server_sock):
# 定義一個列表,用于存放已成功連接但是未完成數據發送的客戶端
client_sock_list = list()
while True:
try:
new_client_sock, new_client_addr = tcp_server_sock.accept()
except Exception as exception:
print(exception)
else:
print("新客戶端", new_client_sock, "已成功連接!")
new_client_sock.setblocking(False)
client_sock_list.append(new_client_sock)
for client_sock in client_sock_list:
try:
recv_data = client_sock.recv(1024)
except Exception as exception:
print(exception)
else:
if recv_data:
# 表明客戶端發來了數據
print(recv_data)
else:
# 客戶端已調用close()方法,recv()返回為空
client_sock_list.remove(client_sock)
client_sock.close()
print("客戶端", client_sock, "已斷開連接!")
def main():
tcp_server_sock = initialize(8888)
non_blocking_serve(tcp_server_sock)
if __name__ == '__main__':
main()
對于上述代碼,需要說明的幾點是:
程序24行定義的列表client_sock_list用于存放已成功連接但是未完成數據發送的客戶端對象;
程序36行遍歷列表client_sock_list,挨個通過recv()方法通過非阻塞方式接收數據,而recv()方法正確返回有兩種情況:
客戶端將數據正確發送了過來,此時recv_data變量非空;
客戶端完成了此次請求,主動先斷開了連接,此時recv_data為空。
程序45行將已經完成請求的客戶端移出列表client_sock_list,避免列表過長產生無效遍歷,導致程序性能下降。