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

来源:互联网 发布:淘宝买家达到钻号 编辑:程序博客网 时间:2024/06/08 10:18

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粘包/分包的实例


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

原创粉丝点击