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();
}
- @sharable,可以标识该handler是一个共享的,如果是别人写的,你看它可标记成@sharable了。
- 如果自己写的,自己最清楚了,主要看可有线程安全和是否符合自己的需求,比如你统计整个系统的,一般都要做成共享的,如果你统计单一连接的,你肯定就是非共享的,不管用哪种方式去看,自己去实际分析代码最准确。其中就有你说的那个小方法,就是看那个类可有成员变量,如果一个没有,十有八九都可以共享。
八、分配ByteBuf:分配器直接用ByteBufAllocator.DEFAULT等,而不是采用ChannelHandlerContext.alloc()
对于新手,他可能是通过ByteBufAllocator.DEFAULT.buffer()创建buffer,这样在大多数情况下是没有问题的,但是问题就在于,分配ByteBuf时,不管是堆内内存、堆外内存、内存池和非内存池的实现,它们都是可以切换实现的,ChannelHandlerContext的alloc()是ServerBoot启动的时候可以指定的alloc(),要是用了其它的话,在以后它们切换了一种实现后,就会出现实现不一致。
九、未考虑ByteBuf的释放
ByteBuf可能来自堆外内存或者内存池,它就要考虑释放,但是我们有的时候可能没有释放。
但是类SimpleChannelInboundHandler,默认是自动帮我们释放的,在实例它时通过向它的构造器传一个true是开启自动释放,具体可查看它的源码。
十、乱用Ctx.channel.writeAndFlush(msg)
可查看第三点
十一、编解码一般是越多还是越少好
看需求,比如你不需要根据信息里面的内容做判断(比如),你就直接中转扔出去,但是像快递那个例子,你要看下编号什么的,那就做一层解码,然后扔出去,所以说做几层看业务需求,大多都是二层。