MQTT协议代码实现详解二

           MQTT的每个消息都有一个可变头结构,但这个可变头的结构标准定可以说定得非常糟糕的....在但在某个消息里扩展自己的可变头属性都会导致无法可以标准的协议兼容1f212b3df24d5d51f90af85b608e2884.png。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<?>();

如果接口上要明确泛型那有些集合规范就无法定义了1ab6cddd37ba3b3f1f33320d5d3afa4a.png!有了接口的规范就可以实现具体的属性类型。

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

关注公众号

b0baf45cd80897a3f0c3a08156b54333.jpeg

7a63d94b2fcd22ba9c70dca009d7daaa.png

https://github.com/beetlex-io/
http://beetlex-io.com

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MQTT(Message Queuing Telemetry Transport)是一种轻量级的消息传输协议,常用于物联网设备之间的通信。在Java中,可以使用Eclipse Paho库来实现MQTT协议代码。 以下是一个简单的Java代码示例,演示了如何使用Eclipse Paho库实现MQTT协议的发布和订阅功能: 1. 首先,需要导入Eclipse Paho库的相关依赖。可以在Maven项目中添加以下依赖项: ```xml <dependency> <groupId>org.eclipse.paho</groupId> <artifactId>org.eclipse.paho.client.mqttv3</artifactId> <version>1.2.5</version> </dependency> ``` 2. 发布消息的代码示例: ```java import org.eclipse.paho.client.mqttv3.MqttClient; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; public class MqttPublisher { public static void main(String[] args) { String broker = "tcp://mqtt.eclipse.org:1883"; String topic = "test/topic"; String message = "Hello, MQTT!"; try { MqttClient client = new MqttClient(broker, MqttClient.generateClientId()); client.connect(); MqttMessage mqttMessage = new MqttMessage(message.getBytes()); client.publish(topic, mqttMessage); client.disconnect(); } catch (MqttException e) { e.printStackTrace(); } } } ``` 3. 订阅消息的代码示例: ```java import org.eclipse.paho.client.mqttv3.MqttCallback; import org.eclipse.paho.client.mqttv3.MqttClient; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; public class MqttSubscriber { public static void main(String[] args) { String broker = "tcp://mqtt.eclipse.org:1883"; String topic = "test/topic"; try { MqttClient client = new MqttClient(broker, MqttClient.generateClientId(), new MemoryPersistence()); client.connect(); client.setCallback(new MqttCallback() { @Override public void connectionLost(Throwable cause) { System.out.println("Connection lost!"); } @Override public void messageArrived(String topic, MqttMessage message) throws Exception { System.out.println("Received message: " + new String(message.getPayload())); } @Override public void deliveryComplete(IMqttDeliveryToken token) { // Not used in this example } }); client.subscribe(topic); } catch (MqttException e) { e.printStackTrace(); } } } ``` 以上代码示例中,发布者使用`MqttClient`类连接到指定的MQTT代理(broker),并通过`publish`方法发布消息到指定的主题(topic)上。订阅者使用`MqttClient`类连接到MQTT代理,并通过`subscribe`方法订阅指定的主题。当有新消息到达时,通过设置的回调函数(`MqttCallback`)进行处理。 注意:以上示例中使用的MQTT代理是公共可用的测试代理,仅供演示目的使用。在实际应用中,需要根据实际情况配置和使用自己的MQTT代理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值