【Netty】Netty重點一鍋端

首先來張網(wǎng)上盛傳的netty框架參考圖,以供讀者把握Netty的整體框架及核心組件,繼而發(fā)散出Netty的重點知識講解:

netty框架參考圖.jpg

1.Netty Reactor模型
Reactor模型是對傳統(tǒng)阻塞IO模型的巨大改進,實現(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負責(zé)接收請求,一個Reactor Thread Pool負責(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負責(zé)接收請求,一個Main Reactor Thread Pool負責(zé)連接,一個Sub Reactor Thread Pool負責(zé)處理I/O操作。
代碼實現(xiàn)大致為:

EventLoopGroup bossGroup = new NioEventLoopGroup(4);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
Reactor模型.png

當(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其實是一個維護ChannelHandlerContext的雙向鏈表,ChannelPipeline創(chuàng)建時,會默認增加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實際維護的是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里維護多個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,并擴展增加了自己的一些方法,使得獲得異步操作結(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。

future狀態(tài)含義.png

4.ByteBuffer、ByteBuf、UnpooledByteBuf、PooledByteBuf關(guān)系
ByteBuffer為Java NIO的數(shù)據(jù)容器,它長度固定,一旦分配完成后,它的容量不能動態(tài)擴展和收縮;它只有一個標(biāo)識位控的指針position,讀寫的時候需要手工調(diào)用flip()和rewind()等,使用者必須小心謹慎地處理這些API,否則很容易導(dǎo)致程序處理失敗。
ByteBuf為Netty的數(shù)據(jù)容器,ByteBuf支持動態(tài)擴容,且通過兩個位置指針來協(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堆外進行內(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對象,它自己維護了一個內(nèi)存池,可以循環(huán)利用創(chuàng)建的ByteBuf,提升內(nèi)存的使用效率,降低由于高負載導(dǎo)致的頻繁GC。測試表明使用內(nèi)存池后的Netty在高負載、大并發(fā)的沖擊下內(nèi)存和GC更加平穩(wěn)。盡管推薦使用基于內(nèi)存池的ByteBuf,但是內(nèi)存池的管理和維護更加復(fù)雜,使用起來也需要更加謹慎,因此,Netty提供了靈活的策略供使用者來做選擇。
通過一個Channel或ChannelHandlerContext的alloc()方法可以獲得一個ByteBuf分配的工具,即ByteBufAllocator,該工具默認使用池化的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)存進行Socket讀寫,不需要進行字節(jié)緩沖區(qū)的二次拷貝。如果使用傳統(tǒng)的堆內(nèi)存(heap buffers)進行Socket讀寫,JVM會將堆內(nèi)存Buffer拷貝一份到直接內(nèi)存中,然后才寫入Socket中。相比于堆外直接內(nèi)存,消息在發(fā)送過程中多了一次緩沖區(qū)的內(nèi)存拷貝。
2).Composite Buffers
Netty提供了復(fù)合Buffer對象,可以聚合多個ByteBuf對象,用戶可以像操作一個Buffer那樣方便的對復(fù)合Buffer進行操作,避免了傳統(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īng)過編解碼轉(zhuǎn)換成字節(jié)流。該入站處理器會負責(zé)字節(jié)流的累加工作,但是具體如何進行解碼,則交由不同的子類(用戶自定義的處理器)去實現(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ù),默認值為Ture(Netty默認為True而操作系統(tǒng)默認為False)。該值設(shè)置Nagle算法的啟用,改算法將小的碎片數(shù)據(jù)連接成更大的報文來最小化所發(fā)送的報文的數(shù)量,如果需要發(fā)送一些較小的報文,則需要禁用該算法。Netty默認禁用該算法,從而最小化報文傳輸延時。(在游戲服務(wù)器中常用)

ChanneOption.SO_REUSEADDR
ChanneOption.SO_REUSEADDR對應(yīng)于套接字選項中的SO_REUSEADDR,這個參數(shù)表示允許重復(fù)使用本地地址和端口,比如,某個服務(wù)器進程占用了TCP的80端口進行監(jiān)聽,此時再次監(jiān)聽該端口就會返回錯誤,使用該參數(shù)就可以解決問題,該參數(shù)允許共用該端口,這個在服務(wù)器程序中比較常使用,比如某個進程非正常退出,該程序占用的端口可能要被占用一段時間才能允許其他進程使用,而且程序死掉以后,內(nèi)核一需要一定的時間才能夠釋放此端口,不設(shè)置SO_REUSEADDR就無法正常使用該端口。(在游戲服務(wù)器中常用)

ChannelOption.SO_KEEPALIVE
Socket參數(shù),連接?;?,默認值為False。啟用該功能時,TCP會主動探測空閑連接的有效性??梢詫⒋斯δ芤暈門CP的心跳機制,需要注意的是:默認的心跳間隔是7200s即2小時。Netty默認關(guān)閉該功能。

ChannelOption.SO_LINGER
Netty對底層Socket參數(shù)的簡單封裝,關(guān)閉Socket的延遲時間,默認值為-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ù)詳解》)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容