news 2026/11/4 3:21:48

树莓派轻量MQTT服务:C#实现的嵌入式消息通信方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
树莓派轻量MQTT服务:C#实现的嵌入式消息通信方案

简介:本资源是一套基于.NET Core 3.0实现MQTT协议通信的完整开发实践包,面向物联网应用开发者、C#后端工程师及学习轻量级消息中间件集成的中高级程序员,解决在跨平台环境中快速构建MQTT客户端与服务器(Broker)的核心问题。压缩包共77个文件,含7个核心C#源码文件(如Form1.cs、Program.cs)、2个可执行项目工程(.csproj)、1个解决方案(.sln),以及DLL依赖库、JSON配置、PDB调试符号等辅助文件,结构清晰,便于理解客户端发布/订阅逻辑与服务端消息路由机制。资源包仅1.15MB,精简实用,已获2570人学习下载。读者可直接运行Mqtt.Demo中的Client与Server双示例,结合详尽注释掌握MQTTnet 2.7.5.0库的连接配置、QoS消息发布、主题订阅及事件回调处理全流程,是深入理解IoT设备通信落地的高价值实操素材。

1. 一个跑在树莓派上的轻量 MQTT 服务,3 分钟就能把温湿度传感器数据推到网页仪表盘

你手头有个 ESP32 或树莓派 Pico,接了 DHT22 温湿度模块,想把数据实时传到局域网另一台电脑的网页上画曲线——但又不想搭 Node-RED、不碰 Python 的 asyncio 黑匣子、更不想啃 Eclipse Mosquitto 的 C 编译玄学。这个基于 .NET Core 3.0 的 MQTT 项目就是为你写的:它自带 Server(Broker)和 Client 两个可直接运行的控制台程序,Server 不依赖任何外部服务,Client 支持发布/订阅双模式,所有代码用 C# 写得干净利落,连Program.cs都没藏私。它不是教学 Demo,而是某物联网实验室真实压测过的通信底座——在 4 核 ARM64 设备上稳定承载 200+ 终端、QoS1 消息吞吐超 800 条/秒。适合嵌入式开发者、工业网关调试员、以及被 Java Spring Boot MQTT Starter 依赖版本搞崩溃过三次的后端同学。别急着翻文档,先dotnet run起来,5 行命令之后,你就能看到“Hello from MQTT Client”出现在 Server 控制台里。


2. 技术栈与架构设计:为什么选 MQTTnet 而不是自己手撸协议解析

这个项目没有造轮子,而是把 MQTTnet 这个成熟、轻量、纯 .NET 实现的库用到了极致。MQTTnet 不是简单封装 System.Net.Sockets,它内部实现了完整的 MQTT 3.1.1 协议状态机、会话持久化抽象、内存/文件两种默认存储策略,还预留了自定义IStorage接口。更重要的是,它对 .NET Core 3.0 的IHostedService和ILogger<T>做了原生适配——这意味着 Server 可以作为后台服务长期运行,日志能直接对接 Serilog 或 ConsoleLogger,不用再写一堆胶水代码。

整个项目结构极简,只有三个核心工程:

MqttDemo.sln ├── MqttServer/ # Broker 主体:HostedService + 内存会话管理 + TCP 监听 ├── MqttClient/ # 双模式客户端:PublishMode(发传感器数据) / SubscribeMode(收告警) └── Shared/ # 公共实体:MqttMessageDto、TopicRule、ConnectionConfig

提示:Shared 工程里没有业务逻辑,只放 DTO 和配置类。这是为后续扩展留的伏笔——比如你要加 TLS 支持,只需在 Shared 里新增TlsOptions类,Server 和 Client 就能共享同一套证书加载逻辑,避免配置散落在两处。

2.1 Server 架构:从 TCP 连接到消息路由的四层链路

Broker 启动后,实际走的是这样一条链路:

  1. HostedService 层:MqttServerHostedService实现IHostedService,在StartAsync()中启动IMqttServer实例;
  2. 网络层:MqttFactory.CreateMqttServer()默认使用TcpServerAdapter,监听0.0.0.0:1883,每个新连接触发OnClientConnected事件;
  3. 会话层:连接建立后,自动创建MqttClientSession,其ClientId作为内存字典 Key 存入ConcurrentDictionary<string, IMqttClientSession>;
  4. 路由层:当 Client 发送PUBLISH包,Server 解析 Topic(如sensor/room1/temperature),按通配符规则(sensor/+/temperature或sensor/#)匹配所有订阅者,逐个投递。

关键不在“怎么连”,而在“怎么不丢”。MQTTnet 的IMqttServerOptionsBuilder允许你精细控制:

  • WithDefaultCommunicationTimeout(TimeSpan.FromSeconds(30)):防僵尸连接;
  • WithMaxPendingMessagesPerClient(1000):防恶意 Client 塞爆内存;
  • WithConnectionValidator(...):可插拔认证,比如校验 JWT Token 或白名单 IP。

2.2 Client 模式拆解:PublishMode 与 SubscribeMode 的行为差异

Client 工程通过命令行参数切换模式:

# 发布模式:每 2 秒向 topic sensor/livingroom/temp 推送 JSON dotnet run --project MqttClient/ -- -m publish -t sensor/livingroom/temp -i 2 # 订阅模式:监听 sensor/#,收到消息就打印并触发本地告警 dotnet run --project MqttClient/ -- -m subscribe -t sensor/#

PublishMode的核心是MqttClient.PublishAsync()调用链:

// MqttClient/PublishMode.cs var message = new MqttApplicationMessageBuilder() .WithTopic(topic) .WithPayload(JsonSerializer.Serialize(data, _jsonOptions)) .WithQualityOfService(MqttQualityOfService.ExactlyOnce) // QoS2 .WithRetain(false) .Build(); await _mqttClient.PublishAsync(message, CancellationToken.None);

注意WithQualityOfService参数:QoS0 是“发了就忘”,QoS1 是“至少一次”(带 PUBACK),QoS2 是“恰好一次”(PUBREC/PUBREL/PUBCOMP 三段握手)。项目默认设为 QoS1 —— 它在可靠性与性能间取得最佳平衡,实测丢包率 < 0.002%,而 QoS2 在高并发下会显著增加延迟。

SubscribeMode则依赖事件驱动:

// MqttClient/SubscribeMode.cs _mqttClient.ApplicationMessageReceivedAsync += e => { var payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); var dto = JsonSerializer.Deserialize<MqttMessageDto>(payload, _jsonOptions); Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {e.ApplicationMessage.Topic}: {payload}"); if (dto.Value > 35 && e.ApplicationMessage.Topic.Contains("temperature")) TriggerLocalAlarm(); // 例如点亮 LED 或写入本地日志 };

这里的关键是ApplicationMessageReceivedAsync事件——它不是轮询,而是由 MQTTnet 底层 Socket 收到PUBLISH包后主动触发,毫秒级响应。你不需要手动while(true) { client.Receive() },这是 .NET Core 异步 I/O 的红利。

2.3 Shared 工程:DTO 与配置如何支撑跨模块一致性

Shared 工程看似简单,却是项目可维护性的命脉。它定义了:

  • MqttMessageDto:统一消息体结构,含Topic、Value、Timestamp、Unit四个字段,强制序列化时忽略 null 值;
  • ConnectionConfig:Server 地址、端口、ClientId、用户名密码等,支持从appsettings.json自动绑定;
  • TopicRule:用于 Server 端做 Topic 白名单校验,例如new TopicRule("sensor/+/temperature", "read")。

MqttMessageDto的设计有讲究:

public class MqttMessageDto { [JsonPropertyName("topic")] public string Topic { get; set; } [JsonPropertyName("value")] public double Value { get; set; } [JsonPropertyName("ts")] public long Timestamp { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); [JsonPropertyName("unit")] public string Unit { get; set; } = "C"; }

Timestamp字段设为long而非DateTime,是为了跨平台兼容(JavaScript Date.now() 也是毫秒时间戳);Unit默认"C",但允许 Client 发送"F"或"%",Server 不做转换,只做透传——语义交给上层应用处理,Broker 保持无状态。


3. 快速运行指南:从零开始跑通 Server + Client 双向通信

别被 “.NET Core 3.0” 吓住,它对运行环境要求极低。只要你的机器装了 .NET SDK 3.1+(注意:3.0 已 EOL,但项目兼容 3.1/5.0/6.0),就能跑。Windows/macOS/Linux 全支持,树莓派 ARM32/ARM64 也验证通过。

3.1 环境准备:确认 SDK 版本与项目还原

首先检查 .NET SDK 是否就位:

dotnet --list-sdks # 输出应包含类似:3.1.426 [/usr/share/dotnet/sdk] # 若无,请去 https://dotnet.microsoft.com/download/dotnet/3.1 下载 SDK 3.1.x(LTS 版本)

克隆或解压项目后,进入根目录执行:

dotnet restore # 此命令会读取 *.csproj 中的 <PackageReference>,下载 MQTTnet 3.1.0 及其依赖 # 注意:项目锁定 MQTTnet 3.1.0,因为 4.0+ 已转向 .NET 5+,与 Core 3.0 不兼容

提示:如果你看到NU1102: Unable to find package MQTTnet...错误,大概率是 NuGet 源被重定向。临时切回官方源:

dotnet nuget add source https://api.nuget.org/v3/index.json -n official

3.2 启动 Server:监听 1883 端口并观察日志

在终端 A 中执行:

cd MqttServer/ dotnet run

你会看到类似输出:

info: MqttServerHostedService[0] Starting MQTT server on port 1883... info: MqttServerHostedService[0] MQTT server started successfully. info: MqttServerHostedService[0] Listening on 0.0.0.0:1883

此时 Server 已就绪。它默认启用内存会话(InMemorySessionStorage),不保存历史消息,符合轻量定位。若需断线重连后接收离线消息,需自行实现IStorage并注入(见第 5 章)。

3.3 启动 Client:发布一条测试消息并验证接收

新开终端 B,执行:

cd MqttClient/ dotnet run -- -m publish -t test/topic -p "Hello from .NET Core MQTT"

几秒后,回到终端 A,你应该看到:

info: MqttServerHostedService[0] Client 'test-client' published to 'test/topic': Hello from .NET Core MQTT

再试订阅模式:终端 C 中运行

dotnet run -- -m subscribe -t test/topic

此时终端 C 会打印:

[14:22:35] test/topic: Hello from .NET Core MQTT

双向链路已通。接下来你可以把-p参数换成真实传感器数据,比如:

dotnet run -- -m publish -t sensor/kitchen/humidity -p "{\"value\":65.2,\"unit\":\"%\",\"ts\":1715638955000}"

Server 会原样转发,SubscribeMode Client 会自动反序列化为MqttMessageDto对象。

3.4 验证工具:用 mosquitto_sub/mosquitto_pub 做第三方交叉验证

为了证明这不是“自家兄弟互吹”,我们用业界标准工具验证互通性。安装 mosquitto(Ubuntu/Debian):

sudo apt update && sudo apt install mosquitto-clients

在 Server 运行状态下,另开终端执行:

# 用 mosquitto 订阅 mosquitto_sub -h localhost -p 1883 -t "test/topic" -v # 在另一个终端用 mosquitto 发布 mosquitto_pub -h localhost -p 1883 -t "test/topic" -m "From mosquitto!"

如果mosquitto_sub终端成功打印test/topic From mosquitto!,说明该项目完全遵循 MQTT 3.1.1 协议,与任何标准 Broker/Client 兼容。这是项目落地可信度的硬指标。


4. 常见问题排查:那些让你卡住 2 小时的典型翻车现场

刚跑起来时,最容易在几个地方栽跟头。这些不是文档遗漏,而是 .NET Core 网络编程和 MQTT 协议细节交织产生的“隐性坑”。我把它们列出来,每条都附真实复现步骤和绕过方案。

4.1 现象:Server 启动报错System.Net.Sockets.SocketException: Permission denied

原因:Linux/macOS 下,1024 以下端口需要 root 权限。MQTT 默认端口 1883 属于特权端口。
解决:

  • 方案一(推荐):改用非特权端口,修改MqttServer/appsettings.json:
    "MqttServerOptions": { "Port": 18830 }
    然后 Client 连接时指定-p 18830;
  • 方案二:用sudo dotnet run(不推荐,安全风险);
  • 方案三:给 dotnet 二进制文件授 CAP_NET_BIND_SERVICE 权限(高级用户)。

4.2 现象:Client 连接 Server 失败,日志显示Unable to connect to remote server

原因:Server 默认绑定0.0.0.0:1883,但 Client 代码中ConnectionConfig.Server默认值是"localhost"。如果 Server 运行在树莓派上,而 Client 在 Windows 上运行,localhost指向的是 Windows 本机,而非树莓派 IP。
解决:

  • 显式传入 Server IP:
    dotnet run -- -m publish -s 192.168.1.123 -p "test"
  • 或修改MqttClient/appsettings.json中的Server字段为树莓派局域网 IP。

4.3 现象:SubscribeMode Client 收不到消息,但 Server 日志显示published to xxx

原因:MQTT 的 Topic 匹配是大小写敏感的,且+和#通配符有严格语法。例如sensor/room1/temp不会被sensor/Room1/temp订阅到;sensor/+不会匹配sensor/room1/temp(因为+只匹配单级);sensor/#才匹配多级。
解决:

  • 用mosquitto_sub -t "sensor/#" -v先验证 Broker 是否真发出消息;
  • 检查 Client 订阅的 Topic 字符串是否与 Publisher 完全一致(包括大小写、斜杠结尾);
  • 在SubscribeMode.cs中加一行日志:
    Console.WriteLine($"Subscribed to: {topic}"); // 确认传入的 topic 值

4.4 现象:发布大量消息后,Client 报OperationCanceledException或 Server CPU 暴涨

原因:默认MqttClientOptions的KeepAlivePeriod是 60 秒,但未设置CleanSession = true。当 Client 频繁断连重连,Server 会为每个ClientId创建新会话,旧会话内存不释放,最终 OOM。
解决:

  • 在 Client 初始化时显式设置:
    var options = new MqttClientOptionsBuilder() .WithTcpServer(config.Server, config.Port) .WithClientId(config.ClientId) .WithCredentials(config.Username, config.Password) .WithCleanSession(true) // 关键!每次连接都清空旧会话 .WithKeepAlivePeriod(TimeSpan.FromSeconds(30)) .Build();
  • 或在 Server 端启用会话过期(需自定义IStorage,见第 5 章)。

4.5 现象:JSON 反序列化失败,SubscribeMode报JsonException: The input does not contain any JSON tokens

原因:Publisher 发送的 payload 是纯字符串(如"25.6"),但SubscribeMode期望MqttMessageDto对象。两者类型不匹配。
解决:

  • Publisher 必须发送合法 JSON 对象,不能只发数字或字符串:
    ✅ 正确:{"value":25.6,"unit":"C","ts":1715638955000}
    ❌ 错误:25.6或"25.6"
  • 在SubscribeMode.cs中加防御性判断:
    try { var dto = JsonSerializer.Deserialize<MqttMessageDto>(payload, _jsonOptions); // 处理 dto } catch (JsonException ex) { Console.WriteLine($"Invalid JSON payload: {payload}. Error: {ex.Message}"); return; }

5. 二次开发实战:给 Server 加持久化存储与 TLS 支持

现在 Server 是纯内存的,断电即失;Client 通信明文,不适合生产环境。这两项改造是项目从 Demo 迈向可用的分水岭。我不会教你抄文档,而是给出可直接粘贴的代码块,并说明每一行为什么这么写。

5.1 为 Server 添加文件级会话持久化:让离线消息不丢失

MQTTnet 提供FileSessionStorage,但它默认只存会话元数据(ClientId、CleanSession 标志),不存未确认的 QoS1/QoS2 消息。我们要补全这个缺口。

第一步:在MqttServer/工程中新增PersistentSessionStorage.cs:

// MqttServer/PersistentSessionStorage.cs public class PersistentSessionStorage : IStorage { private readonly string _basePath; private readonly ILogger<PersistentSessionStorage> _logger; public PersistentSessionStorage(string basePath, ILogger<PersistentSessionStorage> logger) { _basePath = Path.GetFullPath(basePath); _logger = logger; Directory.CreateDirectory(_basePath); } public async Task<IMqttClientSession> LoadClientSessionAsync(string clientId, CancellationToken cancellationToken) { var path = Path.Combine(_basePath, $"{clientId}.session.json"); if (!File.Exists(path)) return null; try { var json = await File.ReadAllTextAsync(path, cancellationToken); return JsonSerializer.Deserialize<MqttClientSession>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to load session for {ClientId}", clientId); return null; } } public async Task StoreClientSessionAsync(IMqttClientSession session, CancellationToken cancellationToken) { var path = Path.Combine(_basePath, $"{session.ClientId}.session.json"); var json = JsonSerializer.Serialize(session, new JsonSerializerOptions { WriteIndented = true }); await File.WriteAllTextAsync(path, json, cancellationToken); } // 其他方法(LoadMessage, StoreMessage)暂留空实现,因本项目暂不需离线消息队列 public Task<IMqttApplicationMessage> LoadMessageAsync(string messageId, CancellationToken cancellationToken) => Task.FromResult<IMqttApplicationMessage>(null); public Task StoreMessageAsync(IMqttApplicationMessage message, CancellationToken cancellationToken) => Task.CompletedTask; }

第二步:在MqttServerHostedService.cs的StartAsync()中注入该存储:

// MqttServer/MqttServerHostedService.cs protected override async Task StartAsync(CancellationToken cancellationToken) { var options = new MqttServerOptionsBuilder() .WithDefaultEndpointPort(_config.Port) .WithStorage(new PersistentSessionStorage("./sessions", _logger)) // ← 新增这行 .Build(); _server = MqttFactory.CreateMqttServer(options); await _server.StartAsync(cancellationToken); }

现在,每次 Client 断连,其会话(含订阅列表、QoS1 待确认消息 ID)都会写入./sessions/{ClientId}.session.json。重启 Server 后,Client 重连时能自动恢复订阅关系。

注意:FileSessionStorage不处理消息体本身,只存会话状态。若需真正离线消息(如 Client 断线期间发布的消息,在重连后推送),需实现StoreMessageAsync/LoadMessageAsync并配合IMqttServerOptions.WithPersistentSessions(true)。本项目未实现,因会显著增加磁盘 IO,与“轻量”定位冲突。

5.2 为 Client 和 Server 同时启用 TLS:用自签名证书搞定加密通信

明文 MQTT 在局域网尚可,但一旦暴露在公网或企业内网,必须上 TLS。.NET Core 3.0 原生支持SslStream,MQTTnet 也提供了WithTcpServerSecure方法。

第一步:生成自签名证书(Linux/macOS):

# 生成私钥和证书(有效期 365 天) openssl req -x509 -newkey rsa:2048 -keyout mqtt.key -out mqtt.crt -days 365 -nodes -subj "/CN=localhost" # 合并为 pfx(Client 需要) openssl pkcs12 -export -in mqtt.crt -inkey mqtt.key -out mqtt.pfx -passout pass:123456

第二步:修改 Server 启动逻辑,监听 8883 端口并加载证书:

// MqttServer/MqttServerHostedService.cs var options = new MqttServerOptionsBuilder() .WithDefaultEndpointPort(_config.Port) // 仍用 1883 .WithDefaultEndpointTlsOptions(tls => { tls.CertificateFilePath = "./mqtt.pfx"; tls.CertificatePassword = "123456"; tls.CertificateValidationHandler = (cert, chain, errors) => true; // 开发期跳过证书校验 }) .Build();

第三步:Client 连接时启用 TLS:

// MqttClient/Program.cs var options = new MqttClientOptionsBuilder() .WithTcpServerSecure(config.Server, config.Port) // ← 关键:用 WithTcpServerSecure 替代 WithTcpServer .WithClientId(config.ClientId) .WithTls(tls => { tls.UseTls = true; tls.CertificateValidationHandler = (cert, chain, errors) => true; }) .Build();

最后,Client 连接命令变为:

dotnet run -- -m publish -s localhost -p 8883 -t test/tls -p "encrypted"

此时 Wireshark 抓包将看不到明文 Topic 和 Payload,只有 TLS 握手流量。证书校验逻辑(CertificateValidationHandler)在生产环境必须改为校验 CA 签名,但开发阶段设为true可避免因证书链不完整导致连接失败——这是血泪经验:宁可先通再安,别一上来就卡在证书上。

5.3 改造 Checklist:上线前必做的五件事

事项操作验证方式
1. 端口收敛Server 关闭 1883 明文端口,只开 8883;或用防火墙限制 1883 仅局域网访问netstat -tuln | grep :1883
2. ClientID 唯一性确保每个物理设备有唯一ClientId(如 MAC 地址哈希),避免会话冲突查看 Server 日志中Client 'xxx' connected的 clientId
3. 日志分级将ILogger的LogLevel从Information调至Warning,减少高频 debug 日志刷屏日志量下降 90% 以上
4. 连接池复用Client 不要每次发消息都新建MqttFactory.CreateMqttClient(),应单例复用_mqttClient观察进程句柄数是否稳定
5. 心跳保活设置WithKeepAlivePeriod(TimeSpan.FromSeconds(30)),防 NAT 超时断连持续运行 2 小时,确认无意外断连

从那以后我每次给嵌入式设备写 MQTT Client,都强制走一遍这五件事 checklist —— 尤其是ClientId唯一性和心跳保活,踩过太多次坑。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/4 3:21:45

goInception MySQL审核工具部署与避坑指南

简介&#xff1a;goInception是一款面向MySQL数据库运维人员与DBA的轻量级SQL审核与执行工具&#xff0c;基于Go语言开发&#xff0c;融合SQL语法解析、规则化审核、在线执行、自动备份及回滚语句生成能力&#xff0c;可有效规避高危DDL/DML操作风险&#xff0c;适用于生产环境…

作者头像 李华
网站建设 2026/11/4 3:21:44

环保检测机构选择指南:资质识别与常见陷阱规避

1. 环保检测行业的现状与痛点环保检测行业近年来发展迅速&#xff0c;但同时也暴露出诸多问题。作为从业十余年的第三方检测机构负责人&#xff0c;我见过太多企业因为不了解行业规则而踩坑的案例。环保检测不同于普通的产品检测&#xff0c;它直接关系到企业的合规经营和持续发…

作者头像 李华
网站建设 2026/11/4 3:19:57

运维工程师高效复盘:10秒记录法与5分钟提炼术

1. 运维周复盘的价值与痛点作为一名从业十年的运维工程师&#xff0c;我深知这个岗位的日常状态&#xff1a;白天像消防员一样四处救火&#xff0c;晚上还要写那些让人头疼的周报。最痛苦的是&#xff0c;明明这周解决了不少问题&#xff0c;但到下周一早会时&#xff0c;却怎么…

作者头像 李华
网站建设 2026/11/4 3:19:27

逻辑回归正则化在金融风控中的实践与优化

1. 项目背景与问题定义信用卡失信检测是金融风控领域的核心课题之一。在实际业务中&#xff0c;我们常常遇到一个典型困境&#xff1a;模型在训练集上表现良好&#xff0c;但在真实场景中却漏掉了大量高风险用户。这种"实验室表现"与"实战效果"的落差&…

作者头像 李华
网站建设 2026/11/4 3:19:26

LSSVM与哈里斯鹰算法优化电容式力传感器温度补偿

1. 项目背景与核心问题电容式力传感器在工业自动化、医疗设备和航空航天等领域应用广泛&#xff0c;但其输出信号容易受到环境温度变化的影响。我在参与某型工业机器人研发时&#xff0c;曾遇到传感器读数随车间温度波动漂移达12%的情况&#xff0c;直接影响了装配精度。传统温…

作者头像 李华
网站建设 2026/11/4 3:18:36

Linux下MySQL C客户端头文件、库文件与多架构链接排错

简介&#xff1a;面向在 Linux 环境下做 C/C 开发、需要对接 MySQL 数据库的开发者&#xff0c;尤其是苦于缺少 mysql-devel 包、或不便在目标机上安装 MySQL 客户端与服务器的场景。资源是一套可直接随工程携带的 MySQL 连接头文件与库文件集合&#xff0c;同时提供 32 位和 6…

作者头像 李华