python非阻塞多线程socket_Python实现web服务器之 单进程单线程非阻塞实现并发及其原理...
在Python實現(xiàn)web服務(wù)器入門學(xué)習(xí)多進(jìn)程、多線程實現(xiàn)并發(fā)HTTP服務(wù)器中,我們知道可以分別通過多進(jìn)程、多線程的方式實現(xiàn)并發(fā)服務(wù)器,那么,是否可以通過單進(jìn)程單線程的程序?qū)崿F(xiàn)類似功能呢?
實際上,在Python多任務(wù)學(xué)習(xí)分別通過yield關(guān)鍵字、greenlet以及gevent實現(xiàn)多任務(wù)中,我們知道gevent可以通過協(xié)程的方式實現(xiàn)多任務(wù),且相較于yield關(guān)鍵字和greenlet而言,gevent屏蔽了很多實現(xiàn)細(xì)節(jié),使用起來簡單方便。
一、gevent實現(xiàn)并發(fā)HTTP服務(wù)器
下面代碼以gevent實現(xiàn)并發(fā)HTTP服務(wù)器(即一種單進(jìn)程、單線程、非阻塞的方式):
from gevent import monkey
import gevent
import socket
import re
monkey.patch_all()
def serve_client(new_client_socket):
"""為這個客戶端返回數(shù)據(jù)"""
# 6.接收瀏覽器發(fā)送過來的http請求
request = new_client_socket.recv(1024).decode("utf-8")
# 7.將請求報文分割成字符串列表
request_lines = request.splitlines()
print(request_lines)
# 8.通過正則表達(dá)式提取瀏覽器請求的文件名
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格式的應(yīng)答數(shù)據(jù)給瀏覽器
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 讀取發(fā)送給瀏覽器的數(shù)據(jù)-->body
html_content = f.read()
f.close()
# 9.2 準(zhǔn)備發(fā)送給瀏覽器的數(shù)據(jù)-->header
response = "HTTP/1.1 200 OK\r\n"
response += "\r\n"
# 將response header發(fā)送給瀏覽器--先以utf-8格式編碼
new_client_socket.send(response.encode("utf-8"))
# 將response body發(fā)送給瀏覽器--直接是以字節(jié)形式發(fā)送
new_client_socket.send(html_content)
# 10. 關(guān)閉此次服務(wù)的套接字
new_client_socket.close()
def main():
"""用來完成程序整體控制"""
# 1.創(chuàng)建套接字
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 通過設(shè)定套接字選項解決[Errno 98]錯誤
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 2.綁定端口
tcp_server_socket.bind(("", 7899))
# 3.變?yōu)楸O(jiān)聽套接字
tcp_server_socket.listen(128)
while True:
# 4.等待新客戶端連接
new_client_socket, client_addr = tcp_server_socket.accept()
# 5.為連接上的客戶端服務(wù)
# 創(chuàng)建一個greenlet并不會導(dǎo)致其立即得到切換執(zhí)行,
# 還需要在其父greenlet(在哪個程序控制流中創(chuàng)建該greenlet,
# 則這個程序控制流就是父greenlet)中遇到正確的阻塞延時類操作或調(diào)用greenlet對象的join()方法
#(此處不需要使用join()函數(shù),因為主程序由于死循環(huán)的緣故不會在greenlet執(zhí)行結(jié)束前退出)
greenlet = gevent.spawn(serve_client, new_client_socket)
# 關(guān)閉監(jiān)聽套接字
tcp_server_socket.close()
if __name__ == "__main__":
main()
至此,本文和Python實現(xiàn)web服務(wù)器入門學(xué)習(xí)筆記(3)——多進(jìn)程、多線程實現(xiàn)并發(fā)HTTP服務(wù)器給出了三種實現(xiàn)并發(fā)HTTP服務(wù)器的方式,對比之后,可以發(fā)現(xiàn):
對于進(jìn)程、線程實現(xiàn)方式,服務(wù)器都是在客戶端數(shù)量大于1時開辟新的程序執(zhí)行控制流(分別叫子進(jìn)程、子線程),且程序控制流之間一般相對獨(dú)立,不會因為一個的阻塞而導(dǎo)致其他程序控制流無法執(zhí)行,以避免在單進(jìn)程且單線程的程序中,先建立請求的客戶端因各種原因引起程序阻塞而導(dǎo)致其他客戶端的請求得不到執(zhí)行。如:上述通過進(jìn)程和線程實現(xiàn)并發(fā)服務(wù)器程序中的accept()、recv()方法均為阻塞類操作。
對于協(xié)程實現(xiàn)方式,服務(wù)器為實現(xiàn)并發(fā),雖然也會開辟新的程序執(zhí)行控制流(這里叫g(shù)reenlet,程序執(zhí)行控制流可以承擔(dān)很多身份,比如:進(jìn)程、線程、greenlet,關(guān)于greenlet的詳細(xì)說明,具體請見Python多任務(wù)學(xué)習(xí)筆記(10)——分別通過yield關(guān)鍵字、greenlet以及gevent實現(xiàn)多任務(wù)),但是這些程序執(zhí)行控制流之間通過確定的時序作相互間切換實現(xiàn)并發(fā),切換時機(jī)為程序中所有延時阻塞類操作的地方,指定程序控制流切換執(zhí)行時機(jī)的方式有兩種:
程序規(guī)模很小時,對于所有延時阻塞類操作,如:time.sleep(),socket.accept(),socket.recv(),使用gevent模塊中的同名操作做替換,如:gevent.sleep(),gevent.accept(),gevent.recv();
程序規(guī)模很大,且多處使用了涉及延時阻塞類操作時,不用挨個做模塊替換,通過gevent.monkey模塊中的patch_all()函數(shù)改變所有的阻塞類操作的行為,使得每當(dāng)程序遇到阻塞類操作則切換至其他greenlet。
對于協(xié)程實現(xiàn)方式,在主程序執(zhí)行控制流中,通過gevent.spawn()這一類方法創(chuàng)建一個greenlet之后,主程序執(zhí)行控制流自動成為該greenlet的父greenlet,如果程序中僅存在這兩個greenlet,則程序也會在遇到正確的阻塞延時類操作時,在二者之間切換執(zhí)行,請比較下面兩段代碼:
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執(zhí)行完畢之后才退出
greenlet.join()
if __name__ == '__main__':
main()
上述代碼的運(yùn)行結(jié)果為:
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()
上述代碼運(yùn)行結(jié)果為:
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!
對比上述兩段代碼,我們知道:
創(chuàng)建一個greenlet并不會導(dǎo)致其立即得到切換執(zhí)行,還需要在其父greenlet(在哪個程序控制流中創(chuàng)建該greenlet,則這個程序控制流就是父greenlet)中遇到正確的阻塞延時類操作或調(diào)用greenlet對象的join()方法;
即使不調(diào)用greenlet對象的join()方法,只要使用正確的阻塞延時類操作,程序依然可以按照期望的順序執(zhí)行完畢。
二、單進(jìn)程單線程非阻塞實現(xiàn)并發(fā)原理
實際上,從單進(jìn)程、單線程、非阻塞這幾個關(guān)鍵字就可以發(fā)現(xiàn),要想通過單進(jìn)程、單線程實現(xiàn)并發(fā),首要是要解決單進(jìn)程、單線程的程序可能面對的程序阻塞問題,因為這一般會無謂地耗費(fèi)時間。鄭州哪個人流醫(yī)院好 http://www.csyhjlyy.com/
那么,自然地,我們會想到:是否可以讓原本阻塞的操作不阻塞?答案是肯定的:對于socket對象中的accept()、recv()等方法,其原本都是阻塞類操作,可以通過調(diào)用socket對象的setblocking()方法設(shè)置其為非阻塞模式。
然而,問題在于:在將socket對象設(shè)置為非阻塞模式的情況下,在調(diào)用其accept()、recv()方法時,如果未能立刻正確返回,則程序會拋出異常。故此時需要進(jìn)行異常捕捉和處理,保證程序不被中斷。
基于上述討論,下面代碼簡單演示了單進(jìn)程單線程非阻塞實現(xiàn)并發(fā)的原理:
import socket
import time
def initialize(port):
# 1.創(chuàng)建服務(wù)器端TCP協(xié)議socket
tcp_server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2.綁定本地IP和端口
tcp_server_sock.bind(("", port))
# 3.設(shè)置套接字為監(jiān)聽狀態(tài)
tcp_server_sock.listen(128)
# 4.設(shè)置套接字為非阻塞狀態(tài)
tcp_server_sock.setblocking(False)
return tcp_server_sock
def non_blocking_serve(tcp_server_sock):
# 定義一個列表,用于存放已成功連接但是未完成數(shù)據(jù)發(fā)送的客戶端
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:
# 表明客戶端發(fā)來了數(shù)據(jù)
print(recv_data)
else:
# 客戶端已調(diào)用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()
對于上述代碼,需要說明的幾點(diǎn)是:
程序24行定義的列表client_sock_list用于存放已成功連接但是未完成數(shù)據(jù)發(fā)送的客戶端對象;
程序36行遍歷列表client_sock_list,挨個通過recv()方法通過非阻塞方式接收數(shù)據(jù),而recv()方法正確返回有兩種情況:
客戶端將數(shù)據(jù)正確發(fā)送了過來,此時recv_data變量非空;
客戶端完成了此次請求,主動先斷開了連接,此時recv_data為空。
程序45行將已經(jīng)完成請求的客戶端移出列表client_sock_list,避免列表過長產(chǎn)生無效遍歷,導(dǎo)致程序性能下降。
總結(jié)
以上是生活随笔為你收集整理的python非阻塞多线程socket_Python实现web服务器之 单进程单线程非阻塞实现并发及其原理...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 生产白糖的上市公司有哪些
- 下一篇: 消费金融上市公司