从NIO到netty(15) netty的编解码处理器

56 篇文章 2 订阅
Netty处理器重要概念:
* 1.Netty处理器可以分为两类,入站处理器和出站处理器,
* 2.入站处理器的顶层是ChannelInboundHandler,出站处理器的顶层是ChannelOutboundHandler
* 3.数据处理时常用的各种解码器本质上都是处理器
* 4.编码:本质是一种出站处理器,因此编码一定是一种ChannelOutboundHandler
* 5.解码:本质是一种入站处理器,因此解码一定是一种ChannelInboundHandler
* 6.在Netty中,编码器通常以XXXEncoder命名,解码器通常以XXXDecoder命名
* 7.前一个处理器的出来的数据要和后一个处理器入参相匹配,否则就会有问题
* 8.TCP粘包和拆包会对消息的处理产生问题 Netty提供了Frame Dectection系列的一些双处理器

解码器通常要继承ByteToMessageDecoder

而编码器通常要继承MessageToByteEncoder

下面自己写一个编码器和一个解码器

public class MyByteToLongDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        System.out.println("decode invoked");
        System.out.println(in.readableBytes());
        if(in.readableBytes()>=8){
                out.add(in.readLong());
        }
    }
}
public class MyByteToLongDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        System.out.println("decode invoked");
        System.out.println(in.readableBytes());
        if(in.readableBytes()>=8){ 这里要特别注意,由于TCP粘包拆包的问题,这里一定要加这个判断,否则会接收不到数据,关于粘包拆包问题,我会在下一篇中说明。
                out.add(in.readLong());
        }
    }
}
再贴上一段客户端的自定义handler

public class MyClientHandler extends SimpleChannelInboundHandler<Long> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Long msg) throws Exception {
        System.out.println(ctx.channel().remoteAddress());
        System.out.println("client output "+msg);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(123456L);//一定要加L,否则会作为int类型处理,最终导致消息发送不出去。】、

如果改成ctx.writeAndFlush(Unpooled.copiedBuffer("helloworld", Charset.forName("utf-8")));

那么服务端的console上接收到的是一串Long型的数字,而不是字符串

         }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
        ctx.close();
    }
}
其他组件详细贴上了。

关于netty编解码器的重要结论: 
1、无论是编码器还是解码器,其接受的消息类型必须要与待处理的参数类型一致,否则该编码器或解码器并不会执行。 
2、在解码器进行数据解码时,一定要记得判断缓冲(ByteBuf)中的数据是否 足够,否则将会产生一些问题。 
例如上边的例子判断是否是8个长度(因为long是占用8个字节的数据类型):
-------------------------------------------------------

在netty中有一个非常重要的decoder    ReplayingDecoder

如果一个解码器继承了ReplayingDecoder,就没有  if(in.readableBytes()>=8)这样的判断了,如下:

public class MyReplayingDecoder extends ReplayingDecoder<Void> {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        System.out.println("MyReplayingDecoder decode invoked!");
        out.add(in.readLong());//注意没有判断字节数!!!!
    }
}
这样写是正确的,因为ReplayingDecoder帮我们做了好多事情。

ReplayingDecoder是

一个特殊的ByteToMessageDecoder ,可以在阻塞的i/o模式下实现非阻塞的解码。 
ReplayingDecoder 和ByteToMessageDecoder 最大的不同就是ReplayingDecoder 允许你实现decode()和decodeLast()就像所有的字节已经接收到一样,不需要判断可用的字节,举例,下面的ByteToMessageDecoder 实现:

public class IntegerHeaderFrameDecoder extends ByteToMessageDecoder {

    @Override
   protected void decode(ChannelHandlerContext ctx,
                           ByteBuf buf, List<Object> out) throws Exception {

     if (buf.readableBytes() < 4) {
        return;
     }

     buf.markReaderIndex();
     int length = buf.readInt();

     if (buf.readableBytes() < length) {
        buf.resetReaderIndex();
        return;
     }

     out.add(buf.readBytes(length));
   }
 }
这段代码表达的意思是先读取前边4隔壁字节(即,一个int类型),然后再去读取后边个字节,如果前边四个字节无法读取,直接返回,如果可以读取前四个字节,那么紧接着读取同样长度个字节,这种方式是一种消息头,消息体的协议模型,先读取消息头,之后再去读取消息体:

è¿éåå¾çæè¿°

如果用 ReplayingDecoder来简化:

public class IntegerHeaderFrameDecoder
      extends ReplayingDecoder<Void> {

   protected void decode(ChannelHandlerContext ctx,
                           ByteBuf buf) throws Exception {

     out.add(buf.readBytes(buf.readInt()));
   }
 }
具体原理:

当ReplayingDecoder 接收的buffer的数据不足时,会抛出一个异常,ReplayingDecoder 通过一个ByteBuf 的具体实现来完成。在上边的IntegerHeaderFrameDecoder ,当你调用buf.readInt().你就假设在buffer里边有4个字节,如果里边确实有4个字节,它将会返回一个整型的头,就像你期望的一样,否则,将会抛出一个Error并且控制会返回到ReplayingDecoder,如果ReplayingDecoder扑捉到这个错误,然后他就会将读的索引重置到刚开始的位置(buffer的开始位置),之后再次调用decode方法当数据继续进入buffer的时候。

请注意ReplayingDecoder 总是返回一个缓冲的Error 的实例,,来避免创建新的Error对象和每次填充堆栈的负担。

简化使用带来的成本,ReplayingDecoder 强制带来了2个限制:

一些buffer 的操作是被禁止的
如果网络过慢并且消息的格式复杂不像上边提到的那种简单的例子,会导致性能下降。在这种情况下,你的解码器就会一遍又一遍解码一个消息的同一个部分。
你必须要记住decode方法为了解码一个消息可能别调用多次,下面的情况是不能工作的。

public class MyDecoder extends ReplayingDecoder<Void> {

   private final Queue<Integer> values = new LinkedList<Integer>();

    @Override
   public void decode(.., ByteBuf buf, List<Object> out) throws Exception {

     // A message contains 2 integers.
     values.offer(buf.readInt());
     values.offer(buf.readInt());

     // This assertion will fail intermittently since values.offer()
     // can be called more than two times!
     assert values.size() == 2;
     out.add(values.poll() + values.poll());
   }
 }
 

上边的方式当中,假如走到第二个values.offer(buf.readInt());时候抛出了异常,下边的逻辑就不会走,但是values 里边已经有一个消息了,当下次再调用decode方法的时候(加入过来了2条数据),那么assert values.size() == 2;永远都不会通过,因为加上之前的一条现在values里边是三条数据。

The correct implementation looks like the following, and you can also utilize the ‘checkpoint’ feature which is explained in detail in the next section. 
正确的实现应该是下边这样的方式

 public class MyDecoder extends ReplayingDecoder<Void> {

   private final Queue<Integer> values = new LinkedList<Integer>();

    @Override
   public void decode(.., ByteBuf buf, List<Object> out) throws Exception {

     // Revert the state of the variable that might have been changed
     // since the last partial decode.
     values.clear();//首先要清理掉里边的消息

     // A message contains 2 integers.
     values.offer(buf.readInt());
     values.offer(buf.readInt());

     // Now we know this assertion will never fail.
     assert values.size() == 2;
     out.add(values.poll() + values.poll());
   }
 }
幸好,复杂解码器性能的提升可以通过checkpoint()方法实现,checkpoint()方法可以更新buffer的初始化的位置,这样ReplayingDecoder 就可以在调用checkpoint()方法的时候重新回到上一次读索引的位置。

通过枚举调用索引,尽管你可以自己使用checkpoint()方法来管理decoder的状态,最易用的方式就是使用枚举来管理decoder的状态,这个枚举代表了当前decoder的状态,当状态改变时可以调用checkpoint()方法,你可以有很多状态取决于你想解码的消息的复杂度。

实际上checkpoint就是一种存档机制,可以跳过不需要的部分进行重试。下面有一个例子

public enum MyDecoderState {
 //假设消息有2种状态,读取长度状态和读取内容的状态。
   READ_LENGTH,
   READ_CONTENT;
 }

 public class IntegerHeaderFrameDecoder
      extends ReplayingDecoder<MyDecoderState> {

   private int length;

   public IntegerHeaderFrameDecoder() {
     // Set the initial state.
     super(MyDecoderState.READ_LENGTH);
   }

    @Override
   protected void decode(ChannelHandlerContext ctx,
                           ByteBuf buf, List<Object> out) throws Exception {
     switch (state()) {
     case READ_LENGTH:
       length = buf.readInt();
       checkpoint(MyDecoderState.READ_CONTENT);
       //注意这里没有break,也就是说接下来就是进入READ_CONTENT的代码块,执行读取内容的逻辑
     case READ_CONTENT:
       ByteBuf frame = buf.readBytes(length);
       checkpoint(MyDecoderState.READ_LENGTH);
       out.add(frame);
       break;
     default:
       throw new Error("Shouldn't reach here.");
     }
   }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值