首先來張網(wǎng)上盛傳的netty框架參考圖药薯,以供讀者把握Netty的整體框架及核心組件拙寡,繼而發(fā)散出Netty的重點知識講解:
1.Netty Reactor模型
Reactor模型是對傳統(tǒng)阻塞IO模型的巨大改進(jìn)盆繁,實現(xiàn)了向異步非阻塞的飛躍秩铆,節(jié)省了頻繁創(chuàng)建線程和切換線程的開銷,極大的提高了IO效率曾棕,是現(xiàn)代高性能網(wǎng)絡(luò)讀寫處理采用的主要模型。
Reactor模型的核心思想是:事件驅(qū)動+分而治之菜循。
它們的Channel注冊翘地,及監(jiān)聽關(guān)心事件(OP_ACCEPT、OP_READ、OP_WRITE衙耕、OP_CONNECT)昧穿,及事件觸發(fā)時處理流程,請見《Netty的啟動過程二》和《從Java.IO到Java.NIO再到Netty》橙喘。
Reactor有三種模型时鸵,分別為:
1).單線程Reactor
所有I/O操作都由一個線程完成,即多路復(fù)用厅瞎、事件分發(fā)和處理都是在一個Reactor線程上完成的饰潜。
代碼實現(xiàn)大致為:
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup)
2).多線程Reactor
一個Acceptor負(fù)責(zé)接收請求,一個Reactor Thread Pool負(fù)責(zé)處理I/O操作和簸。
代碼實現(xiàn)大致為:
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
3).主從多線程Reactor
一個Acceptor負(fù)責(zé)接收請求彭雾,一個Main Reactor Thread Pool負(fù)責(zé)連接,一個Sub Reactor Thread Pool負(fù)責(zé)處理I/O操作锁保。
代碼實現(xiàn)大致為:
EventLoopGroup bossGroup = new NioEventLoopGroup(4);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
當(dāng)Netty boss線程組和worker線程組都啟動后薯酝,一個EventLoopGroup可對應(yīng)多個EventLoop,每個EventLoop相對應(yīng)一個Selector爽柒。
2.Channel吴菠、ChannelPipeline、ChannelHandler浩村、ChannelHandlerContext關(guān)系
Channel是客戶端與服務(wù)端所有I/O操作的通道做葵,當(dāng)客戶端請求連接服務(wù)端時,boss線程就會為此連接創(chuàng)建一個SocketChannel注冊到worker線程組的一個EventLoop上穴亏,并監(jiān)聽讀事件蜂挪。同時,在創(chuàng)建SocketChannel時嗓化,也會創(chuàng)建一個ChannelPipeline棠涮,ChannelPipeline其實是一個維護(hù)ChannelHandlerContext的雙向鏈表,ChannelPipeline創(chuàng)建時刺覆,會默認(rèn)增加HeadContext和TailContext各一個放入其中严肪,最終在SocketChannel注冊到worker線程的EventLoop上時,會將childHandler轉(zhuǎn)為ChannelHandlerContext加入ChannelPipeline中谦屑,因此ChannelHandlerContext與childHandler也是一一對應(yīng)的驳糯。
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();//創(chuàng)建channel同時創(chuàng)建ChannelPipeline
}
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
final AbstractChannelHandlerContext newCtx;
synchronized (this) {
checkMultiplicity(handler);
newCtx = newContext(group, filterName(name, handler), handler);//handler轉(zhuǎn)為context,ChannelPipeline實際維護(hù)的是handlerContext鏈
addLast0(newCtx);
}
callHandlerAdded0(newCtx);
return this;
}
因此氢橙,最后在用戶自定義的handler中和客戶端的交互數(shù)據(jù)其實都是ChannelHandlerContext酝枢,如
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
}
ChannelHandlerContext可以看成是ChannelHandler實例與ChannelPipeline之間的橋梁,在ChannelHandlerContext中可以獲取客戶端相應(yīng)的Channel悍手,與之對應(yīng)的childHandler帘睦,及所在的pipeline袍患。
此外,ctx.channel().writeAndFlush(msg)與ctx.writeAndFlush(msg)的區(qū)別是竣付,ctx.channel().writeAndFlush(msg)會從出站方向ChannelPipeline的最后一個childHandler把數(shù)據(jù)發(fā)出去诡延,ctx.writeAndFlush(msg)是把數(shù)據(jù)發(fā)給出站方向該handlerContext的下一個childHandler。
因此古胆,一個EventLoop可以監(jiān)聽多個Channel肆良,每個Channel都有一個ChannelPipeline,ChannelPipeline里維護(hù)多個ChannelHandlerContext逸绎,每個ChannelHandlerContext都有一個相對應(yīng)的childHandler惹恃。
3.Future、ChannelFuture桶良、ChannelPromise區(qū)別
Future座舍、ChannelFuture、ChannelPromise相同點是都是Netty異步操作的結(jié)果結(jié)構(gòu)陨帆。
Netty中所有的I/O操作都是異步的曲秉,所有的I/O調(diào)用在調(diào)用結(jié)束時會立即返回,但并不保證所有的I/O操作都完成了疲牵,當(dāng)需要知道某些異步操作結(jié)果是否成功或完成或失敗時承二,F(xiàn)uture便存在了它的使用價值。Netty Future繼承自java.util.concurrent.Future纲爸,并擴(kuò)展增加了自己的一些方法亥鸠,使得獲得異步操作結(jié)果更為方便和實用性。比如isSuccess()识啦、Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener)负蚊、sync()方法等。ChannelFuture又繼承自Netty Future颓哮,此外還加入了Channel channel()家妆,即在ChannelFuture中可以獲取該客戶端連接Channe。ChannelPromise又繼承自ChannelFuture冕茅,且實現(xiàn)了Promise接口伤极,但是它與ChannelFuture不同的是,它是可寫的姨伤,如setSuccess()哨坪、setFailure(Throwable cause)等方法,它可以標(biāo)記Futrue的狀態(tài)乍楚,并通知所有的監(jiān)聽者listeners当编,而listeners是通過addListener方法添加的,同樣的徒溪,ChannelPromise中也可以獲取該客戶端連接Channel凌箕。
4.ByteBuffer拧篮、ByteBuf、UnpooledByteBuf牵舱、PooledByteBuf關(guān)系
ByteBuffer為Java NIO的數(shù)據(jù)容器,它長度固定缺虐,一旦分配完成后芜壁,它的容量不能動態(tài)擴(kuò)展和收縮;它只有一個標(biāo)識位控的指針position高氮,讀寫的時候需要手工調(diào)用flip()和rewind()等慧妄,使用者必須小心謹(jǐn)慎地處理這些API,否則很容易導(dǎo)致程序處理失敗剪芍。
ByteBuf為Netty的數(shù)據(jù)容器塞淹,ByteBuf支持動態(tài)擴(kuò)容,且通過兩個位置指針來協(xié)助緩沖區(qū)的讀寫操作罪裹,由于寫操作不修改readerIndex指針饱普,讀操作不修改writerIndex指針,因此讀寫之間不再需要調(diào)整位置指針状共,這極大地簡化了緩沖區(qū)的讀寫操作套耕。
Netty的ByteBuf分為3種類型:
1).heap buffers(堆緩沖區(qū))
這種模式是將數(shù)據(jù)存儲在JVM的堆空間中。 這種模式也稱為 backing array峡继,在未使用池的情況下提供快速分配和釋放冯袍。這種類型ByteBuf在用hasArray()方法判斷時返回為true。
2).direct buffers(直接緩沖區(qū))
非堆內(nèi)存碾牌,它在JVM堆外進(jìn)行內(nèi)存分配康愤。直接緩沖區(qū)的主要缺點是分配和釋放它們比堆緩沖區(qū)更昂貴,因為它不受JVM垃圾回收管控舶吗。如果需要解析直接緩沖區(qū)的ByteBuf數(shù)據(jù)內(nèi)容征冷,那它需要額外做一次內(nèi)存復(fù)制,這種情況性能會有一些下降裤翩。這種類型ByteBuf在用hasArray()方法判斷時返回為false资盅。
netty官方有一句描述了使用直接緩沖區(qū)的風(fēng)險:allocating many short-lived direct NIO buffers often causes an OutOfMemoryError。為了更高效地使用堆外緩沖區(qū)踊赠,netty通過內(nèi)存池和引用計數(shù)很好地繞開了Direct Buffer的劣勢呵扛,發(fā)揚了它的優(yōu)勢。
使用堆緩沖區(qū)還是直接緩沖區(qū)的最佳實踐筐带,應(yīng)該是根據(jù)我們的業(yè)務(wù)類型來今穿,如果我們需要頻繁解析ByteBuf的數(shù)據(jù)內(nèi)容,那我們可以選擇使用堆緩沖區(qū)伦籍,而對于I/O通信線程在讀寫緩沖區(qū)時蓝晒,那可以選擇直接緩沖區(qū)腮出。
3).composite buffers(復(fù)合緩沖區(qū))
它提供了多個或多種類型的ByteBuf的聚合視圖,在這里芝薇,可以根據(jù)需要添加和刪除ByteBuf實例胚嘲,這是JDK的ByteBuffer實現(xiàn)中完全沒有的功能。如果某個消息含有消息頭和消息體洛二,而消息頭不變馋劈,就可以使用此類型,從而消除了消息頭和消息體不必要的復(fù)制晾嘶。
從內(nèi)存回收角度看妓雾,ByteBuf也分為兩類:
1).基于對象池的PooledByteBuf
2).非對象池的UnpooledByteBuf
兩者的主要區(qū)別就是基于對象池的ByteBuf可以重用ByteBuf對象,它自己維護(hù)了一個內(nèi)存池垒迂,可以循環(huán)利用創(chuàng)建的ByteBuf械姻,提升內(nèi)存的使用效率,降低由于高負(fù)載導(dǎo)致的頻繁GC机断。測試表明使用內(nèi)存池后的Netty在高負(fù)載楷拳、大并發(fā)的沖擊下內(nèi)存和GC更加平穩(wěn)。盡管推薦使用基于內(nèi)存池的ByteBuf毫缆,但是內(nèi)存池的管理和維護(hù)更加復(fù)雜唯竹,使用起來也需要更加謹(jǐn)慎,因此苦丁,Netty提供了靈活的策略供使用者來做選擇浸颓。
通過一個Channel或ChannelHandlerContext的alloc()方法可以獲得一個ByteBuf分配的工具,即ByteBufAllocator旺拉,該工具默認(rèn)使用池化的ByteBuf對象分配产上,可見Netty是推薦使用PooledByteBuf分配ByteBuf的,如下:
static final ByteBufAllocator DEFAULT_ALLOCATOR;
static {
String allocType = SystemPropertyUtil.get(
"io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
allocType = allocType.toLowerCase(Locale.US).trim();
ByteBufAllocator alloc;
if ("unpooled".equals(allocType)) {
alloc = UnpooledByteBufAllocator.DEFAULT;
} else if ("pooled".equals(allocType)) {
alloc = PooledByteBufAllocator.DEFAULT;
} else {
alloc = PooledByteBufAllocator.DEFAULT;
}
DEFAULT_ALLOCATOR = alloc;
}
如果因為使用ByteBuf不當(dāng)導(dǎo)致內(nèi)存泄露蛾狗,可以使用參數(shù)'-Dio.netty.leakDetectionLevel=advanced' 定位ByteBuf內(nèi)存泄露問題晋涣。
5.ByteBuf Zero-copy
所謂的 Zero-copy,就是在操作數(shù)據(jù)時沉桌,不需要將數(shù)據(jù)從一個內(nèi)存區(qū)域拷貝到另一個內(nèi)存區(qū)域谢鹊。因為少了一次內(nèi)存的拷貝, 因此 CPU 的效率就得到的提升留凭。在 OS 層面上的 Zero-copy 通常指避免在 用戶態(tài)(User-space) 與 內(nèi)核態(tài)(Kernel-space) 之間來回拷貝數(shù)據(jù)佃扼。
Netty的零拷貝體現(xiàn)在三個方面:
1).Direct Buffers
Netty的接收和發(fā)送ByteBuf采用direct buffers,使用堆外直接內(nèi)存進(jìn)行Socket讀寫蔼夜,不需要進(jìn)行字節(jié)緩沖區(qū)的二次拷貝兼耀。如果使用傳統(tǒng)的堆內(nèi)存(heap buffers)進(jìn)行Socket讀寫,JVM會將堆內(nèi)存Buffer拷貝一份到直接內(nèi)存中,然后才寫入Socket中瘤运。相比于堆外直接內(nèi)存窍霞,消息在發(fā)送過程中多了一次緩沖區(qū)的內(nèi)存拷貝。
2).Composite Buffers
Netty提供了復(fù)合Buffer對象拯坟,可以聚合多個ByteBuf對象但金,用戶可以像操作一個Buffer那樣方便的對復(fù)合Buffer進(jìn)行操作,避免了傳統(tǒng)通過內(nèi)存拷貝的方式將幾個小Buffer合并成一個大的Buffer郁季。
3).FileChannel.transferTo
Netty的文件傳輸采用了transferTo方法傲绣,它可以直接將文件緩沖區(qū)的數(shù)據(jù)發(fā)送到目標(biāo)Channel,避免了傳統(tǒng)通過循環(huán)write方式導(dǎo)致的內(nèi)存拷貝問題巩踏。
(摘自李林鋒《Netty 系列之 Netty 高性能之道》)
6.各種預(yù)置的ChannelHandler、及編解碼器使用
ChannelInboundHandlerAdapter
入站處理器续搀,需要注意的是塞琼,消息在調(diào)用channelRead(ChannelHandlerContext, Object)方法返回后不會自動釋放(內(nèi)存引用),如果你需要找一個入站實現(xiàn)在消息接收后能自動釋放禁舷,請查看SimpleChannelInboundHandler彪杉。ChannelInboundHandlerAdapter是非常常用的一個消息入站處理器,常常用作消息解碼器的父類牵咙。比如在《使用Netty+Protobuf實現(xiàn)游戲WebSocket通信》一文中派近,它就作為websocket的解碼器解析BinaryWebSocketFrame。
SimpleChannelInboundHandler
一種入站處理器洁桌,常用于顯式的處理某種特定類型的消息渴丸,繼承自ChannelInboundHandlerAdapter。需要注意的是另凌,它會自動釋放已經(jīng)處理的消息谱轨,如果你想把消息傳給下個處理器處理,那么需要調(diào)用ReferenceCountUtil#retain(Object)方法保留消息吠谢。它也是一種常用的入站消息處理器土童。
ByteToMessageDecoder
任何數(shù)據(jù)類型想在網(wǎng)絡(luò)中進(jìn)行傳輸,都得經(jīng)過編解碼轉(zhuǎn)換成字節(jié)流工坊。該入站處理器會負(fù)責(zé)字節(jié)流的累加工作献汗,但是具體如何進(jìn)行解碼,則交由不同的子類(用戶自定義的處理器)去實現(xiàn)王污。如在《使用Netty+Protobuf實現(xiàn)游戲TCP通信》一文中罢吃,它就作為tcp協(xié)議的解碼器解析用戶自定義數(shù)據(jù)包。因為tcp就是個流的協(xié)議玉掸。
IdleStateHandler
它的作用是用于檢測channel在指定時間內(nèi)是否有數(shù)據(jù)流通刃麸,如果沒有的話,則觸發(fā)一個IdleStateEvent司浪,該Event是用于通知本channel的泊业,而不是用于通知對方把沼,所以,我們可以根據(jù)收到的Event來決定處理邏輯吁伺,常用于心跳處理饮睬。
此外,還有HttpServerCodec篮奄、HttpObjectAggregator捆愁、WebSocketServerProtocolHandler、DelimeterBasedFrameDecoder窟却、LineBasedFrameDecoder昼丑、FixedLenghtFrameDecoder、LengthFieldBasedFrameDecoder等等夸赫,這些可以自行百度查看如何使用菩帝。更多的見netty源碼包下handler.codec。
7.@ChannelHandler.Sharable有何用茬腿?
通常每個channel都有一個ChannelPipeline對應(yīng)呼奢,而每個ChannelPipeline下channelHandler都是該Channel私有的,但是切平,有些情況下握础,需要將某個channelHandler共有,這時悴品,可以將該channelHandler標(biāo)記為@ChannelHandler.Sharable禀综。
比如游戲服中,客戶端請求連接時他匪,需要將所有的客戶端Channel緩存起來菇存,這時它的消息handler便會標(biāo)記為@ChannelHandler.Sharable。再比如邦蜜,需要對客戶端某些ip過濾依鸥,也可以用此標(biāo)記;或者客戶端報錯統(tǒng)計等等悼沈。
該注解表明這個handler可以在多線程環(huán)境下使用贱迟,那么在使用時,需要注意它的使用安全絮供。
8.Channel的isOpen()衣吠、isRegistered()、isActive()和isWritable()狀態(tài)含義及轉(zhuǎn)換
open表示Channel的開放狀態(tài)壤靶,True表示Channel可用缚俏,F(xiàn)alse表示Channel已關(guān)閉不再可用。registered表示Channel的注冊狀態(tài),True表示已注冊到一個EventLoop忧换,F(xiàn)alse表示沒有注冊到EventLoop恬惯。active表示Channel的激活狀態(tài),對于ServerSocketChannel亚茬,True表示Channel已綁定到端口酪耳;對于SocketChannel,表示Channel可用(open)且已連接到對端刹缝。Writable表示Channel的可寫狀態(tài)碗暗,當(dāng)Channel的寫緩沖區(qū)outboundBuffer非null且可寫時返回True。
一個正常結(jié)束的Channel狀態(tài)轉(zhuǎn)移有以下兩種情況:
REGISTERED->CONNECT/BIND->ACTIVE->CLOSE->INACTIVE->UNREGISTERED
REGISTERED->ACTIVE->CLOSE->INACTIVE->UNREGISTERED
其中第一種是服務(wù)端用于綁定的Channel或者客戶端用于發(fā)起連接的Channel梢夯,第二種是服務(wù)端接受的SocketChannel言疗。一個異常關(guān)閉的Channel則不會服從這樣的狀態(tài)轉(zhuǎn)移。
(摘自《自頂向下深入分析Netty(六)--Channel總述》)
9.ServerBootstrap的option颂砸、childOption的一些常見參數(shù)
ChannelOption.SO_BACKLOG
ChannelOption.SO_BACKLOG對應(yīng)的是tcp/ip協(xié)議listen函數(shù)中的backlog參數(shù)洲守,函數(shù)listen(int socketfd,int backlog)用來初始化服務(wù)端可連接隊列,服務(wù)端處理客戶端連接請求是順序處理的沾凄,所以同一時間只能處理一個客戶端連接,多個客戶端來的時候知允,服務(wù)端將不能處理的客戶端連接請求放在隊列中等待處理撒蟀,backlog參數(shù)指定了隊列的大小。(在游戲服務(wù)器中常用)
ChannelOption.TCP_NODELAY
TCP參數(shù)温鸽,立即發(fā)送數(shù)據(jù)保屯,默認(rèn)值為Ture(Netty默認(rèn)為True而操作系統(tǒng)默認(rèn)為False)。該值設(shè)置Nagle算法的啟用涤垫,改算法將小的碎片數(shù)據(jù)連接成更大的報文來最小化所發(fā)送的報文的數(shù)量姑尺,如果需要發(fā)送一些較小的報文,則需要禁用該算法蝠猬。Netty默認(rèn)禁用該算法切蟋,從而最小化報文傳輸延時。(在游戲服務(wù)器中常用)
ChanneOption.SO_REUSEADDR
ChanneOption.SO_REUSEADDR對應(yīng)于套接字選項中的SO_REUSEADDR榆芦,這個參數(shù)表示允許重復(fù)使用本地地址和端口柄粹,比如,某個服務(wù)器進(jìn)程占用了TCP的80端口進(jìn)行監(jiān)聽匆绣,此時再次監(jiān)聽該端口就會返回錯誤驻右,使用該參數(shù)就可以解決問題,該參數(shù)允許共用該端口崎淳,這個在服務(wù)器程序中比較常使用堪夭,比如某個進(jìn)程非正常退出,該程序占用的端口可能要被占用一段時間才能允許其他進(jìn)程使用,而且程序死掉以后森爽,內(nèi)核一需要一定的時間才能夠釋放此端口恨豁,不設(shè)置SO_REUSEADDR就無法正常使用該端口。(在游戲服務(wù)器中常用)
ChannelOption.SO_KEEPALIVE
Socket參數(shù)拗秘,連接笔バ酰活,默認(rèn)值為False雕旨。啟用該功能時扮匠,TCP會主動探測空閑連接的有效性》采可以將此功能視為TCP的心跳機(jī)制棒搜,需要注意的是:默認(rèn)的心跳間隔是7200s即2小時。Netty默認(rèn)關(guān)閉該功能活箕。
ChannelOption.SO_LINGER
Netty對底層Socket參數(shù)的簡單封裝力麸,關(guān)閉Socket的延遲時間,默認(rèn)值為-1育韩,表示禁用該功能克蚂。-1以及所有<0的數(shù)表示socket.close()方法立即返回,但OS底層會將發(fā)送緩沖區(qū)全部發(fā)送到對端筋讨。0表示socket.close()方法立即返回埃叭,OS放棄發(fā)送緩沖區(qū)的數(shù)據(jù)直接向?qū)Χ税l(fā)送RST包,對端收到復(fù)位錯誤悉罕。非0整數(shù)值表示調(diào)用socket.close()方法的線程被阻塞直到延遲時間到或發(fā)送緩沖區(qū)中的數(shù)據(jù)發(fā)送完畢赤屋,若超時,則對端會收到復(fù)位錯誤壁袄。
(摘自《自頂向下深入分析Netty(六)--Channel總述》及《Netty ChannelOption參數(shù)詳解》)