OpenRobot.Framework.Communication 1.2.2

dotnet add package OpenRobot.Framework.Communication --version 1.2.2
                    
NuGet\Install-Package OpenRobot.Framework.Communication -Version 1.2.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="OpenRobot.Framework.Communication" Version="1.2.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="OpenRobot.Framework.Communication" Version="1.2.2" />
                    
Directory.Packages.props
<PackageReference Include="OpenRobot.Framework.Communication" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add OpenRobot.Framework.Communication --version 1.2.2
                    
#r "nuget: OpenRobot.Framework.Communication, 1.2.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package OpenRobot.Framework.Communication@1.2.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=OpenRobot.Framework.Communication&version=1.2.2
                    
Install as a Cake Addin
#tool nuget:?package=OpenRobot.Framework.Communication&version=1.2.2
                    
Install as a Cake Tool

OpenRobot.Framework.Communication

MQTT 连接、IPC 跨进程通讯与 SignalR 长连接通用库(net8.0,单包内含 Mqtt / Ipc / Signalr 三个独立命名空间)。

  • MQTT:连接控制 + 自动重连 + 重连后重订阅 + 按序补发;订阅以 (Topic, Handler) 批量注册,收包经 Channel 消费队列派发;QoS / Retain / MQTT5 ResponseTopic 与 CorrelationData 可配。
  • IPC:一个服务端 + 多个客户端。服务端按握手 ClientId 维护会话表,定向发送 / 广播 / 单会话断开互不影响;Client 与 Session 接口同构,双向「单向发送(不等待返回)」与「请求-响应(带超时)」都可用;传输可在命名管道 / TCP 之间切换,也可用 MultiTransport 让同一个服务端同时监听多种传输(两种传输上的客户端在同一张会话表里,可互相路由)。信封层(Ipc.Envelope,1.2.0+)在其上做业务分发:按 topic 找处理器、按 target 点名投递给指定客户端,接收方可用 ClaimByTarget 决定认不认别人的名字。
  • SignalR:连接控制 + 自动重连 + 处理器跨重连存活;普通调用(异常 / Result 两种模式)、服务端→客户端 流式接收(Pull + Push 广播)、客户端→服务端 流式上传(含 byte[] 自动分片);空闲超时看门狗(长时间收不到业务数据可配置重连 / 停止 / 失败);授权令牌与自定义头预留。
  • 协议栈类型(MQTTnet、Microsoft.AspNetCore.SignalR.Client)不出现在公共 API:换掉协议栈不影响调用方。
  • 日志全部经 ICommunicationLogger,宿主可实现后接管输出(也支持直接用 Microsoft.Extensions.Logging);IPC 信封层是例外,它直接走 ILogger(见「版本历史」1.2.0)。
  • 无副作用:不写环境变量、不落盘、不生成持久状态;凭据只读,全部由宿主提供。

安装

<PackageReference Include="OpenRobot.Framework.Communication" Version="1.2.2" />

包由本仓库产出:dotnet pack common/OpenRobot.Framework.Communication/OpenRobot.Framework.Communication.csproj -c Release, 产物为 OpenRobot.Framework.Communication.1.2.2.nupkg(lib/net8.0 单个程序集)与配套 .snupkg。

当前版本 1.2.2(IPC 信封层:收端认领 + 逐帧报文日志)。版本号在 OpenRobot.Framework.Communication.csproj 的 <Version> 中维护, 后续发布会在此处一并升级(含上文引用与 版本历史 一节)。

依赖:

  • MQTTnet 4.3.7.1207(仅内部使用)
  • Microsoft.AspNetCore.SignalR.Client 10.0.10 + Microsoft.AspNetCore.SignalR.Protocols.MessagePack 10.0.10(仅内部使用)
  • Microsoft.Extensions.Logging.Abstractions 10.0.10
  • Microsoft.Extensions.DependencyInjection.Abstractions 10.0.10

⚠️ Microsoft.Extensions.Logging.Abstractions / DependencyInjection.Abstractions 与两个 SignalR 客户端包的下限已抬到 10.0.10(原 8.0.x)。 宿主若把其中任何一个钉在更低版本,升级本包时会吃到 NU1605(包降级)而编译失败, 跟着抬到 10.0.10 即可。但 Microsoft.Extensions.Hosting.Abstractions 不要跟着抬 —— 10.x 起 BackgroundService.ExecuteAsync 改跑在线程池上,任何「StartAsync 返回之后」的断言都会稳定失败。


快速开始

MQTT

using OpenRobot.Framework.Communication;
using OpenRobot.Framework.Communication.Mqtt;

var mqtt = new MqttClient(new MqttOptions
{
    Name = "primary",
    Broker = "127.0.0.1",
    Port = 1883,
    Logger = new ConsoleCommunicationLogger(),   // 或实现 ICommunicationLogger
});

mqtt.AddSubscriptions(
    // 处理完要回执:返回值非 null 即回执(带 CancellationToken 的重载用 (m, ct) => ...)
    new MqttSubscription("ota/down/#", HandleOtaDownAsync),
    new MqttSubscription("config/changed", msg => Console.WriteLine(msg.Payload)));

await mqtt.StartAsync();     // 非阻塞:见下方「StartAsync 不等首连」
await Task.Delay(500);
await mqtt.PublishAsync("ota/up", new { deviceId = "d1", state = "ok" });

static Task<string?> HandleOtaDownAsync(MqttMessage msg, CancellationToken ct)
    => Task.FromResult<string?>("{\"accepted\":true}");

IPC 服务端(一个端点,多个客户端)

using OpenRobot.Framework.Communication.Ipc;

await using var service = new IpcService(new IpcOptions { Name = "hub", PipePrefix = "EdgeControl" });

service.On("telemetry.report", inbound =>
{
    // inbound.SourceId 是发来这条消息的客户端 ClientId
    Console.WriteLine($"{inbound.SourceId}: {inbound.Message.Payload}");
    return Task.CompletedTask;
});
// 需要强类型载荷时显式指定泛型参数(否则 (payload, ctx) 双参 lambda 无法推断 T)
service.On<TelemetryReport>("telemetry.report", report => HandleReportAsync(report));

service.OnRequest("api.proxy.request", async ctx =>
{
    if (!await IsAuthorizedAsync(ctx.SourceId)) { await ctx.ReplyErrorAsync("unauthorized"); return; }
    await ctx.ReplyAsync(new { ok = true });
});

service.ClientConnected += (_, e) => Console.WriteLine($"上线: {e.Session.ClientId} @ {e.Session.RemoteEndpoint}");

await service.StartAsync();                       // 返回时已可连接
await service.SendToAsync("upgrader", "ota.dispatch", new { url = "https://..." });
await service.BroadcastAsync("config.changed", new { revision = 42 });
var reply = await service.RequestAsync("upgrader", "api.proxy.request", new { path = "/x" },
    TimeSpan.FromSeconds(30));

IPC 客户端(独立进程)

await using var client = new IpcClient(new IpcOptions
{
    Name = "upgrader",
    ClientId = "upgrader",        // 服务端按它区分会话
    PipePrefix = "EdgeControl",
    DefaultRequestTimeout = TimeSpan.FromSeconds(60),
});

client.On<OtaDispatch>("ota.dispatch", async task => await RunAsync(task));   // 不关心来源
await client.StartAsync();

// 客户端也能反向往服务端发请求(双向对称)
var cfg = await client.RequestAsync("config.get", new { key = "revision" });

IPC 服务端同时监听多种传输(MultiTransport)

一个 IpcService 默认只监听一种传输。要让同一个服务端同时接受命名管道与 TCP 的客户端, 并让它们互相路由(走 TCP 的 A 把消息发给走管道的 C),用 MultiTransport 把多个监听端点合成一个:

using OpenRobot.Framework.Communication.Ipc;
using OpenRobot.Framework.Communication.Ipc.Transport;

await using var service = new IpcService(new IpcOptions
{
    Name = "hub",
    TransportOverride = new MultiTransport(
        new NamedPipeTransport("EdgeControl.hub", maxInstances: 128),
        new TcpTransport("0.0.0.0", 9000)),
});

service.On<Telemetry>("telemetry.report", async (inbound, payload) =>
{
    // 会话表只有一张:只按 ClientId 路由,不关心对端走的是哪种传输
    await service.SendToAsync(payload.TargetClientId, "telemetry.forward", payload);
});

await service.StartAsync();   // 返回时两个端点都已可连接
// 之后 SendToAsync / BroadcastAsync / RequestAsync 对两种传输上的客户端一视同仁

客户端不用改 —— 各自按 Transport 选自己要用的那一种即可。MultiTransport 是服务端专用, 在客户端用它(ConnectAsync)会抛 NotSupportedException:连哪一个属于宿主业务决策, 组合器若自行挑选会让重连在多个端点间来回跳、故障现场无法复现。

SignalR

using OpenRobot.Framework.Communication.Signalr;

await using var hub = new SignalrClient(new SignalrOptions
{
    Name = "shell",
    HubUrl = "http://127.0.0.1:9900/signalr-remote-shell",
    LogTag = "Signalr.shell",
    Protocol = SignalrProtocolKind.MessagePack,   // 默认;两端必须一致
    ReconnectDelaysMs = [0, 2_000, 10_000, 30_000],   // 序列用尽后复用最后一项
    MaxReconnectAttempts = 0,                          // 0 = 真的无限
    IdleTimeoutMs = 60_000,                            // 0 = 不启用(默认)
    IdleAction = SignalrIdleAction.Reconnect,
});

// ① 服务端 → 客户端 事件:处理器跨重连存活
hub.On<string>("ShellOutput", text => { Console.WriteLine(text); return Task.CompletedTask; });

await hub.StartAsync();     // 非阻塞:只启动后台重连循环,不等首连结果

// ② 普通调用(异常模式 / Result 模式)
await hub.InvokeAsync("ResizeTerminal", sessionId, 120, 40);
var result = await hub.TryInvokeAsync<string>("GetVersion");   // 不抛,返回 Success/ErrorType

// ③ 服务端 → 客户端 流(Pull + Push 广播并存)
var stream = await hub.StreamAsync<string>("StreamShellOutput", ct, sessionId);
_ = Task.Run(async () =>
{
    await foreach (var chunk in stream.ReadAllAsync()) Console.Write(chunk);   // Pull
});
using var push = stream.Subscribe(chunk => Send(chunk));                       // Push,各自收全量

// ④ 客户端 → 服务端 流:任意长度 byte[] 自动按 16 KiB 分片
var upload = hub.CreateBinaryUploadStream("ReceiveShellOutput");
await upload.WriteAsync(rawBytes);   // 任意长度,内部切片
upload.Complete();                   // 或 upload.WriteAsync 后自然完成
await upload.WaitForCompletionAsync();

IPC 信封协议(OpenRobot.Framework.Communication.Ipc.Envelope)

IPC 帧只带一个固定类型(IpcWire.FrameType = "edge.message"),业务的一切都在信封里。

// 出方向:把业务载荷装进信封发出去
public sealed class MyHandler : IIpcMessageHandler
{
    private readonly IIpcEnvelopeSender _sender;   // ⚠️ 注 sender,不要注 gateway(会成 DI 环)

    public string Topic => "my.topic";

    public async Task HandleAsync(IpcMessageContext context, CancellationToken ct)
    {
        if (context.IsRequest) await context.ReplyAsync(new { ok = true }, ct);
        await _sender.SendAsync("other.topic", new { hello = "world" }, ct: ct);
    }
}
  • IpcEnvelope —— 五字段(topic / target / source / error / payload), 字段名 camelCase 是契约,服务端另有一份同源副本,改名会静默断开所有客户端。 业务 ID、业务码、业务错误说明全在 payload 里;error 只承载到不了处理器的失败。 target 是点名字段,完整链路见上一节
  • IpcWire —— 线格式常量(FrameType)
  • IIpcMessageHandler —— 入方向扩展点,一个 topic 一个实现类
  • IIpcConnectionHook —— 出方向连接钩子,每次连上后触发,须幂等
  • IIpcEnvelopeSender / IpcEnvelopeSender —— 出方向门面(单向 SendAsync + 请求-响应 RequestAsync / RequestToAsync,超时与未连接一律返回 null 不抛)。 其中 SendAsync 的 target 参数与 RequestToAsync 是点名(见上一节), RequestAsync 只发给服务端本机
  • 逐帧报文日志(1.2.2 起) —— 收、发各一条 Debug: [IPC 收] {Topic} source=… / [IPC 发] {Topic} target=…,载荷截前 200 字。 两条成对,用来回答「这条消息到底有没有出去 / 进来」,不必去翻各业务自己的日志 —— 业务没走到时,它那里与「消息没出去」长得一模一样。⚠️ 两条都是结构化日志 (Topic / Target 是字段,不是拼进字符串的),宿主可以据此在 ILogger 层按 topic 过滤或抑制高频业务;改成字符串插值会让那边的过滤静默失效 —— 不报错,只是不再生效
  • IIpcEnvelopeGateway / IpcEnvelopeGateway —— 入方向网关。一个进程只能注册一个: 库的 On 按帧类型建表、后注册者覆盖前者,网关独占注册后再按信封的 topic 二次分发。 它不继承 IIpcEnvelopeSender —— 想发消息就注入 sender
  • IpcMessageContext —— 处理器上下文。回执关联只认帧的 RequestId,与信封无关; 单向消息上调 ReplyAsync 是 no-op
  • IpcForwardedCommand / IpcUplinkMessage / IpcCorrelation —— 转发链路的载荷契约 (1.2.1 起)。宿主把云端下行转给第三方应用、对方把结果回传,用的就是这三个形状: 前两者是同形状的两个方向(identifier + payload + correlation), 后者承载「这条消息是哪条引出的」。改名的后果与 IpcEnvelope 一样 —— 静默断开对端。 它们是 payload 里的内容,不参与路由,宿主仍按信封的 topic 分发
  • 本机名(信封的 source)由 Attach 从 client.ClientId 取,不从配置取 —— 与握手报给服务端的是同一个值。构造函数因此不需要任何配置
  • ⚠️ 信封层固定用 IpcJson.Default 序列化载荷,不受 IpcOptions.Json 影响 —— IpcOptions.Json 只管 IPC 帧的序列化。别以为配了自定义转换器 (例如给 DateTime 换格式)就能让信封的 payload 跟着变:帧与信封会各按各的来

依赖方向:网关 → 处理器 → IIpcEnvelopeSender。业务处理器注入的必须是 sender —— 注 gateway 会让 DI 环在类型上成立,启动时才崩。(1.2.0 起 IIpcEnvelopeGateway 不再继承发送器,所以那种写法在 1.2.0+ 是编译错误,直接写不出来。)

点名发给指定客户端(信封的 target)

默认只看 topic:谁收到、谁处理,取决于对端注册没注册这个 topic。要让某一条消息只落到 指定客户端,在 topic 之外再点一个名字:

// 单向:点名给 cdspro,由服务端转发过去
await sender.SendAsync("ota.dispatch", new { url = "https://..." }, target: "cdspro");

// 请求-响应:点名给 cdspro 并等它回执(回执由服务端中继回来)
var reply = await sender.RequestToAsync("cdspro", "api.proxy.request", new { path = "/x" },
    TimeSpan.FromSeconds(30));

链路上有三段,只有首尾两段在库里:

  1. 出方向(本库) —— 把名字写进信封的 target 字段。null / 空表示不点名。
  2. 服务端转发(服务端实现,不在本库) —— 按自己那张会话表把帧点对点交给名字匹配的客户端; 名字不在线即丢弃,请求类消息另回一个 error 信封。target 为空的消息不进转发、 由服务端本机处理 —— 这正是 IpcEnvelope.Target 注释里那条契约。
  3. 接收端认领(本库,需显式打开) —— Target 非空且不等于本机名 → 丢弃,不进 topic 分发。

第 3 段由 IpcOptions.ClaimByTarget 控制:

await using var client = new IpcClient(new IpcOptions
{
    Name = "cdspro",
    ClientId = "cdspro",
    ClaimByTarget = true,      // 只认领点了自己名的消息
});

它默认关闭。关着时 Target 被忽略、只按 topic 分发,这是 1.2.1 及以前的行为 —— 打开等于给入方向多加一道过滤,会影响既有消费者,所以要用的场合自己开。打开之后, 「点给隔壁」的消息不会在本机先撞上一条「本机没有 topic=… 的处理器」:那条 Warn 会把排查 引向错误的方向(缺的到底是处理器,还是这条压根不是给我的?)。

几条要知道的口径:

  • 名字就是握手报出的 ClientId(ClientId → Name → 生成的 Guid 三级回退后的那个)。 服务端按它匹配会话表、信封的 source 取它、target 也填它 —— 三处同源, 填错的表现是「消息发得出去、回执转不回来」。
  • 空 Target 视为「谁都能收」,打开认领后照样进分发,广播类消息不受影响。
  • ⚠️ 认领忽略大小写,而服务端转发匹配会话表是区分大小写(Ordinal)。这个不一致是刻意的: 被转发送到就说明 Target 已经逐字等于本机名,认领这侧宽容一点只会多认领、不会漏认领 —— 而漏认领(把给自己的丢掉)才是要命的那个。
  • ⚠️ 打开之前先确认所有发送方点的是本机的 ClientId:名字对不上时消息会被静默丢弃, 只在 Debug 日志里留一行 [IPC 丢] … 未认领,不报错也不重试。

API 速查

IMqttClient

成员 说明
Name 连接名(MqttOptions.Name,多连接区分日志用)
IsConnected / State 是否已连接 / MqttConnectionState(Disconnected、Connecting、Connected、Reconnecting、Failed、Stopped)
ConnectionStateChanged 状态转换事件,一次转换触发一次
StartAsync(ct) 非阻塞启动 + 后台自动重连循环,幂等
ConnectAsync(ct) 单次连接(不等自动重连);失败会抛
StopAsync() 停止自动重连并断开,保留订阅注册,可再次 StartAsync
AddSubscriptions(...) 批量注册订阅,按 TopicFilter 去重(同 filter 只保留首个)
SubscribeAsync(filter, ct) 只确保订阅、不挂处理逻辑
UnsubscribeAsync(filter, ct) 从注册表摘除 + 移出重订阅清单(不发 UNSUBSCRIBE,见「已知限制」)
AddInterceptor(i) 注册收包前置拦截器(入队前按序同步执行)
PublishAsync(topic, payload[, options]) 未连接或发布失败时入补发队列,重连后按序补发

MqttSubscription 四个构造重载:Func<MqttMessage, CancellationToken, Task<string?>> 与 Func<MqttMessage, Task<string?>>(返回值非 null 且消息带 ResponseTopic 时自动回执)、Func<MqttMessage, Task>、Action<MqttMessage>(后两者不回复)。

带 Task<string?> 的两个重载必须都在:只有三参版时,一参 lambda m => Task.FromResult<string?>(json) 会因元数对不上而静默绑定到「不回执」的 Func<MqttMessage, Task>(Task<string?> 可隐式转 Task),回执凭空消失且不报错。

IIpcService(服务端)

成员 说明
Name / IsListening / ClientIds 端点名 / 是否监听中 / 在线客户端 ID 快照
ClientConnected / ClientDisconnected 会话建立 / 断开(含被新连接顶掉,每会话恰好一次)
StartAsync(ct) / StopAsync() 返回时端点已可连接;停止会断开全部会话(保留处理器,可重启)
GetSession(id) / GetSessions() 按 ClientId 取会话 / 取全部会话快照
SendToAsync(id, ...) 定向单向发送;客户端不在线返回 false(不缓存)
BroadcastAsync(...) 广播给全部在线会话;离线跳过,单会话失败不影响其余
RequestAsync(id, ...) 定向请求-响应;不在线或超时返回 null(不抛)
On / OnRequest(继承 IIpcMessageHandlers) 服务级处理器,对所有会话生效(含之后才连上的客户端);会话级同名处理器优先

IIpcClient / IIpcSession(共用 IIpcEndpoint)

成员 说明
Name Client 侧 = 自己的 ClientId;Session 侧 = 该客户端的 ClientId
IsConnected / State / ConnectionStateChanged 连接状态与状态事件(客户端每次重连成功都会触发)
SendAsync(message) / SendAsync<T>(type, payload) 单向发送:仅入队即返回(ValueTask),队列满丢最旧 + Warn;未连接时保留、连上后按序发出
RequestAsync(...) 请求-响应;超时或失败返回 null(不抛)
On / OnRequest 注册处理器(Session 侧仅对该客户端生效,且优先于服务级)
ClientId / ReconnectAttempts 仅 IIpcClient:握手标识 / 当前连续重连次数
RemoteEndpoint / ConnectedAt / Hello 仅 IIpcSession:对端地址 / 握手完成时间 / 握手载荷

处理器内可用 IpcInboundMessage.SourceId 判断来源;请求处理器用 IpcRequestContext.ReplyAsync / ReplyErrorAsync 回传(ReplyErrorAsync 回 {"success":false,"error":...},请求方快速失败而非等超时)。

__ipc. 前缀由库独占(握手帧 __ipc.hello),业务消息类型不得占用。

ISignalrClient

成员 说明
Name 连接名(SignalrOptions.Name,多连接区分日志用)
IsConnected / State / ConnectionId 是否已连接 / SignalrConnectionState(Disconnected、Connecting、Connected、Reconnecting、Failed、Stopped)/ 底层连接 ID
ConnectionStateChanged 状态转换事件,一次真实转换触发一次
TimeSinceLastActivity 距最近一次收到业务数据(On<T> 回调 / 流数据项 / Invoke 响应)的时长
StartAsync(ct) 非阻塞启动 + 后台自动重连循环,幂等
ConnectAsync(ct) 单次连接(连上才返回、失败抛出);不启动自动重连
StopAsync() 停循环并断开,保留处理器与配置,可再次 StartAsync
DisconnectAsync() 只断开、保留处理器(循环会在退避后重新连上)
InvokeAsync(...) ×4 普通调用,失败抛 SignalrConnectionException;用户主动取消原样抛 OperationCanceledException
TryInvokeAsync(...) ×2 Result 模式,业务失败不抛,返回 SignalrInvokeResult(含 ErrorType)
On<T1..T8>(method, handler) 注册服务端→客户端 处理器,返回的 IDisposable 只摘除这一个
RemoveHandler(method) / RemoveAllHandlers() 按方法名移除全部 / 清空全部
StreamAsync<T>(method, ct, args) 发起服务端→客户端 流,返回可 Pull / Push 的订阅(断线自动重建;懒建立,见下)
CreateUploadStream<T>(method, ct) 创建客户端→服务端 上传流(每个写入项 = 线上一个元素)
CreateBinaryUploadStream(method, chunkSize, ct) 客户端流式 byte[]:写入任意长度,内部按分片切开
IdleTimedOut 空闲超时事件(已按 IdleAction 处理完之后触发)

错误分类 SignalrErrorType:NotConnected / Timeout / ConnectionLost / ServerError / Unknown。

重连权威只有一处:本库不配置 SignalR 内建的 WithAutomaticReconnect,退避与尝试次数全部由 SignalrClient 的后台循环独占决策。因此 MaxReconnectAttempts 是真实计数,ReconnectDelaysMs 序列用尽后复用最后一项(不会像原生数组重载那样静默停止重连)。

StartAsync 不等首连:与 IMqttClient 同形。需要「连上再继续」请用 ConnectAsync,或订阅 ConnectionStateChanged 等 Connected。

StreamAsync 会等:连接处于 Connecting / Reconnecting / Disconnected 时等待连接就绪(「还没连上」与「连不上」不是一回事),只有进入 Failed / Stopped 才立即抛 NotConnected。ct 是等待上限的唯一手段。

流是懒建立的:StreamAsync 只登记订阅,首个消费者接入(Subscribe / ReadAllAsync)后才真的向服务端建流 —— 这样「建好流到消费者挂上」之间到达的数据不会静默丢失。代价是后接入的消费者只收得到从它接入那一刻起的数据;也意味着没有任何消费者时该订阅既不建立流、也不会完成(连接进入终态才结束)。

DI 集成

services.AddOpenRobotCommunication(o =>
{
    o.Logger = myLogger;                                   // 全局日志(可选)
    o.AddMqtt("primary", m => { m.Broker = "broker.local"; m.LogTag = "Mqtt.primary"; });
    o.AddIpcService("hub", i => { i.Name = "hub"; i.PipePrefix = "EdgeControl"; i.MaxClients = 32; });
    o.AddIpcClient("upgrader", i => { i.Name = "upgrader"; i.ClientId = "upgrader"; });
    o.AddSignalr("shell", s => { s.HubUrl = "http://127.0.0.1:9900/signalr-remote-shell"; s.LogTag = "Signalr.shell"; });
});

var client = sp.GetRequiredKeyedService<IMqttClient>("primary");
await client.StartAsync();
  • 每个 MQTT 连接 = keyed singleton IMqttClient;每个 IPC 端点 = keyed singleton IIpcClient / IIpcService;每个 SignalR 连接 = keyed singleton ISignalrClient。
  • 不自动连接 / 监听:注册只构造实例,由宿主在适当时机调用 StartAsync。
  • 同名重复登记后者覆盖前者:三种连接都按名索引,后登记的配置会替换先前的(不报错、不追加)。
  • 日志优先级:MqttOptions/IpcOptions/SignalrOptions.Logger > CommunicationOptions.Logger > 容器的 ILoggerFactory > 静默。
  • 容器必须异步释放:四个服务都是 IAsyncDisposable 且不实现 IDisposable,同步 ServiceProvider.Dispose() 会抛 InvalidOperationException("type only implements IAsyncDisposable. Use DisposeAsync to dispose the container.")。用 await using var sp = services.BuildServiceProvider();。

配置项

MqttOptions

项 默认 说明
Name "default" 连接名(日志 + MqttMessage.ClientName)
LogTag null → "Mqtt" 日志 tag
Broker / Port "" / 1883 broker 地址与端口
ClientId / Username / Password null 显式凭据(非空即生效,优先于 CredentialProvider)
KeepAliveSeconds 60 心跳间隔
ConnectTimeoutMs 10000 连接建立超时
UseTls / AllowUntrustedCertificate false / false TLS 开关与跳过证书校验(内网用,生产应关闭)
DefaultQuality AtMostOnce 发布默认 QoS
CleanSession true 不依赖 broker 持久会话,重连靠客户端侧重订阅 + 补发
ReconnectDelayMs 5000 重连检测间隔
MaxReconnectAttempts 0 0 = 不限;达上限置 Failed 并停止重连
PendingPublishCapacity 10000 补发队列上限(超限丢最旧 + Warn)
PendingPublishMaxRetry 3 单条最大补发次数(超限丢弃 + Error)
DispatcherQueueCapacity 10000 消费队列上限(满丢最旧 + Warn)
DispatcherConsumerCount 1 1 保证同连接内按到达顺序处理;>1 提速但放弃顺序保证
CredentialProvider null IMqttCredentialProvider,显式凭据未配置时调用
Logger null 日志实现(null 静默)

IpcOptions(Service 与 Client 共用)

项 默认 说明
Name "" Service 为监听名;Client 为默认标识名(见 ClientId)
LogTag null → "Ipc" 日志 tag
Transport NamedPipe NamedPipe(Windows 管道 / Linux UDS)或 Tcp
PipePrefix "OpenRobot" 管道名 / socket 文件前缀
TcpHost / TcpPort "127.0.0.1" / 0 Transport = Tcp 时生效
ClientId null Client 角色专用:为空取 Name,仍为空则进程内生成 Guid
ClaimByTarget false Client 角色专用:只认领 Target 为空或等于本机 ClientId 的信封,其余丢弃(见「点名发给指定客户端」)。Service 角色忽略
ReconnectDelayMs 3000 Client 重连间隔
ConnectTimeoutMs 10000 连接建立超时
HandshakeTimeoutMs 10000 Service 等待 hello 首帧的时限,超时关连接
MaxClients 128 Service 最大并发会话数(超出拒绝新连接 + Warn)。命名管道下实际封顶 252
OutboundQueueCapacity 10000 单向发送队列上限(满丢最旧 + Warn)
DefaultRequestTimeout 30s RequestAsync 未显式指定超时时的默认值
MaxFrameBytes 64 MB 单帧最大字节数(防御异常长度)
Json null → IpcJson.Default 载荷序列化配置(冻结为只读,运行期不可改)
TransportOverride null 自定义 IIpcTransport(测试 / 自定义传输 / 服务端多传输 MultiTransport;非 null 时忽略上面四个传输项)
Logger null 日志实现(null 静默)

写失败一律拆连接、不在原连接上重试:写失败可能已在线上留下半截帧,重试会把新帧追加进半截帧导致对端静默解析错乱。在途消息保留,由下一条连接优先补发。

SignalrOptions

项 默认 说明
Name "default" 连接名(多连接日志区分)
LogTag null → "Signalr" 日志 tag
HubUrl "" Hub 完整地址(含路径,如 http://host:9900/signalr-remote-shell);须为 http/https 绝对地址
ConnectTimeoutMs 30000 连接建立超时
HandshakeTimeout 15s SignalR 握手阶段超时
KeepAliveInterval 15s 心跳间隔
ServerTimeout 30s 传输层服务端超时;构建连接时校验 须 ≥ 2 × KeepAliveInterval
Protocol MessagePack MessagePack / Json;两端必须一致,否则握手被拒
ConfigureProtocolServices null 协议高级配置逃生舱(在 AddJsonProtocol/AddMessagePackProtocol 之后执行,Action<IServiceCollection> 形式,公共 API 不泄漏协议类型)
AccessTokenProvider null 授权预留:每次协商 / 重连时调用取最新令牌;库只读取,不获取 / 刷新 / 持久化
Headers null 授权预留:随请求发送的自定义头(如 Abp.TenantId)
AutoReconnect true 断线后是否自动重连
ReconnectDelaysMs [0, 2000, 10000, 30000] 第 N 次重连前等第 N 项;序列用尽后复用最后一项
MaxReconnectAttempts 0 0 = 无限;>0 时达上限置 Failed 并停止重连
IdleTimeoutMs 0 0 = 不启用(opt-in):连接期间超过该时长未收到业务数据则触发 IdleAction
IdleAction Reconnect Reconnect / Stop / Fail
IdleCheckIntervalMs 1000 看门狗检查周期(仅影响检测精度)
IdleProbe null 可选探活钩子:超时后先调用,返回 true 视为健康(重置计时、不动作)
StreamBufferCapacity 10000 服务端→客户端 流的每订阅者缓冲上限(满丢最旧 + Warn)
UploadBufferCapacity 10000 客户端→服务端 上传缓冲上限
UploadBufferMode DropOldest 上传缓冲满时策略(DropOldest / DropNewest / Wait)
BinaryChunkSize 16 KiB 二进制上传默认分片大小(见下方说明)
StreamRetryDelayMs 1000 流中断后重新建流前的等待
Logger null 日志实现(null 静默)

空闲超时的判定口径:「业务数据」= On<T> 事件回调、流式订阅数据项、Invoke 响应返回、连接建立;不含传输层心跳——心跳正常但业务静默正是它要抓的状态。副作用是:宿主没为某类服务端推送注册 On<T> 时,SignalR 客户端会静默丢弃那些消息,本库无从计为活动(「宿主没订阅的数据 = 没收到」)。不该断开就别开本项。

为什么 BinaryChunkSize 默认 16 KiB 而不是 64 KiB:SignalR 服务端的 HubOptions.MaximumReceiveMessageSize 默认只有 32 KB,超过即拒收该消息并断开连接。上行流式分片的每一片是独立的 Hub 消息,64 KiB 分片会被默认配置的服务端直接拒收。服务端若已放宽该限制可自行调大。本项只影响上行分片——MaximumReceiveMessageSize 只存在于服务端配置中,客户端程序集里没有这个设置。

授权为预留能力:本库不实现令牌获取 / 刷新 / 持久化,只提供 AccessTokenProvider 与 Headers 两个注入口,由宿主提供最新值。两条已知限制:WebSocket 长连接建立后不会刷新令牌(只在下一次协商 / 重连时刷新);SignalR 只在协商请求上带自定义头,WebSocket 握手走查询串(令牌除外)——需要全程生效的信息请放进令牌或 Hub 方法参数。


平台说明

  • Windows 命名管道:端点名为 {PipePrefix}.{Name}(如 EdgeControl.hub),保留原大小写;管道名不区分大小写。同一服务端的管道实例数受系统上限 254 约束(库会为「在途 Accept」与「待拒绝连接」各留 1 个实例,故会话数实际封顶 252)。需要更多并发会话请改用 Transport = Tcp。
  • Linux / macOS:System.IO.Pipes 自动映射为 Unix Domain Socket,端点名为 /tmp/{prefix}.{name}.sock(转小写——UDS 区分大小写,且路径总长受系统上限约 108 字节约束)。PipePrefix 与 Name 应保持简短,过长会导致连接失败。
  • TCP:跨机器可用,但本期为明文传输,无 TLS。仅建议在受信内网使用;需要跨不可信网络时应在链路上自行加隧道 / 加密,或等待后续 TLS 扩展点。
  • 多传输并存:MultiTransport(服务端专用)把多个监听端点合成一个 IIpcTransport,经 TransportOverride 注入即可 —— 它不改动 IpcService,因为会话层只认 Stream,本来就不关心流来自哪种传输。各传输共用同一张会话表,故 SendToAsync / BroadcastAsync / RequestAsync 天然跨传输可用。
  • 传输可测性:IIpcTransport 是接缝,InMemoryTransport 用于单进程内的确定性测试(TransportOverride 注入)。注意它是 1:1 配对,多次 AcceptAsync 会返回同一组通道,建模不出多条独立连接,多客户端用例请用真实传输。

已知限制

  1. UnsubscribeAsync 不发 UNSUBSCRIBE:内部 broker 接缝(IMqttBrokerConnection)只提供 Connect / Disconnect / Publish / Subscribe,没有 Unsubscribe。因此"注销订阅"只做「从注册表摘除 + 移出重订阅清单」——broker 可能仍在投递,但注册表中已无匹配项,派发器只记 Warn 而不调用任何处理器。需要真实退订需先扩接缝。
  2. MqttClient.StartAsync 不等首次连接:它只启动后台重连循环,首连失败不向上抛。await StartAsync() 返回后 IsConnected 仍可能为 false。需要「连上再继续」请用 ConnectAsync,或订阅 ConnectionStateChanged 等 Connected。宿主的启动就绪判断必须基于 State / ConnectionStateChanged。
  3. 容器需异步释放:见「DI 集成」一节。
  4. MQTT 消息顺序仅在 DispatcherConsumerCount = 1(默认)时保证;提高并发数即放弃该保证。
  5. IPC 单向发送不保证送达:SendAsync 只入队即返回,队列满时丢最旧(记 Warn);需要确认送达请用 RequestAsync。
  6. IPC 服务端不缓存离线消息:SendToAsync 对离线客户端返回 false;BroadcastAsync 跳过离线会话。
  7. IPC 握手不可省略:客户端连接后第一帧必须是 __ipc.hello,首帧非 hello / ClientId 为空 / 帧非法 / 超 HandshakeTimeoutMs 的连接一律被关闭且不建会话。
  8. 凭据只读:库不生成、不持久化、不写环境变量。ClientId 三级回退中最后一级是进程内随机 Guid,多实例场景应显式配置 ClientId。
  9. MqttClient 公开构造只有 (MqttOptions, IMqttCredentialProvider?):协议栈工厂与 logger 的注入构造为 internal(供同程序集与测试使用),宿主无法替换协议栈实现。
  10. SignalrClient.StartAsync 不等首次连接:同 MQTT。await StartAsync() 返回后 IsConnected 仍可能为 false;State 会依次走 Connecting → Connected(首连失败则一直重试)。
  11. StreamAsync 会等待连接就绪:未 StartAsync 又无超时的客户端上建流会一直等,需要「快速失败」请传带超时的 ct。
  12. 流是懒建立的:首个消费者接入后才向服务端建流(避免接入前到达的数据无人接住);后接入的消费者只收到接入之后的数据,且没有消费者时订阅不会建立流也不会完成。
  13. 重连后服务端流从头开始:流订阅断线会自动重建,但服务端会重新执行流方法,产生重复项。库不做内建去重,宿主按业务序号自行处理。
  14. 上传流不保证 at-least-once:缓冲区内的项重连后继续发送(至少一次),已取走在途的项断线即丢失、不重放(至多一次)。真 at-least-once 需要「未确认项队列 + 服务端 ack」的跨端协议。另外重连对服务端而言是一次全新的 Hub 调用(新会话),需服务端自行做幂等。
  15. 客户端无 MaximumReceiveMessageSize:该限制只存在于服务端 HubOptions(默认 32 KB)。下行无等价硬限制;上行分片受服务端限制约束,见 BinaryChunkSize。
  16. 空闲超时把「没订阅的数据」视作「没收到」:未注册 On<T> 的服务端推送被 SignalR 客户端静默丢弃,本库无从计为活动(见 SignalrOptions 一节)。
  17. ISignalrHubConnectionFactory 是 internal 接缝:测试 / 自定义连接构建经 InternalsVisibleTo 使用,宿主无法替换 Hub 连接实现。SignalrOptions.ConnectionOverride 同理。
  18. MultiTransport 是服务端专用:ConnectAsync 抛 NotSupportedException。客户端「连哪一个」由宿主决定,组合器自行挑选会让重连在端点间乱跳。
  19. MultiTransport 下 MaxClients 是所有传输的合计上限:各传输共用同一张会话表,故它是「两个端点加起来最多 N 个会话」,不是每个端点各 N 个。
  20. MultiTransport 下无法从会话反查来源传输:IIpcSession.RemoteEndpoint 是本端监听端点的拼接串(如 EdgeControl.hub | 0.0.0.0:9000),不是对端地址 —— 这一点在单传输时也一样,只是多传输下更明显。需要在日志 / 排查里区分某条会话走的是哪种传输,得自行包一层 Stream 带标记(OwnedStream 可作参考),或按 ClientId 约定区分。
  21. MultiTransport 不改变「新连接顶掉同 ClientId 旧会话」的语义:一个 TCP 客户端与一个管道客户端取同一个 ClientId 会互相顶掉,且是跨传输地顶掉。多传输场景请保证 ClientId 全局唯一。
  22. 收端认领默认关闭,target 只对「打开的接收方」生效:关着时 Target 被忽略,点名发给别人的消息在本机照样走一遍 topic 分发 —— 命中同名 topic 就顺手处理掉(A→B 的点对点消息被 C 也吃掉),不命中则记一条「本机没有 topic=… 的处理器」的 Warn。要真正隔离,接收方必须显式打开 ClaimByTarget。
  23. 打开认领后名字必须对上:Target 与握手 ClientId 对不上即静默丢弃,只在 Debug 留一行 [IPC 丢],不报错也不重试。发送方填的是配置里的名字,认领比的是三级回退后的 ClientId —— ClientId 留空时会取 Name、再空则取进程内生成的 Guid,两端对不上就永远收不到。

版本历史

1.2.2

本版在 IPC 信封层加了两件事,未改动任何既有类型的签名、字段名与线格式。

收端认领 —— target 终于有了接收方一侧的语义

此前 target 只影响服务端转发(点名发给谁),接收方一律不看它 —— 于是「点给别人的消息」 送到本机时,撞上的是「本机没有 topic=… 的处理器」那条 Warn,把排查引向「缺处理器」。

  • 新增 IpcOptions.ClaimByTarget(默认 false,即旧行为):打开后,Target 非空且不等于 本机名的消息直接丢弃、不进 topic 分发,只留一行 [IPC 丢] Debug 日志
  • 空 Target 视为「谁都能收」,始终放行 —— 广播类消息不受影响
  • 认领匹配用 OrdinalIgnoreCase,而服务端转发匹配会话表用 Ordinal。这个不一致是刻意的: 转发送到就说明 Target 已逐字等于本机名,认领这侧宽容只会多认领、不会漏认领
  • IpcEndpointBase.Options 由 protected 放宽为 protected internal,让同程序集的网关 能读到该开关;对外部消费者的可见性不变

逐帧报文日志

  • [IPC 收] {Topic} source=… / [IPC 发] {Topic} target=…,各一条 Debug,载荷截前 200 字, 与 MQTT 侧的 [MQTT 收] / [MQTT 发] 同一套门槛与读法
  • 此前只有「本机没有处理器」这类异常路径才留痕,正常收发的帧一行都不记 —— 对端说发了、这边什么都没看到时,在本进程里查不到任何线索
  • 两条保持结构化(Topic 是字段,不是插值):宿主的日志抑制要按 ILogger 的 state 过滤高频 topic,改成字符串插值会让那边的抑制静默失效。库不认识宿主配置, 所以抑制不在库里做
  • IpcEnvelopeGateway 的挂载日志补上 认领 Target={ClaimByTarget}

1.2.1

本版新增转发链路的载荷契约(仍在 OpenRobot.Framework.Communication.Ipc.Envelope 命名空间): IpcForwardedCommand / IpcUplinkMessage / IpcCorrelation。

代码自 EdgeRDS 的 EdgeRDS.Host.Ipc 抽入。此前这段契约在宿主(SmartEdgeHub)与第三方应用 (EdgeRDS)两侧各有一份手抄的副本,字段名靠人工对齐 —— 改一边忘一边的症状是静默解出空值, 两端都不报错。抽进库后它成为一份契约。

  • 类名是中性名,不带消费者前缀:Rds* 是某一家的名字,而同一个形状对另一家转发目标 (DcProxy)同样成立。类名不在线格式里,字段名才是 —— 所以这次改名不动任何报文
  • 三个类都补了显式 [JsonPropertyName],与 1.2.0 给 IpcEnvelope 补的是同一条理由: 不标就得靠调用方传对 camelCase 策略,传错会静默解出全 null
  • 仅新增类型,未改动任何既有类型与线格式。既有接入方升级到 1.2.1 不需要改一行代码, 换用新类型可在任意时刻单独进行
  • 依赖下限抬高:Microsoft.Extensions.Logging.Abstractions / DependencyInjection.Abstractions 与两个 SignalR 客户端包由 8.0.x 升到 10.0.10。代码不用改,但宿主若把其中任何一个钉在 更低版本,升级时会吃到 NU1605(包降级)而编译失败,跟随抬即可。 Microsoft.Extensions.Hosting.Abstractions 不在其中,也不要跟着抬 —— 10.x 起 BackgroundService.ExecuteAsync 改跑在线程池上,「StartAsync 返回之后」的断言会稳定失败

1.2.0

本版新增 IPC 信封协议层(OpenRobot.Framework.Communication.Ipc.Envelope 命名空间)。 代码自 SmartEdgeHub 的 EdgeHub.CloudHost 抽入,接入方不必再逐项目拷贝。

  • IpcEnvelope(五字段信封 DTO)/ IpcWire(线格式常量 edge.message)/ IpcMessageContext(处理器上下文 + 回执)
  • IIpcMessageHandler(入方向扩展点)/ IIpcConnectionHook(出方向连接钩子)
  • IIpcEnvelopeSender / IpcEnvelopeSender(出方向门面:单向 + 请求-响应)
  • IIpcEnvelopeGateway / IpcEnvelopeGateway(入方向网关,独占帧类型后按 topic 二次分发)
  • 本机名改由 Attach 从 client.ClientId 取,构造函数不再吃任何配置。 原来是读配置里的 ClientId,而库解析客户端标识用三级回退(ClientId → Name → 生成的 Guid)—— 两者在 ClientId 留空时结果不同,会让信封的 source(也就是回执的 Target)填错, 触发「消息发得出去、回执转不回来」的静默故障。现在两者同源
  • 信封层用 ILogger 记日志(与库其余部分用 ICommunicationLogger 的 Ipc / Mqtt 不同)—— 这层是从宿主应用抽上来的,保留了原有的日志形状
  • 公共 API 定型(发布后即为破坏性变更,故在本版落定): IIpcEnvelopeGateway 不再继承 IIpcEnvelopeSender(去掉让 DI 环在类型上合法的写法, 注错方向从「启动时崩」变成编译错误);IpcMessageContext 构造函数由 internal 放开为 public (接入方才能单测自己的处理器,不必起真管道);IpcEnvelope 五个属性补显式 [JsonPropertyName], 把「字段名就是契约」从文档搬进代码(裸 JsonSerializer 反序列化不再静默解出全 null)
  • 未改动 IpcClient / IpcService / IpcOptions / IpcFrame 等既有类型

1.1.0

本版新增 SignalR 模块与 IPC 多传输能力。

SignalR(OpenRobot.Framework.Communication.Signalr 命名空间)

以 WebApiProxy v2 的功能面为基线重写,不迁入 V1 的 ClientSignalrProxy / ISignalrOnEvent / ProtoLogLevel / Options.HubEvents。WebApiProxy 类库本身零改动。

  • 连接控制:StartAsync(非阻塞 + 自动重连)/ ConnectAsync(单次,失败抛出)/ StopAsync(保留处理器,可重启)/ DisconnectAsync,显式状态机与 ConnectionStateChanged 事件
  • 单一重连权威:不配置 SignalR 内建 WithAutomaticReconnect,退避与尝试次数由本库循环独占决策(MaxReconnectAttempts = 0 是真的无限,退避序列用尽后复用最后一项)
  • 处理器跨重连存活:连接对象全程只构建一次,On<T1..T8> 注册的处理器不因重连丢失;返回的句柄只摘除自己那一个
  • 普通调用双形态:InvokeAsync(异常传播)/ TryInvokeAsync(Result 模式)+ 错误分类 SignalrErrorType(NotConnected / Timeout / ConnectionLost / ServerError / Unknown);用户主动取消原样抛 OperationCanceledException
  • 服务端→客户端 流:StreamAsync 返回可 Pull(ReadAllAsync)可 Push(Subscribe)的订阅,广播语义(每消费者各收全量,不是瓜分),断线自动重建,onNext 异常逐项隔离
  • 客户端→服务端 流:CreateUploadStream(逐项)/ CreateBinaryUploadStream(任意长度 byte[] 自动分片),断线后缓冲内数据继续发送
  • 空闲超时看门狗:可按「多长时间收不到业务数据」配置自动重连 / 停止 / 置失败,附可选探活钩子
  • 协议可切 MessagePack / Json,ConfigureProtocolServices 逃生舱;授权预留 AccessTokenProvider / Headers
  • 多连接:CommunicationOptions.AddSignalr(name, …),各连接独立配置、独立状态

IPC

  • 新增 MultiTransport(服务端专用):把多个监听端点合成一个 IIpcTransport,经 IpcOptions.TransportOverride 注入后,同一个 IpcService 可同时接受命名管道与 TCP 的客户端连接,两种客户端之间可互相路由 —— 会话表只有一张,SendToAsync / BroadcastAsync / RequestAsync 对它们一视同仁
  • 未改动 IpcService / IpcSession / IpcOptions:会话层只认 Stream,本就不关心流来自哪种传输
  • 客户端无需改动:每个 IpcClient 仍按 IpcOptions.Transport 选自己要用的那一种
  • 多传输下的行为约束:MaxClients 为所有传输的合计上限;同一 ClientId 跨传输会互相顶掉,需保证全局唯一;会话的 RemoteEndpoint 是监听端点的拼接串,无法反查来源传输

通用

  • AddOpenRobotCommunication 支持 AddSignalr,SignalR 连接同样按 keyed singleton 注册
  • 依赖新增 Microsoft.AspNetCore.SignalR.Client 8.0.8 与 Microsoft.AspNetCore.SignalR.Protocols.MessagePack 8.0.8(程序集 Version → 1.1.0)
  • 公共 API 不含 SignalR 客户端类型(第三方类型全部收在 Signalr/Internal/)

1.0.0(首次发布)

MQTT

  • 连接控制:StartAsync(非阻塞 + 自动重连)/ ConnectAsync / StopAsync,显式状态机与 ConnectionStateChanged 事件
  • 断线自动重连,重连后重订阅全部 filter(幂等,不重复 SUBSCRIBE)+ 按序补发(上限 / 重试次数可配)
  • 订阅以 (Topic, Handler) 批量注册(TopicFilter 去重),收包经 Channel 消费队列派发,命中全部匹配订阅(按 filter 明确度降序)
  • 处理器返回值非 null 且消息带 ResponseTopic 时自动回执(带 CorrelationData);抛异常自动回错误载荷
  • IMqttMessageInterceptor 收包前置钩子(存档 / ACK 等)
  • QoS / Retain / ResponseTopic / CorrelationData 透传;IMqttCredentialProvider 只读凭据接入

IPC

  • 一个服务端 + 多个客户端:按握手 ClientId 建会话表,定向发送 / 广播 / 请求-响应 / 单会话断开互不影响;重复 ClientId 以新连接为准
  • IIpcClient 与 IIpcSession 同构(共用 IIpcEndpoint),双向通讯天然对称
  • 单向发送走 Channel 待发队列(不等待写出);请求-响应带超时(超时返回 null,不抛)
  • 客户端断线自动重连,每次重连重发握手帧
  • 传输可切命名管道(Windows)/ UDS(Linux、macOS)/ TCP(明文),IIpcTransport 为可替换接缝
  • 处理器支持服务级(对所有会话生效)/ 会话级(优先),4 种注册重载(含强类型载荷)

通用

  • ICommunicationLogger 日志抽象(含 ConsoleCommunicationLogger / MicrosoftCommunicationLogger / NullCommunicationLogger)
  • AddOpenRobotCommunication() DI 扩展(keyed singleton,不自动连接)
  • 公共 API 不含 MQTTnet / 源项目类型
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.2.2 90 9/28/2026
1.2.1 95 9/22/2026
1.2.0 97 9/22/2026
1.1.0 96 9/21/2026
1.0.0 96 9/18/2026