1.什么是TCP拆包与粘包

TCP是基于流的协议,也就是说,是一段没有界限的数据。tcp底层并不了解上层应用系统的逻辑,他会根据tcp缓冲区进行包的划分。因此,在应用层面,可能会出现一个完成的数据包被TCP拆分为多个包发送,也有可能会出现多个包在一起发送。这就是所谓的TCP拆包粘包问题

2.TCP拆包与粘包发生的原因

  1. 应用程序写入的字节大于或小于套接口缓冲区的大小
  2. 进行MSS大小的TCP分段
  3. 以太网帧的payload大于MTU进行IP分片

    3.TCP拆包与粘包的解决思路

    因为拆包粘包问题发生在TCP的底层协议,底层协议是无法知道上层业务逻辑的包的具体重组和拆分,因此只能通过上层协议来解决这个问题。大致包括以下几种思路

  4. 消息定长,发送方固定每个报文长度,长度不足进行空格补位。接收方累计读取到定长的报文后,将其视作一个完整的包,然后重启计数器,继续读取下一个数据包

  5. 在包尾增加分隔符进行拆分。例如回车换行分隔符等等。发送方在包体尾部添加分隔符来划分数据包,接收方通过获取数据流中分隔符的位置来进行数据包的拆分读取。 FTP协议利用的就是换行分隔符。
  6. 将消息分为消息头和消息体,消息头中包含消息总长度(或者消息体的长度)。发送方在包体消息头中写入消息体总长度,接收方读取消息头第一个字符获取到总长度,再累计读取对应长度将其视作一个完整的数据包。通常设计为消息头第一个字段使用int32来表示消息总长度。
  7. 更复杂的应用层协议

    4.编写不支持拆包粘包的服务端

    ```java package demo1;

import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel;

/**

  • @author 冯铁城 [17615007230@163.com]
  • @date 2022-05-09 19:11:57
  • @describe: 时间服务器服务端 */ public class TimeServer {

    /**

    • 开启socket链接 *
    • @param port 端口号 */ public void bind(int port) {

      //1.创建线程组用于接受服务端链接 NioEventLoopGroup bossGroup = new NioEventLoopGroup();

      //2.创建线程组用户socketChannel读写 NioEventLoopGroup workGroup = new NioEventLoopGroup(); try {

      1. //3.创建服务端
      2. ServerBootstrap server = new ServerBootstrap();
      3. server.group(bossGroup, workGroup)
      4. .channel(NioServerSocketChannel.class)
      5. .option(ChannelOption.SO_BACKLOG, 1024)
      6. .childHandler(new ChildChannelHandler());
      7. //3.绑定端口,开启同步等待
      8. ChannelFuture sync = server.bind(port).sync();
      9. System.out.println("服务端已启动!");
      10. //4.等待服务端监听端口关闭
      11. sync.channel().closeFuture().sync();

      } catch (InterruptedException e) {

      1. e.printStackTrace();

      } finally {

      1. bossGroup.shutdownGracefully();
      2. workGroup.shutdownGracefully();

      } }

      private class ChildChannelHandler extends ChannelInitializer { @Override protected void initChannel(SocketChannel socketChannel) throws Exception {

      1. socketChannel.pipeline().addLast(new TimeServerHandler());

      } }

      private class TimeServerHandler extends ChannelHandlerAdapter {

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

      1. System.out.println("服务端已与" + ctx.channel().id() + "建立链接");

      }

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

      1. //1.字符串判定
      2. ByteBuf byteBuf = (ByteBuf) msg;
      3. byte[] bytes = new byte[byteBuf.readableBytes()];
      4. byteBuf.readBytes(bytes);
      5. System.out.println("收到" + ctx.channel().id() + "渠道的请求:" + new String(bytes, "utf-8"));
      6. //2.定义返回结果
      7. String responseMessage = "响应信息\r\n";
      8. //3.相应数据
      9. ByteBuf response = Unpooled.copiedBuffer(responseMessage.getBytes());
      10. ctx.write(response);

      }

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

      1. ctx.flush();

      }

      @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {

      1. ctx.close();

      } } } ``` 服务端接收客户端消息以后,正常回复消息

      5.不支持拆包粘包的客户端

      ```java package demo1;

import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel;

/**

  • @author 冯铁城 [17615007230@163.com]
  • @date 2022-05-09 21:09:16
  • @describe: 时间服务器客户端 */ public class TimeClient {

    /**

    • 创建客户端链接 *
    • @param host 主机
    • @param port 端口号 */ public void connect(String host, int port) {

      //1. 创建用于IO读写的线程组 EventLoopGroup group = new NioEventLoopGroup(); try {

      1. //2.创建客户端启动类
      2. Bootstrap bootstrap = new Bootstrap();
      3. //3.启动类初始化
      4. bootstrap.group(group)
      5. .channel(NioSocketChannel.class)
      6. .option(ChannelOption.TCP_NODELAY, true)
      7. .handler(new ChannelInitializer<SocketChannel>() {
      8. @Override
      9. protected void initChannel(SocketChannel socketChannel) throws Exception {
      10. socketChannel.pipeline().addLast(new TimeClientHandler());
      11. }
      12. });
      13. //4.发起异步链接操作
      14. ChannelFuture sync = bootstrap.connect(host, port).sync();
      15. //5.等待客户端链路链接关闭
      16. sync.channel().closeFuture().sync();

      } catch (InterruptedException e) {

      1. e.printStackTrace();

      } finally {

      1. group.shutdownGracefully();

      } }

      /**

    • @author 冯铁城 [17615007230@163.com]
    • @date 2022-05-09 21:01:23
    • @describe: 时间服务器客户端 */ public class TimeClientHandler extends ChannelHandlerAdapter {

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

      1. for (int i = 0; i < 20; i++) {
      2. String requestMessage = "发送消息\r\n";
      3. ByteBuf message = Unpooled.buffer(requestMessage.getBytes().length);
      4. message.writeBytes(requestMessage.getBytes());
      5. ctx.writeAndFlush(message);
      6. }

      }

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

      1. ByteBuf buf = (ByteBuf) msg;
      2. byte[] bytes = new byte[buf.readableBytes()];
      3. buf.readBytes(bytes);
      4. String response = new String(bytes, "UTF-8");
      5. System.out.println("收到服务端答复:" + response);

      }

      @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {

      1. System.out.println("链接出现异常,异常信息:" + cause.getMessage());
      2. ctx.close();

      } } } ``` 客户端在与服务端建立TCP链接时,触发channelActive接口,理论上发送给服务端20条消息,会收到20条服务端的响应
      6.调试结果
      服务端实际上只收到了一条消息
      image.png
      客户端也只收到了一条回复
      image.png
      由此可见出现了粘包问题