Netty使用常見錯誤

Netty使用常見錯誤

一、多個handler的執行順序

通過***V字形看*** :下面的是響應的接收過程,即OrderFrameDecoder()->OrderProtocolDecoder()->OrderServerProcessHandler()
在這裏插入圖片描述
響應的發送即:OrderServerProcessHandler()->OrderProtocolEncoder()->OrderFrameEncoder().
下面是完整的V字形
在這裏插入圖片描述

二、添加了多個處理邏輯的handler,爲什麼後面的handler執行不到

下面是幾個handler的添加

		ch.pipeline().addLast("DelimiterBasedFrameDecoder",new DelimiterBasedFrameDecoder(1024, delimiter));
        ch.pipeline().addLast("StringDecoder",new StringDecoder(CharsetUtil.UTF_8));
        ch.pipeline().addLast("StringEncoder",new StringEncoder(CharsetUtil.UTF_8));
        //下面的是我自己的處理邏輯handler
        ch.pipeline().addLast("Auth",new AuthHandler());
        ch.pipeline().addLast("CheckH",new CheckHandler(false));
        ch.pipeline().addLast("ConnectH",new ConnectHandler(new AtomicInteger(2),true));

但是服務器運行後,客戶端發送數據上來,只有第一個AuthHandler()能夠接收處理數據,後面的兩個卻收不到數據,不是代碼邏輯問題。原因是因爲,當我在AuthHandler()中繼承了 SimpleChannelInboundHandler這個類,或者其它ChannelInboundHandlerAdapter。handler添加完畢之後,其實是一個雙向鏈表,這些handle會有一個前驅和後繼。但是當觸發第一個handler的事件之後,並不是自動觸發第二個handler的相同事件,而是需要手動指定事件。比如下面的代碼,觸發第一個handler的read事件之後,再觸發下一個handler的active事件

	@Override
	public void channelRead(ChannelHandlerContext ctx, Object o) throws Exception {
		System.out.println("執行了讀數據");
		//手動觸發
		ctx.fireChannelRead(o);
	}

其中context中,方法以fire開頭的都是inbound事件,也就是輸入事件,它其實就是去找到下一個handle,並調用下一個的channelRead()。在最後一個handle中就沒必要添加這個ctx.fireChannelRead()了

三、Ctx.write()、Ctx.writeAndFlush()與Ctx.channel.writeAndFlush()

Ctx.write():僅僅是將我們的信息加入到隊列裏面,並沒有發生出去。是在當前handler尋找下一個handler,並不是將這個pipline重新走了一遍
Ctx.writeAndFlush():是向前找到第一個遇到的OutHandler,再發送出去,也並不是將這個pipline重新走了一遍。
Ctx.channel.writeAndFlush():這個是將這個pipline重新走了一遍,可能會引起死循環,假如這個handler是中間的handler,他會將這個pipline重新走了一遍,等走到原點,又會有機會執行Ctx.channel.writeAndFlush(),這個大部分是用在客戶端。

就像官方文檔中對 pipeline 的描述那樣, ctx.write() 是從當前的 handler 中, 寫到離它最近的 out handler 中, 而不是從流水線最後開始從頭穿過處理~

四、Ctx.close() 與 Ctx.channel.close()

讓我們假設在 pipeline 裏有三個 handlers , 它們都都攔截 close() 方法操作, 並且在面裏調用 ctx.close()

假如我們添加三個handler
ch.pipeline().addLast("A", new MyHandler());
ch.pipeline().addLast("B", new MyHandler());
ch.pipeline().addLast("C", new MyHandler());

public class MyHandler extends ChannelOutboundHandlerAdapter {
    @Override
    public void close(ChannelHandlerContext ctx, ChannelPromise promise) {
        ctx.close(promise);
    }
}
  • Channel.close() 會觸發 C.close() , B.close(), A.clos(), 然後再關閉 channel
  • ChannelPipeline.context(“C”).close() 會觸發 B.close(), A.close(), 然後再關閉 channel
  • ChannelPipeline.context(“B”).close() 會觸發 A.close(), 然後再關閉 channel
  • ChannelPipeline.context(“A”).close() 則會直接關閉 channel. 不再會有 handlers 調用了.

所以:
如果你正寫一個 ChannelHandler, 並且想在這個 handler 中關閉 channel, 則調用
ctx.close() 如果你正準備從一個外部的 handler (例如, 你有一個後臺的非I/O線程, 並且你想從該線程中關閉連接). (譯註: 這時是調用 Channel.close() ?)

就像官方文檔中對 pipeline 的描述那樣, ctx.write() 是從當前的 handler 中, 寫到離它最近的 out handler 中, 而不是從流水線最後開始從頭穿過處理一樣~

五、handler中的方法的含義

/**
     * 覆蓋了 channelRead0() 事件處理方法。
     * 每當從服務端讀到客戶端寫入信息時,
     * 其中如果你使用的是 Netty 5.x 版本時,
     * 需要把 channelRead0() 重命名爲messageReceived()
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        Channel incoming = ctx.channel();
        System.out.println("Client:"+incoming.remoteAddress()+" ::"+msg+"; the counter is :"+ ++counter+">>>"+HeartBeatServer.dataTime());
        //主動拋出異常測試,它會進入exceptionCaught()中
        throw new RuntimeException();
    }

/**
     * exceptionCaught() 事件處理方法是當出現 Throwable 對象纔會被調用,
     * 即當 Netty 由於 IO 錯誤或者處理器在處理事件時拋出的異常時。
     * 在大部分情況下,捕獲的異常應該被記錄下來並且把關聯的 channel 給關閉掉。
     * 然而這個方法的處理方式會在遇到不同異常的情況下有不同的實現,
     * 比如你可能想在關閉連接之前發送一個錯誤碼的響應消息。
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx , Throwable cause){
        System.out.println(ctx.channel().id()+"出現異常關閉連接"+">>>"+HeartBeatServer.dataTime());
        //應該發送響應碼給客戶端
        ctx.writeAndFlush(Unpooled.copiedBuffer("500".getBytes()));
        ctx.close();
    }
    /**
     * 覆蓋channelActive 方法在channel被啓用的時候觸發(在建立連接的時候)
     * 覆蓋了 channelActive() 事件處理方法。服務端監聽到客戶端活動
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
    }

    /**
     * (non-Javadoc)
     * .覆蓋了 handlerRemoved() 事件處理方法。
     * 每當從服務端收到客戶端斷開時
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        super.handlerRemoved(ctx);
    }
    /**
     * (non-Javadoc)
     * 覆蓋了 handlerAdded() 事件處理方法。
     * 每當從服務端收到新的客戶端連接時
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        super.handlerAdded(ctx);
    }

六、LengthFieldBasedFrameDecoder中initialBytesToStrip未考慮設置問題

public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip) {
        this(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip, true);
    }

這裏的initialBytesToStrip要是不設置,默認是0
就像文檔中所說:
在這裏插入圖片描述
要是不設置,它會把length字段的數據和內容數據一起當作內容數據,這在json解析中是絕對不行的。

七、ChannelHandler該共享不共享,不該共享卻共享問題

假如把一個不該共享的共享了,在多併發時會出現很嚴重的問題。
該共享的沒有共享:如

//比如這個,在每一個SocketChannel,都有一個pipeline,要是這個日誌LoggingHandler在每一個pipeline中都弄一個是很浪費資源的
	ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));

可以通過在ServerBootstrap上面添加這個handler

try {
            ServerBootstrap b = new ServerBootstrap();
            //可以這樣,
            b.handler(new LoggingHandler(LogLevel.DEBUG));
            
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    // 設置tcp緩衝區
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    // 設置發送緩衝大小
                    .option(ChannelOption.SO_SNDBUF, 32 * 1024)
                    // 這是接收緩衝大小
                    .option(ChannelOption.SO_RCVBUF, 32 * 1024)
                    // 保持連接
                    .option(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new HeartBeatServerChannelHandler());

            /**綁定端口並且添加監聽和異步啓動**/
            ChannelFuture f = b.bind(port).sync();
            f.channel().closeFuture().sync();
        } catch (Exception e) {
            System.out.println(e.getMessage());

        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
  1. @sharable,可以標識該handler是一個共享的,如果是別人寫的,你看它可標記成@sharable了。
  2. 如果自己寫的,自己最清楚了,主要看可有線程安全和是否符合自己的需求,比如你統計整個系統的,一般都要做成共享的,如果你統計單一連接的,你肯定就是非共享的,不管用哪種方式去看,自己去實際分析代碼最準確。其中就有你說的那個小方法,就是看那個類可有成員變量,如果一個沒有,十有八九都可以共享。

八、分配ByteBuf:分配器直接用ByteBufAllocator.DEFAULT等,而不是採用ChannelHandlerContext.alloc()

對於新手,他可能是通過ByteBufAllocator.DEFAULT.buffer()創建buffer,這樣在大多數情況下是沒有問題的,但是問題就在於,分配ByteBuf時,不管是堆內內存、堆外內存、內存池和非內存池的實現,它們都是可以切換實現的,ChannelHandlerContext的alloc()是ServerBoot啓動的時候可以指定的alloc(),要是用了其它的話,在以後它們切換了一種實現後,就會出現實現不一致。

九、未考慮ByteBuf的釋放

ByteBuf可能來自堆外內存或者內存池,它就要考慮釋放,但是我們有的時候可能沒有釋放。
但是類SimpleChannelInboundHandler,默認是自動幫我們釋放的,在實例它時通過向它的構造器傳一個true是開啓自動釋放,具體可查看它的源碼。

十、亂用Ctx.channel.writeAndFlush(msg)

可查看第三點

十一、編解碼一般是越多還是越少好

看需求,比如你不需要根據信息裏面的內容做判斷(比如),你就直接中轉扔出去,但是像快遞那個例子,你要看下編號什麼的,那就做一層解碼,然後扔出去,所以說做幾層看業務需求,大多都是二層。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章