Python並發編程-事件驅動模型


 一、事件驅動模型介紹                                                                                                       

1、傳統的編程模式 

例如:線性模式大致流程

開始--->代碼塊A--->代碼塊B--->代碼塊C--->代碼塊D--->......--->結束

每一個代碼塊里是完成各種各樣事情的代碼,但編程者知道代碼塊A,B,C,D...的執行順序,唯一能夠改變這個流程的是數據。輸入不同的數據,根據條件語句判斷,流程或許就改為A--->C--->E...--->結束。每一次程序運行順序或許都不同,但它的控制流程是由輸入數據和你編寫的程序決定的。如果你知道這個程序當前的運行狀態(包括輸入數據和程序本身),那你就知道接下來甚至一直到結束它的運行流程。

例如:事件驅動型程序模型大致流程

開始--->初始化--->等待

 與上面傳統編程模式不同,事件驅動程序在啟動之后,就在那等待,等待什么呢?等待被事件觸發。傳統編程下也有“等待”的時候,比如在代碼塊D中,你定義了一個input(),需要用戶輸入數據。但這與下面的等待不同,傳統編程的“等待”,比如input(),你作為程序編寫者是知道或者強制用戶輸入某個東西的,或許是數字,或許是文件名稱,如果用戶輸入錯誤,你還需要提醒他,並請他重新輸入。事件驅動程序的等待則是完全不知道,也不強制用戶輸入或者干什么。只要某一事件發生,那程序就會做出相應的“反應”。這些事件包括:輸入信息、鼠標、敲擊鍵盤上某個鍵還有系統內部定時器觸發。

2、事件驅動模型

通常,我們寫服務器處理模型的程序時,有以下幾種模型:

(1)每收到一個請求,創建一個新的進程,來處理該請求; 
(2)每收到一個請求,創建一個新的線程,來處理該請求; 
(3)每收到一個請求,放入一個事件列表,讓主進程通過非阻塞I/O方式來處理請求

3、第三種就是協程、事件驅動的方式,一般普遍認為第(3)種方式是大多數網絡服務器采用的方式 

示例:

 1 #事件驅動之鼠標點擊事件注冊
 2 
 3 <!DOCTYPE html>
 4 <html lang="en">
 5 <head>
 6     <meta charset="UTF-8">
 7     <title>Title</title>
 8 
 9 </head>
10 <body>
11 
12 <p onclick="fun()">點我呀</p>
13 
14 
15 <script type="text/javascript">
16     function fun() {
17           alert('約嗎?')
18     }
19 </script>
20 </body>
21 
22 </html>

執行結果:

在UI編程中,常常要對鼠標點擊進行相應,首先如何獲得鼠標點擊呢?

兩種方式:

1、創建一個線程循環檢測是否有鼠標點擊

      那么這個方式有以下幾個缺點:

  1. CPU資源浪費,可能鼠標點擊的頻率非常小,但是掃描線程還是會一直循環檢測,這會造成很多的CPU資源浪費;如果掃描鼠標點擊的接口是阻塞的呢?
  2. 如果是堵塞的,又會出現下面這樣的問題,如果我們不但要掃描鼠標點擊,還要掃描鍵盤是否按下,由於掃描鼠標時被堵塞了,那么可能永遠不會去掃描鍵盤;
  3. 如果一個循環需要掃描的設備非常多,這又會引來響應時間的問題; 
    所以,該方式是非常不好的。

2、事件驅動模型 

  目前大部分的UI編程都是事件驅動模型,如很多UI平台都會提供onClick()事件,這個事件就代表鼠標按下事件。事件驅動模型大體思路如下:

    1. 有一個事件(消息)隊列;
    2. 鼠標按下時,往這個隊列中增加一個點擊事件(消息);
    3. 有個循環,不斷從隊列取出事件,根據不同的事件,調用不同的函數,如onClick()、onKeyDown()等;
    4. 事件(消息)一般都各自保存各自的處理函數指針,這樣,每個消息都有獨立的處理函數; 

什么是事件驅動模型 ?

  目前大部分的UI編程都是事件驅動模型,如很多UI平台都會提供onClick()事件,這個事件就代表鼠標按下事件。事件驅動模型大體思路如下:

    1. 有一個事件(消息)隊列;
    2. 鼠標按下時,往這個隊列中增加一個點擊事件(消息);
    3. 有個循環,不斷從隊列取出事件,根據不同的事件,調用不同的函數,如onClick()、onKeyDown()等;
    4. 事件(消息)一般都各自保存各自的處理函數指針,這樣,每個消息都有獨立的處理函數; 
      這里寫圖片描述
      事件驅動編程是一種編程范式,這里程序的執行流由外部事件來決定。它的特點是包含一個事件循環,當外部事件發生時使用回調機制來觸發相應的處理。另外兩種常見的編程范式是(單線程)同步以及多線程編程。

 

需知:每個cpu都有其一套可執行的專門指令集,如SPARC和Pentium,其實每個硬件之上都要有一個控制程序,cpu的指令集就是cpu的控制程序。

 

二、IO模型准備                                                                                                            

在進行解釋之前,首先要說明幾個概念:

  1. 用戶空間和內核空間
  2. 進程切換
  3. 進程的阻塞
  4. 文件描述符
  5. 緩存 I/O

1、用戶空間和內核空間

例如:采用虛擬存儲器,對於32bit操作系統,它的尋址空間(虛擬存儲空間為4G,即2的32次方)。

操作系統的核心是內核,獨立於普通的應用程序,可以訪問受保護的內存空間,也可以訪問底層硬件的所有權限。

  為了保證用戶進程不能直接操作內核(kernel),保證內核的安全,操作系統將虛擬空間划分為兩部分:一部分為內核空間,另一部分為用戶空間。

那么操作系統是如何分配空間的?這里就會涉及到內核態和用戶態的兩種工作狀態。

1G: 0 --->內核態 
3G: 1 --->用戶態 

CPU的指令集,是通過0和1 決定你是用戶態,還是內核態

 

計算機的兩種工作狀態內核態用戶態

cpu的兩種工作狀態:

  現在的操作系統都是分時操作系統,分時的根源,來自於硬件層面操作系統內核占用的內存與應用程序占用的內存彼此之間隔離。cpu通過psw(程序狀態寄存器)中的一個2進制位來控制cpu本身的工作狀態,即內核態與用戶態。

  內核態:操作系統內核只能運作於cpu的內核態,這種狀態意味着可以執行cpu所有的指令,可以執行cpu所有的指令,這也意味着對計算機硬件資源有着完全的控制權限,並且可以控制cpu工作狀態由內核態轉成用戶態。

  用戶態:應用程序只能運作於cpu的用戶態,這種狀態意味着只能執行cpu所有的指令的一小部分(或者稱為所有指令的一個子集),這一小部分指令對計算機的硬件資源沒有訪問權限(比如I/O),並且不能控制由用戶態轉成內核態。

2、進程切換                                                                                                        

  為了控制進程的執行,內核必須有能力掛起正在CPU上執行的進程,並恢復以前掛起的某個進程的執行,這種行為就被稱為進程切換。

總結:進程切換是很消耗資源的。

3、進程的阻塞

  正在執行的進程,由於期待的某些事件未發生,如請求系統資源失敗、等待某種操作的完成、新數據尚未到達或無新工作做等,則由系統自動執行阻塞原語(Block),使自己由運行狀態變為阻塞狀態。可見,進程的阻塞是進程自身的一種主動行為,也因此只有處於運行態的進程(獲得CPU),才可能將其轉為阻塞狀態。當進程進入阻塞狀態,是不占用CPU資源的。

4、文件描述符fd

  文件描述符(File descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念。 
文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核為每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者創建一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞着文件描述符展開。但是文件描述符這一概念往往只適用於UNIX、Linux這樣的操作系統。

5、緩存 I/O                                                                                                         

  緩存 I/O 又被稱作標准 I/O,大多數文件系統的默認 I/O 操作都是緩存 I/O。在 Linux 的緩存 I/O 機制中,操作系統會將 I/O 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操作系統內核的緩沖區中,然后才會從操作系統內核的緩沖區拷貝到應用程序的地址空間。用戶空間沒法直接訪問內核空間的,內核態到用戶態的數據拷貝。

緩存 I/O 的缺點: 

數據在傳輸過程中需要在應用程序地址空間和內核進行多次數據拷貝操作,這些數據拷貝操作所帶來的 CPU 以及內存開銷是非常大的。

 

本文討論的背景是Linux環境下的network IO。 

IO發生時涉及的對象和步驟:
  對於一個network IO (這里我們以read舉例),它會涉及到兩個系統對象,

  1、一個是調用這個IO的process (or thread),

  2、另一個就是系統內核(kernel)。

  當一個read操作發生時,它會經歷兩個階段:
  1、等待數據准備 (Waiting for the data to be ready)
  2、將數據從內核拷貝到進程中 (Copying the data from the kernel to the process)

  記住這兩點很重要,因為這些IO Model的區別就是在兩個階段上各有不同的情況。

 

常見的幾種IO 模型:

  •     blocking IO          (阻塞IO)
  •     nonblocking IO    (非阻塞IO)
  •     IO multiplexing    (IO多路復用)
  •     signal driven IO   (信號驅動式IO)
  •     asynchronous IO  (異步IO)

一、不常用的IO模型

  1、信號驅動IO模型(Signal-driven IO)

  使用信號,讓內核在描述符就緒時發送SIGIO信號通知應用程序,稱這種模型為信號驅動式I/O(signal-driven I/O)。

  原理圖:

  首先開啟套接字的信號驅動式I/O功能,並通過sigaction系統調用安裝一個信號處理函數。該系統調用將立即返回,我們的進程繼續工作,也就是說進程沒有被阻塞。當數據報准備好讀取時,內核就為該進程產生一個SIGIO信號。隨后就可以在信號處理函數中調用recvfrom讀取數據報,並通知主循環數據已經准備好待處理,也可以立即通知主循環,讓它讀取數據報。

  無論如何處理SIGIO信號,這種模型的優勢在於等待數據報到達期間進程不被阻塞。主循環可以繼續執行 ,只要等到來自信號處理函數的通知:既可以是數據已准備好被處理,也可以是數據報已准備好被讀取。

 

二、常用的四種IO模型:

1、 blocking IO(阻塞IO模型)

原理圖:

 

示例:一收一發程序會進入死循環

 server.py

 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*- 
 3 #Author: nulige
 4 
 5 import socket
 6 
 7 sk=socket.socket()
 8 
 9 sk.bind(("127.0.0.1",8080))
10 
11 sk.listen(5)
12 
13 while 1:
14     conn,addr=sk.accept()
15 
16     while 1:
17         conn.send("hello client".encode("utf8"))
18         data=conn.recv(1024)
19         print(data.decode("utf8"))

client.py

 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*- 
 3 #Author: nulige
 4 
 5 import socket
 6 
 7 sk=socket.socket()
 8 
 9 sk.connect(("127.0.0.1",8080))
10 
11 while 1:
12     data=sk.recv(1024)
13     print(data.decode("utf8"))
14     sk.send(b"hello server")

  當用戶進程調用了recvfrom這個系統調用,kernel就開始了IO的第一個階段:准備數據。對於network io來說,很多時候數據在一開始還沒有到達(比如,還沒有收到一個完整的UDP包),這個時候kernel就要等待足夠的數據到來。而在用戶進程這邊,整個進程會被阻塞。當kernel一直等到數據准備好了,它就會將數據從kernel中拷貝到用戶內存,然后kernel返回結果,用戶進程才解除block的狀態,重新運行起來。

所以,blocking IO的特點就是在IO執行的兩個階段都被block了。

 

2、non-blocking IO(非阻塞IO)

原理圖:

  從圖中可以看出,當用戶進程發出read操作時,如果kernel中的數據還沒有准備好,那么它並不會block用戶進程,而是立刻返回一個error。從用戶進程角度講 ,它發起一個read操作后,並不需要等待,而是馬上就得到了一個結果。用戶進程判斷結果是一個error時,它就知道數據還沒有准備好,於是它可以再次發送read操作。一旦kernel中的數據准備好了,並且又再次收到了用戶進程的system call,那么它馬上就將數據拷貝到了用戶內存,然后返回。
所以,用戶進程其實是需要不斷的主動詢問kernel數據好了沒有。

 注意:

      在網絡IO時候,非阻塞IO也會進行recvform系統調用,檢查數據是否准備好,與阻塞IO不一樣,”非阻塞將大的整片時間的阻塞分成N多的小的阻塞, 所以進程不斷地有機會 ‘被’ CPU光顧”。即每次recvform系統調用之間,cpu的權限還在進程手中,這段時間是可以做其他事情的,

      也就是說非阻塞的recvform系統調用調用之后,進程並沒有被阻塞,內核馬上返回給進程,如果數據還沒准備好,此時會返回一個error。進程在返回之后,可以干點別的事情,然后再發起recvform系統調用。重復上面的過程,循環往復的進行recvform系統調用。這個過程通常被稱之為輪詢。輪詢檢查內核數據,直到數據准備好,再拷貝數據到進程,進行數據處理。需要注意,拷貝數據整個過程,進程仍然是屬於阻塞的狀態。

示例:  

服務端:

 1 import time
 2 import socket
 3 sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
 4 sk.bind(('127.0.0.1',6667))
 5 sk.listen(5)
 6 sk.setblocking(False)  #設置成非阻塞狀態  7 while True:
 8     try:  
 9         print ('waiting client connection .......')
10         connection,address = sk.accept()   # 進程主動輪詢
11         print("+++",address)
12         client_messge = connection.recv(1024)
13         print(str(client_messge,'utf8'))
14         connection.close()
15     except Exception as e:  #捕捉錯誤 16         print (e)
17         time.sleep(4)  #每4秒打印一個捕捉到的錯誤

客戶端:

 1 import time
 2 import socket
 3 sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
 4 
 5 while True:
 6     sk.connect(('127.0.0.1',6667))
 7     print("hello")
 8     sk.sendall(bytes("hello","utf8"))
 9     time.sleep(2)
10     break

缺點:

1、發送了太多系統調用數據

2、數據處理不及時

 

3、IO multiplexing(IO多路復用)

  IO multiplexing這個詞可能有點陌生,但是如果我說select,epoll,大概就都能明白了。有些地方也稱這種IO方式為event driven IO。我們都知道,select/epoll的好處就在於單個process就可以同時處理多個網絡連接的IO。它的基本原理就是select/epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。

IO多路復用的三種方式:

1、select--->效率最低,但有最大描述符限制,在linux為1024。

2、poll  ---->和select一樣,但沒有最大描述符限制。

3、epoll  --->效率最高,沒有最大描述符限制,支持水平觸發與邊緣觸發。

 IO多路復用的優勢同時可以監聽多個連接,用的是單線程,利用空閑時間實現並發。

注意:

Linux系統: select、poll、epoll

Windows系統:select

Mac系統:select、poll

 

原理圖:

  當用戶進程調用了select,那么整個進程會被block,而同時,kernel會“監視”所有select負責的socket,當任何一個socket中的數據准備好了,select就會返回。這個時候用戶進程再調用read操作,將數據從kernel拷貝到用戶進程。

  這個圖和blocking IO的圖其實並沒有太大的不同,事實上,還更差一些。因為這里需要使用兩個system call (select 和 recvfrom),而blocking IO只調用了一個system call (recvfrom)。但是,用select的優勢在於它可以同時處理多個connection。(多說一句。所以,如果處理的連接數不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優勢並不是對於單個連接能處理得更快,而是在於能處理更多的連接。)

  在IO multiplexing Model中,實際中,對於每一個socket,一般都設置成為non-blocking,但是,如上圖所示,整個用戶的process其實是一直被block的。只不過process是被select這個函數block,而不是被socket IO給block。

  注意1:select函數返回結果中如果有文件可讀了,那么進程就可以通過調用accept()或recv()來讓kernel將位於內核中准備到的數據copy到用戶區。

  注意2: select的優勢在於可以處理多個連接,不適用於單個連接

示例:

 server.py

 1 #server.py
 2 
 3 import socket
 4 import select
 5 sk=socket.socket()
 6 sk.bind(("127.0.0.1",9904))
 7 sk.listen(5)
 8 
 9 while True:
10     # sk.accept() #文件描述符
11     r,w,e=select.select([sk,],[],[],5)  #輸入列表,輸出列表,錯誤列表,5: 是監聽5秒
12     for i in r:   #[sk,]
13         conn,add=i.accept()
14         print(conn)
15         print("hello")
16     print('>>>>>>')

client.py

 1 import socket
 2 
 3 sk=socket.socket()
 4 
 5 sk.connect(("127.0.0.1",9904))
 6 
 7 while 1:
 8     inp=input(">>").strip()
 9     sk.send(inp.encode("utf8"))
10     data=sk.recv(1024)
11     print(data.decode("utf8"))

 

IO多路復用中的兩種觸發方式:

  水平觸發:如果文件描述符已經就緒可以非阻塞的執行IO操作了,此時會觸發通知.允許在任意時刻重復檢測IO的狀態, 沒有必要每次描述符就緒后盡可能多的執行IO.select,poll就屬於水平觸發。
  邊緣觸發:如果文件描述符自上次狀態改變后有新的IO活動到來,此時會觸發通知.在收到一個IO事件通知后要盡可能 多的執行IO操作,因為如果在一次通知中沒有執行完IO那么就需要等到下一次新的IO活動到來才能獲取到就緒的描述 符.信號驅動式IO就屬於邊緣觸發。

  epoll:即可以采用水平觸發,也可以采用邊緣觸發。

1、水平觸發

  只有高電平或低電平的時候才觸發

  1-----高電平---觸發

      0-----低電平---不觸發

示例:

server服務端 

 1 #水平觸發
 2 import socket
 3 import select
 4 sk=socket.socket()
 5 sk.bind(("127.0.0.1",9904))
 6 sk.listen(5)
 7 
 8 while True:
 9     r,w,e=select.select([sk,],[],[],5)  #input輸入列表,output輸出列表,erron錯誤列表,5: 是監聽5秒
10     for i in r:   #[sk,]
11         print("hello")
12 
13     print('>>>>>>')

client客戶端

 1 import socket
 2 
 3 sk=socket.socket()
 4 
 5 sk.connect(("127.0.0.1",9904))
 6 
 7 while 1:
 8     inp=input(">>").strip()
 9     sk.send(inp.encode("utf8"))
10     data=sk.recv(1024)
11     print(data.decode("utf8"))

 

2、邊緣觸發

1---------高電平--------觸發

0---------低電平--------觸發

 

IO多路復用優勢:同時可以監聽多個連接

示例:select可以監控多個對象

服務端

 1 #優勢
 2 import socket
 3 import select
 4 sk=socket.socket()
 5 sk.bind(("127.0.0.1",9904))
 6 sk.listen(5)
 7 inp=[sk,]
 8 
 9 while True:
10     r,w,e=select.select(inp,[],[],5)  #[sk,conn],5是每隔幾秒監聽一次
11 
12     for i in r:   #[sk,]
13         conn,add=i.accept()  #發送系統調用 14         print(conn)
15         print("hello")
16         inp.append(conn)
17         # conn.recv(1024)
18     print('>>>>>>')

客戶端:

 1 import socket
 2 
 3 sk=socket.socket()
 4 
 5 sk.connect(("127.0.0.1",9904))
 6 
 7 while 1:
 8     inp=input(">>").strip()
 9     sk.send(inp.encode("utf8"))
10     data=sk.recv(1024)
11     print(data.decode("utf8"))

 

多了一個判斷,用select方式實現的並發

示例:實現並發聊天功能 (select+IO多路復用,實現並發)

服務端:

 1 import socket
 2 import select
 3 sk=socket.socket()
 4 sk.bind(("127.0.0.1",8801))
 5 sk.listen(5)
 6 inputs=[sk,]
 7 while True:  #監聽sk和conn  8     r,w,e=select.select(inputs,[],[],5) #conn發生變化,sk不變化就走else  9     print(len(r))
10     #判斷sk or conn 誰發生了變化
11     for obj in r:
12         if obj==sk:
13             conn,add=obj.accept()
14             print(conn)
15             inputs.append(conn)
16         else:
17             data_byte=obj.recv(1024)
18             print(str(data_byte,'utf8'))
19             inp=input('回答%s號客戶>>>'%inputs.index(obj))
20             obj.sendall(bytes(inp,'utf8'))
21 
22     print('>>',r)

客戶端:

1 import socket
2 sk=socket.socket()
3 sk.connect(('127.0.0.1',8801))
4 
5 while True:
6     inp=input(">>>>")
7     sk.sendall(bytes(inp,"utf8"))
8     data=sk.recv(1024)
9     print(str(data,'utf8'))

執行結果:

先運行服務端,再運行多個客戶端,就可以聊天啦。(可以接收多個客戶端消息)

 1 #server
 2 >> [<socket.socket fd=276, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8801)>]
 3 1
 4 hello
 5 回答1號客戶>>>word
 6 >> [<socket.socket fd=344, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8801), raddr=('127.0.0.1', 54388)>]
 7 1
 8 
 9 #clinet
10 >>>>hello
11 word
View Code

 

4、Asynchronous I/O(異步IO)

  用戶進程發起read操作之后,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當它受到一個asynchronous read之后,首先它會立刻返回,所以不會對用戶進程產生任何block。然后,kernel會等待數據准備完成,然后將數據拷貝到用戶內存,當這一切都完成之后,kernel會給用戶進程發送一個signal,告訴它read操作完成了。

異步最大特點:全程無阻塞

 

synchronous IO(同步IO)和asynchronous IO(異步IO)的區別:

  •  A synchronous I/O operation causes the requesting process to be blocked until that I/O operationcompletes;
  •  An asynchronous I/O operation does not cause the requesting process to be blocked; 

      兩者的區別就在於synchronous IO做”IO operation”的時候會將process阻塞。(有一丁點阻塞,都是同步IO)按照這個定義,之前所述的blocking IO,non-blocking IO,IO multiplexing都屬於synchronous IO(同步IO)。

 

同步IO:包括 blocking IO、non-blocking、select、poll、epoll(故:epool只是偽異步而已)(有阻塞)

異步IO:包括:asynchronous  (無阻塞)

 

五種IO模型比較:

  經過上面的介紹,會發現non-blocking IO和asynchronous IO的區別還是很明顯的。在non-blocking IO中,雖然進程大部分時間都不會被block,但是它仍然要求進程去主動的check,並且當數據准備完成以后,也需要進程主動的再次調用recvfrom來將數據拷貝到用戶內存。而asynchronous IO則完全不同。它就像是用戶進程將整個IO操作交給了他人(kernel)完成,然后他人做完后發信號通知。在此期間,用戶進程不需要去檢查IO操作的狀態,也不需要主動的去拷貝數據。

 

5、selectors模塊應用

python封裝好的模塊:selectors

selectors模塊: 會選擇一個最優的操作系統實現方式

示例:

select_module.py

 1 import selectors
 2 import socket
 3 
 4 sel = selectors.DefaultSelector()
 5 
 6 def accept(sock, mask):
 7     conn, addr = sock.accept()  # Should be ready
 8     print('accepted', conn, 'from', addr)
 9     conn.setblocking(False)  #設置成非阻塞
10     sel.register(conn, selectors.EVENT_READ, read) #conn綁定的是read
11 
12 def read(conn, mask):
13     try:
14         data = conn.recv(1000)  # Should be ready
15         if not data:
16             raise Exception
17         print('echoing', repr(data), 'to', conn)
18         conn.send(data)  # Hope it won't block
19     except Exception as e:
20         print('closing', conn)
21         sel.unregister(conn)  #解除注冊
22         conn.close()
23 
24 sock = socket.socket()
25 sock.bind(('localhost', 8090))
26 sock.listen(100)
27 sock.setblocking(False)
28 #注冊
29 sel.register(sock, selectors.EVENT_READ, accept)
30 print("server....")
31 
32 while True:
33     events = sel.select() #監聽[sock,conn1,conn2]
34     print("events",events)
35     #拿到2個元素,一個key,一個mask
36     for key, mask in events:
37         # print("key",key)
38         # print("mask",mask)
39         callback = key.data  #綁定的是read函數
40         # print("callback",callback)
41         callback(key.fileobj, mask)  #key.fileobj=sock,conn1,conn2

client.py

 1 import socket
 2 
 3 sk=socket.socket()
 4 
 5 sk.connect(("127.0.0.1",8090))
 6 while 1:
 7     inp=input(">>>")
 8     sk.send(inp.encode("utf8")) #發送內容
 9     data=sk.recv(1024)  #接收信息
10     print(data.decode("utf8"))  #打印出來

執行結果:

先運行select_module.py,再運行clinet.py

 1 #server
 2 
 3 server....
 4 events [(SelectorKey(fileobj=<socket.socket fd=312, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090)>, fd=312, events=1, data=<function accept at 0x01512F60>), 1)]
 5 accepted <socket.socket fd=376, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57638)> from ('127.0.0.1', 57638)
 6 events [(SelectorKey(fileobj=<socket.socket fd=376, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57638)>, fd=376, events=1, data=<function read at 0x015C26A8>), 1)]
 7 echoing b'hello' to <socket.socket fd=376, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57638)>
 8 events [(SelectorKey(fileobj=<socket.socket fd=312, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090)>, fd=312, events=1, data=<function accept at 0x01512F60>), 1)]
 9 accepted <socket.socket fd=324, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57675)> from ('127.0.0.1', 57675)
10 events [(SelectorKey(fileobj=<socket.socket fd=324, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57675)>, fd=324, events=1, data=<function read at 0x015C26A8>), 1)]
11 echoing b'uuuu' to <socket.socket fd=324, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57675)>
12 events [(SelectorKey(fileobj=<socket.socket fd=324, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57675)>, fd=324, events=1, data=<function read at 0x015C26A8>), 1)]
13 closing <socket.socket fd=324, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57675)>
14 events [(SelectorKey(fileobj=<socket.socket fd=312, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090)>, fd=312, events=1, data=<function accept at 0x01512F60>), 1)]
15 accepted <socket.socket fd=324, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57876)> from ('127.0.0.1', 57876)
16 events [(SelectorKey(fileobj=<socket.socket fd=324, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57876)>, fd=324, events=1, data=<function read at 0x015C26A8>), 1)]
17 echoing b'welcome' to <socket.socket fd=324, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57876)>
18 
19 #clinet (啟動兩個client)
20 >>>hello
21 hello
22 
23 >>>welcome
24 welcome
View Code

 

 6、I/O多路復用的應用場景

(1)當客戶處理多個描述字時(一般是交互式輸入和網絡套接口),必須使用I/O復用。

(2)當一個客戶同時處理多個套接口時,而這種情況是可能的,但很少出現。

(3)如果一個TCP服務器既要處理監聽套接口,又要處理已連接套接口,一般也要用到I/O復用。

(4)如果一個服務器即要處理TCP,又要處理UDP,一般要使用I/O復用。

(5)如果一個服務器要處理多個服務或多個協議,一般要使用I/O復用。

'''與多進程和多線程技術相比,I/O多路復用技術的最大優勢是系統開銷小,系統不必創建進程/線程,也不必維護這些進程/線程,從而大大減小了系統的開銷。'''

最后,再舉幾個不是很恰當的例子來說明這四個IO Model:
有A,B,C,D四個人在釣魚:
A用的是最老式的魚竿,所以呢,得一直守着,等到魚上鈎了再拉桿;【阻塞】
B的魚竿有個功能,能夠顯示是否有魚上鈎(這個顯示功能一直去判斷魚是否上鈎),所以呢,B就和旁邊的MM聊天,隔會再看看有沒有魚上鈎,有的話就迅速拉桿;【非阻塞】
C用的魚竿和B差不多,但他想了一個好辦法,就是同時放好幾根魚竿,然后守在旁邊,一旦有顯示說魚上鈎了,它就將對應的魚竿拉起來;【同步】
D是個有錢人,干脆雇了一個人幫他釣魚,一旦那個人把魚釣上來了,就給D發個短信(消息回掉機制,主動告知)。【異步】

 

作業:

1、使用IO多路復用,做一個ftp的上傳和下載作業

要求:實現多用戶操作,可以同時上傳和下載(不能用socketserver),有顯示進度條。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM