IO多路復(fù)用深入淺出

Java程序員進(jìn)階三條必經(jīng)之路:數(shù)據(jù)庫(kù)定庵、虛擬機(jī)吏饿、異步通信。

前言

從零單排高性能問(wèn)題蔬浙,這次輪到異步通信了猪落。這個(gè)領(lǐng)域入門(mén)有點(diǎn)難,需要了解UNIX五種IO模型和TCP協(xié)議畴博,熟練使用三大異步通信框架:Netty笨忌、NodeJS、Tornado俱病。目前所有標(biāo)榜異步的通信框架用的都不是異步IO模型官疲,而是IO多路復(fù)用中的epoll。因?yàn)镻ython提供了對(duì)Linux內(nèi)核API的友好封裝亮隙,所以我選擇Python來(lái)學(xué)習(xí)IO多路復(fù)用途凫。

IO多路復(fù)用

  1. select

舉一個(gè)EchoServer的例子,客戶(hù)端發(fā)送任何內(nèi)容溢吻,服務(wù)端會(huì)原模原樣返回维费。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
Created on Feb 16, 2016

@author: mountain
'''
import socket
import select
from Queue import Queue

#AF_INET指定使用IPv4協(xié)議,如果要用更先進(jìn)的IPv6促王,就指定為AF_INET6掩完。
#SOCK_STREAM指定使用面向流的TCP協(xié)議,如果要使用面向數(shù)據(jù)包的UCP協(xié)議硼砰,就指定SOCK_DGRAM且蓬。
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False)
#設(shè)置監(jiān)聽(tīng)的ip和port
server_address = ('localhost', 1234)
server.bind(server_address)
#設(shè)置backlog為5,client向server發(fā)起connect题翰,server accept后建立長(zhǎng)連接恶阴,
#backlog指定排隊(duì)等待server accept的連接數(shù)量诈胜,超過(guò)這個(gè)數(shù)量,server將拒絕連接冯事。
server.listen(5)
#注冊(cè)在socket上的讀事件
inputs = [server]
#注冊(cè)在socket上的寫(xiě)事件
outputs = []
#注冊(cè)在socket上的異常事件
exceptions = []
#每個(gè)socket有一個(gè)發(fā)送消息的隊(duì)列
msg_queues = {}
print "server is listening on %s:%s." % server_address
while inputs:
     #第四個(gè)參數(shù)是timeout焦匈,可選,表示n秒內(nèi)沒(méi)有任何事件通知昵仅,就執(zhí)行下面代碼
     readable, writable, exceptional = select.select(inputs, outputs, exceptions)
     for sock in readable:
         #client向server發(fā)起connect也是讀事件缓熟,server accept后產(chǎn)生socket加入讀隊(duì)列中
         if sock is server:
             conn, addr = sock.accept()
             conn.setblocking(False)
             inputs.append(conn)
             msg_queues[conn] = Queue()
             print "server accepts a conn."
         else:
             #讀取client發(fā)過(guò)來(lái)的數(shù)據(jù),最多讀取1k byte摔笤。
             data = sock.recv(1024)
             #將收到的數(shù)據(jù)返回給client
             if data:
                 msg_queues[sock].put(data)
                 if sock not in outputs:
                     #下次select的時(shí)候會(huì)觸發(fā)寫(xiě)事件通知够滑,寫(xiě)和讀事件不太一樣,前者是可寫(xiě)就會(huì)觸發(fā)事件吕世,并不一定要真的去寫(xiě)
                     outputs.append(sock)
             else:
                 #client傳過(guò)來(lái)的消息為空彰触,說(shuō)明已斷開(kāi)連接
                 print "server closes a conn."
                 if sock in outputs:
                     outputs.remove(sock)
                 inputs.remove(sock)
                 sock.close()
                 del msg_queues[sock]
     for sock in writable:
         if not msg_queues[sock].empty():
             sock.send(msg_queues[sock].get_nowait())
         if msg_queues[sock].empty():
             outputs.remove(sock)
     for sock in exceptional:
         inputs.remove(sock)
         if sock in outputs:
             outputs.remove(sock)
         sock.close()
         del msg_queues[sock]
[mountain@king ~/workspace/wire]$ telnet localhost 1234
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
1
1

select有3個(gè)缺點(diǎn):
1. 每次調(diào)用select,都需要把fd集合從用戶(hù)態(tài)拷貝到內(nèi)核態(tài)命辖,這個(gè)開(kāi)銷(xiāo)在fd很多時(shí)會(huì)很大况毅。
1. 每次調(diào)用select后,都需要在內(nèi)核遍歷傳遞進(jìn)來(lái)的所有fd尔艇,這個(gè)開(kāi)銷(xiāo)在fd很多時(shí)也很大尔许。
這點(diǎn)從python的例子里看不出來(lái),因?yàn)閜ython select api更加友好终娃,直接返回就緒的socket列表味廊。事實(shí)上linux內(nèi)核select api返回的是就緒socket數(shù)目:
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
1. fd數(shù)量有限,默認(rèn)1024尝抖。

  1. poll

采用poll重新實(shí)現(xiàn)EchoServer毡们,只要搞懂了select,poll也不難昧辽,只是api的參數(shù)不太一樣而已衙熔。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
Created on Feb 27, 2016

@author: mountain
'''
import select
import socket
import sys
import Queue

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False)
server_address = ('localhost', 1234)
server.bind(server_address)
server.listen(5)
print 'server is listening on %s port %s' % server_address
msg_queues = {}
timeout = 1000 * 60
#POLLIN: There is data to read
#POLLPRI: There is urgent data to read
#POLLOUT: Ready for output
#POLLERR: Error condition of some sort
#POLLHUP: Hung up
#POLLNVAL: Invalid request: descriptor not open
READ_ONLY = select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR
READ_WRITE = READ_ONLY | select.POLLOUT
poller = select.poll()
#注冊(cè)需要監(jiān)聽(tīng)的事件
poller.register(server, READ_ONLY)
#文件描述符和socket映射
fd_to_socket = { server.fileno(): server}
while True:
     events = poller.poll(timeout)
     for fd, flag in events:
         sock = fd_to_socket[fd]
         if flag & (select.POLLIN | select.POLLPRI):
             if sock is server:
                 conn, client_address = sock.accept()
                 conn.setblocking(False)
                 fd_to_socket[conn.fileno()] = conn
                 poller.register(conn, READ_ONLY)
                 msg_queues[conn] = Queue.Queue()
             else:
                 data = sock.recv(1024)
                 if data:
                     msg_queues[sock].put(data)
                     poller.modify(sock, READ_WRITE)
                 else:
                     poller.unregister(sock)
                     sock.close()
                     del msg_queues[sock]
         elif flag & select.POLLHUP:
             poller.unregister(sock)
             sock.close()
             del msg_queues[sock]
         elif flag & select.POLLOUT:
             if not msg_queues[sock].empty():
                 msg = msg_queues[sock].get_nowait()
                 sock.send(msg)
             else:
                 poller.modify(sock, READ_ONLY)
         elif flag & select.POLLERR:
             poller.unregister(sock)
             sock.close()
             del msg_queues[sock]

poll解決了select的第三個(gè)缺點(diǎn),fd數(shù)量不受限制搅荞,但是失去了select的跨平臺(tái)特性红氯,它的linux內(nèi)核api是這樣的:

int poll (struct pollfd *fds, unsigned int nfds, int timeout);
struct pollfd { 
     int fd; /* file descriptor */
     short events; /* requested events to watch */
     short revents; /* returned events witnessed */
};
  1. epoll

用法與poll幾乎一樣。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
Created on Feb 28, 2016

@author: mountain
'''
import select
import socket
import Queue

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False)
server_address = ('localhost', 1234)
server.bind(server_address)
server.listen(5)
print 'server is listening on %s port %s' % server_address
msg_queues = {}
timeout = 60
READ_ONLY = select.EPOLLIN | select.EPOLLPRI
READ_WRITE = READ_ONLY | select.EPOLLOUT
epoll = select.epoll()
#注冊(cè)需要監(jiān)聽(tīng)的事件
epoll.register(server, READ_ONLY)
#文件描述符和socket映射
fd_to_socket = { server.fileno(): server}
while True:
     events = epoll.poll(timeout)
     for fd, flag in events:
         sock = fd_to_socket[fd]
         if flag & READ_ONLY:
             if sock is server:
                 conn, client_address = sock.accept()
                 conn.setblocking(False)
                 fd_to_socket[conn.fileno()] = conn
                 epoll.register(conn, READ_ONLY)
                 msg_queues[conn] = Queue.Queue()
             else:
                 data = sock.recv(1024)
                 if data:
                     msg_queues[sock].put(data)
                     epoll.modify(sock, READ_WRITE)
                 else:
                     epoll.unregister(sock)
                     sock.close()
                     del msg_queues[sock]
         elif flag & select.EPOLLHUP:
             epoll.unregister(sock)
             sock.close()
             del msg_queues[sock]
         elif flag & select.EPOLLOUT:
             if not msg_queues[sock].empty():
                 msg = msg_queues[sock].get_nowait()
                 sock.send(msg)
             else:
                 epoll.modify(sock, READ_ONLY)
         elif flag & select.EPOLLERR:
             epoll.unregister(sock)
             sock.close()
             del msg_queues[sock]

epoll解決了select的三個(gè)缺點(diǎn)咕痛,是目前最好的IO多路復(fù)用解決方案痢甘。為了更好地理解epoll,我們來(lái)看一下linux內(nèi)核api的用法茉贡。

int epoll_create(int size)//創(chuàng)建一個(gè)epoll的句柄塞栅,size用來(lái)告訴內(nèi)核這個(gè)監(jiān)聽(tīng)的數(shù)目一共有多大。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)//注冊(cè)事件腔丧,每個(gè)fd只拷貝一次放椰。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)/*等待IO事件作烟,事件發(fā)生時(shí),
內(nèi)核調(diào)用回調(diào)函數(shù)砾医,把就緒fd放入就緒鏈表中拿撩,并喚醒epoll_wait,epoll_wait只需要遍歷就緒鏈表即可如蚜,
而select和poll都是遍歷所有fd压恒,這效率高下立判。*/
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末错邦,一起剝皮案震驚了整個(gè)濱河市探赫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌兴猩,老刑警劉巖期吓,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件暮刃,死亡現(xiàn)場(chǎng)離奇詭異副签,居然都是意外死亡骏融,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)晨另,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人谱姓,你說(shuō)我怎么就攤上這事借尿。” “怎么了屉来?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵路翻,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我茄靠,道長(zhǎng)茂契,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任慨绳,我火速辦了婚禮掉冶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘脐雪。我一直安慰自己厌小,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布战秋。 她就那樣靜靜地躺著璧亚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪脂信。 梳的紋絲不亂的頭發(fā)上癣蟋,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天拐袜,我揣著相機(jī)與錄音,去河邊找鬼梢薪。 笑死蹬铺,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的秉撇。 我是一名探鬼主播甜攀,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼琐馆!你這毒婦竟也來(lái)了规阀?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤瘦麸,失蹤者是張志新(化名)和其女友劉穎谁撼,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體滋饲,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡厉碟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了屠缭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片箍鼓。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖呵曹,靈堂內(nèi)的尸體忽然破棺而出款咖,到底是詐尸還是另有隱情,我是刑警寧澤奄喂,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布铐殃,位于F島的核電站,受9級(jí)特大地震影響跨新,放射性物質(zhì)發(fā)生泄漏富腊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一玻蝌、第九天 我趴在偏房一處隱蔽的房頂上張望蟹肘。 院中可真熱鬧,春花似錦俯树、人聲如沸帘腹。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)阳欲。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間球化,已是汗流浹背秽晚。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留筒愚,地道東北人赴蝇。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像巢掺,于是被迫代替她去往敵國(guó)和親句伶。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 看到網(wǎng)上有不少討論epoll,但大多不夠詳細(xì)準(zhǔn)確轧苫,以前面試有被問(wèn)到這個(gè)問(wèn)題楚堤。不去更深入的了解,只能停留在知其然...
    電臺(tái)_Fang閱讀 11,476評(píng)論 0 8
  • 大綱 一.Socket簡(jiǎn)介 二.BSD Socket編程準(zhǔn)備 1.地址 2.端口 3.網(wǎng)絡(luò)字節(jié)序 4.半相關(guān)與全相...
    y角閱讀 2,380評(píng)論 2 11
  • 我有一位好閨蜜含懊,從小一起長(zhǎng)大身冬,大學(xué)在一個(gè)城市,工作后也會(huì)經(jīng)常見(jiàn)面的那種绢要,反正就是大家所謂的‘’后天親人”吏恭。剛剛我的...
    哆啦的夢(mèng)閱讀 438評(píng)論 0 1
  • 這一篇文章是寫(xiě)在14年11月30日拗小,在我看來(lái)沒(méi)有什么文筆可言重罪,字字句句皆出本心。我仍然記得哀九,當(dāng)時(shí)的我是留著淚把它寫(xiě)...
    Ta_nG閱讀 764評(píng)論 0 1
  • A財(cái)富目標(biāo) 1,公司四個(gè)月以?xún)?nèi)收入50萬(wàn) 2,自己可以獨(dú)立承擔(dān)自己的責(zé)任剿配。 B,伴侶的目標(biāo) 身高一米六到一米七,長(zhǎng)...
    雪痕情閱讀 92評(píng)論 0 1