Netty学习(二)—拆包粘包问题

来源:互联网 发布:java考试认证 编辑:程序博客网 时间:2024/06/05 08:40

Netty学习(二)—拆包粘包问题

无论是服务端还是客户端在进行数据发送收取的时候需要考虑TCP底层的粘包/拆包机制,因为如果不进行处理会造成收取的数据和预想的不一致;

个人主页:tuzhenyu’s page
原文地址:Netty学习(二)—拆包粘包问题

(0) 拆包粘包的原因

TCP粘包发生的原因

  • 粘包现象出现的根本原因是TCP协议是一个面向数据流的通信协议,数据流中没有分界线;在发送或接收时并不会考虑业务数据的具体含义,而是会根据发送缓冲区或者接收缓冲区的情况进行数据包的划分;

  • 为了提高发送效率,发送端会采用Nagle算法优化发送,将时间间隔较短的多次小数据包发送合并成一个大数据包发送,接收端不能辨别出合并情况就出现了粘包情况;

  • 接收端的应用程序未能及时的读取接收缓冲区的数据,多次发送的数据堆积在套接字缓冲区,当应用程序读取时无法分辨就出现了粘包现象;

TCP拆包发生的原因

  • 拆包现象出现的根本原因在于数据包的发送受到链路MTU的限制,也就是链路能传输的最大数据包的大小的限制;

  • 当发送的数据包大小大于MTU-20(TCP报文头部)-20(IP报文头部)时就会出现TCP分组,将大数据包拆分成多个小数据包分别发送;

(1) 拆包粘包的解决策略

  • 数据定长,发送数据报文的长度小于MSS(最大可发送长度)如果不够则用空格补齐,接收时也进行定长接收,能解决粘包问题,不能解决拆包问题;

  • 特殊分割符,在数据结尾用回车换行符等分割,接收到的数据也以特殊字符进行分割,能解决粘包问题,不能解决拆包问题;

  • 将数据长度和数据一起发送,将数据分为消息头和消息体两部分,消息头中包含数据总长度,能解决拆包和粘包问题;

(2) 粘包的示例

  • 客户连续端发送数据到服务端
public void channelActive(ChannelHandlerContext ctx) throws Exception {    byte[] req = null;    ByteBuf buffer = null;    for (int i=0;i<100;i++){        req = ("this is No."+i+" server sent the message ").getBytes();        buffer = Unpooled.buffer(req.length);        buffer.writeBytes(req);        ctx.writeAndFlush(buffer);    }}
  • 服务端接收数据
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {    ByteBuf buf = (ByteBuf)msg;    byte[] bytes = new byte[buf.readableBytes()];    buf.readBytes(bytes);    System.out.println("receive the bytes: "+new String(bytes,"UTF-8"));}
  • 如果没有出现粘包现象则服务端会接收100次数据,输出100行数据;但是真实的情况服务端只接收了四次数据,出现了粘包现象,具体的是客户端发送粘包还是服务端接粘包不能判断;
receive the bytes: this is No.0 server sent the message this is No.1 server sent the message this is ...receive the bytes: No.27 server sent the message this is No.28 server sent the message this is...receive the bytes: s No.54 server sent the message this is No.55 server sent the message this is...receive the bytes:  is No.81 server sent the message this is No.82 server sent the message this is...

(3) 分包的示例

  • 客户端发送大数据包到服务端
public void channelActive(ChannelHandlerContext ctx) throws Exception {    byte[] req = null;    ByteBuf buffer = null;    StringBuilder sb = new StringBuilder();    for (int i=0;i<100;i++){        sb.append("abcdefghijklmnopqrstuvwxyz");    }    req = sb.toString().getBytes();    buffer = Unpooled.buffer(req.length);    buffer.writeBytes(req);    ctx.writeAndFlush(buffer);}
  • 服务端接收数据,每接一次输出一行
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {    ByteBuf buf = (ByteBuf)msg;    byte[] bytes = new byte[buf.readableBytes()];    buf.readBytes(bytes);    System.out.println("receive the bytes: "+new String(bytes,"UTF-8"));}
  • 按照预想的效果如果没有分包则客户端发送一次,服务端接收输出一次;实际上由于发送的数据包过大则客户端分多次进行发送,服务端也接收输出了多次;
receive the bytes: abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz...receive the bytes: klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgh...receive the bytes: uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrst...

(4) LineBasedFrameDecoder解决粘包问题

  • LineBasedFrameDecoder解决粘包问题主要是通过对输入流以换行符“\n”对数据流进行分割,每次读取时只读取换行符之前的数据;LineBasedFrameDecoder不能解决拆包问题;

  • 服务端发送数据,在每次发送结尾处添加换行符

ServerBootstrap b = new ServerBootstrap();            b.group(bossGroup,workGroup).channel(NioServerSocketChannel.class)                    .option(ChannelOption.SO_BACKLOG, 1024)                    .childHandler(new ChannelInitializer<SocketChannel>() {                        @Override                        protected void initChannel(SocketChannel socketChannel) throws Exception {                            socketChannel.pipeline().addLast(new ServerHandler());                        }                    });
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {        ByteBuf buf = (ByteBuf)msg;        byte[] bytes = new byte[buf.readableBytes()];        buf.readBytes(bytes);        String body = new String(bytes,"UTF-8");        System.out.println("the client says: "+ body);        byte[] req = null;        ByteBuf buffer = null;        for (int i=0;i<100;i++){            req = ("this is No."+i+" server sent the message\n").getBytes();            buffer = Unpooled.buffer(req.length);            buffer.writeBytes(req);            ctx.writeAndFlush(buffer);        }    }
  • 客户端接收数据,添加LineBasedFrameDecoder对数据流进行分割
Bootstrap b = new Bootstrap();            b.group(group).channel(NioSocketChannel.class)                    .option(ChannelOption.TCP_NODELAY, true)                    .handler(new ChannelInitializer<SocketChannel>() {                        @Override                        protected void initChannel(SocketChannel socketChannel) throws Exception {                            socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));                            socketChannel.pipeline().addLast(new ClientHandler());                        }                    });
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {        ByteBuf buf = (ByteBuf)msg;        byte[] bytes = new byte[buf.readableBytes()];        buf.readBytes(bytes);        String body = new String(bytes,"UTF-8");        System.out.println("the server says: "+ body);    }
  • 接收端添加LineBasedFrameDecoder解码器后对输入数据流进行分割,解决了粘包的情况
the server says: this is No.0 server sent the messagethe server says: this is No.1 server sent the messagethe server says: this is No.2 server sent the messagethe server says: this is No.3 server sent the messagethe server says: this is No.4 server sent the messagethe server says: this is No.5 server sent the messagethe server says: this is No.6 server sent the messagethe server says: this is No.7 server sent the messagethe server says: this is No.8 server sent the messagethe server says: this is No.9 server sent the messagethe server says: this is No.10 server sent the message

(4) DelimiterBasedFrameDecoder解决粘包问题

  • DelimiterBasedFrameDecoder特殊字符解码器和LineBasedFrameDecoder换行符解码器类似,都是在读取输入流时以特殊字符为分割符读取数据;DelimiterBasedFrameDecoder特殊字符解码器不能解决拆包问题,只能解决粘包问题;

  • 服务端发送数据到客户端

byte[] req = null;ByteBuf buffer = null;for (int i=0;i<100;i++){    req = ("this is No."+i+" server sent the message$").getBytes();    buffer = Unpooled.buffer(req.length);    buffer.writeBytes(req);    ctx.writeAndFlush(buffer);}
  • 客户端添加DelimiterBasedFrameDecoder解码器按照特殊字符对输入数据流进行分割;
            Bootstrap b = new Bootstrap();            b.group(group).channel(NioSocketChannel.class)                    .option(ChannelOption.TCP_NODELAY, true)                    .handler(new ChannelInitializer<SocketChannel>() {                        @Override                        protected void initChannel(SocketChannel socketChannel) throws Exception {                            socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,                                    Unpooled.copiedBuffer("$".getBytes())));                            socketChannel.pipeline().addLast(new ClientHandler());                        }                    });
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {        ByteBuf buf = (ByteBuf)msg;        byte[] bytes = new byte[buf.readableBytes()];        buf.readBytes(bytes);        String body = new String(bytes,"UTF-8");        System.out.println("the server says: "+ body);    }

(5) FixedLengthFrameDecoder定长解码器解决粘包问题

  • FixedLengthFrameDecoder定长解码器是提前设定消息数据的长度,接收端按照设定的长度进行数据流的分割;FixedLengthFrameDecoder只能解决粘包问题不能解决拆包问题;

  • 客户端接收数据,添加定长解码器分割数据流;

Bootstrap b = new Bootstrap();            b.group(group).channel(NioSocketChannel.class)                    .option(ChannelOption.TCP_NODELAY, true)                    .handler(new ChannelInitializer<SocketChannel>() {                        @Override                        protected void initChannel(SocketChannel socketChannel) throws Exception {                            socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(("hello world").getBytes().length));                            socketChannel.pipeline().addLast(new ClientHandler());                        }                    });
  • 服务端发送特定长度的数据,如果数据长度不够则使用空格填充保证数据包长度的一定;
for (int i=0;i<100;i++){    req = ("hello world").getBytes();    buffer = Unpooled.buffer(req.length);    buffer.writeBytes(req);    ctx.writeAndFlush(buffer);}

总结

  • 本文主要总结了粘包/拆包问题出现的原因和解决办法,以及Netty利用编码解码器对拆包/粘包问题的解决办法,并以LineBasedFrameDecoder,DelimiterBasedFrameDecoder和FixedLengthBasedFrameDecoder解码器为例解决TCP粘包问题;
原创粉丝点击