zoukankan      html  css  js  c++  java
  • 使用Dotnetty解决粘包问题

    一,为什么TCP会有粘包和拆包的问题

    粘包:TCP发送方发送多个数据包,接收方收到数据时这几个数据包粘成了一个包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾,接收方必需根据协议将这几个数据包分离出来才能得到正确的数据。

     为什么会发生粘包,从几个方面来看:

    1,TCP是基于字节流的,TCP的报文没有规划数据长度,发送端和接收端从缓存中取数据,应用程序对于消息的长度是不可见的,不知道数据流应该从什么地方开始,从什么地方结束。

    2,发送方:TCP默认启用了Negal算法,优化数据流的发送效率,Nagle算法主要做两件事:1)只有上一个分组得到确认,才会发送下一个分组;2)收集多个小分组,在一个确认到来时一起发送。

    3,接收方:TCP将收到的分组保存至接收缓存里,然后应用程序主动从缓存里读收。这样一来,如果TCP接收的速度大于应用程序读的速度,多个包就会被存至缓存,应用程序读时,就会读到多个首尾相接粘到一起的包。

    二,Dotnetty项目

    Dotnetty监听端口,启用管道处理器

    public class NettyServer
        {
            public async System.Threading.Tasks.Task RunAsync(int[] port)
            {
                IEventLoopGroup bossEventLoop = new MultithreadEventLoopGroup(port.Length);
                IEventLoopGroup workerLoopGroup = new MultithreadEventLoopGroup();
                try
                {
                    ServerBootstrap boot = new ServerBootstrap();
                    boot.Group(bossEventLoop, workerLoopGroup)
                        .Channel<TcpServerSocketChannel>()
                        .Option(ChannelOption.SoBacklog, 100)
                        .ChildOption(ChannelOption.SoKeepalive, true)
                        .Handler(new LoggingHandler("netty server"))
                        .ChildHandler(new ActionChannelInitializer<IChannel>(channel => {
                            IPEndPoint ip = (IPEndPoint)channel.LocalAddress;
                            Console.WriteLine(ip.Port);
                            channel.Pipeline.AddLast(new NettyServerHandler());
                        }));
                    List<IChannel> list = new List<IChannel>();
                    foreach(var item in port)
                    {
                        IChannel boundChannel = await boot.BindAsync(item);
                        list.Add(boundChannel);
                    }
                    Console.WriteLine("按任意键退出");
                    Console.ReadLine();
                    list.ForEach(r =>
                    {
                        r.CloseAsync();
                    });
                    //await boundChannel.CloseAsync();
                }
                catch(Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
                finally
                {
                    await bossEventLoop.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1));
                    await workerLoopGroup.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1));
                }
            }
        }
    

     管理处理器,简单打印一下收到的内容

     public class NettyServerHandler: ChannelHandlerAdapter
        {
            public override void ChannelRead(IChannelHandlerContext context, object message)
            {
                if (message is IByteBuffer buffer)
                {
                    StringBuilder sb = new StringBuilder();
                    while (buffer.IsReadable())
                    {
                        sb.Append(buffer.ReadByte().ToString("X2"));
                    }
                    Console.WriteLine($"RECIVED FROM CLIENT : {sb.ToString()}");
                }
            }
            public override void ExceptionCaught(IChannelHandlerContext context, Exception exception)
            {
                Console.WriteLine("Exception: " + exception);
                context.CloseAsync();
            }
        }
    

      入口方法启动服务,监听四个端口

    static void Main(string[] args)
            {
                new NettyServer().RunAsync(new int[] { 9001 ,9002,9003,9004}).Wait();
            }

    二,在应用层解决TCP的粘包问题

    要在应用层解决粘包问题,只需要让程序知道数据流什么时候开始,什么时候结束。按常用协议规划,可以有以下四种方案:

    1,定长协议解码。每次发送的数据包长度为固定的,这种协议最简单,但没有灵活性。

    粘包处理:根据固定的长度处理。如协议长度为4,发送方发送数据包 FF 00 00 FF 00 FF 00 AA会被解析成二包:FF 00 00 FF及 FF 00 00 AA

    拆包处理:根据固定的长度处理。如协议长度为4,发送方发送二个数据包 FF 00, 00 FF 00 FF 00 AA会被解析成二包:FF 00 00 FF及 FF 00 00 AA

    Dotnetty实现:监听9001端口,处理室长协议数据包

    新建一个管理处理器,当缓存中的可读长度达到4,处理数据包,管道向下执行,否则不处理。

      public class KsLengthfixedHandler : ByteToMessageDecoder
        {
            protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List<object> output)
            {
                if (input.ReadableBytes < 12) {
                    return; // (3)
                }
                output.Add(input.ReadBytes(12)); // (4)
            }
        }
    

    在启动服务器是根据端口号加入不同的管道处理器

    .ChildHandler(new ActionChannelInitializer<IChannel>(channel => {
                            IPEndPoint ip = (IPEndPoint)channel.LocalAddress;
                            Console.WriteLine(ip.Port);
                            if (ip.Port == 9001)
                            {
                                channel.Pipeline.AddLast(new KsLengthfixedHandler());
                            }
                            channel.Pipeline.AddLast(new NettyServerHandler());
    }

    2,行解码,即每个数据包的结尾都是/r/n或者/n(换成16进制为0d0a)。  

    粘包处理:根据换行符做分隔处理。如发送方发送一包数据 FF 00 00 FF 0D 0A FF 00 00 AA 0D 0A则将会解析成两包:FF 00 00 FF以及FF 00 00 AA

    拆包处理:根据换行符做分隔处理。如发送方发送二包数据 FF 00以及00 FF 0D 0A FF 00 00 AA 0D 0A则将会解析成两包:FF 00 00 FF以及FF 00 00 AA

    Dotnetty实现:监听9002端口,处理行结尾协议数据包

    .ChildHandler(new ActionChannelInitializer<IChannel>(channel => {
                            IPEndPoint ip = (IPEndPoint)channel.LocalAddress;
                            Console.WriteLine(ip.Port);
                            if (ip.Port == 9001)
                            {
                                channel.Pipeline.AddLast(new KsLengthfixedHandler());
                            } 
                            else if (ip.Port == 9002)
                            {
                                channel.Pipeline.AddLast(new LineBasedFrameDecoder(
                                    maxLength: 1024, //可接收数据包最大长度
                                    stripDelimiter: true, //解码后的数据包是否去掉分隔符
                                    failFast: false //是否读取超过最大长度的数据包内容
                                    ));
                            }
                            channel.Pipeline.AddLast(new NettyServerHandler());
    }

    3,特定的分隔符解码。与行解码规定了以换行符做分隔符不同,这个解码方案可以自己定义分隔符。

    粘包处理:根据自定义的分隔符做分隔处理。如发送方发送一包数据 FF 00 00 FF 0D 0A FF 00 00 AA 0D 0A则将会解析成两包:FF 00 00 FF以及FF 00 00 AA

    拆包处理:根据自定义的分隔符做分隔处理。如发送方发送二包数据 FF 00以及00 FF 0D 0A FF 00 00 AA 0D 0A则将会解析成两包:FF 00 00 FF以及FF 00 00 AA

    下面的实例是自定义了一个以“}”为分隔符的解码器

    Dotnetty实现:监听9003端口,处理自定义分隔符协议数据包

    .ChildHandler(new ActionChannelInitializer<IChannel>(channel => {
                            IPEndPoint ip = (IPEndPoint)channel.LocalAddress;
                            Console.WriteLine(ip.Port);
                            if (ip.Port == 9001)
                            {
                                channel.Pipeline.AddLast(new KsLengthfixedHandler());
                            } 
                            else if (ip.Port == 9002)
                            {
                                channel.Pipeline.AddLast(new LineBasedFrameDecoder(
                                    maxLength: 1024, //可接收数据包最大长度
                                    stripDelimiter: true, //解码后的数据包是否去掉分隔符
                                    failFast: false //是否读取超过最大长度的数据包内容
                                    ));
                            }
                            else if (ip.Port == 9003)
                            {
                                IByteBuffer delimiter = Unpooled.CopiedBuffer(Encoding.UTF8.GetBytes("}"));
                                channel.Pipeline.AddLast(new DotNetty.Codecs.DelimiterBasedFrameDecoder(
                                   maxFrameLength: 1024,
                                   stripDelimiter: true,
                                   failFast: false, 
                                  delimiter:  delimiter));
                            }
                            channel.Pipeline.AddLast(new NettyServerHandler());
    }

    4,指定长度标识。与第一种方案的固定长度不同,这种方案可以指定一个位置存放该数据包的长度,以便程序推算出开始及结束位置。Modbus协议是典型的变长协议。

    Dotnetty中使用LengthFieldBasedFrameDecoder解码器对变长协议解析,了解下以下几个参数:

      * +------+--------+------+----------------+
      * | HDR1 | Length | HDR2 | Actual Content |
      * | 0xCA | 0x000C | 0xFE | "HELLO, WORLD" |
      * +------+--------+------+----------------+   

    1,maxFrameLength:数据包最大长度

    2,lengthFieldOffset:长度标识的偏移量。如上面的协议,lengthFieldOffset为1(HDR1的长度)

    3,lengthFieldLength:长度标识位的长度。如上面的协议,lengthFieldLength为2(Length的长度)

    4,lengthAdjustment:调整长度。如上面的协议,0x000c转为10进制为12,只标识了Content的长度,并不包括HDR2的长度,在解析时就要设置该默值为HDR2的长度。

    5,initialBytesToStrip:从何处开始剥离。如上面的协议,如果想要的解析结果为:0xFE  "HELLO, WORLD" 则将initialBytesToStrip设置为3(HDR1的长度+Length的长度)。

    Dotnetty实现:监听9004端口处理变长协议

      .ChildHandler(new ActionChannelInitializer<IChannel>(channel => {
                            IPEndPoint ip = (IPEndPoint)channel.LocalAddress;
                            Console.WriteLine(ip.Port);
                            if (ip.Port == 9001)
                            {
                                channel.Pipeline.AddLast(new KsLengthfixedHandler());
                            } 
                            else if (ip.Port == 9002)
                            {
                                channel.Pipeline.AddLast(new LineBasedFrameDecoder(
                                    maxLength: 1024, //可接收数据包最大长度
                                    stripDelimiter: true, //解码后的数据包是否去掉分隔符
                                    failFast: false //是否读取超过最大长度的数据包内容
                                    ));
                            }
                            else if (ip.Port == 9003)
                            {
                                IByteBuffer delimiter = Unpooled.CopiedBuffer(Encoding.UTF8.GetBytes("}"));
                                channel.Pipeline.AddLast(new DotNetty.Codecs.DelimiterBasedFrameDecoder(
                                   maxFrameLength: 1024,
                                   stripDelimiter: true,
                                   failFast: false, 
                                  delimiter:  delimiter));
                            }
                            else if (ip.Port == 9004)
                            {
                                channel.Pipeline.AddLast(new LengthFieldBasedFrameDecoder(
                                   maxFrameLength: 1024,
                                   lengthFieldOffset: 1, 
                                   lengthFieldLength: 2));
                            }
                            channel.Pipeline.AddLast(new NettyServerHandler());
                        }));
    

      

  • 相关阅读:
    java多线程
    java垃圾回收
    java研发常见问题总结 1
    js获取时间加多山天和时间戳转换成日期
    php时间选择器亲测可以自己修改
    html5时间选择器
    php生成员工编号,产品编号
    桌面远程链接
    SQL 左外连接查询 将右表中的多行变为左表的一列或多列
    PHPMailer发匿名邮件及Extension missing: openssl的解决
  • 原文地址:https://www.cnblogs.com/liujiabing/p/13878885.html
Copyright © 2011-2022 走看看