網(wǎng)絡(luò)IO的模型中睹耐,之前介紹了select模型钝尸。select 確實是一個簡明好用的模型÷Ц可是現(xiàn)在的服務(wù)器卻越來越少采取這樣的模型珍促,原因之一就是它的性能讓人擔(dān)憂。雖然后來升級了poll模型剩愧,本質(zhì)上還是和select模型類似猪叙。當然,當一個技術(shù)逐漸被人放棄的時候仁卷,很大程度上是有了更好的替代方案穴翩。沒錯,還有select/poll模型更好的網(wǎng)絡(luò)IO模型锦积,就是今天介紹的主角---Epoll芒帕。在很多地方,epoll都是高性能代名詞丰介,準確的說epoll是Linux內(nèi)核升級的多路復(fù)用IO模型背蟆,在Unix和MacOS上類似的則是 Kqueue。
epoll優(yōu)點
select的缺點之一就是在網(wǎng)絡(luò)IO流到來的時候哮幢,線程會輪詢監(jiān)控文件數(shù)組带膀,并且是線性掃描,還有最大值的限制橙垢。相比select垛叨,epoll則無需如此。服務(wù)器主線程創(chuàng)建了epoll對象柜某,并且注冊socket和文件事件即可嗽元。當數(shù)據(jù)抵達的時候,也就是對于事件發(fā)生喂击,則會調(diào)用此前注冊的那個io文件剂癌。
先看一個python的epoll例子,采用了網(wǎng)絡(luò)上一段著名的code:
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!'
# 創(chuàng)建套接字對象并綁定監(jiān)聽端口
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)
# 創(chuàng)建epoll對象惭等,并注冊socket對象的 epoll可讀事件
epoll = select.epoll()
epoll.register(serversocket.fileno(), select.EPOLLIN)
try:
connections = {}
requests = {}
responses = {}
while True:
# 主循環(huán)珍手,epoll的系統(tǒng)調(diào)用办铡,一旦有網(wǎng)絡(luò)IO事件發(fā)生辞做,poll調(diào)用返回。這是和select系統(tǒng)調(diào)用的關(guān)鍵區(qū)別
events = epoll.poll(1)
# 通過事件通知獲得監(jiān)聽的文件描述符寡具,進而處理
for fileno, event in events:
# 注冊監(jiān)聽的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:
# 連接對象可讀童叠,處理客戶端發(fā)生的信息框喳,并注冊連接對象可寫
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:
# 連接對象可寫事件發(fā)生课幕,發(fā)送數(shù)據(jù)到客戶端
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使用也很簡單,并沒有過多復(fù)雜的邏輯五垮,當然主要是在系統(tǒng)層面封裝的好乍惊。至于Epoll的原理,也不是三言兩語可以解釋清楚放仗,作為開發(fā)者润绎,先學(xué)會如何使用API。
epoll與tornado
既然epoll是一種高性能的網(wǎng)絡(luò)io模型诞挨,很多web框架也采取epoll模型莉撇。大名鼎鼎tornado是python框架中一個高性能的異步框架,其底層也是來者epoll的IO模型惶傻。
當然棍郎,tornado是跨平臺的,因此他的網(wǎng)絡(luò)io银室,在linux下是epoll涂佃,unix下則是kqueue。幸好tornado都做了封裝蜈敢,對于開發(fā)者及其友好巡李,下面看一個tornado寫的回顯例子。
import errno
import functools
import tornado.ioloop
import socket
def handle_connection(connection, address):
""" 處理請求扶认,返回數(shù)據(jù)給客戶端 """
data = connection.recv(2014)
print data
connection.send(data)
def connection_ready(sock, fd, events):
""" 事件回調(diào)函數(shù)侨拦,主要用于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對象注冊網(wǎng)絡(luò)io文件描述符和回調(diào)函數(shù)與io事件的綁定
io_loop.add_handler(sock.fileno(), callback, io_loop.READ)
io_loop.start()
上面的代碼來者tornado的模塊IOLoop源碼的文檔狱从,很簡明的介紹了在tornado中如何使用網(wǎng)絡(luò)IO。當然具體的封裝實現(xiàn)叠纹,可以參考tornado源碼獲知季研,在此不做介紹了。