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 平台