python網路程式設計——IO多路複用之epoll

pythontab發表於2016-10-10

什麼是epoll

epoll是什麼?在linux的網路程式設計中,很長的時間都在使用select來做事件觸發。在linux新的核心中,有了一種替換它的機制,就是epoll。當然,這不是2.6核心才有的,它是在2.5.44核心中被引進的(epoll(4) is a new API introduced in Linux kernel 2.5.44),它幾乎具備了之前所說的一切優點,被公認為Linux2.6下效能最好的多路複用I/O就緒通知方法。

相比於select,epoll最大的好處在於它不會隨著監聽fd數目的增長而降低效率。因為在核心中的select實現中,它是採用輪詢來處理的,輪詢的fd數目越多,自然耗時越多。

epoll工作原理

epoll同樣只告知那些就緒的檔案描述符,而且當我們呼叫epoll_wait()獲得就緒檔案描述符時,返回的不是實際的描述符,而是一個代表就緒描述符數量的值,你只需要去epoll指定的一個陣列中依次取得相應數量的檔案描述符即可,這裡也使用了記憶體對映(mmap)技術,這樣便徹底省掉了這些檔案描述符在系統呼叫時複製的開銷。

 

另一個本質的改進在於epoll採用基於事件的就緒通知方式。在select/poll中,程式只有在呼叫一定的方法後,核心才對所有監視的檔案描述符進行掃描,而epoll事先透過epoll_ctl()來註冊一個檔案描述符,一旦基於某個檔案描述符就緒時,核心會採用類似callback的回撥機制,迅速啟用這個檔案描述符,當程式呼叫epoll_wait()時便得到通知。


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

python中的epoll

1、觸發方式:

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

2、原理圖

python網路程式設計——IO多路複用之epoll

3、一般步驟

Create an epoll object——建立1個epoll物件

Tell the epoll object to monitor specific events on specific sockets——告訴epoll物件,在指定的socket上監聽指定的事件

Ask the epoll object which sockets may have had the specified event since the last query——詢問epoll物件,從上次查詢以來,哪些socket發生了哪些指定的事件

Perform some action on those sockets——在這些socket上執行一些操作

Tell the epoll object to modify the list of sockets and/or events to monitor——告訴epoll物件,修改socket列表和(或)事件,並監控

Repeat steps 3 through 5 until finished——重複步驟3-5,直到完成

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 例項:客戶端傳送資料 服務端將接收的資料返回給客戶端

服務端程式碼

#!/usr/bin/env python
#-*- coding:utf-8 -*-
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", 8888)
#繫結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,}
while True:
  print "等待活動連線......"
  #輪詢註冊的事件集合,返回值為[(檔案控制程式碼,對應的事件),(...),....]
  events = epoll.poll(timeout)
  if not events:
     print "epoll超時無活動連線,重新輪詢......"
     continue
  print "有" , len(events), "個新事件,開始處理......"
  
  for fd, event in events:
     socket = fd_to_socket[fd]
     #如果活動socket為當前伺服器socket,表示有新連線
     if socket == serversocket:
            connection, address = serversocket.accept()
            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)
     #可寫事件
     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',8888)
#客戶端連線指定的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()


相關文章