首先引用Netty官網(wǎng)的內(nèi)容對Netty進(jìn)行一個(gè)正式的介紹窒升。
Netty是為了快速開發(fā)可維護(hù)的高性能蓝晒、高可擴(kuò)展声功、網(wǎng)絡(luò)服務(wù)器和客戶端程序而提供的異步事件驅(qū)動基礎(chǔ)框架和工具怀偷。換句話說,Netty是一個(gè)Java NIO客戶端/服務(wù)器框架扔仓『职拢基于Netty,可以快速輕松地開發(fā)網(wǎng)絡(luò)服務(wù)器和客戶端的應(yīng)用程序翘簇。與直接使用Java NIO相比撬码,Netty給大家造出了一個(gè)非常優(yōu)美的輪子,它可以大大簡化了網(wǎng)絡(luò)編程流程版保。例如呜笑,Netty極大地簡化TCP、UDP套接字彻犁、HTTP Web服務(wù)程序的開發(fā)叫胁。
Netty的目標(biāo)之一,是要使開發(fā)可以做到“快速和輕松”汞幢。除了做到“快速和輕松”的開發(fā)TCP/UDP等自定義協(xié)議的通信程序之外驼鹅,Netty經(jīng)過精心設(shè)計(jì),還可以做到“快速和輕松”地開發(fā)應(yīng)用層協(xié)議的程序,如FTP谤民,SMTP堰酿,HTTP以及其他的傳統(tǒng)應(yīng)用層協(xié)議。
Netty的目標(biāo)之二张足,是要做到高性能触创、高可擴(kuò)展性∥梗基于Java的NIO哼绑,Netty設(shè)計(jì)了一套優(yōu)秀的Reactor反應(yīng)器模式。后面會詳細(xì)介紹Netty中反應(yīng)器模式的實(shí)現(xiàn)碉咆。在基于Netty的反應(yīng)器模式實(shí)現(xiàn)中的Channel(通道)抖韩、Handler(處理器)等基類,能快速擴(kuò)展以覆蓋不同協(xié)議疫铜、完成不同業(yè)務(wù)處理的大量應(yīng)用類茂浮。
6.1 第一個(gè)Netty的實(shí)踐案例DiscardServer
在開始實(shí)踐之前,第一步就是要準(zhǔn)備Netty的版本壳咕,配置好開發(fā)環(huán)境席揽。
6.1.1 創(chuàng)建第一個(gè)Netty項(xiàng)目
首先我們需要創(chuàng)建項(xiàng)目,項(xiàng)目名稱是NettyDemos谓厘。這是一個(gè)丟棄服務(wù)器(DiscardServer)幌羞,功能很簡單:讀取客戶端的輸入數(shù)據(jù),直接丟棄竟稳,不給客戶端任何回復(fù)属桦。
在使用Netty前,首先需要考慮一下JDK的版本他爸,官網(wǎng)建議使用JDK1.6以上聂宾,本書使用的是JDK1.8。然后是Netty自己的版本诊笤,建議使用Netty 4.0以上的版本亏吝。雖然Netty在不斷升級,但是4.0以上的版本使用比較廣泛盏混。Netty曾經(jīng)升級到5.0,不過出現(xiàn)了一些問題惜论,版本又回退了许赃。本書使用的Netty版本是4.1.6。
使用maven導(dǎo)入Netty以依賴到工程(或項(xiàng)目)馆类,Netty的maven依賴如下:
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
那么現(xiàn)在可以正式開始編寫第一個(gè)項(xiàng)目程序了混聊。
6.1.2 第一個(gè)Netty服務(wù)器端程序
這里創(chuàng)建一個(gè)服務(wù)端類:NettyDiscardServer,用以實(shí)現(xiàn)消息的Discard“丟棄”功能乾巧,它的源代碼如下:
//...
public class NettyDiscardServer {
private final int serverPort;
ServerBootstrap b = new ServerBootstrap();
public NettyDiscardServer(int port) {
this.serverPort = port;
}
public void runServer() {
//創(chuàng)建反應(yīng)器線程組
EventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
EventLoopGroup workerLoopGroup = new NioEventLoopGroup();
try {
//1 設(shè)置反應(yīng)器線程組
b.group(bossLoopGroup, workerLoopGroup);
//2 設(shè)置nio類型的通道
b.channel(NioServerSocketChannel.class);
//3 設(shè)置監(jiān)聽端口
b.localAddress(serverPort);
//4 設(shè)置通道的參數(shù)
b.option(ChannelOption.SO_KEEPALIVE, true);
b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
//5 裝配子通道流水線
b.childHandler(new ChannelInitializer<SocketChannel>() {
//有連接到達(dá)時(shí)會創(chuàng)建一個(gè)通道
protected void initChannel(SocketChannelch) throws Exception {
// 流水線管理子通道中的Handler處理器
// 向子通道流水線添加一個(gè)handler處理器
ch.pipeline().addLast(new NettyDiscardHandler());
}
});
// 6 開始綁定服務(wù)器
// 通過調(diào)用sync同步方法阻塞直到綁定成功
ChannelFuturechannelFuture = b.bind().sync();
Logger.info(" 服務(wù)器啟動成功句喜,監(jiān)聽端口: " +
channelFuture.channel().localAddress());
// 7 等待通道關(guān)閉的異步任務(wù)結(jié)束
// 服務(wù)監(jiān)聽通道會一直等待通道關(guān)閉的異步任務(wù)結(jié)束
ChannelFuturecloseFuture = channelFuture.channel().closeFuture();
closeFuture.sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 8關(guān)閉EventLoopGroup预愤,
// 釋放掉所有資源包括創(chuàng)建的線程
workerLoopGroup.shutdownGracefully();
bossLoopGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
int port = NettyDemoConfig.SOCKET_SERVER_PORT;
new NettyDiscardServer(port).runServer();
}
}
如果是第一次看Netty開發(fā)的程序,上面的代碼是看不懂的咳胃,因?yàn)榇a里邊涉及很多的Netty組件植康。
Netty是基于反應(yīng)器模式實(shí)現(xiàn)的。還好展懈,大家已經(jīng)非常深入地了解了反應(yīng)器模式销睁,現(xiàn)在大家順藤摸瓜學(xué)習(xí)Netty的結(jié)構(gòu)就相對簡單了。
首先要說的是Reactor反應(yīng)器存崖。前面講到冻记,反應(yīng)器的作用是進(jìn)行一個(gè)IO事件的select查詢和dispatch分發(fā)。Netty中對應(yīng)的反應(yīng)器組件有多種来惧,應(yīng)用場景不同冗栗,用到的反應(yīng)器也各不相同。一般來說供搀,對應(yīng)于多線程的Java NIO通信的應(yīng)用場景隅居,Netty的反應(yīng)器類型為:NioEventLoopGroup。
在上面的例子中趁曼,使用了兩個(gè)NioEventLoopGroup實(shí)例军浆。第一個(gè)通常被稱為“包工頭”,負(fù)責(zé)服務(wù)器通道新連接的IO事件的監(jiān)聽挡闰。第二個(gè)通常被稱為“工人”乒融,主要負(fù)責(zé)傳輸通道的IO事件的處理。
其次要說的是Handler處理器(也稱為處理程序)摄悯。Handler處理器的作用是對應(yīng)到IO事件赞季,實(shí)現(xiàn)IO事件的業(yè)務(wù)處理。Handler處理器需要專門開發(fā)奢驯,稍后申钩,將專門對它進(jìn)行介紹。
再次瘪阁,在上面的例子中撒遣,還用到了Netty的服務(wù)啟動類ServerBootstrap,它的職責(zé)是一個(gè)組裝和集成器管跺,將不同的Netty組件組裝在一起义黎。另外,ServerBootstrap能夠按照應(yīng)用場景的需要豁跑,為組件設(shè)置好對應(yīng)的參數(shù)廉涕,最后實(shí)現(xiàn)Netty服務(wù)器的監(jiān)聽和啟動。服務(wù)啟動類ServerBootstrap也是本章重點(diǎn)之一,稍候另起一小節(jié)進(jìn)行詳細(xì)的介紹狐蜕。
6.1.3 業(yè)務(wù)處理器NettyDiscardHandler
在反應(yīng)器(Reactor)模式中宠纯,所有的業(yè)務(wù)處理都在Handler處理器中完成。這里編寫一個(gè)新類:NettyDiscardHandler层释。NettyDiscardHandler的業(yè)務(wù)處理很簡單:把收到的任何內(nèi)容直接丟棄(discard)婆瓜,也不會回復(fù)任何消息。代碼如下:
//...
public class NettyDiscardHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContextctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
try {
Logger.info("收到消息,丟棄如下:");
while (in.isReadable()) {
System.out.print((char) in.readByte());
}
System.out.println();
} finally {
ReferenceCountUtil.release(msg);
}
}
}
首先說明一下湃累,這里將引入一個(gè)新的概念:入站和出站勃救。簡單來說,入站指的是輸入治力,出站指的是輸出蒙秒。后面也會有詳細(xì)介紹。
Netty的Handler處理器需要處理多種IO事件(如可讀宵统、可寫)晕讲,對應(yīng)于不同的IO事件,Netty提供了一些基礎(chǔ)的方法马澈。這些方法都已經(jīng)提前封裝好瓢省,后面直接繼承或者實(shí)現(xiàn)即可。比如說痊班,對于處理入站的IO事件的方法勤婚,對應(yīng)的接口為ChannelInboundHandler入站處理接口,而ChannelInboundHandlerAdapter則是Netty提供的入站處理的默認(rèn)實(shí)現(xiàn)涤伐。
也就是說馒胆,如果要實(shí)現(xiàn)自己的入站處理器Handler,只要繼承ChannelInboundHandlerAdapter入站處理器凝果,再寫入自己的入站處理的業(yè)務(wù)邏輯祝迂。如果要讀取入站的數(shù)據(jù),只要寫在了入站處理方法channelRead中即可器净。
在上面例子中的channelRead方法型雳,它讀取了Netty的輸入數(shù)據(jù)緩沖區(qū)ByteBuf。Netty的ByteBuf山害,可以對應(yīng)到前面介紹的NIO的數(shù)據(jù)緩沖區(qū)纠俭。它們在功能上是類似的,不過相對而言浪慌,Netty的版本性能更好柑晒,使用也更加方便。后面會另開一節(jié)進(jìn)行詳細(xì)的介紹眷射。
6.1.4 運(yùn)行NettyDiscardServer
在上面的例子中,出現(xiàn)了Netty中的各種組件:服務(wù)器啟動器、緩沖區(qū)妖碉、反應(yīng)器涌庭、Handler業(yè)務(wù)處理器、Future異步任務(wù)監(jiān)聽欧宜、數(shù)據(jù)傳輸通道等坐榆。這些Netty組件都是需要掌握的,也都是我們在后面要專項(xiàng)學(xué)習(xí)的冗茸。
如果看不懂這個(gè)NettyDiscardServer程序席镀,一點(diǎn)兒也沒關(guān)系。這個(gè)程序在本章的目的夏漱,僅僅是為大家展示一下Netty開發(fā)中會涉及什么內(nèi)容豪诲,給大家留一個(gè)初步的印象。
接下來挂绰,大家可以啟動NettyDiscardServer服務(wù)器屎篱,體驗(yàn)一下Netty程序的運(yùn)行。
找到服務(wù)器類NettyDiscardServer葵蒂。啟動它的main方法交播,就啟動了這個(gè)服務(wù)器。
但是践付,如果要看到最終的丟棄效果秦士,不能僅僅啟動服務(wù)器,還需要啟動客戶端永高,由客戶端向服務(wù)器發(fā)送消息隧土。客戶端在哪兒呢乏梁?
這里的客戶端次洼,只要能發(fā)消息到服務(wù)器即可,不需要其他特殊的功能遇骑。因此卖毁,可以直接使用前面示例中的EchoClient程序來作為客戶端運(yùn)行即可,因?yàn)槎丝谑且恢碌摹?/p>
找到發(fā)送消息到服務(wù)器的客戶端類:EchoClient落萎。啟動它的main方法亥啦,就啟動了這個(gè)客戶端。然后在客戶端的標(biāo)準(zhǔn)化輸入窗口练链,不斷輸入要發(fā)送的消息翔脱,發(fā)送到服務(wù)器即可。
雖然EchoClient客戶端是使用Java NIO編寫的媒鼓,而NettyDiscardServer服務(wù)端是使用Netty編寫的届吁,但是不影響它們之間的相互通信错妖。因?yàn)镹ettyDiscardServer的底層也是使用Java NIO。
client執(zhí)行結(jié)果如下:
[main|EchoClient.start]:客戶端啟動成功疚沐!
[Thread-0|EchoClient$Processer.run]:請輸入發(fā)送內(nèi)容:
neetyDiscardServer
服務(wù)器執(zhí)行結(jié)果如下:
[main|NettyDiscardServer.runServer] |> 服務(wù)器啟動成功暂氯,監(jiān)聽端口: /0:0:0:0:0:0:0:0:18899
[nioEventLoopGroup-3-1|NettyDiscardHandler.channelRead] |> 收到消息,丟棄如下:
2019-11-21 11:37:55 >>neetyDiscardServer
6.2 解密Netty中的Reactor反應(yīng)器模式
在前面的章節(jié)中,已經(jīng)反復(fù)說明:設(shè)計(jì)模式是Java代碼或者程序的重要組織方式亮蛔,如果不了解設(shè)計(jì)模式痴施,學(xué)習(xí)Java程序往往找不到頭緒,上下求索而不得其法究流。故而辣吃,在學(xué)習(xí)Netty組件之前,我們必須了解Netty中的反應(yīng)器模式是如何實(shí)現(xiàn)的芬探。
現(xiàn)在神得,先回顧一下Java NIO中IO事件的處理流程和反應(yīng)器模式的基礎(chǔ)內(nèi)容。
6.2.1 回顧Reactor反應(yīng)器模式中IO事件的處理流程
一個(gè)IO事件從操作系統(tǒng)底層產(chǎn)生后灯节,在Reactor反應(yīng)器模式中的處理流程如圖6-1所示循头。
整個(gè)流程大致分為4步,具體如下:
第1步:通道注冊炎疆。IO源于通道(Channel)卡骂。IO是和通道(對應(yīng)于底層連接而言)強(qiáng)相關(guān)的。一個(gè)IO事件形入,一定屬于某個(gè)通道全跨。但是,如果要查詢通道的事件亿遂,首先要將通道注冊到選擇器浓若。只需通道提前注冊到Selector選擇器即可,IO事件會被選擇器查詢到蛇数。
第2步:查詢選擇挪钓。在反應(yīng)器模式中,一個(gè)反應(yīng)器(或者SubReactor子反應(yīng)器)會負(fù)責(zé)一個(gè)線程耳舅;不斷地輪詢碌上,查詢選擇器中的IO事件(選擇鍵)。
第3步:事件分發(fā)浦徊。如果查詢到IO事件馏予,則分發(fā)給與IO事件有綁定關(guān)系的Handler業(yè)務(wù)處理器。
第4步:完成真正的IO操作和業(yè)務(wù)處理盔性,這一步由Handler業(yè)務(wù)處理器負(fù)責(zé)霞丧。
以上4步,就是整個(gè)反應(yīng)器模式的IO處理器流程冕香。其中蛹尝,第1步和第2步后豫,其實(shí)是Java NIO的功能,反應(yīng)器模式僅僅是利用了Java NIO的優(yōu)勢而已箩言。
題外話:上面的流程比較重要硬贯,是學(xué)習(xí)Netty的基礎(chǔ)。如果這里看不懂陨收,作為鋪墊,請先回到反應(yīng)器模式的詳細(xì)介紹部分鸵赖,回頭再學(xué)習(xí)一下反應(yīng)器模式务漩。
6.2.2 Netty中的Channel通道組件
Channel通道組件是Netty中非常重要的組件,為什么首先要說的是Channel通道組件呢它褪?原因是:反應(yīng)器模式和通道緊密相關(guān)饵骨,反應(yīng)器的查詢和分發(fā)的IO事件都來自于Channel通道組件。
Netty中不直接使用Java NIO的Channel通道組件茫打,對Channel通道組件進(jìn)行了自己的封裝居触。在Netty中,有一系列的Channel通道組件老赤,為了支持多種通信協(xié)議妖异,換句話說罚攀,對于每一種通信連接協(xié)議,Netty都實(shí)現(xiàn)了自己的通道。
另外一點(diǎn)就是钦铺,除了Java的NIO,Netty還能處理Java的面向流的OIO(Old-IO司光,即傳統(tǒng)的阻塞式IO)惹悄。
總結(jié)起來,Netty中的每一種協(xié)議的通道责鳍,都有NIO(異步IO)和OIO(阻塞式IO)兩個(gè)版本碾褂。
對應(yīng)于不同的協(xié)議,Netty中常見的通道類型如下:
NioSocketChannel:異步非阻塞TCP Socket傳輸通道历葛。
NioServerSocketChannel:異步非阻塞TCP Socket服務(wù)器端監(jiān)聽通道正塌。
NioDatagramChannel:異步非阻塞的UDP傳輸通道。
NioSctpChannel:異步非阻塞Sctp傳輸通道啃洋。
NioSctpServerChannel:異步非阻塞Sctp服務(wù)器端監(jiān)聽通道传货。
OioSocketChannel:同步阻塞式TCP Socket傳輸通道。
OioServerSocketChannel:同步阻塞式TCP Socket服務(wù)器端監(jiān)聽通道宏娄。
OioDatagramChannel:同步阻塞式UDP傳輸通道问裕。
OioSctpChannel:同步阻塞式Sctp傳輸通道。
OioSctpServerChannel:同步阻塞式Sctp服務(wù)器端監(jiān)聽通道孵坚。
一般來說粮宛,服務(wù)器端編程用到最多的通信協(xié)議還是TCP協(xié)議窥淆。對應(yīng)的傳輸通道類型為NioSocketChannel類,服務(wù)器監(jiān)聽類為NioServerSocketChannel巍杈。在主要使用的方法上忧饭,其他的通道類型和這個(gè)NioSocketChannel類在原理上基本是相通的,因此筷畦,本書的很多案例都以NioSocketChannel通道為主词裤。
在Netty的NioSocketChannel內(nèi)部封裝了一個(gè)Java NIO的SelectableChannel成員。通過這個(gè)內(nèi)部的Java NIO通道鳖宾,Netty的NioSocketChannel通道上的IO操作吼砂,最終會落地到Java NIO的SelectableChannel底層通道。NioSocketChannel的繼承關(guān)系圖鼎文,如圖6-2所示渔肩。
<div class="ne-content J_NEContent" style="min-height: 380px;"><h3 class="section j-chapter" data-paragraphid="368be3ce46c14ac89a7e432d7231f839_5">6.2.3 Netty中的Reactor反應(yīng)器</h3><p class="section" data-paragraphid="63a8dd0324ed4f9cb23ea04a75397994_5">在反應(yīng)器模式中,一個(gè)反應(yīng)器(或者SubReactor子反應(yīng)器)會負(fù)責(zé)一個(gè)事件處理線程拇惋,不斷地輪詢周偎,通過Selector選擇器不斷查詢注冊過的IO事件(選擇鍵)。如果查詢到IO事件撑帖,則分發(fā)給Handler業(yè)務(wù)處理器蓉坎。</p><p class="section" data-paragraphid="d7457d86297b4b1c8fdd86ffeb99a80c_5">Netty中的反應(yīng)器有多個(gè)實(shí)現(xiàn)類,與Channel通道類有關(guān)系磷仰。對應(yīng)于NioSocketChannel通道袍嬉,Netty的反應(yīng)器類為:NioEventLoop。</p><p class="section" data-paragraphid="63ed314cc2d84c1789b0ecee43403251_5">NioEventLoop類綁定了兩個(gè)重要的Java成員屬性:一個(gè)是Thread線程類的成員灶平,一個(gè)是Java NIO選擇器的成員屬性伺通。NioEventLoop的繼承關(guān)系和主要的成員屬性,如下圖6-3所示逢享。</p><p class="section" data-paragraphid="00f9fb758e7d46a4b3adc401fbe30c17_5" style="clear: both;"><img class="paragraph-img" data-src="https://easyreadfs.nosdn.127.net/image_25da655d9f4f4086a6dceaa1301837d4" data-width="716" data-height="430" src="https://easyreadfs.nosdn.127.net/image_25da655d9f4f4086a6dceaa1301837d4" style="width: auto; height: 380px;"></p><p class="yd-imagetitle section" data-paragraphid="e63a5f41223c46bb8c62e046f4e3cb6c_5">圖6-3 NioEventLoop的繼承關(guān)系和主要的成員</p><p class="section" data-paragraphid="19de58688bad4e44a56e6304c117edbb_5">通過這個(gè)關(guān)系圖罐监,可以看出:NioEventLoop和前面章節(jié)講到反應(yīng)器,在思路上是一致的:一個(gè)NioEventLoop擁有一個(gè)Thread線程瞒爬,負(fù)責(zé)一個(gè)Java NIO Selector選擇器的IO事件輪詢弓柱。</p><p class="section" data-paragraphid="c7e13172192f40508efe2b58850f16d9_5">在Netty中,EventLoop反應(yīng)器和Netty Channel通道侧但,關(guān)系如何呢矢空?理論上來說,一個(gè)EventLoopNetty反應(yīng)器和NettyChannel通道是一對多的關(guān)系:一個(gè)反應(yīng)器可以注冊成千上萬的通道禀横。</p><p class="section" data-paragraphid="c1b11b9cc107483ebea6ad373fa0f128_5" style="clear: both;"><img class="paragraph-img" data-src="https://easyreadfs.nosdn.127.net/image_3d7585d868ba45e9a61c52c92023b72f" data-width="934" data-height="302" src="https://easyreadfs.nosdn.127.net/image_3d7585d868ba45e9a61c52c92023b72f" style="width: 660px;"></p><p class="yd-imagetitle section" data-paragraphid="fa38737ed7c04e1fa34f235773f7fc35_5">圖6-4 EventLoop反應(yīng)器和通道(Channel)的關(guān)系</p></div>
<div class="ne-content J_NEContent" style="min-height: 380px;"><h3 class="section j-chapter" data-paragraphid="6171f90705004186873c2b9531916e6b_5">6.2.4 Netty中的Handler處理器</h3><p class="section" data-paragraphid="f3f8264ff01c45d2be4c747d81df6c66_5">在前面的章節(jié)屁药,解讀Java NIO的IO事件類型時(shí)講到,可供選擇器監(jiān)控的通道IO事件類型包括以下4種:</p><p class="section" data-paragraphid="f889295c837b40679f47ca9ddfe2425b_5"><span class="yd-font-hkkai">? 可讀:SelectionKey.OP_READ</span></p><p class="section" data-paragraphid="b9d3ba2c8b7e4a19a184e53aa1278790_5"><span class="yd-font-hkkai">? 可寫:SelectionKey.OP_WRITE</span></p><p class="section" data-paragraphid="e7c9230fbd6f47c3829ce1bc690d07e1_5"><span class="yd-font-hkkai">? 連接:SelectionKey.OP_CONNECT</span></p><p class="section" data-paragraphid="ad0a483c2e4448e4809632345f5f166a_5"><span class="yd-font-hkkai">? 接收:SelectionKey.OP_ACCEPT</span></p><p class="section" data-paragraphid="55f14811fb024b3292248ece8f756c5b_5">在Netty中柏锄,EventLoop反應(yīng)器內(nèi)部有一個(gè)Java NIO選擇器成員執(zhí)行以上事件的查詢酿箭,然后進(jìn)行對應(yīng)的事件分發(fā)复亏。事件分發(fā)(Dispatch)的目標(biāo)就是Netty自己的Handler處理器。</p><p class="section" data-paragraphid="76fde06e885f4975bb5cf60507b02b69_5">Netty的Handler處理器分為兩大類:第一類是ChannelInboundHandler通道入站處理器缭嫡;第二類是ChannelOutboundHandler通道出站處理器缔御。二者都繼承了ChannelHandler處理器接口。Netty中的Handler處理器的接口與繼承關(guān)系妇蛀,如圖6-5所示耕突。</p><p class="section" data-paragraphid="2b7579581b524f6fa9dc23c9a898e15f_5" style="clear: both;"><img class="paragraph-img" data-src="https://easyreadfs.nosdn.127.net/image_bee94aa9aff843da8fff6d2257acad32" data-width="955" data-height="262" src="https://easyreadfs.nosdn.127.net/image_bee94aa9aff843da8fff6d2257acad32" style="width: 660px;"></p><p class="yd-imagetitle section" data-paragraphid="543f28efcdfe4812932c939ef4697b34_5">圖6-5 Netty中的Handler處理器的接口與繼承關(guān)系</p><p class="section" data-paragraphid="1fec128031a34de59c6fc9ff2bd030bf_5">Netty中的入站處理,不僅僅是OP_READ輸入事件的處理评架,還是從通道底層觸發(fā)有勾,由Netty通過層層傳遞,調(diào)用ChannelInboundHandler通道入站處理器進(jìn)行的某個(gè)處理古程。以底層的Java NIO中的OP_READ輸入事件為例:在通道中發(fā)生了OP_READ事件后,會被EventLoop查詢到喊崖,然后分發(fā)給ChannelInboundHandler通道入站處理器挣磨,調(diào)用它的入站處理的方法read。在ChannelInboundHandler通道入站處理器內(nèi)部的read方法可以從通道中讀取數(shù)據(jù)荤懂。</p><p class="section" data-paragraphid="fbe05e2668c74971bd1290389728e4da_5">Netty中的入站處理茁裙,觸發(fā)的方向?yàn)椋簭耐ǖ赖紺hannelInboundHandler通道入站處理器。</p><p class="section" data-paragraphid="d8a58b717d4a4220a9c486066ef82d1d_5">Netty中的出站處理节仿,本來就包括Java NIO的OP_WRITE可寫事件晤锥。注意,OP_WRITE可寫事件是Java NIO的底層概念廊宪,它和Netty的出站處理的概念不是一個(gè)維度矾瘾,Netty的出站處理是應(yīng)用層維度的。那么箭启,Netty中的出站處理壕翩,具體指的是什么呢?指的是從ChanneOutboundHandler通道出站處理器到通道的某次IO操作傅寡,例如放妈,在應(yīng)用程序完成業(yè)務(wù)處理后,可以通過ChanneOutboundHandler通道出站處理器將處理的結(jié)果寫入底層通道荐操。它的最常用的一個(gè)方法就是write()方法芜抒,把數(shù)據(jù)寫入到通道。</p><p class="section" data-paragraphid="979b1da7e11b457d80c7fe83bece0971_5">這兩個(gè)業(yè)務(wù)處理接口都有各自的默認(rèn)實(shí)現(xiàn):ChannelInboundHandler的默認(rèn)實(shí)現(xiàn)為ChannelInboundHandlerAdapter托启,叫作通道入站處理適配器宅倒。ChanneOutboundHandler的默認(rèn)實(shí)現(xiàn)為ChanneloutBoundHandlerAdapter,叫作通道出站處理適配器驾中。這兩個(gè)默認(rèn)的通道處理適配器唉堪,分別實(shí)現(xiàn)了入站操作和出站操作的基本功能模聋。如果要實(shí)現(xiàn)自己的業(yè)務(wù)處理器,不需要從零開始去實(shí)現(xiàn)處理器的接口唠亚,只需要繼承通道處理適配器即可链方。</p></div>
<div class="ne-content J_NEContent" style="min-height: 380px;"><h3 class="section j-chapter" data-paragraphid="535265601f6149d5ba0b2255b9587175_5">6.2.5 Netty的流水線(Pipeline)</h3><p class="section" data-paragraphid="8564c54c41794dc7b51be018e930de5c_5">來梳理一下Netty的反應(yīng)器模式中各個(gè)組件之間的關(guān)系:</p><p class="section" data-paragraphid="c69b64297fa64842b9d63af09e8a1d7b_5">(1)反應(yīng)器(或者SubReactor子反應(yīng)器)和通道之間是一對多的關(guān)系:一個(gè)反應(yīng)器可以查詢很多個(gè)通道的IO事件。</p><p class="section" data-paragraphid="517cb3479c2a4dc6b3f9bf1ae548231b_5">(2)通道和Handler處理器實(shí)例之間灶搜,是多對多的關(guān)系:一個(gè)通道的IO事件被多個(gè)的Handler實(shí)例處理祟蚀;一個(gè)Handler處理器實(shí)例也能綁定到很多的通道,處理多個(gè)通道的IO事件割卖。</p><p class="section" data-paragraphid="a965a4adcf7e42ccb151780a58f5f599_5">問題是:通道和Handler處理器實(shí)例之間的綁定關(guān)系前酿,Netty是如何組織的呢?</p><p class="section" data-paragraphid="5cebbdb6e3fe42d1926bb05a951f40f5_5">Netty設(shè)計(jì)了一個(gè)特殊的組件鹏溯,叫作ChannelPipeline(通道流水線)罢维,它像一條管道,將綁定到一個(gè)通道的多個(gè)Handler處理器實(shí)例丙挽,串在一起肺孵,形成一條流水線。ChannelPipeline(通道流水線)的默認(rèn)實(shí)現(xiàn)颜阐,實(shí)際上被設(shè)計(jì)成一個(gè)雙向鏈表平窘。所有的Handler處理器實(shí)例被包裝成了雙向鏈表的節(jié)點(diǎn),被加入到了ChannelPipeline(通道流水線)中凳怨。</p><p class="section" data-paragraphid="60e862a0c8024493b6e7f755bcf1dc29_5">重點(diǎn)申明:一個(gè)Netty通道擁有一條Handler處理器流水線瑰艘,成員的名稱叫作pipeline。</p><p class="section" data-paragraphid="2f23ab92d0fe45aabe574750b7830dbd_5">問題來了:這里為什么將pipeline翻譯成流水線肤舞,而不是翻譯成為管道呢紫新?這是有原因的。具體來說萨赁,與流水線內(nèi)部的Handler處理器之間處理IO事件的先后次序有關(guān)弊琴。</p><p class="section" data-paragraphid="1c80508f949e4a80ae6e465843b6835e_5">以入站處理為例。每一個(gè)來自通道的IO事件杖爽,都會進(jìn)入一次ChannelPipeline通道流水線敲董。在進(jìn)入第一個(gè)Handler處理器后,這個(gè)IO事件將按照既定的從前往后次序慰安,在流水線上不斷地向后流動腋寨,流向下一個(gè)Handler處理器。</p><p class="section" data-paragraphid="25b472a3ee1d43e783204068d53d9014_5">在向后流動的過程中化焕,會出現(xiàn)3種情況:</p><p class="section" data-paragraphid="f7b5e5719bce464487cc057df984a58f_5">(1)如果后面還有其他Handler入站處理器萄窜,那么IO事件可以交給下一個(gè)Handler處理器,向后流動。</p><p class="section" data-paragraphid="8480060258e543f58f2f38a122c90f5e_5">(2)如果后面沒有其他的入站處理器查刻,這就意味著這個(gè)IO事件在此次流水線中的處理結(jié)束了键兜。</p><p class="section" data-paragraphid="22ed6958ff904db7b2c5cd505f124565_5">(3)如果在流水線中間需要終止流動,可以選擇不將IO事件交給下一個(gè)Handler處理器穗泵,流水線的執(zhí)行也被終止了普气。</p><p class="section" data-paragraphid="e61839f7f2a649ec831ee0488cabd527_5">為什么說Handler的處理是按照既定的次序,而不是從前到后的次序呢佃延?Netty是這樣規(guī)定的:入站處理器Handler的執(zhí)行次序现诀,是從前到后;出站處理器Handler的執(zhí)行次序履肃,是從后到前仔沿。總之尺棋,IO事件在流水線上的執(zhí)行次序封锉,與IO事件的類型是有關(guān)系的,如圖6-6所示膘螟。</p><p class="section" data-paragraphid="7f3af6e5654a4e428c48c4a8334703f7_5" style="clear: both;"><img class="paragraph-img" data-src="https://easyreadfs.nosdn.127.net/image_7cfd7ede1148462b8ca6d1b58dcd6f52" data-width="1328" data-height="338" src="https://easyreadfs.nosdn.127.net/image_7cfd7ede1148462b8ca6d1b58dcd6f52" style="width: 660px;"></p><p class="yd-imagetitle section" data-paragraphid="2df29a5fef3246d5a263f9df9b897a37_5">圖6-6 流水線上入站處理器和出站處理器的執(zhí)行次序</p><p class="section" data-paragraphid="8ca898c0b5a54d379747714d21d8f5a7_5">除了流動的方向與IO操作的類型有關(guān)之外烘浦,流動過程中經(jīng)過的處理器節(jié)點(diǎn)的類型,也是與IO操作的類型有關(guān)萍鲸。入站的IO操作只會且只能從Inbound入站處理器類型的Handler流過;出站的IO操作只會且只能從Outbound出站處理器類型的Handler流過擦俐。</p><p class="section" data-paragraphid="2e2ad261b29747dcb6c7227df7be0be9_5">總之脊阴,流水線是通道的“大管家”,為通道管理好了它的一大堆Handler“小弟”蚯瞧。</p><p class="section" data-paragraphid="280aa39aa2c6409cbd93a0465b45ae2a_5">了解完了流水線之后嘿期,大家應(yīng)該對Netty中的通道、EventLoop反應(yīng)器埋合、Handler處理器备徐,以及三者之間的協(xié)作關(guān)系,有了一個(gè)清晰的認(rèn)知和了解甚颂。至此蜜猾,大家基本可以動手開發(fā)簡單的Netty程序了。不過振诬,為了方便開發(fā)者蹭睡,Netty提供了一個(gè)類把上面的三個(gè)組件快速組裝起來,這個(gè)系列的類叫作Bootstrap啟動器赶么。嚴(yán)格來說肩豁,不止一個(gè)類名字為Bootstrap,例如在服務(wù)器端的啟動類叫作ServerBootstrap類。</p><p class="section" data-paragraphid="0cd95f9b6bef43809825ddf7058c1d2c_5">下面清钥,為大家詳細(xì)介紹一下這個(gè)提升開發(fā)效率的Bootstrap啟動器類琼锋。</p></div>
<div class="ne-content J_NEContent" style="min-height: 380px;"><h2 class="section j-chapter" data-paragraphid="eb6f27a4afc54a6e8c4bba05b389fcb9_5">6.3 詳解Bootstrap啟動器類</h2><p class="section" data-paragraphid="931751baa13b47918dfca58718620aad_5">Bootstrap類是Netty提供的一個(gè)便利的工廠類,可以通過它來完成Netty的客戶端或服務(wù)器端的Netty組件的組裝祟昭,以及Netty程序的初始化缕坎。當(dāng)然,Netty的官方解釋是从橘,完全可以不用這個(gè)Bootstrap啟動器念赶。但是,一點(diǎn)點(diǎn)去手動創(chuàng)建通道恰力、完成各種設(shè)置和啟動叉谜、并且注冊到EventLoop,這個(gè)過程會非常麻煩踩萎。通常情況下停局,還是使用這個(gè)便利的Bootstrap工具類會效率更高。</p><p class="section" data-paragraphid="23560e82539d431a8c11f98e9acf6d66_5">在Netty中香府,有兩個(gè)啟動器類董栽,分別用在服務(wù)器和客戶端。如圖6-7所示企孩。</p><p class="section" data-paragraphid="70d8aefdb71f4727af67313ff6d7fca3_5" style="clear: both;"><img class="paragraph-img" data-src="https://easyreadfs.nosdn.127.net/image_4aa9efed815e470a8f510d36e37b6791" data-width="680" data-height="366" src="https://easyreadfs.nosdn.127.net/image_4aa9efed815e470a8f510d36e37b6791" style="width: 660px;"></p><p class="yd-imagetitle section" data-paragraphid="8c1dcb7e61eb4090b082f00eaced90b7_5">圖6-7 Netty中的兩個(gè)啟動器類</p><p class="section" data-paragraphid="8cc7100e5e144bdb97b34ba3877c8b2c_5">這兩個(gè)啟動器僅僅是使用的地方不同锭碳,它們大致的配置和使用方法都是相同的。下面以ServerBootstrap服務(wù)器啟動類作為重點(diǎn)的介紹對象勿璃。</p><p class="section" data-paragraphid="bafe6a3e8b744063bc280c9705c198c8_5">在介紹ServerBootstrap的服務(wù)器啟動流程之前擒抛,首先介紹一下涉及到的兩個(gè)基礎(chǔ)概念:父子通道、EventLoopGroup線程組(事件循環(huán)線程組)补疑。</p></div>
<div class="ne-content J_NEContent" style="min-height: 380px;"><h3 class="section j-chapter" data-paragraphid="b2aa9d877894421791c49127c13d6f0f_5">6.3.1 父子通道</h3><p class="section" data-paragraphid="05f8a8de632242a1b41c1d9c745f2a6a_5">在Netty中歧沪,每一個(gè)NioSocketChannel通道所封裝的是Java NIO通道,再往下就對應(yīng)到了操作系統(tǒng)底層的socket描述符莲组。理論上來說诊胞,操作系統(tǒng)底層的socket描述符分為兩類:</p><p class="section" data-paragraphid="177f0babbbfb40bf966cc05f99cb2d9a_5"><span class="yd-font-hkkai">? 連接監(jiān)聽類型。連接監(jiān)聽類型的socket描述符锹杈,放在服務(wù)器端撵孤,它負(fù)責(zé)接收客戶端的套接字連接;在服務(wù)器端竭望,一個(gè)“連接監(jiān)聽類型”的socket描述符可以接受(Accept)成千上萬的傳輸類的socket描述符早直。</span></p><p class="section" data-paragraphid="a64b3bf7e616406f91179ff1eb7d2ccb_5"><span class="yd-font-hkkai">? 傳輸數(shù)據(jù)類型。數(shù)據(jù)傳輸類的socket描述符負(fù)責(zé)傳輸數(shù)據(jù)市框。同一條TCP的Socket傳輸鏈路霞扬,在服務(wù)器和客戶端,都分別會有一個(gè)與之相對應(yīng)的數(shù)據(jù)傳輸類型的socket描述符。</span></p><p class="section" data-paragraphid="ad45cc650b064fdf8ae1965ae8e75a43_5">在Netty中喻圃,異步非阻塞的服務(wù)器端監(jiān)聽通道NioServerSocketChannel萤彩,封裝在Linux底層的描述符,是“連接監(jiān)聽類型”socket描述符斧拍;而NioSocketChannel異步非阻塞TCP Socket傳輸通道雀扶,封裝在底層Linux的描述符,是“數(shù)據(jù)傳輸類型”的socket描述符肆汹。</p><p class="section" data-paragraphid="6e5c5ce46a434e01b87ae5cc787cd016_5">在Netty中愚墓,將有接收關(guān)系的NioServerSocketChannel和NioSocketChannel,叫作父子通道昂勉。其中浪册,NioServerSocketChannel負(fù)責(zé)服務(wù)器連接監(jiān)聽和接收,也叫父通道(Parent Channel)岗照。對應(yīng)于每一個(gè)接收到的NioSocketChannel傳輸類通道村象,也叫子通道(Child Channel)。</p></div>
<div class="ne-content J_NEContent" style="min-height: 380px;"><h3 class="section j-chapter" data-paragraphid="30096e0ec3a34a9aa0380dcd53673d56_5">6.3.2 EventLoopGroup線程組</h3><p class="section" data-paragraphid="4274acbb38c5495bafcbfa217fee4929_5">Netty中的Reactor反應(yīng)器模式攒至,肯定不是單線程版本的反應(yīng)器模式厚者,而是多線程版本的反應(yīng)器模式。Netty的多線程版本的反應(yīng)器模式是如何實(shí)現(xiàn)的呢迫吐?</p><p class="section" data-paragraphid="29f7ea94f3894d06b73668ac8fcb2c74_5">在Netty中库菲,一個(gè)EventLoop相當(dāng)于一個(gè)子反應(yīng)器(SubReactor)。大家已經(jīng)知道志膀,一個(gè)NioEventLoop子反應(yīng)器擁有了一個(gè)線程蝙昙,同時(shí)擁有一個(gè)Java NIO選擇器。Netty如何組織外層的反應(yīng)器呢梧却?答案是使用EventLoopGroup線程組。多個(gè)EventLoop線程組成一個(gè)EventLoopGroup線程組败去。</p><p class="section" data-paragraphid="a74a413d1d7c4487bd1e334c7a355dd3_5">反過來說放航,Netty的EventLoopGroup線程組就是一個(gè)多線程版本的反應(yīng)器。而其中的單個(gè)EventLoop線程對應(yīng)于一個(gè)子反應(yīng)器(SubReactor)圆裕。</p><p class="section" data-paragraphid="f1009fa8199249afa7f7185c21c53fc4_5">Netty的程序開發(fā)不會直接使用單個(gè)EventLoop線程广鳍,而是使用EventLoopGroup線程組。EventLoopGroup的構(gòu)造函數(shù)有一個(gè)參數(shù)吓妆,用于指定內(nèi)部的線程數(shù)赊时。在構(gòu)造器初始化時(shí),會按照傳入的線程數(shù)量行拢,在內(nèi)部構(gòu)造多個(gè)Thread線程和多個(gè)EventLoop子反應(yīng)器(一個(gè)線程對應(yīng)一個(gè)EventLoop子反應(yīng)器)祖秒,進(jìn)行多線程的IO事件查詢和分發(fā)。</p><p class="section" data-paragraphid="3de395dea1424efab0a5896f82093dec_5">如果使用EventLoopGroup的無參數(shù)的構(gòu)造函數(shù),沒有傳入線程數(shù)或者傳入的線程數(shù)為0竭缝,那么EventLoopGroup內(nèi)部的線程數(shù)到底是多少呢房维?默認(rèn)的EventLoopGroup內(nèi)部線程數(shù)為最大可用的CPU處理器數(shù)量的2倍。假設(shè)電腦使用的是4核的CPU抬纸,那么在內(nèi)部會啟動8個(gè)EventLoop線程咙俩,相當(dāng)8個(gè)子反應(yīng)器(SubReactor)實(shí)例。</p><p class="section" data-paragraphid="a884b788f0914ab5870442e450a5dfbc_5">從前文可知湿故,為了及時(shí)接受(Accept)到新連接阿趁,在服務(wù)器端,一般有兩個(gè)獨(dú)立的反應(yīng)器坛猪,一個(gè)反應(yīng)器負(fù)責(zé)新連接的監(jiān)聽和接受脖阵,另一個(gè)反應(yīng)器負(fù)責(zé)IO事件處理。對應(yīng)到Netty服務(wù)器程序中砚哆,則是設(shè)置兩個(gè)EventLoopGroup線程組独撇,一個(gè)EventLoopGroup負(fù)責(zé)新連接的監(jiān)聽和接受,一個(gè)EventLoopGroup負(fù)責(zé)IO事件處理躁锁。</p><p class="section" data-paragraphid="198ef0ee33b247f1bcd42d48923e8f81_5">那么纷铣,兩個(gè)反應(yīng)器如何分工呢?負(fù)責(zé)新連接的監(jiān)聽和接受的EventLoopGroup線程組战转,查詢父通道的IO事件搜立,有點(diǎn)像負(fù)責(zé)招工的包工頭,因此槐秧,可以形象地稱為“包工頭”(Boss)線程組啄踊。另一個(gè)EventLoopGroup線程組負(fù)責(zé)查詢所有子通道的IO事件,并且執(zhí)行Handler處理器中的業(yè)務(wù)處理——例如數(shù)據(jù)的輸入和輸出(有點(diǎn)兒像搬磚)刁标,這個(gè)線程組可以形象地稱為“工人”(Worker)線程組颠通。</p><p class="section" data-paragraphid="bfd50f76d77b4ed984ffbb0a1cc203fb_5">至此,已經(jīng)介紹完了兩個(gè)基礎(chǔ)概念:父子通道膀懈、EventLoopGroup線程組顿锰。下一節(jié)將介紹ServerBootstrap的啟動流程。</p></div>
<div class="ne-content J_NEContent" style="min-height: 380px;"><h3 class="section j-chapter" data-paragraphid="e79b35c264374e4caf213b56bff8d10c_5">6.3.3 Bootstrap的啟動流程</h3><p class="section" data-paragraphid="1b15da4950484dae88b587088d9fb2fa_5">Bootstrap的啟動流程启搂,也就是Netty組件的組裝硼控、配置,以及Netty服務(wù)器或者客戶端的啟動流程胳赌。在本節(jié)中對啟動流程進(jìn)行了梳理牢撼,大致分成了8個(gè)步驟。本書僅僅演示的是服務(wù)器端啟動器的使用疑苫,用到的啟動器類為ServerBootstrap熏版。正式使用前纷责,首先創(chuàng)建一個(gè)服務(wù)器端的啟動器實(shí)例。</p><p class="yd-code section" data-paragraphid="8c419657f1054d11b22033cbb797d36d_5">//創(chuàng)建一個(gè)服務(wù)器端的啟動器
ServerBootstrap b = new ServerBootstrap();</p><p class="section" data-paragraphid="7ee67facabce41278dbcc1fc07cfe295_5">接下來纳决,結(jié)合前面的NettyDiscardServer服務(wù)器的程序代碼碰逸,給大家詳細(xì)介紹一下Bootstrap啟動流程中精彩的8個(gè)步驟。</p><p class="section" data-paragraphid="62b04a4b695d411e91cd231710e8cbe6_5"><span class="bold">第1步:創(chuàng)建反應(yīng)器線程組阔加,并賦值給ServerBootstrap啟動器實(shí)例</span></p><p class="yd-code section" data-paragraphid="7a8edc9c3a3d4683afd53f37a402f72c_5">//創(chuàng)建反應(yīng)器線程組
//boss線程組
EventLoopGroupbossLoopGroup = new NioEventLoopGroup(1);
//worker線程組
EventLoopGroupworkerLoopGroup = new NioEventLoopGroup();
//...
//1 設(shè)置反應(yīng)器線程組
b.group(bossLoopGroup, workerLoopGroup);</p><p class="section" data-paragraphid="08615faf35d245939421127ac03f7b9d_5">在設(shè)置反應(yīng)器線程組之前饵史,創(chuàng)建了兩個(gè)NioEventLoopGroup線程組,一個(gè)負(fù)責(zé)處理連接監(jiān)聽IO事件,名為bossLoopGroup;另一個(gè)負(fù)責(zé)數(shù)據(jù)IO事件和Handler業(yè)務(wù)處理枣宫,名為workerLoopGroup。</p><p class="section" data-paragraphid="be60dac69ace45439646d5459ba44559_5">在線程組創(chuàng)建完成后吭露,就可以配置給啟動器實(shí)例,調(diào)用的方法是b.group(bossGroup,workerGroup)尊惰,它一次性地給啟動器配置了兩大線程組讲竿。</p><p class="section" data-paragraphid="ac0129ea8fac48f9b4d4b3319457fc86_5">不一定非得配置兩個(gè)線程組,可以僅配置一個(gè)EventLoopGroup反應(yīng)器線程組弄屡。具體的配置方法是調(diào)用b.group(workerGroup)题禀。在這種模式下,連接監(jiān)聽IO事件和數(shù)據(jù)傳輸IO事件可能被擠在了同一個(gè)線程中處理膀捷。這樣會帶來一個(gè)風(fēng)險(xiǎn):新連接的接受被更加耗時(shí)的數(shù)據(jù)傳輸或者業(yè)務(wù)處理所阻塞迈嘹。</p><p class="section" data-paragraphid="1c18fd0dea384bb89262a7a8b4d759b4_5">在服務(wù)器端,建議設(shè)置成兩個(gè)線程組的工作模式全庸。</p><p class="section" data-paragraphid="c1c0f7e763454faea2b2384d729dd3ee_5"><span class="bold">第2步:設(shè)置通道的IO類型</span></p><p class="section" data-paragraphid="7e43e397fe8b4768bde76e7d94eb1d1f_5">Netty不止支持Java NIO秀仲,也支持阻塞式的OIO(也叫BIO,Block-IO壶笼,即阻塞式IO)神僵。下面配置的是Java NIO類型的通道類型,方法如下:</p><p class="yd-code section" data-paragraphid="42f8d4ed28344676934d8f171ad90bfa_5">//2 設(shè)置nio類型的通道
b.channel(NioServerSocketChannel.class);</p><p class="section" data-paragraphid="8bde26b4a29a4c0f99e751dae0288ab9_5">如果確實(shí)需要指定Bootstrap的IO模型為BIO覆劈,那么這里配置上Netty的OioServerSocketChannel.class類即可保礼。由于NIO的優(yōu)勢巨大,通常不會在Netty中使用BIO墩崩。</p><p class="section" data-paragraphid="c3ea478e516748bab543610446f64526_5"><span class="bold">第3步:設(shè)置監(jiān)聽端口</span></p><p class="yd-code section" data-paragraphid="900a6cc7f5124bcaa7abcdfb375f2f1b_5">//3 設(shè)置監(jiān)聽端口
b.localAddress(new InetSocketAddress(port));</p><p class="section" data-paragraphid="36461ca9a056417c9ab6c263b05cc64b_5">這是最為簡單的一步操作,主要是設(shè)置服務(wù)器的監(jiān)聽地址侯勉。</p><p class="section" data-paragraphid="85152b4b1151424caaa267d212ecba41_5"><span class="bold">第4步:設(shè)置傳輸通道的配置選項(xiàng)</span></p><p class="yd-code section" data-paragraphid="6998415d34c14d009decb38d9f6b4771_5">//4 設(shè)置通道的參數(shù)
b.option(ChannelOption.SO_KEEPALIVE, true);
b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);</p><p class="section" data-paragraphid="2ba44a9c17bc437c9171f978316dc8fd_5">這里用到了Bootstrap的option()選項(xiàng)設(shè)置方法鹦筹。對于服務(wù)器的Bootstrap而言,這個(gè)方法的作用是:給父通道(Parent Channel)接收連接通道設(shè)置一些選項(xiàng)址貌。</p><p class="section" data-paragraphid="1fba09f56c924dcc97f8a25c45a8233a_5">如果要給子通道(Child Channel)設(shè)置一些通道選項(xiàng)铐拐,則需要用另外一個(gè)childOption()設(shè)置方法徘键。</p><p class="section" data-paragraphid="7f46c363fd8a4b7486927e1f874b80d4_5">可以設(shè)置哪些通道選項(xiàng)(ChannelOption)呢?在上面的代碼中遍蟋,設(shè)置了一個(gè)底層TCP相關(guān)的選項(xiàng)ChannelOption.SO_KEEPALIVE吹害。該選項(xiàng)表示:是否開啟TCP底層心跳機(jī)制,true為開啟虚青,false為關(guān)閉它呀。</p><p class="section" data-paragraphid="937525bb278e428f8a661b1db444761d_5">其他的通道設(shè)置選項(xiàng),參見下一小節(jié)棒厘。</p><p class="section" data-paragraphid="5b1e7a4111614a35937fca3fd47f7584_5"><span class="bold">第5步:裝配子通道的Pipeline流水線</span></p><p class="section" data-paragraphid="3e4978171e0f48ea9a3306d579a3eac5_5">上一節(jié)介紹到纵穿,每一個(gè)通道的子通道,都用一條ChannelPipeline流水線奢人。它的內(nèi)部有一個(gè)雙向的鏈表谓媒。裝配流水線的方式是:將業(yè)務(wù)處理器ChannelHandler實(shí)例加入雙向鏈表中。</p><p class="section" data-paragraphid="956aa489a6384a209c7824399bacf7c1_5">裝配子通道的Handler流水線調(diào)用childHandler()方法何乎,傳遞一個(gè)ChannelInitializer通道初始化類的實(shí)例句惯。在父通道成功接收一個(gè)連接,并創(chuàng)建成功一個(gè)子通道后支救,就會初始化子通道抢野,這里配置的ChannelInitializer實(shí)例就會被調(diào)用。</p><p class="section" data-paragraphid="6f285ebc1dac44c4a61382aaf0cdfe42_5">在ChannelInitializer通道初始化類的實(shí)例中搂妻,有一個(gè)initChannel初始化方法蒙保,在子通道創(chuàng)建后會被執(zhí)行到,向子通道流水線增加業(yè)務(wù)處理器欲主。</p><p class="yd-code section" data-paragraphid="07006449048d4347b548dee308bc4d87_5">//5 裝配子通道流水線
b.childHandler(new ChannelInitializer<SocketChannel>() {
//有連接到達(dá)時(shí)會創(chuàng)建一個(gè)通道的子通道邓厕,并初始化
protected void initChannel(SocketChannelch) throws Exception {
// 流水線管理子通道中的Handler業(yè)務(wù)處理器
// 向子通道流水線添加一個(gè)Handler業(yè)務(wù)處理器
ch.pipeline().addLast(new NettyDiscardHandler());
}
});</p><p class="section" data-paragraphid="09c88744546d4d46b7b5416c664fffff_5">為什么僅裝配子通道的流水線,而不需要裝配父通道的流水線呢扁瓢?原因是:父通道也就是NioServerSocketChannel連接接受通道详恼,它的內(nèi)部業(yè)務(wù)處理是固定的:接受新連接后,創(chuàng)建子通道引几,然后初始化子通道昧互,所以不需要特別的配置。如果需要完成特殊的業(yè)務(wù)處理伟桅,可以使用ServerBootstrap的handler(ChannelHandler handler)方法敞掘,為父通道設(shè)置ChannelInitializer初始化器。</p><p class="section" data-paragraphid="9a67f1f3df254f23af95ceed4238b0f0_5">說明一下楣铁,ChannelInitializer處理器器有一個(gè)泛型參數(shù)SocketChannel玖雁,它代表需要初始化的通道類型,這個(gè)類型需要和前面的啟動器中設(shè)置的通道類型盖腕,一一對應(yīng)起來赫冬。</p><p class="section" data-paragraphid="61761428bd1c43fc8998713df5a2ec44_5"><span class="bold">第6步:開始綁定服務(wù)器新連接的監(jiān)聽端口</span></p><p class="yd-code section" data-paragraphid="350f6c3e4a0f4d1ca8f5438d1fe32ebb_5">// 6 開始綁定端口浓镜,通過調(diào)用sync同步方法阻塞直到綁定成功
ChannelFuturechannelFuture = b.bind().sync();
Logger.info(" 服務(wù)器啟動成功,監(jiān)聽端口: " +
channelFuture.channel().localAddress());</p><p class="section" data-paragraphid="1a8d9d6ebdb4454a8b11b2a7c7c9d569_5">這個(gè)也很簡單劲厌。b.bind()方法的功能:返回一個(gè)端口綁定Netty的異步任務(wù)channelFuture膛薛。在這里,并沒有給channelFuture異步任務(wù)增加回調(diào)監(jiān)聽器补鼻,而是阻塞channelFuture異步任務(wù)哄啄,直到端口綁定任務(wù)執(zhí)行完成。</p><p class="section" data-paragraphid="8020f741deb547b099b2555f47fe61ae_5">在Netty中辽幌,所有的IO操作都是異步執(zhí)行的增淹,這就意味著任何一個(gè)IO操作會立刻返回,在返回的時(shí)候乌企,異步任務(wù)還沒有真正執(zhí)行虑润。什么時(shí)候執(zhí)行完成呢?Netty中的IO操作加酵,都會返回異步任務(wù)實(shí)例(如ChannelFuture實(shí)例)拳喻,通過自我阻塞一直到ChannelFuture異步任務(wù)執(zhí)行完成,或者為ChannelFuture增加事件監(jiān)聽器的兩種方式猪腕,以獲得Netty中的IO操作的真正結(jié)果冗澈。上面使用了第一種。</p><p class="section" data-paragraphid="f4621a321b6348769697a527267b3c17_5">至此陋葡,服務(wù)器正式啟動亚亲。</p><p class="section" data-paragraphid="8626828d1d664753a49d82b346df3bf0_5"><span class="bold">第7步:自我阻塞,直到通道關(guān)閉</span></p><p class="yd-code section" data-paragraphid="6ec09c11e7684c00a24a9cbce861eaf9_5">// 7 等待通道關(guān)閉
// 自我阻塞腐缤,直到通道關(guān)閉的異步任務(wù)結(jié)束
ChannelFuturecloseFuture = channelFuture.channel().closeFuture();
closeFuture.sync();</p><p class="section" data-paragraphid="2202f4d06cfd45de8ba63726f861523d_5">如果要阻塞當(dāng)前線程直到通道關(guān)閉捌归,可以使用通道的closeFuture()方法,以獲取通道關(guān)閉的異步任務(wù)岭粤。當(dāng)通道被關(guān)閉時(shí)惜索,closeFuture實(shí)例的sync()方法會返回。</p><p class="section" data-paragraphid="02c3a98bbdbb48b396375f253bf9f5b2_5"><span class="bold">第8步:關(guān)閉EventLoopGroup</span></p><p class="yd-code section" data-paragraphid="9ef4f6dd6d844103b3edf2af1c829955_5">// 8關(guān)閉EventLoopGroup剃浇,
// 釋放掉所有資源巾兆,包括創(chuàng)建的反應(yīng)器線程
workerLoopGroup.shutdownGracefully();
bossLoopGroup.shutdownGracefully();</p><p class="section" data-paragraphid="f2c9c693086140ba90b850111f73d146_5">關(guān)閉Reactor反應(yīng)器線程組,同時(shí)會關(guān)閉內(nèi)部的subReactor子反應(yīng)器線程虎囚,也會關(guān)閉內(nèi)部的Selector選擇器角塑、內(nèi)部的輪詢線程以及負(fù)責(zé)查詢的所有的子通道。在子通道關(guān)閉后淘讥,會釋放掉底層的資源圃伶,如TCP Socket文件描述符等。</p></div>
<div class="ne-content J_NEContent" style="min-height: 380px;"><h3 class="section j-chapter" data-paragraphid="d6354f8b3dad43848bf7ae07df7c1297_5">6.3.4 ChannelOption通道選項(xiàng)</h3><p class="section" data-paragraphid="3a03798801494205b158e2812da9ab28_5">無論是對于NioServerSocketChannel父通道類型适揉,還是對于NioSocketChannel子通道類型留攒,都可以設(shè)置一系列的ChannelOption選項(xiàng)。在ChannelOption類中嫉嘀,定義了一大票通道選項(xiàng)炼邀。下面介紹一些常見的選項(xiàng)。</p><p class="section" data-paragraphid="7c9c9d561f504f0ea2bebb38460f6601_5"><span class="bold">1. SO_RCVBUF剪侮,SO_SNDBUF</span></p><p class="section" data-paragraphid="fdac18c8243a498b80d261c1c2096eab_5">此為TCP參數(shù)拭宁。每個(gè)TCP socket(套接字)在內(nèi)核中都有一個(gè)發(fā)送緩沖區(qū)和一個(gè)接收緩沖區(qū),這兩個(gè)選項(xiàng)就是用來設(shè)置TCP連接的這兩個(gè)緩沖區(qū)大小的瓣俯。TCP的全雙工的工作模式以及TCP的滑動窗口便是依賴于這兩個(gè)獨(dú)立的緩沖區(qū)及其填充的狀態(tài)杰标。</p><p class="section" data-paragraphid="c7c7ef4876cf4eb899322bb739ee75b3_5"><span class="bold">2. TCP_NODELAY</span></p><p class="section" data-paragraphid="33a02f9d1dcf45af8324baf15ea8545a_5">此為TCP參數(shù)。表示立即發(fā)送數(shù)據(jù)彩匕,默認(rèn)值為True(Netty默認(rèn)為True腔剂,而操作系統(tǒng)默認(rèn)為False)。該值用于設(shè)置Nagle算法的啟用驼仪,該算法將小的碎片數(shù)據(jù)連接成更大的報(bào)文(或數(shù)據(jù)包)來最小化所發(fā)送報(bào)文的數(shù)量掸犬,如果需要發(fā)送一些較小的報(bào)文,則需要禁用該算法绪爸。Netty默認(rèn)禁用該算法湾碎,從而最小化報(bào)文傳輸?shù)难訒r(shí)。</p><p class="section" data-paragraphid="26eab14dc16f4aa6a216af8b98c61084_5">說明一下:這個(gè)參數(shù)的值奠货,與是否開啟Nagle算法是相反的介褥,設(shè)置為true表示關(guān)閉,設(shè)置為false表示開啟递惋。通俗地講柔滔,如果要求高實(shí)時(shí)性,有數(shù)據(jù)發(fā)送時(shí)就立刻發(fā)送丹墨,就設(shè)置為true廊遍,如果需要減少發(fā)送次數(shù)和減少網(wǎng)絡(luò)交互次數(shù),就設(shè)置為false贩挣。</p><p class="section" data-paragraphid="9f3536d4a4e94ac99116c3372d5df0b4_5"><span class="bold">3. SO_KEEPALIVE</span></p><p class="section" data-paragraphid="49aac33ea23b4a7f8f720abcbb8097cd_5">此為TCP參數(shù)喉前。表示底層TCP協(xié)議的心跳機(jī)制。true為連接保持心跳王财,默認(rèn)值為false卵迂。啟用該功能時(shí),TCP會主動探測空閑連接的有效性绒净〖洌可以將此功能視為TCP的心跳機(jī)制,需要注意的是:默認(rèn)的心跳間隔是7200s即2小時(shí)挂疆。Netty默認(rèn)關(guān)閉該功能改览。</p><p class="section" data-paragraphid="9257bb81e1a84799a05fe276e74047af_5"><span class="bold">4. SO_REUSEADDR</span></p><p class="section" data-paragraphid="a028c67886a243af9a8482d3aa1b6665_5">此為TCP參數(shù)下翎。設(shè)置為true時(shí)表示地址復(fù)用,默認(rèn)值為false宝当。有四種情況需要用到這個(gè)參數(shù)設(shè)置:</p><p class="section" data-paragraphid="72497c600f674371bccb6ff59b12c9b3_5"><span class="yd-font-hkkai">? 當(dāng)有一個(gè)有相同本地地址和端口的socket1處于TIME_WAIT狀態(tài)時(shí)视事,而我們希望啟動的程序的socket2要占用該地址和端口。例如在重啟服務(wù)且保持先前端口時(shí)庆揩。</span></p><p class="section" data-paragraphid="ed32091d11f7426eb164b762f6ff2bbb_5"><span class="yd-font-hkkai">? 有多塊網(wǎng)卡或用IP Alias技術(shù)的機(jī)器在同一端口啟動多個(gè)進(jìn)程俐东,但每個(gè)進(jìn)程綁定的本地IP地址不能相同。</span></p><p class="section" data-paragraphid="b2f87bdfaa0141b4adb9238a3c32f03a_5"><span class="yd-font-hkkai">? 單個(gè)進(jìn)程綁定相同的端口到多個(gè)socket(套接字)上订晌,但每個(gè)socket綁定的IP地址不同虏辫。</span></p><p class="section" data-paragraphid="cddf235522ba4dc7bebf45e37c955301_5"><span class="yd-font-hkkai">? 完全相同的地址和端口的重復(fù)綁定。但這只用于UDP的多播锈拨,不用于TCP砌庄。</span></p><p class="section" data-paragraphid="8aa104c1cbac4f499c450fc1a5b7e594_5"><span class="bold">5. SO_LINGER</span></p><p class="section" data-paragraphid="58c70f4e4acc43c9a38c373f349ba4ea_5">此為TCP參數(shù)。表示關(guān)閉socket的延遲時(shí)間奕枢,默認(rèn)值為-1鹤耍,表示禁用該功能。-1表示socket.close()方法立即返回验辞,但操作系統(tǒng)底層會將發(fā)送緩沖區(qū)全部發(fā)送到對端稿黄。0表示socket.close()方法立即返回,操作系統(tǒng)放棄發(fā)送緩沖區(qū)的數(shù)據(jù)跌造,直接向?qū)Χ税l(fā)送RST包杆怕,對端收到復(fù)位錯(cuò)誤。非0整數(shù)值表示調(diào)用socket.close()方法的線程被阻塞壳贪,直到延遲時(shí)間到來陵珍、發(fā)送緩沖區(qū)中的數(shù)據(jù)發(fā)送完畢,若超時(shí)违施,則對端會收到復(fù)位錯(cuò)誤互纯。</p><p class="section" data-paragraphid="c5366055a3cc4c418376fe81b7724c5b_5"><span class="bold">6. SO_BACKLOG</span></p><p class="section" data-paragraphid="825b52025e3b466fa81740dcd7671d83_5">此為TCP參數(shù)。表示服務(wù)器端接收連接的隊(duì)列長度磕蒲,如果隊(duì)列已滿留潦,客戶端連接將被拒絕。默認(rèn)值辣往,在Windows中為200兔院,其他操作系統(tǒng)為128。</p><p class="section" data-paragraphid="b17660b3faf54239887343560a51c492_5">如果連接建立頻繁站削,服務(wù)器處理新連接較慢坊萝,可以適當(dāng)調(diào)大這個(gè)參數(shù)。</p><p class="section" data-paragraphid="225f6e5600424682b0f5c8dea1597def_5"><span class="bold">7. SO_BROADCAST</span></p><p class="section" data-paragraphid="6f8c057c4eeb40e9bb65bf6dd1a1db99_5">此為TCP參數(shù)。表示設(shè)置廣播模式十偶。</p></div>
<div class="ne-content J_NEContent" style="min-height: 380px;"><h2 class="section j-chapter" data-paragraphid="84a8d7cf99b344abbd531cf48f45b2c7_5">6.4 詳解Channel通道</h2><p class="section" data-paragraphid="db5501de48e14527af3764218d4692f3_5">先介紹一下菩鲜,在使用Channel通道的過程中所涉及的主要成員和方法。然后惦积,為大家介紹一下Netty所提供了一個(gè)專門的單元測試通道——EmbeddedChannel(嵌入式通道)睦袖。</p></div>
<div class="ne-content J_NEContent" style="min-height: 380px;"><h3 class="section j-chapter" data-paragraphid="530dcff67cd34ef2a95eb0de6729a96a_5">6.4.1 Channel通道的主要成員和方法</h3><p class="section" data-paragraphid="ce86663a62d14e69880fbbcde3c90145_5">在Netty中,通道是其中的核心概念之一荣刑,代表著網(wǎng)絡(luò)連接。通道是通信的主題伦乔,由它負(fù)責(zé)同對端進(jìn)行網(wǎng)絡(luò)通信厉亏,可以寫入數(shù)據(jù)到對端,也可以從對端讀取數(shù)據(jù)烈和。</p><p class="section" data-paragraphid="0bc33fa62df04061aa93bf2d2ae543d7_5">通道的抽象類AbstractChannel的構(gòu)造函數(shù)如下:</p><p class="yd-code section" data-paragraphid="053317f6a32649299586b217a6392e1b_5">protected AbstractChannel(Channel parent) {
this.parent = parent; //父通道
id = newId();
unsafe = newUnsafe(); //底層的NIO 通道,完成實(shí)際的IO操作
pipeline = newChannelPipeline(); //一條通道爱只,擁有一條流水線
}</p><p class="section" data-paragraphid="915e5ed3ea544e54a517e35dc27920e6_5">AbstractChannel內(nèi)部有一個(gè)pipeline屬性,表示處理器的流水線招刹。Netty在對通道進(jìn)行初始化的時(shí)候恬试,將pipeline屬性初始化為DefaultChannelPipeline的實(shí)例。這段代碼也表明疯暑,每個(gè)通道擁有一條ChannelPipeline處理器流水線训柴。</p><p class="section" data-paragraphid="5d1599d1cf424383a93684426cbe7867_5">AbstractChannel內(nèi)部有一個(gè)parent屬性,表示通道的父通道妇拯。對于連接監(jiān)聽通道(如NioServerSocketChannel實(shí)例)來說幻馁,其父親通道為null;而對于每一條傳輸通道(如NioSocketChannel實(shí)例)越锈,其parent屬性的值為接收到該連接的服務(wù)器連接監(jiān)聽通道仗嗦。</p><p class="section" data-paragraphid="7b82897fec204ba6a883da5559063a9a_5">幾乎所有的通道實(shí)現(xiàn)類都繼承了AbstractChannel抽象類,都擁有上面的parent和pipeline兩個(gè)屬性成員甘凭。</p><p class="section" data-paragraphid="46bd4fc11a79476591e3d4e8fba0c6f8_5">再來看一下稀拐,在通道接口中所定義的幾個(gè)重要方法:</p><p class="section" data-paragraphid="7c045e9595cd4387aec3ab3d06a414ec_5"><span class="bold">方法1.ChannelFuture connect(SocketAddress address)</span></p><p class="section" data-paragraphid="9c10b743df19498ea67896838616a19e_5">此方法的作用為:連接遠(yuǎn)程服務(wù)器。方法的參數(shù)為遠(yuǎn)程服務(wù)器的地址丹弱,調(diào)用后會立即返回德撬,返回值為負(fù)責(zé)連接操作的異步任務(wù)ChannelFuture。此方法在客戶端的傳輸通道使用躲胳。</p><p class="section" data-paragraphid="863376875004423aa48bd99681d4662b_5"><span class="bold">方法2.ChannelFuture bind(SocketAddress address)</span></p><p class="section" data-paragraphid="dc197032016a4a7798f6be8dcc2f63b5_5">此方法的作用為:綁定監(jiān)聽地址砰逻,開始監(jiān)聽新的客戶端連接。此方法在服務(wù)器的新連接監(jiān)聽和接收通道使用泛鸟。</p><p class="section" data-paragraphid="739e4806135d4fc1ac26b466b593efe1_5"><span class="bold">方法3.ChannelFuture close()</span></p><p class="section" data-paragraphid="5bcecca0d8f943c28340dbc45e4358ec_5">此方法的作用為:關(guān)閉通道連接蝠咆,返回連接關(guān)閉的ChannelFuture異步任務(wù)。如果需要在連接正式關(guān)閉后執(zhí)行其他操作,則需要為異步任務(wù)設(shè)置回調(diào)方法刚操;或者調(diào)用ChannelFuture異步任務(wù)的sync( )方法來阻塞當(dāng)前線程闸翅,一直等到通道關(guān)閉的異步任務(wù)執(zhí)行完畢。</p><p class="section" data-paragraphid="fb9558f129fb415daa204d6550a5ee8b_5"><span class="bold">方法4.Channel read()</span></p><p class="section" data-paragraphid="6fb72f508d844cb5a79cd1f068ecc6c8_5">此方法的作用為:讀取通道數(shù)據(jù)菊霜,并且啟動入站處理坚冀。具體來說,從內(nèi)部的Java NIO Channel通道讀取數(shù)據(jù)鉴逞,然后啟動內(nèi)部的Pipeline流水線记某,開啟數(shù)據(jù)讀取的入站處理。此方法的返回通道自身用于鏈?zhǔn)秸{(diào)用构捡。</p><p class="section" data-paragraphid="7b39e0d4e5ed44efab526a2dde505aef_5"><span class="bold">方法5.ChannelFuture write(Object o)</span></p><p class="section" data-paragraphid="2802225de27f428ebdd898f30d623312_5">此方法的作用為:啟程出站流水處理液南,把處理后的最終數(shù)據(jù)寫到底層Java NIO通道。此方法的返回值為出站處理的異步處理任務(wù)勾徽。</p><p class="section" data-paragraphid="3860cd9fbaca471799e1e38c27f575cb_5"><span class="bold">方法6.Channel flush()</span></p><p class="section" data-paragraphid="872f16159e8f4bc897bb5341c0264e14_5">此方法的作用為:將緩沖區(qū)中的數(shù)據(jù)立即寫出到對端滑凉。并不是每一次write操作都是將數(shù)據(jù)直接寫出到對端,write操作的作用在大部分情況下僅僅是寫入到操作系統(tǒng)的緩沖區(qū)喘帚,操作系統(tǒng)會將根據(jù)緩沖區(qū)的情況畅姊,決定什么時(shí)候把數(shù)據(jù)寫到對端。而執(zhí)行flush()方法立即將緩沖區(qū)的數(shù)據(jù)寫到對端吹由。</p><p class="section" data-paragraphid="3b7aff530a4444de9731e64df5044326_5">上面的6種方法若未,僅僅是比較常見的方法。在Channel接口中以及各種通道的實(shí)現(xiàn)類中倾鲫,還定義了大量的通道操作方法陨瘩。在一般的日常的開發(fā)中,如果需要用到级乍,請直接查閱Netty API文檔或者Netty源代碼舌劳。</p></div>
<div class="ne-content J_NEContent" style="min-height: 380px;"><h3 class="section j-chapter" data-paragraphid="d3c4a017655e4680aeb2ef05e16e7a36_5">6.4.2 EmbeddedChannel嵌入式通道</h3><p class="section" data-paragraphid="5c146459a4104a5ab2241d898c246e6e_5">在Netty的實(shí)際開發(fā)中,通信的基礎(chǔ)工作玫荣,Netty已經(jīng)替大家完成甚淡。實(shí)際上,大量的工作是設(shè)計(jì)和開發(fā)ChannelHandler通道業(yè)務(wù)處理器捅厂,而不是開發(fā)Outbound出站處理器贯卦,換句話說就是開發(fā)Inbound入站處理器。開發(fā)完成后焙贷,需要投入單元測試撵割。單元測試的大致流程是:需要將Handler業(yè)務(wù)處理器加入到通道的Pipeline流水線中,接下來先后啟動Netty服務(wù)器辙芍、客戶端程序啡彬,相互發(fā)送消息羹与,測試業(yè)務(wù)處理器的效果。如果每開發(fā)一個(gè)業(yè)務(wù)處理器庶灿,都進(jìn)行服務(wù)器和客戶端的重復(fù)啟動纵搁,這整個(gè)的過程是非常的煩瑣和浪費(fèi)時(shí)間的。如何解決這種徒勞的往踢、低效的重復(fù)工作呢腾誉?</p><p class="section" data-paragraphid="4cf63943eed54297b5414154051aa558_5">Netty提供了一個(gè)專用通道——名字叫EmbeddedChannel(嵌入式通道)。</p><p class="section" data-paragraphid="803ca89a5a4c48418ea6cc8285531388_5">EmbeddedChannel僅僅是模擬入站與出站的操作峻呕,底層不進(jìn)行實(shí)際的傳輸利职,不需要啟動Netty服務(wù)器和客戶端。除了不進(jìn)行傳輸之外瘦癌,EmbeddedChannel的其他的事件機(jī)制和處理流程和真正的傳輸通道是一模一樣的猪贪。因此,使用它佩憾,開發(fā)人員可以在開發(fā)的過程中方便、快速地進(jìn)行ChannelHandler業(yè)務(wù)處理器的單元測試干花。</p><p class="section" data-paragraphid="4229503d3f6b4072ad4de3857fd0a762_5">為了模擬數(shù)據(jù)的發(fā)送和接收妄帘,EmbeddedChannel提供了一組專門的方法,具體如表6-1所示池凄。</p><p class="yd-paragraph-c section" data-paragraphid="1b6647ccba51446db4bdc77cbd6a4fd2_5"><span class="bold">表6-1 EmbeddedChannel單元測試的輔助方法</span></p><p class="section" data-paragraphid="d0ab161562a84283af15e2f777f39551_5" style="clear: both;"><img class="paragraph-img" data-src="https://easyreadfs.nosdn.127.net/image_fb77b280cdcf4198aa930db69473d867" data-width="1436" data-height="493" src="https://easyreadfs.nosdn.127.net/image_fb77b280cdcf4198aa930db69473d867" style="width: 660px;"></p><p class="section" data-paragraphid="c25102a0978b44f4baba2166bbadbc2e_5">最為重要的兩個(gè)方法為:writeInbound和readOutbound方法抡驼。</p><p class="section" data-paragraphid="9d3ef7da75c44099a3709a3675a67f6c_5"><span class="bold">方法1.writeInbound入站數(shù)據(jù)寫到通道</span></p><p class="section" data-paragraphid="f313b1fc84c7408c863edad0627ab0d8_5">它的使用場景是:測試入站處理器。在測試入站處理器時(shí)(例如測試一個(gè)解碼器)肿仑,需要讀取Inbound(入站)數(shù)據(jù)致盟。可以調(diào)用writeInbound方法尤慰,向EmbeddedChannel寫入一個(gè)入站二進(jìn)制ByteBuf數(shù)據(jù)包馏锡,模擬底層的入站包。</p><p class="section" data-paragraphid="4bb202ea5815491dbf19df8abd25ed39_5"><span class="bold">方法2.readOutbound讀取通道的出站數(shù)據(jù)</span></p><p class="section" data-paragraphid="45c78b07aab7440cb8769694b2be42c9_5">它的使用場景是:測試出站處理器伟端。在測試出站處理器時(shí)(例如測試一個(gè)編碼器)杯道,需要查看處理過的結(jié)果數(shù)據(jù)≡痱穑可以調(diào)用readOutbound方法党巾,讀取通道的最終出站結(jié)果,它是經(jīng)過流水線一系列的出站處理后霜医,最終的出站數(shù)據(jù)包齿拂。比較繞口,重復(fù)一遍肴敛,通過readOutbound署海,可以讀取完成EmbeddedChannel最后一個(gè)出站處理器吗购,處理后的ByteBuf二進(jìn)制出站包。</p><p class="section" data-paragraphid="8a12b9e851d142f49c375eed5cf05ded_5">總之叹侄,這個(gè)EmbeddedChannel類巩搏,既具備通道的通用接口和方法,又增加了一些單元測試的輔助方法趾代,在開發(fā)時(shí)是非常有用的贯底。它的具體用法,后面還會結(jié)合其他的Netty組件的實(shí)例反復(fù)提到撒强。</p></div>
<div class="ne-content J_NEContent" style="min-height: 380px;"><h2 class="section j-chapter" data-paragraphid="d4e07f99394c42539c7f4effaffd1f44_5">6.5 詳解Handler業(yè)務(wù)處理器</h2><p class="section" data-paragraphid="3fc9ae12e70b45d48652b35a95cd6d95_5">在Reactor反應(yīng)器經(jīng)典模型中禽捆,反應(yīng)器查詢到IO事件后,分發(fā)到Handler業(yè)務(wù)處理器飘哨,由Handler完成IO操作和業(yè)務(wù)處理胚想。</p><p class="section" data-paragraphid="a6a533508395455a9cf2493fa62b3556_5">整個(gè)的IO處理操作環(huán)節(jié)包括:從通道讀數(shù)據(jù)包、數(shù)據(jù)包解碼芽隆、業(yè)務(wù)處理浊服、目標(biāo)數(shù)據(jù)編碼、把數(shù)據(jù)包寫到通道胚吁,然后由通道發(fā)送到對端牙躺,如圖6-8所示。</p><p class="section" data-paragraphid="bfacb21ae32c44d3aa0a107420856992_5">前后兩個(gè)環(huán)節(jié)腕扶,從通道讀數(shù)據(jù)包和由通道發(fā)送到對端孽拷,由Netty的底層負(fù)責(zé)完成,不需要用戶程序負(fù)責(zé)半抱。</p><p class="section" data-paragraphid="381f3ba046a94b308ad06c4cbe00230d_5" style="clear: both;"><img class="paragraph-img" data-src="https://easyreadfs.nosdn.127.net/image_c7cd89a1a56549aa8f58df56614bf2dc" data-width="951" data-height="383" src="https://easyreadfs.nosdn.127.net/image_c7cd89a1a56549aa8f58df56614bf2dc" style="width: 660px;"></p><p class="yd-imagetitle section" data-paragraphid="fad15b28dc0f46d6b6dfa6050e39a64d_5">圖6-8 整個(gè)的IO處理操作環(huán)節(jié)</p><p class="section" data-paragraphid="6478f1656bbc4fe586bf61d6e77ba3c8_5">用戶程序主要在Handler業(yè)務(wù)處理器中脓恕,Handler涉及的環(huán)節(jié)為:數(shù)據(jù)包解碼、業(yè)務(wù)處理窿侈、目標(biāo)數(shù)據(jù)編碼炼幔、把數(shù)據(jù)包寫到通道中。</p><p class="section" data-paragraphid="49ca94b6e99542e5b739d24ddc02a318_5">前面已經(jīng)介紹過史简,從應(yīng)用程序開發(fā)人員的角度來看江掩,有入站和出站兩種類型操作。</p><p class="section" data-paragraphid="18a172efba394b6687f0fe2c063b882d_5"><span class="yd-font-hkkai">? 入站處理乘瓤,觸發(fā)的方向?yàn)椋鹤缘紫蛏匣沸危琋etty的內(nèi)部(如通道)到ChannelInboundHandler入站處理器。</span></p><p class="section" data-paragraphid="48934d02ba0047a88ecdb413db3fce77_5"><span class="yd-font-hkkai">? 出站處理衙傀,觸發(fā)的方向?yàn)椋鹤皂斚蛳绿б鳎瑥腃hannelOutboundHandler出站處理器到Netty的內(nèi)部(如通道)。</span></p><p class="section" data-paragraphid="55b5487d9470459ba3b1c741297a6ac8_5">按照這種方向來分统抬,前面數(shù)據(jù)包解碼火本、業(yè)務(wù)處理兩個(gè)環(huán)節(jié)——屬于入站處理器的工作危队;后面目標(biāo)數(shù)據(jù)編碼、把數(shù)據(jù)包寫到通道中兩個(gè)環(huán)節(jié)——屬于出站處理器的工作。</p></div>
<div class="ne-content J_NEContent" style="min-height: 380px;"><h3 class="section j-chapter" data-paragraphid="b4cb5e07bd5543639b19c7c1341ca259_5">6.5.1 ChannelInboundHandler通道入站處理器</h3><p class="section" data-paragraphid="e1d29736afca428385804b894e088bb5_5">當(dāng)數(shù)據(jù)或者信息入站到Netty通道時(shí),Netty將觸發(fā)入站處理器ChannelInboundHandler所對應(yīng)的入站API慰丛,進(jìn)行入站操作處理。</p><p class="section" data-paragraphid="585e9e0f79544034b1491e9bd088d7d5_5">ChannelInboundHandler的主要操作簿盅,如圖6-9所示,具體的介紹如下:</p><p class="section" data-paragraphid="654dfec3442541dca6da948483b5abf4_5" style="clear: both;"><img class="paragraph-img" data-src="https://easyreadfs.nosdn.127.net/image_e4623cec888c4f44b9f05f7657b11b68" data-width="819" data-height="616" src="https://easyreadfs.nosdn.127.net/image_e4623cec888c4f44b9f05f7657b11b68" style="width: auto; height: 380px;"></p><p class="yd-imagetitle section" data-paragraphid="ee06daef17344c3cac40c472823be2e7_5">圖6-9 ChannelInboundHandler的主要操作</p><p class="section" data-paragraphid="dab3e0f588fc4c9cbacbd9445fda44ca_5"><span class="bold">1. channelRegistered</span></p><p class="section" data-paragraphid="b866e003a3d04a7bbf11ef0b00a845f0_5">當(dāng)通道注冊完成后揍魂,Netty會調(diào)用fireChannelRegistered桨醋,觸發(fā)通道注冊事件。通道會啟動該入站操作的流水線處理现斋,在通道注冊過的入站處理器Handler的channelRegistered方法喜最,會被調(diào)用到。</p><p class="section" data-paragraphid="2427ca1ba2e04b62b0038c9e53f9809b_5"><span class="bold">2. channelActive</span></p><p class="section" data-paragraphid="25c1c9b0c17e4b54b843083b5d1d5f28_5">當(dāng)通道激活完成后庄蹋,Netty會調(diào)用fireChannelActive瞬内,觸發(fā)通道激活事件。通道會啟動該入站操作的流水線處理限书,在通道注冊過的入站處理器Handler的channelActive方法虫蝶,會被調(diào)用到。</p><p class="section" data-paragraphid="d855cdca15de4b08b3b91f1dfaa29b06_5"><span class="bold">3. channelRead</span></p><p class="section" data-paragraphid="0820da23d35c4cb19687cad7e9d1f539_5">當(dāng)通道緩沖區(qū)可讀蔗包,Netty會調(diào)用fireChannelRead秉扑,觸發(fā)通道可讀事件慧邮。通道會啟動該入站操作的流水線處理调限,在通道注冊過的入站處理器Handler的channelRead方法,會被調(diào)用到误澳。</p><p class="section" data-paragraphid="374e253b57d04b7a80956fafbfde8721_5"><span class="bold">4. channelReadComplete</span></p><p class="section" data-paragraphid="5337245b4d324fcf9f38c0d1e7dfc0f1_5">當(dāng)通道緩沖區(qū)讀完耻矮,Netty會調(diào)用fireChannelReadComplete,觸發(fā)通道讀完事件忆谓。通道會啟動該入站操作的流水線處理裆装,在通道注冊過的入站處理器Handler的channelReadComplete方法,會被調(diào)用到倡缠。</p><p class="section" data-paragraphid="bca716c2e86b4e2f98b5b0d6ea35a594_5"><span class="bold">5. channelInactive</span></p><p class="section" data-paragraphid="2f59133d23fb4faeb5ae3465182d08c3_5">當(dāng)連接被斷開或者不可用哨免,Netty會調(diào)用fireChannelInactive,觸發(fā)連接不可用事件昙沦。通道會啟動對應(yīng)的流水線處理琢唾,在通道注冊過的入站處理器Handler的channelInactive方法,會被調(diào)用到盾饮。</p><p class="section" data-paragraphid="1db6e36fdb2247ada973844404a24f75_5"><span class="bold">6. exceptionCaught</span></p><p class="section" data-paragraphid="0c9216827d7c4a79838000170f6341be_5">當(dāng)通道處理過程發(fā)生異常時(shí)采桃,Netty會調(diào)用fireExceptionCaught懒熙,觸發(fā)異常捕獲事件。通道會啟動異常捕獲的流水線處理普办,在通道注冊過的處理器Handler的exceptionCaught方法工扎,會被調(diào)用到。注意衔蹲,這個(gè)方法是在通道處理器中ChannelHandler定義的方法肢娘,入站處理器、出站處理器接口都繼承到了該方法踪危。</p><p class="section" data-paragraphid="6b224a6c56b14e14918e448e88979cc8_5">上面介紹的并不是ChanneInboundHandler的全部方法蔬浙,僅僅介紹了其中幾種比較重要的方法。在Netty中贞远,它的默認(rèn)實(shí)現(xiàn)為ChannelInboundHandlerAdapter畴博,在實(shí)際開發(fā)中,只需要繼承這個(gè)ChannelInboundHandlerAdapter默認(rèn)實(shí)現(xiàn)蓝仲,重寫自己需要的方法即可俱病。</p></div>
<div class="ne-content J_NEContent" style="min-height: 380px;"><h3 class="section j-chapter" data-paragraphid="1b071317fdc341fb8bdf4fe20e8b8623_5">6.5.2 ChannelOutboundHandler通道出站處理器</h3><p class="section" data-paragraphid="571f24d914f04eba82e352bed4129ae2_5">當(dāng)業(yè)務(wù)處理完成后,需要操作Java NIO底層通道時(shí)袱结,通過一系列的ChannelOutboundHandler通道出站處理器亮隙,完成Netty通道到底層通道的操作。比方說建立底層連接垢夹、斷開底層連接溢吻、寫入底層Java NIO通道等。ChannelOutboundHandler接口定義了大部分的出站操作果元,如圖6-10所示促王,具體的介紹如下:</p><p class="section" data-paragraphid="32872eda841f497b86d6076071881a6e_5" style="clear: both;"><img class="paragraph-img" data-src="https://easyreadfs.nosdn.127.net/image_08d2d0d7b18d496aa84bcdf37ae69c04" data-width="853" data-height="654" src="https://easyreadfs.nosdn.127.net/image_08d2d0d7b18d496aa84bcdf37ae69c04" style="width: auto; height: 380px;"></p><p class="yd-imagetitle section" data-paragraphid="1eec92a037f74e08ba9d0315d098dc21_5">如圖6-10 ChannelOutboundHandler的主要操作</p><p class="section" data-paragraphid="557ced287e694286aa92787490121674_5">再強(qiáng)調(diào)一下,出站處理的方向:是通過上層Netty通道而晒,去操作底層Java IO通道蝇狼。主要出站(Outbound)的操作如下:</p><p class="section" data-paragraphid="64f2eae7008749429b3ef374f3e48a3a_5"><span class="bold">1. bind</span></p><p class="section" data-paragraphid="7db9169c1cb0412eb7b0a8716d10acdf_5">監(jiān)聽地址(IP+端口)綁定:完成底層Java IO通道的IP地址綁定。如果使用TCP傳輸協(xié)議倡怎,這個(gè)方法用于服務(wù)器端迅耘。</p><p class="section" data-paragraphid="e2c4301e8a07464c80f786cd13253b82_5"><span class="bold">2. connect</span></p><p class="section" data-paragraphid="c16c175b5e414cd0bce5eff20bc270f5_5">連接服務(wù)端:完成底層Java IO通道的服務(wù)器端的連接操作。如果使用TCP傳輸協(xié)議监署,這個(gè)方法用于客戶端颤专。</p><p class="section" data-paragraphid="72781b9fc2fd40a58e7d81fc5434c120_5"><span class="bold">3. write</span></p><p class="section" data-paragraphid="a50ea1aca84a4101be637650fd027ba6_5">寫數(shù)據(jù)到底層:完成Netty通道向底層Java IO通道的數(shù)據(jù)寫入操作。此方法僅僅是觸發(fā)一下操作而已钠乏,并不是完成實(shí)際的數(shù)據(jù)寫入操作栖秕。</p><p class="section" data-paragraphid="76ffea7fc45c4d34a3e222410b721402_5"><span class="bold">4. flush</span></p><p class="section" data-paragraphid="50d5ae8ec8784e9e96fdddd0b00fd60b_5">騰空緩沖區(qū)中的數(shù)據(jù),把這些數(shù)據(jù)寫到對端:將底層緩存區(qū)的數(shù)據(jù)騰空缓熟,立即寫出到對端累魔。</p><p class="section" data-paragraphid="b62ca1c3951f4dd38830f5829518fee5_5"><span class="bold">5. read</span></p><p class="section" data-paragraphid="0393b6c40f544da4a5e4bfb626beb539_5">從底層讀數(shù)據(jù):完成Netty通道從Java IO通道的數(shù)據(jù)讀取摔笤。</p><p class="section" data-paragraphid="e6446a0116344658829b37f0e7b09edc_5"><span class="bold">6. disConnect</span></p><p class="section" data-paragraphid="95d803b007064d1c87713dafff737f3b_5">斷開服務(wù)器連接:斷開底層Java IO通道的服務(wù)器端連接。如果使用TCP傳輸協(xié)議垦写,此方法主要用于客戶端吕世。</p><p class="section" data-paragraphid="cb4c7c29cc5f4edf8e43874a14afe68f_5"><span class="bold">7. close</span></p><p class="section" data-paragraphid="1a8d4e49e6c54c398579ce1f5b776f30_5">主動關(guān)閉通道:關(guān)閉底層的通道,例如服務(wù)器端的新連接監(jiān)聽通道梯投。</p><p class="section" data-paragraphid="aac7f3639e15480aa1ceac833a5dcd31_5">上面介紹的并不是ChannelOutboundHandler的全部方法命辖,僅僅介紹了其中幾個(gè)比較重要的方法。在Netty中分蓖,它的默認(rèn)實(shí)現(xiàn)為ChannelOutboundHandlerAdapter尔艇,在實(shí)際開發(fā)中,只需要繼承這個(gè)ChannelOutboundHandlerAdapter默認(rèn)實(shí)現(xiàn)么鹤,重寫自己需要的方法即可终娃。</p></div>
<div class="ne-content J_NEContent" style="min-height: 380px;"><h3 class="section j-chapter" data-paragraphid="748071b29d4b474fb2e3864f707855fc_5">6.5.3 ChannelInitializer通道初始化處理器</h3><p class="section" data-paragraphid="69c2d9d874a442bda54cdb3c322b4518_5">在前面已經(jīng)講到,通道和Handler業(yè)務(wù)處理器的關(guān)系是:一條Netty的通道擁有一條Handler業(yè)務(wù)處理器流水線蒸甜,負(fù)責(zé)裝配自己的Handler業(yè)務(wù)處理器棠耕。裝配Handler的工作,發(fā)生在通道開始工作之前∧拢現(xiàn)在的問題是:如果向流水線中裝配業(yè)務(wù)處理器呢窍荧?這就得借助通道的初始化類——ChannelInitializer。</p><p class="section" data-paragraphid="c6c8cdf134334a8d83f43f2f7cea5065_5">首先回顧一下NettyDiscardServer丟棄服務(wù)端的代碼恨憎,在給接收到的新連接裝配Handler業(yè)務(wù)處理器時(shí)蕊退,使用childHandler()方法設(shè)置了一個(gè)ChannelInitializer實(shí)例:</p><p class="yd-code section" data-paragraphid="90c958807daf4130b661fb139400b074_5">//5 裝配子通道流水線
b.childHandler(new ChannelInitializer<SocketChannel>() {
//有連接到達(dá)時(shí)會創(chuàng)建一個(gè)通道
protected void initChannel(SocketChannelch) throws Exception {
// 流水線管理子通道中的Handler業(yè)務(wù)處理器
// 向子通道流水線添加一個(gè)Handler業(yè)務(wù)處理器
ch.pipeline().addLast(new NettyDiscardHandler());
}
});</p><p class="section" data-paragraphid="2c1a2211c5ee48e58b95a848a63b1103_5">上面的ChannelInitializer也是通道初始化器,屬于入站處理器的類型憔恳。在示例代碼中瓤荔,使用了ChannelInitializer的initChannel()方法。它是何方神圣呢喇嘱?</p><p class="section" data-paragraphid="f0c4895700054394aec007d6cedc99fb_5">initChannel()方法是ChannelInitializer定義的一個(gè)抽象方法茉贡,這個(gè)抽象方法需要開發(fā)人員自己實(shí)現(xiàn)塞栅。在父通道調(diào)用initChannel()方法時(shí)者铜,會將新接收的通道作為參數(shù),傳遞給initChannel()方法放椰。initChannel()方法內(nèi)部大致的業(yè)務(wù)代碼是:拿到新連接通道作為實(shí)際參數(shù)作烟,往它的流水線中裝配Handler業(yè)務(wù)處理器。</p></div>