Netty学习(六)-LengthFieldBasedFrameDecoder解码器

原创 2017年04月01日 00:12:55

在TCP协议中我们知道当我们在接收消息时候,我们如何判断我们一次读取到的包就是整包消息呢,特别是对于使用了长连接和使用了非阻塞I/O的程序。上节我们也说了上层应用协议为了对消息进行区分一般采用4种方式。前面三种我们都说了,第四种是:通过在消息头定义长度字段来标识消息总长度。这个我们还没讲。当然Netty也提供了相应的解码器:LengthFieldBasedFrameDecoder。

大多数的协议(私有或者公有),协议头中会携带长度字段,用于标识消息体或者整包消息的长度,例如SMPP、HTTP协议等。由于基于长度解码需求 的通用性,Netty提供了LengthFieldBasedFrameDecoder,自动屏蔽TCP底层的拆包和粘 包问题,只需要传入正确的参数,即可轻松解决“读半包“问题。

我们先来看一下他的构造函数:

   public LengthFieldBasedFrameDecoder(ByteOrder byteOrder, 
                                    int maxFrameLength, 
                                    int lengthFieldOffset, 
                                    int lengthFieldLength, 
                                    int lengthAdjustment, 
                                    int initialBytesToStrip, 
                                    boolean failFast) {

   }
  • byteOrder:表示字节流表示的数据是大端还是小端,用于长度域的读取;

  • maxFrameLength:表示的是包的最大长度,超出包的最大长度netty将会做一些特殊处理;

  • lengthFieldOffset:指的是长度域的偏移量,表示跳过指定长度个字节之后的才是长度域;

  • lengthFieldLength:记录该帧数据长度的字段本身的长度;

  • lengthAdjustment:该字段加长度字段等于数据帧的长度,包体长度调整的大小,长度域的数值表示的长度加上这个修正值表示的就是带header的包;

  • initialBytesToStrip:从数据帧中跳过的字节数,表示获取完一个完整的数据包之后,忽略前面的指定的位数个字节,应用解码器拿到的就是不带长度域的数据包;

  • failFast:如果为true,则表示读取到长度域,TA的值的超过maxFrameLength,就抛出一个 TooLongFrameException,而为false表示只有当真正读取完长度域的值表示的字节之后,才会抛出 TooLongFrameException,默认情况下设置为true,建议不要修改,否则可能会造成内存溢出。

LengthFieldBasedFrameDecoder定义了一个长度的字段来表示消息的长度,因此能够处理可变长度的消息。将消息分为消息头和消息体,消息头固定位置增加一个表示长度的字段,通过长度字段来获取整包的信息。LengthFieldBasedFrameDecoder继承了ByteToMessageDecoder,即转换字节这样的工作是由ByteToMessageDecoder来完成,而LengthFieldBasedFrameDecoder只用安心完成他的解码工作就好。Netty在解耦和方面确实做的不错。

既然我们知道了LengthFieldBasedFrameDecoder处理的是带有消息头和消息体的消息类型,那么我们完全可以来定义一个我们自己的消息,我们来写一个消息类:

public class Message {

    //消息类型
    private byte type;

    //消息长度
    private int length;

    //消息体
    private String msgBody;

    public Message(byte type, int length, String msgBody) {
        this.type = type;
        this.length = length;
        this.msgBody = msgBody;
    }

    public byte getType() {
        return type;
    }

    public void setType(byte type) {
        this.type = type;
    }

    public int getLength() {
        return length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    public String getMsgBody() {
        return msgBody;
    }

    public void setMsgBody(String msgBody) {
        this.msgBody = msgBody;
    }
}

我们先来写服务端:

public class NewServer {
    private static final int MAX_FRAME_LENGTH = 1024 * 1024;
    private static final int LENGTH_FIELD_LENGTH = 4;
    private static final int LENGTH_FIELD_OFFSET = 1;
    private static final int LENGTH_ADJUSTMENT = 0;
    private static final int INITIAL_BYTES_TO_STRIP = 0;

    private int port;

    public NewServer(int port) {
        this.port = port;
    }

    public void start(){
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap sbs = new ServerBootstrap()
                    .group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .localAddress(new InetSocketAddress(port))
                    .childHandler(new NewServerChannelInitializer(MAX_FRAME_LENGTH,LENGTH_FIELD_LENGTH,LENGTH_FIELD_OFFSET,LENGTH_ADJUSTMENT,INITIAL_BYTES_TO_STRIP))
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture future = sbs.bind(port).sync();

            System.out.println("Server start listen at " + port );
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) {
        NewServer server = new NewServer(7788);
        server.start();
    }

}

注意到服务端我们在上面定义了5个参数,这5个参数是为了传入LengthFieldBasedFrameDecoder里面用的,因为我们的LengthFieldBasedFrameDecoder写在了NewServerChannelInitializer类里面,所以这几个参数采用可配置的方式也更符合可扩展性,我们分别说一下这几个参数定值的含义:

  • MAX_FRAME_LENGTH = 1024 * 1024 :这个没什么说的,消息体的最大长度;
  • LENGTH_FIELD_LENGTH = 4 :指的就是我们的Message类中的length的长度,int占4位
  • LENGTH_FIELD_OFFSET = 1 :偏移多少位之后才是我们的消息体,因为我们消息头只有type一个参数,byte类型占1位,所以是1;
  • LENGTH_ADJUSTMENT = 0 :该字段加长度字段等于数据帧的长度,一般数据帧长度都是这样定义(即我们在设置Message中的length属性),加入你的消息体是20位,再加上
  • LENGTH_FIELD_LENGTH就是24位,所以在此处为了正确的解析出消息体,需要偏移4位才能解析出消息体的正确位置,我们在发送的消息里面设置的就是消息体本身的长度,所以无需偏移。
  • INITIAL_BYTES_TO_STRIP = 0 :这里我们也不需要跳过数据帧中的字节数,因为我们的消息体和长度是分别发送的,详情见下面EnCoder代码。

然后我们写ChannelInitializer:

public class NewServerChannelInitializer extends ChannelInitializer<SocketChannel> {
    private  final int MAX_FRAME_LENGTH;
    private  final int LENGTH_FIELD_LENGTH;
    private  final int LENGTH_FIELD_OFFSET;
    private  final int LENGTH_ADJUSTMENT;
    private  final int INITIAL_BYTES_TO_STRIP;

    public NewServerChannelInitializer(int MAX_FRAME_LENGTH, int LENGTH_FIELD_LENGTH, int LENGTH_FIELD_OFFSET, int LENGTH_ADJUSTMENT, int INITIAL_BYTES_TO_STRIP) {
        this.MAX_FRAME_LENGTH = MAX_FRAME_LENGTH;
        this.LENGTH_FIELD_LENGTH = LENGTH_FIELD_LENGTH;
        this.LENGTH_FIELD_OFFSET = LENGTH_FIELD_OFFSET;
        this.LENGTH_ADJUSTMENT = LENGTH_ADJUSTMENT;
        this.INITIAL_BYTES_TO_STRIP = INITIAL_BYTES_TO_STRIP;
    }

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();

        pipeline.addLast(new NewDecoder(MAX_FRAME_LENGTH,LENGTH_FIELD_LENGTH,LENGTH_FIELD_OFFSET,LENGTH_ADJUSTMENT,INITIAL_BYTES_TO_STRIP,false));
        // 自己的逻辑Handler
        pipeline.addLast("handler", new NewServerHandler());
    }

}

上面用到了我们自己写的Decoder,接下来定义一个Decoder,继承LengthFieldBasedFrameDecoder,以方便我们做一些改写:

public class NewDecoder extends LengthFieldBasedFrameDecoder {

    /**
     * 我们在Message类中定义了type和length,这都放在消息头部
     * type占1个字节,length占4个字节所以头部总长度是5个字节
     */
    private static final int HEADER_SIZE = 5;
    private byte type;
    private int length;
    private String msgBody;


    /**
     *
     * @param maxFrameLength   网络字节序,默认为大端字节序
     * @param lengthFieldOffset 消息中长度字段偏移的字节数
     * @param lengthFieldLength 数据帧的最大长度
     * @param lengthAdjustment 该字段加长度字段等于数据帧的长度
     * @param initialBytesToStrip 从数据帧中跳过的字节数
     * @param failFast 如果为true,则表示读取到长度域,TA的值的超过maxFrameLength,就抛出一个 TooLongFrameException
     */
    public NewDecoder(int maxFrameLength, int lengthFieldOffset,
                      int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip,
                      boolean failFast) {
        super(maxFrameLength, lengthFieldOffset, lengthFieldLength,
                lengthAdjustment, initialBytesToStrip, failFast);
    }

    @Override
    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        if(in == null){
            return null;
        }
        if(in.readableBytes() < HEADER_SIZE){
            throw new Exception("错误的消息");
        }

        /**
         * 通过源码我们能看到在读的过程中
         * 每读一次读过的字节即被抛弃
         * 即指针会往前跳
         */
        type = in.readByte();

        length = in.readByte();


        if(in.readableBytes() < length){
            throw new Exception("消息不正确");
        }

        ByteBuf buf = in.readBytes(length);
        byte[] b = new byte[buf.readableBytes()];
        buf.readBytes(b);

        msgBody = new String(b,"UTF-8");
        Message msg = new Message(type,length,msgBody);
        return msg;
    }
}

在上面的NewDecoder中有一个HEADER_SIZE-消息头。上面也解释过了,我们在Message中定义的type和length分别占一个字节和4个字节(别问我为啥是4个哈)。所以我们的消息头就是5个字节啦。

接下来就是服务端的handler了:

public class NewServerHandler extends SimpleChannelInboundHandler<Object> {

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {
        if(o instanceof Message) {
            Message msg = (Message)o;
            System.out.println("Client->Server:"+channelHandlerContext.channel().remoteAddress()+" send "+msg.getMsgBody());
        }
    }
}

在handler中我们用来接收已经被NewDecoder解码过后的客户端发送过来的消息。

下面是客户端:

public class NewClient {

    private  int port;
    private  String address;

    public NewClient(int port,String address) {
        this.port = port;
        this.address = address;
    }

    public void start(){
        EventLoopGroup group = new NioEventLoopGroup();

        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(new NewClientChannelInitializer());

        try {
            ChannelFuture future = bootstrap.connect(address,port).sync();
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) {
        NewClient client = new NewClient(7788,"127.0.0.1");
        client.start();
    }
}

客户端Initializer:

public class NewClientChannelInitializer extends ChannelInitializer<SocketChannel> {

    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();

        pipeline.addLast(new NewEncoder());
        pipeline.addLast(new NewClientHandler());
    }
}

客户端中我们又定义了一个编码器NewEncoder,继承了MessageToByteEncoder,该类用于将文本信息转换为流:

public class NewEncoder extends MessageToByteEncoder<Message> {

    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Message message, ByteBuf byteBuf) throws Exception {
        if(message == null){
            throw new Exception("未获得消息内容");
        }


        String msgBody = message.getMsgBody();
        byte[] b = msgBody.getBytes(Charset.forName("utf-8"));
        byteBuf.writeByte(message.getType());
        byteBuf.writeByte(b.length);
        byteBuf.writeBytes(b);


    }
}

接下来是我们的客户端handler:

public class NewClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        String m = "你好啊,Netty。昂昂";
        Message msg = new Message((byte)0xCA, m.length(), m);
        ctx.writeAndFlush(msg);
    }
}

注意到在handler中我们发送了一个Message对象。然后会由NewEncoder编码发送出去,服务端对消息解码获得消息头和消息体。分别启动服务端和客户端,打印结果为:

我们的消息就发送出去了。

版权声明:本文为博主原创文章,未经博主允许不得转载。

一起学Netty(九)之LengthFieldBasedFrameDecoder

之前介绍了Netty天然的几种解析器,也稍微介绍了一下ByteToMessageDecoder类,我们对Netty的解码器还是有了一定的了解~ 今天要介绍的是Netty中一个很重要的解码器,因为相...
  • linuu
  • linuu
  • 2016年05月11日 19:43
  • 20899

[netty]--最通用TCP黏包解决方案:LengthFieldBasedFrameDecoder和LengthFieldPrepender

前面已经说过: TCP以流的方式进行数据传输,上层应用协议为了对消息进行区分,往往采用如下4种方式。 (1)消息长度固定:累计读取到固定长度为LENGTH之后就认为读取到了一个完整的消息...
  • u010853261
  • u010853261
  • 2017年02月19日 15:02
  • 4874

Netty学习(六)-LengthFieldBasedFrameDecoder解码器

在TCP协议中我们知道当我们在接收消息时候,我们如何判断我们一次读取到的包就是整包消息呢,特别是对于使用了长连接和使用了非阻塞I/O的程序。上节我们也说了上层应用协议为了对消息进行区分一般采用4种方式...
  • a953713428
  • a953713428
  • 2017年04月01日 00:12
  • 3223

Netty4.0中LengthFieldBasedFrameDecoder的使用心得

本文主要是针对遇到二进制数据包的粘包与分包问题的学习心得。 Netty作为服务端中LengthFieldBasedFrameDecoder的使用,其中使用1)Netty自带客户端;2)java.net...
  • xxxxmm007
  • xxxxmm007
  • 2016年02月15日 13:37
  • 6339

netty中LengthFieldBasedFrameDecoder的使用

在org.jboss.netty.handler.codec.frame包中,有LengthFieldBasedFrameDecoder类用来解析带有长度属性的包,只要我们在传输协议中加入包的总长度就...
  • zshake
  • zshake
  • 2015年12月23日 15:33
  • 1624

Netty LengthFieldBasedFrameDecoder

先看看LengthFieldBasedFrameDecoder的官方API  http://docs.jboss.org/netty/3.1/api/org/jboss/netty/handler/...
  • joeyon
  • joeyon
  • 2016年12月01日 17:53
  • 651

Netty LengthFieldBasedFrameDecoder

先看看LengthFieldBasedFrameDecoder的官方API  http://docs.jboss.org/netty/3.1/api/org/jboss/netty/handler/c...
  • educast
  • educast
  • 2015年08月16日 21:18
  • 9774

Netty权威指南 第2版学习笔记7——MessagePack编解码及LengthFieldBasedFrameDecoder

MessagePack是一个高效的二进制序列化框架,像JSON一样支持不同语言间的数据交换,速度更快,序列化之后的码流更小MessagePack特点 编解码高效,性能高 序列化后的码流小 支持跨语言 ...
  • xundh
  • xundh
  • 2017年01月02日 21:55
  • 2239

netty 过长内容分成了多次发送 问题 LengthFieldBasedFrameDecoder使用

这个问题比较常见,在高并发大数据传输时数据分包接收会乱 在org.jboss.netty.handler.codec.frame包中,有LengthFieldBasedFrameDecoder类用来...
  • zzycgfans
  • zzycgfans
  • 2011年08月30日 16:29
  • 15214

netty5 LengthFieldBasedFrameDecoder实例(解决半包)

1,使用LengthFieldPrepender编码,LengthFieldBasedFrameDecoder解码的netty传输 可以解决半包粘包 2 代码部分 tcpserver package...
  • AlbertFly
  • AlbertFly
  • 2016年07月28日 14:08
  • 2602
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Netty学习(六)-LengthFieldBasedFrameDecoder解码器
举报原因:
原因补充:

(最多只允许输入30个字)