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