netty如何解决拆包、粘包?

一、何为拆包、粘包

1.TCP协议

拆包和粘包问题得从TCP协议说起了。

TCP协议是一个面向字节流得协议,它得性质是流式的,并没有分段。就像水流一样,你无法知道它什么时候开始,什么时候结束。

所以它会根据当前的套接字缓冲区的情况进行拆包和粘包。

下面展示一个TCP协议的传输过程

发送方 ——->向发送端缓冲区写入数据 —->TCP(缓冲区) ——> 发送TCP报文段 ——-> TCP(缓冲区) ——> 从接收缓冲端读取数据

发送端的字节流都会先写入缓冲区,然后再通过网络传到接收端缓冲区,最终由接收端读取数据。

当我们发送两个完整的报文到接收端时,正常情况下,接收端会收到两个完整的报文。

但是,也会出现下面情况:

  • 接收端只接收到一个报文,它由发送的两个报文组成(粘包
  • 第一个报文中只有正常的一部分,第二个报文中有正常第一个报文剩余的部分和第二个报文的整个(拆包
  • 第一个报文中有正常的第一个报文的整个和第二个报文的部分,第二个报文只有剩余的部分(拆包

对于上面的拆包和粘包我们只能将其放在应用层来处理了

常见的处理方式有:

  • 在每次发送报文的末尾添加一个换行符或者其它特殊符号,这样在接收端通过这个换行符或者特殊符号来判断报文是否完整,如何不完整则缓存起来和下一个报文拼接
  • 规定好报文的长度,不足的空位补齐,接收端按照指定长度截取数据

二、netty中处理拆包、粘包案例

定义netty服务端

  1. public static void main(String[] args) {
  2. EventLoopGroup bossGroup=new NioEventLoopGroup();
  3. EventLoopGroup workGroup=new NioEventLoopGroup();
  4. try {
  5. ServerBootstrap serverBootstrap=new ServerBootstrap();
  6. serverBootstrap.group(bossGroup,workGroup)
  7. .channel(NioServerSocketChannel.class)
  8. // 服务端自定义处理器
  9. .childHandler(new TestServerInitializer());
  10. ChannelFuture channelFuture = serverBootstrap.bind(8989).sync();
  11. channelFuture.channel().closeFuture().sync();
  12. }catch (Exception e){
  13. e.printStackTrace();
  14. }finally {
  15. bossGroup.shutdownGracefully();
  16. workGroup.shutdownGracefully();
  17. }
  18. }

服务端自定义处理器

  1. public class TestServerInitializer extends ChannelInitializer<SocketChannel> {
  2. @Override
  3. protected void initChannel(SocketChannel socketChannel) throws Exception {
  4. ChannelPipeline pipeline = socketChannel.pipeline();
  5. /**
  6. * 1) lengthFieldOffset //长度字段的偏差
  7. * 2) lengthFieldLength //长度字段占的字节数
  8. * 3) lengthAdjustment //添加到长度字段的补偿值
  9. * 4) initialBytesToStrip //从解码帧中第一次去除的字节数
  10. */
  11. pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4));
  12. //计算当前待发送消息的二进制字节长度,将该长度添加到ByteBuf的缓冲区头中
  13. pipeline.addLast(new LengthFieldPrepender(4));
  14. pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
  15. pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
  16. pipeline.addLast(new TestServerHandler());
  17. }
  18. }

定义客户端

  1. EventLoopGroup bossGroup=new NioEventLoopGroup();
  2. try {
  3. Bootstrap bootstrap=new Bootstrap();
  4. bootstrap.group(bossGroup)
  5. .channel(NioSocketChannel.class)
  6. // 客户端自定义处理器
  7. .handler(new TestClientInitializer());
  8. ChannelFuture channelFuture = bootstrap.connect("127.0.0.1",8989).sync();
  9. channelFuture.channel().closeFuture().sync();
  10. }catch (Exception e){
  11. e.printStackTrace();
  12. }finally {
  13. bossGroup.shutdownGracefully();
  14. }

客户端自定义处理器

  1. public class TestClientInitializer extends ChannelInitializer<SocketChannel>{
  2. @Override
  3. protected void initChannel(SocketChannel socketChannel) throws Exception {
  4. ChannelPipeline pipeline = socketChannel.pipeline();
  5. /**
  6. * 1) lengthFieldOffset //长度字段的偏差
  7. * 2) lengthFieldLength //长度字段占的字节数
  8. * 3) lengthAdjustment //添加到长度字段的补偿值
  9. * 4) initialBytesToStrip //从解码帧中第一次去除的字节数
  10. */
  11. pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4));
  12. //计算当前待发送消息的二进制字节长度,将该长度添加到ByteBuf的缓冲区头中
  13. pipeline.addLast(new LengthFieldPrepender(4));
  14. //将byte数据解码成String
  15. pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
  16. //将字符串编码成byte数据
  17. pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
  18. pipeline.addLast(new TestClientHandler());
  19. }
  20. }

上面的例子是基于长度的解码器,可以按照指定长度截取

netty中常见的解码器有:

  • DelimiterBasedFrameDecoder 基于分割符的解码器
  • LengthFieldBasedFrameDecoder 基于长度的解码器