MAUI 嵌入式 Web 架构实战(七) 构建设备实时通信与控制系统

MAUI 嵌入式 Web 架构实战(七)

PicoServer + WebSocket

构建设备实时通信与控制系统

源码地址
https://github.com/densen2014/MauiPicoAdmin


一、为什么需要 WebSocket

在前面的文章中,我们已经实现了完整架构:

复制代码
Web Admin UI
      ↓
PicoServer REST API
      ↓
MAUI Service
      ↓
SQLite / Device

Web API 适合:

复制代码
CRUD
请求响应

但对于 实时系统,REST API 有明显局限:

例如:

场景 REST API 问题
设备状态变化 需要不断轮询
实时日志 延迟高
设备控制 交互慢

例如浏览器:

复制代码
每秒请求一次
/api/device/status

这叫:

Polling(轮询)

问题:

复制代码
服务器压力大
延迟高
体验差

解决方案是:

WebSocket


二、什么是 WebSocket

WebSocket 是一种 长连接通信协议

通信模式:

复制代码
浏览器
   ⇅
WebSocket
   ⇅
Server

特点:

特性 说明
双向通信 Client / Server 都能发送
长连接 不需要重复建立连接
实时性 毫秒级

因此非常适合:

复制代码
设备控制
实时日志
消息推送
IoT系统

三、系统架构升级

加入 WebSocket 后架构变成:

复制代码
           Web Admin
             │
      ┌──────┴───────┐
      │              │
 REST API        WebSocket
      │              │
      ▼              ▼
         PicoServer
              │
              ▼
            Service
              │
              ▼
         Device / DB

REST API:

复制代码
CRUD

WebSocket:

复制代码
实时通信
设备控制

四、代码实现 WebSocket 服务器

在 PicoServer 中增加:

复制代码
WebSocketManager

创建:

复制代码
Services/WebSocketManager.cs

示例实现:

csharp 复制代码
using PicoServer;

public class WebSocketManager
{
    private WebAPIServer? api;

    public void RegisterWebSocket(WebAPIServer api)
    {
        this.api = api;
        api.enableWebSocket = true; 
        api.WsOnConnectionChanged = WsConnectChanged;
        api.WsOnMessage = OnMessageReceived;

    }
 
    public async Task OnMessageReceived(string clientId, string message, Func<string, Task> reply)
    {
        await reply("收到!");
        var clients = api!.WsGetOnlineClients();
        foreach (var client in clients)
        {
            await api.WsSendToClientAsync(client, $"{clientId}说:{message}");
        }
    }

    //相关方法
    //api.enableWebSocket = true; //启用WebSocket支持
    //api.WsOnConnectionChanged; // 事件:WebSocket客户端连接状态发生变化
    //api.WsOnMessage; //事件:收到WebSocket客户端发送来的消息
    //api.WsBroadcastAsync(); //对所有在线客户端广播消息
    //api.WsGetOnlineClients; //获取在线客户端列表
    //api.WsSendToClientAsync(client, message); //给指定客户端发送消息
    //api.WsEnableHeartbeat = true; //启用 WebSocket 服务端心跳检测,默认false
    //api.WsHeartbeatTimeout = 60; //设置 WebSocket 心跳时间,默认30秒
    //api.WsMaxConnections = 200; //设置 WebSocket 最大连接数,默认100
    //api.WsPingString = "hi"; //设置 WebSocket 的ping消息,默认"pong",不区分大小写

    public async Task WsConnectChanged(string clientId, bool connected)
    {
        await api!.WsBroadcastAsync($"{clientId} {connected}");
    }

}

这个组件实现了:

复制代码
连接管理
消息接收
消息广播

五、注册 WebSocket 路由

在 ServerHost 中注册:

csharp 复制代码
ws.RegisterWebSocket(api);

现在浏览器可以连接:

复制代码
ws://localhost:8090/ws

六、前端连接 WebSocket

在 Web Admin 中:

javascript 复制代码
const ws = new WebSocket("ws://127.0.0.1:8090/ws");

ws.onopen = () => {
  console.log("WebSocket Connected");
};

ws.onmessage = (event) => {
console.log("Message:", event.data);
};

ws.onclose = () => {
console.log("Disconnected");
};

function send() {
ws.send("hello device");
        }

发送消息:

javascript 复制代码
ws.send("hello device");

七、设备控制协议设计

实际系统中需要定义 通信协议

推荐使用 JSON。

例如:

设备控制:

json 复制代码
{
  "type": "device_control",
  "device": "printer",
  "cmd": "start"
}

设备状态:

json 复制代码
{
  "type": "device_status",
  "device": "printer",
  "status": "running"
}

日志消息:

json 复制代码
{
  "type": "log",
  "message": "device started"
}

八、Server 处理设备命令

解析 WebSocket 消息:

csharp 复制代码
var cmd = JsonSerializer.Deserialize<WsCommand>(msg);

switch(cmd.Type)
{
    case "device_control":
        DeviceService.Execute(cmd.Device, cmd.Cmd);
        break;
}

示例:

csharp 复制代码
DeviceService.Execute("printer","start");

九、实时推送设备状态

当设备状态变化时:

csharp 复制代码
await ws.Broadcast(JsonSerializer.Serialize(new
{
    type = "device_status",
    device = "printer",
    status = "running"
}));

前端立即收到:

javascript 复制代码
ws.onmessage = e => {

    let msg = JSON.parse(e.data);

    if(msg.type === "device_status")
    {
        updateUI(msg);
    }
}

实现:

复制代码
设备 → Server → Web Admin

实时更新。

完整前端代码

html 复制代码
<!DOCTYPE html>
<html>

<head>

    <meta charset="utf-8">

    <title>WebSocket</title>

    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
          rel="stylesheet">

</head>

<body class="container">

    <h1 class="mt-4">WebSocket</h1>

    <div class="mt-4">

        <button onclick="send()" class="btn btn-primary">发送消息</button>
        <button onclick="start()" class="btn btn-success">控制设备(start)</button>
        <button onclick="stop()" class="btn btn-danger">控制设备(stop)</button>
        <button onclick="startSSE()" class="btn btn-success">HTTP消息推送(SSE Demo)</button>

    </div>

    <p id="result"></p>

    <script>

        const ws = new WebSocket("ws://127.0.0.1:8090/ws");

        ws.onopen = () => {
            document.getElementById("result").innerHTML += "WebSocket Connected" + "<br>";
        };

        ws.onmessage = (e) => {
            document.getElementById("result").innerHTML += "Message:" + e.data + "<br>";

            try {
                let msg = JSON.parse(e.data);

                if (msg.type === "device_status") {
                    document.getElementById("result").innerHTML += "Status: " + JSON.stringify(msg, null, 2) + "<br>";
                }
            } catch {

            }
        };

        ws.onclose = () => {
            document.getElementById("result").innerHTML += "Disconnected" + "<br>";
        };

        function send() {

            ws.send("hello device");
        }

        function start() {
            const msg = {
                type: "device_control",
                device: "printer",
                cmd: "start"
            };
            ws.send(JSON.stringify(msg));
        }

        function stop() {
            const msg = {
                type: "device_control",
                device: "printer",
                cmd: "stop"
            };
            ws.send(JSON.stringify(msg));
        }

        function startSSE() {
            const source = new EventSource("http://127.0.0.1:8090/iot/notify");
            source.onmessage = function (event) {
                document.getElementById("result").innerHTML += event.data + "<br>";
            };
            source.onerror = function () {
                source.close();
            };
        }
    </script>
</body>
</html>

后端代码

csharp 复制代码
    public async Task OnMessageReceived(string clientId, string message, Func<string, Task> reply)
    {
        await reply("收到!");
        var clients = api!.WsGetOnlineClients();
        foreach (var client in clients)
        {
            await api.WsSendToClientAsync(client, $"{clientId}说:{message}");
        }

        try
        {
            var cmd = JsonSerializer.Deserialize<WsCommand>(message, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
            if (cmd != null)
            {
                switch (cmd.Type)
                {
                    case "device_control":
                        await Task.Delay(200);//模拟执行控制命令
                        //DeviceService.Execute(cmd.Device, cmd.Cmd);
                        await api.WsBroadcastAsync(JsonSerializer.Serialize(new
                        {
                            type = "device_status",
                            device = "printer",
                            status = cmd.Cmd == "start" ? "running" : "stop"
                        }));

                        break;
                }
            }
        }
        catch (JsonException)
        {
            // 处理 JSON 解析错误
        }
    }

    //SSE(Server - Sent Events)推送, 请注册路由 api.AddRoute("/iot/notify", HttpHelper.Notify, "GET");
    public static async Task Notify(HttpListenerRequest request, HttpListenerResponse response)
    {
        response.ContentType = "text/event-stream";
        response.Headers.Add("Cache-Control", "no-cache");
        response.SendChunked = true;

        try
        {
            for (int i = 0; i < 5; i++)
            {
                string msg = $"data: 消息推送 {i} 时间: {DateTime.Now}\n\n";
                byte[] buffer = System.Text.Encoding.UTF8.GetBytes(msg);
                await response.OutputStream.WriteAsync(buffer, 0, buffer.Length);
                await response.OutputStream.FlushAsync();
                await Task.Delay(1000);
            }
        }
        finally
        {
            //在使用 HttpListenerResponse 进行 SSE(Server - Sent Events)推送时,response.Close(); 并不是必须的,但推荐在推送结束后调用它,以确保资源释放和连接正确关闭。

            // 示例这里是推送结束后调用 response.Close();,确保响应流关闭
            // 如果是无限推送(如实时设备报警),不要关闭响应,直到客户端断开。
            response.Close();
        }
    }

运行截图


十、实时日志系统

例如设备日志:

csharp 复制代码
await ws.Broadcast(JsonSerializer.Serialize(new
{
    type="log",
    message="print job started"
}));

前端:

javascript 复制代码
if(msg.type==="log"){
   logPanel.append(msg.message)
}

效果:

复制代码
实时日志窗口

十一、完整实时架构

最终系统变成:

复制代码
                 Web Admin UI
                /            \
          REST API        WebSocket
              │               │
              ▼               ▼
             PicoServer Core
                    │
                    ▼
                 Services
                    │
          ┌─────────┴─────────┐
          ▼                   ▼
       SQLite              Device

系统能力升级为:

复制代码
后台管理
+
实时通信
+
设备控制
+
数据存储

十二、本篇总结

本篇为系统新增:

核心能力:

复制代码
WebSocket 实时通信
设备控制协议
实时日志推送
状态同步

系统能力升级为:

复制代码
Web Admin
+
REST API
+
WebSocket
+
设备控制

已经可以用于:

复制代码
IoT 系统
设备管理平台
本地控制软件
工业工具系统

下一篇预告

下一篇将进入 架构升级的重要一步

MAUI 嵌入式 Web 架构实战(八)

插件化架构与模块系统

我们将实现:

复制代码
插件加载
模块扩展
动态 API
模块管理

最终把系统升级为:

真正可扩展的本地 Web 平台