網路IO的模型中,之前介紹了select模型。select 確實是一個簡明好用的模型。可是現在的伺服器卻越來越少採取這樣的模型,原因之一就是它的效能讓人擔憂。雖然後來升級了poll模型,本質上還是和select模型類似。當然,當一個技術逐漸被人放棄的時候,很大程度上是有了更好的替代方案。沒錯,還有select/poll模型更好的網路IO模型,就是今天介紹的主角—Epoll。在很多地方,epoll都是高效能代名詞,準確的說epoll是Linux核心升級的多路複用IO模型,在Unix和MacOS上類似的則是 Kqueue。
epoll優點
select的缺點之一就是在網路IO流到來的時候,執行緒會輪詢監控檔案陣列,並且是線性掃描,還有最大值的限制。相比select,epoll則無需如此。伺服器主執行緒建立了epoll物件,並且註冊socket和檔案事件即可。當資料抵達的時候,也就是對於事件發生,則會呼叫此前註冊的那個io檔案。
先看一個python的epoll例子,採用了網路上一段著名的code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
import socket import select EOL1 = b'\n\n' EOL2 = b'\n\r\n' response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n' response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n' response += b'Hello, world!' # 建立套接字物件並繫結監聽埠 serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) serversocket.bind(('0.0.0.0', 8080)) serversocket.listen(1) serversocket.setblocking(0) # 建立epoll物件,並註冊socket物件的 epoll可讀事件 epoll = select.epoll() epoll.register(serversocket.fileno(), select.EPOLLIN) try: connections = {} requests = {} responses = {} while True: # 主迴圈,epoll的系統呼叫,一旦有網路IO事件發生,poll呼叫返回。這是和select系統呼叫的關鍵區別 events = epoll.poll(1) # 通過事件通知獲得監聽的檔案描述符,進而處理 for fileno, event in events: # 註冊監聽的socket物件可讀,獲取連線,並註冊連線的可讀事件 if fileno == serversocket.fileno(): connection, address = serversocket.accept() connection.setblocking(0) epoll.register(connection.fileno(), select.EPOLLIN) connections[connection.fileno()] = connection requests[connection.fileno()] = b'' responses[connection.fileno()] = response elif event & select.EPOLLIN: # 連線物件可讀,處理客戶端發生的資訊,並註冊連線物件可寫 requests[fileno] += connections[fileno].recv(1024) if EOL1 in requests[fileno] or EOL2 in requests[fileno]: epoll.modify(fileno, select.EPOLLOUT) print('-' * 40 + '\n' + requests[fileno].decode()[:-2]) elif event & select.EPOLLOUT: # 連線物件可寫事件發生,傳送資料到客戶端 byteswritten = connections[fileno].send(responses[fileno]) responses[fileno] = responses[fileno][byteswritten:] if len(responses[fileno]) == 0: epoll.modify(fileno, 0) connections[fileno].shutdown(socket.SHUT_RDWR) elif event & select.EPOLLHUP: epoll.unregister(fileno) connections[fileno].close() del connections[fileno] finally: epoll.unregister(serversocket.fileno()) epoll.close() serversocket.close() |
可見epoll使用也很簡單,並沒有過多複雜的邏輯,當然主要是在系統層面封裝的好。至於Epoll的原理,也不是三言兩語可以解釋清楚,作為開發者,先學會如何使用API。
epoll與tornado
既然epoll是一種高效能的網路io模型,很多web框架也採取epoll模型。大名鼎鼎tornado是python框架中一個高效能的非同步框架,其底層也是來者epoll的IO模型。
當然,tornado是跨平臺的,因此他的網路io,在linux下是epoll,unix下則是kqueue。幸好tornado都做了封裝,對於開發者及其友好,下面看一個tornado寫的回顯例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import errno import functools import tornado.ioloop import socket def handle_connection(connection, address): """ 處理請求,返回資料給客戶端 """ data = connection.recv(2014) print data connection.send(data) def connection_ready(sock, fd, events): """ 事件回撥函式,主要用於socket可讀事件,用於獲取socket的連結 """ while True: try: connection, address = sock.accept() except socket.error as e: if e.args[0] not in (errno.EWOULDBLOCK, errno.EAGAIN): raise return connection.setblocking(0) handle_connection(connection, address) if __name__ == '__main__': sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setblocking(0) sock.bind(("", 5000)) sock.listen(128) # 使用tornado封裝好的epoll介面,即IOLoop物件 io_loop = tornado.ioloop.IOLoop.current() callback = functools.partial(connection_ready, sock) # io_loop物件註冊網路io檔案描述符和回撥函式與io事件的繫結 io_loop.add_handler(sock.fileno(), callback, io_loop.READ) io_loop.start() |
上面的程式碼來者tornado的模組IOLoop原始碼的文件,很簡明的介紹了在tornado中如何使用網路IO。當然具體的封裝實現,可以參考tornado原始碼獲知,在此不做介紹了。