MQTT协议代码实现详解一

        为了打发空闲时间会对之前写的MQTT协议的代码实现进行详细讲解;主要是讲解的内容是对MQTT网络通讯协议的分析和实现,具体涉及对网络数据流拆分处理,因此很多内容都涉及到具体的代码功能,如果你有兴趣学习这方面的那应该是有一定帮助的。接来就讲解BeetleX.MQTT.Protocols的设计和具体实现细节(对于MQTT协议是5.0版本)。
参考文档 
        中文: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

什么是MQTT
        MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的“轻量级”通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布(最新版本协议于2019年定稿的5.0)。

基础类型
        通讯协议是对数据流规范的定义,根据具体规范进行数据拆分和读取。每个被拆分成一个完整消息的可以称作为消息包,而消息包则有多个数据成员组件,而这些数据成员则是协议的基础类型。而在MQTT协议操作过程中涉及的类型如下:

  • 比特
            网络和存储的基础单位是字节,但一个字节包含了8个比特,即可以表达8布尔状态属性。在网络协议设计上为了减少传输带宽往往都会用到比特来定义一些状态类型,对于读写这种类型往往通过以 & | <<和>>这几个操作完成。

bool tag = (0b0000_0001 & data) > 0;//读取0位是否为1
data|=0b0000_0001 //设置第0位为1
  • 字节
            在协议中字节往往用来表自定义的数据类型或状态,如MQTT中ConnAck指令的响应状态码:

Value

Hex

Reason Code name

Description

0

0x00

Success

The Connection is accepted.

128

0x80

Unspecified error

The Server does not wish to reveal the reason for the failure, or none of the other Reason Codes apply.

129

0x81

Malformed Packet

Data within the CONNECT packet could not be correctly parsed.

130

0x82

Protocol Error

Data in the CONNECT packet does not conform to this specification.

131

0x83

Implementation specific error

The CONNECT is valid but is not accepted by this Server.

132

0x84

Unsupported Protocol Version

The Server does not support the version of the MQTT protocol requested by the Client.

  • 变长整型
            在程序中整型的存储是4字节,不管值的大小都是固定4字节。为了让传输更节省带宽设计出可变长存储方式,根据数值的大小存储空间为1-5个字节,由于传输消息包都不会很大所以在绝大多数情况下存储的消息长度不会占用超过4字节。以下是针对可变长度读写的帮助类:

public class Int7bit
{
    [ThreadStatic]
    private static Byte[] mDataBuffer;
    public void Write(System.IO.Stream stream, int value)
    {
        if (mDataBuffer == null)
            mDataBuffer = new byte[32];
        var count = 0;
        var num = (UInt64)value;
        while (num >= 0x80)
        {
            mDataBuffer[count++] = (Byte)(num | 0x80);
            num >>= 7;
        }
        mDataBuffer[count++] = (Byte)num;
        stream.Write(mDataBuffer, 0, count);
    }
    private uint mResult = 0;
    private byte mBits = 0;
    public int? Read(System.IO.Stream stream)
    {
        Byte b;
        while (true)
        {
            if (stream.Length < 1)
                return null;
            var bt = stream.ReadByte();
            if (bt < 0)
            {
                mBits = 0;
                mResult = 0;
                throw new BXException("Read 7bit int error:byte value cannot be less than zero!");
            }
            b = (Byte)bt;
            mResult |= (UInt32)((b & 0x7f) << mBits);
            if ((b & 0x80) == 0) break;
            mBits += 7;
            if (mBits >= 32)
            {
                mBits = 0;
                mResult = 0;
                throw new BXException("Read 7bit int error:out of maximum value!");
            }
        }
        mBits = 0;
        var result = mResult;
        mResult = 0;
        return (Int32)result;
    }
}
  • 整型
            协议中有两种数值类型,分别是短整型和整型;存储大小分别是2字节和4字节。协议对于整型存储方式是高字序,而C#默认是低字序所以在处理上要进行简单的转换。

public static short SwapInt16(short v)
{
    return (short)(((v & 0xFF) << 8) | ((v >> 8) & 0xFF));
}
public static ushort SwapUInt16(ushort v)
{
    return (ushort)((uint)((v & 0xFF) << 8) | ((uint)(v >> 8) & 0xFFu));
}


public static int SwapInt32(int v)
{
    return ((SwapInt16((short)v) & 0xFFFF) << 16) | (SwapInt16((short)(v >> 16)) & 0xFFFF);
}


public static uint SwapUInt32(uint v)
{
    return (uint)((SwapUInt16((ushort)v) & 0xFFFF) << 16) | (SwapUInt16((ushort)(v >> 16)) & 0xFFFFu);
}

通过以上函数就可以进行对应存储字序的转换了,在读写流的时候加入这个函数调用即可。

public virtual void WriteUInt16(System.IO.Stream stream, ushort value)
{
    value = BitHelper.SwapUInt16(value);
    var data = BitConverter.GetBytes(value);
    stream.Write(data, 0, data.Length);
}


public virtual ushort ReadUInt16(System.IO.Stream stream)
{
    var buffer = GetIntBuffer();
    stream.Read(buffer, 0, 2);
    var result = BitConverter.ToUInt16(buffer, 0);
    return BitHelper.SwapUInt16(result);
}


public virtual void WriteInt16(System.IO.Stream stream, short value)
{
    value = BitHelper.SwapInt16(value);
    var data = BitConverter.GetBytes(value);
    stream.Write(data, 0, data.Length);
}


public virtual short ReadInt16(System.IO.Stream stream)
{
    var buffer = GetIntBuffer();
    stream.Read(buffer, 0, 2);
    var result = BitConverter.ToInt16(buffer, 0);
    return BitHelper.SwapInt16(result);
}
  • 字符
            Utf8的字符类型,这个类型是由一个2字节的整型头部描述字符的长度。

public virtual void WriteString(System.IO.Stream stream, string value, Encoding encoding = null)
{
    if (encoding == null)
        encoding = Encoding.UTF8;
    if (string.IsNullOrEmpty(value))
    {
        WriteUInt16(stream, 0);
        return;
    }
    byte[] buffer = System.Buffers.ArrayPool<byte>.Shared.Rent(value.Length * 6);
    var count = encoding.GetBytes(value, 0, value.Length, buffer, 0);
    WriteUInt16(stream, (ushort)count);
    stream.Write(buffer, 0, count);
    System.Buffers.ArrayPool<byte>.Shared.Return(buffer);


}


public virtual string ReadString(System.IO.Stream stream, Encoding encoding = null)
{
    if (encoding == null)
        encoding = Encoding.UTF8;
    UInt16 len = ReadUInt16(stream);
    if (len == 0)
        return string.Empty;
    byte[] buffer = System.Buffers.ArrayPool<byte>.Shared.Rent(len);
    stream.Read(buffer, 0, len);
    string result = encoding.GetString(buffer, 0, len);
    System.Buffers.ArrayPool<byte>.Shared.Return(buffer);
    return result;
}
  • 二进制
            类型为字节数组,该类型是由一个2字节的整型头部描述数组的长度

public void WriteBinary(System.IO.Stream stream, byte[] data)
{
    WriteUInt16(stream, (ushort)data.Length);
    stream.Write(data, 0, data.Length);


}


public byte[] ReadBinary(System.IO.Stream stream)
{
    var len = ReadUInt16(stream);
    byte[] result = new byte[len];
    stream.Read(result, 0, len);
    return result;
}

以上是MQTT的协议类型描述和对应的实现操作,接下来就可以实现具体的协议了。

拆分消息包
        在编写代码前先了解一下MQTT的消息包是如何拆分的,以下是协议定义的格式

Bit

7

6

5

4

3

2

1

0

byte 1

MQTT Control Packet type

Flags specific to each MQTT Control Packet type

byte 2…

Remaining Length

协议把第一个字节拆分成两部分,第一部分的4比特用于描述消息相关的控制类型,不同消息包有不同的情况具体查看协议文档;第二部分是高4位比特用于描述消息类型。从第二个字节开始是一个变长整型用于描述消息体的剩下的数据长度。
        其实从实现性能上来说我不认同这种协议设计,可变长度虽然可以节省2个节字的带宽(5.0协议是2019年定的,个人感觉现有的网络资源并不缺这点流量).....这种设计导致消息写入网络流存在一次内存拷贝,然而后面的扩展属性也是,这样一来导致一个消息的写入流就存在多次拷贝影响性能。下面来简单讲述这情况:

35806c91e8605e0d091e6ab13a2dbbf2.png

由于可变长度是根据消息内容来确定,导致无法在网络缓冲内存中分配一个固定的空间,因此只能在新的内存中写入内容得到长度后再把长度写入的网络内存块,然后再把对应的内容再复制到网络内存块中。如果长度是固定的如4字节,就可以在写入消息头后马上分配一个4字节的空间后直接写入具体的消息内容,当内容写入后得到的长再反填充之前分配的长度空间即可;这样的好处是消息内容可以直接写入到网络内存块中,无须用其他内存写入后再拷贝。

实现消息类型
        既然有了协议的基础规则,那就可以针对它来实现一个消息抽像类来表达数据消息了。

public abstract class MQTTMessage
    {


        public MQTTMessage()
        {


        }


        public abstract MQTTMessageType Type { get; }


        public byte Bit1 { get; set; }


        public byte Bit2 { get; set; }


        public byte Bit3 { get; set; }


        public byte Bit4 { get; set; }


        internal void Read(MQTTParse parse, System.IO.Stream stream, ISession session)
        {
            OnRead(parse, stream, session);
        }


        protected virtual void OnRead(MQTTParse parse, Stream stream, ISession session)
        {


        }


        internal void Write(MQTTParse parse, System.IO.Stream stream, ISession session)
        {
            OnWrite(parse, stream, session);
        }


        protected virtual void OnWrite(MQTTParse parse, Stream stream, ISession session)
        {


        }
}

在设计过程中预留OnRead和OnWrite方法给具体消息实现对应的网络数据读写;有了这个抽象的消息结构就可以对协议进行读写拆分了。

public override MQTTMessage Read(Stream stream, ISession session)
{
    IServer server = session?.Server;
    if (stream.Length > 0)
    {
        if (mType == null)
        {
            mHeader = (byte)stream.ReadByte();
            if (mHeader < 0)
            {
                throw new BXException("parse mqtt message error:fixed header byte value cannot be less than zero!");
            }
            mType = (MQTTMessageType)((mHeader & 0b1111_0000) >> 4);
            if (server != null && server.EnableLog(EventArgs.LogType.Debug))
                server.Log(EventArgs.LogType.Debug, session, "parse mqtt header souccess");
        }
        if (mLength == null)
        {
            mLength = mInt7BitHandler.Read(stream);
            if (server != null && server.EnableLog(EventArgs.LogType.Debug))
                server.Log(EventArgs.LogType.Debug, session, $"parse mqtt size {mLength}");
        }
        if (mLength != null && stream.Length >= mLength.Value)
        {
            Stream protocolStream = GetProtocolStream();
            CopyStream(stream, protocolStream, mLength.Value);
            MQTTMessage msg = CreateMessage(mType.Value, session);
            msg.Bit1 = (byte)(BIT_1 & mHeader);
            msg.Bit2 = (byte)(BIT_2 & mHeader);
            msg.Bit3 = (byte)(BIT_3 & mHeader);
            msg.Bit4 = (byte)(BIT_4 & mHeader);
            msg.Read(this, protocolStream, session);
            if (server != null && server.EnableLog(EventArgs.LogType.Debug))
                server.Log(EventArgs.LogType.Debug, session, $"parse mqtt type {msg} success");
            mLength = null;
            mType = null;
            return msg;
        }


    }
    return null;
}


public override void Write(MQTTMessage msg, Stream stream, ISession session)
{
    IServer server = session?.Server;
    if (server != null && server.EnableLog(EventArgs.LogType.Debug))
        server.Log(EventArgs.LogType.Debug, session, $"write mqtt message {msg}");
    var protocolStream = GetProtocolStream();
    int header = 0;
    header |= ((int)msg.Type << 4);
    header |= msg.Bit1;
    header |= msg.Bit2 << 1;
    header |= msg.Bit3 << 2;
    header |= msg.Bit4 << 3;
    stream.WriteByte((byte)header);
    msg.Write(this, protocolStream, session);
    if (server != null && server.EnableLog(EventArgs.LogType.Debug))
        server.Log(EventArgs.LogType.Debug, session, $"write mqtt message body size {protocolStream.Length}");
    mInt7BitHandler.Write(stream, (int)protocolStream.Length);
    protocolStream.Position = 0;
    protocolStream.CopyTo(stream);
    if (server != null && server.EnableLog(EventArgs.LogType.Debug))
        server.Log(EventArgs.LogType.Debug, session, $"write mqtt message success");
}

通过以上的Read和Write方法就可以把网络数据和程序对象进行一个转换了。
        到这里MQTT最上层的消息包解释处理就完成了,剩下不同消息的实现留给后面的章节再详细讲解。

 
 
BeetleX

开源跨平台通讯框架(支持TLS)

提供HTTP,Websocket,MQTT,Redis,RPC和服务网关开源组件

个人微信:henryfan128    QQ:28304340

关注公众号

297fc21dc41ef31a940846583ca6f64e.jpeg

d70f23acc64e55c5c60694c457180f61.png

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
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、付费专栏及课程。

余额充值