使用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());
                    }));

  

posted @ 2020-10-26 15:52  坚持坚持  阅读(2528)  评论(2编辑  收藏  举报