3.3.4 IO多路復用
定義
系統同時監控多個IO事件,當哪個IO事件准備就緒就執行哪個IO事件。以此形成可以同時處理多個IO的行為,避免一個IO阻塞造成其他IO均無法執行,提高了IO執行效率。
在單進程的狀態下,IO想要實現跳過阻塞部分去執行非阻塞部分來提高程序的執行的效率,應用層是沒有這樣功能,需要借助於操作系統的輪循機制,不斷尋找沒有阻塞的部分去先執行。
2個具體方案
select方法 : Windows Linux Unix
epoll方法: Linux --效率略高於select
select 方法
import select rs, ws, xs=select(rlist, wlist, xlist,[timeout]) 功能: 監控IO事件,阻塞等待IO的發生 參數: rlist 列表 讀IO列表,添加等待發生的或者可讀的IO事件 讀例如:read recv accept ---別的地方往程序裡面搞東西的,經常被動阻塞 wlist 列表 寫IO列表,存放要可以主動處理的或者可寫的IO事件 寫例如:send write---程序往別的地方輸出信息,經常主動不阻塞,實際很少用它。 xlist 列表 異常IO列表,存放出現異常要處理的IO事件,基本在Linux下無用,系統不會幫你捕捉異常事 件,需要在我們應用層捕捉。因此基本不用他。 timeout 超時時間---不寫默認死等 返回值: rs 列表 rlist中准備就緒的IO ws 列表 wlist中准備就緒的IO xs 列表 xlist中准備就緒的IO
""" IO 多路復用方法 select """ from select import select from socket import * # 准備一些IO操作對象 file = open("../day15/my.log", 'rb') # open創建的對象比較特殊,同時具備讀與寫事件 udp = socket(type=SOCK_DGRAM) # 具有寫事件已經就緒 tcp = socket() tcp.bind(("0.0.0.0", 8888)) tcp.listen(5) # 沒有客戶端連接就一直阻塞 # 監控IO print("開始監控") rs, ws, xs = select([tcp, file], [udp, file], []) print("rlist:", rs) print("wlist:", ws) print("xlist:", xs)
from socket import * # 服務器地址 ADDR = ("127.0.0.1",8888) tcp_socket = socket() tcp_socket.connect(ADDR) while True: msg = input(">>") if not msg: break tcp_socket.send(msg.encode()) data = tcp_socket.recv(1024) print("From server:",data.decode()) # 關閉 tcp_socket.close()
""" 單進程基於select 的IO多路復用並發模型 重點代碼 !! 目標 : 服務端能夠同時應對多個客戶端發消息和連接 思路 : 將監聽套接字與連接套接字一起監控 """ from socket import * from select import select HOST = "0.0.0.0" PORT = 8888 ADDR = (HOST, PORT) # 監控列表 rlist = [] wlist = [] xlist = [] # 處理客戶端連接 def connect_client(sock): connfd, addr = sock.accept() print("Connect from", addr) connfd.setblocking(False) # 設置套接字為非阻塞IO rlist.append(connfd) # 具體客戶端發送請求 def handle(connfd): data = connfd.recv(1024) if not data: rlist.remove(connfd) # 不關注 connfd.close() return print(data.decode()) connfd.send(b"OK") # 啟動服務函數 def main(): # 創建監聽套接字 sock = socket() sock.bind(ADDR) sock.listen(5) sock.setblocking(False) # 與非阻塞IO配合 print("Listen the port %d" % PORT) rlist.append(sock) # 初始關注監聽套接字 # 循環接收監控IO發生 while True: rs, ws, xs = select(rlist, wlist, xlist) for r in rs: if r is sock: # 判斷對象用is最合適 connect_client(r) # 處理連接 else: handle(r) # 處理請求 if __name__ == '__main__': main()
""" 加入寫到wlist """ from socket import * from select import select HOST = "0.0.0.0" PORT = 8888 ADDR = (HOST, PORT) # 啟動服務函數 def main(): # 創建監聽套接字 sock = socket() sock.bind(ADDR) sock.listen(5) sock.setblocking(False) # 與非阻塞IO配合 print("Listen the port %d" % PORT) # 監控列表 rlist = [sock] # 初始關注監聽套接字 wlist = [] xlist = [] # 循環接收監控IO發生 while True: rs, ws, xs = select(rlist, wlist, xlist) for r in rs: if r is sock: connfd, addr = r.accept() # 處理連接 print("Connect from", addr) connfd.setblocking(False) rlist.append(connfd) else: data = r.recv(1024) if not data: rlist.remove(r) # 不關注 r.close() continue print(data.decode()) # r.send(b"OK") wlist.append(r) # 加入寫到wlist for w in ws: w.send(b"OK") wlist.remove(w) # 移除 if __name__ == '__main__': main()
epoll方法
import select ep = select.epoll() 功能 : 創建epoll對象 返回值: epoll對象
ep.register(fd,event) 功能: 注冊關注的IO事件 參數:fd 要關注的IO event 要關注的IO事件類型 常用類型EPOLLIN 讀IO事件(rlist) EPOLLOUT 寫IO事件 (wlist) EPOLLERR 異常IO (xlist) e.g. ep.register(sockfd,EPOLLIN|EPOLLERR)#關注多個事件用 | ep.unregister(fd) 功能:取消對IO的關注 參數:IO對象 或者IO對象的fileno(即文件描述符) 文件描述符 : Linux操作系統會給每個IO對象分配一個 不重復的整數編號,這個編號就是文件描述符
events = ep.poll() 功能: 將關注的IO提交監控,阻塞等待監控的IO事件發生 返回值: 返回發生的IO events格式 [(fileno,event),()....] 每個元組為一個就緒IO,其中元組第一項是該IO的fileno,第二項為該IO就緒的事件類型EPOLLIN,EPOLLOUT , EPOLLERR
""" 基於epoll 的IO多路復用並發模型 重點代碼 !! 目標 : 服務端能夠同時應對多個客戶端發消息和連接 思路 : 將監聽套接字與連接套接字一起監控 """ from socket import * from select import * HOST = "0.0.0.0" PORT = 8888 ADDR = (HOST, PORT) # 查找字典 時刻與關注的IO對象一致 {fileno:object} map = {} # 處理客戶端連接 def connect_client(ep, fd): connfd, addr = map[fd].accept() print("Connect from", addr) connfd.setblocking(False) # 添加新的關注 設置邊緣觸發 ep.register(connfd, EPOLLIN|EPOLLET) map[connfd.fileno()] = connfd # 維護字典 # 具體客戶端發送請求 def handle(ep, fd): data = map[fd].recv(1024) if not data: ep.unregister(fd) map[fd].close() del map[fd] # 從字典中移除 return print(data.decode()) map[fd].send(b"OK") # 啟動服務函數 def main(): # 創建監聽套接字 sock = socket() sock.bind(ADDR) sock.listen(5) sock.setblocking(False) # 與非阻塞IO配合 print("Listen the port %d" % PORT) # 創建epoll對象 ep = epoll() ep.register(sock, EPOLLIN) # 初始關注監聽套接字 # 查找字典 時刻與關注的IO對象一致 {fileno:object} map[sock.fileno()] = sock # 循環接收監控IO發生 while True: events = ep.poll() # 監控函數 [(),()] print("你有新的IO需要處理哦",events) for fd, event in events: if fd == sock.fileno(): connect_client(ep, fd) # 處理連接 # else: # handle(ep, fd) # 處理請求 if __name__ == '__main__': main()
""" IO 多路復用方法 epoll """ from select import * from socket import * # 准備一些IO操作對象 file = open("../day15/my.log",'rb') udp = socket(type=SOCK_DGRAM) tcp = socket() tcp.bind(("0.0.0.0",8888)) tcp.listen(5) # 查找字典 {fileno : object} map = {} ep = epoll() ep.register(tcp,EPOLLIN) # 讀事件 map[tcp.fileno()] = tcp # 添加到字典 # 監控IO print("開始監控") events = ep.poll() # 阻塞等待 print("events:",events) # [(5,1)] map[5].accept()
select 方法與epoll方法對比
epoll 效率比select要高
epoll 同時監控IO數量比select要多
epoll 支持EPOLLET觸發方式
3.3.5 IO並發模型
利用IO多路復用等技術,同時處理多個客戶端IO請求。
優點 : 資源消耗少,能同時高效處理多個IO行為
缺點 : 只針對處理並發產生的IO事件
適用情況:HTTP請求,網絡傳輸等都是IO行為,可以通過IO多路復用監控多個客戶端的IO請求。
網絡並發服務實現過程
【1】將套接字對象設置為關注的IO,通常設置為非阻塞狀態。
【2】通過IO多路復用方法提交,進行IO監控。
【3】阻塞等待,當監控的IO有事件發生時結束阻塞。
【4】遍歷返回值列表,確定就緒的IO事件類型。
【5】處理發生的IO事件。
【6】繼續循環監控IO發生。
IO與進程線程對比
兩者表面上都是服務端應對多個服務端,前者是創建多進程線程實現,適合大型的底層框架結構,資源消耗多。後者是單進程,只用一個計算機內核,需要操作系統功能支持,利用等待阻塞的時間來完成,資源消耗少,輕量級,適合處理網絡收發事件。工作中兩者配合用,例如來一個用戶開啟一個進程專門應對客戶端,客戶端請求復雜情況下,在這個進程裡面用IO多路復用減少多個請求的阻塞時間。
4.1.1 協議概述
用途 : 網頁獲取,數據的傳輸
特點
應用層協議,選擇使用tcp進行數據傳輸
簡單,靈活,很多語言都有HTTP專門接口
有豐富的請求類型
可以傳輸的數據類型眾多
4.1.2 網頁訪問流程
客戶端(浏覽器)通過tcp傳輸,發送http請求給服務端
服務端接收到http請求後進行解析
服務端處理請求內容,組織響應內容
服務端將響應內容以http響應格式發送給浏覽器
浏覽器接收到響應內容,解析展示
前端工程師用JavaScript(寫行為動作)與HTML(寫內容)與CSS(寫樣式),給到服務器的某個位置保存起來,因此網頁就是一個文件。DNS是實現域名解析,把百度轉換成IP地址,
前情回顧 1. 多進程多線程並發模型 用函數編寫 / 用類編寫 2. ftp文件服務 * 通信協議 響應: 響應情況 響應信息 * 請求和處理請求的模型 客戶端發送請求-> 等待響應-> 根據響應不同分情況討論 接收請求->處理請求->根據處理情況不同發送響應 3. IO 模型 輸入(讀) 輸出(寫) IO 密集 : IO多 耗時長 多阻塞 cpu消耗少 計算密集 : 計算多 cpu消耗多 無阻塞 耗時短 4. 阻塞 IO 和 非阻塞 IO 阻塞 IO : 默認 效率最低 非阻塞IO : 將阻塞 --> 不阻塞 sock.setblocking() sock.settimeout(3) cookie :Python支持位運算 位運算: 將整數轉換為二進制數再按照位進行計算 | 按位或 14 | 17 1110 10001 11111 --> 31 & 按位與 14 & 17 1110 10001 00000 --> 0 Cookie : 文件描述符 : Linux操作系統會給每個IO對象分配一個 不重復的整數編號,這個編號就是文件描述符 >=0的 訓練 : 將select_server 改寫為使用epoll方法來 實現. select : 支持系統多,使用簡單,只有水平出發 epoll : 效率比select高 , 同時監控IO數量多 默認水平出發,但是支持邊緣出發 水平觸發 : 當有IO事件發生時如果不處理則一直提醒 作業 : 1. 聊天室和ftp文件服務器 最少二選一寫一下 2. 第二階段 做一個 思維導圖 把知識點梳理