python 網路編程 IO多工之epoll

來源:互聯網
上載者:User

標籤:mon   stream   height   資訊   睡眠   queue   接收   odi   http   

python網路編程——IO多工之epoll1、核心EPOLL模型講解

    此部分參考http://blog.csdn.net/mango_song/article/details/42643971博文並整理

    首先我們來定義流的概念,一個流可以是檔案,socket,pipe等可以進行I/O操作的核心對象。不管是檔案,還是通訊端(socket),還是管道(pipe),我們都可以把他們看作流。

    之後我們來討論I/O操作,通過read,我們可以從流中讀入資料;通過write,我們可以往流中寫入資料。現在假定1種情形,我們需要從流中讀資料,但是流中還沒有資料,(典型的例子為,用戶端要從socket讀資料,但是伺服器端還沒有把資料傳回來),這時候該怎麼辦?

    阻塞:阻塞是個什麼概念呢?比如某個時候你在等快遞,但是你還不知道快遞什麼時候過來,而且你也沒有別的事可以幹(或者說接下來的事要等快遞來了才能做);那麼你可以去睡覺了,因為你知道快遞把貨送來時一定會給你打電話(假定一定能叫醒你)。

    非阻塞忙輪詢:接著上面等快遞的例子,如果用忙輪詢的方法,那麼你需要知道快遞員的手機號,然後每分鐘給他打個電話:“你到了沒?”

    很明顯一般人不會用第二種做法,不僅顯得無腦,浪費話費不說,還佔用了快遞員大量的時間。

    大部分程式也不會用第二種做法,因為第一種方法經濟而簡單,經濟是指消耗很少的CPU時間,如果線程睡眠了,就掉出了系統的調度隊列,暫時不會去瓜分CPU寶貴的時間片。

    為了瞭解阻塞是如何進行的,我們來討論緩衝區,以及核心緩衝區,最終把I/O事件解釋清楚。緩衝區的引入是為了減少頻繁I/O操作而引起頻繁的系統調用(你知道它很慢的),當你操作一個流時,更多的是以緩衝區為單位進行操作,這是相對於使用者空間而言。對於核心來說,也需要緩衝區。

    假設有一個管道,進程A為管道的寫入方,B為管道的讀出方。假設一開始核心緩衝區是空的,B作為讀出方,被阻塞著。然後首先A往管道寫入,這時候核心緩衝區由空的狀態變到非空狀態,核心就會產生一個事件告訴B該醒來了,這個事件姑且稱之為“緩衝區非空”。但是“緩衝區非空”事件通知B後,B卻還沒有讀出資料;且核心確保了不能把寫入管道中的資料丟掉這個時候,A寫入的資料會滯留在核心緩衝區中,如果核心也緩衝區滿了,B仍未開始讀資料,最終核心緩衝區會被填滿,這個時候會產生一個I/O事件,告訴進程A,你該等等(阻塞)了,我們把這個事件定義為“緩衝區滿”。假設後來B終於開始讀資料了,於是核心的緩衝區空了出來,這時候核心會告訴A,核心緩衝區有空位了,你可以從長眠中醒來了,繼續寫資料了,我們把這個事件叫做“緩衝區非滿”。也許事件Y1已經通知了A,但是A也沒有資料寫入了,而B繼續讀出資料,知道核心緩衝區空了。這個時候核心就告訴B,你需要阻塞了!,我們把這個時間定為“緩衝區空”。

    這四種情形涵蓋了四個I/O事件,核心緩衝區滿,核心緩衝區空,核心緩衝區非空,核心緩衝區非滿。這四個I/O事件是進行阻塞同步的根本。(如果不能理解“同步”是什麼概念,請學習作業系統的鎖,訊號量,條件變數等任務同步方面的相關知識)。

    然後我們來說說阻塞I/O的缺點。但是阻塞I/O模式下,一個線程只能處理一個流的I/O事件。如果想要同時處理多個流,要麼多進程(fork),要麼多線程(pthread_create),很不幸這兩種方法效率都不高。於是再來考慮非阻塞忙輪詢的I/O方式,我們發現可以同時處理多個流(把一個流從阻塞模式切換到非阻塞模式再此不予討論):

while true {       for i in stream[]; {             if i has data             read until unavailable       }  }

 

我們只要不停的把所有流從頭到尾問一遍,又從頭開始。這樣就可以處理多個流了,但這樣的做法顯然不好,因為如果所有的流都沒有資料,那麼只會白白浪費CPU。這裡要補充一點,阻塞模式下,核心對於I/O事件的處理是阻塞或者喚醒,而非阻塞模式下則把I/O事件交給其他對象(後文介紹的select以及epoll)處理甚至直接忽略。

    為了避免CPU空轉,可以引進一個代理(一開始有一位叫做select的代理,後來又有一位叫做poll的代理,不過兩者的本質是一樣的)。這個代理比較厲害,可以同時觀察許多流的I/O事件,在閒置時候,會把當前線程阻塞掉,當有一個或多個流有I/O事件時,就從阻塞態中醒來,於是我們的程式就會輪詢一遍所有的流(於是我們可以把“忙”字去掉了)。代碼長這樣:

while true {        select(streams[])        for i in streams[] {              if i has data              read until unavailable         }  } 

 

    於是,如果沒有I/O事件產生,我們的程式就會阻塞在select處。但是依然有個問題,我們從select那裡僅僅知道了,有I/O事件發生了,但卻並不知道是那幾個流(可能有一個,多個,甚至全部),我們只能無差別輪詢所有流,找出能讀出資料,或者寫入資料的流,對他們進行操作。

    但是使用select,我們有O(n)的無差別輪詢複雜度,同時處理的流越多,每一次無差別輪詢時間就越長。再次說了這麼多,終於能好好解釋epoll了。    epoll可以理解為event poll, 不同於忙輪詢和無差別輪詢,epoll只會把哪個流發生了怎樣的I/O事件通知我們。此時我們對這些流的操作都是有意義的(複雜度降低到了O(1))。    在討論epoll的實現細節之前,先把epoll的相關操作列出: 
1 epoll_create建立一個epoll對象,一般epollfd = epoll_create()  2 epoll_ctl (epoll_add/epoll_del的合體),往epoll對象中增加/刪除某一個流的某一個事件  3  比如  4 epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//註冊緩衝區非空事件,即有資料流入  5 epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//註冊緩衝區非滿事件,即流可以被寫入  6 epoll_wait(epollfd,...)等待直到註冊的事件發生  7 (註:當對一個非阻塞流的讀寫發生緩衝區滿或緩衝區空,write/read會返回-1,並設定errno=EAGAIN。而epoll只關心緩衝區非滿和緩衝區非空事件)。  

 

一個epoll模式的代碼大概的樣子

 

2 python中的epoll

   從以上可知,epoll是對select、poll模型的改進,提高了網路編程的效能,廣泛應用於大規模並發請求的C/S架構中。

  1、觸發方式:

     邊緣觸發/水平觸發,只適用於Unix/Linux作業系統

   2、原理圖

  3、一般步驟

  1. Create an epoll object——建立1個epoll對象
  2. Tell the epoll object to monitor specific events on specific sockets——告訴epoll對象,在指定的socket上監聽指定的事件
  3. Ask the epoll object which sockets may have had the specified event since the last query——詢問epoll對象,從上次查詢以來,哪些socket發生了哪些指定的事件
  4. Perform some action on those sockets——在這些socket上執行一些操作
  5. Tell the epoll object to modify the list of sockets and/or events to monitor——告訴epoll對象,修改socket列表和(或)事件,並監控
  6. Repeat steps 3 through 5 until finished——重複步驟3-5,直到完成
  7. Destroy the epoll object——銷毀epoll對象

  4、相關用法

import select 匯入select模組epoll = select.epoll() 建立一個epoll對象epoll.register(檔案控制代碼,事件類型) 註冊要監控的檔案控制代碼和事件事件類型:  select.EPOLLIN    可讀事件  select.EPOLLOUT   可寫事件  select.EPOLLERR   錯誤事件  select.EPOLLHUP   用戶端斷開事件epoll.unregister(檔案控制代碼)   銷毀檔案控制代碼epoll.poll(timeout)  當檔案控制代碼發生變化,則會以列表的形式主動報告給使用者進程,timeout                     為逾時時間,預設為-1,即一直等待直到檔案控制代碼發生變化,如果指定為1                     那麼epoll每1秒彙報一次當前檔案控制代碼的變化情況,如果無變化則返回空epoll.fileno() 返回epoll的控制檔案描述符(Return the epoll control file descriptor)epoll.modfiy(fineno,event) fineno為檔案描述符 event為事件類型  作用是修改檔案描述符所對應的事件epoll.fromfd(fileno) 從1個指定的檔案描述符建立1個epoll對象epoll.close()   關閉epoll對象的控制檔案描述符

 

5 執行個體:用戶端發送資料 服務端將接收的資料返回給用戶端

def epoll_socket():    import socket    import select    import Queue    # 建立socket對象    serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)    # 設定IP地址複用    serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)    # ip地址和連接埠號碼    server_address = ("127.0.0.1", 6666)    # 綁定IP地址    serversocket.bind(server_address)    # 監聽,並設定最大串連數    serversocket.listen(10)    print  "伺服器啟動成功,監聽IP:", server_address    # 服務端設定非阻塞    serversocket.setblocking(False)    # 逾時時間    timeout = 10    # 建立epoll事件對象,後續要監控的事件添加到其中    epoll = select.epoll()    # 註冊伺服器監聽fd到等待讀事件集合    epoll.register(serversocket.fileno(), select.EPOLLIN)    # 儲存串連用戶端訊息的字典,格式為{}    message_queues = {}    # 檔案控制代碼到所對應對象的字典,格式為{控制代碼:對象}    fd_to_socket = {serversocket.fileno(): serversocket, }    num = 0    while True:        print "等待活動串連......"        # 輪詢註冊的事件集合,傳回值為[(檔案控制代碼,對應的事件),(...),....]        events = epoll.poll(timeout)        if not events:            print "epoll逾時無活動串連,重新輪詢......"            continue        print "有", len(events), "個新事件,開始處理......"        for fd, event in events:            print ‘fd and event: ‘, fd, event            socket = fd_to_socket[fd]            # 如果活動socket為當前伺服器socket,表示有新串連            print ‘socket and serversocket: ‘, socket, serversocket            print ‘----------event & select.EPOLLIN: ‘, event & select.EPOLLIN            if socket == serversocket:                connection, address = serversocket.accept()  # connection新通訊端對象                print "新串連:", address                # 新串連socket設定為非阻塞                connection.setblocking(False)                # 註冊新串連fd到待讀事件集合                epoll.register(connection.fileno(), select.EPOLLIN)                # 把新串連的檔案控制代碼以及對象儲存到字典                fd_to_socket[connection.fileno()] = connection                # 以新串連的對象為索引值,值儲存在隊列中,儲存每個串連的資訊                message_queues[connection] = Queue.Queue()            # 關閉事件            elif event & select.EPOLLHUP:                print ‘client close‘                # 在epoll中登出用戶端的檔案控制代碼                epoll.unregister(fd)                # 關閉用戶端的檔案控制代碼                fd_to_socket[fd].close()                # 在字典中刪除與已關閉用戶端相關的資訊                del fd_to_socket[fd]            # 可讀事件            elif event & select.EPOLLIN:                # 接收資料                data = socket.recv(1024)                if data:                    print "收到資料:", data, "用戶端:", socket.getpeername()                    # 將資料放入對應用戶端的字典                    message_queues[socket].put(data)                    # 修改讀取到訊息的串連到等待寫事件集合(即對應用戶端收到訊息後,再將其fd修改並加入寫事件集合)                    epoll.modify(fd, select.EPOLLOUT)                    num = 0                else:                    print ‘-------client close connect or send null data -----------‘                    epoll.modify(fd, select.EPOLLHUP)  # 用戶端關閉串連            # 可寫事件            elif event & select.EPOLLOUT:                try:                    # 從字典中擷取對應用戶端的資訊                    msg = message_queues[socket].get_nowait()                except Queue.Empty:                    print socket.getpeername(), " queue empty"                    # 修改檔案控制代碼為讀事件                    epoll.modify(fd, select.EPOLLIN)                else:                    print "發送資料:", data, "用戶端:", socket.getpeername()                    # 發送資料                    socket.send(msg)    # 在epoll中登出服務端檔案控制代碼    epoll.unregister(serversocket.fileno())    # 關閉epoll    epoll.close()    # 關閉伺服器socket    serversocket.close()

用戶端:

#!/usr/bin/env python#-*- coding:utf-8 -*-import socket#建立用戶端socket對象clientsocket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#服務端IP地址和連接埠號碼元組server_address = (‘127.0.0.1‘, 6666)#用戶端串連指定的IP地址和連接埠號碼clientsocket.connect(server_address)while True:    #輸入資料    data = raw_input(‘please input:‘)    #用戶端發送資料    clientsocket.sendall(data)    #用戶端接收資料    server_data = clientsocket.recv(1024)    print ‘用戶端收到的資料:‘, server_data    #關閉用戶端socket    # clientsocket.close() 

 

 

 

 

 

 

 

python 網路編程 IO多工之epoll

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.