nettyServer的標(biāo)準(zhǔn)啟動代碼
netty官方源碼中的示例 DiscardServer 中nettyServer的標(biāo)準(zhǔn)啟動姿勢如下
- 初始化ServerBootstrap版仔,這里面保存著所有nettyServer運(yùn)行過程中需要的各種信息些举,相當(dāng)于整個nettyServer的環(huán)境
- 綁定操作,將形如 127.0.0.1:8888 這樣的地址端口綁定到nettyServer上沼侣,即打開相應(yīng)端口的socket連接丧靡,并且把接收連接后的響應(yīng)事件與bootstrap關(guān)聯(lián)(這一步之后服務(wù)就可以開始接收連接請求了)
- 等待關(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的一種封裝
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阁猜。
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ì)上是initAndRegister
和channel.bind
這兩個方法蹭沛,這兩個方法的執(zhí)行是通過向NioEventLoop提交任務(wù)來執(zhí)行的逆甜。所以我們需要先分析NioEventLoop的實現(xiàn)。
ServerBootstrap的模板代碼中會設(shè)置bossGroup和workerGroup致板,分別是兩個NioEventLoopGroup類型交煞,里面包含若干個NioEventLoop,是任務(wù)的核心斟或。
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
方法。
當(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)的。
inbound與outbound
netty中有一對概念患民,inboundHandler與outboundHandler缩举,如下圖所示,分別用于處理read和write流程匹颤,同樣是在第一張圖中ServerBootstrap初始化的時候設(shè)置到bootstrap的嗦明。這些handler最終兜兜轉(zhuǎn)轉(zhuǎn)會設(shè)置到NioServerSocketChannel#pipeline
中钉答,在channel收到讀/寫事件時從不同方向順序執(zhí)行
inbound與outbound的總結(jié)如下
inbound/outbound | 典型用法 | 需要關(guān)注的類 | 需要關(guān)注的方法 |
---|---|---|---|
inbound | LengthFieldBasedFrameDecoder - 收到半包請求之后黏包 | ChannelInboundHandler | channelRead() |
outbound | LengthFieldPrepender - 為要發(fā)送的請求增加頭部信息標(biāo)識消息長度 | ChannelOutboundHandler | write/writeAndFlush() |
netty中的方法調(diào)用都是向eventloop提交任務(wù)
在了解了NioEventLoop的大致原理之后肉微,我們可以回頭來看bootstrap.bind()方法的兩個核心操作遵馆,上面提到bind方法時,為了簡化邏輯赦肃,我們對其執(zhí)行邏輯進(jìn)行了最大規(guī)模的概括溅蛉。而實際上讀者可以自行下載源碼看到公浪,它們的本質(zhì)都是向eventloop中提交了一個任務(wù)到taskQueue,并觸發(fā)了NioEventLoop#run
方法的執(zhí)行船侧。
通過追蹤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è)了爪膊。
- 傳統(tǒng)的nio模型是一條while循環(huán)線程通關(guān)裸准,一方面這不利于發(fā)揮現(xiàn)在多核CPU的全部能力;另一方面單線程因為任何原因掛掉就會導(dǎo)致nio直接掛掉赔硫,穩(wěn)定性很差炒俱。因此netty有了
-
為什么大家都在推崇用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ù)其實就是一個操作系統(tǒng)的系統(tǒng)調(diào)用疮方,在linux中是
綜上惫谤,nio是java對操作系統(tǒng)epoll等多路復(fù)用系統(tǒng)調(diào)用的封裝,而netty則是在修復(fù)nio bug的同時珠洗、支持了更加豐富定制化的擴(kuò)展(工頭和打工人職責(zé)分離溜歪、pipeline責(zé)任鏈)。