Java NIO(六)Netty解决TCP粘包/拆包

TCP粘包/拆包

Tcp是个“流”协议,所谓流就是没有界限的一串数据。可以类比一下水流,没有分极限。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分。所以一个业务数据可能被TCP拆分成多个包进行发送,也有可能把多个小的数据包封装成一个大的数据包发送。这就是TCP的拆包粘包

出现TCP拆包/粘包的几个原因:

  • 程序write写入的字节大小大于套接口发送缓冲区大小。
  • 进行MSS(TCP传输时的最大报文段长度)大小的TCP分段。
  • 以太网帧的payload(封装后的不含头和尾的数据包部分)大于MTU( 最大传输单元()进行IP分片。

TCP拆包/粘包问题的解决策略:

  • 消息定长,例如,每个报文的大小固定长度为200字节,如果不够,空位补空格。
  • 在包尾增加回车换行符进行分割,例如FTP协议。
  • 将特殊字符作为消息结束的标志,回车换行符只是其中的一种。
  • 将消息分为消息头和消息体,消息头中包含表示消息长度的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度。

Netty对于粘包/拆包问题重现

重现问题,将上一篇文章中的代码进行修改。

1.服务端修改NettyServerHandler类,加一个计数器:

public class NettyServerHandler extends ChannelHandlerAdapter {

    private int count = 0;//记录客户端请求次数。

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws UnsupportedEncodingException {
        ByteBuf buf = (ByteBuf) msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);
        String body = new String(req,"UTF-8");
        System.out.println("收到客户端消息:" + body + ";次数是:" + count++);
        String currentTime = "query".equalsIgnoreCase(body)?new java.util.Date(System.currentTimeMillis()).toString():"error";

        ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
        ctx.write(resp);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

2.客户端修改NettyClientHandler类,向服务端写100次数据。

public class NettyClientHandler extends ChannelHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {

        ByteBuf firstMessage = null;
        //此处做了修改。
        byte[] req = ("query"+System.getProperty("line.separator")).getBytes();
        for(int j=0; j<100; j++){
            firstMessage = Unpooled.buffer(req.length);
            firstMessage.writeBytes(req);
            ctx.writeAndFlush(firstMessage);
        }

    }

    @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("Now is : " + body);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

服务端的运行结果如下,次数并不是100次,而是发生了粘包,如果把客户端的字符串换成一个更长的字符串,效果会更明显。

这里写图片描述

客户端的运行结果如下,客户端在接收消息时也发生了粘包,收到了两个连在一起的error字符串。
这里写图片描述

Netty对于粘包/拆包问题的初步解决

分别在服务端和客户端的初始化时使用了两个解码器LineBasedFrameDecoder和StringDecoder,这两个解码器是通过换行符来配合实现的拆包粘包。

服务端修改内容如下

//修改了NettyServerInit类
public class NettyServerInit extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
        //添加了下面两行。
        socketChannel.pipeline().addLast(new StringDecoder());
        socketChannel.pipeline().addLast(new NettyServerHandler());
    }
}
//NettyServerHandler修改如下:
public class NettyServerHandler extends ChannelHandlerAdapter {

    private int count = 0;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws UnsupportedEncodingException {
        //直接强转了消息,不用再处理
        String body = (String) msg;

        System.out.println("收到客户端消息:" + body + "次数是:" + ++count);
        String currentTime = ("query".equalsIgnoreCase(body)?new java.util.Date(System.currentTimeMillis()).toString():"error") +System.getProperty("line.separator");

        ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
        ctx.writeAndFlush(resp);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

客户端端修改内容如下

public class NettyClient {

    public void connect(int port,String host) throws InterruptedException {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY,true)
                    .handler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        public void initChannel(NioSocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
                            //在此处加了两个解码器。
                            ch.pipeline().addLast(new StringDecoder());
                            ch.pipeline().addLast(new NettyClientHandler());
                        }
                    });
            ChannelFuture f = b.connect("192.168.1.104",port).sync();
            f.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new NettyClient().connect(55557,"127.0.0.1");
    }
}
public class NettyClientHandler extends ChannelHandlerAdapter {

    private int count = 0;

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {

        ByteBuf firstMessage = null;

        byte[] req = ("query"+System.getProperty("line.separator")).getBytes();
        for(int j=0; j<100; j++){
            firstMessage = Unpooled.buffer(req.length);
            firstMessage.writeBytes(req);
            ctx.writeAndFlush(firstMessage);
        }

    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        //此处不用再处理消息,直接强转即可。
        String body = (String) msg;
        System.out.println("第" + ++count + "次收到时间N : " + body);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

服务端运行结果如下:
这里写图片描述

客户端运行结果如下:
这里写图片描述

可以发现,服务端和客户端都达到了预期效果,客户端发送了100次消息,服务端响应了100次消息。

Netty对于粘包/拆包问题其他解决方案

LineBasedFrameDecoder是通过换行符来实现的拆包粘包,Netty中还有两种常见的解码器,分别是利用“分隔符”和“定长”的解码器。

文章最开始提到了TCP拆包/粘包问题的解决策略,大体分为4种,Netty分别对这四种进行了抽象。

  • DelimiterBasedFrameDecoder自定义消息分隔符解码器。
  • FixedLengthFrameDecoder固定长度解码器。

猜你喜欢

转载自blog.csdn.net/xyh930929/article/details/80941759