Netty-4 TCP粘包拆包问题

完整代码:https://gitee.com/firewolf/java-io/tree/master/java-io/netty-02-tcppacket

一、TCP粘包/拆包问题

TCP是一个“流”协议,所谓流,也就是一串没界限的数字,在TCP底层是连接成一片的,没有界限,因为不知道业务层含义,所以只会根据TCP缓冲区的实际情况进行划分,导致在业务层来看,一个完整的包可能会被TCP拆分成多个包,也可能把多个小的包封装起来,形成一个大的包。
粘包和拆包可能会出现如下情况:
Netty-4 TCP粘包拆包问题_第1张图片
假设客户端发送两个数据包D1和D2给服务端,由于服务端一次读到的字节数是不固定的,所以可能出现如下几种情况:

  1. 服务端分两次读取到了两个完整的数据包,分别是D1和D2,没有发生粘包和拆包;
  2. 服务端一次读取到了两个数据包,D1和D2粘在了一起,发生了粘包;
  3. 服务端分两次读了两个数据包,第一次读取了完整的D1包和D2包的一部分内容D2_1,第二次读取了D2包的另外一部分D2_2。发生了拆包和粘包;
  4. 服务端分两次读了两个数据包,第一次读取了D1包的部分D1_1,第二次读取了D1包的另一部分D1_2和D2完整包,发生了粘包和拆包;
  5. 除此之外,还可能发生多次拆包和粘包;

二、TCP粘包/拆包问题产生的原因

问题产生的原因主要有三个:

  1. 应用程序write写入的字节大小大于套接口发送缓冲区的大小;
  2. 进行MSS大小的TCP分段
  3. 以太网的payload大于MTU进行IP分片
    Netty-4 TCP粘包拆包问题_第2张图片

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

  1. 消息定长,例如每个报文的长度为200,不够的时候,使用空格补齐;
  2. 在尾部增加回车换行符进行分割,如TCP协议;
  3. 在尾部增加特殊的字符进行分割,回车换行就是一种特殊的情况;
  4. 将消息分为消息头和消息体,消息头包含表示消息总长度字段;
  5. 使用更复杂的应用层协议;

四、TCP粘包/拆包问题演示

这里简单的写一个程序,功能是客户端连接服务端之后,向服务端发送20个字符串,服务端收到后进行显示:

(一)服务端

package com.firewolf.java.io.netty.packagee.paste.origin;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.util.HashMap;
import java.util.Map;

/**
 * 作者:刘兴 时间:2019/5/15
 **/
public class HelloServer {


  public HelloServer(int port) {

    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workGroup = new NioEventLoopGroup();
    try {
      ServerBootstrap bootstrap = new ServerBootstrap();
      bootstrap.group(bossGroup, workGroup)
          .channel(NioServerSocketChannel.class)
          .option(ChannelOption.SO_BACKLOG, 1024)
          .childHandler(new ChannelInitializer() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
              socketChannel.pipeline()
                  .addLast(new MessageServerHandler());
            }
          });
      ChannelFuture future = bootstrap.bind(port).sync();
      System.out.println("启动服务端监听端口:" + port);
      future.channel().closeFuture().sync();

    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      bossGroup.shutdownGracefully();
      workGroup.shutdownGracefully();
    }

  }

  class MessageServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
      ByteBuf buf = (ByteBuf) msg;
      byte[] bytes = new byte[buf.readableBytes()];
      buf.readBytes(bytes);
      String message = new String(bytes);
      System.out.println("有客户端消息:" + message);
      buf.release();
    }
  }


  public static void main(String[] args) {
    new HelloServer(9999);
  }

}

(二)客户端

package com.firewolf.java.io.netty.packagee.paste.origin;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.net.InetSocketAddress;

/**
 * 作者:刘兴 时间:2019/5/15
 **/
public class HelloClient {


  public HelloClient(String host, int port) {
    NioEventLoopGroup group = new NioEventLoopGroup();
    try {
      Bootstrap bootstrap = new Bootstrap();
      bootstrap.group(group)
          .channel(NioSocketChannel.class)
          .option(ChannelOption.TCP_NODELAY, true)
          .handler(new ChannelInitializer() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
              socketChannel.pipeline()
                  .addLast(new MessageClientHandler());
            }
          });

      ChannelFuture f = bootstrap.connect(new InetSocketAddress(host, port)).sync();
      System.out.println("连接服务器成功-----");
      f.channel().closeFuture().sync();
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      group.shutdownGracefully();
    }
  }


  class MessageClientHandler extends ChannelInboundHandlerAdapter {

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

      //遍历写出100个字符串
      for (int i = 0; i < 20; i++) {
        String message = "helllo," + i + System.getProperty("line.separator");
        ByteBuf buf = Unpooled.buffer(message.getBytes().length);
        buf.writeBytes(message.getBytes());
        ctx.writeAndFlush(buf);
      }
    }
  }


  public static void main(String[] args) {
    new HelloClient("127.0.0.1", 9999);
  }
}

启动服务端,再启动客户端,服务端打印信息如下:

有客户端消息:helllo,0
helllo,1
helllo,2
helllo,3
helllo,4
helllo,5
helllo,6
helllo,7
helllo,8
helllo,9
helllo,10
helllo,11
helllo,12
helllo,13
helllo,14
helllo,15
helllo,16
helllo,17
helllo,18
helllo,19

我们会发现,实际上, 服务端一次接受了全部字符串,也就是说,这些包并没有正常的接受到,而是发生了粘包。

五、Netty对粘包/拆包问题解决

为了解决TCP拆包/粘包导致的半读包问题,Netty默认提供了很多编码解码器用于处理半包。

(一)LineBasedFrameDecoder

LineBasedFrameDecoder会对包进行识别,根据包里面的回车(13)换行(10)符进行切割。

1. 服务端代码

package com.firewolf.java.io.netty.packagee.paste.solve;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import java.util.HashMap;
import java.util.Map;

/**
 * 作者:刘兴 时间:2019/5/15
 **/
public class HelloServerSolve {


  public HelloServerSolve(int port) {

    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workGroup = new NioEventLoopGroup();
    try {
      ServerBootstrap bootstrap = new ServerBootstrap();
      bootstrap.group(bossGroup, workGroup)
          .channel(NioServerSocketChannel.class)
          .option(ChannelOption.SO_BACKLOG, 1024)
          .childHandler(new ChannelInitializer() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
              socketChannel.pipeline()
                  //添加编码器
                  .addLast(new LineBasedFrameDecoder(1024))
                  .addLast(new StringDecoder())
                  .addLast(new MessageServerHandler());
            }
          });
      ChannelFuture future = bootstrap.bind(port).sync();
      System.out.println("启动服务端监听端口:" + port);
      future.channel().closeFuture().sync();

    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      bossGroup.shutdownGracefully();
      workGroup.shutdownGracefully();
    }

  }

  class MessageServerHandler extends ChannelInboundHandlerAdapter {


    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
      //由于StringDecoder的功劳,接受消息的时候,我们可以直接转成String
      String message = (String) msg;
      System.out.println("有客户端消息:" + message);
    }
  }


  public static void main(String[] args) {
    new HelloServerSolve(9999);
  }

}

服务端有两处改动

  • 给启动类BootStrap添加了消息解码器;
  • 接受消息的时候直接使用String接受,这个是由于添加了StringDecoder具有的功能。

2.客户端代码

package com.firewolf.java.io.netty.packagee.paste.solve;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import java.net.InetSocketAddress;

/**
 * 作者:刘兴 时间:2019/5/15
 **/
public class HelloClientSolve {


  public HelloClientSolve(String host, int port) {
    NioEventLoopGroup group = new NioEventLoopGroup();
    try {
      Bootstrap bootstrap = new Bootstrap();
      bootstrap.group(group)
          .channel(NioSocketChannel.class)
          .option(ChannelOption.TCP_NODELAY, true)
          .handler(new ChannelInitializer() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
              socketChannel.pipeline()
                  .addLast(new MessageClientHandler());
            }
          });

      ChannelFuture f = bootstrap.connect(new InetSocketAddress(host, port)).sync();
      System.out.println("连接服务器成功-----");
      f.channel().closeFuture().sync();
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      group.shutdownGracefully();
    }
  }


  class MessageClientHandler extends ChannelInboundHandlerAdapter {

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

      //遍历写出100个字符串
      for (int i = 0; i < 20; i++) {
        String message = "helllo," + i + System.getProperty("line.separator");
        ByteBuf buf = Unpooled.buffer(message.getBytes().length);
        buf.writeBytes(message.getBytes());
        ctx.writeAndFlush(buf);
      }
    }
  }


  public static void main(String[] args) {
    new HelloClientSolve("127.0.0.1", 9999);
  }
}

我们可以看到,客户端没有任何变化。

3.查看效果

再次启动后,打印内容如下:

有客户端消息:helllo,0
有客户端消息:helllo,1
有客户端消息:helllo,2
有客户端消息:helllo,3
有客户端消息:helllo,4
有客户端消息:helllo,5
有客户端消息:helllo,6
有客户端消息:helllo,7
有客户端消息:helllo,8
有客户端消息:helllo,9
有客户端消息:helllo,10
有客户端消息:helllo,11
有客户端消息:helllo,12
有客户端消息:helllo,13
有客户端消息:helllo,14
有客户端消息:helllo,15
有客户端消息:helllo,16
有客户端消息:helllo,17
有客户端消息:helllo,18
有客户端消息:helllo,19

可以看到,这些消息是分开了的,没有了粘包和拆包问题。
LineBasedFrameDecoder:以回车换行符作为对包进行切割;
StringDecoder:把ByteBuf转换成字符串
一般情况下,StringDecoder和LineBasedFrameDecoder会配合使用。

(二)DelimiterBasedFrameDecoder

其实DelimiterBasedFrameDecoder的功能和LineBasedFrameDecoder类似,只不过会比LineBasedFrameDecoder更加强大,支持我们自己指定用来切割的字符(串),使用方法和LineBasedFrameDecoder几乎是一模一样,这里只是简单的贴出部分代码:

1.服务端添加解码器

这里使用_$进行分割:

 socketChannel.pipeline()
                  //添加编码器
                  .addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("_$".getBytes())))

2.发送消息的时候,带上_$结尾

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

      //遍历写出100个字符串
      for (int i = 0; i < 20; i++) {
        String message = "helllo," + i + "_$";
        ByteBuf buf = Unpooled.buffer(message.getBytes().length);
        buf.writeBytes(message.getBytes());
        ctx.writeAndFlush(buf);
      }
    }

(三)FixedLengthFrameDecoder

FixedLengthFrameDecoder是固定长度解码器,它能够按照指定的长度对消息进行解码。
同样,这里只是简单的贴出发生变化的代码(相对于DelimiterBasedFrameDecoder)。

1. 服务端添加解码器

              socketChannel.pipeline()
                  //添加编码器
                  .addLast(new FixedLengthFrameDecoder(20))

这里设置长度为20

2. 客户端

没有任何变化,就不再贴出

效果如下:

有客户端消息:helllo,0_$helllo,1_$
有客户端消息:helllo,2_$helllo,3_$
有客户端消息:helllo,4_$helllo,5_$
有客户端消息:helllo,6_$helllo,7_$
有客户端消息:helllo,8_$helllo,9_$
有客户端消息:helllo,10_$helllo,11
有客户端消息:_$helllo,12_$helllo,
有客户端消息:13_$helllo,14_$helll
有客户端消息:o,15_$helllo,16_$hel
有客户端消息:llo,17_$helllo,18_$h

可以看到,得到的字符串长度都是20,而且,最后一个由于长度不足20,就没有显示出来。

六、总结

自行阅读上面几个解码器的原代码我们可以看到,这些解码器都是继承了ByteToMessageDecoder,对原始获取到的ByteBuf进行了处理,也就是说,我们也可以通过继承ByteToMessageDecoder来实现自己的解码器。

你可能感兴趣的:(#,Netty)