C#应用:MQTT分析——CONNECT为例子

 源代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Sockets;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Connect();
        }
        /// <summary>
        /// 向服务器端发送请求,使用socket连接请求
        /// </summary>
        static void Connect()
        {
            Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            socket.Connect("127.0.0.1", 1883);//TCP三次握手
            //发送MQTT的登录请求
            //固定报头
            //第一个字节8个位,(消息类型4个位,DUP位1个位,Qos2个位,Retain1个位)
            //请求,根据固定报文控制类型,第一个请求为 00010000,表示从客户端到服务器
            List<byte> list = new List<byte>();
            list.Add(0x10);

            //===========可变报头+载荷部分==============
            List<byte> list2 = new List<byte>();
            //可变报头由以下组成:协议名(MQTT)+协议级别+连接标志+保持连接
            string protocolName = "MQTT";
            byte[] probyet = Encoding.UTF8.GetBytes(protocolName);//MQTT对应的ascii码的十六进制
            //协议长度
            list2.Add((byte)(probyet.Length / 256 % 256));
            list2.Add((byte)(probyet.Length % 256));
            list2.AddRange(probyet);//协议名称

            list2.Add(0x04);//协议级别,只能是0x04
            //连接标志 一个字节(8位)
            //11000010
            byte flag = 0; //0000 0000
            flag |= 128;   //1000 0000
            //flag |=128 ==> 1000 0000或运算
            flag |= 64;    //0100 0000
            //flag |=64 ==>//1100 0000或运算
            flag |= 2;     //0000 0010
            //flag |=2  ==>//1100 0010
            list2.Add(flag);

            //keep alive保持连接
            int second = 100; //保持100秒,持续到150秒
            list2.Add((byte)(second / 256 % 256));
            list2.Add((byte)(second % 256));

            //有效载荷(客户端标识符,遗嘱主题,遗嘱消息,用户名,密码)
            string clientStr = "JTVL";
            byte[] idBytes = Encoding.UTF8.GetBytes(clientStr); //ID的字节数组
            int idBytesLen = idBytes.Length; //ID字节数组的长度
            list2.Add((byte)(idBytes.Length / 256 % 256));
            list2.Add((byte)(idBytesLen % 256));
            list2.AddRange(idBytes); //添加ID字节数组

            //UserName
            string userName = "JT";
            byte[] nameByte = Encoding.UTF8.GetBytes(userName);
            list2.Add((byte)(nameByte.Length / 256 % 256));
            list2.Add((byte)(nameByte.Length % 256));
            list2.AddRange(nameByte);
            //Password
            string passWord = "123";
            byte[] passByte = Encoding.UTF8.GetBytes(passWord);
            list2.Add((byte)(passByte.Length / 256 % 256));
            list2.Add((byte)(passByte.Length  % 256));
            list2.AddRange(passByte);

            //把可变报头+载荷部分加到固定报头的后面
            //添加固定报头的第二个字节
            list.Add((byte)(list2.Count));
            //添加
            list.AddRange(list2);

            //发送连接请求报文
            socket.Send(list.ToArray());

        }
    }
}

一、知识基础

1、C#的byte类型

在C#中,byte类型表示一个8位无符号整数(也称为字节)。由于它是无符号的,它的值范围是0到255,即 1111 1111

2、BitConverter.GetBytes

BitConverter.GetBytes(int) 的返回值为 byte数组 ,但是创建的数组容量为4个,如果不够会依次增加4个,返回的顺序是顺序。即:如果是 256 ,则为 1 0000 0000 ,也就是第二个数组的值为1,第一个数组为0 ,而第三和第四为0;如果是 1 ,则为 0000 0001 ,第二第三第四值为0。

二、按照步骤分析

 mqtt 报文结构 = 固定报头 + 可变报头 + 有效载荷

1、固定报头

(1)MQTT报文类型:

(2)标志位:

(3)剩余长度:

包括可变报头和负载的数据,变长度编码方案,每个字节可以编码 128 个数值和一个延续位,剩余长度字段最大 4 个字节,注意最高位为 1 表示后面至少还有一个字节,这允许应用发送最大 256MB(268,435,455) 大小的控制报文。这个数值在报文中的表示是:0xFF,0xFF,0xFF,0x7F

总结写法:

CONNECT报文类型值为1,标志位全0,而剩余长度是可变报头+负载,所以在最后才写。

因此创建一个byte类型的数组a

第一个字节 0001 0000

第二个字节是剩余长度,先不写,最后再写

2、可变报头

 可变报头需要十个字节

(1)第一个字节(MSB)为字符串长度最高有效字节数        

(2)第二个字节(LSB)为字符串长度最低有效字节数

因为目前主流的CPU都是大端序(BigEndian),MSB和LSB两个字节组成的big int 表示协议名称的长度,即’MQTT’ 为4字节长度。

(3)第三到第六个字节,分别存放 'M' 'Q' 'T' 'T' 的ASCII码十六进制

(4)第七个字节表示协议级别, 此处固定为 0x04。(表示v3.1.1)

(5)第八个字节表示连接标志,用1、0表示是否开启。

  • 第0位为保留位,固定为0。
  • 第1位为会话清理标志, 1表示每次建立连接,要求服务端重新开启会话;0则表示服务端会话持久化。一般选用1。
  • 第2位为遗嘱标志位,1表示启用遗嘱功能,0则表示关闭遗嘱功能。启用时,连接标志中的Will Qos(第3、4位),Will Retain (第5位)会被使用,有效载荷中必须包含 遗嘱topic(Will Topic)、遗嘱消息(Will Message)字段;未启用时,连接标志中的Will Qos(第3、4位),Will Retain (第5位)必须为0,。一般选用0。
  • 第4位、第3位用来表示遗嘱Qos,遗嘱标志位关闭时,强制填00,遗嘱标志位启用时,可以为00(qos0)、01(qos1)、10(qos2)。
  • 第5位为遗嘱保留标志位,1表示启用遗嘱保留,0表示关闭遗嘱保留。 遗嘱标志位关闭时,强制为0。
  • 第6位为密码标志位,1表示启用密码,有效载荷中必须包含密码字段;0表示不启用密码,有效载荷中不能包含密码字段。正常都传密码。
  • 第7位为用户名标志位,1表示启用用户名,有效载荷中必须包含用户名字段;0表示不启用用户名,有效载荷中不能包含用户名字段。正常都传用户名。如果不启用用户名时,密码也不能启用。

(6)byte9、byte10两个字节以MSB+LSB表示心跳间隔,单位为秒。

客户端按照心跳间隔发送PINGREQ,服务端回PINGRESP,服务端在1.5倍心跳间隔没有收到PINGREQ时,将以keepalive timeout的理由断开连接。 

总结写法:

再创建一个byte类型的数组b

第一个字节 0000 0000

第二个字节 0000 0100 因为MQTT是四个字节

ASCII码是十进制

第三个字节 'M' 的ASCII码为 77,0100 1101

第四个字节 'Q' 的ASCII码为 81,0101 0001

第五个字节 'T' 的ASCII码为 84,0101 0100

第六个字节  'T' 的ASCII码为 84,0101 0100

第七个字节 0000 0100

第八个字节 需要用户名、密码、会话清理,1100 0010

第九个字节 没走MSB,0000 0000

第十个字节 心跳保活时间,这里我需要100秒,0110 0100

3、有效载荷

CONNECT报文的有效载荷(payload)包含一个或多个以长度为前缀的字段,可变报头中的标志决定是否包含这些字段。

如果包含的话,必须按这个顺序出现:客户端标识符,遗嘱主题,遗嘱消息,用户名,密码。

需要注意的是每一个字段的拼接方式都遵循 MSB+LSB+Content的格式。

即在字段内容前拼入两个字节表示的大端序bit int来表示字段的长度,Content内容必须是UTF-8格式。

(1)客户端标识符

服务端使用客户端标识符 (ClientId) 识别客户端。连接服务端的每个客户端都有唯一的客户端标识符(ClientId)。

客户端标识符 (ClientId) 必须存在而且必须是 CONNECT 报文有效载荷的第一个字段

服务端可以允许客户端提供一个零字节的客户端标识符,这样服务端会认为是特殊情况自动分配一个且唯一,那样必须将同时将清理会话标志设置为 1

(2)遗嘱主题

如果可变报头连接标志部分遗嘱标志被设置为 1,则有效载荷的下一个字段是遗嘱主题(Will Topic)

(3)遗嘱消息

如果可变报头连接标志部分遗嘱标志被设置为 1,有效载荷的下一个字段是遗嘱消息

(4)用户名

如果可变报头连接标志部分用户名(User Name)标志被设置为 1,有效载荷的下一个字段就是它

(5)密码

如果可变报头连接标志部分密码(Password)标志被设置为 1,有效载荷的下一个字段就是它

总结写法:

创建第三个数组c

客户端标识符 "Jtvl" 4个字符 

byte1 0000 0000

byte2 0000 0100

byte3 'J' 转ASCII为74 0100 1010

byte4 'T' 转ASCII为84 0101 0100

byte5 'V' 转ASCII为86 0101 0110

byte6 'L' 转ASCII为76 0100 1100

创建第四个数组d

用户名 "jt"

byte1 0000 0000

byte2 0000 0010

byte3 'J' 转ASCII为74 0100 1010

byte4 'T' 转ASCII为84 0101 0100

创建第五个数组e

密码 "123"

byte1 0000 0000 

byte2 0000 0011

byte3 '1'转ASCII为49 0011 0001

byte4 '2'转ASCII为50 0011 0010

byte5 '3'转ASCII为51 0011 0011

4、最后的总结写法:

剩余长度是可变报头加有效荷载的长度

剩余长度10+6+4+5=25

固定报头的byte2 0001 1001

5、整理总结写法:

ASCII二进制十进制十六进制
固定报文byte10001 0000160x10
2byte20001 1001250x19
可变报头byte30000 000000x00
2byte40000 010040x04
3byte5M0100 1101770x4D
4byte6Q0101 0001810x51
5byte7T0101 0100840x54
6byte8T0101 0100840x54
7byte90000 010040x04
8byte101100 00101940xC2
9byte110000 000000x00
10byte120110 01001000x64
有效载荷byte130000 00000
byte140000 010040x04
byte15J0100 1010740x4A
byte16T0101 0100840x54
byte17V0101 0110860x56
byte18L0100 1100760x4C
用户名byte190000 000000x00
byte200000 001020x02
byte21J0100 1010740x4A
byte22T0101 0100840x54
密码byte230000 000000x00
byte240000 001130x03
byte2510011 0001490x31
byte2620011 0010500x32
byte2730011 0011510x33

6、思考代码写法:

            list2.Add((byte)(probyet.Length / 256 % 256));        //判断MSB
            list2.Add((byte)(probyet.Length % 256));                //判断LSB

字符串长度超过256的部分算是长度最高有效字节数

字符串小于256的部分算是长度最低有效字节数

                                                    //11000010
                                                    byte flag = 0;                 //0000 0000

128的二进制是1000 0000
                                                    flag |= 128;                    //1000 0000
64的二进制是0100 0000
                                                    flag |= 64;                      //1100 0000
2的二进制是0000 0010
                                                    flag |= 2;                        //1100 0010

通过异或将0000 0000慢慢变成 1100 0010 

 部分图片截选自: MQTT协议报文格式解析 - 知乎一、概述MQTT是一个客户端服务端架构的发布/订阅模式的消息传输协议。v3.1.1版本协议仅仅包含14个协议帧,它格式简单、规范且易于实现,非常适合物联网场景使用。 本文开始梳理MQTT协议的报文格式。官方的协议文档…icon-default.png?t=N7T8https://zhuanlan.zhihu.com/p/429891532

  • 24
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

走丢的男孩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值