一个月玩转MQTT(篇五:开发自己的MQTT WEB页面)

前面讲到了:

一个月玩转MQTT(篇二:部署阿里云服务器和EMQX)-CSDN博客

一个月玩转MQTT(篇三:测试EMQX)-CSDN博客

一个月玩转MQTT(篇四:移远EC200U模块MQTT连接测试)-CSDN博客

现在我讲一下,如何在云服务器上,生成一个自己的web网页,然后web网页可以订阅和发布消息。

背景:

1、服务器还是之前提到的阿里云ECS服务器,搭载的是Ubuntu 22.04 64位 UEFI版

2、web页面,采用ASP.NET跨平台开发。我的开发环境是visual studio 2022

由于我一直是C#系开发者,一直对ASP.NET熟悉,所以驾轻就熟的使用这门语言。当然你们也可以用自己熟悉的语言,比如node.js + Vue 的方案等等。

好吧,那我们就开始吧!

第一步:Windows 11 + VS 2022 开发准备

1. 确保 VS 2022 安装了 .NET 8 开发工具

打开 VS 2022 → 点击「工具」→「获取工具和功能」;在「工作负载」里勾选:

ASP.NET 和 Web 开发(必选);

点击「修改」,安装完成后重启 VS。

2. 创建 ASP.NET Core MVC 项目

打开 VS 2022 → 「创建新项目」→ 选择「ASP.NET Core Web 应用程序(MVC)」

框架选择:.NET 8 (长期支持)取消勾选「启用 Docker」 → 创建

3、安装 MQTTnet 依赖(对接 EMQX)
  1. 右键项目 →「管理 NuGet 程序包」→ 「浏览」;
  2. 搜索并安装以下包(版本选最新稳定版)
    1. MQTTnet 4.3.1207(核心 MQTT 客户端)
    2. MQTTnet.Extensions.ManagedClient 4.3.1207(自动重连的托管客户端,适合后台运行)
    3. Microsoft.AspNetCore.SignalR(实时推送数据到 Web 前端,可选但推荐)

上图中的5.1.0.1559,我后边都统一改成了4.3.1207。

第二步: 实现 MQTT 核心功能(订阅传感器消息 + 存储数据)

我们公司长沙湾流智能科技有限公司,是专门做倾角传感器的,可以检测物体倾斜,比如货柜,铁塔,这些传感器通过4G MQTT与服务器进行连接通信。那么接下来我就以倾角传感器来举例进行说明。

我们计划按下述步骤实现:

  • 后台服务(asp.net)持续连接 EMQX,订阅传感器主题;
  • 接收倾角传感器的 roll/pitch (X、Y)角度数据,存储到内存(这里先不使用数据库);
  • 通过 SignalR 实时推送到 Web 前端。
步骤 1:创建传感器数据模型

在项目中新建「Models」文件夹→新建类SensorData.cs

cs 复制代码
// 传感器数据模型(对应roll/pitch)
namespace SensorMqttDashboard.Models
{
    public class SensorData
    {
        // 传感器ID(区分5个传感器,如Sensor_1、Sensor_2)
        public string SensorId { get; set; } = string.Empty;
        // 横滚角
        public double Roll { get; set; }
        // 俯仰角
        public double Pitch { get; set; }
        // 数据更新时间
        public DateTime UpdateTime { get; set; } = DateTime.Now;
    }
}
步骤 2:创建 SignalR 集线器(实时推送数据)

新建「Hubs」文件夹→新建类SensorHub.cs

cs 复制代码
using Microsoft.AspNetCore.SignalR;
using SensorMqttDashboard.Models;

namespace SensorMqttDashboard.Hubs
{
    // SignalR集线器,用于向前端推送传感器数据
    public class SensorHub : Hub
    {
        // 前端调用此方法获取所有传感器数据(可选)
        public async Task SendSensorData(SensorData data)
        {
            // 推送给所有连接的客户端
            await Clients.All.SendAsync("ReceiveSensorData", data);
        }
    }
}
步骤 3:创建 MQTT 后台服务(核心)

新建「Services」文件夹→新建类MqttBackgroundService.cs

cs 复制代码
using Microsoft.AspNetCore.SignalR;
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Extensions.ManagedClient;
using MQTTnet.Packets;
using MQTTnet.Protocol;
using SensorMqttDashboard.Hubs;
using SensorMqttDashboard.Models;
using System.Text;
using System.Text.Json;

namespace SensorMqttDashboard.Services
{
    // 后台MQTT服务,持续运行并订阅传感器消息
    public class MqttBackgroundService : BackgroundService
    {
        // 存储所有传感器数据(内存中,重启会丢失,新手先这样)
        public static Dictionary<string, SensorData> AllSensorData = new Dictionary<string, SensorData>();

        private readonly IManagedMqttClient _mqttClient;
        private readonly ILogger<MqttBackgroundService> _logger;
        private readonly IHubContext<SensorHub> _hubContext;

        // 阿里云EMQX配置(替换成你的服务器信息)
        private readonly string _mqttServer = "你的阿里云服务器公网IP"; // 例如 120.78.xxx.xxx
        private readonly int _mqttPort = 1883; // EMQX默认MQTT端口
        private readonly string _mqttUsername = "mqtt_user"; // 步骤1.3创建的用户名
        private readonly string _mqttPassword = "mqtt_pass123"; // 步骤1.3创建的密码
        private readonly string _sensorTopic = "sensor/inclination/#"; // 订阅所有传感器主题(#是通配符)

        public MqttBackgroundService(ILogger<MqttBackgroundService> logger, IHubContext<SensorHub> hubContext)
        {
            _logger = logger;
            _hubContext = hubContext;

            // 创建托管MQTT客户端(自动重连,断网后会自动重试)
            var factory = new MqttFactory();
            _mqttClient = factory.CreateManagedMqttClient();
        }

        // 服务启动时执行:连接EMQX + 订阅主题
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            // 配置MQTT客户端选项
            var mqttOptions = new ManagedMqttClientOptionsBuilder()
                .WithAutoReconnectDelay(TimeSpan.FromSeconds(5)) // 重连间隔5秒
                .WithClientOptions(new MqttClientOptionsBuilder()
                    .WithTcpServer(_mqttServer, _mqttPort) // 阿里云EMQX地址+端口
                    .WithClientId($"AspNetClient_{Guid.NewGuid()}") // 唯一客户端ID
                    .WithCredentials(_mqttUsername, _mqttPassword) // 认证信息
                    .WithCleanSession() // 清理会话
                    .Build())
                .Build();

            // 注册消息接收事件(核心:收到传感器消息后的处理)
            _mqttClient.ApplicationMessageReceivedAsync += async e =>
            {
                try
                {
                    // 1. 解析MQTT消息
                    var topic = e.ApplicationMessage.Topic; // 传感器主题,如 sensor/inclination/Sensor_1
                    var payload = System.Text.Encoding.UTF8.GetString(e.ApplicationMessage.Payload); // 消息内容,如 {"SensorId":"Sensor_1","Roll":12.3,"Pitch":4.5}

                    _logger.LogInformation("收到传感器消息:Topic={Topic}, Payload={Payload}", topic, payload);

                    // 2. 反序列化为SensorData对象
                    var sensorData = JsonSerializer.Deserialize<SensorData>(payload);
                    if (sensorData == null || string.IsNullOrEmpty(sensorData.SensorId))
                    {
                        _logger.LogWarning("消息格式错误:{Payload}", payload);
                        return;
                    }

                    // 3. 更新内存中的传感器数据
                    if (AllSensorData.ContainsKey(sensorData.SensorId))
                    {
                        AllSensorData[sensorData.SensorId] = sensorData;
                    }
                    else
                    {
                        AllSensorData.Add(sensorData.SensorId, sensorData);
                    }

                    // 4. 通过SignalR推送到Web前端
                    await _hubContext.Clients.All.SendAsync("ReceiveSensorData", sensorData);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "处理MQTT消息失败");
                }
            };

            // 连接EMQX并订阅主题
            await _mqttClient.StartAsync(mqttOptions);
            
            await _mqttClient.SubscribeAsync(new List<MqttTopicFilter>{
                new MqttTopicFilterBuilder()
                    .WithTopic(_sensorTopic)
                    .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce)
                    .Build()
            });

            _logger.LogInformation("MQTT客户端已连接到 {Server}:{Port},订阅主题:{Topic}", _mqttServer, _mqttPort, _sensorTopic);

            // 保持服务运行,直到停止
            await Task.Delay(Timeout.Infinite, stoppingToken);
        }

        // 服务停止时断开MQTT连接
        public override async Task StopAsync(CancellationToken cancellationToken)
        {
            await _mqttClient.StopAsync();
            _logger.LogInformation("MQTT客户端已断开连接");
            await base.StopAsync(cancellationToken);
        }

        // 可选:发布消息到EMQX(比如前端手动发送指令给传感器)
        public async Task PublishMessage(string topic, string payload)
        {
            var message = new MqttApplicationMessageBuilder()
                .WithTopic(topic)
                .WithPayload(Encoding.UTF8.GetBytes(payload)) // 显式转字节数组,避免编码问题
                .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce)
                .WithRetainFlag(false)
                .Build();

            // 托管客户端用EnqueueAsync替代PublishAsync
            await _mqttClient.EnqueueAsync(message);
        }
    }
}

步骤 4:注册服务和中间件(Program.cs)

替换Program.cs的全部代码:

cs 复制代码
using SensorMqttDashboard.Hubs;
using SensorMqttDashboard.Services;

var builder = WebApplication.CreateBuilder(args);

// 添加MVC控制器和视图支持
builder.Services.AddControllersWithViews();

// 注册SignalR
builder.Services.AddSignalR();

// 注册MQTT后台服务
builder.Services.AddHostedService<MqttBackgroundService>();

var app = builder.Build();

// 配置HTTP请求管道
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

// 配置SignalR路由
app.MapHub<SensorHub>("/sensorHub");

// 配置MVC路由
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

第三步:Web 前端展示传感器数据(实时更新)

我们修改默认的 Home 视图,展示 5 个传感器的 roll/pitch 值,通过 SignalR 实时刷新。

步骤 1:修改 Home 控制器(HomeController.cs)

打开「Controllers/HomeController.cs」,添加获取所有传感器数据的方法:

cs 复制代码
using Microsoft.AspNetCore.Mvc;
using SensorMqttDashboard.Models;
using SensorMqttDashboard.Services;

namespace SensorMqttDashboard.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }

        // 首页:展示传感器数据
        public IActionResult Index()
        {
            // 将内存中的传感器数据传递到视图
            ViewBag.SensorDataList = MqttBackgroundService.AllSensorData.Values.ToList();
            return View();
        }

        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View();
        }
    }
}
步骤 2:修改首页视图(Index.cshtml)

打开「Views/Home/Index.cshtml」,替换全部代码

cs 复制代码
@{
    ViewData["Title"] = "倾角传感器监控面板";
}

<!-- 传感器数据展示面板 -->
<div class="container mt-5">
    <h1 class="text-center mb-4" style="font-size: 2.5rem; font-weight: bold;">倾角传感器实时数据</h1>
    <h4 class="text-center mb-2" style="font-size: 1rem; color: #666;">长沙湾流智能科技有限公司</h4>
    <h4 class="text-center mb-4" style="font-size: 1rem; color: #666;">抖音号:铁甲前沿</h4>

    <!-- 5个传感器卡片(动态生成) -->
    <div class="row g-4" id="sensorCards">
        @foreach (var sensor in ViewBag.SensorDataList)
        {
            <div class="col-md-6 col-lg-4">
                <div class="card shadow-sm h-100">
                    <div class="card-header bg-primary text-white">
                        <h5 class="card-title mb-0">传感器 @sensor.SensorId</h5>
                    </div>
                    <div class="card-body">
                        <p class="card-text"><strong>横滚角 (Roll):</strong> <span id="roll_@sensor.SensorId">@sensor.Roll</span> °</p>
                        <p class="card-text"><strong>俯仰角 (Pitch):</strong> <span id="pitch_@sensor.SensorId">@sensor.Pitch</span> °</p>
                        <p class="card-text text-muted"><small>最后更新:@sensor.UpdateTime.ToString("yyyy-MM-dd HH:mm:ss")</small></p>
                    </div>
                </div>
            </div>
        }
        <!-- 如果暂无数据,显示提示 -->
        @if (ViewBag.SensorDataList.Count == 0)
        {
            <div class="col-12 text-center text-muted">
                <h4>暂无传感器数据,请等待设备上报...</h4>
            </div>
        }
    </div>
</div>

<!-- 引入SignalR客户端 + jQuery(简化DOM操作) -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<script src="~/lib/signalr/dist/browser/signalr.js"></script>

<script>
    // 初始化SignalR连接
    const connection = new signalR.HubConnectionBuilder()
        .withUrl("/sensorHub") // 对应Program.cs中的SignalR路由
        .withAutomaticReconnect() // 自动重连
        .build();

    // 接收传感器数据的回调函数
    connection.on("ReceiveSensorData", function (sensorData) {
        console.log("收到实时数据:", sensorData);

        // 检查卡片是否存在,不存在则创建
        const cardId = `card_${sensorData.sensorId}`;
        if ($(`#${cardId}`).length === 0) {
            // 创建新的传感器卡片
            const newCard = `
                <div class="col-md-6 col-lg-4" id="${cardId}">
                    <div class="card shadow-sm h-100">
                        <div class="card-header bg-primary text-white">
                            <h5 class="card-title mb-0">传感器 ${sensorData.sensorId}</h5>
                        </div>
                        <div class="card-body">
                            <p class="card-text"><strong>横滚角 (Roll):</strong> <span id="roll_${sensorData.sensorId}">${sensorData.roll}</span> °</p>
                            <p class="card-text"><strong>俯仰角 (Pitch):</strong> <span id="pitch_${sensorData.sensorId}">${sensorData.pitch}</span> °</p>
                            <p class="card-text text-muted"><small>最后更新:${new Date(sensorData.updateTime).toLocaleString()}</small></p>
                        </div>
                    </div>
                </div>
            `;
            // 清空"暂无数据"提示,添加新卡片
            $("#sensorCards").empty().append(newCard);
        } else {
            // 更新已有卡片的数据
            $(`#roll_${sensorData.sensorId}`).text(sensorData.roll);
            $(`#pitch_${sensorData.sensorId}`).text(sensorData.pitch);
            $(`#${cardId} .text-muted small`).text(`最后更新:${new Date(sensorData.updateTime).toLocaleString()}`);
        }
    });

    // 启动SignalR连接
    connection.start().catch(function (err) {
        return console.error(err.toString());
    });
</script>

第四步:测试运行

程序架构:

运行效果:

好啦,

现在能正常看到 Web 界面啦。

显示 "暂无传感器数据,请等待设备上报",这说明核心的 MVC 项目、视图、MQTT 后台服务都已经正常运行了

现在只需要完成「模拟传感器发消息」和「验证数据实时显示」这最后两步,就能看到传感器的 roll/pitch 值了。

后边继续讲,如何将刚才这个ASP.NET项目发布到阿里云的 Ubuntu 服务器上。

相关推荐
~央千澈~2 小时前
抖音弹幕游戏开发之第7集:识别不同类型的消息·优雅草云桧·卓伊凡
java·服务器·前端
拾荒李2 小时前
在 Vue 项目里“无痛”使用 React 组件:以 Veaury + Vite 为例
前端·vue.js·react.js
dangfulin2 小时前
简单的视差滚动效果
前端·css·视差滚动
Forget_85502 小时前
RHEL——web应用服务器TOMCAT
java·前端·tomcat
myFirstName3 小时前
离谱!React中不起眼的[]和{}居然也会导致性能问题
前端
我是伪码农3 小时前
Vue 2.11
前端·javascript·vue.js
Amumu121383 小时前
CSS:字体属性
前端·css
凯里欧文4273 小时前
html与CSS伪类技巧
前端
UIUV3 小时前
构建Git AI提交助手:从零到全栈实现的学习笔记
前端·后端·typescript