MQTT的每个消息都有一个可变头结构,但这个可变头的结构标准定可以说定得非常糟糕的....在但在某个消息里扩展自己的可变头属性都会导致无法可以标准的协议兼容。5.0版本引入了一个属性集来解决用户扩展定义属性的问题,虽然标准定义的规范表;但由于属性集是基于K-V结构,因此自己添加一些标准以外的属性都可以得到兼容。
参考文档
中文:vitsumoc.github.io/mqtt-v5-0-chinese.html
英文: docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html
编程语言: C#
完整代码: //github.com/beetlex-io/mqtt
属性表规范
Identifier | Name (usage) | Type | Packet / Will Properties | |
Dec | Hex | |||
1 | 0x01 | Payload Format Indicator | Byte | PUBLISH, Will Properties |
2 | 0x02 | Message Expiry Interval | Four Byte Integer | PUBLISH, Will Properties |
3 | 0x03 | Content Type | UTF-8 Encoded String | PUBLISH, Will Properties |
8 | 0x08 | Response Topic | UTF-8 Encoded String | PUBLISH, Will Properties |
9 | 0x09 | Correlation Data | Binary Data | PUBLISH, Will Properties |
11 | 0x0B | Subscription Identifier | Variable Byte Integer | PUBLISH, SUBSCRIBE |
17 | 0x11 | Session Expiry Interval | Four Byte Integer | CONNECT, CONNACK, DISCONNECT |
18 | 0x12 | Assigned Client Identifier | UTF-8 Encoded String | CONNACK |
19 | 0x13 | Server Keep Alive | Two Byte Integer | CONNACK |
21 | 0x15 | Authentication Method | UTF-8 Encoded String | CONNECT, CONNACK, AUTH |
22 | 0x16 | Authentication Data | Binary Data | CONNECT, CONNACK, AUTH |
23 | 0x17 | Request Problem Information | Byte | CONNECT |
24 | 0x18 | Will Delay Interval | Four Byte Integer | Will Properties |
25 | 0x19 | Request Response Information | Byte | CONNECT |
26 | 0x1A | Response Information | UTF-8 Encoded String | CONNACK |
28 | 0x1C | Server Reference | UTF-8 Encoded String | CONNACK, DISCONNECT |
31 | 0x1F | Reason String | UTF-8 Encoded String | CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBACK, UNSUBACK, DISCONNECT, AUTH |
33 | 0x21 | Receive Maximum | Two Byte Integer | CONNECT, CONNACK |
34 | 0x22 | Topic Alias Maximum | Two Byte Integer | CONNECT, CONNACK |
35 | 0x23 | Topic Alias | Two Byte Integer | PUBLISH |
36 | 0x24 | Maximum QoS | Byte | CONNACK |
37 | 0x25 | Retain Available | Byte | CONNACK |
38 | 0x26 | User Property | UTF-8 String Pair | CONNECT, CONNACK, PUBLISH, Will Properties, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, DISCONNECT, AUTH |
39 | 0x27 | Maximum Packet Size | Four Byte Integer | CONNECT, CONNACK |
40 | 0x28 | Wildcard Subscription Available | Byte | CONNACK |
41 | 0x29 | Subscription Identifier Available | Byte | CONNACK |
42 | 0x2A | Shared Subscription Available | Byte | CONNACK |
属性ID占用一个字节,每个属性都有专门的类型对应;由于一个字节可以存储255种情况而表中只有42个,因此有足够的空间可以让用户定义自己的属性类型。在这里有个属性是比较特别的,那就是38的UserProperty,它内部是一个K-V结构,所以在消息传递时是可以存在多个UserProperty项的。
属性结构定义
由于每种属性的都有着不同的值类型,因此只是简单地定义这样一个结构显然是不可能的;为了规范这些不同类型属性的数据转换需要定义以
下接口:
public interface IHeaderProperty
{
HeaderType Type { get; }
void Read(MQTTParse mqttParse, System.IO.Stream stream);
void Write(MQTTParse mqttParse, System.IO.Stream stream);
}
public interface IHeaderPropertyExpend<T> : IHeaderProperty
{
T Value { get; set; }
}
可以有同学会问为什么不只定义一个接口,如果定义一个泛型接口那这种情况就无法更好使用了
protected List<IHeaderProperty> mWriteProperties = new List<IHeaderProperty>();
protected List<?> mWriteProperties = new List<?>();
如果接口上要明确泛型那有些集合规范就无法定义了!有了接口的规范就可以实现具体的属性类型。
public class ContentType : IHeaderPropertyExpend<string>
{
public string Value { get; set; }
public HeaderType Type => HeaderType.ContentType;
public void Read(MQTTParse mqttParse, Stream stream)
{
Value = mqttParse.ReadString(stream);
}
public void Write(MQTTParse mqttParse, Stream stream)
{
mqttParse.WriteString(stream, Value);
}
public static implicit operator ContentType(PropertyStream b) => b.To<ContentType>(HeaderType.ContentType);
}
public class MessageExpiryInterval : IHeaderPropertyExpend<int>
{
public int Value { get; set; }
public HeaderType Type => HeaderType.MessageExpiryInterval;
public void Read(MQTTParse mqttParse, Stream stream)
{
Value = mqttParse.ReadInt(stream);
}
public void Write(MQTTParse mqttParse, Stream stream)
{
mqttParse.WriteInt(stream, Value);
}
public static implicit operator MessageExpiryInterval(PropertyStream b) => b.To<MessageExpiryInterval>(HeaderType.MessageExpiryInterval);
}
public class UserProperty : IHeaderPropertyExpend<string>
{
public string Value { get; set; }
public string Name { get; set; }
public HeaderType Type => HeaderType.UserProperty;
public void Read(MQTTParse mqttParse, Stream stream)
{
Name = mqttParse.ReadString(stream);
Value = mqttParse.ReadString(stream);
}
public void Write(MQTTParse mqttParse, Stream stream)
{
mqttParse.WriteString(stream, Name);
mqttParse.WriteString(stream, Value);
}
public static implicit operator UserProperty(PropertyStream b) => b.To<UserProperty>(HeaderType.UserProperty);
}
每个属性都重载了一个类型转换,后面在使用的时候就会方便很多。
PropertyStream
每个消息都带有一个可变数量的属性集,该结构也是通过一个可变长度整数来定义其内容。文档描述如下:属性长度是一个变长整数。长度的值不包括自己所占用的字节数,其长度是所有属性占用的字节数量。如果没有属性,必须通过一个 0 值的属性长度来明确表示。有了以上规范就可以实现这个Stream了
public class PropertyStream : System.IO.MemoryStream
{
private Dictionary<HeaderType, object> mReadProperties = new Dictionary<HeaderType, object>();
public List<UserProperty> UserProperties { get; set; }
protected List<IHeaderProperty> mWriteProperties = new List<IHeaderProperty>();
public void Refresh()
{
mReadProperties.Clear();
mWriteProperties.Clear();
UserProperties = null;
SetLength(0);
}
public T To<T>(HeaderType type)
where T : IHeaderProperty
{
mReadProperties.TryGetValue(type, out var property);
return (T)property;
}
public PropertyStream Add(IHeaderProperty property)
{
if (property != null)
mWriteProperties.Add(property);
return this;
}
public void Read(MQTTParse parse, Stream stream, Action<IHeaderProperty> handler = null)
{
var len = parse.Int7BitHandler.Read(stream);
if (len > 0)
{
SetLength(0);
parse.CopyStream(stream, this, (int)len);
this.Position = 0;
while (this.Position < this.Length)
{
HeaderType htype = (HeaderType)this.ReadByte();
var property = HeaderFactory.GetHeader(htype);
property.Read(parse, this);
handler?.Invoke(property);
if (property is UserProperty)
{
if (UserProperties == null)
UserProperties = new List<UserProperty>();
UserProperties.Add((UserProperty)property);
}
else
{
mReadProperties.Add(property.Type, property);
}
}
}
}
public void Write(MQTTParse parse, Stream stream)
{
foreach (var item in mWriteProperties)
{
WriteByte((byte)item.Type);
item.Write(parse, this);
}
parse.Int7BitHandler.Write(stream, (int)Length);
this.Position = 0;
CopyTo(stream);
}
public static PropertyStream operator +(PropertyStream stream, Tuple<IHeaderProperty, IHeaderProperty, IHeaderProperty> properties)
{
stream.Add(properties.Item1).Add(properties.Item2).Add(properties.Item3);
return stream;
}
public static PropertyStream operator +(PropertyStream stream, Tuple<IHeaderProperty, IHeaderProperty> properties)
{
stream.Add(properties.Item1).Add(properties.Item2);
return stream;
}
public static PropertyStream operator +(PropertyStream stream, IHeaderProperty property) => stream.Add(property);
public static PropertyStream operator +(PropertyStream stream, IEnumerable<IHeaderProperty> properties)
{
if (properties != null)
foreach (var item in properties)
{
stream.Add(item);
}
return stream;
}
public static implicit operator List<UserProperty>(PropertyStream d) => d.UserProperties;
}
由于是一个内存的读写块,因此从MemoryStream派生下来;考虑到对象复用实现Refresh方法重置应该对象的一些属性。同样这个类也重载了运算符方便操作,有了这些重载代码编写就相对简单很多
protected override void OnRead(MQTTParse parse, Stream stream, ISession session)
{
base.OnRead(parse, stream, session);
SessionFlag = (byte)stream.ReadByte();
Status = (ReturnType)stream.ReadByte();
var ps = GetPropertiesStream();
ps.Read(parse, stream);
SessionExpiryInterval = ps;
ReceiveMaximum = ps;
MaximumQoS = ps;
RetainAvailable = ps;
AssignedClientIdentifier = ps;
TopicAliasMaximum = ps;
ReasonString = ps;
UserProperties = ps;
WildcardSubscriptionAvailable = ps;
SubscriptionIdentifierAvailable = ps;
SharedSubscriptionAvailable = ps;
ServerKeepAlive = ps;
ResponseInformation = ps;
ServerReference = ps;
AuthenticationData = ps;
}
protected override void OnWrite(MQTTParse parse, Stream stream, ISession session)
{
base.OnWrite(parse, stream, session);
stream.WriteByte(SessionFlag);
stream.WriteByte((byte)Status);
var ps = GetPropertiesStream()
+ SessionExpiryInterval
+ ReceiveMaximum
+ MaximumQoS
+ RetainAvailable
+ AssignedClientIdentifier
+ TopicAliasMaximum
+ ReasonString
+ WildcardSubscriptionAvailable
+ SubscriptionIdentifierAvailable
+ SharedSubscriptionAvailable
+ ServerKeepAlive
+ ResponseInformation
+ ServerReference
+ AuthenticationData
+ UserProperties;
ps.Write(parse, stream);
}
通过运算符重载就可以让数据流和对象之间做个自动转换,后续相关代码编写起来也方便多了。
到这里协议解释的基础封装都完成了,后面的章节就使用这些基础功能封装具体的消息了。
BeetleX
开源跨平台通讯框架(支持TLS)
提供HTTP,Websocket,MQTT,Redis,RPC和服务网关开源组件
个人微信:henryfan128 QQ:28304340
关注公众号
https://github.com/beetlex-io/
http://beetlex-io.com