python網絡編程——IO多路複用之select

1 IO多路複用的概念  

      原生socket客戶端在與服務端創建鏈接時,即服務端調用accept方法時是阻塞的,同時服務端和客戶端在收發數據(調用recv、send、sendall)時也是阻塞的。原生socket服務端在同一時刻只能處理一個客戶端請求,即服務端不能同時與多個客戶端進行通訊,實現併發,致使服務端資源閒置(此時服務端只佔據 I/O,CPU空閒)。html

    如今的需求是:咱們要讓多個客戶端鏈接至服務器端,並且服務器端須要處理來自多個客戶端請求。很明顯,原生socket實現不了這種需求,此時咱們該採用什麼方式來處理呢?python

    解決方法:採用I/O多路複用機制。在python網絡編程中,I/O多路複用機制就是用來解決多個客戶端鏈接請求服務器端,而服務器端能正常處理並響應給客戶端的一種機制。書面上來講,就是經過1種機制:能夠同時監聽多個文件描述符,一旦描述符就緒,可以通知程序進行相應的讀寫操做。linux

    I/O多路複用是指:經過一種機制,能夠監視多個描述符,一旦某個描述符就緒(通常是讀就緒或者寫就緒),可以通知程序進行相應的讀寫操做。程序員

    1.1 linux中的IO多路複用django

(1)select編程

    select最先於1983年出如今4.2BSD中,它經過一個select()系統調用來監視多個文件描述符的數組,當select()返回後,該數組中就緒的文件描述符便會被內核修改標誌位,使得進程能夠得到這些文件描述符從而進行後續的讀寫操做。windows

    select目前幾乎在全部的平臺上支持,其良好跨平臺支持也是它的一個優勢,事實上從如今看來,這也是它所剩很少的優勢之一。數組

    select的一個缺點在於單個進程可以監視的文件描述符的數量存在最大限制,在Linux上通常爲1024,不過能夠經過修改宏定義甚至從新編譯內核的方式提高這一限制。瀏覽器

    另外,select()所維護的存儲大量文件描述符的數據結構,隨着文件描述符數量的增大,其複製的開銷也線性增加。同時,因爲網絡響應時間的延遲使得大 量TCP鏈接處於非活躍狀態,但調用select()會對全部socket進行一次線性掃描,因此這也浪費了必定的開銷。安全

(2)poll

    poll在1986年誕生於System V Release 3,它和select在本質上沒有多大差異,可是poll沒有最大文件描述符數量的限制。

    poll和select一樣存在一個缺點就是,包含大量文件描述符的數組被總體複製於用戶態和內核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨着文件描述符數量的增長而線性增大。

    另外,select()和poll()將就緒的文件描述符告訴進程後,若是進程沒有對其進行IO操做,那麼下次調用select()和poll()的時候 將 再次報告這些文件描述符,因此它們通常不會丟失就緒的消息,這種方式稱爲水平觸發(Level Triggered)。

(3)epoll

    直到Linux2.6纔出現了由內核直接支持的實現方法,那就是epoll,它幾乎具有了以前所說的一切優勢,被公認爲Linux2.6下性能最好的多路I/O就緒通知方法。

    epoll能夠同時支持水平觸發和邊緣觸發(Edge Triggered,只告訴進程哪些文件描述符剛剛變爲就緒狀態,它只說一遍,若是咱們沒有采起行動,那麼它將不會再次告知,這種方式稱爲邊緣觸發),理論上邊緣觸發的性能要更高一些,可是代碼實現至關複雜。

    epoll一樣只告知那些就緒的文件描述符,並且當咱們調用epoll_wait()得到就緒文件描述符時,返回的不是實際的描述符,而是一個表明就緒描 述符數量的 值,你只須要去epoll指定的一個數組中依次取得相應數量的文件描述符便可,這裏也使用了內存映射(mmap)技術,這樣便完全省掉了這些文件描述符在 系統調用時複製的開銷。

    另外一個本質的改進在於epoll採用基於事件的就緒通知方式。在select/poll 中,進程只有在調用必定的方法後,內核纔對全部監視的文件描述符進行掃描,而epoll事先經過epoll_ctl()來註冊一個文件描述符,一旦基於某 個文件描述符就緒時,內核會採用相似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便獲得通知。

     1.2 python中的IO多路複用

Python中有一個select模塊,其中提供了:select、poll、epoll三個方法,分別調用系統的 select,poll,epoll從而實現IO多路複用。

    Windows Python:提供: select

    Mac Python:提供: select

    Linux Python:提供: select、poll、epoll

2 python中的IO多路複用

    IO多路複用有些地方也稱這種IO方式爲事件驅動IO(event driven IO)。咱們都知道,select/epoll的好處就在於單個process就能夠同時處理多個網絡鏈接的IO。它的基本原理就是select /epoll這個function會不斷的輪詢所負責的全部socket,當某個socket有數據到達了,就通知用戶進程。它的流程如圖:

      Python中有一個select模塊,其中提供了:select、poll、epoll三個方法(根據系統的不一樣,select模塊提供了不一樣的方法,在linux中select模塊提供了所有三種方法),分別調用系統的 select,poll,epoll從而實現IO多路複用。

    注意:網絡操做、文件操做、終端操做等均屬於IO操做,對於windows只支持Socket操做,其餘系統支持其餘IO操做,可是沒法檢測普通文件操做,自動檢測文件是否已經變化。普通文件操做,全部系統都是完成不了的,普通文件是屬於I/O操做!可是對於python來講文件變動,python是監控不了的,因此咱們能用的只有是「終端的輸入輸出,Socket的輸入輸出」

   2.1 select方法

    select 的中文含義是」選擇「,select機制也如其名,監聽一些 server 關心的套接字、文件等對象,關注他們是否可讀、可寫、發生異常等事件。一旦出現某個 select 關注的事件,select 會對相應的套接字或文件進行特定的處理,這就是 select 機制最主要的功能。

  select 機制能夠只使用一個進程/線程來處理多個socket或其餘對象,所以又被稱爲I/O複用。

  關於select機制的進程阻塞形式,與普通的套接字略有不一樣。socket對象可能阻塞在accept(),recvfrom()等方法上,以recvfrom()方法爲例,當執行到socket.recvfrom()這一句時,就會調用一個系統調用詢問內核:client/server發來的數據包準備好了沒?此時從進程空間切換到內核地址空間,內核可能須要等數據包徹底到達,而後將數據複製到程序的地址空間後,recvfrom()纔會返回,接下來進程繼續執行,對讀取到的數據進行必要的處理。

  而使用select函數編程時,一樣針對上面的recvfrom()方法,進程會阻塞在select()調用上,等待出現一個或多個套接字對象知足可讀事件,當內核將數據準備好後,select()返回某個套接字對象可讀這一條件,隨後再調用recvfrom()將數據包從內核複製到進程地址空間。

  因此可見,若是僅僅從單個套接字的處理來看,select()反倒性能更低,由於select機制使用兩個系統調用。但select機制的優點就在於它能夠同時等待多個fd就緒,而當某個fd發生知足咱們關心的事件時,就對它執行特定的操做。

句柄列表11, 句柄列表22, 句柄列表33 = select.select(句柄列表1, 句柄列表2, 句柄列表3, 超時時間) 參數: 可接受四個參數(前三個必須) 返回值:三個列表 select方法用來監視文件句柄,若是句柄發生變化,則獲取該句柄。 1、當 參數1 序列中的句柄發生可讀時(accetp和read),則獲取發生變化的句柄並添加到 返回值1 序列中 2、當 參數2 序列中含有句柄時,則將該序列中全部的句柄添加到 返回值2 序列中 3、當 參數3 序列中的句柄發生錯誤時,則將該發生錯誤的句柄添加到 返回值3 序列中 4、當 超時時間未設置,則select會一直阻塞,直到監聽的句柄發生變化 五、當 超時時間=1時,那麼若是監聽的句柄均無任何變化,則select會阻塞1秒,以後返回三個空列表,若是監聽的句柄有變化,則直接執行。

      因爲select()接口能夠同時對多個句柄進行讀狀態、寫狀態和錯誤狀態的探測,因此能夠很容易構建爲多個客戶端提供獨立問答服務的服務器系統。這裏須要指出的是,客戶端的一個connect()操做,將在服務器端激發一個「可讀事件」,因此 select() 也能探測來自客戶端的connect()行爲。
    上述模型中,最關鍵的地方是如何動態維護select()的三個參數。程序員須要檢查對應的返回值列表,以肯定到底哪些句柄發生了事件。因此若是select()發現某句柄捕捉到了「可讀事件」,服務器程序應及時作recv()操做,並根據接收到的數據準備好待發送數據,並將對應的句柄值加入句柄序列1,準備下一次的「可寫事件」的select()探測。一樣,若是select()發現某句柄捕捉到「可寫事件」,則程序應及時作send()操做,並準備好下一次的「可讀事件」探測準備。

 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 
 4 import select
 5 import sys
 6 
 7 while True:
 8     readable, writeable, error = select.select([sys.stdin,],[],[],1)
 9     '''select.select([sys.stdin,],[],[],1)用到I/O多路複用,第一個參數是列表,sys.stdin是系統標準輸入的文件描述符, 就是打開標準輸入終端返回的文件描述符,一旦終端有輸入操做,select就感知sys.stdin描述符的變化,那麼會將變化的描述符sys.stdin添加到返回值readable中;若是終端一直沒有輸入,那麼readable他就是一個空列表
10     '''
11     if sys.stdin in readable:
12         print 'select get stdin',sys.stdin.readline()
13 '''
14 注:
15 一、[sys.stdin,]之後無論是列表仍是元組,在最後一個元素的後面建議增長一個逗號,(1,) | (1) 這兩個有區別嗎?是否是第二個更像方法的調用或者函數的調用,加個,是否是更容易分清楚。還有就是在之後寫django的配置文件的時候,他是必需要加的。
16 二、select的第一個參數就是要監聽的文件句柄,只要監聽的文件句柄有變化,那麼就會將其加入到返回值readable列表中。
17 三、select最後一個參數1是超時時間,當執行select時,若是監聽的文件句柄沒有變化,則會阻塞1秒,而後向下繼續執行;默認timeout=None,就是會一直阻塞,直到感知到變化
18 '''
19 '''
20 when runing the program get error :
21 Traceback (most recent call last):
22   File "E:/study/GitHub/homework/tianshuai/share_3_select_socket.py", line 8, in <module>
23     readable, writeable, error = select.select([sys.stdin,],[],[],1)
24 select.error: (10093, 'Either the application has not called WSAStartup, or WSAStartup failed')
25 
26 when windows only use select socket !!!!!
27 '''
實例1利用select監聽終端輸入
 1 #/usr/bin/env python
 2 #-*- coding:utf-8 -*-
 3 
 4 import time
 5 import socket
 6 import select
 7 
 8 #生成socket對象
 9 sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
10 #綁定IP和端口
11 sk.bind(('127.0.0.1',6666))
12 #監聽,並設置最大鏈接數爲5
13 sk.listen(5)
14 #設置setblocking爲False,即非阻塞模式,accept將不在阻塞,若是沒有收到請求就會報錯
15 sk.setblocking(False) 
16 
17 while True:
18     rlist, wlist, elist = select.select([sk,],[],[],2)  #監聽第一個列表的文件描述符,若是其中有文件描述符發生改變,則捕獲並放到rlist中
19     for r in rlist:   #若是rlist非空將執行,不然不執行
20         conn,addr = r.accept()   #創建鏈接,生成客戶端的socket對象以及IP地址和端口號
21         print addr
實例2:利用select監聽瀏覽器訪問

執行程序,在瀏覽器中輸入地址:127.0.0.1:6666  

執行結果以下:

('127.0.0.1', 42510)
 1 #/usr/bin/env python
 2 #-*- coding:utf-8 -*-
 3 
 4 import time
 5 import socket
 6 import select
 7 
 8 #生成socket對象
 9 sk1 = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
10 #綁定IP和端口
11 sk1.bind(('127.0.0.1',6666))
12 #監聽並設置最大鏈接數
13 sk1.listen(5)
14 sk1.setblocking(False) #設置setblocking爲False,accept將不在阻塞,若是沒有收到請求就會報錯,即非阻塞模式
15 
16 sk2 = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
17 sk2.bind(('127.0.0.1',7777))
18 sk2.listen(5)
19 sk2.setblocking(False) #設置非阻塞模式
20 
21 while True:
22     rlist, wlist, elist = select.select([sk1,sk2,],[],[],2)  #監聽第一個列表的文件描述符,若是其中有文件描述符發生改變,則捕獲並放到rlist中
23     for r in readable_list:  
24         conn,address = r.accept()
25         print address
實例3:利用select監聽多端口

執行程序,在瀏覽器中分別輸入地址:127.0.0.1:6666  127.0.0.1:7777

執行結果以下:

('127.0.0.1', 54509)
('127.0.0.1', 53458)
 1 #/usr/bin/env python
 2 #-*- coding:utf-8 -*-
 3 
 4 import time
 5 import socket
 6 import select
 7 
 8 sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
 9 sk.bind(('127.0.0.1',6666))
10 sk.listen(5)
11 sk.setblocking(False) #非阻塞
12 inputs = [sk,] #構造select第一個參數
13 #緣由:看上例conn是客戶端對象,客戶是一直鏈接着呢,鏈接的時候狀態變了,鏈接上以後,仍是服務端的socket有關嗎?是否是的把他改成動態的?
14 
15 while True:
16     rlist, wlist, elist = select.select(inputs,[],[],1)  #把第一個參數設爲列表動態的添加
17     time.sleep(2) #測試使用
18     print "inputs list :",inputs     #打印inputs列表,查看執行變化
19     print "file descriptor :",readable_list #打印rlist ,查看執行變化
20 
21     for r in rlist:
22         #當客戶端第1次鏈接服務端時
23         if r == sk:  
24             conn,address = r.accept()
25             inputs.append(conn)
26             print address
27         else:
28         # 當客戶端鏈接上服務端以後,再次發送數據時
29             client_data = r.recv(1024)
30             r.sendall(client_data)
實例4:利用select實現僞同時處理多個Socket客戶端請求—服務端
 1 #!/usr/bin/env python
 2 #-*- coding:utf-8 -*-
 3 
 4 import socket
 5 
 6 client = socket.socket()
 7 client.connect(('127.0.0.1',6666))
 8 client.settimeout(5)
 9 
10 while True:
11     client_input = raw_input('please input message:').strip()
12     client.sendall(client_input)
13     server_data = client.recv(1024)
14     print server_data
實例4:利用select實現僞同時處理多個Socket客戶端請求—客戶端
 1 '''
 2 第1點:
 3 #定義1個包含服務端文件句柄sk的inputs列表,服務端利用select監聽到客戶端的請求時(目前對於服務端來講,就是accept和recv請求;對於客戶端來講就是connect、send、sendall請求),
 4 
 5 第2點:
 6 #最開始服務端select會監聽inputs=[sk,],當有新客戶端connect請求時,select就會監聽到服務端文件句柄sk發生了改變,此時select會把sk賦值給第1個返回值rlist,即rlist=[sk,],
 7 
 8 #執行for循環,當r==sk時,即表示是服務端文件句柄sk發生了變化,此時服務端就會執行accpet,同時返回客戶端文件句柄conn以及其IP地址和端口port,再把conn加入到inputs列表中,
 9 #便於後面服務端對該客戶端的其它請求的監聽;當r!=sk時,說明是監聽到某個客戶端文件句柄發生了變化,便是以前監聽到的客戶端有新的請求(發送了數據),此時服務端就會recv該數據
10 #若是無任何請求,那麼select就不會監聽到任何變化,即rlist = []
11 
12 #程序執行的打印結果:
13 
14 #1 有1個客戶端鏈接,此後無操做
15 inputs list : [<socket._socketobject object at 0x0000000002C66798>]      #初始化時inputs包含了sk對象
16 file descriptor : [<socket._socketobject object at 0x0000000002C66798>]  #當客戶端有conncet請求時,sk就會發生變化,此時rlist=[sk,]
17 ('127.0.0.1', 62495)   #客戶端的IP地址和端口號
18 inputs list : [<socket._socketobject object at 0x0000000002C66798>, <socket._socketobject object at 0x0000000002C66800>]  #第2次循環時,inputs = [sk,conn1,]
19 file descriptor : [] #第2次循環時rlist = [],由於客戶端無任何操做,此時select沒有監聽到inputs中的文件句柄有任何變化,因此rlist爲空
20 
21 #2 有1個新客戶端鏈接,即有客戶端向服務端發起了一個connect請求,此時sk發生了變化,那麼如今inputs = [sk,conn1,conn2]  rlist = [sk]
22 #本次循環完成以後再循環的時候inputs = [sk,conn1,conn2,] rlist = [],由於咱們沒有繼續作操做
23 
24 #第1個鏈接請求
25 inputs list : [<socket._socketobject object at 0x0000000002C56798>]  #默認只有sk
26 file descriptor : []
27 inputs list : [<socket._socketobject object at 0x0000000002C56798>]  
28 file descriptor : [<socket._socketobject object at 0x0000000002C56798>] #監聽到sk發生了變化,表示有新客戶端請求
29 ('127.0.0.1', 62539)
30 inputs list : [<socket._socketobject object at 0x0000000002C56798>, <socket._socketobject object at 0x0000000002C56800>]  #inputs列表變動爲了[sk,conn1]
31 file descriptor : []  #後續無操做,因此rlist=[]
32 
33 #第二個連接
34 inputs list : [<socket._socketobject object at 0x0000000002C56798>, <socket._socketobject object at 0x0000000002C56800>] 
35 file descriptor : [<socket._socketobject object at 0x0000000002C56798>] #監聽到inputs列表中的文件句柄發生了改變
36 ('127.0.0.1', 62548)
37 #加入到inputs列表中
38 inputs list : [<socket._socketobject object at 0x0000000002C56798>, <socket._socketobject object at 0x0000000002C56800>, <socket._socketobject object at 0x0000000002C56868>] 
39 file descriptor : []
40 inputs list : [<socket._socketobject object at 0x0000000002C56798>, <socket._socketobject object at 0x0000000002C56800>, <socket._socketobject object at 0x0000000002C56868>]
41 file descriptor : []
42 inputs list : [<socket._socketobject object at 0x0000000002C56798>, <socket._socketobject object at 0x0000000002C56800>, <socket._socketobject object at 0x0000000002C56868>]
43 file descriptor : []
44 '''
實例4執行過程分析
 1 #優化點:當客戶端斷開鏈接時,從inputs列表中刪除其文件描述符
 2 
 3 #!/usr/bin/env python
 4 # -*- coding:utf-8 -*-
 5 
 6 import socket
 7 import select
 8 
 9 sk1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10 sk1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
11 sk1.bind(('127.0.0.1',8002))
12 sk1.listen(5)
13 sk1.setblocking(0)
14 
15 inputs = [sk1,]
16 
17 while True:
18     readable_list, writeable_list, error_list = select.select(inputs, [], inputs, 1)
19     for r in readable_list:
20         # 當客戶端第一次鏈接服務端時
21         if sk1 == r:
22             print 'accept'
23             request, address = r.accept()
24             request.setblocking(0)
25             inputs.append(request)
26         # 當客戶端鏈接上服務端以後,再次發送數據時
27         else:
28             received = r.recv(1024)
29             # 當正常接收客戶端發送的數據時
30             if received:
31                 print 'received data:', received
32             # 當客戶端關閉程序時
33             else:
34                 inputs.remove(r)
35 
36 sk1.close()
實例5:實例4服務端的優化

    此處的Socket服務端相比與原生的Socket,他支持當某一個請求再也不發送數據時,服務器端不會等待而是能夠去處理其餘請求的數據。可是,若是每一個請求的耗時比較長時,select版本的服務器端也沒法完成同時操做。

    select參數註解

rlist, wlist, elist = select.select(inputs,[],[],1)
一、第一個參數,監聽的句柄序列,當有任一句柄變化時,select就能捕獲到並返回賦值給rlist; 二、若是第二參數有值,即只要不是空列表,select就能感知,wlist就能獲取第二個參數的值; 三、對於第三個參數,在select內部會檢測列表中的描述符在底層執行過程當中是否發生異常,若是發生異常,則把發生異常的句柄賦值給elist,
通常第三個參數和第一個參數相同 四、第四個參數是設置阻塞時間,如1秒(這個若是不寫,select會阻塞住,直到監聽的描述符發生變化才繼續往下執行)

    對於I/O多路複用,我們上面的例子就能夠了,可是爲了遵循select規範須要把讀和寫進行分離:

#rlist -- wait until ready for reading #等待直到有讀的操做 #wlist -- wait until ready for writing #等待直到有寫的操做 #xlist -- wait for an ``exceptional condition'' #等待一個錯誤的狀況

    爲了實現讀寫分離,須要構造一個字典,字典裏爲每一客戶端維護一個隊列。收到的信息放到隊列裏,而後寫的時候直接從隊列取數據

     2.2 Queue 隊列

      隊列的特色:

      一、隊列是先進先出,棧是相反的,後進先出
      二、隊列是線程安全的
1 import Queue
2 
3 q = Queue.Queue() #調用隊列生成對象
4 q.put(1)  #存放第一個值到隊列
5 q.put(2)  #存放第二個值到隊列
6 
7 print 'get frist one:',q.get() #獲取隊列的第一個值
8 print 'get second on:',q.get() #獲取隊列的第二個值
隊列取值順序
 1 q = Queue.Queue() #調用隊列生成對象
 2 
 3 q.put(1)  #存放第一個值到隊列
 4 q.put(2)  #存放第二個值到隊列
 5 
 6 a = q.get() #獲取隊列的第一個值
 7 print 'get frist one:%s' % a
 8 b = q.get() #獲取隊列的第二個值
 9 print 'get second one:%s' % b
10 c = q.get()#獲取隊列的第三個值
11 print 'get third one:%s' % c
12 
13 #結果:
14 '''
15 get frist one:1
16 get second one:2
17 #這裏一直在等待着值進來~
18 '''
當隊列爲空時,取值會阻塞
 1 q = Queue.Queue() #調用隊列生成對象
 2 
 3 q.put(1)  #存放第一個值到隊列
 4 q.put(2)  #存放第二個值到隊列
 5 
 6 a = q.get() #獲取隊列的第一個值
 7 print 'get frist one:%s' % a
 8 b = q.get() #獲取隊列的第二個值
 9 print 'get second one:%s' % b
10 c = q.get_nowait()#獲取隊列的第三個值 ,使用:get_nowait()
11 print 'get third one:%s' % c
取值時,get_nowait非阻塞,但無值時會報錯
1 q = Queue.Queue() #調用隊列生成對象 2 try: 3  q.get_nowait() 4 except Queue.Empty as f: 5 print 'The Queue is empty!'
捕獲get_nowait異常
 1 q = Queue.Queue(2) #調用隊列生成對象
 2 
 3 q.put(1)  #存放第一個值到隊列
 4 print 'put value 1 done'
 5 q.put(2)  #存放第二個值到隊列
 6 print 'put vlaue 2 done'
 7 q.put(3) #存放第三個值到隊列
 8 print 'put value 3 done'
 9 
10 
11 #結果:
12 '''
13 put value 1 done
14 put vlaue 2 done
15 #這裏會一直等待~
16 '''
存放數據時,若是隊列已滿,也會阻塞
 1 q = Queue.Queue(2) #調用隊列生成對象
 2 
 3 q.put(1)  #存放第一個值到隊列
 4 print 'put value 1 done'
 5 q.put(2)  #存放第二個值到隊列
 6 print 'put vlaue 2 done'
 7 q.put_nowait(3) #存放第三個值到隊列,若是使用put_nowait()隊列沒法存放後會報錯!
 8 print 'put value 3 done'
 9 #結果:
10 '''
11 put value 1 done
12 put vlaue 2 done
13 #這裏會一直等待~
存放數據時,put_nowait非阻塞,可是隊列沒法存放時會報錯
 1 #!/usr/bin/env python
 2 #-*- coding:utf-8 -*-
 3 
 4 import select
 5 import socket
 6 import Queue
 7 import time
 8 
 9 sk = socket.socket()
10 sk.bind(('127.0.0.1',6666))
11 sk.listen(5)
12 sk.setblocking(False) #設置非阻塞
13 inputs = [sk,]  #定義一個列表,select第一個參數監聽句柄序列,當有變更是,捕獲並把socket server加入到句柄序列中
14 outputs = [] #定義一個列表,select第二個參數監聽句柄序列,當有值時就捕獲,並加入到句柄序列
15 message = {}
16 #message的樣板信息
17 #message = {
18 #    'c1':隊列,[這裏存放着用戶C1發過來的消息]例如:[message1,message2]
19 #    'c2':隊列,[這裏存放着用戶C2發過來的消息]例如:[message1,message2]
20 #}
21 
22 while True:
23     readable_list, writeable_list, error_list = select.select(inputs,outputs,[],1)
24     #文件描述符可讀 readable_list    只有第一個參數變化時候才捕獲,並賦值給readable_list
25     #文件描述符可寫 writeable_list   只要有值,第二個參數就捕獲並賦值給writeable_list
26     #time.sleep(2)
27     print 'inputs:',inputs
28     print 'output:'
29     print 'readable_list:',readable_list
30     print 'writeable_list:',writeable_list
31     print 'message',message
32     for r in readable_list: #當readable_list有值得時候循環
33         if r == sk:  #判斷是否爲連接請求變化的是不是socket server
34             conn,addr = r.accept() #獲取請求
35             inputs.append(conn) #把客戶端對象(句柄)加入到inputs裏
36             message[conn] = Queue.Queue() #並在字典裏爲這個客戶端鏈接創建一個消息隊列
37         else:
38             client_data = r.recv(1024) #若是請求的不是sk是客戶端接收消息
39             if client_data:#若是有數據
40                 outputs.append(r)#把用戶加入到outpus裏觸發select第二個參數
41                 message[r].put(client_data)#在指定隊列中插入數據
42             else:
43                 inputs.remove(r)#沒有數據,刪除監聽連接
44                 del message[r] #當數據爲空的時候刪除隊列~~
45     for w in writeable_list:#若是第二個參數有數據
46         try:
47             data = message[w].get_nowait()#去指定隊列取數據 而且不阻塞
48             w.sendall(data) #返回請求輸入給client端
49         except Queue.Empty:#反之觸發異常
50             pass
51         outputs.remove(w) #由於第二個參數有值得時候就觸發捕獲值,因此使用完以後須要移除它
52         #del message[r]
53     print '%s' %('-' * 40)
利用select和隊列實現多客戶端讀寫分離

       使用select() 的事件驅動模型只用單線程(進程)執行,佔用資源少,不消耗太多CPU,同時可以爲多客戶端提供服務。若是試圖創建一個簡單的事件驅動的服務器程序,這個模型有必定的參考價值。但這個模型依舊有着不少問題。首先select()接口並非實現「事件驅動」的最好選擇。由於當須要探測的句柄值較大時,select()接口自己須要消耗大量時間去輪詢各個句柄。不少操做系統提供了更爲高效的接口,如linux提供了epoll,BSD提供了queue,Solaris提供了/dev/poll  ...。若是須要實現更高效的服務器程序,相似epoll這樣的接口更被推薦。遺憾的是不一樣的操做系統特供的epoll接口有很大差別,因此使用相似於epoll的接口實現具備較好跨平臺能力的服務器程序會比較困難。
       其次,該模型將事件探測和事件響應夾雜在一塊兒,一旦事件響應的執行體龐大,則對整個模型是災難性的。以下例,龐大的執行體1的將直接致使響應事件2的執行體遲遲得不到執行,並在很大程度上下降了事件探測的及時性。

 

 

參考資料:

    http://www.cnblogs.com/wupeiqi/articles/5040823.html

    http://www.cnblogs.com/luotianshuai/p/5098408.html

    http://www.cnblogs.com/Security-Darren/p/4746230.html

相關文章
相關標籤/搜索