李sir_Blog

博客园 首页 联系 订阅 管理
  705 随笔 :: 58 文章 :: 134 评论 :: 193万 阅读

MQTT协议中,一个MQTT数据包由:固定头(Fixed header)、 可变头(Variable header)、 消息体(payload)三部分构成。

 

MQTT 数据包结构

  • 固定头(Fixed header),存在于所有MQTT数据包中,表示数据包类型及数据包的分组类标识
  • 可变头(Variable header),存在于部分MQTT数据包中,数据包类型决定了可变头是否存在及其具体内容
  • 消息体(Payload),存在于部分MQTT数据包中,表示客户端收到的具体内容

 

MQTT固定头

固定头存在于所有MQTT数据包中,其结构如下:

Bit76543210
byte 1 MQTT数据包类型 不同类型MQTT数据包的具体标识
byte 2… 剩余长度

 

1.1 MQTT数据包类型

位置:byte 1, bits 7-4。

相于一个4位的无符号值,类型如下:

名称流方向描述
Reserved 0 不可用 保留位
CONNECT 1 客户端到服务器 客户端请求连接到服务器
CONNACK 2 服务器到客户端 连接确认
PUBLISH 3 双向 发布消息
PUBACK 4 双向 发布确认
PUBREC 5 双向 发布收到(保证第1部分到达)
PUBREL 6 双赂 发布释放(保证第2部分到达)
PUBCOMP 7 双向 发布完成(保证第3部分到达)
SUBSCRIBE 8 客户端到服务器 客户端请求订阅
SUBACK 9 服务器到客户端 订阅确认
UNSUBSCRIBE 10 客户端到服务器 请求取消订阅
UNSUBACK 11 服务器到客户端 取消订阅确认
PINGREQ 12 客户端到服务器 PING请求
PINGRESP 13 服务器到客户端 PING应答
DISCONNECT 14 客户端到服务器 中断连接
Reserved 15 不可用 保留位

 

1.2 标识位

位置:byte 1, bits 3-0。

在不使用标识位的消息类型中,标识位被做为保留位。如果收到无效的标志时,接收端必须关闭网络连接:

数据包标识位Bit 3Bit 2Bit 1Bit 0
CONNECT 保留位 0 0 0 0
CONNACK 保留位 0 0 0 0
PUBLISH MQTT 3.1.1使用 DUP1 QoS2 QoS2 RETAIN3
PUBACK 保留位 0 0 0 0
PUBREC 保留位 0 0 0 0
PUBREL 保留位 0 0 0 0
PUBCOMP 保留位 0 0 0 0
SUBSCRIBE 保留位 0 0 0 0
SUBACK 保留位 0 0 0 0
UNSUBSCRIBE 保留位 0 0 0 0
UNSUBACK 保留位 0 0 0 0
PINGREQ 保留位 0 0 0 0
PINGRESP 保留位 0 0 0 0
DISCONNECT 保留位 0 0 0 0
  • DUP:发布消息的副本。用来在保证消息的可靠传输,如果设置为 1,则在下面的变长中增加MessageId,并且需要回复确认,以保证消息传输完成,但不能用于检测消息重复发送。
  • QoS:发布消息的服务质量,即:保证消息传递的次数
    • 00:最多一次,即:<=1
    • 01:至少一次,即:>=1
    • 10:一次,即:=1
    • 11:预留
  • RETAIN: 发布保留标识,表示服务器要保留这次推送的信息,如果有新的订阅者出现,就把这消息推送给它,如果设有那么推送至当前订阅者后释放。

 

 

1.3 剩余长度(Remaining Length)

位置:byte 1。

固定头的第二字节用来保存变长头部和消息体的总大小的,但不是直接保存的。这一字节是可以扩展,其保存机制,前7位用于保存长度,后一部用做标识。当最后一位为 1时,表示长度不足,需要使用二个字节继续保存。 例如:计算出后面的大小为0

 

MQTT可变头

MQTT数据包中包含一个可变头,它驻位于固定的头和负载之间。可变头的内容因数据包类型而不同,较常的应用是做为包的标识:

Bit76543210
byte 1 包标签符(MSB)
byte 2… 包标签符(LSB)

很多类型数据包中都包括一个2字节的数据包标识字段,这些类型的包有:PUBLISH (QoS > 0)、PUBACK、PUBREC、PUBREL、PUBCOMP、SUBSCRIBE、SUBACK、UNSUBSCRIBE、UNSUBACK

 

Payload消息体

Payload消息体位MQTT数据包的第三部分,CONNECT、SUBSCRIBE、SUBACK、UNSUBSCRIBE四种类型的消息 有消息体:

    • CONNECT,消息体内容主要是:客户端的ClientID、订阅的Topic、Message以及用户名和密码。
    • SUBSCRIBE,消息体内容是一系列的要订阅的主题以及QoS
    • SUBACK,消息体内容是服务器对于SUBSCRIBE所申请的主题及QoS进行确认和回复。
    • UNSUBSCRIBE,消息体内容是要订阅的主题。

C# 具体应用举例:

一、安装MQTT库

C#中有多个MQTT库可供选择,例如M2Mqtt、MQTTnet等,本文以MQTTnet为例进行讲解。在Visual Studio中,可以使用NuGet包管理器安装MQTTnet库,或者通过命令行安装,具体如下:

使用NuGet包管理器安装:在Visual Studio中,右键单击项目,选择“管理NuGet程序包”,在搜索框中搜索MQTTnet,选择MQTTnet库进行安装。

使用命令行安装:在Visual Studio中,打开“工具”菜单,选择“NuGet包管理器”,选择“程序包管理器控制台”,在控制台中输入以下命令进行安装:

Install-Package MQTTnet

安装完成后,就可以在项目中使用MQTTnet库了。

二、连接MQTT服务器

在使用MQTT进行通信之前,需要先连接MQTT服务器。连接MQTT服务器需要指定MQTT服务器的地址、端口、客户端ID等信息。下面是一个示例代码,展示如何连接MQTT服务器:

复制代码
复制代码
 1 using MQTTnet;
 2 using MQTTnet.Client;
 3 using MQTTnet.Client.Options;
 4  
 5 var factory = new MqttFactory();
 6 var client = factory.CreateMqttClient();
 7 var options = new MqttClientOptionsBuilder()
 8     .WithTcpServer("localhost", 1883)
 9     .WithClientId("client1")
10     .Build();
11  
12 await client.ConnectAsync(options);
复制代码
复制代码

在上面的代码中,我们创建了一个MQTT客户端,并指定了MQTT服务器的地址和端口。其中,WithTcpServer方法指定了MQTT服务器的地址和端口,WithClientId方法指定了客户端ID。最后,使用await client.ConnectAsync(options)方法连接MQTT服务器。

三、发布MQTT消息

连接到MQTT服务器后,就可以开始发布消息了。使用MQTTnet库可以轻松地发布MQTT消息。下面是一个示例代码,展示如何发布MQTT消息:

复制代码
复制代码
1 var message = new MqttApplicationMessageBuilder()
2     .WithTopic("topic1")
3     .WithPayload("Hello MQTT")
4     .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce)
5     .WithRetainFlag(false)
6     .Build();
7  
8 await client.PublishAsync(message);
复制代码
复制代码

在上面的代码中,我们创建了一个MQTT消息,指定了消息的主题、负载、服务质量等信息,并使用await client.PublishAsync(message)方法发布消息。

四、订阅MQTT消息

订阅MQTT消息可以接收其他物联网设备发布的消息。使用MQTTnet库可以轻松地订阅MQTT消息。下面是一个示例代码,展示如何订阅MQTT消息:

复制代码
复制代码
 1 var mqttClient = new MqttFactory().CreateMqttClient();
 2  
 3 mqttClient.UseConnectedHandler(async e =>
 4 {
 5     Console.WriteLine("### CONNECTED WITH SERVER ###");
 6  
 7     // Subscribe to a topic
 8     await mqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("my/topic").Build());
 9  
10     Console.WriteLine("### SUBSCRIBED ###");
11 });
12  
13 mqttClient.UseDisconnectedHandler(async e =>
14 {
15     Console.WriteLine("### DISCONNECTED FROM SERVER ###");
16     await Task.Delay(TimeSpan.FromSeconds(5));
17  
18     try
19     {
20         await mqttClient.ConnectAsync(options.Build(), CancellationToken.None);
21     }
22     catch
23     {
24         Console.WriteLine("### RECONNECTING FAILED ###");
25     }
26 });
27  
28 mqttClient.UseApplicationMessageReceivedHandler(e =>
29 {
30     Console.WriteLine("### RECEIVED APPLICATION MESSAGE ###");
31     Console.WriteLine($"+ Topic = {e.ApplicationMessage.Topic}");
32     Console.WriteLine($"+ Payload = {Encoding.UTF8.GetString(e.ApplicationMessage.Payload)}");
33     Console.WriteLine($"+ QoS = {e.ApplicationMessage.QualityOfServiceLevel}");
34     Console.WriteLine($"+ Retain = {e.ApplicationMessage.Retain}");
35 });
36  
37 await mqttClient.ConnectAsync(options.Build(), CancellationToken.None);
复制代码
复制代码

在这个示例代码中,我们使用了UseConnectedHandler和UseDisconnectedHandler方法来处理连接成功和连接断开事件。在连接成功后,我们使用SubscribeAsync方法来订阅一个主题,并在控制台上输出一条订阅成功的信息。在消息处理方面,我们使用了UseApplicationMessageReceivedHandler方法来处理收到的消息,其中包括消息的主题、负载、QoS等信息。
以上就是如何使用MQTTnet库在C#中与其他物联网设备进行通信的方法,包括连接MQTT服务器、发布消息和订阅消息。使用MQTT协议可以轻松地实现物联网设备之间的通信,从而构建一个可靠的物联网系统。

 

1 什么是 MQTT ?
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是 IBM 开发的一个即时通讯协议,有可能成为物联网的重要组成部分。MQTT 是基于二进制消息的发布/订阅编程模式的消息协议,如今已经成为 OASIS 规范,由于规范很简单,非常适合需要低功耗和网络带宽有限的 IoT 场景。MQTT官网

2 MQTTnet
MQTTnet 是一个基于 MQTT 通信的高性能 .NET 开源库,它同时支持 MQTT 服务器端和客户端。而且作者也保持更新,目前支持新版的.NET core,这也是选择 MQTTnet 的原因。 MQTTnet 在 Github 并不是下载最多的 .NET 的 MQTT 开源库,其他的还 MqttDotNet、nMQTT、M2MQTT 等

MQTTnet is a high performance .NET library for MQTT based communication. It provides a MQTT client and a MQTT server (broker). The implementation is based on the documentation from http://mqtt.org/.

3 创建项目并导入类库
这里我们使用 Visual Studio 2017 创建一个空解决方案,并在其中添加两个项目,即一个服务端和一个客户端,服务端项目模板选择最新的 .NET Core 控制台应用,客户端项目选择传统的 WinForm 窗体应用程序。.NET Core 项目模板如下图所示:

在解决方案在右键单击-选择“管理解决方案的 NuGet 程序包”-在“浏览”选项卡下面搜索 MQTTnet,为服务端项目和客户端项目都安装上 MQTTnet 库,当前最新稳定版为 2.4.0。项目结构如下图所示:

4 服务端
MQTT 服务端主要用于与多个客户端保持连接,并处理客户端的发布和订阅等逻辑。一般很少直接从服务端发送消息给客户端(可以使用 mqttServer.Publish(appMsg); 直接发送消息),多数情况下服务端都是转发主题匹配的客户端消息,在系统中起到一个中介的作用。

4.1 创建服务端并启动
创建服务端最简单的方式是采用 MqttServerFactory 对象的 CreateMqttServer 方法来实现,该方法需要一个MqttServerOptions 参数。

var options = new MqttServerOptions();
var mqttServer = new MqttServerFactory().CreateMqttServer(options);

通过上述方式创建了一个 IMqttServer 对象后,调用其 StartAsync 方法即可启动 MQTT 服务。值得注意的是:之前版本采用的是 Start 方法,作者也是紧跟 C# 语言新特性,能使用异步的地方也都改为异步方式。

await mqttServer.StartAsync();

4.2 验证客户端

在 MqttServerOptions 选项中,你可以使用 ConnectionValidator 来对客户端连接进行验证。比如客户端ID标识 ClientId,用户名 Username 和密码 Password 等。

复制代码
复制代码
var options = new MqttServerOptions
{
ConnectionValidator = c =>
{
if (c.ClientId.Length < 10)
{
return MqttConnectReturnCode.ConnectionRefusedIdentifierRejected;
}

if (c.Username != "xxx" || c.Password != "xxx")
{
return MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword;
}

return MqttConnectReturnCode.ConnectionAccepted;
}
};
复制代码
复制代码

4.3 相关事件

服务端支持 ClientConnected、ClientDisconnected 和 ApplicationMessageReceived 事件,分别用来检查客户端连接、客户端断开以及接收客户端发来的消息。

其中 ClientConnected 和 ClientDisconnected 事件的事件参数一个客户端连接对象 ConnectedMqttClient,通过该对象可以获取客户端ID标识 ClientId 和 MQTT 版本 ProtocolVersion。

ApplicationMessageReceived 的事件参数包含了客户端ID标识 ClientId 和 MQTT 应用消息 MqttApplicationMessage 对象,通过该对象可以获取主题 Topic、QoS QualityOfServiceLevel 和消息内容 Payload 等信息。

5 客户端
MQTT 与 HTTP 不同,后者是基于请求/响应方式的,服务器端无法直接发送数据给客户端。而 MQTT 是基于发布/订阅模式的,所有的客户端均与服务端保持连接状态。

那么客户端之间是如何通信的呢?

具体逻辑是:某些客户端向服务端订阅它感兴趣(主题)的消息,另一些客户端向服务端发布(主题)消息,服务端将订阅和发布的主题进行匹配,并将消息转发给匹配通过的客户端。

5.1 创建客户端并连接
使用 MQTTnet 创建 MQTT 也非常简单,只需要使用 MqttClientFactory 对象的 CreateMqttClient 方法即可。

var mqttClient = new MqttClientFactory().CreateMqttClient();

创建客户端对象后,调用其异步方法 ConnectAsync 来连接到服务端。

await mqttClient.ConnectAsync(options);

调用该方法时需要传递一个 MqttClientTcpOptions 对象(之前的版本是在创建对象时使用该选项),该选项包含了客户端ID标识 ClientId、服务端地址(可以使用IP地址或域名)Server、端口号 Port、用户名 UserName、密码 Password 等信息。

复制代码
复制代码
var options = new MqttClientTcpOptions
{
    Server = "127.0.0.1",
    ClientId = "c001",
    UserName = "u001",
    Password = "p001",
    CleanSession = true
};
复制代码
复制代码

5.2 相关事件

客户端支持 Connected、Disconnected 和 ApplicationMessageReceived 事件,用来处理客户端与服务端连接、客户端从服务端断开以及客户端收到消息的事情。

5.2 订阅消息
客户端连接到服务端之后,可以使用 SubscribeAsync 异步方法订阅消息,该方法可以传入一个可枚举或可变参数的主题过滤器 TopicFilter 参数,主题过滤器包含主题名和 QoS 等级。

mqttClient.SubscribeAsync(new List<TopicFilter> {
    new TopicFilter("家/客厅/空调/#", MqttQualityOfServiceLevel.AtMostOnce)
});

5.3 发布消息

发布消息前需要先构建一个消息对象 MqttApplicationMessage,最直接的方法是使用其实构造函数,传入主题、内容、Qos 等参数。

mqttClient.SubscribeAsync(new List<TopicFilter> {
    new TopicFilter("家/客厅/空调/#", MqttQualityOfServiceLevel.AtMostOnce)
});

得到 MqttApplicationMessage 消息对象后,通过客户端对象调用其 PublishAsync 异步方法进行消息发布。

mqttClient.PublishAsync(appMsg);

6 跟踪消息

MQTTnet 提供了一个静态类 MqttNetTrace 来对消息进行跟踪,该类可用于服务端和客户端。MqttNetTrace 的事件TraceMessagePublished 用于跟踪服务端和客户端应用的日志消息,比如启动、停止、心跳、消息订阅和发布等。事件参数MqttNetTraceMessagePublishedEventArgs 包含了线程ID ThreadId、来源 Source、日志级别 Level、日志消息 Message、异常信息 Exception 等。

复制代码
复制代码
MqttNetTrace.TraceMessagePublished += MqttNetTrace_TraceMessagePublished;

private static void MqttNetTrace_TraceMessagePublished(object sender, MqttNetTraceMessagePublishedEventArgs e)
{
    Console.WriteLine($">> 线程ID:{e.ThreadId} 来源:{e.Source} 跟踪级别:        {e.Level} 消息: {e.Message}");

    if (e.Exception != null)
    {
        Console.WriteLine(e.Exception);
    }
}    
复制代码
复制代码

同时 MqttNetTrace 类还提供了4个不同消息等级的静态方法,Verbose、Information、Warning 和 Error,用于给出不同级别的日志消息,该消息将会在 TraceMessagePublished 事件中输出,你可以使用 e.Level 进行过虑。

7 运行效果
以下分别是服务端、客户端1和客户端2的运行效果,其中客户端1和客户端2只是同一个项目运行了两个实例。客户端1用于订阅传感器的“温度”数据,并模拟上位机(如 APP 等)发送开关控制命令;客户端2订阅上位机传来的“开关”控制命令,并模拟温度传感器上报温度数据。

7.1 服务端

7.2 客户端1

7.2 客户端2

8 Demo代码
8.1 服务端代码

复制代码
复制代码
using MQTTnet;
using MQTTnet.Core.Adapter;
using MQTTnet.Core.Diagnostics;
using MQTTnet.Core.Protocol;
using MQTTnet.Core.Server;
using System;
using System.Text;
using System.Threading;

namespace MqttServerTest
{
    class Program
    {
        private static MqttServer mqttServer = null;

        static void Main(string[] args)
        {
            MqttNetTrace.TraceMessagePublished += MqttNetTrace_TraceMessagePublished;
            new Thread(StartMqttServer).Start();

            while (true)
            {
                var inputString = Console.ReadLine().ToLower().Trim();

                if (inputString == "exit")
                {
                    mqttServer?.StopAsync();
                    Console.WriteLine("MQTT服务已停止!");
                    break;
                }
                else if (inputString == "clients")
                {
                    foreach (var item in mqttServer.GetConnectedClients())
                    {
                        Console.WriteLine($"客户端标识:{item.ClientId},协议版本:{item.ProtocolVersion}");
                    }
                }
                else
                {
                    Console.WriteLine($"命令[{inputString}]无效!");
                }
            }
        }

        private static void StartMqttServer()
        {
            if (mqttServer == null)
            {
                try
                {
                    var options = new MqttServerOptions
                    {
                        ConnectionValidator = p =>
                        {
                            if (p.ClientId == "c001")
                            {
                                if (p.Username != "u001" || p.Password != "p001")
                                {
                                    return MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword;
                                }
                            }

                            return MqttConnectReturnCode.ConnectionAccepted;
                        }
                    };

                    mqttServer = new MqttServerFactory().CreateMqttServer(options) as MqttServer;
                    mqttServer.ApplicationMessageReceived += MqttServer_ApplicationMessageReceived;
                    mqttServer.ClientConnected += MqttServer_ClientConnected;
                    mqttServer.ClientDisconnected += MqttServer_ClientDisconnected;
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    return;
                }
            }

            mqttServer.StartAsync();
            Console.WriteLine("MQTT服务启动成功!");
        }

        private static void MqttServer_ClientConnected(object sender, MqttClientConnectedEventArgs e)
        {
            Console.WriteLine($"客户端[{e.Client.ClientId}]已连接,协议版本:{e.Client.ProtocolVersion}");
        }

        private static void MqttServer_ClientDisconnected(object sender, MqttClientDisconnectedEventArgs e)
        {
            Console.WriteLine($"客户端[{e.Client.ClientId}]已断开连接!");
        }

        private static void MqttServer_ApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs e)
        {
            Console.WriteLine($"客户端[{e.ClientId}]>> 主题:{e.ApplicationMessage.Topic} 负荷:{Encoding.UTF8.GetString(e.ApplicationMessage.Payload)} Qos:{e.ApplicationMessage.QualityOfServiceLevel} 保留:{e.ApplicationMessage.Retain}");
        }

        private static void MqttNetTrace_TraceMessagePublished(object sender, MqttNetTraceMessagePublishedEventArgs e)
        {
            /*Console.WriteLine($">> 线程ID:{e.ThreadId} 来源:{e.Source} 跟踪级别:{e.Level} 消息: {e.Message}");
            if (e.Exception != null)
            {
            Console.WriteLine(e.Exception);
            }*/
        }
    }
}
复制代码
复制代码

8.2 客户端代码

复制代码
复制代码
using MQTTnet;
using MQTTnet.Core;
using MQTTnet.Core.Client;
using MQTTnet.Core.Packets;
using MQTTnet.Core.Protocol;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace MqttClientWin
{
    public partial class FmMqttClient : Form
    {
        private MqttClient mqttClient = null;

        public FmMqttClient()
        {
            InitializeComponent();

            Task.Run(async () => { await ConnectMqttServerAsync(); });
        }

        private async Task ConnectMqttServerAsync()
        {
            if (mqttClient == null)
            {
                mqttClient = new MqttClientFactory().CreateMqttClient() as MqttClient;
                mqttClient.ApplicationMessageReceived += MqttClient_ApplicationMessageReceived;
                mqttClient.Connected += MqttClient_Connected;
                mqttClient.Disconnected += MqttClient_Disconnected;
            }

            try
            {
                var options = new MqttClientTcpOptions
                {
                    Server = "127.0.0.1",
                    ClientId = Guid.NewGuid().ToString().Substring(0, 5),
                    UserName = "u001",
                    Password = "p001",
                    CleanSession = true
                };

                await mqttClient.ConnectAsync(options);
            }
            catch (Exception ex)
            {
                Invoke((new Action(() =>
                {
                    txtReceiveMessage.AppendText($"连接到MQTT服务器失败!" + Environment.NewLine + ex.Message + Environment.NewLine);
                })));
            }
        }

        private void MqttClient_Connected(object sender, EventArgs e)
        {
            Invoke((new Action(() =>
            {
                txtReceiveMessage.AppendText("已连接到MQTT服务器!" + Environment.NewLine);
            })));
        }

        private void MqttClient_Disconnected(object sender, EventArgs e)
        {
            Invoke((new Action(() =>
            {
                txtReceiveMessage.AppendText("已断开MQTT连接!" + Environment.NewLine);
            })));
        }

        private void MqttClient_ApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs e)
        {
            Invoke((new Action(() =>
            {
                txtReceiveMessage.AppendText($">> {Encoding.UTF8.GetString(e.ApplicationMessage.Payload)}{Environment.NewLine}");
            })));
        }

        private void BtnSubscribe_ClickAsync(object sender, EventArgs e)
        {
            string topic = txtSubTopic.Text.Trim();

            if (string.IsNullOrEmpty(topic))
            {
                MessageBox.Show("订阅主题不能为空!");
                return;
            }

            if (!mqttClient.IsConnected)
            {
                MessageBox.Show("MQTT客户端尚未连接!");
                return;
            }

            mqttClient.SubscribeAsync(new List<TopicFilter> {new TopicFilter(topic, MqttQualityOfServiceLevel.AtMostOnce)});

            txtReceiveMessage.AppendText($"已订阅[{topic}]主题" + Environment.NewLine);
            txtSubTopic.Enabled = false;
            btnSubscribe.Enabled = false;
        }

        private void BtnPublish_Click(object sender, EventArgs e)
        {
            string topic = txtPubTopic.Text.Trim();

            if (string.IsNullOrEmpty(topic))
            {
                MessageBox.Show("发布主题不能为空!");
                return;
            }

            string inputString = txtSendMessage.Text.Trim();
            var appMsg = new MqttApplicationMessage(topic, Encoding.UTF8.GetBytes(inputString), MqttQualityOfServiceLevel.AtMostOnce, false);
            mqttClient.PublishAsync(appMsg);
        }
    }
}
复制代码
复制代码

9 本文的Demo下载地址
点击下载 Demo https://download.csdn.net/download/panwen1111/11018592

版权声明:本文为CSDN博主「weixin_pwtank1983」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/panwen1111/article/details/79245161

posted on   李sir  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
历史上的今天:
2011-01-24 unresolved external symbol "private: static void *錯誤——靜態變量沒有初始化
2011-01-24 C++静态成员函数小结(转)
2011-01-24 TerminateThread不要使用的證據
点击右上角即可分享
微信分享提示