摘要:我想這很好的解釋了中,僅僅一個(gè)都這么復(fù)雜,在單線程或者說串行的程序中,編程往往是很簡單的,說白了就是調(diào)用,調(diào)用,調(diào)用然后返回。
Netty源碼分析(三) 前提概要
這次停更很久了,原因是中途迷茫了一段時(shí)間,不過最近調(diào)整過來了。不過有點(diǎn)要說下,前幾天和業(yè)內(nèi)某個(gè)大佬聊天,收獲很多,所以這篇博文和之前也會(huì)不太一樣,我們會(huì)先從如果是我自己去實(shí)現(xiàn)這個(gè)功能需要怎么做開始,然后去看netty源碼,與自己的實(shí)現(xiàn)做對(duì)比。
Server端NIO復(fù)習(xí)Netty有基于很多IO的實(shí)現(xiàn)(BIO/OIO/NIO...),而我們最常用的也就是NIO了,我們這次分析源碼,也是基于NIO的實(shí)現(xiàn),前提條件要先弄清楚NIO的流程,再分析Netty是怎么基于他開發(fā)出這個(gè)高性能網(wǎng)絡(luò)框架的,這里我們先來簡單的復(fù)習(xí)下,已經(jīng)熟悉的同學(xué)可以跳過不看了。
四個(gè)步驟拋開數(shù)據(jù)的讀寫,我們把NIO服務(wù)端監(jiān)聽分成四個(gè)步驟
channel初始化
注冊(cè) selector到 channel上
channel綁定端口
循環(huán)select 等待事件
其中第二步又分為幾個(gè)小步驟
創(chuàng)建selector
調(diào)用channel的register
然后第四步也分為幾個(gè)步驟
selector.select(或者幾個(gè)帶參的重寫方法)
處理所有的selectedKey
然后再繼續(xù)分解:[處理所有的selectedKey]
獲取selectedKey的channel
根據(jù)selectedKey的狀態(tài)處理channel
其中需要注意的是:如果key的狀態(tài)(或者說是readyops)是accept的話,需要把channel轉(zhuǎn)成ServerSocktChannel然后通過accept獲取新的channel,再把這個(gè)selector注冊(cè)到這個(gè)channel中去。如果是其他的讀寫狀態(tài)的話,獲取的channel要轉(zhuǎn)為SocketChannel,然后進(jìn)行操作。
Register的實(shí)現(xiàn)通過NIO的復(fù)習(xí),我們可以看到一個(gè)完整的NIO的創(chuàng)建流程。在之前的兩篇博文中,我們實(shí)現(xiàn)了第一步初始化以及第三步綁定端口的流程,現(xiàn)在我們先來實(shí)現(xiàn)channel的register。
這里多提一句,網(wǎng)上很多人都是在bind端口之后,再channel register的,但是其實(shí)這個(gè)前后順序并沒有關(guān)系,Netty中則是在bind之前進(jìn)行的。
先看下之前的代碼,我們?cè)赿oBind方法中,先實(shí)例化了一個(gè)channel,然后調(diào)用channel的bind方法。
private void doBind(Integer port){ Channel channel = channelFactory.newChannel(); channel.bind(new InetSocketAddress(port)); }
這樣看,我們現(xiàn)在需要做的就是在這兩行代碼中插入一段channel register的代碼就可以了,那是不是可以這樣,Channel中新增一個(gè)register接口,然后在實(shí)現(xiàn)類中去實(shí)現(xiàn)他,最后在這里中間寫一句channel.register,貌似看起來沒問題啊,不管怎么樣,先動(dòng)手實(shí)現(xiàn)下。
/* in Channel Interface */ void register(Selector selector); /* in AbstractNioChannel */ @Override public void register(Selector selector) throws ClosedChannelException { ch().register(selector,0,null); } /*in AbstractBootstrap*/ try { channel.register(Selector.open()); } catch (IOException e) { e.printStackTrace(); }
我們最后的doBind看起來就會(huì)是這樣
private void doBind(Integer port){ Channel channel = channelFactory.newChannel(); try { channel.register(Selector.open()); } catch (IOException e) { e.printStackTrace(); } channel.bind(new InetSocketAddress(port)); }
功能貌似是實(shí)現(xiàn)了。。。但是,看起來是不是有些問題?
沒錯(cuò),首先看起來很丑,try/catch一大塊的很難看,但其次拋去美觀問題不談,這么寫不符合設(shè)計(jì)思想,我這個(gè)類是一個(gè)抽象的啟動(dòng)類,但是這個(gè)register只是NIO的寫法,那BIO,AIO...其他IO實(shí)現(xiàn)怎么辦?
那我們先來做下最簡單的改造,軟件不就是在一次次的重構(gòu)中,慢慢成長起來的嘛。我簡單的分了下步驟。
Channel接口更改,取消入?yún)⒑彤惓伋?/p>
void register();
抽象實(shí)現(xiàn)類中增加新的帶參方法,供接口方法調(diào)用
@Override public void register(){ try { register(Selector.open()); } catch (IOException e) { e.printStackTrace(); } } private void register(Selector selector) throws ClosedChannelException { ch().register(selector,0,null); }
doBind內(nèi)register調(diào)用
private void doBind(Integer port){ Channel channel = channelFactory.newChannel(); channel.register(); channel.bind(new InetSocketAddress(port)); }
這樣看,代碼是不是美觀很多?最關(guān)鍵的是,此時(shí)的我們的抽象啟動(dòng)類里,真正做到了抽象,不管什么IO實(shí)現(xiàn),都會(huì)去調(diào)用自身Channel實(shí)現(xiàn)的register方法。
目前來看還挺不錯(cuò)的,接下來看下Netty是怎么實(shí)現(xiàn)的吧,自己寫的思路對(duì)的,但是肯定有很多地方?jīng)]考慮到的。
Netty中的Register How to doNetty中,doBind方法里是包裝了一個(gè)initAndRegister方法去完成初始化和注冊(cè)地功能,這里我們直接看下initAndRegister這個(gè)方法體,看下Netty怎么做的
是不是和我們寫的差不多呢,先實(shí)例化一個(gè)channel,然后再進(jìn)行register的,不過有兩點(diǎn)不同的地方,其是register之前先要調(diào)用一個(gè)init方法,玩過Netty的應(yīng)該都清楚,主要是處理我們自定義的一些配置的,這里我們先不提,等后面再說。先來說下第二個(gè)不同,也就是register方法。
我們的代碼里,是直接通過擴(kuò)展channel的接口,直接調(diào)用channel的register的方法的,但是Netty這里則是通過group(包含了Netty很重要的一個(gè)角色EventLoop,我們后面會(huì)詳細(xì)說他),傳入channel對(duì)象,然后再去調(diào)用channel的register方法,不信,我們看下他的調(diào)用鏈。
撇去Netty復(fù)雜的繼承關(guān)系,我們最終定位到方法的最終調(diào)用的地方,SingleThreadEventLoop里
我們可以看到,register方法里,先是包裝了一個(gè)promise對(duì)象(實(shí)現(xiàn)promise接口的對(duì)象,這里維護(hù)了channel對(duì)象和這個(gè)eventloop對(duì)象,題外話:promise接口其實(shí)是future接口的一個(gè)超集),然后調(diào)用了promise里的channel的unsafe對(duì)象的register(ps:好繞口的感覺),這個(gè)unsafe就是channel的一個(gè)內(nèi)部類,感覺越來越接近了,我們就到unsafe里面看下register方法吧。
這下應(yīng)該很清晰了吧,unsafe的register里又調(diào)用了doRegister,然后就和我們的方法差不多了,這里的javaChannel其實(shí)就是JAVA NIO的channel對(duì)象,唯一不同的就是這里的selector不是open出來的,而是早在eventloop初始化的時(shí)候就存下來了,可能有人會(huì)問:這里eventloop是怎么來的呢?答案就在上面,在調(diào)用unsafe的register的時(shí)候,我們傳入了兩個(gè)對(duì)象,第一個(gè)this就是指向的eventloop(別忘了我們是在eventloop里調(diào)用的register)。然后就是把this(AbstractNioChannel)作為register的第三個(gè)參數(shù)(后面可以通過selectedKey.attachment獲取到)
How to sayNetty中怎么做的我們也看過了,相信你和我一樣,有很多疑問(大神們請(qǐng)自動(dòng)無視)。這里我舉出我覺得比較重要的兩個(gè)。
為什么一個(gè)注冊(cè)這么復(fù)雜?明明用原生NIO只需要一行就搞定的,又是promise又是unsafe,在channel.register之間抽象出來兩層。
為什么最后的doRegister里,要用一個(gè)無限循環(huán)包起來呢?
答案還是要從源碼里獲得,先看下eventloop的register的接口注釋
簡單來說,就是把eventloop對(duì)象注冊(cè)到channel里,并且要能知道注冊(cè)的結(jié)果,并且返回。這句話,可能有些同學(xué)不太明白,這是Java中的異步調(diào)用,我們傳統(tǒng)開發(fā)的Java web程序,我們所處理的基本都是單線程的模式,首先這符合人的大腦的習(xí)慣嘛(人的大腦本質(zhì)是串行處理事情的),其次是多線程的部分,框架和容器已經(jīng)幫我做好了,所以可能會(huì)不太理解異步這個(gè)概念。
然后再看下unsafe這個(gè)register接口的注釋
看到了吧,執(zhí)行promise的channel的注冊(cè)并在完成的時(shí)候通知返回,這是靠一個(gè)叫觀察者模式來完成的,具體的內(nèi)容在下一章會(huì)詳細(xì)講解。
我想這很好的解釋了Netty中,僅僅一個(gè)register都這么復(fù)雜,在單線程或者說串行的程序中,編程往往是很簡單的,說白了就是調(diào)用,調(diào)用,調(diào)用然后返回。但Netty是個(gè)充滿并行和異步的程序,所以光從設(shè)計(jì)上就會(huì)比較復(fù)雜,這讓我想起了沈神說的一句話:簡單的容易理解的模型性能都差,性能好的都很復(fù)雜。雖然他指的是數(shù)據(jù)庫的設(shè)計(jì),但是我覺得道理是相通的。
還有一點(diǎn)就是這些設(shè)計(jì)也是為了同時(shí)兼容服務(wù)端和客戶端,軟件開發(fā)的思想里,很重要的一點(diǎn)就是復(fù)用。這也是為什么會(huì)有第二個(gè)疑問,我們還是要看下注釋。
jdk的register的注釋說明,如果這個(gè)channel已經(jīng)被注冊(cè)過了,并且再次注冊(cè)的過程中連接斷了,則會(huì)拋出這個(gè)CanceledKeyException異常,那么這里捕獲了異常并且調(diào)用select.selectNow,理論上會(huì)從selector中移除這個(gè)無效的channel,但是事實(shí)上并沒有,所以下面也寫到了,可能是個(gè)jdk bug,所以要顯示的往外繼續(xù)拋出異常。
仔細(xì)想想,如果僅僅是服務(wù)端的channel register,怎么可能發(fā)生上述的情況呢,所以這里的doRegister同時(shí)也是客戶端channel復(fù)用的注冊(cè)方法。
總結(jié)&&預(yù)告這一章差不多就結(jié)束了,我們做了哪些事情呢?
復(fù)習(xí)Java NIO 流程
動(dòng)手實(shí)現(xiàn)channel register
讀Netty源碼,理解Netty是如何實(shí)現(xiàn)register以及為什么這樣實(shí)現(xiàn)的
軟件開發(fā)的思想
然后畫個(gè)簡單的圖來總結(jié)下把。
接下來在實(shí)現(xiàn)循環(huán)select監(jiān)聽和處理事件之前,我們先實(shí)現(xiàn)一下Netty中一個(gè)很重要的EventLoop,并且了解下運(yùn)用到的異步框架(Future and Promise),希望我的拖延癥得到解決,應(yīng)該在下周會(huì)寫出來。。。
希望同學(xué)們看到后會(huì)有收獲,如果我有理解不對(duì)的地方,歡迎來指正。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/68364.html
摘要:對(duì)于,目前大家只知道是個(gè)線程組,其內(nèi)部到底如何實(shí)現(xiàn)的,它的作用到底是什么,大家也都不太清楚,由于篇幅原因,這里不作詳細(xì)介紹,后面會(huì)有文章作專門詳解。 在上一篇《ServerBootstrap 與 Bootstrap 初探》中,我們已經(jīng)初步的了解了ServerBootstrap是netty進(jìn)行服務(wù)端開發(fā)的引導(dǎo)類。 且在上一篇的服務(wù)端示例中,我們也看到了,在使用netty進(jìn)行網(wǎng)絡(luò)編程時(shí),我...
摘要:下面無恥的貼點(diǎn)源碼。啟動(dòng)類我們也學(xué),把啟動(dòng)類抽象成兩層,方便以后寫客戶端。別著急,我們慢慢來,下一篇我們會(huì)了解以及他的成員,然后,完善我們的程序,增加其接收數(shù)據(jù)的能力。文章的源碼我會(huì)同步更新到我的上,歡迎大家,哈哈。 廢話兩句 這次更新拖了很長時(shí)間,第一是自己生病了,第二是因?yàn)樽铋_始這篇想寫的很大,然后構(gòu)思了很久,發(fā)現(xiàn)不太合適把很多東西寫在一起,所以做了點(diǎn)拆分,準(zhǔn)備國慶前完成這篇博客。...
摘要:一些想法這個(gè)系列想開很久了,自己使用也有一段時(shí)間了,利用也編寫了一個(gè)簡單的框架,并運(yùn)用到工作中了,感覺還不錯(cuò),趁著這段時(shí)間工作不是很忙,來分析一波源碼,提升下技術(shù)硬實(shí)力。 一些想法 這個(gè)系列想開很久了,自己使用netty也有一段時(shí)間了,利用netty也編寫了一個(gè)簡單的框架,并運(yùn)用到工作中了,感覺還不錯(cuò),趁著這段時(shí)間工作不是很忙,來分析一波源碼,提升下技術(shù)硬實(shí)力。 結(jié)構(gòu) 這里先看下net...
摘要:本篇將通過實(shí)例化過程,來深入剖析。及初始化完成后,它們會(huì)相互連接。我們?cè)诨氐降臉?gòu)造方法父類構(gòu)造方法調(diào)用完成后,還要初始化一下自己的配置對(duì)象是的內(nèi)部類而又是繼承自,通過代碼分析,此對(duì)象就是就會(huì)對(duì)底層一些配置設(shè)置行為的封裝。 根據(jù)上一篇《Netty4.x 源碼實(shí)戰(zhàn)系列(二):服務(wù)端bind流程詳解》所述,在進(jìn)行服務(wù)端開發(fā)時(shí),必須通過ServerBootstrap引導(dǎo)類的channel方法來...
摘要:而用于主線程池的屬性都定義在中本篇只是簡單介紹了一下引導(dǎo)類的配置屬性,下一篇我將詳細(xì)介紹服務(wù)端引導(dǎo)類的過程分析。 從Java1.4開始, Java引入了non-blocking IO,簡稱NIO。NIO與傳統(tǒng)socket最大的不同就是引入了Channel和多路復(fù)用selector的概念。傳統(tǒng)的socket是基于stream的,它是單向的,有InputStream表示read和Outpu...
閱讀 2510·2021-11-25 09:43
閱讀 1274·2021-09-07 10:16
閱讀 2696·2021-08-20 09:38
閱讀 2999·2019-08-30 15:55
閱讀 1554·2019-08-30 13:21
閱讀 972·2019-08-29 15:37
閱讀 1502·2019-08-27 10:56
閱讀 2140·2019-08-26 13:45