Netty笔记(五)之编解码器与粘包拆包

文章目录

  • netty版本
  • 解码
    • ByteToMessageDecoder
    • TCP粘包与拆包
    • 没有解码器的案例
    • 使用解码器的案例
    • ReplayingDecoder
  • 编码器
    • MessageToByteEncoder

netty版本

  1. netty版本:io.netty:netty-all:4.1.33.Final

解码

  1. 将字节解码为消息或者另一个字节序列是一项常见的任务,Netty提供了一个基类ByteToMessageDecoder,由于无法知道消息字节会发送多少次才能发送完毕,所以这个类会对入站数据进行缓冲,直到数据已经完整到达。

ByteToMessageDecoder

  1. 方法

    方法 描述
    decode(ChannelHandlerContext ctx, ByteBuf in, List out) 这是你必须实现的唯一抽象方法。decode()方法被调用时将会传入一个包含了传入数据的ByteBuf,以及一个用来添加解码消息的List。对这个方法的调用将会重复进行,直到确定没有新的元素被添加到该 List,或者该ByteBuf中没有更多可以读取的字节时为止。然后 ,如果该List不为空,那么它的内容将会被传递给ChannelPipeline中的下一个 ChannelInboundHandler
    decodeLast(ChannelHandlerContext ctx, ByteBuf in, List out) Netty提供的这个默认实现只是简单地调用了 decode()方法。 当Channel的状态变为非活动时,这个方法将会被调用一次。可以重写该方法以提供特殊的处理,比如在Channel关闭之后产生最后一个消息
  2. 对于编码解码器,一旦消息被编码或者解码,就会被ReferenceCountUtil.release(msg)调用自动释放。如果需要保留引用,可以调用ReferenceCountUtil.retain(msg)方法来增加引用计数,防止消息被释放

TCP粘包与拆包

  1. TCP是一个流协议,所谓流就是一个没有界限的一串数据,TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际数据进行包的划分,一个完整的数据包可能会被拆分成多个TCP包进行发送,也有可能把多个小的数据包封装成一个大的TCP包进行发送

  2. 为什么会粘包?

    • 在用户数据量非常小的情况下,极端情况下,一个字节,该TCP数据包的有效载荷非常低,传递100字节的数据,需要100次TCP传送,100次ACK,在应用及时性要求不高的情况下,将这100个有效数据拼接成一个数据包,那会缩短到一个TCP数据包,以及一个ACK,有效载荷提高了,带宽也节省了

    • 粘包现象

             写入数据
          +------+   +------+
          | MSG1 |   | MSG2 |
          +------+   +------+
          发送数据
          +------------+
          | MSG1  MSG2 |
          +------------+
          接收到的数据
          +----+     +---------+      +-------+    +-----+ 
          | MS |     |  G1MSG2 |  或者 | MSG1M |    | SG2 | 
          +----+     +---------+      +-------+    +-----+
      
  3. 解决方案:通过应用协议对消息进行区分

    • 消息长度固定,累计读取到的长度总和为定长LENGTH的报文后,就认为读取到了一个完整的消息,此时将计数器置位,重新开始读取下一个数据包
    • 在包尾增加一个特殊分隔符,通过这个标志进行分割,比如回车换行符
    • 通过在消息头中定义长度字段来标识消息的总长度
  4. netty针对以上的解决方案

    • FixedLengthFrameDecoder:定长解码器来解决定长消息的粘包问题
    • LineBasedFrameDecoder:解决以回车换行符作为消息结束符的TCP粘包的问题;
    • DelimiterBasedFrameDecoder:特殊分隔符解码器来解决以特殊符号作为消息结束符的TCP粘包问题;
    • LengthFieldBasedFrameDecoder自定义长度解码器解决TCP粘包问题。
  5. 拆包的原理

    • 如果当前读取的数据不足以拼接成一个完整的业务数据包,那就保留该数据,继续从tcp缓冲区中读取,直到得到一个完整的数据包
    • 如果当前读到的数据加上已经读取的数据足够拼接成一个数据包,那就将已经读取的数据拼接上本次读取的数据,够成一个完整的业务数据包传递到业务逻辑,多余的数据仍然保留,以便和下次读到的数据尝试拼接

没有解码器的案例

  1. 客户端Handler代码

        public class TimeClientHandler extends ChannelInboundHandlerAdapter {
        
            private static final String LINE = System.getProperty("line.separator");
        
            private static final String REQUEST_DATA = "this is message from client!";
        
            private static final Logger LOGGER = LoggerFactory.getLogger(TimeClientHandler.class);
        
            private volatile int counter;
            private byte[] req;
        
        
            public TimeClientHandler() {
                req = (REQUEST_DATA + LINE).getBytes();
            }
        
            /**
             * 向服务端连续发送100 条this is message from client!\n
             */
            @Override
            public void channelActive(ChannelHandlerContext ctx) {
                ByteBuf message = null;
                for (int i = 0; i < 100; i++) {
                    message = Unpooled.buffer(req.length);
                    message.writeBytes(req);
                    ctx.writeAndFlush(message);
                }
            }
        
        
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                ByteBuf buf = (ByteBuf) msg;
                byte[] req = new byte[buf.readableBytes()];
                buf.readBytes(req);
                String body = new String(req, "UTF-8");
                System.out.println("服务器发送给客户端的数据是:" + body + " ;总共次数是: " + (++counter));
            }
        
            @Override
            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
                LOGGER.warn(cause.getMessage(),cause.getCause());
                cause.printStackTrace();
                ctx.close();
            }
        }
        
    
  2. 客户端启动代码

        public class TimeClient {
            public void connect(int port, String host) {
                EventLoopGroup group = new NioEventLoopGroup();
                Bootstrap b = new Bootstrap();
                b.group(group).channel(NioSocketChannel.class)
                        .option(ChannelOption.TCP_NODELAY, true)
                        .handler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel ch) throws Exception {
                                ch.pipeline().addLast(new TimeClientHandler());
                            }
                        });
                try {
                    ChannelFuture f = b.connect(host, port).sync();
                    f.channel().closeFuture().sync();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    group.shutdownGracefully();
                }
            }
            public static void main(String[] args) {
                new TimeClient().connect(8080, "localhost");
            }
        }
    
    
  3. 服务端Handler代码

        public class TimeServerHandler extends ChannelInboundHandlerAdapter {
            private volatile int counter;
            private static final String LINE = System.getProperty("line.separator");
            private static final String REQUEST_DATA = "this is message from client!";
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                ByteBuf buf = (ByteBuf) msg;
                byte[] req = new byte[buf.readableBytes()];
                buf.readBytes(req);
        
                String body = new String(req, "UTF-8").substring(0, req.length - LINE.length());
        
                System.out.println("客户端发送给服务端的数据: " + body + " ; 总共的次数是: " + (++counter));
        
                String currentTime = REQUEST_DATA.equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "消息不完整";
                currentTime = currentTime + LINE;
        
                ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
                ctx.writeAndFlush(resp);
            }
        
            @Override
            public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
                ctx.flush();
            }
        
            @Override
            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
                ctx.close();
            }
        }
        
    
  4. 服务端启动代码

         public class TimeServer {
            public static void main(String[] args) {
                new TimeServer().bind(8080);
            }
        
            public void bind(int port) {
                EventLoopGroup boosGroup = new NioEventLoopGroup();
                EventLoopGroup workerGroup = new NioEventLoopGroup();
        
                try {
                    ServerBootstrap b = new ServerBootstrap();
                    b.group(boosGroup, workerGroup)
                            .channel(NioServerSocketChannel.class)
                            .option(ChannelOption.SO_BACKLOG, 1012)
                            .childHandler(new ChildChannelHandler());
        
                    ChannelFuture f = b.bind(port).sync();
                    f.channel().closeFuture().sync();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    boosGroup.shutdownGracefully();
                    workerGroup.shutdownGracefully();
                }
            }
        
            private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new TimeServerHandler());
                }
            }
        }
    
    
  5. 输出结果

    • Server端打印(部分截图)
      Netty笔记(五)之编解码器与粘包拆包_第1张图片
    • client端打印(全部截图),从客户端打印3条信息可以看出,服务端只收到3条客户端发送的信息。实际上我们是发了100条信息给服务端
      Netty笔记(五)之编解码器与粘包拆包_第2张图片

使用解码器的案例

  1. 客户端handler

        public class TimeClientHandler extends ChannelInboundHandlerAdapter {
        
            private static final String LINE = System.getProperty("line.separator");
        
            private static final String REQUEST_DATA = "this is message from client!";
        
            private static final Logger LOGGER = LoggerFactory.getLogger(TimeClientHandler.class);
        
            private volatile int counter;
            private byte[] req;
        
        
            public TimeClientHandler() {
                req = (REQUEST_DATA + LINE).getBytes();
            }
        
            /**
             * 向服务端连续发送100 条this is message from client!\n
             */
            @Override
            public void channelActive(ChannelHandlerContext ctx) {
                ByteBuf message = null;
                for (int i = 0; i < 100; i++) {
                    message = Unpooled.buffer(req.length);
                    message.writeBytes(req);
                    ctx.writeAndFlush(message);
                }
            }
        
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                ByteBuf buf = (ByteBuf) msg;
                byte[] req = new byte[buf.readableBytes()];
                buf.readBytes(req);
                String body = new String(req, "UTF-8");
                System.out.println("服务器发送给客户端的数据是:" + body + " ;总共次数是: " + (++counter));
            }
        
            @Override
            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
                LOGGER.warn(cause.getMessage(),cause.getCause());
                cause.printStackTrace();
                ctx.close();
            }
        }
    
  2. 客户端启动代码

        public class TimeClient {
            public void connect(int port, String host) {
                EventLoopGroup group = new NioEventLoopGroup();
                Bootstrap b = new Bootstrap();
                b.group(group).channel(NioSocketChannel.class)
                        .option(ChannelOption.TCP_NODELAY, true)
                        .handler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel ch) throws Exception {
                                ch.pipeline().addLast(new TimeClientHandler());
                            }
                        });
                try {
                    ChannelFuture f = b.connect(host, port).sync();
                    f.channel().closeFuture().sync();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    group.shutdownGracefully();
                }
            }
            public static void main(String[] args) {
                new TimeClient().connect(8080, "localhost");
            }
        }
    
  3. 服务端handler

        public class TimeServerHandler extends ChannelInboundHandlerAdapter {
            private volatile int counter;
            private static final String LINE = System.getProperty("line.separator");
            private static final String REQUEST_DATA = "this is message from client!";
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                ByteBuf buf = (ByteBuf) msg;
                byte[] req = new byte[buf.readableBytes()];
                buf.readBytes(req);
        
                String body = new String(req, "UTF-8").substring(0, req.length - LINE.length());
        
                System.out.println("客户端发送给服务端的数据: " + body + " ; 总共的次数是: " + (++counter));
        
                String currentTime = REQUEST_DATA.equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "消息不完整";
                currentTime = currentTime + LINE;
        
                ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
                ctx.writeAndFlush(resp);
            }
        
            @Override
            public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
                ctx.flush();
            }
        
            @Override
            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
                ctx.close();
            }
        }
        
    
  4. 服务端启动代码

        public class TimeServer {
            public static void main(String[] args) {
                new TimeServer().bind(8080);
            }
        
            public void bind(int port) {
                EventLoopGroup boosGroup = new NioEventLoopGroup();
                EventLoopGroup workerGroup = new NioEventLoopGroup();
        
                try {
                    ServerBootstrap b = new ServerBootstrap();
                    b.group(boosGroup, workerGroup)
                            .channel(NioServerSocketChannel.class)
                            .option(ChannelOption.SO_BACKLOG, 1012)
                            .childHandler(new ChildChannelHandler());
        
                    ChannelFuture f = b.bind(port).sync();
                    f.channel().closeFuture().sync();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    // 优雅退出,并释放线程池资源
                    boosGroup.shutdownGracefully();
                    workerGroup.shutdownGracefully();
                }
            }
        
            private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new TimeServerHandler());
                }
            }
        }
    
    
  5. 输出结果
    Netty笔记(五)之编解码器与粘包拆包_第3张图片
    Netty笔记(五)之编解码器与粘包拆包_第4张图片

ReplayingDecoder

  1. TODO…
  2. ReplayingDecoder扩展了ByteToMessageDecoder

编码器

MessageToByteEncoder

  1. 编码的复杂度大大小于解码的复杂度,这是因为编码不需考虑TCP粘包。编解码的处理还有一个常用的类MessageToMessageCodec用于POJO对象之间的转换

  2. MessageToByteEncoder框架可让用户使POJO对象编码为字节数据存储到ByteBuf。用户只需定义自己的编码方法encode()即可

    方法 描述
    encode(ChannelHandlerContext ctx, I msg, ByteBuf out) encode()是你需要实现的唯一抽象方法,它被调用时将会传入要被该编码为ByteBuf的消息类(类型为I)。该ByteBuf之后会被转发给ChannelPipeline中的下一个ChannelOutboundHandler

你可能感兴趣的:(#,netty基础)