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)

可查看第三点

十一、编解码一般是越多还是越少好

看需求,比如你不需要根据信息里面的内容做判断(比如),你就直接中转扔出去,但是像快递那个例子,你要看下编号什么的,那就做一层解码,然后扔出去,所以说做几层看业务需求,大多都是二层。

猜你喜欢

转载自blog.csdn.net/qq_41257365/article/details/105013829
今日推荐