【Netty入门】TCP 粘包/拆包问题产生原因

TCP粘包/分包问题的由来

因为TCP是以流的方式来处理数据,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送。

这样说可能比较抽象,下面举例来说明TCP拆包/粘包问题!

  • 图解:如果客户端分别发送两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,可能会出现四种情况。

(1)服务端分别读取到D1和D2,没有产生粘包和拆包的情况,如下图:

(2)服务端一次接收到二个数据包,D1和D2粘合在一起,被成为TCP粘包。如下图:

(3)服务端分二次读取到了二个数据包,第一次读取到了完整的D1包和D2包的一部分,第二次读取到了D2包的剩余部分,这被成为TCP拆包(D2拆包),如下图:

(4)服务器还是分二次读取到了二个数据包,但第一次是读取到了D1包的部分内容 ,第二次读取到了D1包剩余部分和完整的D2包,这被成为TCP拆包(D1拆包),如下图:

  • 代码示例:

服务端代码

public class Server4 {
    public static void main(String[] args) throws SigarException {

        //boss线程监听端口,worker线程负责数据读写
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();

        try{
            //辅助启动类
            ServerBootstrap bootstrap = new ServerBootstrap();
            //设置线程池
            bootstrap.group(boss,worker);

            //设置socket工厂
            bootstrap.channel(NioServerSocketChannel.class);

            //设置管道工厂
            bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    //获取管道
                    ChannelPipeline pipeline = socketChannel.pipeline();
                    //处理类
                    pipeline.addLast(new ServerHandler4());
                }
            });

            //绑定端口
            ChannelFuture future = bootstrap.bind(8866).sync();
            System.out.println("server start ...... ");

            //等待服务端监听端口关闭
            future.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //优雅退出,释放线程池资源
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }

}

class ServerHandler4 extends SimpleChannelInboundHandler {

    //用于记录次数
    private int count = 0;
    //读取客户端发送的数据
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf)msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);
        String c = new String(req,"UTF-8").substring(0, req.length - System.getProperty("line.separator").length());
        count++;
        System.out.println("RESPONSE--------"+c+";"+" @ "+count);

    }

    //新客户端接入
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channelActive");
    }

    //客户端断开
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channelInactive");
    }

    //异常
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //关闭通道
        ctx.channel().close();
        //打印异常
        cause.printStackTrace();
    }
}

客户端代码

public class Client4 {

    public static void main(String[] args) {

        //worker负责读写数据
        EventLoopGroup worker = new NioEventLoopGroup();

        try {
            //辅助启动类
            Bootstrap bootstrap = new Bootstrap();

            //设置线程池
            bootstrap.group(worker);

            //设置socket工厂
            bootstrap.channel(NioSocketChannel.class);

            //设置管道
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    //获取管道
                    ChannelPipeline pipeline = socketChannel.pipeline();
                    //处理类
                    pipeline.addLast(new ClientHandler4());
                }
            });

            //发起异步连接操作
            ChannelFuture futrue = bootstrap.connect(new InetSocketAddress("127.0.0.1",8866)).sync();

            //等待客户端链路关闭
            futrue.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //优雅的退出,释放NIO线程组
            worker.shutdownGracefully();
        }
    }

}

class ClientHandler4 extends SimpleChannelInboundHandler<String> {

    //接受服务端发来的消息
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println("server response : "+msg);
    }

    //与服务器建立连接
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //给服务器发消息
        ByteBuf message = null;
        byte[] req = " I am client ".getBytes();
        //发送50次消息
        for (int i = 0; i < 50; i++) {
            message = Unpooled.buffer(req.length);
            message.writeBytes(req);
            ctx.channel().writeAndFlush(message);
        }
    }

    //与服务器断开连接
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channelInactive");
    }

    //异常
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //关闭管道
        ctx.channel().close();
        //打印异常信息
        cause.printStackTrace();
    }

}

服务端运行结果:

分析:通过代码可知,客户端向服务端发送了50条消息,正常结果是服务端应该接收了50条消息,但服务端的运行结果显示只收到了两条客户端的消息,由图知,第一条消息包含37 个I am client,而第二条消息包含13个I am client。这明显是出现了TCP粘包问题。

出现TCP粘包/分包的原因

1.应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象,而应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包现象;

2.进行mss(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包

3.以太网帧的payload(净荷)大于MTU(1500字节)进行ip分片。

图解:

TCP粘包/分包的解决方法

1.消息定长

例如:每个报文的大小固定为200个字节,如果不够,空位补空格

对应Netty中的定长类 :FixedLengthFrameDecoder

2.在包尾都增加特殊字符进行分割

例如:加回车、加换行、FTP协议等

对应Netty中的类

  • 自定义分隔符类 :DelimiterBasedFrameDecoder
  • 行分隔符类:LineBasedFrameDecoder

3.将消息分为消息头和消息体

例:在消息头中包含表示消息总长度的字段,然后进行业务逻辑的处理。

对应Netty中的基于消息头指定消息长度类:LengthFieldBasedFrameDecoder

4.更复杂的应用层协议

解决TCP粘包/分包问题的实例请阅读我的下一篇博文:解决TCP粘包/分包的实例


本人才疏学浅,若有错误,请指出
谢谢!

你可能感兴趣的:(tcp,粘包拆包)