DotNetty 版 mqtt 开源客户端 (MqttFx)
一、DotNetty背景介绍
某天发现 dotnet 是个好东西,就找了个项目来练练手。于是有了本文的 Mqtt 客户端 (github: MqttFx )
DotNetty是微软的Azure团队,使用C#实现的Netty的版本发布。不但使用了C#和.Net平台的技术特点,并且保留了Netty原来绝大部分的编程接口。让我们在使用时,完全可以依照Netty官方的教程来学习和使用DotNetty应用程序。
DotNetty同时也是开源的,它的源代码托管在Github上: https://github.com/azure/dotnetty
Netty 的官方文档 : http://netty.io/wiki/all-documents.html
二、Packet
套件里是有个 DotNetty.Codecs.Mqtt, 本项目没有使用。直接写了一个。
FixedHeader: 固定报头
/// <summary> /// 固定报头 /// </summary> public class FixedHeader { /// <summary> /// 报文类型 /// </summary> public PacketType PacketType { get; set; } /// <summary> /// 重发标志 /// </summary> public bool Dup { get; set; } /// <summary> /// 服务质量等级 /// </summary> public MqttQos Qos { get; set; } /// <summary> /// 保留标志 /// </summary> public bool Retain { get; set; } /// <summary> /// 剩余长度 /// </summary> public int RemaingLength { internal get; set; } public FixedHeader(PacketType packetType) { PacketType = packetType; } public FixedHeader(byte signature, int remainingLength) { PacketType = (PacketType)((signature & 0xf0) >> 4); Dup = ((signature & 0x08) >> 3) > 0; Qos = (MqttQos)((signature & 0x06) >> 1); Retain = (signature & 0x01) > 0; RemaingLength = remainingLength; } public void WriteTo(IByteBuffer buffer) { var flags = (byte)PacketType << 4; flags |= Dup.ToByte() << 3; flags |= (byte)Qos << 1; flags |= Retain.ToByte(); buffer.WriteByte((byte)flags); buffer.WriteBytes(EncodeLength(RemaingLength)); } static byte[] EncodeLength(int length) { var result = new List<byte>(); do { var digit = (byte)(length % 0x80); length /= 0x80; if (length > 0) digit |= 0x80; result.Add(digit); } while (length > 0); return result.ToArray(); } }
Packet: 消息基类
/// <summary> /// 消息基类 /// </summary> public abstract class Packet { #region FixedHeader /// <summary> /// 固定报头 /// </summary> public FixedHeader FixedHeader { protected get; set; } /// <summary> /// 报文类型 /// </summary> public PacketType PacketType => FixedHeader.PacketType; /// <summary> /// 重发标志 /// </summary> public bool Dup => FixedHeader.Dup; /// <summary> /// 服务质量等级 /// </summary> public MqttQos Qos => FixedHeader.Qos; /// <summary> /// 保留标志 /// </summary> public bool Retain => FixedHeader.Retain; /// <summary> /// 剩余长度 /// </summary> public int RemaingLength => FixedHeader.RemaingLength; #endregion public Packet(PacketType packetType) => FixedHeader = new FixedHeader(packetType); public virtual void Encode(IByteBuffer buffer) { } public virtual void Decode(IByteBuffer buffer) { } }
PacketWithId: 消息基类(带ID)
/// <summary> /// 消息基类(带ID) /// </summary> public abstract class PacketWithId : Packet { public PacketWithId(PacketType packetType) : base(packetType) { } /// <summary> /// 报文标识符 /// </summary> public ushort PacketId { get; set; } /// <summary> /// EncodePacketIdVariableHeader /// </summary> /// <param name="buffer"></param> public override void Encode(IByteBuffer buffer) { var buf = Unpooled.Buffer(); try { EncodePacketId(buf); FixedHeader.RemaingLength = buf.ReadableBytes; FixedHeader.WriteTo(buffer); buffer.WriteBytes(buf); buf = null; } finally { buf?.Release(); } } /// <summary> /// DecodePacketIdVariableHeader /// </summary> /// <param name="buffer"></param> public override void Decode(IByteBuffer buffer) { int remainingLength = RemaingLength; DecodePacketId(buffer, ref remainingLength); FixedHeader.RemaingLength = remainingLength; } protected void EncodePacketId(IByteBuffer buffer) { if (Qos > MqttQos.AtMostOnce) { buffer.WriteUnsignedShort(PacketId); } } protected void DecodePacketId(IByteBuffer buffer, ref int remainingLength) { if (Qos > MqttQos.AtMostOnce) { PacketId = buffer.ReadUnsignedShort(ref remainingLength); if (PacketId == 0) throw new DecoderException("[MQTT-2.3.1-1]"); } } }
ConnectPacket: 发起连接包
/// <summary> /// 发起连接 /// </summary> internal sealed class ConnectPacket : Packet { public ConnectPacket() : base(PacketType.CONNECT) { } #region Variable header /// <summary> /// 协议名 /// </summary> public string ProtocolName { get; } = "MQTT"; /// <summary> /// 协议级别 /// </summary> public byte ProtocolLevel { get; } = 0x04; /// <summary> /// 保持连接 /// </summary> public short KeepAlive { get; set; } #region Connect Flags /// <summary> /// 用户名标志 /// </summary> public bool UsernameFlag { get; set; } /// <summary> /// 密码标志 /// </summary> public bool PasswordFlag { get; set; } /// <summary> /// 遗嘱保留 /// </summary> public bool WillRetain { get; set; } /// <summary> /// 遗嘱QoS /// </summary> public MqttQos WillQos { get; set; } /// <summary> /// 遗嘱标志 /// </summary> public bool WillFlag { get; set; } /// <summary> /// 清理会话 /// </summary> public bool CleanSession { get; set; } #endregion #endregion #region Payload /// <summary> /// 客户端标识符 Client Identifier /// </summary> public string ClientId { get; set; } /// <summary> /// 遗嘱主题 Will Topic /// </summary> public string WillTopic { get; set; } /// <summary> /// 遗嘱消息 Will Message /// </summary> public byte[] WillMessage { get; set; } /// <summary> /// 用户名 User Name /// </summary> public string UserName { get; set; } /// <summary> /// 密码 Password /// </summary> public string Password { get; set; } #endregion public override void Encode(IByteBuffer buffer) { var buf = Unpooled.Buffer(); try { //variable header buf.WriteString(ProtocolName); //byte 1 - 8 buf.WriteByte(ProtocolLevel); //byte 9 //connect flags; //byte 10 var flags = UsernameFlag.ToByte() << 7; flags |= PasswordFlag.ToByte() << 6; flags |= WillRetain.ToByte() << 5; flags |= ((byte)WillQos) << 3; flags |= WillFlag.ToByte() << 2; flags |= CleanSession.ToByte() << 1; buf.WriteByte((byte)flags); //keep alive buf.WriteShort(KeepAlive); //byte 11 - 12 //payload buf.WriteString(ClientId); if (WillFlag) { buf.WriteString(WillTopic); buf.WriteBytes(WillMessage); } if (UsernameFlag && PasswordFlag) { buf.WriteString(UserName); buf.WriteString(Password); } FixedHeader.RemaingLength = buf.ReadableBytes; FixedHeader.WriteTo(buffer); buffer.WriteBytes(buf); } finally { buf?.Release(); buf = null; } } }
连接回执: ConnAckPacket
/// <summary> /// 连接回执 /// </summary> internal sealed class ConnAckPacket : Packet { public ConnAckPacket() : base (PacketType.CONNACK) { } /// <summary> /// 当前会话 /// </summary> public bool SessionPresent { get; set; } /// <summary> /// 连接返回码 /// </summary> public ConnectReturnCode ConnectReturnCode { get; set; } public override void Decode(IByteBuffer buffer) { SessionPresent = (buffer.ReadByte() & 0x01) == 1; ConnectReturnCode = (ConnectReturnCode)buffer.ReadByte(); } }
剩余几个包,,大家看看源码。
三、包解码编码 MqttDecoder MqttEncoder
粘包拆包问题是处于网络比较底层的问题,在数据链路层、网络层以及传输层都有可能发生。我们日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生这个问题。
什么是粘包、拆包?
对于什么是粘包、拆包问题,我想先举两个简单的应用场景:
-
客户端和服务器建立一个连接,客户端发送一条消息,客户端关闭与服务端的连接。
-
客户端和服务器简历一个连接,客户端连续发送两条消息,客户端关闭与服务端的连接。
对于第一种情况,服务端的处理流程可以是这样的:当客户端与服务端的连接建立成功之后,服务端不断读取客户端发送过来的数据,当客户端与服务端连接断开之后,服务端知道已经读完了一条消息,然后进行解码和后续处理...。对于第二种情况,如果按照上面相同的处理逻辑来处理,那就有问题了,我们来看看第二种情况下客户端发送的两条消息递交到服务端有可能出现的情况:
第一种情况:
服务端一共读到两个数据包,第一个包包含客户端发出的第一条消息的完整信息,第二个包包含客户端发出的第二条消息,那这种情况比较好处理,服务器只需要简单的从网络缓冲区去读就好了,第一次读到第一条消息的完整信息,消费完再从网络缓冲区将第二条完整消息读出来消费。
没有发生粘包、拆包示意图
第二种情况:
服务端一共就读到一个数据包,这个数据包包含客户端发出的两条消息的完整信息,这个时候基于之前逻辑实现的服务端就蒙了,因为服务端不知道第一条消息从哪儿结束和第二条消息从哪儿开始,这种情况其实是发生了TCP粘包。
TCP粘包示意图
第三种情况:
服务端一共收到了两个数据包,第一个数据包只包含了第一条消息的一部分,第一条消息的后半部分和第二条消息都在第二个数据包中,或者是第一个数据包包含了第一条消息的完整信息和第二条消息的一部分信息,第二个数据包包含了第二条消息的剩下部分,这种情况其实是发送了TCP拆,因为发生了一条消息被拆分在两个包里面发送了,同样上面的服务器逻辑对于这种情况是不好处理的。
TCP拆包示意图
为什么会发生TCP粘包、拆包呢?
发生TCP粘包、拆包主要是由于下面一些原因:
-
应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。
-
应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。
-
进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包。
-
接收方法不及时读取套接字缓冲区数据,这将发生粘包。
-
……
如何处理粘包、拆包问题?
知道了粘包、拆包问题及根源,那么如何处理粘包、拆包问题呢?TCP本身是面向流的,作为网络服务器,如何从这源源不断涌来的数据流中拆分出或者合并出有意义的信息呢?通常会有以下一些常用的方法:
-
使用带消息头的协议、消息头存储消息开始标识及消息长度信息,服务端获取消息头的时候解析出消息长度,然后向后读取该长度的内容。
-
设置定长消息,服务端每次读取既定长度的内容作为一条完整消息。
-
设置消息边界,服务端从网络流中按消息编辑分离出消息内容。
-
……
如何基于DotNetty处理粘包、拆包问题?
ChannelPipeline 网络层数据的流向
ChannelHandler 组件对网络数据的处理
-
ByteToMessageDecoder
-
MessageToMessageDecoder
这两个组件都实现了ChannelInboundHandler接口,这说明这两个组件都是用来解码网络上过来的数据的。而他们的顺序一般是ByteToMessageDecoder位于head channel handler的后面,MessageToMessageDecoder位于ByteToMessageDecoder的后面。DotNetty中,涉及到粘包、拆包的逻辑主要在ByteToMessageDecoder及其实现中。
ByteToMessageDecoder
顾名思义、ByteToMessageDecoder是用来将从网络缓冲区读取的字节转换成有意义的消息对象的
当上面一个channel handler传入的ByteBuf有数据的时候,这里我们可以把in参数看成网络流,这里有不断的数据流入,而我们要做的就是从这个byte流中分离出message,然后把message添加给out。分开将一下代码逻辑:
-
当out中有Message的时候,直接将out中的内容交给后面的channel handler去处理。
-
当用户逻辑把当前channel handler移除的时候,立即停止对网络数据的处理。
-
记录当前in中可读字节数。
-
decode是抽象方法,交给子类具体实现。
-
同样判断当前channel handler移除的时候,立即停止对网络数据的处理。
-
如果子类实现没有分理出任何message的时候,且子类实现也没有动bytebuf中的数据的时候,这里直接跳出,等待后续有数据来了再进行处理。
-
如果子类实现没有分理出任何message的时候,且子类实现动了bytebuf中的数据,则继续循环,直到解析出message或者不在对bytebuf中数据进行处理为止。
-
如果子类实现解析出了message但是又没有动bytebuf中的数据,那么是有问题的,抛出异常。
-
如果标志位只解码一次,则退出。
可以知道,如果要实现具有处理粘包、拆包功能的子类,及decode实现,必须要遵守上面的规则,我们以实现处理第一部分的第二种粘包情况和第三种情况拆包情况的服务器逻辑来举例:
对于粘包情况的decode需要实现的逻辑对应于将客户端发送的两条消息都解析出来分为两个message加入out,这样的话callDecode只需要调用一次decode即可。
对于拆包情况的decode需要实现的逻辑主要对应于处理第一个数据包的时候第一次调用decode的时候out的size不变,从continue跳出并且由于不满足继续可读而退出循环,处理第二个数据包的时候,对于decode的调用将会产生两个message放入out,其中两次进入callDecode上下文中的数据流将会合并为一个bytebuf和当前channel handler实例关联,两次处理完毕即清空这个bytebuf。
MqttDecoder : Mqtt 解码器
public sealed class MqttDecoder : ByteToMessageDecoder { readonly bool _isServer; readonly int _maxMessageSize; public MqttDecoder(bool isServer, int maxMessageSize) { _isServer = isServer; _maxMessageSize = maxMessageSize; } protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List<object> output) { try { if (!TryDecodePacket(context, input, out Packet packet)) return; output.Add(packet); } catch (DecoderException) { input.SkipBytes(input.ReadableBytes); throw; } } bool TryDecodePacket(IChannelHandlerContext context, IByteBuffer buffer, out Packet packet) { if (!buffer.IsReadable(2)) { packet = null; return false; } byte signature = buffer.ReadByte(); if (!TryDecodeRemainingLength(buffer, out int remainingLength) || !buffer.IsReadable(remainingLength)) { packet = null; return false; } //DecodePacketInternal var fixedHeader = new FixedHeader(signature, remainingLength); switch (fixedHeader.PacketType) { case PacketType.CONNECT: packet = new ConnectPacket(); break; case PacketType.CONNACK: packet = new ConnAckPacket(); break; case PacketType.DISCONNECT: packet = new DisconnectPacket(); break; case PacketType.PINGREQ: packet = new PingReqPacket(); break; case PacketType.PINGRESP: packet = new PingRespPacket(); break; case PacketType.PUBACK: packet = new PubAckPacket(); break; case PacketType.PUBCOMP: packet = new PubCompPacket(); break; case PacketType.PUBLISH: packet = new PublishPacket(); break; case PacketType.PUBREC: packet = new PubRecPacket(); break; case PacketType.PUBREL: packet = new PubRelPacket(); break; case PacketType.SUBSCRIBE: packet = new SubscribePacket(); break; case PacketType.SUBACK: packet = new SubAckPacket(); break; case PacketType.UNSUBSCRIBE: packet = new UnsubscribePacket(); break; case PacketType.UNSUBACK: packet = new UnsubscribePacket(); break; default: throw new DecoderException("Unsupported Message Type"); } packet.FixedHeader = fixedHeader; packet.Decode(buffer); //if (remainingLength > 0) // throw new DecoderException($"Declared remaining length is bigger than packet data size by {remainingLength}."); return true; } bool TryDecodeRemainingLength(IByteBuffer buffer, out int value) { int readable = buffer.ReadableBytes; int result = 0; int multiplier = 1; byte digit; int read = 0; do { if (readable < read + 1) { value = default; return false; } digit = buffer.ReadByte(); result += (digit & 0x7f) * multiplier; multiplier <<= 7; read++; } while ((digit & 0x80) != 0 && read < 4); if (read == 4 && (digit & 0x80) != 0) throw new DecoderException("Remaining length exceeds 4 bytes in length"); int completeMessageSize = result + 1 + read; if (completeMessageSize > _maxMessageSize) throw new DecoderException("Message is too big: " + completeMessageSize); value = result; return true; } //static int DecodeRemainingLength(IByteBuffer buffer) //{ // byte encodedByte; // var multiplier = 1; // var remainingLength = 0; // do // { // encodedByte = buffer.ReadByte(); // remainingLength += (encodedByte & 0x7f) * multiplier; // multiplier *= 0x80; // } while ((encodedByte & 0x80) != 0); // return remainingLength; //} }
MqttEncoder: mqtt 编码器
public sealed class MqttEncoder : MessageToMessageEncoder<Packet> { public static readonly MqttEncoder Instance = new MqttEncoder(); protected override void Encode(IChannelHandlerContext context, Packet message, List<object> output) => DoEncode(context.Allocator, message, output); public static void DoEncode(IByteBufferAllocator bufferAllocator, Packet packet, List<object> output) { IByteBuffer buffer = bufferAllocator.Buffer(); try { packet.Encode(buffer); output.Add(buffer); buffer = null; } finally { buffer?.SafeRelease(); } } }
未完待续。。。