看完本篇文章,你將會了解到什么是I/O多路復(fù)用檬某,以及如何通過代碼來防止服務(wù)器上的阻塞現(xiàn)象。
1. 基本IO模型
1.1. 數(shù)據(jù)流概念
數(shù)據(jù)流(data stream)是一組有序耕拷,有起點和終點的字節(jié)的數(shù)據(jù)序列锚国,是只能被讀取一次或少數(shù)幾次的點的有序序列蒙袍,其包括輸入流和輸出流。
輸入流可從鍵盤或文件中獲得數(shù)據(jù)邑遏,輸出流可向顯示器、打印機(jī)或文件中傳輸數(shù)據(jù)棍苹。
數(shù)據(jù)流分為輸入流(InputStream)和輸出流(OutputStream)兩類嫩挤。輸入流只能讀不能寫害幅,而輸出流只能寫不能讀,通常程序中使用輸入流讀出數(shù)據(jù)岂昭,輸出流寫入數(shù)據(jù)以现,就好像數(shù)據(jù)流入到程序并從程序中流出,采用數(shù)據(jù)流使程序的輸入輸出操作獨立于相關(guān)設(shè)備约啊。
1.2. IO解釋與IO交互
IO即 input and output,在unix世界里无宿,一切皆文件。而文件是什么呢枢里?文件就是一串二進(jìn)制流,不管socket蹂午、還是FIFO栏豺、管道或終端,一切都是文件豆胸,一切都是流奥洼。在信息交換的過程中,對這些流進(jìn)行數(shù)據(jù)的收發(fā)操作晚胡,簡稱為I/O操作(input and output)灵奖。
用戶空間中存放的是用戶程序的代碼和數(shù)據(jù)洞渔。內(nèi)核空間用來存放的是內(nèi)核代碼和數(shù)據(jù)鱼的。
往流中讀出數(shù)據(jù),系統(tǒng)調(diào)用read估盘;寫入數(shù)據(jù)瓷患,系統(tǒng)調(diào)用write。但是計算機(jī)里有這么多的流遣妥,怎么知道要操作哪個流呢擅编?做到這個的就是文件描述符,即通常所說的fd箫踩。一個fd就是一個整數(shù)爱态,所以對這個整數(shù)的操作,就是對這個文件(流)的操作境钟。創(chuàng)建一個socket锦担,通過系統(tǒng)調(diào)用會返回一個文件描述符,那么剩下對socket的操作就會轉(zhuǎn)化為對這個描述符的操作慨削。
1.3. 阻塞IO
很多時候數(shù)據(jù)在一開始還沒有到達(dá),這個時候內(nèi)核就要等待足夠的數(shù)據(jù)到來痘煤。而在用戶進(jìn)程這邊凑阶,整個進(jìn)程會被阻塞,當(dāng)內(nèi)核一直等到數(shù)據(jù)準(zhǔn)備好了衷快,它就會將數(shù)據(jù)從內(nèi)核中拷貝到用戶內(nèi)存宙橱,然后返回結(jié)果,用戶進(jìn)程才解除阻塞的狀態(tài)蘸拔,重新運行起來师郑。
阻塞IO
2. 非阻塞IO模型與非阻塞套接字
2.1. 非阻塞IO模型
從用戶進(jìn)程角度講,它發(fā)起一個read操作后调窍,并不需要等待宝冕,而是馬上就得到了一個結(jié)果,當(dāng)用戶進(jìn)程判斷結(jié)果是一個error時,它就知道數(shù)據(jù)還沒有準(zhǔn)備好邓萨。
于是它可以再次發(fā)送read操作地梨,一旦內(nèi)核中的數(shù)據(jù)準(zhǔn)備好了,并且又再次收到了用戶進(jìn)程的系統(tǒng)調(diào)用缔恳,那么它馬上就將數(shù)據(jù)拷貝到了用戶內(nèi)存宝剖,然后返回,非阻塞的接口相比于阻塞型接口的顯著差異在于歉甚,在被調(diào)用之后立即返回万细。
2.2. 非阻塞IO
利用異常處理來處理非阻塞IO產(chǎn)生的異常
服務(wù)器代碼:
import socket
server = socket.socket() # 創(chuàng)建一個socket對象,命名為服務(wù)器
server.setblocking(False) #設(shè)置非阻塞套接字
server.bind(('127.0.0.1',8989)) # 綁定端口纸泄,注意這里填入的是元組
server.listen(10) # 設(shè)置最大監(jiān)聽數(shù)赖钞,最大連接量
while True:
try:
result = server.accept() # 與客戶端創(chuàng)建對等套接字
conn,address = result # 元組拆包
conn.setblocking(False) # 設(shè)置非阻塞io
except BlockingIOError:
pass
except Exception as e:
print(f'發(fā)生了未知異常{e}')
"""若端口被占用,可以打開虛擬機(jī)查看進(jìn)程并結(jié)束它"""
# ps -aux | grep python # 查看進(jìn)程
# kill -9 2821 # 這里的2821指的是服務(wù)器運行進(jìn)程id聘裁,照具體情況而定
客戶端代碼
import socket
client = socket.socket() # 創(chuàng)建一個socket對象雪营,命名為客戶端
client.connect(('127.0.0.1',8989))# 連接服務(wù)器端口,注意這里填入的是元組
for i in range(10):
client.send(b'hello') # 發(fā)送hello給服務(wù)器
print(client.recv(1024))
2.3. 并發(fā)與并行
2.3.1. 并發(fā)
指一個時間段中有幾個程序都處于已啟動運行到運行完畢之間咧虎,且這幾個程序都是在同一個處理機(jī)上運行卓缰,但任一個時刻點上只有一個程序在處理機(jī)上運行。
2.3.2. 并行
指一個時間段中有幾個程序都處于已啟動運行到運行完畢之間砰诵,且這幾個程序都是在同個處理機(jī)上運行征唬,并任一個時刻點上有多個程序在處理機(jī)上運行。
2.4. 實現(xiàn)并發(fā)
服務(wù)器代碼
import socket
server = socket.socket() # 創(chuàng)建一個socket對象茁彭,命名為服務(wù)器
server.setblocking(False) #設(shè)置非阻塞套接字
server.bind(('127.0.0.1',8989)) # 綁定端口总寒,注意這里填入的是元組
server.listen(10) # 設(shè)置最大監(jiān)聽數(shù),最大連接量
all_conn = [] # 創(chuàng)建一個空列表理肺,等待套接字添加進(jìn)來
while True:
try:
result = server.accept() # 與客戶端創(chuàng)建對等套接字
conn,address = result # 元組拆包
conn.setblocking(False) # 設(shè)置非阻塞io
all_conn.append(conn)
except BlockingIOError:
pass
except Exception as e:
print(f'發(fā)生了未知異常{e}')
for conn in all_conn: # 依次處理套接字中的數(shù)據(jù)
try:
recv_data = conn.recv(1024)
if recv_data:
print(recv_data)
conn.send(recv_data)
else:
conn.close()
all_conn.remove(conn)
except BlockingIOError:
pass
except Exception as e:
print(f'發(fā)生了未知異常{e}')
"""若端口被占用摄闸,可以打開虛擬機(jī)查看進(jìn)程并結(jié)束它"""
# ps -aux | grep python # 查看進(jìn)程
# kill -9 2821 # 這里的2821指的是服務(wù)器運行進(jìn)程id善镰,照具體情況而定
客戶端代碼
import socket
client = socket.socket() # 創(chuàng)建一個socket對象,命名為客戶端
client.connect(('127.0.0.1',8989))# 連接服務(wù)器端口年枕,注意這里填入的是元組
for i in range(10):
client.send(b'hello') # 發(fā)送hello給服務(wù)器
print(client.recv(1024))
3. IO多路復(fù)用
3.1. 概念介紹
在之前的非阻塞IO模型中炫欺,通過不斷的詢問來查看是否有數(shù)據(jù),會造成資源的浪費熏兄。將查看的過程由主動的查詢變成交給復(fù)用器完成品洛,這樣能夠更加節(jié)省系統(tǒng)資源,并且性能更好摩桶。
3.2. epoll
3.2.1. 非阻塞套接字與多路復(fù)用
非阻塞套接寧需要自身遍歷每個對等連接套接字桥状,并且每次都進(jìn)行IO操作。復(fù)用器不需要進(jìn)行大量的IO操作硝清,因為復(fù)用器會告訴哪個對等連接套接字有數(shù)據(jù)傳輸過來辅斟,然后再去處理即可。
3.2.2. epoll概念
epoll是一個惰性事件回調(diào)芦拿,即回調(diào)過程是用戶自己去調(diào)用的士飒,操作系統(tǒng)只起到通知的作用。epoll是Linux上最好的IO多路復(fù)用器防嗡,但是也只有Linux有变汪,在其他的地方都沒有。
3.2.3. 代碼實現(xiàn)
服務(wù)器代碼
import time
import socket
import selectors # 導(dǎo)入IO多路復(fù)用選擇器
epoll_sel = selectors.EpollSelector() # 實例化一個復(fù)用器對象
default_sel = selectors.DefaultSelector() # 使用默認(rèn)選擇器蚁趁,根據(jù)系統(tǒng)自動選擇
server = socket.socket() # 創(chuàng)建一個socket對象,命名為服務(wù)器
server.bind(('127.0.0.1',8989)) # 綁定端口实胸,注意這里填入的是元組
server.listen(10) # 設(shè)置最大監(jiān)聽數(shù)他嫡,最大連接量
def f_recv(conn):
recv_data = conn.recv(1024)
if recv_data:
print(recv_data)
conn.send(recv_data)
else:
conn.close()
def f_accept(server):
conn,address = server.accept()
epoll_sel.register(conn,selectors.EVENT_READ,f_recv)
# 參數(shù)一:可能會發(fā)生事件的對象;參數(shù)二:檢查是否發(fā)生了事件庐完;參數(shù)三:發(fā)生事件之后需要執(zhí)行的功能钢属。
epoll_sel.register(server,selectors.EVENT_READ,f_accept)
while True:
events=epoll_sel.select() # 是否有事件發(fā)生
time.sleep(1)
print(events)
for key,i in events: # i用來接收1
obj = key.fileobj # 注冊進(jìn)來的發(fā)生事件對象,采用實例對象.屬性的方式進(jìn)行調(diào)用
func = key.data # 要執(zhí)行的方法
func(obj)
客戶端代碼
import socket
client = socket.socket() # 創(chuàng)建一個socket對象门躯,命名為客戶端
client.connect(('127.0.0.1',8989))# 連接服務(wù)器端口淆党,注意這里填入的是元組
for i in range(10):
client.send(b'hello') # 發(fā)送hello給服務(wù)器
print(client.recv(1024))
文章到這里就結(jié)束了!希望大家能多多支持Python(系列)讶凉!六個月帶大家學(xué)會Python染乌,私聊我,可以問關(guān)于本文章的問題懂讯!以后每天都會發(fā)布新的文章荷憋,喜歡的點點關(guān)注!一個陪伴你學(xué)習(xí)Python的新青年褐望!不管多忙都會更新下去勒庄,一起加油串前!
Editor:Lonelyroots