当前位置: 移动技术网 > IT编程>开发语言>Java > Netty粘包拆包问题解决方案

Netty粘包拆包问题解决方案

2020年08月17日  | 移动技术网IT编程  | 我要评论
tcp黏包拆包tcp是一个流协议,就是没有界限的一长串二进制数据。tcp作为传输层协议并不不了解上层业务数据的具体含义,它会根据tcp缓冲区的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,

tcp黏包拆包

tcp是一个流协议,就是没有界限的一长串二进制数据。tcp作为传输层协议并不不了解上层业务数据的具体含义,它会根据tcp缓冲区的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被tcp拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的tcp粘包和拆包问题。
怎么解决?

  • • 消息定长度,传输的数据大小固定长度,例如每段的长度固定为100字节,如果不够空位补空格
  • • 在数据包尾部添加特殊分隔符,比如下划线,中划线等
  • • 将消息分为消息头和消息体,消息头中包含表示信息的总长度

netty提供了多个解码器,可以进行分包的操作,分别是:

  • • linebasedframedecoder (回车换行分包)
  • • delimiterbasedframedecoder(特殊分隔符分包)
  • • fixedlengthframedecoder(固定长度报文来分包)
  • • lengthfieldbasedframedecoder(自定义长度来分包)

制造粘包和拆包问题

为了验证我们的解码器能够解决这种粘包和拆包带来的问题,首先我们就制造一个这样的问题,以此用来做对比。
服务端:

public static void main(string[] args) {
    eventloopgroup bossgroup = new nioeventloopgroup();
    eventloopgroup workergroup = new nioeventloopgroup();
    serverbootstrap bootstrap = new serverbootstrap();
    bootstrap.group(bossgroup, workergroup)
        .channel(nioserversocketchannel.class)
        .childhandler(new channelinitializer<socketchannel>() { 
          @override
          public void initchannel(socketchannel ch) throws exception {
            ch.pipeline().addlast("decoder", new stringdecoder());
            ch.pipeline().addlast("encoder", new stringencoder());
            ch.pipeline().addlast(new channelinboundhandleradapter() {
              @override
              public void channelread(channelhandlercontext ctx, object msg) {
                system.err.println("server:" + msg.tostring());
                ctx.writeandflush(msg.tostring() + "你好" );
              }
            });
          }
        })
        .option(channeloption.so_backlog, 128)
        .childoption(channeloption.so_keepalive, true);
    try {
      channelfuture f = bootstrap.bind(2222).sync();
       f.channel().closefuture().sync();
    } catch (interruptedexception e) {
      e.printstacktrace();
    } finally {
      workergroup.shutdowngracefully();
      bossgroup.shutdowngracefully();
    }
  }

客户端我们发送一个比较长的字符串,如果服务端收到的消息是一条,那么就是对的,如果是多条,那么就有问题了。

public static void main(string[] args) {
    eventloopgroup workergroup = new nioeventloopgroup();
    channel channel = null;
    try {
      bootstrap b = new bootstrap();
      b.group(workergroup);
      b.channel(niosocketchannel.class);
      b.option(channeloption.so_keepalive, true);
      b.handler(new channelinitializer<socketchannel>() {
        @override
        public void initchannel(socketchannel ch) throws exception {
          ch.pipeline().addlast("decoder", new stringdecoder());
          ch.pipeline().addlast("encoder", new stringencoder());
          ch.pipeline().addlast(new channelinboundhandleradapter() {
            @override
            public void channelread(channelhandlercontext ctx, object msg) {
              system.err.println("client:" + msg.tostring());
            }
          });
        }
      });
      channelfuture f = b.connect("127.0.0.1", 2222).sync();
      channel = f.channel();
      stringbuilder msg = new stringbuilder();
      for (int i = 0; i < 100; i++) {
        msg.append("hello yinjihuan");
      }
      channel.writeandflush(msg);
    } catch(exception e) {
      e.printstacktrace();
    }
  }

首先启动服务端,然后再启动客户端,通过控制台可以看到服务接收的数据分成了2次,这就是我们要解决的问题。

server:hello yinjihuanhello....
server:o yinjihuanhello...

linebasedframedecoder

用linebasedframedecoder 来解决需要在发送的数据结尾加上回车换行符,这样linebasedframedecoder 才知道这段数据有没有读取完整。

改造服务端代码,只需加上linebasedframedecoder 解码器即可,构造函数的参数是数据包的最大长度。

 public void initchannel(socketchannel ch) throws exception {
   ch.pipeline().addlast(new linebasedframedecoder(10240));
   ch.pipeline().addlast("decoder", new stringdecoder());
   ch.pipeline().addlast("encoder", new stringencoder());
   ch.pipeline().addlast(new channelinboundhandleradapter() {
      @override
      public void channelread(channelhandlercontext ctx, object msg) {
        system.err.println("server:" + msg.tostring());
        ctx.writeandflush(msg.tostring() + "你好");
      }
   });
}

改造客户端发送代码,再数据后面加上回车换行符

channelfuture f = b.connect("127.0.0.1", 2222).sync();
channel = f.channel();
stringbuilder msg = new stringbuilder();
for (int i = 0; i < 100; i++) {
  msg.append("hello yinjihuan");
}
channel.writeandflush(msg + system.getproperty("line.separator"));

delimiterbasedframedecoder

delimiterbasedframedecoder和linebasedframedecoder差不多,delimiterbasedframedecoder可以自己定义需要分割的符号,比如下划线,中划线等等。
改造服务端代码,只需加上delimiterbasedframedecoder解码器即可,构造函数的参数是数据包的最大长度。我们用下划线来分割。

public void initchannel(socketchannel ch) throws exception {
   ch.pipeline().addlast(new delimiterbasedframedecoder(10240, unpooled.copiedbuffer("_".getbytes())));
   ch.pipeline().addlast("decoder", new stringdecoder());
   ch.pipeline().addlast("encoder", new stringencoder());
   ch.pipeline().addlast(new channelinboundhandleradapter() {
      @override
      public void channelread(channelhandlercontext ctx, object msg) {
        system.err.println("server:" + msg.tostring());
        ctx.writeandflush(msg.tostring() + "你好");
      }
   });
}

改造客户端发送代码,再数据后面加上下划线

channelfuture f = b.connect("127.0.0.1", 2222).sync();
channel = f.channel();
stringbuilder msg = new stringbuilder();
for (int i = 0; i < 100; i++) {
  msg.append("hello yinjihuan");
}
channel.writeandflush(msg + "_");

fixedlengthframedecoder

fixedlengthframedecoder是按固定的数据长度来进行解码的,也就是说你客户端发送的每条消息的长度是固定的,下面我们看看怎么使用。

服务端还是一样,增加fixedlengthframedecoder解码器即可。

 public void initchannel(socketchannel ch) throws exception {
   ch.pipeline().addlast(new fixedlengthframedecoder(1500));
   ch.pipeline().addlast("decoder", new stringdecoder());
   ch.pipeline().addlast("encoder", new stringencoder());
   ch.pipeline().addlast(new channelinboundhandleradapter() {
      @override
      public void channelread(channelhandlercontext ctx, object msg) {
        system.err.println("server:" + msg.tostring());
        ctx.writeandflush(msg.tostring() + "你好");
      }
   });
}

客户端,msg输出的长度就是1500

channelfuture f = b.connect("127.0.0.1", 2222).sync();
channel = f.channel();
stringbuilder msg = new stringbuilder();
for (int i = 0; i < 100; i++) {
  msg.append("hello yinjihuan");
}
system.out.println(msg.length());
channel.writeandflush(msg);

服务端代码:

public void initchannel(socketchannel ch) throws exception {
   ch.pipeline().addlast("framedecoder", new lengthfieldbasedframedecoder(integer.max_value, 0, 4, 0, 4));
   ch.pipeline().addlast("frameencoder", new lengthfieldprepender(4));
   ch.pipeline().addlast("decoder", new stringdecoder());
   ch.pipeline().addlast("encoder", new stringencoder());
   ch.pipeline().addlast(new channelinboundhandleradapter() {
      @override
      public void channelread(channelhandlercontext ctx, object msg) {
        system.err.println("server:" + msg.tostring());
        ctx.writeandflush(msg.tostring() + "你好");
      }
   });
}

客户端,直接发送就行

channelfuture f = b.connect("127.0.0.1", 2222).sync();
channel = f.channel();![](https://s4.51cto.com/images/blog/202008/04/fb05cdb6bd8458bd1006a127ff9d12dc.png?x-oss-process=image/watermark,size_16,text_qduxq1rp5y2a5a6i,color_ffffff,t_100,g_se,x_10,y_10,shadow_90,type_zmfuz3pozw5nagvpdgk=)
stringbuilder msg = new stringbuilder();
for (int i = 0; i < 100; i++) {
  msg.append("hello yinjihuan");
}
channel.writeandflush(msg);

源码参考:

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如您对本文有疑问或者有任何想说的,请 点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网