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

 1、事件驅動模型介紹                                                                                                       javascript

一、傳統的編程模式 html

例如:線性模式大體流程java

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

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

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

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

 與上面傳統編程模式不一樣,事件驅動程序在啓動以後,就在那等待,等待什麼呢?等待被事件觸發。傳統編程下也有「等待」的時候,好比在代碼塊D中,你定義了一個input(),須要用戶輸入數據。但這與下面的等待不一樣,傳統編程的「等待」,好比input(),你做爲程序編寫者是知道或者強制用戶輸入某個東西的,或許是數字,或許是文件名稱,若是用戶輸入錯誤,你還須要提醒他,並請他從新輸入。事件驅動程序的等待則是徹底不知道,也不強制用戶輸入或者幹什麼。只要某一事件發生,那程序就會作出相應的「反應」。這些事件包括:輸入信息、鼠標、敲擊鍵盤上某個鍵還有系統內部定時器觸發。緩存

二、事件驅動模型安全

一般,咱們寫服務器處理模型的程序時,有如下幾種模型:服務器

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

三、第三種就是協程、事件驅動的方式,通常廣泛認爲第(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. CPU資源浪費,可能鼠標點擊的頻率很是小,可是掃描線程仍是會一直循環檢測,這會形成不少的CPU資源浪費;若是掃描鼠標點擊的接口是阻塞的呢?
  2. 若是是堵塞的,又會出現下面這樣的問題,若是咱們不但要掃描鼠標點擊,還要掃描鍵盤是否按下,因爲掃描鼠標時被堵塞了,那麼可能永遠不會去掃描鍵盤;
  3. 若是一個循環須要掃描的設備很是多,這又會引來響應時間的問題; 
    因此,該方式是很是很差的。

二、事件驅動模型 

  目前大部分的UI編程都是事件驅動模型,如不少UI平臺都會提供onClick()事件,這個事件就表明鼠標按下事件。事件驅動模型大致思路以下:

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

什麼是事件驅動模型 ?

  目前大部分的UI編程都是事件驅動模型,如不少UI平臺都會提供onClick()事件,這個事件就表明鼠標按下事件。事件驅動模型大致思路以下:

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

 

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

 

2、IO模型準備                                                                                                            

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

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

一、用戶空間和內核空間

例如:採用虛擬存儲器,對於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),而且不能控制由用戶態轉成內核態。

二、進程切換                                                                                                        

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

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

三、進程的阻塞

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

四、文件描述符fd

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

五、緩存 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舉例),它會涉及到兩個系統對象,

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

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

  當一個read操做發生時,它會經歷兩個階段:
  一、等待數據準備 (Waiting for the data to be ready)
  二、將數據從內核拷貝到進程中 (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)

1、不經常使用的IO模型

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

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

  原理圖:

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

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

 

2、經常使用的四種IO模型:

一、 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了。

 

二、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

缺點:

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

二、數據處理不及時

 

三、IO multiplexing(IO多路複用)

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

IO多路複用的三種方式:

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

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

三、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-----高電平---觸發

      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"))

 

二、邊緣觸發

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

 

四、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操做的狀態,也不須要主動的去拷貝數據。

 

五、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發個短信(消息回掉機制,主動告知)。【異步】

 

做業:

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

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

相關文章
相關標籤/搜索