netty--從bind方法流程分析netty的實現(xiàn)原理

nettyServer的標(biāo)準(zhǔn)啟動代碼

netty官方源碼中的示例 DiscardServer 中nettyServer的標(biāo)準(zhǔn)啟動姿勢如下

netty server啟動過程

  1. 初始化ServerBootstrap版仔,這里面保存著所有nettyServer運(yùn)行過程中需要的各種信息些举,相當(dāng)于整個nettyServer的環(huán)境
  2. 綁定操作,將形如 127.0.0.1:8888 這樣的地址端口綁定到nettyServer上沼侣,即打開相應(yīng)端口的socket連接丧靡,并且把接收連接后的響應(yīng)事件與bootstrap關(guān)聯(lián)(這一步之后服務(wù)就可以開始接收連接請求了)
  3. 等待關(guān)閉操作蟆沫,如果讀者debug一下的話會看到,主線程會一直阻塞在這里

ServerBootstrap#bind()

上面netty server啟動三部曲的第一步和第三部本身并沒有什么特殊邏輯温治,第一步就是new了一個ServerBootstrap對象并且設(shè)置了各種屬性饭庞,而第三步就是synchronized + wait等待close的消息通知。
netty server啟動的核心在于第二步bind方法熬荆,本文不再貼大篇幅源碼但绕,感興趣的讀者可以自行下載netty源碼。ServerBootstrap#bind()方法的偽代碼如下:

def bind(address):
    channel = initAndRegister() # 打開serverSocketChannel惶看,執(zhí)行serverSocketChannel的register
    doBind(channel, address) # 執(zhí)行serverSocketChannel.bind(address)

可以看到捏顺,ServerBootstrap#bind() 執(zhí)行的核心方法只有兩個,initAndRegister() 和 doBind()

initAndRegister

def initAndRegister():
    channel = channelFactory.newChannel() # 1. 通過工廠模式實例化出來的NioServerSocketChannel纬黎,同時會進(jìn)行nio相關(guān)操作
    bossGroup.register(channel) # 2. 本質(zhì)上是執(zhí)行了channel.register()方法

initAndRegister方法做的事情可以概括為

  • 通過工廠模式實例化出來NioServerSocketChannel幅骄,還記得上文中為ServerBootstrap設(shè)置的channel屬性嗎,netty為了支持不同類型channel的可擴(kuò)展性本今,會通過工廠模式+反射機(jī)制創(chuàng)建NioServerSocketChannel的實例拆座。這個 NioServerSocketChannel 是對jdk的nio的ServerSocketChannel的一種封裝。
    • selectorProvider.openServerSocketChannel()冠息,jdk nio的操作挪凑,打開ServerSocketChannel
  • ServerSocketChannel.register(selector),nio的操作逛艰,在selector上注冊了該channel

至此躏碳,通過initAndRegister我們

  • 初始化了ServerSocketChannel(里面封裝著nio的ServerChannel)
  • 在selector上注冊了該channel

doBind

上文中的channel打開并注冊多路復(fù)用選擇器后,一切都準(zhǔn)備好了散怖,channel就可以打開相應(yīng)的socket端口開始接收請求了菇绵,因此doBind做的事情就是給nio的ServerSocketChannel綁定端口
ServerSocektChannel#bind()

netty、nio镇眷、與操作系統(tǒng)調(diào)用

讀者可以從源碼中發(fā)現(xiàn)咬最,netty的ServerBootstrap的本質(zhì),是對java nio的一層封裝欠动,而java nio的本質(zhì)又是對操作系統(tǒng)多路復(fù)用API的一種封裝


netty套娃

epoll

眾所周知永乌,Java是一個跨平臺的語言,在不同的操作系統(tǒng)上(windows、mac翅雏、linux硝桩、Solaris)的JDK封裝不了不同的調(diào)用實現(xiàn),以最常見的linux系統(tǒng)為例枚荣,linux系統(tǒng)上支持多路復(fù)用功能的API是經(jīng)典的epoll函數(shù)(關(guān)于select-->poll-->epoll是如何一步步進(jìn)化過來的,也是一個經(jīng)典的發(fā)展過程啼肩,本文不再贅述)橄妆。C語言通過使用epoll函數(shù)單線程同時監(jiān)聽處理多個socket套接字的模板代碼如下:

int s = socket(AF_INET, SOCK_STREAM, 0); // 建立socket
bind(s, ...); // 為socket綁定ip:port
listen(s, ...); // 開始監(jiān)聽ip:port

int epfd = epoll_create(...); // 創(chuàng)建特殊的fd -- epoll_fd
epoll_ctl(epfd, ...); // 將所有需要監(jiān)聽的socket添加到epfd中
while(1) {
    int n = epoll_wait(); // 阻塞等待連接事件
    for(接收到數(shù)據(jù)的socket) {
        // 處理
    }
}

epoll的功能可以概括為:同時監(jiān)聽多個文件/網(wǎng)絡(luò)事件的變更,當(dāng)收到變更之后能知道哪些文件/網(wǎng)絡(luò)產(chǎn)生的變更祈坠,并且依次處理害碾。

類似于java中萬物皆是Object對象,linux系統(tǒng)中萬物皆是文件赦拘,每個文件都有一個類似于指針的id慌随,文件描述符,英文File Descriptor躺同,簡稱fd阁猜。

epoll

Java nio

JDK中的NIO包是對操作系統(tǒng)多路復(fù)用API的一種封裝,由于Java語言的跨平臺特性蹋艺,不同OS上的JDK包中關(guān)于selector等功能的實現(xiàn)源碼是不一樣的剃袍,經(jīng)典的java nio多路復(fù)用代碼模板如下:

ServerSocketChannel channel = SelectorProvider.provider().openServerSocketChannel();
channel.bind(...);
channel.configureBlocking();
Selector selector = SelectorProvider.provider().openSelector();
while(true) {
    int readyChannels = selector.select();
    Set<SelectionKey> selectionKeys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
    while (keyIterator.hasNext()) {
        // 處理
    }
    keyIterator.remove();
}

以linux系統(tǒng)下的epoll為例,可以粗略的將Java的若干調(diào)用與epoll代碼模板中進(jìn)行類比捎谨,有如下表格民效。

nio linux系統(tǒng)調(diào)用 description
SelectorProvider.provider().openSelector() epoll_create 創(chuàng)建selector(epoll fd)
socketChannel.register epoll_ctl epfd上注冊socket
selector.select() epoll_wait 多路復(fù)用,等待多個socket的事件通知
SelectorProvider.openServerSocketChannel() socket 建立套接字
socketChannel.bind() bind&listen 綁定監(jiān)聽端口

JDK在若干調(diào)用上使用了懶加載等手段涛救,因此實際在JDK native源碼的實現(xiàn)中并不完全一一對應(yīng)畏邢,,只是在概念上可以類比著理解检吆,具體不同的nio方法的實際調(diào)用舒萎,讀者可以自行下載JDK源碼閱讀。

NioEventLoop-netty的動脈

通過上面分析我們知道bootstrap.bind(addr)本質(zhì)上是initAndRegisterchannel.bind這兩個方法蹭沛,這兩個方法的執(zhí)行是通過向NioEventLoop提交任務(wù)來執(zhí)行的逆甜。所以我們需要先分析NioEventLoop的實現(xiàn)。

ServerBootstrap的模板代碼中會設(shè)置bossGroup和workerGroup致板,分別是兩個NioEventLoopGroup類型交煞,里面包含若干個NioEventLoop,是任務(wù)的核心斟或。


NioEventLoop組

NioEventLoop#run

核心步驟-單線程處理io事件和任務(wù)事件

NioEventLoop的核心方法是NioEventLoop#run()素征,netty的一系列操作從源碼追過去都會落到這個方法上,我們先分析下這個方法的大致實現(xiàn)

while(1):
    selector.select();
    processSelectedKeys();
    runAllTasks(timeout);

方法的源碼很長,核心就是這三步

  • 先執(zhí)行selector.select()御毅,獲取準(zhǔn)備好的io事件
  • processSelectedKeys()根欧,依次處理上述io事件,方法的內(nèi)部就是switch語句對不同的時間類型進(jìn)行不同的處理邏輯(讀/寫/接收連接)
  • runAllTasks端蛆,執(zhí)行所有taskQueue隊列中的任務(wù)和所有定時調(diào)度的任務(wù)凤粗,定時調(diào)度任務(wù)的超時時間是基于select處理的io事件的耗時動態(tài)生成的,默認(rèn)情況下隊列任務(wù)的超時時間和io耗時五五開

nio epoll空輪詢的處理

  • 除了上述三步之外今豆,還有nio經(jīng)典的epoll空輪詢的bug的處理嫌拣,netty也是在這個while循環(huán)中處理的,通過統(tǒng)計while循環(huán)執(zhí)行的頻率呆躲,當(dāng)發(fā)現(xiàn)頻率過高時异逐,就重建selector

nio epoll空輪詢bug,java nio有一定概率會出現(xiàn)selector.select()方法明明什么io事件都沒收到的情況下卻沒有阻塞插掂,而是立即返回灰瞻,進(jìn)而導(dǎo)致這個while循環(huán)出現(xiàn)空輪訓(xùn),表現(xiàn)為CPU打滿100%

channel的pipeline(boss觸發(fā)worker)

上面的processSelectedKeys步驟中辅甥,boss eventloop會處理不同的io事件酝润,通過debug追蹤可以看到,boss eventloop在處理read/accept類型的io事件時璃弄,會調(diào)用pipeline.fireChannelRead()袍祖,通過責(zé)任鏈的方式依次調(diào)用責(zé)任鏈上的channelRead方法。

eventloop

當(dāng)調(diào)用到ServerBootstrap#ServerBootstrapAcceptor的方法時谢揪,ServerBootstrap會為這個accept事件執(zhí)行child eventloop的register方法蕉陋,這個方法又會執(zhí)行上面提到的NioEventLoop#run方法,這樣就又觸發(fā)了child eventloop的while輪回拨扶。
綜上凳鬓,我們可以得到結(jié)論:bossGroup與childGroup是通過boss eventloop的accept事件觸發(fā)啟動child eventloop的自轉(zhuǎn)的。

boss觸發(fā)worker

inbound與outbound

netty中有一對概念患民,inboundHandler與outboundHandler缩举,如下圖所示,分別用于處理read和write流程匹颤,同樣是在第一張圖中ServerBootstrap初始化的時候設(shè)置到bootstrap的嗦明。這些handler最終兜兜轉(zhuǎn)轉(zhuǎn)會設(shè)置到NioServerSocketChannel#pipeline中钉答,在channel收到讀/寫事件時從不同方向順序執(zhí)行

inbound & outbound

inbound與outbound的總結(jié)如下

inbound/outbound 典型用法 需要關(guān)注的類 需要關(guān)注的方法
inbound LengthFieldBasedFrameDecoder - 收到半包請求之后黏包 ChannelInboundHandler channelRead()
outbound LengthFieldPrepender - 為要發(fā)送的請求增加頭部信息標(biāo)識消息長度 ChannelOutboundHandler write/writeAndFlush()
workerloop

netty中的方法調(diào)用都是向eventloop提交任務(wù)

在了解了NioEventLoop的大致原理之后肉微,我們可以回頭來看bootstrap.bind()方法的兩個核心操作遵馆,上面提到bind方法時,為了簡化邏輯赦肃,我們對其執(zhí)行邏輯進(jìn)行了最大規(guī)模的概括溅蛉。而實際上讀者可以自行下載源碼看到公浪,它們的本質(zhì)都是向eventloop中提交了一個任務(wù)到taskQueue,并觸發(fā)了NioEventLoop#run方法的執(zhí)行船侧。

AbstractChannel#AbstractChannel#register()方法

通過追蹤bootstrap.bind()方法欠气,可以看到在AbstractChannel#AbstractChannel#register()方法中,以及在很多地方都有 eventLoop.inEventLoop() 這樣的判斷镜撩,這是netty中實現(xiàn)異步任務(wù)串行無鎖化的方式预柒。
異步任務(wù)串行無鎖化:每個EventLoop正如其名字,就是個死循環(huán)袁梗,串行的執(zhí)行selector事件和task隊列的事件宜鸯,當(dāng)有某個方法(如上文的register)被調(diào)用時,通過判斷當(dāng)前線程围段,

  • 如果是eventloop自己的線程發(fā)起的,說明是正在執(zhí)行task隊列任務(wù)投放,直接執(zhí)行
  • 如果是其它線程發(fā)起的奈泪,則加入到task任務(wù)隊列中。就像一個手忙腳亂的程序員灸芳,為了能夠更流暢的處理手頭的任務(wù)涝桅,往往會將零碎的事情記下來挨個做,而不是立刻有求必應(yīng)烙样,因為立刻響應(yīng)的話需要打斷手頭的工作冯遂、處理完之后再回來(切換上下文)實在不是一個聰明的策略

最后總結(jié)

綜上所述谒获,我們可以回顧最初的問題蛤肌,

  • nio是jdk對epoll等多路復(fù)用系統(tǒng)調(diào)用的封裝,那么netty在nio之上到底做了什么批狱?

    • 傳統(tǒng)的nio模型是一條while循環(huán)線程通關(guān)裸准,一方面這不利于發(fā)揮現(xiàn)在多核CPU的全部能力;另一方面單線程因為任何原因掛掉就會導(dǎo)致nio直接掛掉赔硫,穩(wěn)定性很差炒俱。因此netty有了eventloop這樣的概念動態(tài)控制reactor模式的boss和worker的數(shù)量,更像一個成熟運(yùn)轉(zhuǎn)的企業(yè)了爪膊。
  • 為什么大家都在推崇用netty权悟?netty、nio封裝了這么多邏輯為什么就能夠比傳統(tǒng)bio強(qiáng)推盛?

    • netty/nio的模式可以概括為峦阁,一個線程有規(guī)劃的依次處理多個任務(wù),免去了傳統(tǒng)BIO線程切換的代價耘成;另一方面拇派,傳統(tǒng)BIO一個連接就分配一個線程處理的模式荷辕,并發(fā)量上來之后線程數(shù)根本不夠用啊。
  • 零拷貝技術(shù)是什么件豌?

    • 零拷貝技術(shù)其實就是一個操作系統(tǒng)的系統(tǒng)調(diào)用疮方,在linux中是sendfile(),在java中被封裝為了FileChannel.transferTo()茧彤,就是把傳統(tǒng) read() and write() ==> sendfile()骡显,這一步系統(tǒng)調(diào)用直接將數(shù)據(jù)從內(nèi)核緩沖區(qū) ==> socket緩沖區(qū)省略了內(nèi)核緩沖區(qū)==>用戶緩沖區(qū)曾掂,用戶緩沖區(qū)==>socket緩沖區(qū)
    沒有零拷貝技術(shù)的時候--read and write

    有零拷貝技術(shù)的時候--sendfile()

綜上惫谤,nio是java對操作系統(tǒng)epoll等多路復(fù)用系統(tǒng)調(diào)用的封裝,而netty則是在修復(fù)nio bug的同時珠洗、支持了更加豐富定制化的擴(kuò)展(工頭和打工人職責(zé)分離溜歪、pipeline責(zé)任鏈)。

refercences

  1. ^ netty源碼
  2. ^ 如果這篇文章說不清epoll的本質(zhì)许蓖,那就過來掐死我吧蝴猪!
  3. ^ 徹底理解 IO 多路復(fù)用實現(xiàn)機(jī)制
  4. ^《Netty權(quán)威指南(第2版)》 - 李林鋒
  5. ^ Netty中的異步串行無鎖化
  6. ^ 零拷貝(Zero-copy)及其應(yīng)用詳解
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市膊爪,隨后出現(xiàn)的幾起案子自阱,更是在濱河造成了極大的恐慌,老刑警劉巖米酬,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沛豌,死亡現(xiàn)場離奇詭異,居然都是意外死亡赃额,警方通過查閱死者的電腦和手機(jī)加派,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來跳芳,“玉大人哼丈,你說我怎么就攤上這事∩秆希” “怎么了醉旦?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長桨啃。 經(jīng)常有香客問我车胡,道長,這世上最難降的妖魔是什么照瘾? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任匈棘,我火速辦了婚禮,結(jié)果婚禮上析命,老公的妹妹穿的比我還像新娘主卫。我一直安慰自己逃默,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布簇搅。 她就那樣靜靜地躺著完域,像睡著了一般。 火紅的嫁衣襯著肌膚如雪瘩将。 梳的紋絲不亂的頭發(fā)上吟税,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機(jī)與錄音姿现,去河邊找鬼肠仪。 笑死,一個胖子當(dāng)著我的面吹牛备典,可吹牛的內(nèi)容都是我干的异旧。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼提佣,長吁一口氣:“原來是場噩夢啊……” “哼吮蛹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起镐依,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤匹涮,失蹤者是張志新(化名)和其女友劉穎天试,沒想到半個月后槐壳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡喜每,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年务唐,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片带兜。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡枫笛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出刚照,到底是詐尸還是另有隱情刑巧,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布无畔,位于F島的核電站啊楚,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏浑彰。R本人自食惡果不足惜恭理,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望郭变。 院中可真熱鬧颜价,春花似錦涯保、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至横辆,卻和暖如春撇他,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背狈蚤。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工困肩, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人脆侮。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓锌畸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親靖避。 傳聞我的和親對象是個殘疾皇子潭枣,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

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