一辐脖、Netty框架簡(jiǎn)介
(本文中部分圖片摘自Netty-In-Depth)
Netty是一款以異步事件為驅(qū)動(dòng)的網(wǎng)絡(luò)開發(fā)框架和工具影锈,能夠快速的幫助開發(fā)者開發(fā)出可維護(hù)的高性能计福,高擴(kuò)張性的服務(wù)器和客戶端。
二薄湿、Netty相較于其他I/O編程的優(yōu)點(diǎn)
1叫倍、BIO編程
在基于傳統(tǒng)同步阻塞模型開發(fā)中,ServerSocket 負(fù)責(zé)綁定 IP 地址豺瘤,啟動(dòng)監(jiān)聽端口段标;Socket 負(fù)責(zé)發(fā)起連接操作。連接成功之后炉奴,雙方通過輸入和輸出流進(jìn)行同步阻塞式通信。server端為每一個(gè)連上來的client端新建一個(gè)線程進(jìn)行鏈路處理蛇更,處理完成之后通過輸出流返回應(yīng)答到客戶端瞻赶,然后線程銷毀。也就是典型的一請(qǐng)求一應(yīng)答通信模型派任。但是這種模型在連接的客戶端數(shù)量龐大的時(shí)候砸逊,相應(yīng)的服務(wù)端線程數(shù)量也會(huì)劇增,這就會(huì)使服務(wù)端因?yàn)榫€程數(shù)量過多而宕機(jī)掌逛。
2师逸、偽異步IO編程
偽異步IO線程其實(shí)就是在BIO編程基礎(chǔ)上增加了線程池,將處理客戶端連接請(qǐng)求的操作交給線程池去處理豆混,這樣線程數(shù)量就處于可控狀態(tài)篓像,可以有效的防止線程耗盡动知,但是這種模型會(huì)出現(xiàn)通信時(shí)間過長(zhǎng)導(dǎo)致級(jí)聯(lián)故障:比如服務(wù)端處理時(shí)間過長(zhǎng),或者其他線程出現(xiàn)故障员辩,由于IO操作是阻塞的盒粮,因此假如當(dāng)前所有可用線程都被阻塞了,那么后續(xù)的所有連接都會(huì)在隊(duì)列中排隊(duì)等待奠滑,當(dāng)隊(duì)列達(dá)到最大可容納數(shù)量時(shí)丹皱,后續(xù)入隊(duì)列操作會(huì)被阻塞。這樣acceptor因?yàn)樽枞诰€程池的隊(duì)列中宋税,所以無法處理后續(xù)客戶端的連接請(qǐng)求摊崭,出現(xiàn)大量的連接超時(shí)。
3杰赛、NIO編程
前面兩種編程模型出現(xiàn)的問題其實(shí)還是在于IO操作是同步阻塞的呢簸,所以要解決這些問題,最好的辦法就是從“同步阻塞”這方面入手淆攻,因此JAVA提供了NIO類庫阔墩,其實(shí)就是讓JAVA支持非阻塞IO,與傳統(tǒng)BIO編程中的Socket連接方式來說瓶珊,我們通過Socket跟ServerSocket來進(jìn)行連接啸箫、監(jiān)聽端口、獲取輸入輸出流等操作伞芹,而與之對(duì)應(yīng)的忘苛,NIO提供了SocketChannel和ServerSocketChannel,我們可以稱其為“通道”唱较,它們支持阻塞跟非阻塞兩種模式扎唾,阻塞方式會(huì)出現(xiàn)上面我們提到過的問題,而非阻塞模式則可以大大提高性能南缓。一般來說胸遇,低負(fù)載、低并發(fā)的應(yīng)用程序可以選擇同步阻塞 I/O 以降低編程復(fù)雜度汉形,但是對(duì)于高負(fù)載纸镊、高并發(fā)的網(wǎng)絡(luò)應(yīng)用,需要使用 NIO 的非阻塞模式進(jìn)行開發(fā)概疆。
4逗威、AIO編程
NIO編程雖然是非阻塞的,但是他依然采用的是同步IO(多路復(fù)用)岔冀。也就是說需要通過一個(gè)多路復(fù)用器(Selector)對(duì)注冊(cè)的通道進(jìn)行輪詢操作凯旭,這對(duì)性能也會(huì)有所影響,所以便有了NIO2.0,它引入了異步通道的概念罐呼,提供了異步文件通道和異步套接字通道的實(shí)現(xiàn)鞠柄。NIO2.0是異步非阻塞IO,不需要通過多路復(fù)用器(Selector)來對(duì)注冊(cè)的通道進(jìn)行輪詢操作即可實(shí)現(xiàn)異步讀寫弄贿。
剛剛有提到異步IO是異步非阻塞的春锋,它與阻塞、非阻塞差凹、多路復(fù)用等IO的區(qū)別可以參考http://blog.csdn.net/zhangzeyuaaa/article/details/42609723 簡(jiǎn)單來說異步非阻塞就是應(yīng)用發(fā)起讀寫請(qǐng)求之后就交由系統(tǒng)去處理期奔,等操作完成之后,系統(tǒng)會(huì)通過回調(diào)來通知應(yīng)用操作結(jié)果危尿。
三呐萌、Netty架構(gòu)
Reactor層的職責(zé)主要是負(fù)責(zé)監(jiān)聽網(wǎng)絡(luò)讀寫、客戶端連接等事件谊娇,將網(wǎng)絡(luò)數(shù)據(jù)讀到內(nèi)存緩存中肺孤,上層可通過ByteBuf類讀取數(shù)據(jù),Reactor還負(fù)責(zé)觸發(fā)事件济欢,產(chǎn)生的事件交由Pipeline處理赠堵。
Pipeline層是基于責(zé)任鏈模式實(shí)現(xiàn)的,用戶定制的各種Handler組成一個(gè)鏈?zhǔn)浇Y(jié)構(gòu)由Pipeline管理法褥,當(dāng)事件觸發(fā)時(shí)茫叭,Pipeline尋找最接近的Handler并執(zhí)行,處理完后繼續(xù)將事件傳給下一個(gè)Handler處理半等。如下圖所示:
以下內(nèi)容根據(jù)官網(wǎng)的描述翻譯:
一個(gè)Inbound事件交由InboundHandler類處理揍愁,方向?yàn)樽缘紫蛏希魅氲臄?shù)據(jù)通常是通過實(shí)際的輸入操作從服務(wù)端讀取杀饵,如SocketChannel.read(ByteBuffer)
莽囤。當(dāng)一個(gè)Inbound事件流到最頂層的InboundHandler后將會(huì)被廢棄或者被記錄下來(當(dāng)你需要的時(shí)候)。
一個(gè)Outbound事件由OutboundHandler類處理切距,處理方向?yàn)橛缮现料滦喽校粋€(gè)OutboundHandler通常會(huì)生成或轉(zhuǎn)換Outbound數(shù)據(jù)流,如write請(qǐng)求谜悟。如果Outbound事件流過最底部的OutboundHandler饵沧,它將會(huì)交給關(guān)聯(lián)了一個(gè)Channel的I/O線程處理,I/O線程通常會(huì)執(zhí)行實(shí)際的輸出操作如SocketChannel.write(ByteBuffer)
赌躺。
例如,假設(shè)我們創(chuàng)建如下的ChannelPipeline:
ChannelPipeline p = ...;
p.addLast("1", new InboundHandlerA());
p.addLast("2", new InboundHandlerB());
p.addLast("3", new OutboundHandlerA());
p.addLast("4", new OutboundHandlerB());
p.addLast("5", new InboundOutboundHandlerX());
如上所示羡儿,開頭為Inbound的類意味著它是一個(gè)實(shí)現(xiàn)了ChannelInboundHandler接口的類礼患,以O(shè)utbound為開頭的類則是實(shí)現(xiàn)了ChannelOutboundHandler接口的類,當(dāng)一個(gè)Inbound事件觸發(fā),ChannelPipeline只會(huì)把Inbound事件交給實(shí)現(xiàn)了ChannelInboundHandler接口的類處理缅叠,而且執(zhí)行順序是1悄泥、2、5肤粱;同理弹囚,當(dāng)一個(gè)Outbound事件觸發(fā)則只會(huì)交給實(shí)現(xiàn)了ChannelOutboundHandler接口的類處理,執(zhí)行順序相反领曼,為5鸥鹉、4、3(因?yàn)椤?”號(hào)Handler兩種接口都實(shí)現(xiàn)了庶骄,所以當(dāng)然兩種事件發(fā)生時(shí)都會(huì)流入該類)毁渗。
四、Netty線程模型
Netty提供了多種線程模型的實(shí)現(xiàn)方式单刁,用戶可以根據(jù)自身應(yīng)用場(chǎng)景選擇相應(yīng)的線程模型灸异。
由于Netty使用的是異步非阻塞I/O,所有的I/O操作都不會(huì)導(dǎo)致線程被掛起羔飞,所以理論上一個(gè)線程是可以處理所有跟I/O有關(guān)的操作肺樟。通過 Acceptor 類接收客戶端的 TCP連接請(qǐng)求消息,當(dāng)鏈路建立成功之后逻淌,通過 Dispatch 將對(duì)應(yīng)的 ByteBuffer 派發(fā)到指定的 Handler 上么伯,進(jìn)行消息解碼。用戶線程消息編碼后通過 NIO 線程將消息發(fā)送給客戶端恍风。在一些小容量應(yīng)用場(chǎng)景下蹦狂,可以使用單線程模型。但是這對(duì)于高負(fù)載朋贬、大并發(fā)的應(yīng)用場(chǎng)景卻不合適凯楔,會(huì)出現(xiàn)如下問題:
- 一個(gè)NIO線程同時(shí)處理成百上千的鏈路,性能上無法支撐锦募,即便NIO線程的CPU負(fù)荷達(dá)到100%摆屯,也無法滿足海量消息的編碼、解碼糠亩、讀取和發(fā)送虐骑。
- 當(dāng)NIO線程負(fù)載過重之后,處理速度將變慢赎线,這會(huì)導(dǎo)致大量客戶端連接超時(shí)廷没,超時(shí)之后往往會(huì)進(jìn)行重發(fā),這更加重了NIO線程的負(fù)載垂寥,最終會(huì)導(dǎo)致大量消息積壓和處理超時(shí)颠黎,成為系統(tǒng)的性能瓶頸另锋。
- 可靠性問題:一旦NIO線程意外跑飛,或者進(jìn)入死循環(huán)狭归,會(huì)導(dǎo)致整個(gè)系統(tǒng)通信模塊不可用夭坪,不能接收和處理外部消息,造成節(jié)點(diǎn)故障过椎。
為了處理這些問題室梅,就演進(jìn)出了Reactor多線程模型:
Rector 多線程模型與單線程模型最大的區(qū)別就是有一組 NIO 線程來處理 I/O操作。Reactor 多線程模型的特點(diǎn)如下疚宇。
- 有專門一個(gè)NIO線程——Acceptor線程用于監(jiān)聽服務(wù)端亡鼠,接收客戶端的TCP連接請(qǐng)求。
- 網(wǎng)絡(luò)I/O操作——讀灰嫉、寫等由一個(gè)NIO線程池負(fù)責(zé)拆宛,線程池可以采用標(biāo)準(zhǔn)的JDK線程池實(shí)現(xiàn),它包含一個(gè)任務(wù)隊(duì)列和N個(gè)可用的線程讼撒,由這些NIO線程負(fù)責(zé)消息的讀取浑厚、解碼、編碼和發(fā)送根盒。
- 一個(gè)NIO線程可以同時(shí)處理N條鏈路钳幅,但是一個(gè)鏈路只對(duì)應(yīng)一個(gè)NIO線程,防止發(fā)生并發(fā)操作問題炎滞。
在絕大多數(shù)場(chǎng)景下敢艰,Reactor 多線程模型可以滿足性能需求。但是册赛,在個(gè)別特殊場(chǎng)景中钠导,一個(gè) NIO 線程負(fù)責(zé)監(jiān)聽和處理所有的客戶端連接可能會(huì)存在性能問題。例如并發(fā)百萬客戶端連接森瘪,或者服務(wù)端需要對(duì)客戶端握手進(jìn)行安全認(rèn)證牡属,但是認(rèn)證本身非常損耗性能。在這類場(chǎng)景下扼睬,單獨(dú)一個(gè) Acceptor 線程可能會(huì)存在性能不足的問題逮栅,為了解決性能問題,產(chǎn)生了第三種 Reactor 線程模型——主從Reactor 多線程模型窗宇。
主從 Reactor 線程模型的特點(diǎn)是:服務(wù)端用于接收客戶端連接的不再是一個(gè)單獨(dú)的 NIO 線程措伐,而是一個(gè)獨(dú)立的 NIO 線程池。Acceptor 接收到客戶端 TCP連接請(qǐng)求并處理完成后(可能包含接入認(rèn)證等)军俊,將新創(chuàng)建的 SocketChannel注 冊(cè) 到 I/O 線 程 池(sub reactor 線 程 池) 的 某 個(gè) I/O 線 程 上侥加, 由 它 負(fù) 責(zé)SocketChannel 的讀寫和編解碼工作。Acceptor 線程池僅僅用于客戶端的登錄粪躬、握手和安全認(rèn)證担败,一旦鏈路建立成功矗蕊,就將鏈路注冊(cè)到后端 subReactor 線程池的 I/O 線程上,由 I/O 線程負(fù)責(zé)后續(xù)的 I/O 操作氢架。利用主從 NIO 線程模型,可以解決一個(gè)服務(wù)端監(jiān)聽線程無法有效處理所有客戶端連接的性能不足問題朋魔。因此岖研,在 Netty 的官方 demo 中,推薦使用該線程模型警检。
五孙援、Android端基于Netty實(shí)現(xiàn)的Socket通信Demo
Demo中后臺(tái)開啟一個(gè)Service作為服務(wù)端,客戶端輸入信息發(fā)送給服務(wù)端扇雕,服務(wù)端接收到信息后在客戶端發(fā)送過來的信息前加上“res”后返回給客戶端顯示拓售,數(shù)據(jù)傳輸格式使用的是Google使用的Protocol Buffer。Demo鏈接:https://github.com/qaz3366639/NettyDemo
服務(wù)端的配置代碼如下:
mWorkerGroup = new NioEventLoopGroup();
//服務(wù)端啟動(dòng)引導(dǎo)類,負(fù)責(zé)配置服務(wù)端信息
mServerBootstrap = new ServerBootstrap();
mServerBootstrap.group(mWorkerGroup)
.channel(NioServerSocketChannel.class)
.handler(new ChannelInitializer<NioServerSocketChannel>() {
@Override
protected void initChannel(NioServerSocketChannel nioServerSocketChannel) throws Exception {
ChannelPipeline pipeline = nioServerSocketChannel.pipeline();
pipeline.addLast("ServerSocketChannel out", new OutBoundHandler());
pipeline.addLast("ServerSocketChannel in", new InBoundHandler());
}
})
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//為連接上來的客戶端設(shè)置pipeline
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast("decoder", new ProtobufDecoder(Test.ProtoTest.getDefaultInstance()));
pipeline.addLast("encoder", new ProtobufEncoder());
pipeline.addLast("out1", new OutBoundHandler());
pipeline.addLast("out2", new OutBoundHandler());
pipeline.addLast("in1", new InBoundHandler());
pipeline.addLast("in2", new InBoundHandler());
pipeline.addLast("handler", new ServerChannelHandler());
}
});
channelFuture = mServerBootstrap.bind(PORT_NUMBER);```
客戶端配置如下:
if (mBootstrap == null) {
mWorkerGroup = new NioEventLoopGroup();
mBootstrap = new Bootstrap();
mBootstrap.group(mWorkerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast("decoder", new ProtobufDecoder(Test.ProtoTest.getDefaultInstance()));
pipeline.addLast("encoder", new ProtobufEncoder());
pipeline.addLast("handler", mDispatcher);
}
})
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
}
ChannelFuture future = mBootstrap.connect(mServerAddress);
future.addListener(mConnectFutureListener);