netty解决拆包粘包

139 阅读4分钟

netty拆包粘包产生原因

由于TCP底层无法理解上层的应用数据,所以TCP底层无法保证数据包不会被拆分和重组,该问题只能通过上层应用协议栈设计来解决。

解决TCP拆包粘包的根本原因,就是找出消息的边界。业界主流的方案有如下几个:

  1. 消息定长,譬如每个报文规定长度为100个字节,如果不够,空位用空格或其余字符代替。
  2. 指定分隔符,在包尾添加如换行符进行分割。
  3. 将消息分为消息头和消息体,消息头中包含标示数据总长度的字段,根据长度读取数据。
  4. 更复杂的应用层协议。

以上是TCP粘包/拆包的基础知识,下面用实际案例来看看netty是如何解决TCP粘包拆包问题。

1. 固定长度-FixedLengthFrameDecoder

消息长度固定,累计读取到长度总和为定长Len的报文后,就认为读取到了一条完整的消息。

注意:FixedLengthFrameDecoder并没有提供一个对应的编码器,因为接收方只需要根据字节数进行判断即可,发送方无需编码。

例如:我们规定每个报文的大小为固定长度 5个字节,表示一个有效报文,如果不够,空位补空格;

优点是实现比较简单,缺点是造成资源浪费,因为此方式不推荐使用。

服务端开发

@Slf4j
public class FixedLengthServer {
    public static void main(String[] args) {
        EventLoopGroup boos = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(boos, worker)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new FixedLengthFrameDecoder(5))
                                .addLast(new StringDecoder())
                                .addLast(new ChannelInboundHandlerAdapter() {
                                    @Override
                                    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                        log.info("server:{}", msg);
                                    }
                                });
                    }
                });

        ChannelFuture channelFuture = bootstrap.bind(8080);
        try {
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            boos.shutdownGracefully();
            worker.shutdownGracefully();
        }

    }
}

客户端开发

@Slf4j
public class FixeLengthClient {
    public static void main(String[] args) throws InterruptedException {
        Channel channel = new Bootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        //将发送的内容encode编码
                        nioSocketChannel.pipeline().addLast(new StringEncoder());
                    }
                })
                .connect(new InetSocketAddress("localhost", 8080))
                .sync()
                .channel();
        for (int i = 0; i < 10; i++) {
            channel.writeAndFlush("hello222");
        }


    }
}

运行结果

image.png

指定分隔符-DelimiterBasedFrameDecoder

DelimiterBasedFrameDecoder提供了多个构造方法,比如下面两个

image.png 其中:

  • maxFrameLength:
    1. 指定消息的最大长度,如果消息长度超过指定的长度并且没有找到分隔符,则会抛异常。
    2. 如果消息长度小于指定的长度并且没有找到分隔符,会缓存收到的消息,直到接收到分隔符。
    3. 同时存在多个分隔符时,优先匹配长度最短的分隔符,如果一样长,则哪个先出现,匹配哪个。
  • stripDelimiter:
    1. 解码后的消息是否去除分隔符。
  • delimiter
    1. 自定义的分隔符

优点是实现简单,缺点是需要对整个包消息进行扫描(在数据中寻找分隔符),因为效率不高。

服务端开发

/**
 * 指定分隔符解决粘包粘包
 */
@Slf4j
public class DelimiterServer {
    public static void main(String[] args) {
        EventLoopGroup boos = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(boos, worker)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("$_".getBytes())))
                                .addLast(new StringDecoder())
                                .addLast(new ChannelInboundHandlerAdapter() {
                                    @Override
                                    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                        log.info("server:{}", msg);
                                    }
                                });
                    }
                });

        ChannelFuture channelFuture = bootstrap.bind(8080);
        try {
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            boos.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

客户端开发

@Slf4j
public class DelimiterClient {
    public static void main(String[] args) throws InterruptedException {
        Channel channel = new Bootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        //将发送的内容encode编码
                        nioSocketChannel.pipeline().addLast(new StringEncoder());
                    }
                })
                .connect(new InetSocketAddress("localhost", 8080))
                .sync()
                .channel();
        for (int i = 0; i < 10; i++) {
            channel.writeAndFlush("hello$_");
        }


    }
}

运行结果

image.png

指定长度-LengthFieldBasedFrameDecoder

简单来说就是在消息中增加数据长度域,当然还可以增加其它信息,该方式类似于http协议,如http协议中header的Content-Length属性。

LengthFieldBasedFrameDecoder有多种配置参数的方式,常见于私有客户端和服务器协议。

参数说明

LengthFieldBasedFrameDecoder 类里面主要有以下这几个参数:

  1. maxFrameLength:本次能接收的最大的数据长度

  2. lengthFieldOffset:设置的长度域的偏移量,长度域在数据包的起始位置

  3. lengthFieldLength:长度域的长度

  4. lengthAdjustment:数据包的偏移量,可以理解为长度域和content区间的数据长度

  5. initialBytesToStrip:需要跳过的字节数

lengthFieldLength

image.png

这里将 lengthFieldLength 设置为2,说明用2个字节(0x000C转成十进制等于12)表示消息内容的总长度(Content)

lengthAdjustment

image.png

这里将 initialBytesToStrip 设置为2,代表解码后跳过消息中的前两个字节(长度域)

lengthFieldOffset

image.png 长度域 lengthFieldLength 长为3个字节(0x00000C),lengthFieldOffset 偏移量为2(因为前两个字节给Header1 占了)

initialBytesToStrip

image.png

长度域 lengthFieldLength 为两个字节,initialBytesToStrip设置为2,解码后跳过两个字节,即解码后的数据不包含长度域

lengthAdjustment

image.png

lengthAdjustment 设置为2,表示 LengthContent 中间还有两个字节的Header1

服务端开发


@Slf4j
public class LengthFieldServer {

    public static void main(String[] args) {

        LengthFieldServer lengthFieldServer = new LengthFieldServer();
        lengthFieldServer.start(8888);
    }

    private void start(int port) {
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(boss, worker)
                    .channel(NioServerSocketChannel.class)
                    //给boss添加handler
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel ch) {
                            ChannelPipeline pipeline = ch.pipeline();
                            //发送的数据包最大长度为1024*64
                            //length域的偏移,正常情况下读 取数据从偏移为0处开始读取,如果有需要可以从其他偏移量处开始读取
                            //length域占用的字节数
                            //在length域和content域中间是 否需要填充其他字节数
                            //解码后跳过的字节数 ( 解码后把 length占用的字节跳过,直接传数据包)
                            pipeline.addLast(new LengthFieldBasedFrameDecoder(1024 * 64, 0, 4, 0, 4));
                            pipeline.addLast(new MySimpleChannelInboundHandler());
                        }
                    });

            //绑定端口
            ChannelFuture future = serverBootstrap.bind(port).sync();
            //监听端口的关闭
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            log.error("netty server error ,{}", e.getMessage(), e);
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }


    }

}

服务端handle

@Slf4j
public class MySimpleChannelInboundHandler extends SimpleChannelInboundHandler<ByteBuf> {

    private int count = 0;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        count++;
        log.info("服务端第:{} 次收到消息,消息内容为:{}", count, msg.toString(StandardCharsets.UTF_8));
    }
}

客户端handle


@Slf4j
public class ClientInboundHandler extends ChannelInboundHandlerAdapter {


    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i = 1; i <= 100; i++) {
            ByteBuf byteBuf = ctx.alloc().buffer().writeBytes((i + "").getBytes());
            ctx.channel().writeAndFlush(byteBuf);
        }
        super.channelActive(ctx);
    }


}

客户端开发

@Slf4j
public class LengthFieldClient {

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

    public void start(String host, int port) {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ChannelPipeline pipeline = ch.pipeline();
                            //添加编码器
                            //参数,长度域
                            pipeline.addLast(new LengthFieldPrepender(4));

                            //添加客户端channel对应的handler
                            pipeline.addLast(new ClientInboundHandler());
                        }
                    });
            //连接远程启动
            ChannelFuture future = bootstrap.connect(host, port).sync();
            //监听通道关闭
            Channel channel = future.channel();

            channel.closeFuture().sync();

        } catch (Exception e) {
            log.error("netty client error ,msg={}", e.getMessage());
        } finally {
            //优雅关闭
            group.shutdownGracefully();
        }
    }

}

结果

image.png

客户端发送了100条消息,服务端均接收到。