Repository: NewLifeX/NewLife.RocketMQ Branch: master Commit: 88a51ffbfe13 Files: 140 Total size: 1.3 MB Directory structure: gitextract_ldvbuhvt/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── copilot-instructions.md │ ├── instructions/ │ │ ├── benchmark.instructions.md │ │ ├── development.instructions.md │ │ └── net.instructions.md │ └── workflows/ │ ├── publish-beta.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── ChangeLog.md ├── DLL/ │ ├── NewLife.Core.xml │ └── NewLife.RocketMQ.xml ├── Doc/ │ ├── Changelog.md │ ├── RequestReply_Guide.md │ ├── newlife.snk │ ├── rmq_4.9.7.pcap │ ├── 架构设计.md │ └── 需求文档.md ├── LICENSE ├── NewLife.RocketMQ/ │ ├── .github/ │ │ └── copilot-instructions.md │ ├── AclOptions.cs │ ├── AclProvider.cs │ ├── AliyunOptions.cs │ ├── AliyunProvider.cs │ ├── BrokerClient.cs │ ├── ClusterClient.cs │ ├── Common/ │ │ ├── BrokerInfo.cs │ │ ├── ILoadBalance.cs │ │ └── WeightRoundRobin.cs │ ├── Consumer.cs │ ├── Grpc/ │ │ ├── GrpcClient.cs │ │ ├── GrpcEnums.cs │ │ ├── GrpcMessagingService.cs │ │ ├── GrpcModels.cs │ │ ├── GrpcServiceMessages.cs │ │ └── ProtoExtensions.cs │ ├── Helper.cs │ ├── HuaweiProvider.cs │ ├── ICloudProvider.cs │ ├── MessageTrace/ │ │ ├── AsyncTraceDispatcher.cs │ │ ├── MessageTraceHook.cs │ │ └── TraceModel.cs │ ├── Models/ │ │ ├── ConsumeEventArgs.cs │ │ ├── ConsumeFromWheres.cs │ │ ├── ConsumeTypes.cs │ │ ├── DelayTimeLevels.cs │ │ └── MessageModels.cs │ ├── MqBase.cs │ ├── MqSetting.cs │ ├── NameClient.cs │ ├── NewLife.RocketMQ.csproj │ ├── Producer.cs │ ├── Properties/ │ │ └── PublishProfiles/ │ │ └── FolderProfile.pubxml │ ├── Protocol/ │ │ ├── Command.cs │ │ ├── ConsumerData.cs │ │ ├── ConsumerRunningInfo.cs │ │ ├── ConsumerStates/ │ │ │ ├── ConsumerStatesModel.cs │ │ │ ├── MessageQueueModel.cs │ │ │ └── OffsetWrapperModel.cs │ │ ├── EndTransactionRequestHeader.cs │ │ ├── Header.cs │ │ ├── HeartbeatData.cs │ │ ├── LanguageCode.cs │ │ ├── MQVersion.cs │ │ ├── Message.cs │ │ ├── MessageExt.cs │ │ ├── MessageQueue.cs │ │ ├── MqCodec.cs │ │ ├── ProducerData.cs │ │ ├── PullMessageRequestHeader.cs │ │ ├── PullResult.cs │ │ ├── QueryResult.cs │ │ ├── RequestCode.cs │ │ ├── ResponseCode.cs │ │ ├── ResponseException.cs │ │ ├── SendMessageRequestHeader.cs │ │ ├── SendResult.cs │ │ ├── SendStatus.cs │ │ ├── SerializeType.cs │ │ ├── ServiceState.cs │ │ ├── SubscriptionData.cs │ │ └── TransactionState.cs │ └── TencentProvider.cs ├── NewLife.RocketMQ.sln ├── Readme.MD ├── Test/ │ ├── Program.cs │ └── Test.csproj └── XUnitTestRocketMQ/ ├── .github/ │ └── copilot-instructions.md ├── AliyunIssuesTests.cs ├── AliyunTests.cs ├── BasicTest.cs ├── BatchAckTests.cs ├── BatchMessageTests.cs ├── BroadcastOffsetTests.cs ├── BrokerFailoverTests.cs ├── BrokerInfoTests.cs ├── CloudProviderTests.cs ├── CommandTests.cs ├── CompressionTests.cs ├── ConcurrentConsumeTests.cs ├── ConsumeStatsTests.cs ├── ConsumerStatesModelTests.cs ├── ConsumerTests.cs ├── HeaderTests.cs ├── IPv6Tests.cs ├── MQVersionTests.cs ├── MQVersionUpdateTests.cs ├── ManagementTests.cs ├── MessageExtendedTests.cs ├── MessageId5xTests.cs ├── MessageQueueTests.cs ├── MessageTests.cs ├── MessageTraceTests.cs ├── ModelTests.cs ├── MqBasePropertyTests.cs ├── MqSettingTests.cs ├── MultiTopicTests.cs ├── NameClientTests.cs ├── OrderConsumeTests.cs ├── PopConsumeTests.cs ├── ProducerTests.cs ├── ProducerTracerTests.cs ├── ProtoTests.cs ├── ProtocolDataTests.cs ├── PullResultTests.cs ├── QueryMessageTests.cs ├── RequestHeaderTests.cs ├── RequestReplyTests.cs ├── ResponseExceptionTests.cs ├── RetryTests.cs ├── SQL92FilterTests.cs ├── SendResultTests.cs ├── SpanRefactorTests.cs ├── SupportApacheAclTest.cs ├── TraceModelTests.cs ├── TransactionCheckTests.cs ├── VipChannelTests.cs ├── WeightRoundRobinTests.cs └── XUnitTestRocketMQ.csproj ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # EditorConfig is awesome:http://EditorConfig.org # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference # top-most EditorConfig file root = true # Don't use tabs for indentation. [*] indent_style = space # (Please don't specify an indent_size here; that has too many unintended consequences.) # Code files [*.{cs,csx,vb,vbx}] indent_size = 4 insert_final_newline = false charset = utf-8-bom end_of_line = crlf # Xml project files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] indent_size = 2 # Xml config files [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] indent_size = 2 # JSON files [*.json] indent_size = 2 # Dotnet code style settings: [*.{cs,vb}] # Sort using and Import directives with System.* appearing first dotnet_sort_system_directives_first = true csharp_indent_case_contents = true csharp_indent_switch_labels = true csharp_indent_labels = flush_left #csharp_space_after_cast = true #csharp_space_after_keywords_in_control_flow_statements = true #csharp_space_between_method_declaration_parameter_list_parentheses = true #csharp_space_between_method_call_parameter_list_parentheses = true #csharp_space_between_parentheses = control_flow_statements, type_casts # 单行放置代码 csharp_preserve_single_line_statements = true csharp_preserve_single_line_blocks = true # Avoid "this." and "Me." if not necessary dotnet_style_qualification_for_field = false:warning dotnet_style_qualification_for_property = false:warning dotnet_style_qualification_for_method = false:warning dotnet_style_qualification_for_event = false:warning # Use language keywords instead of framework type names for type references dotnet_style_predefined_type_for_locals_parameters_members = false:suggestion dotnet_style_predefined_type_for_member_access = false:suggestion #dotnet_style_require_accessibility_modifiers = for_non_interface_members:none/always:suggestion # Suggest more modern language features when available dotnet_style_object_initializer = true:suggestion dotnet_style_collection_initializer = true:suggestion dotnet_style_coalesce_expression = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_explicit_tuple_names = true:suggestion dotnet_style_prefer_inferred_tuple_names = true:suggestion dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion # CSharp code style settings: [*.cs] # Prefer "var" everywhere csharp_style_var_for_built_in_types = true:warning csharp_style_var_when_type_is_apparent = true:warning csharp_style_var_elsewhere = true:warning # Prefer method-like constructs to have a block body csharp_style_expression_bodied_methods = when_on_single_line:suggestion csharp_style_expression_bodied_constructors = when_on_single_line:suggestion csharp_style_expression_bodied_operators = when_on_single_line:suggestion # Prefer property-like constructs to have an expression-body csharp_style_expression_bodied_properties = true:suggestion csharp_style_expression_bodied_indexers = true:suggestion #csharp_style_expression_bodied_accessors = true:suggestion # Suggest more modern language features when available csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion csharp_style_pattern_matching_over_as_with_null_check = true:suggestion csharp_style_inlined_variable_declaration = true:suggestion csharp_prefer_simple_default_expression = true:suggestion csharp_style_deconstructed_variable_declaration = true:suggestion csharp_style_pattern_local_over_anonymous_function = true:suggestion csharp_style_throw_expression = true:suggestion csharp_style_conditional_delegate_call = true:suggestion # 单行不需要大括号 csharp_prefer_braces = false:suggestion # Newline settings csharp_new_line_before_open_brace = all csharp_new_line_before_else = true csharp_new_line_before_catch = true csharp_new_line_before_finally = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_between_query_expression_clauses = true [*.md] trim_trailing_whitespace = false ================================================ FILE: .gitattributes ================================================ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto ############################################################################### # Set default behavior for command prompt diff. # # This is need for earlier builds of msysgit that does not have it on by # default for csharp files. # Note: This is only used by command line ############################################################################### #*.cs diff=csharp ############################################################################### # Set the merge driver for project and solution files # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS # the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below ############################################################################### #*.sln merge=binary #*.csproj merge=binary #*.vbproj merge=binary #*.vcxproj merge=binary #*.vcproj merge=binary #*.dbproj merge=binary #*.fsproj merge=binary #*.lsproj merge=binary #*.wixproj merge=binary #*.modelproj merge=binary #*.sqlproj merge=binary #*.wwaproj merge=binary ############################################################################### # behavior for image files # # image files are treated as binary by default. ############################################################################### #*.jpg binary #*.png binary #*.gif binary ############################################################################### # diff behavior for common document formats # # Convert binary document formats to text before diffing them. This feature # is only available from the command line. Turn it on by uncommenting the # entries below. ############################################################################### #*.doc diff=astextplain #*.DOC diff=astextplain #*.docx diff=astextplain #*.DOCX diff=astextplain #*.dot diff=astextplain #*.DOT diff=astextplain #*.pdf diff=astextplain #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain ================================================ FILE: .github/copilot-instructions.md ================================================ # NewLife Copilot 协作指令 适用于 NewLife 系列全部 C#/.NET 仓库,本文件可随仓库/指令目录一起拷贝到其他项目直接复用。存在本文件则必须遵循。**简体中文回复。** 通用 C# 最佳实践(设计模式、SOLID、健壮性等)AI 已知,此处不赘述,**仅列出组织专属规则与反常规约定**。 --- ## 1. 专用指令(前置检查,必须执行) **开始任何任务前,必须先将用户请求与下表触发信号逐行匹配。命中则立即用 `get_file` 读取 `.github/instructions/{指令文件}`,读取成功后遵循其中全部规则。未命中任何行才跳过。** | 触发信号(用户请求含以下任意关键词即命中) | 指令文件 | |---------|---------| | XCode/实体生成/Model.xml/数据库 CRUD/`NewLife.XCode` 引用/`*.xcode.xml`/项目名含 `.Data`/`XCode.*` 命名空间/用户提及修改任意 `.xml` 文件 | `xcode.instructions.md` | | Cube/魔方/Web开发/`NewLife.Cube` 引用/`NewLife.Cube.*` 命名空间 | `cube.instructions.md` | | 性能测试/基准测试/压力测试/压测/BenchmarkDotNet/Benchmark/benchmark/吞吐量评估/性能分析/性能对比/性能报告/速度对比/速度测试/内存分配/perf/性能优化测试/做性能/跑分/测试报告 | `benchmark.instructions.md` | | NetServer/NetSession/网络服务器/网络客户端/Socket服务/TCP服务/UDP服务/`NewLife.Net` 引用/`NewLife.Net.*` 命名空间/ISocketClient/ISocketRemote/CreateRemote/StandardCodec/LengthFieldCodec/管道编解码/网络编程/Echo服务/网络会话/长连接/粘包拆包 | `net.instructions.md` | | 新建系统/新建项目/新增模块/需求整理/需求文档/需求分析/架构设计/技术方案/功能清单/功能拆分/任务分解/迭代开发/迭代计划/验收/PRD/用户故事/做一个系统/做一个平台/开发流程/全部搞完/批量开发/自治模式/一次性做完/继续处理/接着做 | `development.instructions.md` | | 缓存/ICache/MemoryCache/Redis缓存/ICacheProvider/缓存设计/`NewLife.Caching` 命名空间 | `caching.instructions.md` | | 序列化/JSON/Binary/JsonHelper/序列化设计/SpanSerializer/CSV导出/`NewLife.Serialization` 命名空间 | `serialization.instructions.md` | | 加密/安全/Hash/MD5/SHA/AES/SM4/RSA/JWT/SecurityHelper/TokenProvider/`NewLife.Security` 命名空间 | `security.instructions.md` | | 远程调用/ApiHttpClient/ApiClient/ApiServer/负载均衡/LoadBalancer/RPC/HTTP客户端/`NewLife.Remoting` 命名空间 | `remoting.instructions.md` | | 配置/Config/IConfigProvider/HttpConfigProvider/CommandParser/配置中心/`NewLife.Configuration` 命名空间 | `configuration.instructions.md` | **自动匹配指令**(无需触发,按 `applyTo` 路径自动生效):`caching`、`serialization`、`security`、`remoting`、`configuration` 这 5 个指令文件同时配置了 `applyTo` 模式,编辑对应目录下的文件时 VS Code 会自动加载。 --- ## 2. 核心原则 检索优先、风格一致、兼容友好、**主动优化**。 发现明显缺陷(资源泄漏、空引用、逻辑错误)时主动修复;优化请求时深入分析,不做表面工作。 改动较小直接做并说明;改动较大(涉及公共 API 或大范围重构)先列方案询问确认。 --- ## 3. 兼容性约束(极重要) - **语言版本**:当前为 **C# 14**(`latest`),最大化使用最新语法糖(switch 表达式、集合表达式 `[]`、`?.`/`??`/`??=`、模式匹配、目标类型 `new`、record 等) - **框架版本**:新增 API 前,先查看当前项目 `.csproj` 的 `` 配置,**只需满足已声明版本的兼容性**,无需对所有历史版本降级。若包含 `net45`/`netstandard2.0` 等低版本,再提供条件编译降级实现。 - **禁止高版本专属 BCL API**(低版本项目):❌ `ArgumentNullException.ThrowIfNull()` → ✅ `if (x == null) throw new ArgumentNullException(nameof(x));` - **条件编译符号**:`NETFRAMEWORK`、`NETSTANDARD2_0`、`NETCOREAPP`、`NET5_0_OR_GREATER`、`NET6_0_OR_GREATER`、`NET8_0_OR_GREATER` --- ## 4. 编码规范 ### 4.1 类型名(关键差异) **必须**使用 .NET 正式名:`String`/`Int32`/`Boolean`/`Int64`/`Double`/`Object` 等。 ❌ **禁止**使用 C# 别名:`string`/`int`/`bool`/`long`/`double`/`object` ### 4.2 命名 | 成员类型 | 规则 | 示例 | |---------|------|------| | 类型/公共成员 | PascalCase | `UserService`、`GetName()` | | 参数/局部变量 | camelCase | `userName`、`count` | | 私有字段 | `_camelCase` | `_cache`、`_instance` | | 扩展方法类 | `xxxHelper` 或 `xxxExtensions` | `StringHelper`、`CollectionExtensions` | ### 4.3 代码风格 - **命名空间**:file-scoped namespace - **单文件**:每文件一个主要公共类型;较大平台差异使用 `partial` - **集合初始化**:优先使用集合表达式 `[]`,如 `List Tags { get; set; } = [];` - **Null 条件运算符**:优先使用 `?.`/`??` 简化空值检查;**C# 14 空条件赋值 `??=`**:变量为 null 时才赋值,可显著提升可读性 ```csharp // ✅ C#14 空条件赋值(??=):为 null 时才赋值,替代 if (x == null) x = ... _cache ??= new MemoryCache(); list ??= []; // ✅ if 内只有单行代码时可不加花括号(单行 if 同行或换行均可) if (value == null) return; if (key == null) throw new ArgumentNullException(nameof(key)); // ✅ 语句较长时另起一行,仍不加花括号 if (value == null) throw new ArgumentNullException(nameof(value), "Value cannot be null"); // ✅ 多分支单语句:不加花括号 if (count > 0) DoSomething(); else DoOther(); // ✅ for/foreach/while 循环体必须保留花括号(即使单语句) foreach (var item in list) { Process(item); } for (var i = 0; i < count; i++) { Process(i); } // ✅ using 优先无花括号声明;仅需生命周期(如锁)时用弃元 using var stream = File.OpenRead("file.txt"); using var _ = _lock.AcquireLock(); ``` ### 4.4 Region 与日志 较长类使用 `#region` 分段,顺序:`属性` → `静态` → `构造` → `方法` → `辅助` → **`日志`**。 含 `ILog Log` 和 `WriteLog` 时:**必须放类末尾**,用名为"日志"的 region 包裹,不放入"辅助"。 关键过程可使用 `Tracer?.NewSpan()` 埋点。 ### 4.5 文档注释 - `` **必须同行闭合**:`/// 获取名称` - 每个参数**必须有** `` 标签,无论方法可见性 - 有返回值**必须有** ``;复杂方法可增加 `` - `public`/`protected` 成员必须注释;`[Obsolete]` 必须包含迁移建议 ### 4.6 异步与性能 - 异步方法后缀 `Async`,库内部默认 `ConfigureAwait(false)` - 热点路径避免反射/复杂 Linq,优先手写循环/`ArrayPool`/`Span` - 池化资源明确获取/归还,异常分支不遗失归还 ### 4.7 错误处理 - 精准异常类型:`ArgumentNullException`/`InvalidOperationException` 等 - TryXxx 模式:不用异常作常规分支 - 类型转换:优先使用 `Utility` 扩展方法,完整列表:`ToInt()`/`ToLong()`/`ToDouble()`/`ToDecimal()`/`ToBoolean()`/`ToDateTime()`/`ToDateTimeOffset()` - 对外异常不暴露内部实现/路径 --- ## 5. NewLife 内置工具 优先使用项目内置工具而非标准库,**禁止重复造轮子**: - 字符串构建:`Pool.StringBuilder`(替代 `new StringBuilder()`) - 时间戳(毫秒级相对时间):`Runtime.TickCount64`;**代码计时(精确耗时测量):`Stopwatch`** - 类型转换:`Utility` 扩展方法 — `ToInt()`/`ToLong()`/`ToDouble()`/`ToDecimal()`/`ToBoolean()`/`ToDateTime()`/`ToDateTimeOffset()` - 二进制读写:`SpanReader` / `SpanWriter`(替代手动字节偏移操作) - 追踪埋点:`Tracer?.NewSpan()` --- ## 6. 防御性注释(禁止删除) 代码中带有说明文字的被注释代码属于**防御性注释**,记录历史踩坑经验。**禁止删除,禁止"恢复"执行**。可补充更详细说明。 ```csharp // 曾经尝试过同步等待,但会导致线程池饥饿和死锁 // var result = task.Result; // 不要使用 SendAsync 的无超时重载,否则会造成连接泄漏 // await client.SendAsync(data); ``` --- ## 7. 工作流 触发检查(第 1 节触发信号表匹配,命中则读取专用指令) → 检索(**优先复用**现有实现) → 评估(公共 API/兼容性/性能) → 方案 → 实施 → 验证 → **AskQuestions 多选确认** → [需调整则循环] → 说明 - **触发检查**:开始工作前必须完成,遗漏专用指令将导致输出不符合要求 - **实施**:完成主任务;顺带修复明显缺陷;顺带简化重复代码;保留原注释与结构 - **验证**:代码变更必须编译通过;找到相关测试则运行;仅文档变更可跳过 - **AskQuestions 多选确认**:使用 `vscode_askQuestions` 工具,多选询问用户是否满意;若需调整则修改后重新编译,循环至满意;确认满意后才进入下一项开发 ### 主动优化原则 用户要求**分析/优化代码**时: | 行动 | 说明 | |------|------| | **架构梳理** | 重构不清晰的结构,让代码更易懂 | | **缺陷修复** | 资源泄漏、空引用、并发问题、逻辑错误 → 直接修复 | | **代码简化** | 提取重复代码、合并冗余判断、应用现代语法 | | **性能优化** | 缓存重复计算、池化高频对象、避免无用分配 | | **注释完善** | 补充缺失的 XML 注释和关键逻辑说明 | --- ## 8. 测试 - 框架 xUnit;类名 `{ClassName}Tests`;方法加 `[DisplayName("中文描述意图")]` - 网络端口用 `0`/随机,IO 用临时目录 - 先搜索 `{ClassName}` 引用定位测试文件,再找 `{ClassName}Tests.cs`;**未找到需说明**,不自动创建测试项目 --- ## 9. 文档与发布 ### Markdown 文档 **UTF-8 无 BOM**;存放 `Doc/` 目录;文件名优先中文,内容优先简体中文,避免乱码。**已有文件必须先读取再增量修改,禁止覆盖。** > 代码注释同样要求 UTF-8 无 BOM,优先简体中文。生成或编辑任何文件时须确保编码正确,防止中文乱码。 ### NuGet 版本 | 类型 | 格式 | 示例 | |------|------|------| | 正式版 | `{主}.{子}.{年}.{月日}` | `11.9.2025.0701` | | 测试版 | `{主}.{子}.{年}.{月日}-beta{时分}` | `11.9.2025.0701-beta0906` | --- ## 10. 重要禁止项 以下是 AI 容易犯但在本项目影响严重的错误: - 将 `String`/`Int32` 改为 `string`/`int`(本项目反 C# 惯例,**必须用正式名**) - 删除防御性注释(带说明的注释代码) - 删除 for/foreach/while 循环体的花括号(**循环体必须有花括号,即使只有一行**) - 将 `` 拆成多行 - 擅自删除 `public`/`protected` 成员 - 擅自新增外部 NuGet 依赖(需说明理由) - 仅删除空白行/注释制造"格式优化"提交 - 虚构不存在的 API/文件/类型 - 伪造测试结果/性能数据 - 在热点路径添加未缓存反射/复杂 Linq - 输出敏感凭据/内部地址 - 发现问题却视而不见 - 用户要求优化时仅做注释/测试等表面工作 - **跳过第 1 节触发检查**(命中关键词却未加载专用指令文件,是最严重的遗漏错误) --- ## 11. 变更说明模板 ```markdown ## 概述 做了什么 / 为什么 ## 影响 - 公共 API:是/否 - 性能影响:无/有(说明) ## 兼容性 降级策略 / 条件编译点 ## 风险与后续 潜在回归 / 是否补测试 ``` --- ## 12. Skills 技能文件 `.github/skills/` 目录下的技能文件提供特定领域的详细使用指南和代码示例,用户可在 Copilot Chat 中通过 `#` 引用。 | 技能文件 | 覆盖领域 | |---------|---------| | `caching.skill.md` | ICache/MemoryCache/Redis 统一缓存接口 | | `logging-tracing.skill.md` | ILog/XTrace 日志与 ITracer/DefaultTracer 链路追踪 | | `networking.skill.md` | NetServer/NetSession TCP/UDP/WebSocket 网络编程 | | `serialization.skill.md` | JSON/Binary/Span/CSV 序列化 | | `configuration.skill.md` | Config<T>/IConfigProvider/HttpConfigProvider 配置管理 | | `http-client.skill.md` | ApiHttpClient 多节点 HTTP 客户端与负载均衡 | | `dependency-injection.skill.md` | ObjectContainer/Host/Plugin/Actor 依赖注入与宿主 | | `timer-scheduling.skill.md` | TimerX/Cron 高级定时调度 | | `security.skill.md` | Hash/AES/SM4/RSA/JWT/TokenProvider 安全与加密 | | `type-conversion.skill.md` | ToInt/ToBoolean/StringHelper/Pool.StringBuilder 类型转换与工具 | --- ## 13. Agents 智能代理 `.github/agents/` 目录下定义了专用 AI 代理角色,用户可在 Copilot Chat 中通过 `@` 调用。 | 代理文件 | 用途 | |---------|------| | `newlife-expert.agent.md` | NewLife 组件专家:功能查询、组件推荐、编码指导 | | `code-review.agent.md` | 代码审查:按 NewLife 规范 8 维度检查代码 | | `project-init.agent.md` | 项目初始化:按模板创建新 NewLife 项目结构 | --- (完) ================================================ FILE: .github/instructions/benchmark.instructions.md ================================================ --- applyTo: "**/Benchmark/**" --- # 性能测试指令 适用于性能测试、压力测试、基准测试、BenchmarkDotNet 相关任务。 --- ## 1. 项目结构 - 基准测试统一放在 `Benchmark/` 项目,按主题分子目录(如 `PacketBenchmarks/`、`CacheBenchmarks/`) - 入口 `Program.cs` 使用 `BenchmarkSwitcher` 模式,**不要修改** - TFM 使用最新稳定版,`latest` ## 2. 代码规范 遵循主指令全部编码规范(类型名用 `String`/`Int32` 等、file-scoped namespace),另有以下补充: - **命名空间**:`Benchmark.{主题}Benchmarks` - **类名**:`{被测类型}Benchmark` 或 `{被测主题}Benchmark` - **必须标注** `[MemoryDiagnoser]` 和 `[SimpleJob]`(需调整迭代次数时用 `[SimpleJob(iterationCount: N)]`) - **方法描述**:`[Benchmark(Description = "中文描述")]`,方便报告阅读 - **参数化**:用 `[Params]` 或 `[ParamsSource]` 控制数据规模 - **初始化 / 清理**:分别放 `[GlobalSetup]` 和 `[GlobalCleanup]` - **分组**:同类测试用 `#region` 分组 - **多线程并发**:动态线程数包含 CPU 核心数,推荐模板: ```csharp public static IEnumerable ThreadCounts { get { var cores = Environment.ProcessorCount; var set = new SortedSet { 1, 4, 8, 32 }; set.Add(cores); return set; } } [ParamsSource(nameof(ThreadCounts))] public Int32 ThreadCount { get; set; } ``` ## 3. 运行要求 - 必须以 **Release 模式**运行,获取有代表性的峰值数据 - 运行全部:`dotnet run -c Release` - 运行指定类:`dotnet run -c Release -- --filter *ClassName*` - ❌ 禁止在 Debug 模式下采集数据写入报告 ## 4. 测试维度 - **并发维度**:单线程 + 多线程(多线程含与当前 CPU 核心数相同的并发数) - **操作维度**:单一操作 + 批量操作 ## 5. 常见错误 - ❌ 在 `[Benchmark]` 方法内做初始化(应放 `[GlobalSetup]`) - ❌ 忽略返回值导致 JIT 死码消除(确保返回或赋值给字段) - ❌ 手动 `Stopwatch` 计时(BDN 自动处理) - ❌ `using` 的 `Dispose` 开销混入测量(仅在测试 Dispose 本身时才包含) ## 6. 报告存放 `Doc/Benchmark/{测试主题}性能测试.md`(UTF-8 无 BOM) ## 7. 报告结构(顺序固定) 1. **性能概览**(放最前:一句话点明被测功能用途 + 简单语言总结核心发现) 2. 测试环境 → 测试方法 → 测试结果(BDN 原始表格,保留 Mean/Error/StdDev/Allocated) 3. 核心指标(换算 msg/s、QPS 等业务指标) 4. **对比分析** - 纵向:同场景不同并发趋势,找出最优并发点 - 横向:不同方案同并发差异百分比 5. 性能瓶颈定位(按重要程度排序)→ 优化建议(含预期收益与内存节省预估) ## 8. 性能瓶颈定位规范 性能瓶颈定位章节是报告的核心价值输出,必须遵循以下规范: ### 8.1 瓶颈点结构(每个瓶颈必须包含) 每个瓶颈点必须包含以下要素,缺一不可: | 要素 | 说明 | 示例 | |------|------|------| | **优先级标签** | P0/P1/P2/P3,按影响程度降序 | P0 | | **瓶颈名称** | 一句话准确描述瓶颈 | VisitTime 写入触发 MESI 缓存行争用 | | **优化收益占比** | 该瓶颈在总体可优化空间中的占比 | ~35% | | **现象与数据** | 用 BDN 实测数据量化问题严重程度 | 4T→8T 扩展仅 1.3x,低于预期 2.0x | | **根因分析** | 从代码执行路径分析到底层硬件行为 | Get 每次写 VisitTime → 缓存行 Modified → 多核 MESI 失效 | | **开销占比估算** | 在单次操作总耗时中的占比 | 占 Get 总耗时 30%~40% | | **内存影响** | 每次操作的额外内存分配或 GC 压力 | 48 B/次装箱分配,32 线程累计 3 MB | | **优化方向** | 具体可落地的优化方案 | 时间窗口内跳过更新(如 1s 内不重复写) | | **预期收益** | 速度提升倍数 + 内存节省比例 | 多线程吞吐 +20-30%,消除缓存行争用 | ### 8.2 瓶颈分级标准 | 级别 | 定义 | 优化收益占比 | 行动 | |------|------|------------|------| | **P0** | 影响核心吞吐或造成 >30% 性能损失 | ≥25% | 必须优化 | | **P1** | 影响多线程扩展性或造成显著内存压力 | 15%~25% | 建议优化 | | **P2** | 特定场景下的次要瓶颈 | 5%~15% | 可选优化 | | **P3** | 微小开销,仅在极端场景有影响 | <5% | 记录备查 | ### 8.3 瓶颈定位表格模板 性能瓶颈定位章节使用以下统一表格格式: ```markdown ### 核心瓶颈点总览 | 优先级 | 瓶颈 | 优化收益占比 | 当前开销 | 优化后预估 | 内存节省 | |--------|------|------------|---------|-----------|---------| | P0 | {瓶颈名称} | ~{X}% | {耗时/分配} | {目标值} | {节省比例} | | P1 | {瓶颈名称} | ~{X}% | {耗时/分配} | {目标值} | {节省比例} | | ... | ... | ... | ... | ... | ... | ``` ### 8.4 内存优化方向表格模板 紧跟瓶颈总览表之后,补充内存优化方向: ```markdown ### 关键内存优化方向 | 优先级 | 优化方向 | 当前分配 | 优化后预估 | 节省比例 | 实施方案 | |--------|---------|---------|-----------|---------|---------| | P0 | {方向} | {X} B/op | {Y} B/op | {Z}% | {方案} | | ... | ... | ... | ... | ... | ... | ``` ### 8.5 开销拆解要求 对每个核心操作,必须给出开销来源拆解表: ```markdown | 开销来源 | 占比估算 | 耗时估算 | 说明 | |---------|---------|---------|------| | {来源1} | ~{X}% | ~{N} ns | {原因} | | {来源2} | ~{X}% | ~{N} ns | {原因} | ``` ### 8.6 撰写原则 - **数据驱动**:所有结论必须有 BDN 实测数据支撑,禁止无数据臆测 - **量化优先**:用"快 X 倍"、"省 Y%"、"降 Z B/op"表达,避免"显著"、"明显"等模糊词 - **根因到底**:从应用层代码 → 运行时机制 → CPU 微架构逐层分析 - **可操作**:每个优化建议必须指明具体修改位置和实施方案,而非泛泛建议 - **排序严格**:P0 在前,P3 在后,同级按收益占比降序 ================================================ FILE: .github/instructions/development.instructions.md ================================================ --- applyTo: "Doc/**" --- # AI 辅助开发流程指令 适用于新建应用系统、新增功能模块、需求整理、架构设计等研发全流程任务。 --- ## 1. 流程总览 ``` 需求整理 → 需求评审与拆分 → 技术方案设计 → 任务分解 → 迭代开发 → 集成验证 → 验收回顾 ``` **核心原则**:大需求必须拆小,每个迭代交付可验证的最小功能单元。禁止"一次性全做完"。 --- ## 2. 各阶段规范 ### 2.1 需求整理 用户提供原始描述(口语化、列表、草稿均可),AI 整理为以下结构(需求 + 功能清单 + 验收 合为一个文件): ```markdown # {项目/模块名}需求 ## 1. 背景与目标 - 为什么做(痛点/动机) - 做到什么程度算成功(可衡量目标) ## 2. 用户角色 | 角色 | 说明 | 核心诉求 | |------|------|---------| ## 3. 功能需求 ### 3.1 {功能模块名} - **描述**:一句话说明 - **用户故事**:作为{角色},我希望{操作},以便{价值} - **验收条件**(AC): - [ ] 条件 1 - [ ] 条件 2 - **优先级**:Must / Should / Could / Won't ## 4. 非功能需求 - 性能 / 安全 / 兼容性(三项必填) ## 5. 边界与约束 - 不做什么(明确排除项) - 已知限制 / 技术债务 ## 6. 功能清单与迭代计划 (需求评审拆分后填写,见 2.2) ## 7. 验收记录 (开发完成后填写,见 2.7) ## 8. 术语表 | 术语 | 定义 | |------|------| ``` **规则**:每个功能必须有 AC,无 AC 不可进入开发;优先级用 MoSCoW 四级;非功能需求至少覆盖性能、安全、兼容性。 ### 2.2 需求评审与拆分 按**纵向切片**(端到端功能,非技术层)拆分,遵循 INVEST 原则,单个功能单元 ≤ 1-2 天工作量,有依赖须标注。 写入需求文档「6. 功能清单与迭代计划」: ```markdown ## 6. 功能清单与迭代计划 ### 迭代 1:{主题}(Must 级别) | 编号 | 功能点 | 验收条件 | 前置依赖 | 预估工作量 | |------|--------|---------|---------|----------| | F001 | xxx | AC1, AC2 | 无 | 0.5d | | F002 | xxx | AC1 | F001 | 1d | ### 迭代 2:{主题}(Should 级别) ... ``` ### 2.3 技术方案设计 ```markdown # {项目/模块名}架构 ## 1. 架构概览 ## 2. 数据模型 ## 3. 接口设计 | 接口 | 方法 | 路径/签名 | 入参 | 出参 | 说明 | |------|------|----------|------|------|------| ## 4. 技术选型 | 领域 | 选型 | 理由 | |------|------|------| ## 5. 关键设计决策 | 决策点 | 方案 | 备选方案 | 选择理由 | |--------|------|---------|---------| ## 6. 任务分解 (见 2.4) ## 7. 风险与缓解 | 风险 | 影响 | 缓解措施 | |------|------|---------| ``` **规则**:优先使用 NewLife 已有组件(XCode、Remoting、Stardust 等);数据模型考虑 XCode 实体规范;接口遵循现有 API 风格。 ### 2.4 任务分解 单个任务 = 一次 AI 对话可完成的工作量(编码 + 测试 + 自测通过)。写入技术方案「6. 任务分解」: ```markdown ### 任务 T001:{动词 + 目标} - **对应功能**:F001 - **输入**:前置条件 / 已有代码 - **产出**:新增/修改哪些文件 - **验收**:怎样算完成 ``` **批次编排**(用于自治模式,见第 6 节):按依赖关系编排为批次,每批次 5-8 个任务,同批次内尽量无相互依赖,基础设施任务排在前面,每批次结束设 `[检查点 N]`,标注本批次产出是下批次哪些输入。 ### 2.5 迭代开发 流程:`理解任务 → 检索现有实现 → 编码 → 编译通过 → 测试通过 → 提交说明` - 严格遵守主指令编码规范,每个任务必须编译通过 - 常规模式:遇歧义暂停确认;自治模式:记录跳过继续(见第 6 节) - 有依赖按顺序执行,不跳跃 ### 2.6 集成验证 全部编译通过 → 单元测试通过 → 端到端主流程走通 → 异常场景覆盖 → 性能符合预期 ### 2.7 验收与回顾 对照需求文档逐条验收,写入「7. 验收记录」: ```markdown ## 7. 验收记录 ### 功能验收 | 编号 | 功能点 | 验收条件 | 状态 | 备注 | |------|--------|---------|------|------| ### 遗留问题 | 问题 | 影响 | 后续计划 | |------|------|---------| ### 经验总结 - 做得好的 / 待改进的 ``` --- ## 3. 文档存放规范 全流程仅产出 **2 个文档**,扁平存放在 `Doc/` 下: | 文档 | 文件名 | 包含内容 | |------|--------|--------| | 需求文档 | `Doc/{项目名}需求.md` | 背景目标 + 功能需求 + 功能清单 + 验收记录 + 术语表 | | 技术方案 | `Doc/{项目名}架构.md` | 架构 + 数据模型 + 接口 + 技术选型 + 任务分解 + 风险 | UTF-8 无 BOM;已有文件必须先读取再增量修改,禁止覆盖;各阶段产出追加到对应章节,不新建文件。 --- ## 4. AI 协作要点 ### 4.1 阶段切换 | 用户说 | 进入阶段 | |--------|---------| | "整理需求"/"写需求" | 2.1 需求整理 | | "拆分"/"拆解"/"排优先级" | 2.2 需求评审与拆分 | | "技术方案"/"架构设计"/"怎么实现" | 2.3 技术方案设计 | | "开始开发"/"写代码"/"实现 F001" | 2.5 迭代开发 | | "全部搞完"/"批量开发"/"自治模式"/"一次性做完"/"继续处理"/"接着做" | 第 6 节自治批处理 | | "验收"/"检查完成情况" | 2.7 验收与回顾 | | 一大段描述未指定阶段 | 默认 2.1 需求整理 | ### 4.2 主动引导 每阶段完成后提示下一步:需求整理完 → 拆分? → 技术方案? → 任务分解 → 开发? ### 4.3 大需求防护 功能点 > 5 / 实体 > 3 / 跨 2 层以上 / 描述 > 500 字 → 必须先拆分再开发。 --- ## 5. 常见反模式(禁止) - ❌ 跳过需求直接编码 - ❌ 一次性输出所有代码(大需求必须拆迭代或使用自治模式) - ❌ 需求文档没有验收条件 - ❌ 功能拆分按技术层而非用户价值 - ❌ 任务没有完成标准就开始编码 - ❌ 完成后不做验收对照 - ❌ 自治模式下遇阻塞问题死等用户(应记录跳过,继续后续) - ❌ 自治模式下做需要人工决策的架构变更(应记录待确认,现有方案兜底) - ❌ 跨批次不做编译验证 --- ## 6. 自治批处理模式 架构师已确认需求和技术方案后,AI 按任务清单自主执行,最小化人工介入。 ### 6.1 进入条件(全部满足) - [ ] 需求文档已完成且架构师已确认 - [ ] 技术方案已完成且架构师已确认 - [ ] 任务已分解并编排为批次 - [ ] 用户明确触发("全部搞完"/"批量开发"/"自治模式"等) 未满足时提示缺少哪些条件。 ### 6.2 计划结构与循环刷新 AI 用 plan 工具创建层次化计划,「前置刷新 + 批次执行」循环: ``` 1. [前置] 读取需求文档与技术方案 2. [前置] 读取任务清单与进度状态 3. [前置] 全量编译确认基线 4. [前置] 识别可并行的批次组 5. [批次1] 执行 T001-T005(子步骤展开各任务) 6. [检查点1] 输出批次1报告 7. [刷新] 重读需求文档与技术方案 8. [批次2] 执行 T006-T010 9. [检查点2] 输出批次2报告 ...(循环:刷新 → 批次 → 检查点) N-2. [后置] 全量编译与集成验证 N-1. [后置] 补完被跳过的任务 N. [后置] 生成验收报告 ``` **要点**: - 主步骤 15-25 个(不超过 30),子步骤展开具体任务仅供参考不单独追踪 - 刷新步骤穿插在每两个批次之间,`get_file` 重读文档对抗上下文漂移 - 用 `update_plan_progress` 跟踪主步骤,不为每个子任务调用 - 无依赖的批次可合并为一个主步骤执行,有依赖的必须顺序执行 ### 6.3 执行协议 | 情况 | 处理方式 | |------|----------| | 任务明确无歧义 | 直接执行:编码 → 编译 → 测试 | | 小歧义可合理推断 | 执行并在问题日志记录推断依据 | | 重大歧义或多种等价方案 | 标记 `⏸️ 待确认`,跳过 | | 前置任务被跳过 | 标记 `⏸️ 依赖阻塞:T0xx`,跳过 | | 编译失败短时间无法修复 | 回滚改动,记录并跳过 | | 涉及公共 API / 架构变更 | 标记 `⏸️ 需架构师决策`,兜底或跳过 | ### 6.4 检查点报告 每批次完毕后输出: ```markdown ## 检查点 N 报告 ### 完成情况 | 任务 | 状态 | 说明 | |------|------|------| | T001 | ✅ 完成 | | | T003 | ⏸️ 跳过 | 需确认:xxx | ### 编译状态 - 全量编译:✅ 通过 / ❌ 失败(错误详情) ### 问题日志 | 编号 | 类型 | 描述 | 影响任务 | 建议方案 | |------|------|------|---------|----------| ### 统计 - 本批次 N 个,完成 X 个,跳过 Y 个 - 累计进度:已完成 X / 总计 Z(XX%) - 上下文预估:{已处理任务数} / {建议上限} ``` ### 6.5 用户回复与继续 架构师回来后:AI 呈现检查点报告 → 架构师批量回复问题("Q001 OK,Q002 选 A")→ AI 修正推断 + 执行跳过的任务 + 继续下批次 → 循环至完成。 触发词:"继续"/"继续处理"/"回复完了"/"接着做" ### 6.6 质量护栏(自动执行) 编译门禁(失败即修复或回滚)/ 命名与技术方案一致 / 编码规范严格遵守 / 新增代码前搜索现有实现避免重复 / 不擅自引入新 NuGet 包 ### 6.7 会话边界处理 每个检查点后、连续完成 15+ 任务后、搜索结果不准确时 → 评估是否需要新会话。 **新会话续接模板**: ``` 我们在做 {项目名} 的自治批处理开发。 - 需求文档:Doc/{项目名}需求.md - 技术方案:Doc/{项目名}架构.md - 当前进度:批次 N 已完成,从批次 N+1 的 T0xx 开始继续 - 待解决问题:{问题编号} 请读取以上文档,从 T0xx 继续执行,自治模式。 ``` 上下文即将耗尽时 AI 主动提醒并生成上述模板。新会话前 4 步仍为前置刷新,已完成批次直接标记完成。 ### 6.8 批次大小建议 | 复杂度 | 批次大小 | |--------|---------| | 简单(CRUD) | 8-10 | | 中等(业务逻辑) | 5-7 | | 复杂(算法、并发) | 3-5 | 单会话上限:3-4 个批次(约 15-25 个任务)。 --- (完) ================================================ FILE: .github/instructions/net.instructions.md ================================================ --- applyTo: "**/Net/**" --- # 网络编程指令 适用于基于 `NewLife.Net` 的网络服务器(`NetServer`)和客户端(`ISocketClient`)开发任务。 --- ## 1. 架构概览 NewLife 网络框架分为两层: | 层级 | 服务端 | 客户端 | 说明 | |------|--------|--------|------| | **应用层** | `NetServer` / `NetServer` | — | 管理监听、会话生命周期、管道 | | **传输层** | `TcpServer` / `UdpServer` | `TcpSession` / `UdpServer`(客户端模式) | 底层 Socket 收发 | | **会话** | `NetSession` / `NetSession` | — | 每个连接对应一个会话,业务逻辑入口 | | **管道** | `IPipeline` + `IPipelineHandler` | 同左 | 编解码、粘包拆包、消息匹配 | **关键接口**: - `ISocketClient` — 客户端连接接口(Open/Close/Send/Receive) - `ISocketRemote` — 远程通信接口(Send/Receive/SendMessageAsync) - `INetSession` — 网络会话接口(服务端每个连接的业务处理单元) - `INetHandler` — 网络数据处理器接口(Init/Process) --- ## 2. 服务端开发规范 ### 2.1 基本模式 推荐使用泛型 `NetServer` + 自定义 `NetSession` 子类: ```csharp /// 自定义网络服务器 class MyServer : NetServer { } /// 自定义会话,每个客户端连接对应一个实例 class MySession : NetSession { /// 客户端连接 protected override void OnConnected() { base.OnConnected(); WriteLog("客户端已连接 {0}", Remote); } /// 收到客户端数据 protected override void OnReceive(ReceivedEventArgs e) { base.OnReceive(e); // 业务处理 } /// 客户端断开 protected override void OnDisconnected(String reason) { base.OnDisconnected(reason); } } ``` ### 2.2 服务器启动配置 ```csharp var server = new MyServer { Port = 8080, // 监听端口,0 表示随机 ProtocolType = NetType.Tcp, // Tcp/Udp/Unknown(同时监听) // AddressFamily = AddressFamily.InterNetwork, // 仅IPv4,默认同时IPv4+IPv6 ServiceProvider = provider, // 依赖注入 Log = XTrace.Log, // 应用日志 SessionLog = XTrace.Log, // 会话日志 Tracer = tracer, // APM 追踪 #if DEBUG SocketLog = XTrace.Log, // Socket 层日志(仅调试) LogSend = true, LogReceive = true, #endif }; server.Start(); ``` ### 2.3 会话生命周期 ``` 连接建立 → OnConnected() → OnReceive()... → OnDisconnected(reason) → Dispose() ``` - **OnConnected**:初始化会话状态、发送欢迎消息 - **OnReceive**:核心业务处理入口,`e.Packet` 为原始数据,`e.Message` 为管道解码后的消息 - **OnDisconnected**:清理资源、记录日志,`reason` 包含断开原因 - 会话内可通过 `ServiceProvider` 获取 Scoped 服务 ### 2.4 服务端发送数据 | 方法 | 说明 | |------|------| | `Send(IPacket)` | 直接发送原始数据,不经过管道 | | `Send(String)` | 发送字符串,默认 UTF-8 | | `Send(ReadOnlySpan)` | 高性能发送 | | `SendMessage(Object)` | 通过管道编码后发送,不等待响应 | | `SendReply(Object, ReceivedEventArgs)` | 发送响应消息,与请求关联(用于 StandardCodec 等协议) | | `SendMessageAsync(Object)` | 通过管道发送并等待响应 | ### 2.5 群发 ```csharp // 群发数据给所有在线客户端 await server.SendAllAsync(data); // 带过滤条件群发 await server.SendAllAsync(data, session => session.ID > 100); // 群发管道消息 server.SendAllMessage(message, session => session["VIP"] is true); ``` 群发要求 `UseSession = true`(默认开启)。 ### 2.6 事件模式(简单场景) 不需要自定义会话时,可直接使用事件: ```csharp var server = new NetServer { Port = 8080 }; server.Received += (sender, e) => { if (sender is INetSession session) session.Send(e.Packet); // Echo }; server.Start(); ``` --- ## 3. 客户端开发规范 ### 3.1 创建客户端 通过 `NetUri.CreateRemote()` 扩展方法创建: ```csharp // TCP 客户端 var client = new NetUri("tcp://127.0.0.1:8080").CreateRemote(); // UDP 客户端 var client = new NetUri("udp://127.0.0.1:8080").CreateRemote(); // WebSocket 客户端 var client = new NetUri("ws://127.0.0.1:8080/path").CreateRemote(); ``` `CreateRemote` 根据协议自动返回 `TcpSession` / `UdpServer` / `WebSocketClient`。 ### 3.2 客户端使用 ```csharp var uri = new NetUri("tcp://127.0.0.1:8080"); var client = uri.CreateRemote(); client.Log = XTrace.Log; client.Open(); // 发送原始数据(不经过管道) client.Send("Hello"); // 事件驱动接收 client.Received += (sender, e) => { // e.Packet 原始数据,e.Message 管道解码后的消息 }; // 或同步/异步接收 using var pk = client.Receive(); using var pk = await client.ReceiveAsync(cancellationToken); client.Close("完成"); // 或 client.Dispose() ``` ### 3.3 请求-响应模式(需要管道编解码器) ```csharp var client = new NetUri("tcp://127.0.0.1:8080").CreateRemote(); client.Add(); client.Open(); var response = await client.SendMessageAsync(payload, cancellationToken); // 等待响应 client.SendMessage(message); // 不等待响应 ``` ### 3.4 SSL/TLS ```csharp // 服务端 SSL var server = new NetServer { Port = 443, SslProtocol = SslProtocols.Tls12, Certificate = new X509Certificate2("server.pfx", "password"), }; // 客户端 SSL(自动根据端口判断,或手动指定) var client = new NetUri("tcp://host:443").CreateRemote(); if (client is TcpSession tcp) { tcp.SslProtocol = SslProtocols.Tls12; // tcp.Certificate = cert; // 客户端证书(如果服务端要求) } ``` --- ## 4. 管道与编解码器 ### 4.1 管道机制 管道(`IPipeline`)是处理器链,Read/Write 返回值作为下一个处理器的输入,返回 `null` 截断管道: ``` 接收:Socket → [Codec1.Read] → [Codec2.Read] → FireRead → OnReceive 发送:SendMessage → [Codec2.Write] → [Codec1.Write] → FireWrite → Socket ``` Open 正序传播,Close 逆序传播。先添加的在底层(靠近 Socket),后添加的在上层(靠近业务)。 ### 4.2 内置编解码器 | 编解码器 | 基类 | 说明 | 典型场景 | |---------|------|------|---------| | `StandardCodec` | `MessageCodec` | 4字节头部(Flag+Seq+Length),支持请求-响应匹配 | 自定义 RPC 协议 | | `LengthFieldCodec` | `MessageCodec` | 长度字段头部,可配置偏移和大小 | MQTT、通用二进制协议 | | `JsonCodec` | `Handler` | JSON 文本编解码,不处理粘包 | 文本协议(通常与 StandardCodec 级联) | | `SplitDataCodec` | `Handler` | 分隔符拆包(默认 `\r\n`) | 文本行协议 | | `WebSocketCodec` | `Handler` | WebSocket 帧编解码 | WebSocket 通信 | ### 4.3 添加编解码器 ```csharp // 服务端添加 server.Add(); // 客户端添加 client.Add(); // 多层管道级联(按添加顺序组成链) server.Add(); // 底层:粘包拆包 + 请求响应匹配 server.Add(); // 上层:JSON 编解码 ``` ### 4.4 StandardCodec 请求-响应 StandardCodec 使用 `DefaultMessage`,包含 Flag(1字节)、Sequence(1字节)、Length(2字节), 支持自动序列号分配和请求-响应匹配。 ```csharp // 服务端 Echo 示例 server.Add(); server.Received += (sender, e) => { if (sender is INetSession session && e.Message is IPacket pk) session.SendReply(pk, e); // 使用 SendReply 关联请求上下文 }; // 客户端请求-响应 client.Add(); var response = await client.SendMessageAsync(payload); ``` ### 4.5 基类选择 | 基类 | 适用场景 | 典型代表 | |------|---------|---------| | `MessageCodec` | 需要粘包拆包和/或请求-响应匹配(内置 `IMatchQueue`、`Encode`/`Decode`) | `StandardCodec`、`LengthFieldCodec` | | `Handler` | 简单转换、帧协议、文本协议(轻量,仅 `Read`/`Write`/`Open`/`Close`) | `JsonCodec`、`SplitDataCodec`、`WebSocketCodec` | ### 4.6 编解码器设计规范 #### 4.6.1 粘包拆包(PacketCodec 模式) TCP 是字节流协议,必须处理粘包拆包。统一模式(完整实现见 4.7 模板): 1. 每个连接独立的 `PacketCodec` 实例,存储在 `ss["Codec"]` 中 2. 通过 `GetLength2` 委托告诉 `PacketCodec` 如何计算完整帧长度 3. `PacketCodec.Parse()` 返回完整帧列表,自动缓存不完整数据 **`GetLength2` 规范**(签名 `Int32 GetLength(ReadOnlySpan span)`):返回帧完整长度(含头部),数据不足时返回 `0`。 ```csharp public static Int32 GetLength(ReadOnlySpan span) { if (span.Length < 4) return 0; var reader = new SpanReader(span) { IsLittleEndian = true }; reader.Advance(2); return 4 + reader.ReadUInt16(); // 头部4字节 + 负载长度 } ``` #### 4.6.2 编码与内存管理 - **`ExpandHeader(size)`**:编码时优先复用负载缓冲区前置空间写入头部,零拷贝;空间不足时创建 `OwnerPacket`,原包作为 `Next` 链节点 - **`SpanWriter`**:配合 `ExpandHeader` 写入头部字段,注意 `IsLittleEndian` 大小端 - **兜底释放**:`MessageCodec.Write` 基类自动 `TryDispose`;`Handler` 子类需在 `Write` 的 `finally` 中手动调用 - **对象池**:`DefaultMessage.Rent()` / `DefaultMessage.Return()` 减少 GC 压力 #### 4.6.3 请求-响应匹配 `MessageCodec` 内置 `IMatchQueue`,流程:`Write` → `AddToQueue` 入队 → `Decode` 解码 → `Queue.Match` 按 `IsMatch` 匹配 → 唤醒 `SendMessageAsync` 的 `Task`。 - 重载 `AddToQueue`:控制哪些消息入队(通常只有请求消息) - 重载 `IsMatch`:根据序列号等字段匹配请求和响应(见 4.7 模板) - `QueueSize`:匹配队列大小,默认 256 - `Timeout`:等待响应超时,默认 30_000ms - `UserPacket`:为 `true` 时向上层传递 `Payload` 而非整个 `IMessage`,用于编码器级联 #### 4.6.4 Close 清理 **必须**在 `Close` 中执行 `ss["Codec"] = null` 清理 `PacketCodec`,否则 `MemoryStream` 缓存泄漏(见 4.7 模板)。 #### 4.6.5 上下文扩展(IExtend) 管道处理器通过 `IExtend` 在会话/上下文上传递元数据: | 键 | 用途 | 示例 | |---|------|------| | `"Codec"` | 每连接的 `PacketCodec` 实例 | 编解码器的 `Decode`/`Close` 中读写 | | `"Flag"` | 数据类型标记 `DataKinds` | `JsonCodec.Write` 设置 → `StandardCodec.Write` 消费 | | `"_raw_message"` | 原始请求消息 | `MessageCodec.Read` 设置 → `Write` 中创建响应时消费 | | `"TaskSource"` | `TaskCompletionSource` | 框架内部,`AddToQueue` 消费 | #### 4.6.6 多层管道级联 - 底层编解码器处理粘包拆包和请求-响应匹配,上层处理数据格式转换 - `UserPacket = true` 让底层向上层传递 `Payload` 而非整个 `IMessage` - 上层通过 `ext["Flag"]` 向底层传递数据类型标记 ### 4.7 自定义编解码器模板 #### 方式一:继承 MessageCodec(需要粘包/请求响应匹配) ```csharp /// 自定义协议编解码器 public class MyCodec : MessageCodec { /// 编码消息为数据包 protected override Object? Encode(IHandlerContext context, MyMessage msg) { return msg.ToPacket(); } /// 解码数据包为消息 protected override IEnumerable? Decode(IHandlerContext context, IPacket pk) { if (context.Owner is not IExtend ss) yield break; if (ss["Codec"] is not PacketCodec pc) { ss["Codec"] = pc = new PacketCodec { GetLength2 = MyMessage.GetLength, MaxCache = MaxCache, Tracer = (context.Owner as ISocket)?.Tracer }; } foreach (var item in pc.Parse(pk)) { var msg = new MyMessage(); if (msg.Read(item)) yield return msg; } } /// 是否匹配响应 protected override Boolean IsMatch(Object? request, Object? response) => request is MyMessage req && response is MyMessage res && req.Sequence == res.Sequence; /// 连接关闭时清理 public override Boolean Close(IHandlerContext context, String reason) { if (context.Owner is IExtend ss) ss["Codec"] = null; return base.Close(context, reason); } } ``` #### 方式二:继承 Handler(简单转换/帧协议) ```csharp /// 自定义帧编解码器 public class MyFrameCodec : Handler { /// 读取数据(接收时) public override Object? Read(IHandlerContext context, Object message) { if (message is IPacket pk) { // 解码:二进制 → 业务对象 var frame = MyFrame.Parse(pk); message = frame; } return base.Read(context, message); } /// 写入数据(发送时) public override Object? Write(IHandlerContext context, Object message) { IPacket? owner = null; if (message is MyFrame frame) { // 编码:业务对象 → 二进制 message = owner = frame.ToPacket(); } try { return base.Write(context, message); } finally { owner.TryDispose(); // 兜底释放 } } /// 连接关闭时清理缓存 public override Boolean Close(IHandlerContext context, String reason) { if (context.Owner is IExtend ss) ss["Codec"] = null; return base.Close(context, reason); } } ``` --- ## 5. 常见模式与最佳实践 ### 5.1 端口选择 - 测试代码使用端口 `0`(系统自动分配随机端口),避免端口冲突 - 正式服务指定固定端口 - 启动后可通过 `server.Port` 获取实际监听端口 ### 5.2 协议选择 | 场景 | 推荐 | |------|------| | 可靠传输、长连接 | `NetType.Tcp` | | 低延迟、广播、允许丢包 | `NetType.Udp` | | 同时支持(默认) | `NetType.Unknown` | | Web 浏览器通信 | `NetType.WebSocket` | ### 5.3 会话管理 - `UseSession = true`(默认):维护会话集合,支持群发、按 ID 查找 - `UseSession = false`:不维护会话集合,减少内存开销,适合海量短连接 - `SessionTimeout`:设置会话超时时间(秒),超时无数据自动断开 - 会话中通过 `Items` 字典存储自定义数据 ### 5.4 日志分层 | 属性 | 用途 | 建议 | |------|------|------| | `Log` | 服务器应用层日志 | 始终设置 | | `SessionLog` | 会话级别日志 | 调试时设置 | | `SocketLog` | 底层 Socket 日志 | 仅 DEBUG 时设置 | | `LogSend` / `LogReceive` | 收发数据内容日志 | 仅 DEBUG 时开启 | | `Tracer` | 应用层 APM | 生产环境追踪 | | `SocketTracer` | Socket 层 APM | 排查底层问题 | ### 5.5 资源释放 - 服务端:调用 `server.Stop(reason)` 或 `server.Dispose()` - 客户端:调用 `client.Close(reason)` 或 `client.Dispose()` - 会话自动随连接断开释放,无需手动管理 - `ISocketClient` 实现 `IDisposable`,推荐 `using` 模式 ### 5.6 INetHandler 业务处理器 通过重载 `NetServer.CreateHandler` 注入自定义业务处理器: ```csharp class MyServer : NetServer { /// 为会话创建网络数据处理器 public override INetHandler? CreateHandler(INetSession session) => new MyHandler(); } ``` 处理器在会话 `Start` 时初始化,`OnReceive` 前调用 `Process`,适合前置协议解析。 --- ## 6. 常见错误 - ❌ 在 `OnReceive` 中执行长时间阻塞操作(会影响其他连接的数据接收) - ❌ 不加管道编解码器直接调用 `SendMessageAsync`(无法匹配响应) - ❌ 混淆 `Send` 与 `SendMessage`:前者直接发原始数据,后者经过管道编码 - ❌ 混淆 `SendMessage` 与 `SendReply`:响应消息必须用 `SendReply` 关联请求上下文 - ❌ 忘记调用 `base.OnConnected()` / `base.OnDisconnected(reason)` / `base.OnReceive(e)` - ❌ 在会话中使用 `Task.Result` 或 `Task.Wait()`(导致死锁和线程池饥饿) - ❌ 使用固定端口编写测试(端口冲突),应使用 `Port = 0` - ❌ 服务端 SSL 未指定证书 --- ## 7. 完整示例 ### 7.1 带 StandardCodec 的 Echo 服务 ```csharp // 服务端 var server = new NetServer { Port = 8080, ProtocolType = NetType.Tcp, Log = XTrace.Log, }; server.Add(); server.Received += (sender, e) => { if (sender is INetSession session && e.Message is IPacket pk) session.SendReply(pk, e); }; server.Start(); // 客户端 var client = new NetUri($"tcp://127.0.0.1:{server.Port}").CreateRemote(); client.Add(); client.Open(); var response = await client.SendMessageAsync(new ArrayPacket("Hello".GetBytes())); ``` ### 7.2 自定义会话服务器 ```csharp class ChatServer : NetServer { } class ChatSession : NetSession { protected override void OnConnected() { base.OnConnected(); Send($"欢迎 [{Remote}] 进入聊天室!\r\n"); } protected override void OnReceive(ReceivedEventArgs e) { base.OnReceive(e); var msg = e.Packet?.ToStr(); if (msg.IsNullOrEmpty()) return; // 广播给所有在线用户 var host = (this as INetSession).Host; host.SendAllMessage($"[{ID}] {msg}"); } protected override void OnDisconnected(String reason) { base.OnDisconnected(reason); WriteLog("用户离开:{0}", reason); } } ``` --- (完) ================================================ FILE: .github/workflows/publish-beta.yml ================================================ name: publish-beta on: push: branches: [ master,dev ] paths: - 'NewLife.RocketMQ/**' workflow_dispatch: jobs: build-publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup dotNET uses: actions/setup-dotnet@v4 with: dotnet-version: | 6.x 7.x 8.x 9.x 10.x - name: Build run: | dotnet pack --version-suffix $(date "+%Y.%m%d-beta%H%M") -c Release -o out NewLife.RocketMQ/NewLife.RocketMQ.csproj - name: Publish run: | # dotnet nuget push ./out/*.nupkg --skip-duplicate --source https://nuget.pkg.github.com/NewLifeX/index.json --api-key ${{ github.token }} dotnet nuget push ./out/*.nupkg --skip-duplicate --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.nugetKey }} ================================================ FILE: .github/workflows/publish.yml ================================================ name: publish on: push: tags: [ v* ] workflow_dispatch: jobs: build-publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup dotNET uses: actions/setup-dotnet@v4 with: dotnet-version: | 6.x 7.x 8.x 9.x 10.x - name: Build run: | dotnet pack -c Release -o out NewLife.RocketMQ/NewLife.RocketMQ.csproj - name: Publish run: | # dotnet nuget push ./out/*.nupkg --skip-duplicate --source https://nuget.pkg.github.com/NewLifeX/index.json --api-key ${{ github.token }} dotnet nuget push ./out/*.nupkg --skip-duplicate --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.nugetKey }} ================================================ FILE: .github/workflows/test.yml ================================================ name: test on: push: branches: [ '*' ] pull_request: branches: [ '*' ] jobs: build-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup dotNET uses: actions/setup-dotnet@v4 with: dotnet-version: | 6.x 7.x 8.x 9.x 10.x - name: Build run: | dotnet build -c Release ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json project.fragment.lock.json artifacts/ *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted #*.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings node_modules/ orleans.codegen.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml # CodeRush .cr/ # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc /BinTest /BinTest - 副本/Config /生产 /BinUnitTest ================================================ FILE: ChangeLog.md ================================================ # 更新日志 ## v3.0.2026.0501 (2026-05-01) ### 问题修复 - **[fix]** 修复 Pop/Ack/ChangeInvisibleTime 操作缺少 `queueId` 参数导致服务端处理异常的问题 - **便利方法**:`MessageExt` 新增多个便利访问方法,简化消息属性读取 ### 依赖更新 - 升级 NewLife.Core 依赖包到最新版本(2026-04-xx) --- ## v3.0.2026.0305 (2026-03-05) ### 云适配重构(重大版本) - **架构重构**:全面升级为 v3.0 云适配架构,新增 `ICloudProvider` 接口统一阿里云、华为云、腾讯云适配 - **事务消息**:新增 RocketMQ 事务消息发布与回查接口,支持分布式事务场景 - **请求-应答模式**:新增 Request-Reply 同步调用模式,支持消息级 RPC ### gRPC 协议支持 - **gRPC 5.x Proxy**:新增 gRPC 协议支持,零依赖不引入第三方 Protobuf/gRPC 库 - **SpanReader/SpanWriter 重构**:将 gRPC 协议编解码器重构为基于 `SpanReader`/`SpanWriter` 的零分配实现,提升性能 - **gRPC Telemetry**:新增 gRPC Telemetry 链路追踪支持 ### 新增功能 - **VIP 通道**:支持 VIP Channel 高优先级消息通道 - **批量确认**:支持批量 Ack 操作,减少网络往返 - **5.x MsgId**:支持 RocketMQ 5.x 消息 ID 格式生成与解析 - **客户端拉取超时**:新增 `Consumer.PullTimeout` 客户端侧应用层超时保护,防止 4.9.8 无响应导致消费线程永久阻塞 ### 测试覆盖 - 新增 152 个单元测试,覆盖协议层、模型层、工具类等核心组件 - 完善架构文档与需求文档 --- ## v2.7.2026.0301 (2026-03-01) ### 问题修复 - 新增`Consumer.PullTimeout`属性,默认值0表示自动取`SuspendTimeout+10_000ms`,作为客户端拉取消息的应用层超时保护,防止RocketMQ 4.9.8在SuspendTimeout后无响应导致消费线程永久阻塞 ## v2.7.2026.0201 (2026-02-01) ### 依赖更新 - 升级 NewLife.Core 依赖包到最新版本(2026-01-24) - 升级 NewLife.Core 依赖包(2026-01-14) - 升级 NewLife.Core 依赖包(2026-01-12) ## v2.7.2026.0102 (2026-01-03) 初始发布版本 ================================================ FILE: DLL/NewLife.Core.xml ================================================ NewLife.Core 服务类型 服务主函数 服务程序基类 显示名 描述 初始化 服务主函数 显示状态 显示菜单 添加菜单 开始工作 停止服务 服务管理线程封装 检查内存是否超标 是否超标重启 检查服务进程的总线程数是否超标 检查服务进程的句柄数是否超标 服务开始时间 检查自动重启 重启服务 服务启动事件 服务停止事件 在系统关闭时执行。 指定在系统关闭之前应该发生什么。 在计算机的电源状态已发生更改时执行。 这适用于便携式计算机,当他们进入挂起模式,这不是系统关闭相同。 在终端服务器会话中接收的更改事件时执行 看门狗要保护的服务 检查看门狗。 XAgent看门狗功能由管理线程完成,每分钟一次。 检查指定的任务是否已经停止,如果已经停止,则启动它。 安装、卸载 服务 是否安装 获取安装服务的命令参数 日志 写日志 服务助手 Exe程序名 启动、停止 服务 服务对象 执行一个命令 执行SC命令 是否已安装 是否已启动 是否已安装 是否已启动 写日志 服务设置 服务名 显示名 服务描述 最大占用内存。超过最大占用时,整个服务进程将会重启,以释放资源。默认8096M 最大线程数。超过最大占用时,整个服务进程将会重启,以释放资源。默认1000个 最大句柄数。超过最大占用时,整个服务进程将会重启,以释放资源。默认10000 自动重启时间。到达自动重启时间时,整个服务进程将会重启,以释放资源。默认0分,表示无限 看门狗,保护其它服务,每分钟检查一次。多个服务名逗号分隔 IP搜索 数据文件 获取IP地址 获取IP地址 数据流 析构 销毁 索引结构 缓存 默认缓存 名称 默认缓存时间。默认0秒表示不过期 获取和设置缓存,永不过期 缓存个数 所有键 构造函数 初始化配置 是否包含缓存项 设置缓存项 过期时间,秒。小于0时采用默认缓存时间 设置缓存项 过期时间 获取缓存项 批量移除缓存项 键集合 清空所有缓存项 设置缓存项有效期 过期时间,秒 获取缓存项有效期 批量获取缓存项 批量设置缓存项 过期时间,秒。小于0时采用默认缓存时间 获取列表 元素类型 获取哈希 元素类型 获取队列 元素类型 获取Set 添加,已存在时不更新 值类型 过期时间,秒。小于0时采用默认缓存时间 设置新值并获取旧值,原子操作 值类型 累加,原子操作 变化量 累加,原子操作 变化量 递减,原子操作 变化量 递减,原子操作 变化量 提交变更。部分提供者需要刷盘 申请分布式锁 多线程性能测试 随机读写 批量操作。默认0不分批 Memory性能测试[顺序],逻辑处理器 32 个 2,000MHz Intel(R) Xeon(R) CPU E5-2640 v2 @ 2.00GHz 测试 10,000,000 项, 1 线程 赋值 10,000,000 项, 1 线程,耗时 3,764ms 速度 2,656,748 ops 读取 10,000,000 项, 1 线程,耗时 1,296ms 速度 7,716,049 ops 删除 10,000,000 项, 1 线程,耗时 1,230ms 速度 8,130,081 ops 测试 20,000,000 项, 2 线程 赋值 20,000,000 项, 2 线程,耗时 3,088ms 速度 6,476,683 ops 读取 20,000,000 项, 2 线程,耗时 1,051ms 速度 19,029,495 ops 删除 20,000,000 项, 2 线程,耗时 1,011ms 速度 19,782,393 ops 测试 40,000,000 项, 4 线程 赋值 40,000,000 项, 4 线程,耗时 3,060ms 速度 13,071,895 ops 读取 40,000,000 项, 4 线程,耗时 1,023ms 速度 39,100,684 ops 删除 40,000,000 项, 4 线程,耗时 994ms 速度 40,241,448 ops 测试 80,000,000 项, 8 线程 赋值 80,000,000 项, 8 线程,耗时 3,124ms 速度 25,608,194 ops 读取 80,000,000 项, 8 线程,耗时 1,171ms 速度 68,317,677 ops 删除 80,000,000 项, 8 线程,耗时 1,199ms 速度 66,722,268 ops 测试 320,000,000 项, 32 线程 赋值 320,000,000 项, 32 线程,耗时 13,857ms 速度 23,093,021 ops 读取 320,000,000 项, 32 线程,耗时 1,950ms 速度 164,102,564 ops 删除 320,000,000 项, 32 线程,耗时 3,359ms 速度 95,266,448 ops 测试 320,000,000 项, 64 线程 赋值 320,000,000 项, 64 线程,耗时 9,648ms 速度 33,167,495 ops 读取 320,000,000 项, 64 线程,耗时 1,974ms 速度 162,107,396 ops 删除 320,000,000 项, 64 线程,耗时 1,907ms 速度 167,802,831 ops 测试 320,000,000 项,256 线程 赋值 320,000,000 项,256 线程,耗时 12,429ms 速度 25,746,238 ops 读取 320,000,000 项,256 线程,耗时 1,907ms 速度 167,802,831 ops 删除 320,000,000 项,256 线程,耗时 2,350ms 速度 136,170,212 ops 使用指定线程测试指定次数 次数 线程 随机读写 批量操作 读取测试 次数 线程 随机读写 批量操作 赋值测试 次数 线程 随机读写 批量操作 累加测试 次数 线程 随机读写 批量操作 删除测试 次数 线程 随机读写 已重载。 分布式锁 实例化 申请锁 销毁 缓存接口 名称 默认缓存时间。默认0秒表示不过期 获取和设置缓存,永不过期 缓存个数 所有键 是否包含缓存项 设置缓存项 过期时间,秒。小于0时采用默认缓存时间 设置缓存项 过期时间 获取缓存项 批量移除缓存项 键集合 清空所有缓存项 设置缓存项有效期 过期时间 获取缓存项有效期 批量获取缓存项 批量设置缓存项 过期时间,秒。小于0时采用默认缓存时间 获取列表 元素类型 获取哈希 元素类型 获取队列 元素类型 获取Set 添加,已存在时不更新 值类型 过期时间,秒。小于0时采用默认缓存时间 设置新值并获取旧值,原子操作 常常配合Increment使用,用于累加到一定数后重置归零,又避免多线程冲突。 值类型 累加,原子操作 变化量 累加,原子操作 变化量 递减,原子操作 变化量 递减,原子操作 变化量 提交变更。部分提供者需要刷盘 申请分布式锁 要锁定的key 多线程性能测试 随机读写 批量操作。默认0不分批 生产者消费者接口 生产添加 消费获取 默认字典缓存 缓存核心 容量。容量超标时,采用LRU机制删除,默认100_000 定时清理时间,默认60秒 实例化一个内存字典缓存 销毁 缓存项。原子计数 所有键。实际返回只读列表新实例,数据量较大时注意性能 初始化配置 获取或添加缓存项 值类型 过期时间,秒。小于0时采用默认缓存时间 是否包含缓存项 添加缓存项,已存在时更新 值类型 过期时间,秒。小于0时采用默认缓存时间 获取缓存项,不存在时返回默认值 批量移除缓存项 键集合 实际移除个数 清空所有缓存项 设置缓存项有效期 过期时间 设置是否成功 获取缓存项有效期,不存在时返回Zero 添加,已存在时不更新,常用于锁争夺 值类型 过期时间,秒。小于0时采用默认缓存时间 设置新值并获取旧值,原子操作 值类型 累加,原子操作 变化量 累加,原子操作 变化量 递减,原子操作 变化量 递减,原子操作 变化量 获取列表 获取哈希 获取队列 获取Set 基于HashSet,非线程安全 获取 或 添加 缓存项 缓存项 数值 过期时间 是否过期 访问时间 构造缓存项 设置数值和过期时间 更新访问时间并返回数值 设置过期时间 递增 递减 清理会话计时器 移除过期的缓存项 使用指定线程测试指定次数 次数 线程 随机读写 批量操作 生产者消费者 实例化内存队列 实例化内存队列 生产添加 消费获取 Redis缓存 创建指定服务器的实例 服务器地址。支持前面加上密码,@分隔 使用的数据库 创建指定服务器的实例,支持密码 服务器地址。支持前面加上密码,@分隔 密码 使用的数据库 服务器 密码 目标数据库。默认0 出错重试次数。如果出现协议解析错误,可以重试的次数,默认3 完全管道。读取操作是否合并进入管道,默认false 自动管道。管道操作达到一定数量时,自动提交,默认0 性能计数器 初始化 销毁 已重载。 为同一服务器创建不同Db的子级库 连接池 执行命令 返回类型 命令key,用于选择集群节点 回调函数 是否写入操作 开始管道模式 结束管道模式 要求结果。默认false 提交变更。处理某些残留在管道里的命令 缓存个数 所有键 单个实体项 过期时间,秒。小于0时采用默认缓存时间 获取单体 批量移除缓存项 键集合 清空所有缓存项 是否存在 设置缓存项有效期 过期时间 获取缓存项有效期 批量获取缓存项 批量设置缓存项 过期时间,秒。小于0时采用默认缓存时间 添加,已存在时不更新 值类型 过期时间,秒。小于0时采用默认缓存时间 设置新值并获取旧值,原子操作 值类型 累加,原子操作 变化量 累加,原子操作,乘以100后按整数操作 变化量 递减,原子操作 变化量 递减,原子操作,乘以100后按整数操作 变化量 性能测试 Redis性能测试[随机],批大小[100],逻辑处理器 40 个 2,400MHz Intel(R) Xeon(R) CPU E5-2640 v4 @ 2.40GHz 测试 100,000 项, 1 线程 赋值 100,000 项, 1 线程,耗时 418ms 速度 239,234 ops 读取 100,000 项, 1 线程,耗时 520ms 速度 192,307 ops 删除 100,000 项, 1 线程,耗时 125ms 速度 800,000 ops 测试 200,000 项, 2 线程 赋值 200,000 项, 2 线程,耗时 548ms 速度 364,963 ops 读取 200,000 项, 2 线程,耗时 549ms 速度 364,298 ops 删除 200,000 项, 2 线程,耗时 315ms 速度 634,920 ops 测试 400,000 项, 4 线程 赋值 400,000 项, 4 线程,耗时 694ms 速度 576,368 ops 读取 400,000 项, 4 线程,耗时 697ms 速度 573,888 ops 删除 400,000 项, 4 线程,耗时 438ms 速度 913,242 ops 测试 800,000 项, 8 线程 赋值 800,000 项, 8 线程,耗时 1,206ms 速度 663,349 ops 读取 800,000 项, 8 线程,耗时 1,236ms 速度 647,249 ops 删除 800,000 项, 8 线程,耗时 791ms 速度 1,011,378 ops 测试 4,000,000 项, 40 线程 赋值 4,000,000 项, 40 线程,耗时 4,848ms 速度 825,082 ops 读取 4,000,000 项, 40 线程,耗时 5,399ms 速度 740,877 ops 删除 4,000,000 项, 40 线程,耗时 6,281ms 速度 636,841 ops 测试 4,000,000 项, 64 线程 赋值 4,000,000 项, 64 线程,耗时 6,806ms 速度 587,716 ops 读取 4,000,000 项, 64 线程,耗时 5,365ms 速度 745,573 ops 删除 4,000,000 项, 64 线程,耗时 6,716ms 速度 595,592 ops 随机读写 批量操作 使用指定线程测试指定次数 次数 线程 随机读写 批量操作 日志 写日志 Redis客户端 以极简原则进行设计,每个客户端不支持并行命令处理,可通过多客户端多线程解决。 收发共用64k缓冲区,所以命令请求和响应不能超过64k。 客户端 内容类型 密码 是否已登录 登录时间 是否正在处理命令 销毁 异步请求 新建连接 发出请求 接收响应 发出请求 重置。干掉历史残留数据 执行命令。返回字符串、Packet、Packet[] 执行命令。返回基本类型、对象、对象数组 尝试转换类型 管道命令个数 开始管道模式 结束管道模式 要求结果 心跳 选择Db 验证密码 退出 获取信息 设置 超时时间 读取 批量设置 批量获取 数值转字节数组 字节数组转对象 字节数组转对象 获取命令对应的字节数组,全局缓存 日志 写日志 并行哈希集合 主要用于频繁添加删除而又要遍历的场合 是否空集合 元素个数 是否包含元素 尝试添加 尝试删除 字典缓存。当指定键的缓存项不存在时,调用委托获取值,并写入缓存。 常用匿名函数或者Lambda表达式作为委托。 键类型 值类型 过期时间。单位是秒,默认0秒,表示永不过期 定时清理时间,默认0秒,表示不清理过期项 容量。容量超标时,采用LRU机制删除,默认10_000 是否允许缓存控制,避免缓存穿透。默认false 查找数据的方法 实例化一个字典缓存 实例化一个字典缓存 实例化一个字典缓存 销毁 缓存项 数值 过期时间 是否过期 访问时间 更新访问时间并返回数值 重写索引器。取值时如果没有该项则返回默认值;赋值时如果已存在该项则覆盖,否则添加。 获取 GetOrAdd 获取 GetOrAdd 设置 AddOrUpdate 尝试添加,或返回旧值 扩展获取数据项,当数据项不存在时,通过调用委托获取数据项。线程安全。 获取值的委托,该委托以键作为参数 移除指定缓存项 清空 缓存项。原子计数 是否包含指定键 赋值到目标缓存 清理会话计时器 移除过期的缓存项 枚举 集群管理 资源列表 打开 关闭 关闭原因。便于日志分析 是否成功 从集群中获取资源 归还 集群异常 资源 实例化 集群助手 借助集群资源处理事务 对集群进行多次调用 对象池接口 对象池大小 获取 归还 清空 对象池扩展 字符串构建器池 归还一个字符串构建器到对象池 是否需要返回结果 字符串构建器池 初始容量。默认100个 最大容量。超过该大小时不进入池内,默认4k 创建 归还 内存流池 归还一个内存流到对象池 是否需要返回结果 内存流池 初始容量。默认1024个 最大容量。超过该大小时不进入池内,默认64k 创建 归还 可空字典。获取数据时如果指定键不存在可返回空而不是抛出异常 实例化一个可空字典 指定比较器实例化一个可空字典 实例化一个可空字典 获取或设置与指定的属性是否有脏数据。 资源池。支持空闲释放,主要用于数据库连接池和网络连接池 名称 空闲个数 繁忙个数 最大个数。默认100,0表示无上限 最小个数。默认1 空闲清理时间。最小个数之上的资源超过空闲时间时被清理,默认10s 完全空闲清理时间。最小个数之下的资源超过空闲时间时被清理,默认0s永不清理 基础空闲集合。只保存最小个数,最热部分 扩展空闲集合。保存最小个数以外部分 借出去的放在这 实例化一个资源池 销毁 数值 过期时间 借出 借出时是否可用 申请资源包装项,Dispose时自动归还到池中 归还 归还时是否可用 清空已有对象 销毁 创建实例 总请求数 成功数 新创建数 释放数 平均耗时。单位ms 日志 写日志 资源池包装项,自动归还资源到池中 数值 包装项 销毁 轻量级对象池。数组无锁实现,高性能 内部 1+N 的存储结果,保留最热的一个对象在外层,便于快速存取。 数组具有极快的查找速度,结构体确保没有GC操作。 对象池大小。默认CPU*2,初始化后改变无效 实例化对象池。默认大小CPU*2 获取 归还 清空 创建实例 具有是否已释放和释放后事件的接口 是否已经释放 被销毁时触发事件 具有销毁资源处理的抽象基类 /// <summary>子类重载实现资源释放逻辑时必须首先调用基类方法</summary> /// <param name="disposing">从Dispose调用(释放所有资源)还是析构函数调用(释放非托管资源)。 /// 因为该方法只会被调用一次,所以该参数的意义不太大。</param> protected override void OnDispose(bool disposing) { base.OnDispose(disposing); if (disposing) { // 如果是构造函数进来,不执行这里的代码 } } 释放资源 是否已经释放 被销毁时触发事件 释放资源,参数表示是否由Dispose调用。该方法保证OnDispose只被调用一次! 子类重载实现资源释放逻辑时必须首先调用基类方法 从Dispose调用(释放所有资源)还是析构函数调用(释放非托管资源)。 因为该方法只会被调用一次,所以该参数的意义不太大。 析构函数 如果忘记调用Dispose,这里会释放非托管资源 如果曾经调用过Dispose,因为GC.SuppressFinalize(this),不会再调用该析构函数 销毁助手。扩展方法专用 尝试销毁对象,如果有则调用 汉字拼音转换类 取拼音第一个字段 取拼音第一个字段 取拼音第一个字段 获取单字拼音 把汉字转换成拼音(全拼) 汉字字符串 转换后的拼音(全拼)字符串 系统设置。提供系统名称、版本等基本设置。 系统设置。提供系统名称、版本等基本设置。泛型基类,可继承扩展。 系统名称 系统版本 显示名称 公司 开发者模式 启用 安装时间 实例化 新建配置 系统主程序集 运行时 是否控制台。用于判断是否可以执行一些控制台操作。 是否Mono环境 是否Web环境 是否Windows环境 是否Linux环境 是否OSX环境 7Zip 实例化 压缩文件 解压缩文件 是否覆盖目标同名文件 日志 写日志 二叉树 遍历所有二叉树 构建表达式树 遍历全排列 从4种运算符中挑选3个运算符 立方根 数学运算 数据行 数据表 行索引 构造数据行 基于列索引访问 基于列名访问 读取指定行的字段值 数据表 数据列 数据列类型 数据行 总函数 读取数据 从数据流读取 读取头部 读取数据 读取 从文件加载 写入数据流 写入头部到数据流 写入数据部分到数据流 转数据包 保存到文件 读取指定行的字段值 尝试读取指定行的字段值 根据名称找字段序号 数据集 克隆 获取枚举 地理地址 名称 坐标 地址 行政区域编码 国家 省份 城市 区县 乡镇 乡镇编码 街道 级别 精确打点 可信度。[0-100] 已重载。 地理区域 编码 名称 父级 中心 边界 级别 已重载。 经纬度坐标 经度 纬度 经纬度坐标 经纬度坐标 已重载 数据帧接口 数据包 远程地址 消息 用户数据 具有扩展数据的接口 数据项 设置 或 获取 数据项 数据过滤器 下一个过滤器 对封包执行过滤器 过滤器上下文 封包 过滤器助手 在链条里面查找指定类型的过滤器 数据过滤器基类 下一个过滤器 对封包执行过滤器 执行过滤 返回是否执行下一个过滤器 数据包 数据 偏移 长度 下一个链式包 总长度 根据数据区实例化 根据数组段实例化 从可扩展内存流实例化,尝试窃取内存流内部的字节数组,失败后拷贝 因数据包内数组窃取自内存流,需要特别小心,避免多线程共用 获取/设置 指定位置的字节 设置新的数据区 数据区 偏移 字节个数 截取子数据区 相对偏移 字节个数 查找目标数组 目标数组 本数组起始偏移 本数组搜索个数 附加一个包到当前包链的末尾 返回字节数组。如果是完整数组直接返回,否则截取 不一定是全新数据,如果需要全新数据请克隆 从封包中读取指定数据 返回数据段 返回数据段集合 获取封包的数据流形式 把封包写入到数据流 把封包写入到目标数组 异步复制到目标数据流 深度克隆一份数据包,拷贝数据区 以字符串表示 字符串编码,默认URF-8 以十六进制编码表示 最大显示多少个字节。默认-1显示全部 分隔符 分组大小,为0时对每个字节应用分隔符,否则对每个分组使用 重载类型转换,字节数组直接转为Packet对象 重载类型转换,一维数组直接转为Packet对象 已重载 分页参数信息 获取 或 设置 排序字段,前台接收,便于做安全性校验 获取 或 设置 是否降序 获取 或 设置 页面索引。从1开始,默认1 如果设定了开始行,分页时将不再使用PageIndex 获取 或 设置 页面大小。默认20,若为0表示不分页 获取 或 设置 总记录数 获取 页数 获取 或 设置 组合起来的排序字句。如果没有设置则取Sort+Desc,后台设置,不经过安全性校验 获取 或 设置 开始行 如果设定了开始行,分页时将不再使用PageIndex 获取 或 设置 是否获取总记录数,默认false 获取 或 设置 状态。用于传递统计等数据 获取 或 设置 是否获取统计,默认false 实例化分页参数 通过另一个分页参数来实例化当前分页参数 从另一个分页参数拷贝到当前分页参数 获取表示分页参数唯一性的键值,可用作缓存键 泛型事件参数 参数 使用参数初始化 弹出 泛型事件参数 参数 参数2 使用参数初始化 弹出 泛型事件参数 参数 参数2 参数3 使用参数初始化 弹出 泛型事件参数 参数 参数2 参数3 参数4 使用参数初始化 弹出 弱引用Action 常见的事件和委托,都包括两部分:对象和方法,当然如果委托到静态方法上,对象是为空的。 如果把事件委托到某个对象的方法上,同时就间接的引用了这个对象,导致其一直无法被回收,从而造成内存泄漏。 弱引用Action,原理就是把委托拆分,然后弱引用对象部分,需要调用委托的时候,再把对象“拉”回来,如果被回收了,就没有必要再调用它的方法了。 目标对象。弱引用,使得调用方对象可以被GC回收 委托方法 经过包装的新的委托 取消注册的委托 是否只使用一次,如果只使用一次,执行委托后马上取消注册 是否可用 实例化 目标对象 目标方法 实例化 目标对象 目标方法 取消注册回调 是否一次性事件 实例化 事件处理器 使用事件处理器、取消注册回调、是否一次性事件来初始化 事件处理器 取消注册回调 是否一次性事件 调用委托 把弱引用事件处理器转换为普通事件处理器 已重载 X组件异常 初始化 初始化 初始化 初始化 初始化 初始化 异常事件参数 发生异常时进行的动作 异常 异常助手 是否对象已被释放异常 数学表达式 加法 减法 乘法 除法 实例化 计算运算符优先级 适配和替换 解逆波兰表达式 标准逆波兰表达式 逆波兰表达式的解 是否有效 转为浮点数 计算逆波兰表达式 最后压入数字堆栈的数字 首先压入数字堆栈的数字 操作运算符 返回计算结果 逆波兰表达式 左括号 右括号 连接符 空格 操作符数组 是否括号 是否括号 计算操作等级 是否括号匹配 适配器和替换 将中缀表达式转换为逆波兰表达式 标准中缀表达式 标准逆波兰表达式 是否有效 编译计算 与或非表达式 位与 位或 实例化 操作符等级 适配替换 容器 解逆波兰表达式 标准逆波兰表达式 逆波兰表达式的解 是否有效 所有掩码 停止话音播报 Http请求响应基类 内容长度 内容类型 头部集合 获取/设置 头部 过期时间 是否已完整 主体长度 分析第一行 创建请求响应包 创建头部 Http编解码器 写入数据 读取数据 Http消息 是否响应 是否有错 单向请求 头部数据 负载数据 根据请求创建配对的响应消息 从数据包中读取消息 是否成功 把消息转为封包 Http编码器 编码 解码参数 解码结果 转换为目标类型 创建请求 创建响应 解码 请求/响应 消息 服务动作 错误码 参数或结果 Http帮助类 创建请求包 创建响应包 分析头部 建立握手包 握手 分析WS数据包 创建WS请求包 Http请求 Http方法 资源路径 用户代理 是否压缩 保持连接 可接受内容 接受语言 引用路径 分析第一行 创建头部 Http响应 是否启用SSL 状态码 状态描述 分析第一行 创建头部 验证,如果失败则抛出异常 Http服务器 实例化 迷你Http客户端。不支持https和302跳转 客户端 内容类型 内容长度 保持连接 状态码 超时时间。默认15s 头部集合 销毁 异步请求 异步发出请求,并接收响应 同步请求 异步发出请求,并接收响应 构造请求头 解析响应 根据主机获取对象池 异步获取 地址 同步获取 Csv文件 支持整体读写以及增量式读写,目标是读写超大Csv文件 文件编码 分隔符。默认逗号 数据流实例化 Csv文件实例化 销毁 读取一行 读取所有行 写入全部 写入一行 编码助手 检测文件编码 文件名 检测文件编码 检测数据流编码 数据流 BOM检测失败时用于启发式探索的数据大小 检测字节数组编码 字节数组 检测BOM字节序 检测是否ASCII 启发式探测Unicode编码 是否可见ASCII 检测可能的UTF8序列长度 文件资源 释放文件 释放文件夹 释放文件夹 获取文件资源 Json配置文件基类 标准用法:TConfig.Current 配置实体类通过特性指定配置文件路径以及自动更新时间。 Current将加载配置文件,如果文件不存在或者加载失败,将实例化一个对象返回。 考虑到自动刷新,不提供LoadFile和SaveFile等方法,可通过扩展方法ToXmlFileEntity和ToXmlFile实现。 用户也可以通过配置实体类的静态构造函数修改基类的来动态配置加载信息。 当前实例。通过置空可以使其重新加载。 一些设置。派生类可以在自己的静态构造函数中指定 是否调试 配置文件路径 重新加载时间。单位:毫秒 没有配置文件时是否保存新配置。默认true 配置文件 最后写入时间 过期时间。如果在这个时间之后再次访问,将检查文件修改时间 是否已更新。通过文件写入时间判断 设置过期重新加载配置的时间 是否新的配置文件 销毁 加载指定配置文件 从配置文件中读取完成后触发 保存到配置文件中去 保存到配置文件中去 异步保存 新创建配置文件时执行 Json配置文件特性 配置文件名 重新加载时间。单位:毫秒 指定配置文件名 指定配置文件名和重新加载时间(毫秒) 依托于动作的日志类 方法 使用指定方法否则动作日志 写日志 已重载 代码性能计时器 参考了老赵(http://www.cnblogs.com/jeffreyzhao/archive/2009/03/10/codetimer.html)和eaglet(http://www.cnblogs.com/eaglet/archive/2009/03/10/1407791.html)两位的作品 为了保证性能比较的公平性,采用了多种指标,并使用计时器重写等手段来避免各种不必要的损耗 计时 次数 需要计时的委托 是否需要预热 计时,并用控制台输出行 标题 次数 需要计时的委托 是否需要预热 显示头部 次数 迭代方法,如不指定,则使用Time(int index) 是否显示控制台进度 进度 CPU周期 线程时间,单位是ms GC代数 执行时间 实例化一个代码计时器 计时核心方法,处理进程和线程优先级 真正的计时 执行一次迭代,预热所有方法 迭代前执行,计算时间 每一次迭代,计算时间 迭代后执行,计算时间 基准时间 已重载。输出依次分别是:执行时间、CPU线程时间、时钟周期、GC代数 复合日志提供者,多种方式输出 日志提供者集合 实例化 实例化 实例化 添加一个日志提供者 删除日志提供者 写日志 从复合日志提供者中提取指定类型的日志提供者 已重载。 控制台输出日志 是否使用多种颜色,默认使用 写日志 已重载。 性能计数器接口 数值 次数 速度 平均耗时,单位us 增加 增加的数量 耗时,单位us 计数器助手 开始计时 结束计时 日志接口 写日志 日志级别 格式化字符串 格式化参数 调试日志 格式化字符串 格式化参数 信息日志 格式化字符串 格式化参数 警告日志 格式化字符串 格式化参数 错误日志 格式化字符串 格式化参数 严重错误日志 格式化字符串 格式化参数 是否启用日志 日志等级,只输出大于等于该级别的日志,默认Info,打开NewLife.Debug时默认为最低的Debug 日志基类。提供日志的基本实现 调试日志 格式化字符串 格式化参数 信息日志 格式化字符串 格式化参数 警告日志 格式化字符串 格式化参数 错误日志 格式化字符串 格式化参数 严重错误日志 格式化字符串 格式化参数 写日志 写日志 格式化参数,特殊处理异常和时间 是否启用日志。默认true 日志等级,只输出大于等于该级别的日志,默认Info,打开NewLife.Debug时默认为最低的Debug 空日志实现 输出日志头,包含所有环境信息 日志等级 打开所有日志记录 最低调试。细粒度信息事件对调试应用程序非常有帮助 普通消息。在粗粒度级别上突出强调应用程序的运行过程 警告 错误 严重错误 关闭所有日志记录 网络日志 网络套接字 远程服务器地址 实例化网络日志。默认广播到514端口 指定日志服务器地址来实例化网络日志 销毁 写日志 性能计数器。次数、TPS、平均耗时 是否启用。默认true 数值 次数 耗时,单位us 销毁 增加 增加的数量 耗时,单位us 采样间隔,默认1000毫秒 持续采样时间,默认60秒 当前速度 最大速度 最后一个采样周期的平均耗时,单位us 持续采样时间内的最大平均耗时,单位us 定期采样,保存最近60组到数组队列里面 已重载。输出统计信息 文本控件输出日志 文本控件 最大行数,超过该行数讲清空文本控件。默认1000行 写日志 在WinForm控件上输出日志,主要考虑非UI线程操作 不是常用功能,为了避免干扰常用功能,保持UseWinForm开头 要绑定日志输出的WinForm控件 日志 最大行数 文本文件日志类。提供向文本文件写日志的能力 2015-06-01 为了继承TextFileLog,增加了无参构造函数,修改了异步写日志方法为虚方法,可以进行重载 该构造函数没有作用,为了继承而设置 每个目录的日志实例应该只有一个,所以采用静态创建 日志目录或日志文件路径 每个目录的日志实例应该只有一个,所以采用静态创建 日志目录或日志文件路径 销毁 日志文件 日志目录 日志文件格式 是否当前进程的第一次写日志 初始化日志记录文件 写文件 关闭文件 写日志 已重载。 统计代码的时间消耗 名称 最大时间。毫秒 日志输出 指定最大执行时间来构造一个代码时间统计 析构 开始 停止 跟踪流。包装一个基础数据流,主要用于重写Read/Write等行为,跟踪程序操作数据流的过程 基础流 跟踪的成员 是否小端字节序。x86系列则采用Little-Endian方式存储数据;网络协议都是Big-Endian; 网络协议都是Big-Endian; Java编译的都是Big-Endian; Motorola的PowerPC是Big-Endian; x86系列则采用Little-Endian方式存储数据; ARM同时支持 big和little,实际应用中通常使用Little-Endian。 显示位置的步长,位移超过此长度后输出位置。默认16,设为0不输出位置 写入 缓冲区 偏移 数量 写入一个字节 数值 读取 缓冲区 偏移 数量 读取一个字节 异步开始读 缓冲区 偏移 数量 异步开始写 缓冲区 偏移 数量 异步读结束 异步写结束 设置流位置 偏移 关闭数据流 刷新缓冲区 设置长度 数值 可读 可搜索 可超时 可写 可读 读写超时 长度 位置 实例化跟踪流 实例化跟踪流 操作时触发 是否使用控制台 编码 写日志事件参数 日志等级 日志信息 异常 时间 线程编号 是否线程池线程 是否Web线程 线程名 任务编号 实例化一个日志事件参数 线程专有实例。线程静态,每个线程只用一个,避免GC浪费 初始化为新日志 日志等级 返回自身,链式写法 初始化为新日志 日志 异常 返回自身,链式写法 已重载。 设置当前线程输出日志时的线程名 日志类,包含跟踪调试功能 该静态类包括写日志、写调用栈和Dump进程内存等调试功能。 默认写日志到文本文件,可通过修改属性来增加日志输出方式。 对于控制台工程,可以直接通过UseConsole方法,把日志输出重定向为控制台输出,并且可以为不同线程使用不同颜色。 文本文件日志 日志提供者,默认使用文本文件日志 输出日志 信息 写日志 输出异常日志 异常信息 2012.11.05 修正初次调用的时候,由于同步BUG,导致Log为空的问题。 使用控制台输出日志,只能调用一次 是否使用颜色,默认使用 是否同时使用文件日志,默认使用 拦截WinForm异常并记录日志,可指定是否用显示。 发为捕获异常时,是否显示提示,默认显示 在WinForm控件上输出日志,主要考虑非UI线程操作 不是常用功能,为了避免干扰常用功能,保持UseWinForm开头 要绑定日志输出的WinForm控件 是否同时使用文件日志,默认使用 最大行数 控件绑定到日志,生成混合日志 是否调试。 文本日志目录 临时目录 输出核心库和启动程序的版本号 输出程序集版本 标准消息SRMP 标准网络封包协议:1 Flag + 1 Sequence + 2 Length + N Payload 1个字节标识位,标识请求、响应、错误、加密、压缩等; 1个字节序列号,用于请求响应包配对; 2个字节数据长度N,小端,指示后续负载数据长度(不包含头部4个字节),解决粘包问题; N个字节负载数据,数据内容完全由业务决定,最大长度65535=64k。 如: Open => OK 01-01-04-00-"Open" => 81-01-02-00-"OK" 标记位 序列号,匹配请求和响应 根据请求创建配对的响应消息 从数据包中读取消息 是否成功 把消息转为封包 获取数据包长度 消息摘要 消息命令 是否响应 是否有错 单向请求 负载数据 根据请求创建配对的响应消息 从数据包中读取消息 是否成功 把消息转为封包 消息命令基类 是否响应 是否有错 单向请求 负载数据 根据请求创建配对的响应消息 从数据包中读取消息 是否成功 把消息转为封包 收到消息时的事件参数 数据包 消息 用户数据。比如远程地址等 数据包编码器 缓存流 获取长度的委托 最后一次解包成功,而不是最后一次接收 缓存有效期。超过该时间后仍未匹配数据包的缓存数据将被抛弃 最大缓存待处理数据。默认0无限制 分析数据流,得到一帧数据 待分析数据包 检查缓存 无锁并行编程模型 独立线程轮询消息队列,简单设计避免影响默认线程池。 适用于任务颗粒较大的场合,例如IO操作。 添加消息,驱动内部处理 消息 发送者 返回待处理消息数 Actor上下文 发送者 消息 无锁并行编程模型 独立线程轮询消息队列,简单设计避免影响默认线程池。 名称 是否启用 受限容量。最大可堆积的消息数 批大小。每次处理消息数,默认1,大于1表示启用批量处理模式 存放消息的邮箱。默认FIFO实现,外部可覆盖 实例化 销毁 已重载。显示名称 通知开始处理 添加消息时自动触发 开始时,返回执行线程包装任务,默认LongRunning 通知停止添加消息,并等待处理完成 添加消息,驱动内部处理 消息 发送者 返回待处理消息数 循环消费消息 循环消费消息 处理消息 上下文 批量处理消息 上下文集合 延迟队列。缓冲合并对象,批量处理 借助实体字典,缓冲实体对象,定期给字典换新,实现批量处理。 有可能外部拿到对象后,正在修改,内部恰巧执行批量处理,导致外部的部分修改未能得到处理。 解决办法是增加一个提交机制,外部用完后提交修改,内部需要处理时,等待一个时间。 名称 实体字典 跟踪数。达到该值时输出跟踪日志,默认1000 周期。默认10_000毫秒 最大个数。超过该个数时,进入队列将产生堵塞。默认100_000 批大小。默认5_000 等待借出对象确认修改的时间,默认3000ms 保存速度,每秒保存多少个实体 是否异步处理。true表示异步处理,共用DQ定时调度;false表示同步处理,独立线程 合并保存的总次数 实例化 销毁。统计队列销毁时保存数据 初始化 初始化 尝试添加 获取 或 添加 实体对象,在外部修改对象值 外部正在修改对象时,内部不允许执行批量处理 等待确认修改的借出对象数 提交对象的修改,外部不再使用该对象 当前缓存个数 定时处理全部数据 处理一批 发生错误 认证用户接口,具有登录验证、注册、在线等基本信息 密码 在线 登录次数 最后登录 最后登录IP 注册时间 注册IP 保存 用户接口工具类 比较密码相等 比较密码MD5 比较密码RC4 保存登录信息 保存注册信息 用于创建对象的工厂接口 创建对象实例 反射创建对象的工厂 创建对象实例 处理器 上一个处理器 下一个处理器 读取数据,返回结果作为下一个处理器消息 上下文 消息 写入数据,返回结果作为下一个处理器消息 上下文 消息 打开连接 上下文 关闭连接 上下文 原因 发生错误 上下文 异常 处理器 上一个处理器 下一个处理器 读取数据,返回结果作为下一个处理器消息 上下文 消息 写入数据,返回结果作为下一个处理器消息 上下文 消息 打开连接 上下文 关闭连接 上下文 原因 发生错误 上下文 异常 处理器上下文 管道 上下文拥有者 读取管道过滤后最终处理消息 写入管道过滤后最终处理消息 处理器上下文 管道 上下文拥有者 数据项 设置 或 获取 数据项 读取管道过滤后最终处理消息 写入管道过滤后最终处理消息 用户接口 编号 名称 昵称 启用 对象容器接口 1,如果容器里面没有这个类型,则返回空; 2,如果容器里面包含这个类型,返回单例; 3,如果容器里面包含这个类型,创建对象返回多实例; 4,如果有带参数构造函数,则从容器内获取各个参数的实例,最后创建对象返回。 这里有一点跟大多数对象容器非常不同,其它对象容器会控制对象的生命周期,在对象不再使用时收回到容器里面。 这里的对象容器主要是为了用于解耦,所以只有最简单的功能实现。 代码注册的默认优先级是0; 配置注册的默认优先级是1; 自动注册的外部实现(非排除项)的默认优先级是1,排除项的优先级是0; 所以,配置注册的优先级最高 注册类型和名称 接口类型 实现类型 实例 标识 优先级 遍历所有程序集的所有类型,自动注册实现了指定接口或基类的类型。如果没有注册任何实现,则默认注册第一个排除类型 自动注册一般用于单实例功能扩展型接口 接口或基类 要排除的类型,一般是内部默认实现 解析类型指定名称的实例 接口类型 标识 解析类型指定名称的实例 接口类型 标识 解析接口指定名称的实现类型 接口类型 标识 解析接口所有已注册的对象映射 接口类型 对象映射接口 名称 实现类型 对象实例 管道。进站顺序,出站逆序 头部处理器 尾部处理器 添加处理器到开头 处理器 添加处理器到末尾 处理器 添加处理器到指定名称之前 基准处理器 处理器 添加处理器到指定名称之后 基准处理器 处理器 删除处理器 处理器 读取数据,返回结果作为下一个处理器消息 上下文 消息 写入数据,返回结果作为下一个处理器消息 上下文 消息 打开连接 上下文 关闭连接 上下文 原因 发生错误 上下文 异常 管道。进站顺序,出站逆序 头部处理器 尾部处理器 添加处理器到开头 处理器 添加处理器到末尾 处理器 添加处理器到指定名称之前 基准处理器 处理器 添加处理器到指定名称之后 基准处理器 处理器 删除处理器 处理器 读取数据,顺序过滤消息,返回结果作为下一个处理器消息 上下文 消息 写入数据,逆序过滤消息,返回结果作为下一个处理器消息 上下文 消息 打开连接 上下文 关闭连接 上下文 原因 发生错误 上下文 异常 枚举器 通用插件接口 为了方便构建一个简单通用的插件系统,先规定如下: 1,负责加载插件的宿主,在加载插件后会进行插件实例化,此时可在插件构造函数中做一些事情,但不应该开始业务处理,因为宿主的准备工作可能尚未完成 2,宿主一切准备就绪后,会顺序调用插件的Init方法,并将宿主标识传入,插件通过标识区分是否自己的目标宿主。插件的Init应尽快完成。 3,如果插件实现了接口,宿主最后会清理资源。 初始化 插件宿主标识 服务提供者 返回初始化是否成功。如果当前宿主不是所期待的宿主,这里返回false 插件特性。用于判断某个插件实现类是否支持某个宿主 插件宿主标识 实例化 插件管理器 宿主标识,用于供插件区分不同宿主 宿主服务提供者 插件集合 日志提供者 实例化一个插件管理器 使用宿主对象实例化一个插件管理器 子类重载实现资源释放逻辑时必须首先调用基类方法 从Dispose调用(释放所有资源)还是析构函数调用(释放非托管资源)。 因为该方法只会被调用一次,所以该参数的意义不太大。 加载插件。此时是加载所有插件,无法识别哪些是需要的 开始初始化。初始化之后,不属于当前宿主的插件将会被过滤掉 服务接口。 服务代理XAgent可以附加代理实现了IServer接口的服务。 开始 停止 关闭原因。便于日志分析 实现 接口的对象容器 1,如果容器里面没有这个类型,则返回空; 2,如果容器里面包含这个类型,返回单例; 3,如果容器里面包含这个类型,创建对象返回多实例; 4,如果有带参数构造函数,则从容器内获取各个参数的实例,最后创建对象返回。 这里有一点跟大多数对象容器非常不同,其它对象容器会控制对象的生命周期,在对象不再使用时收回到容器里面。 这里的对象容器主要是为了用于解耦,所以只有最简单的功能实现。 代码注册的默认优先级是0; 配置注册的默认优先级是1; 自动注册的外部实现(非排除项)的默认优先级是1,排除项的优先级是0; 所以,配置注册的优先级最高 当前容器 初始化一个对象容器实例,自动从配置文件中加载注册 不存在又不添加时返回空列表 名称 实现类型 优先级 实例 注册 接口类型 实现类型 实例 标识 优先级 遍历所有程序集的所有类型,自动注册实现了指定接口或基类的类型。如果没有注册任何实现,则默认注册第一个排除类型 自动注册一般用于单实例功能扩展型接口 接口或基类 要排除的类型,一般是内部默认实现 解析类型指定名称的实例 接口类型 标识 解析类型指定名称的实例 接口类型 标识 解析接口指定名称的实现类型 接口类型 标识 解析接口所有已注册的对象映射 接口类型 已重载。 消息匹配队列接口。用于把响应数据包配对到请求包 加入请求队列 拥有者 请求消息 超时取消时间 任务源 检查请求队列是否有匹配该响应的请求 拥有者 响应消息 任务结果 用于检查匹配的回调 清空队列 消息匹配队列。子类可重载以自定义请求响应匹配逻辑 加入请求队列 拥有者 请求的数据 超时取消时间 任务源 检查请求队列是否有匹配该响应的请求 拥有者 响应消息 任务结果 用于检查匹配的回调 定时检查发送队列,超时未收到响应则重发 清空队列 长度字段作为头部 长度所在位置 长度占据字节数,1/2/4个字节,0表示压缩编码整数,默认2 过期时间,超过该时间后按废弃数据处理,默认500ms 编码 解码 连接关闭时,清空粘包编码器 消息封包 消息队列。用于匹配请求响应包 调用超时时间。默认30_000ms 使用数据包,写入时数据包转消息,读取时消息自动解包返回数据负载。默认true 写入数据 编码 加入队列 连接关闭时,清空粘包编码器 读取数据 解码 是否匹配响应 从数据流中获取整帧数据长度 数据帧长度(包含头部长度位) 按指定分割字节来处理粘包的处理器 默认以"0x0D 0x0A"即换行来分割,分割的包包含分割字节本身,使用时请注意。 默认分割方式:ISocket.Add<SplitDataCodec>() 自定义分割方式:ISocket.Add(new SplitDataHandler { SplitData = 自定义分割字节数组 }) 自定义最大缓存大小方式:ISocket.Add(new SplitDataHandler { MaxCacheDataLength = 2048 }) 自定义方式:ISocket.Add(new SplitDataHandler { MaxCacheDataLength = 2048, SplitData = 自定义分割字节数组 }) 粘包分割字节数据(默认0x0D,0x0A) 最大缓存待处理数据(字节) 读取数据 连接关闭时,清空粘包编码器 解码 获取包含分割字节在内的数据长度 标准网络封包。头部4字节定长 写入数据 加入队列 解码 是否匹配响应 连接关闭时,清空粘包编码器 网络服务会话接口 所有应用服务器以会话作为业务处理核心。 应用服务器收到新会话请求后,通过启动一个会话处理。 会话进行业务处理的过程中,可以通过多个Send方法向客户端发送数据。 编号 主服务 Socket服务器。当前通讯所在的Socket服务器,其实是TcpServer/UdpServer 客户端。跟客户端通讯的那个Socket,其实是服务端TcpSession/UdpSession 客户端地址 开始会话处理。 发送数据 数据包 发送数据流 发送字符串 异步发送并等待响应 数据到达事件 会话事件参数 会话 基础Socket接口 封装所有基础接口的共有特性! 核心设计理念:事件驱动,接口统一,简单易用! 异常处理理念:确保主流程简单易用,特殊情况的异常通过事件处理! 名称。主要用于日志输出 基础Socket对象 本地地址 端口 管道 是否抛出异常,默认false不抛出。Send/Receive时可能发生异常,该设置决定是直接抛出异常还是通过事件 异步处理接收到的数据。 异步处理有可能造成数据包乱序,特别是Tcp。true利于提升网络吞吐量。false避免拷贝,提升处理速度 发送统计 接收统计 日志提供者 是否输出发送日志。默认false 是否输出接收日志。默认false 已重载。日志加上前缀 错误发生/断开连接时 远程通信Socket,仅具有收发功能 标识 远程地址 通信开始时间 最后一次通信时间,主要表示会话活跃时间,包括收发 缓冲区大小 发送数据 目标地址由决定 数据包 是否成功 接收数据。阻塞当前线程等待返回 数据到达事件 异步发送数据并等待响应 消息 发送消息 消息 处理数据帧 数据帧 远程通信Socket扩展 获取统计信息 发送数据流 会话 数据流 返回是否成功 发送字符串 会话 要发送的字符串 文本编码,默认null表示UTF-8编码 返回自身,用于链式写法 异步多次发送数据 会话 数据包 次数 间隔 接收字符串 会话 文本编码,默认null表示UTF-8编码 添加处理器 会话 添加处理器 会话 处理器 Socket客户端 具备打开关闭 超时。默认3000ms 是否活动 打开 是否成功 关闭 关闭原因。便于日志分析 是否成功 打开后触发。 关闭后触发。可实现掉线重连 Socket服务器接口 是否活动 会话超时时间。默认20*60秒 对于每一个会话连接,如果超过该时间仍然没有收到任何数据,则断开会话连接。 会话统计 会话集合。用地址端口作为标识,业务应用自己维持地址端口与业务主键的对应关系。 新会话时触发 服务端通信Socket扩展 获取统计信息 用于与对方进行通讯的Socket会话,仅具有收发功能,也专用于上层应用收发数据 Socket会话发送数据不需要指定远程地址,因为内部已经具有。 接收数据时,Tcp接收全部数据,而Udp只接受来自所属远方的数据。 Socket会话不具有连接和断开的能力,所以需要外部连接好之后再创建Socket会话。 但是会话可以销毁,来代替断开。 对于Udp额外创建的会话来说,仅仅销毁会话而已。 所以,它必须具有收发数据的能力。 Socket服务器。当前通讯所在的Socket服务器,其实是TcpServer/UdpServer 会话事件参数 会话 帧数据传输接口 实现者确保数据以包的形式传输,屏蔽数据的粘包和拆包 超时 打开 关闭 写入数据 数据包 读取数据 数据到达事件 网络异常 初始化 初始化 初始化 初始化 初始化 网络处理器上下文 远程连接 数据帧 读取管道过滤后最终处理消息 写入管道过滤后最终处理消息 网络服务器。可同时支持多个Socket服务器,同时支持IPv4和IPv6,同时支持Tcp和Udp 网络服务器模型,所有网络应用服务器可以通过继承该类实现。 该类仅实现了业务应用对网络流的操作,与具体网络协议无关。 收到请求后,会建立会话,并加入到会话集合中,然后启动会话处理; 快速用法: 指定端口后直接,NetServer将同时监听Tcp/Udp和IPv4/IPv6(会检查是否支持)四个端口。 简单用法: 重载方法来创建一个SocketServer并赋值给属性,将会在时首先被调用。 标准用法: 使用方法向网络服务器添加Socket服务,其中第一个将作为默认Socket服务。 如果Socket服务集合为空,将依据地址、端口、地址族、协议创建默认Socket服务。 如果地址族指定为IPv4和IPv6以外的值,将同时创建IPv4和IPv6两个Socket服务; 如果协议指定为Tcp和Udp以外的值,将同时创建Tcp和Udp两个Socket服务; 默认情况下,地址族和协议都是其它值,所以一共将会创建四个Socket服务(Tcp、Tcpv6、Udp、Udpv6)。 服务名 本地结点 端口 协议类型 寻址方案 服务器集合 服务器。返回服务器集合中的第一个服务器 是否活动 会话超时时间。默认0秒,使用SocketServer默认值 对于每一个会话连接,如果超过该时间仍然没有收到任何数据,则断开会话连接。 管道 使用会话集合,允许遍历会话。默认true 会话统计 发送统计 接收统计 是否输出发送日志。默认false 是否输出接收日志。默认false 用户会话数据 获取/设置 用户会话数据 实例化一个网络服务器 通过指定监听地址和端口实例化一个网络服务器 通过指定监听地址和端口实例化一个网络服务器 通过指定监听地址和端口,还有协议,实例化一个网络服务器,默认支持Tcp协议和Udp协议 已重载。释放会话集合等资源 添加Socket服务器 添加是否成功 同时添加指定端口的IPv4和IPv6服务器,如果协议不是指定的Tcp或Udp,则同时添加Tcp和Udp服务器 确保建立服务器 添加处理器 添加处理器 处理器 开始服务 开始时调用的方法 停止服务 关闭原因。便于日志分析 停止时调用的方法 新会话,对于TCP是新连接,对于UDP是新客户端 某个会话的数据到达。sender是ISocketSession 接受连接时,对于Udp是收到数据时(同时触发OnReceived)。 收到连接时,建立会话,并挂接数据接收和错误处理事件 收到数据时 收到数据时,最原始的数据处理,但不影响会话内部的数据处理 错误发生/断开连接时。sender是ISocketSession 触发异常 会话集合。用自增的数字ID作为标识,业务应用自己维持ID与业务主键的对应关系。 会话数 最高会话数 添加会话。子类可以在添加会话前对会话进行一些处理 创建会话 根据会话ID查找会话 异步群发 创建Tcp/Udp、IPv4/IPv6服务 获取统计信息 日志提供者 用于内部Socket服务器的日志提供者 用于网络会话的日志提供者 日志前缀 写日志 输出错误日志 已重载。 网络服务器 创建会话 获取指定标识的会话 网络服务的会话 网络服务类型 主服务 网络服务的会话 实际应用可通过重载OnReceive实现收到数据时的业务逻辑。 编号 主服务 客户端。跟客户端通讯的那个Socket,其实是服务端TcpSession/UdpServer 服务端 客户端地址 用户会话数据 获取/设置 用户会话数据 开始会话处理。 子类重载实现资源释放逻辑时必须首先调用基类方法 从Dispose调用(释放所有资源)还是析构函数调用(释放非托管资源) 收到客户端发来的数据,触发事件,重载者可直接处理数据 数据到达事件 发送数据 数据包 发送数据流 发送字符串 异步发送并等待响应 错误处理 日志提供者 是否记录会话日志 日志前缀 写日志 输出错误日志 已重载。 协议类型 未知协议 传输控制协议 用户数据报协议 Http协议 Https协议 WebSocket协议 网络资源标识,指定协议、地址、端口、地址族(IPv4/IPv6) 仅序列化,其它均是配角! 有可能代表主机域名,而指定主机IP地址。 协议类型 主机 地址 端口 终结点 是否Tcp协议 是否Udp协议 实例化 实例化 实例化 实例化 实例化 分析 分析地址 主机地址 已重载。 重载类型转换,字符串直接转为NetUri对象 收到数据时的事件参数 数据包 远程地址 解码后的消息 用户数据 串口配置 串口名 波特率 数据位 停止位 奇偶校验 文本编码 编码 十六进制显示 十六进制自动换行 十六进制发送 最后更新时间 扩展数据 DtrEnable RtsEnable BreakState 串口传输 标准例程: var st = new SerialTransport(); st.PortName = "COM65"; // 通讯口 st.FrameSize = 16; // 数据帧大小 st.Received += (s, e) => { Console.WriteLine("收到 {0}", e.ToHex()); }; // 开始异步操作 st.Open(); //var buf = "01080000801A".ToHex(); var buf = "0111C02C".ToHex(); for (int i = 0; i < 100; i++) { Console.WriteLine("发送 {0}", buf.ToHex()); st.Send(buf); Thread.Sleep(1000); } 串口对象 端口名称。默认COM1 波特率。默认115200 奇偶校验位。默认None 数据位。默认8 停止位。默认One 超时时间。超过该大小未收到数据,说明是另一帧。默认10ms 描述信息 字节超时。数据包间隔,默认20ms 串口传输 销毁 确保创建 打开 关闭 写入数据 数据包 异步发送数据并等待响应 接收数据 处理收到的数据。默认匹配同步接收委托 数据到达事件 断开时触发,可能是人为断开,也可能是串口链路断开 检查串口是否已经断开 FX串口异步操作有严重的泄漏缺陷,如果外部硬件长时间断开, SerialPort.IsOpen检测不到,并且会无限大占用内存。 获取带有描述的串口名,没有时返回空数组 获取串口列表,名称和描述 从串口列表选择串口,支持自动选择关键字 串口名称或者描述符的关键字 日志对象 输出日志 已重载 会话基类 标识 名称 本地绑定信息 端口 远程结点地址 超时。默认3000ms 是否活动 底层Socket 是否抛出异常,默认false不抛出。Send/Receive时可能发生异常,该设置决定是直接抛出异常还是通过事件 发送数据包统计信息 接收数据包统计信息 通信开始时间 最后一次通信时间,主要表示活跃时间,包括收发 是否使用动态端口。如果Port为0则为动态端口 最大并行接收数。Tcp默认1,Udp默认CPU*1.6,0关闭异步接收使用同步接收 异步处理接收到的数据,Tcp默认false,Udp默认true。 异步处理有可能造成数据包乱序,特别是Tcp。true利于提升网络吞吐量。false避免拷贝,提升处理速度 缓冲区大小。默认8k 构造函数,初始化默认名称 销毁 已重载。 打开 是否成功 打开 检查是否动态端口。如果是动态端口,则把随机得到的端口拷贝到Port 关闭 关闭原因。便于日志分析 是否成功 关闭 关闭原因。便于日志分析 打开后触发。 关闭后触发。可实现掉线重连 直接发送数据包 Byte[]/Packet 目标地址由决定 数据包 是否成功 发送数据 目标地址由决定 数据包 是否成功 接收数据 当前异步接收个数 开始异步接收 是否成功 释放一个事件参数 用一个事件参数来开始异步接收 事件参数 是否在IO线程调用 同步或异步收到数据 接收预处理,粘包拆包 预处理 数据包 远程地址 将要处理该数据包的会话 处理收到的数据。默认匹配同步接收委托 接收事件参数 是否已处理,已处理的数据不再向下传递 数据到达事件 触发数据到达事件 接收事件参数 收到异常时如何处理。默认关闭会话 是否当作异常处理并结束会话 消息管道。收发消息都经过管道处理器 创建上下文 远程会话 通过管道发送消息 通过管道发送消息并等待响应 处理数据帧 数据帧 错误发生/断开连接时 触发异常 动作 异常 数据项 设置 或 获取 数据项 日志前缀 日志对象。禁止设为空对象 是否输出发送日志。默认false 是否输出接收日志。默认false 输出日志 会话集合。带有自动清理不活动会话的功能 服务端 清理周期。单位毫秒,默认10秒。 清理会话计时器 添加新会话,并设置会话编号 返回添加新会话是否成功 获取会话,加锁 关闭所有 移除不活动的会话 网络设置 网络调试 会话超时时间。默认20*60秒 缓冲区大小。默认64k 实例化 Socket扩展 异步发送数据 异步发送数据 发送数据流 返回自身,用于链式写法 向指定目的地发送信息 缓冲区 返回自身,用于链式写法 向指定目的地发送信息 文本编码,默认null表示UTF-8编码 返回自身,用于链式写法 广播数据包 缓冲区 广播字符串 接收字符串 文本编码,默认null表示UTF-8编码 检查并开启广播 关闭连接 SafeHandle字段 Socket是否未被关闭 根据异步事件获取可输出异常,屏蔽常见异常 TCP服务器 核心工作:启动服务时,监听端口,并启用多个(逻辑处理器数的10倍)异步接受操作。 服务器完全处于异步工作状态,任何操作都不可能被阻塞。 注意:服务器接受连接请求后,不会开始处理数据,而是由事件订阅者决定何时开始处理数据。 名称 本地绑定信息 端口 会话超时时间 对于每一个会话连接,如果超过该时间仍然没有收到任何数据,则断开会话连接。 异步处理接收到的数据,默认false。 异步处理有可能造成数据包乱序,特别是Tcp。true利于提升网络吞吐量。false避免拷贝,提升处理速度 底层Socket 是否活动 是否抛出异常,默认false不抛出。Send/Receive时可能发生异常,该设置决定是直接抛出异常还是通过事件 最大并行接收连接数。默认CPU*1.6 启用Http,数据处理时截去请求响应头,默认false 管道 会话统计 发送统计 接收统计 构造TCP服务器对象 构造TCP服务器对象 已重载。释放会话集合等资源 开始 停止 关闭原因。便于日志分析 新会话时触发 开启异步接受新连接 是否IO线程 开启异步是否成功 收到新连接时处理 会话集合。用地址端口作为标识,业务应用自己维持地址端口与业务主键的对应关系。 创建会话 错误发生/断开连接时 触发异常 动作 异常 日志前缀 日志对象 是否输出发送日志。默认false 是否输出接收日志。默认false 输出日志 已重载。 增强TCP客户端 收到空数据时抛出异常并断开连接。默认true Socket服务器。当前通讯所在的Socket服务器,其实是TcpServer/UdpServer。该属性决定本会话是客户端会话还是服务的会话 自动重连次数,默认3。发生异常断开连接时,自动重连服务端。 是否匹配空包。Http协议需要 不延迟直接发送。Tcp为了合并小包而设计,客户端默认false,服务端默认true 实例化增强TCP 使用监听口初始化 用TCP客户端初始化 打开 关闭 关闭原因。便于日志分析 发送数据 目标地址由决定 数据包 是否成功 预处理 数据包 远程地址 将要处理该数据包的会话 处理收到的数据 接收事件参数 重连次数 日志前缀 已重载。 增强的UDP 如果已经打开异步接收,还要使用同步接收,则同步Receive内部不再调用底层Socket,而是等待截走异步数据。 会话超时时间 对于每一个会话连接,如果超过该时间仍然没有收到任何数据,则断开会话连接。 最后一次同步接收数据得到的远程地址 是否接收来自自己广播的环回数据。默认false 会话统计 实例化增强UDP 使用监听口初始化 打开 关闭 发送数据 目标地址由决定 数据包 是否成功 发送消息并等待响应。必须调用会话的发送,否则配对会失败 预处理 数据包 远程地址 将要处理该数据包的会话 处理收到的数据 接收事件参数 收到异常时如何处理。Tcp/Udp客户端默认关闭会话,但是Udp服务端不能关闭服务器,仅关闭会话 是否当作异常处理并结束会话 新会话时触发 会话集合。用地址端口作为标识,业务应用自己维持地址端口与业务主键的对应关系。 创建会话 已重载。 Udp扩展 发送数据流 返回自身,用于链式写法 向指定目的地发送信息 缓冲区 返回自身,用于链式写法 向指定目的地发送信息 文本编码,默认null表示UTF-8编码 返回自身,用于链式写法 广播数据包 缓冲区 广播字符串 接收字符串 文本编码,默认null表示UTF-8编码 Udp会话。仅用于服务端与某一固定远程地址通信 会话编号 名称 服务器 底层Socket 本地地址 端口 远程地址 超时。默认3000ms 管道 Socket服务器。当前通讯所在的Socket服务器,其实是TcpServer/UdpServer 是否抛出异常,默认false不抛出。Send/Receive时可能发生异常,该设置决定是直接抛出异常还是通过事件 异步处理接收到的数据,默认true利于提升网络吞吐量。 异步处理有可能造成数据包乱序,特别是Tcp。false避免拷贝,提升处理速度 发送数据包统计信息 接收数据包统计信息 通信开始时间 最后一次通信时间,主要表示活跃时间,包括收发 缓冲区大小。默认8k 发送消息 发送消息并等待响应 接收数据 处理数据帧 数据帧 错误发生/断开连接时 触发异常 动作 异常 已重载。 数据项 设置 或 获取 数据项 日志提供者 是否输出发送日志。默认false 是否输出接收日志。默认false 日志前缀 输出日志 输出日志 升级更新 自动更新的难点在于覆盖正在使用的exe/dll文件,通过改名可以解决 名称 服务器地址 版本 本地编译时间 更新完成以后自动启动主程序 更新目录 超链接信息,其中第一个为最佳匹配项 更新源文件 实例化一个升级对象实例,获取当前应用信息 获取版本信息,检查是否需要更新 开始更新 检查并执行更新操作 正在使用锁定的文件不可删除,但可以改名 删除备份文件 日志对象 输出日志 程序集辅助类。使用Create创建,保证每个程序集只有一个辅助类 程序集 名称 程序集版本 程序集标题 文件版本 编译时间 编译版本 公司名称 说明 获取包含清单的已加载文件的路径或 UNC 位置。 创建程序集辅助对象 类型集合,当前程序集的所有类型,包括私有和内嵌,非内嵌请直接调用Asm.GetTypes() 是否系统程序集 入口程序集 从程序集中查找指定名称的类型 在程序集中查找类型 查找插件 查找插件,带缓存 类型 查找所有非系统程序集中的所有插件 继承类所在的程序集会引用baseType所在的程序集,利用这一点可以做一定程度的性能优化。 是否从未加载程序集中获取类型。使用仅反射的方法检查目标类型,如果存在,则进行常规加载 指示是否应检查来自所有引用程序集的类型。如果为 false,则检查来自所有引用程序集的类型。 否则,只检查来自非全局程序集缓存 (GAC) 引用的程序集的类型。 是否引用了 程序集 被引用程序集全名 根据名称获取类型 类型名 是否从未加载程序集中获取类型。使用仅反射的方法检查目标类型,如果存在,则进行常规加载 获取指定程序域所有程序集 程序集目录集合 获取当前程序域所有只反射程序集的辅助类 只反射加载指定路径的所有程序集 只反射加载指定路径的所有程序集 获取当前应用程序的所有程序集,不包括系统程序集,仅限本目录 在对程序集的解析失败时发生 已重载。 包装程序集内部类的动态对象 类型转换 成员取值 调用成员 包装 已重载。 动态Xml 节点 实例化 实例化 实例化 设置 获取 索引器接访问口。 该接口用于通过名称快速访问对象属性或字段(属性优先)。 获取/设置 指定名称的属性或字段的值 名称 反射接口 该接口仅用于扩展,不建议外部使用 根据名称获取类型 类型名 是否从未加载程序集中获取类型。使用仅反射的方法检查目标类型,如果存在,则进行常规加载 获取方法 用于具有多个签名的同名方法的场合,不确定是否存在性能问题,不建议普通场合使用 类型 名称 参数类型数组 获取指定名称的方法集合,支持指定参数个数来匹配过滤 参数个数,-1表示不过滤参数个数 获取属性 类型 名称 忽略大小写 获取字段 类型 名称 忽略大小写 获取成员 类型 名称 忽略大小写 获取字段 获取属性 反射创建指定类型的实例 类型 参数数组 反射调用指定对象的方法 要调用其方法的对象,如果要调用静态方法,则target是类型 方法 方法参数 反射调用指定对象的方法 要调用其方法的对象,如果要调用静态方法,则target是类型 方法 方法参数字典 获取目标对象的属性值 目标对象 属性 获取目标对象的字段值 目标对象 字段 设置目标对象的属性值 目标对象 属性 数值 设置目标对象的字段值 目标对象 字段 数值 从源对象拷贝数据到目标对象 目标对象 源对象 递归深度拷贝,直接拷贝成员值而不是引用 要忽略的成员 从源字典拷贝数据到目标对象 目标对象 源字典 递归深度拷贝,直接拷贝成员值而不是引用 获取一个类型的元素类型 类型 类型转换 数值 获取类型的友好名称 指定类型 是否全名,包含命名空间 是否能够转为指定基类 在指定程序集中查找指定基类或接口的所有子类实现 指定程序集 基类或接口,为空时返回所有类型 在所有程序集中查找指定基类或接口的子类实现 基类或接口 是否加载为加载程序集 默认反射实现 该接口仅用于扩展,不建议外部使用 根据名称获取类型 类型名 是否从未加载程序集中获取类型。使用仅反射的方法检查目标类型,如果存在,则进行常规加载 获取方法 用于具有多个签名的同名方法的场合,不确定是否存在性能问题,不建议普通场合使用 类型 名称 参数类型数组 获取指定名称的方法集合,支持指定参数个数来匹配过滤 参数个数,-1表示不过滤参数个数 获取属性 类型 名称 忽略大小写 获取字段 类型 名称 忽略大小写 获取成员 类型 名称 忽略大小写 获取字段 获取属性 反射创建指定类型的实例 类型 参数数组 反射调用指定对象的方法 要调用其方法的对象,如果要调用静态方法,则target是类型 方法 方法参数 反射调用指定对象的方法 要调用其方法的对象,如果要调用静态方法,则target是类型 方法 方法参数字典 获取目标对象的属性值 目标对象 属性 获取目标对象的字段值 目标对象 字段 设置目标对象的属性值 目标对象 属性 数值 设置目标对象的字段值 目标对象 字段 数值 从源对象拷贝数据到目标对象 目标对象 源对象 递归深度拷贝,直接拷贝成员值而不是引用 要忽略的成员 从源字典拷贝数据到目标对象 目标对象 源字典 递归深度拷贝,直接拷贝成员值而不是引用 获取一个类型的元素类型 类型 类型转换 数值 获取类型的友好名称 指定类型 是否全名,包含命名空间 是否子类 在指定程序集中查找指定基类的子类 指定程序集 基类或接口,为空时返回所有类型 在所有程序集中查找指定基类或接口的子类实现 基类或接口 是否加载为加载程序集 获取类型,如果target是Type类型,则表示要反射的是静态成员 目标对象 反射工具类 当前反射提供者 根据名称获取类型。可搜索当前目录DLL,自动加载 类型名 是否从未加载程序集中获取类型。使用仅反射的方法检查目标类型,如果存在,则进行常规加载 获取方法 用于具有多个签名的同名方法的场合,不确定是否存在性能问题,不建议普通场合使用 类型 名称 参数类型数组 获取指定名称的方法集合,支持指定参数个数来匹配过滤 参数个数,-1表示不过滤参数个数 获取属性。搜索私有、静态、基类,优先返回大小写精确匹配成员 类型 名称 忽略大小写 获取字段。搜索私有、静态、基类,优先返回大小写精确匹配成员 类型 名称 忽略大小写 获取成员。搜索私有、静态、基类,优先返回大小写精确匹配成员 类型 名称 忽略大小写 获取用于序列化的字段 过滤特性的字段 获取用于序列化的属性 过滤特性的属性和索引器 反射创建指定类型的实例 类型 参数数组 反射调用指定对象的方法。target为类型时调用其静态方法 要调用其方法的对象,如果要调用静态方法,则target是类型 方法名 方法参数 反射调用指定对象的方法 要调用其方法的对象,如果要调用静态方法,则target是类型 方法名 数值 方法参数 反射调用是否成功 反射调用指定对象的方法 要调用其方法的对象,如果要调用静态方法,则target是类型 方法 方法参数 反射调用指定对象的方法 要调用其方法的对象,如果要调用静态方法,则target是类型 方法 方法参数字典 获取目标对象指定名称的属性/字段值 目标对象 名称 出错时是否抛出异常 获取目标对象指定名称的属性/字段值 目标对象 名称 数值 是否成功获取数值 获取目标对象的成员值 目标对象 成员 设置目标对象指定名称的属性/字段值,若不存在返回false 目标对象 名称 数值 反射调用是否成功 设置目标对象的成员值 目标对象 成员 数值 从源对象拷贝数据到目标对象 目标对象 源对象 递归深度拷贝,直接拷贝成员值而不是引用 要忽略的成员 从源字典拷贝数据到目标对象 目标对象 源字典 递归深度拷贝,直接拷贝成员值而不是引用 获取一个类型的元素类型 类型 类型转换 数值 类型转换 数值 获取类型的友好名称 指定类型 是否全名,包含命名空间 从参数数组中获取类型数组 获取成员的类型,字段和属性是它们的类型,方法是返回类型,类型是自身 获取类型代码 是否整数 是否泛型列表 是否泛型字典 是否能够转为指定基类 是否能够转为指定基类 在指定程序集中查找指定基类的子类 指定程序集 基类或接口 在所有程序集中查找指定基类或接口的子类实现 基类或接口 是否加载为加载程序集 获取类型,如果target是Type类型,则表示要反射的是静态成员 目标对象 判断某个类型是否可空类型 类型 把一个方法转为泛型委托,便于快速反射调用 脚本引擎 三大用法: 1,单个表达式,根据参数计算表达式结果并返回 2,多个语句,最后有返回语句 3,多个方法,有一个名为Execute的静态方法作为入口方法 脚本引擎禁止实例化,必须通过方法创建,以代码为键进行缓存,避免重复创建反复编译形成泄漏。 其中方法的第二个参数为true表示前两种用法,为false表示第三种用法。 最简单而完整的用法: // 根据代码创建脚本实例,相同代码只编译一次 var se = ScriptEngine.Create("a+b"); // 如果Method为空说明未编译,可设置参数 if (se.Method == null) { se.Parameters.Add("a", typeof(Int32)); se.Parameters.Add("b", typeof(Int32)); } // 脚本固定返回Object类型,需要自己转换 var n = (Int32)se.Invoke(2, 3); Console.WriteLine("2+3={0}", n); 无参数快速调用: var n = (Int32)ScriptEngine.Execute("2*3"); 约定参数快速调用: var n = (Int32)ScriptEngine.Execute("p0*p1", new Object[] { 2, 3 }); Console.WriteLine("2*3={0}", n); 代码 是否表达式 参数集合。编译后就不可修改。 最终代码 编译得到的类型 根据代码编译出来可供直接调用的入口方法,Eval/Main 命名空间集合 引用程序集集合 日志 工作目录。执行时,将会作为环境变量的当前目录和PathHelper目录,执行后还原 构造函数私有,禁止外部越过Create方法直接创建实例 代码片段 是否表达式,表达式将编译成为一个Main方法 为指定代码片段创建脚本引擎实例。采用缓存,避免同一脚本重复创建引擎。 代码片段 是否表达式,表达式将编译成为一个Main方法 执行表达式,返回结果 代码片段 执行表达式,返回结果 代码片段 参数名称 参数类型 参数值 执行表达式,返回结果 代码片段 参数名值对 执行表达式,返回结果。参数名默认为p0/p1/p2/pn 参数数组 生成代码。根据完善得到最终代码 获取完整源代码 编译 编译 按照传入参数执行代码 参数 结果 分析命名空间 Api动作 动作名称 动作所在类型 方法 控制器对象 如果指定控制器对象,则每次调用前不再实例化对象 是否二进制参数 是否二进制返回 实例化 获取名称 已重载。 标识Api 名称 实例化 应用接口客户端 是否已打开 服务端地址集合。负载均衡 客户端连接集群 是否使用连接池。true时建立多个到服务端的连接,默认false使用单一连接 主机 最后活跃时间 所有服务器所有会话,包含自己 发送数据包统计信息 接收数据包统计信息 实例化应用接口客户端 实例化应用接口客户端 服务端地址集合,逗号分隔 销毁 打开客户端 关闭 关闭原因。便于日志分析 是否成功 查找Api动作 创建控制器实例 异步调用,等待返回结果 返回类型 服务操作 参数 标识 异步调用,等待返回结果 服务操作 参数 标识 同步调用,阻塞等待 服务操作 参数 标识 单向发送。同步调用,不等待返回 服务操作 参数 标识 指定客户端的异步调用,等待返回结果 常用于在OnLoginAsync中实现连接后登录功能 客户端 服务操作 参数 标识 新会话。客户端每次连接或断线重连后,可用InvokeWithClientAsync做登录 会话 状态。客户端ISocketClient 连接后自动登录 客户端 强制登录 登录 创建客户端之后,打开连接之前 显示统计信息的周期。默认600秒,0表示不显示统计信息 远程调用异常 代码 实例化远程调用异常 实例化远程调用异常 Api主机 名称 编码器 处理器 调用超时时间。请求发出后,等待响应的最大时间,默认15_000ms 发送数据包统计信息 接收数据包统计信息 用户会话数据 获取/设置 用户会话数据 接口动作管理器 注册服务提供类。该类的所有公开方法将直接暴露 注册服务 控制器对象 动作名称。为空时遍历控制器所有公有成员方法 显示可用服务 获取消息编码器。重载以指定不同的封包协议 处理消息 执行 新会话。服务端收到新连接,客户端每次连接或断线重连后,可用于做登录 会话 状态。客户端ISocketClient 日志 编码器日志 显示调用和处理错误。默认false 写日志 已重载。返回具有本类特征的字符串 String 初始化 主机 当前服务器所有会话 调用超时时间。默认30_000ms 初始化 主机 最后活跃时间 所有服务器所有会话,包含自己 开始会话处理 查找Api动作 创建控制器实例 远程调用 服务操作 参数 标识 应用接口服务器 是否正在工作 端口 服务器 实例化一个应用接口服务器 使用指定端口实例化网络服务应用接口提供者 实例化 销毁时停止服务 添加服务器 确保已创建服务器对象 开始服务 停止服务 关闭原因。便于日志分析 显示统计信息的周期。默认600秒,0表示不显示统计信息 客户端连接池负载均衡集群 服务器地址列表 创建回调 连接池 实例化连接池集群 打开 关闭 关闭原因。便于日志分析 是否成功 从集群中获取资源 归还 Round-Robin 负载均衡 为连接池创建连接 客户端单连接故障转移集群 服务器地址列表 创建回调 打开 关闭 关闭原因。便于日志分析 是否成功 从集群中获取资源 归还 Round-Robin 负载均衡 为连接池创建连接 API控制器 主机 获取所有接口 服务器信息,用户健康检测 状态信息 控制器上下文 控制器实例 处理动作 真实动作名称 会话 请求 请求参数 获取或设置操作方法参数。 获取或设置由操作方法返回的结果。 获取或设置在操作方法的执行过程中发生的异常(如果有)。 获取或设置一个值,该值指示是否处理异常。 实例化 当前线程上下文 重置为默认状态 定义操作筛选器中使用的方法。 在执行操作方法之前调用。 在执行操作方法后调用。 Api接口 会话 Api处理器 执行 默认处理器 Api接口主机 执行 准备上下文 获取参数 Api主机 编码器 处理器 接口动作管理器 获取消息编码器。重载以指定不同的封包协议 处理消息 发送统计 接收统计 日志 写日志 Api主机助手 调用 结果类型 服务操作 参数 标识 调用 服务操作 参数 标识 创建控制器实例 获取统计信息 接口管理器 可提供服务的方法 注册服务提供类。该类的所有公开方法将直接暴露 注册服务 控制器对象 动作名称。为空时遍历控制器所有公有成员方法 查找服务 可提供服务的方法 注册服务提供类。该类的所有公开方法将直接暴露 注册服务 控制器对象 动作名称。为空时遍历控制器所有公有成员方法 查找服务 应用接口服务器接口 主机 当前服务器所有会话 初始化 开始 停止 关闭原因。便于日志分析 日志 Api会话 主机 最后活跃时间 所有服务器所有会话,包含自己 获取/设置 用户会话数据 查找Api动作 创建控制器实例 发送消息。低级接口,由框架使用 发送消息。低级接口,由框架使用 远程调用 服务操作 参数 标识 编码器 创建请求 创建响应 解码 请求/响应 消息 服务动作 错误码 参数或结果 解码参数 解码结果 转换为目标类型 日志提供者 编码器基类 解码 请求/响应 消息 服务动作 错误码 参数或结果 日志提供者 写日志 Json编码器 编码 请求/响应 编码 解码参数 解码结果 转换为目标类型 创建请求 创建响应 证书 http://blogs.msdn.com/b/dcook/archive/2008/11/25/creating-a-self-signed-certificate-in-c.aspx 建立自签名证书 建立自签名证书 建立自签名证书 建立自签名证书 例如CN=SelfSignCertificate;C=China;OU=NewLife;O=Development Team;E=nnhy@vip.qq.com,其中CN是显示名 建立自签名证书 CRC16校验 CRC16表 校验值 重置清零 添加整数进行校验 the byte is taken as the lower 8 bits of value 添加字节数组进行校验 CRC16-CCITT x16+x12+x5+1 1021 ISO HDLC, ITU X.25, V.34/V.41/V.42, PPP-FCS 字符串123456789的Crc16是31C3 数据缓冲区 偏移量 字节个数 添加数据流进行校验 CRC-16 x16+x15+x2+1 8005 IBM SDLC 数量 计算校验码 计算数据流校验码 计算数据流校验码,指定起始位置和字节数偏移量 一般用于计算数据包校验码,需要回过头去开始校验,并且可能需要跳过最后的校验码长度。 position小于0时,数据流从当前位置开始计算校验; position大于等于0时,数据流移到该位置开始计算校验,最后由count决定可能差几个字节不参与计算; 如果大于等于0,则表示从该位置开始计算 字节数偏移量,一般用负数表示 CRC32校验 Generate a table for a byte-wise 32-bit CRC calculation on the polynomial: x^32+x^26+x^23+x^22+x^16+x^12+x^11+x^10+x^8+x^7+x^5+x^4+x^2+x+1. Polynomials over GF(2) are represented in binary, one bit per coefficient, with the lowest powers in the most significant bit. Then adding polynomials is just exclusive-or, and multiplying a polynomial by x is a right shift by one. If we call the above polynomial p, and represent a byte as the polynomial q, also with the lowest power in the most significant bit (so the byte 0xb1 is the polynomial x^7+x^3+x+1), then the CRC is (q*x^32) mod p, where a mod b means the remainder after dividing a by b. This calculation is done using the shift-register method of multiplying and taking the remainder. The register is initialized to zero, and for each incoming bit, x^32 is added mod p to the register if the bit is a one (where x^32 mod p is p+x^32 = x^26+...+1), and the register is multiplied mod p by x (which is shifting right by one and adding x^32 mod p if the bit shifted out is a one). We start with the highest power (least significant bit) of q and repeat for all eight bits of q. The table is simply the CRC of all possible eight bit values. This is all the information needed to generate CRC's on data a byte at a time for all combinations of CRC register values and incoming bytes. 校验表 校验值 校验值 重置清零 添加整数进行校验 the byte is taken as the lower 8 bits of value 添加字节数组进行校验 The buffer which contains the data The offset in the buffer where the data starts The number of data bytes to update the CRC with. 添加数据流进行校验 数量 计算校验码 计算数据流校验码 计算数据流校验码,指定起始位置和字节数偏移量 一般用于计算数据包校验码,需要回过头去开始校验,并且可能需要跳过最后的校验码长度。 position小于0时,数据流从当前位置开始计算校验; position大于等于0时,数据流移到该位置开始计算校验,最后由count决定可能差几个字节不参与计算; 如果大于等于0,则表示从该位置开始计算 字节数偏移量,一般用负数表示 DSA算法 产生非对称密钥对(私钥和公钥) 密钥长度,默认1024位强密钥 私钥和公钥 签名 验证 从Xml加载DSA密钥 保存DSA密钥到Xml 随机数 返回一个小于所指定最大值的非负随机数 返回的随机数的上界(随机数不能取该上界值) 返回一个指定范围内的随机数 调用平均耗时37.76ns,其中GC耗时77.56% 返回的随机数的下界(随机数可取该下界值) 返回的随机数的上界(随机数不能取该上界值) 返回指定长度随机字节数组 调用平均耗时5.46ns,其中GC耗时15% 返回指定长度随机字符串 长度 是否包含符号 RC4对称加密算法 RC4于1987年提出,和DES算法一样,是一种对称加密算法,也就是说使用的密钥为单钥(或称为私钥)。 但不同于DES的是,RC4不是对明文进行分组处理,而是字节流的方式依次加密明文中的每一个字节,解密的时候也是依次对密文中的每一个字节进行解密。 RC4算法的特点是算法简单,运行速度快,而且密钥长度是可变的,可变范围为1-256字节(8-2048比特), 在如今技术支持的前提下,当密钥长度为128比特时,用暴力法搜索密钥已经不太可行,所以可以预见RC4的密钥范围任然可以在今后相当长的时间里抵御暴力搜索密钥的攻击。 实际上,如今也没有找到对于128bit密钥长度的RC4加密算法的有效攻击方法。 加密 数据 密码 打乱密码 密码 密码箱长度 打乱后的密码 RSA算法 RSA加密或签名小数据块时,密文长度128,速度也很快。 产生非对称密钥对 RSAParameters的各个字段采用大端字节序,转为BigInteger的之前一定要倒序。 RSA加密后密文最小长度就是密钥长度,所以1024密钥最小密文长度是128字节。 密钥长度,默认1024位强密钥 RSA加密 如果为 true,则使用 OAEP 填充(仅可用于运行 Windows XP 及更高版本的计算机)执行直接 System.Security.Cryptography.RSA加密;否则,如果为 false,则使用 PKCS#1 v1.5 填充。 RSA解密 如果为 true,则使用 OAEP 填充(仅可用于运行 Microsoft Windows XP 及更高版本的计算机)执行直接 System.Security.Cryptography.RSA解密;否则,如果为 false 则使用 PKCS#1 v1.5 填充。 配合DES加密 配合DES解密 配合对称算法加密 配合对称算法解密 签名 验证 使用随机数设置 访问器基类 从数据流中读取消息 数据流 上下文 是否成功 把消息写入到数据流中 数据流 上下文 消息转为字节数组 创建序列化器 输出消息实体 获取成员输出 获取用于输出的成员值 访问器泛型基类 从流中读取消息 从字节数组中读取消息 二进制编码解码器 使用7位编码整数。默认true使用 对象转二进制 二进制转对象 二进制编码解码器 使用7位编码整数。默认true使用 对象转二进制 二进制转对象 二进制序列化 使用7位编码整数。默认false不使用 小端字节序。默认false大端 使用指定大小的FieldSizeAttribute特性,默认false 使用对象引用,默认true 大小宽度。可选0/1/2/4,默认0表示压缩编码整数 要忽略的成员 处理器列表 实例化 添加处理器 添加处理器 获取处理器 写入一个对象 目标对象 类型 写入字节 将字节数组部分写入当前流,不写入数组长度。 包含要写入的数据的字节数组。 buffer 中开始写入的起始点。 要写入的字节数。 写入大小,如果有FieldSize则返回,否则写入编码的大小 写7位压缩编码整数 以7位压缩格式写入32位整数,小于7位用1个字节,小于14位用2个字节。 由每次写入的一个字节的第一位标记后面的字节是否还是当前数据,所以每个字节实际可利用存储空间只有后7位。 数值 实际写入字节数 读取指定类型对象 读取指定类型对象 尝试读取指定类型对象 读取字节 从当前流中将 count 个字节读入字节数组 要读取的字节数。 读取大小 读取整数的字节数组,某些写入器(如二进制写入器)可能需要改变字节顺序 数量 从当前流中读取 2 字节有符号整数,并使流的当前位置提升 2 个字节。 从当前流中读取 4 字节有符号整数,并使流的当前位置提升 4 个字节。 以压缩格式读取16位整数 以压缩格式读取32位整数 以压缩格式读取64位整数 使用跟踪流。实际上是重新包装一次Stream,必须在设置Stream后,使用之前 快速读取 数据流 使用7位编码整数 快速写入 对象 使用7位编码整数 颜色处理器。 实例化 写入对象 目标对象 类型 尝试读取指定类型对象 复合对象处理器 实例化 写入对象 目标对象 类型 尝试读取指定类型对象 获取成员 字典数据编码 初始化 写入一个对象 目标对象 类型 尝试读取指定类型对象 字体处理器。 实例化 写入对象 目标对象 类型 尝试读取指定类型对象 二进制基础类型处理器 实例化 写入一个对象 目标对象 类型 是否处理成功 尝试读取指定类型对象 将一个无符号字节写入 要写入的无符号字节。 将字节数组写入,如果设置了UseSize,则先写入数组长度。 包含要写入的数据的字节数组。 将字节数组部分写入当前流,不写入数组长度。 包含要写入的数据的字节数组。 buffer 中开始写入的起始点。 要写入的字节数。 写入字节数组,自动计算长度 缓冲区 数量 将 2 字节有符号整数写入当前流,并将流的位置提升 2 个字节。 要写入的 2 字节有符号整数。 将 4 字节有符号整数写入当前流,并将流的位置提升 4 个字节。 要写入的 4 字节有符号整数。 将 8 字节有符号整数写入当前流,并将流的位置提升 8 个字节。 要写入的 8 字节有符号整数。 判断字节顺序 缓冲区 将 2 字节无符号整数写入当前流,并将流的位置提升 2 个字节。 要写入的 2 字节无符号整数。 将 4 字节无符号整数写入当前流,并将流的位置提升 4 个字节。 要写入的 4 字节无符号整数。 将 8 字节无符号整数写入当前流,并将流的位置提升 8 个字节。 要写入的 8 字节无符号整数。 将 4 字节浮点值写入当前流,并将流的位置提升 4 个字节。 要写入的 4 字节浮点值。 将 8 字节浮点值写入当前流,并将流的位置提升 8 个字节。 要写入的 8 字节浮点值。 将一个十进制值写入当前流,并将流位置提升十六个字节。 要写入的十进制值。 将 Unicode 字符写入当前流,并根据所使用的 Encoding 和向流中写入的特定字符,提升流的当前位置。 要写入的非代理项 Unicode 字符。 将字符数组部分写入当前流,并根据所使用的 Encoding(可能还根据向流中写入的特定字符),提升流的当前位置。 包含要写入的数据的字符数组。 chars 中开始写入的起始点。 要写入的字符数。 写入字符串 要写入的值。 从当前流中读取下一个字节,并使流的当前位置提升 1 个字节。 从当前流中将 count 个字节读入字节数组,如果count小于0,则先读取字节数组长度。 要读取的字节数。 读取整数的字节数组,某些写入器(如二进制写入器)可能需要改变字节顺序 数量 从当前流中读取 2 字节有符号整数,并使流的当前位置提升 2 个字节。 从当前流中读取 4 字节有符号整数,并使流的当前位置提升 4 个字节。 从当前流中读取 8 字节有符号整数,并使流的当前位置向前移动 8 个字节。 使用 Little-Endian 编码从当前流中读取 2 字节无符号整数,并将流的位置提升 2 个字节。 从当前流中读取 4 字节无符号整数并使流的当前位置提升 4 个字节。 从当前流中读取 8 字节无符号整数并使流的当前位置提升 8 个字节。 从当前流中读取 4 字节浮点值,并使流的当前位置提升 4 个字节。 从当前流中读取 8 字节浮点值,并使流的当前位置提升 8 个字节。 从当前流中读取下一个字符,并根据所使用的 Encoding 和从流中读取的特定字符,提升流的当前位置。 从当前流中读取一个字符串。字符串有长度前缀,一次 7 位地被编码为整数。 从当前流中读取十进制数值,并将该流的当前位置提升十六个字节。 以压缩格式读取16位整数 以压缩格式读取32位整数 以压缩格式读取64位整数 以7位压缩格式写入16位整数,小于7位用1个字节,小于14位用2个字节。 由每次写入的一个字节的第一位标记后面的字节是否还是当前数据,所以每个字节实际可利用存储空间只有后7位。 数值 实际写入字节数 以7位压缩格式写入32位整数,小于7位用1个字节,小于14位用2个字节。 由每次写入的一个字节的第一位标记后面的字节是否还是当前数据,所以每个字节实际可利用存储空间只有后7位。 数值 实际写入字节数 以7位压缩格式写入64位整数,小于7位用1个字节,小于14位用2个字节。 由每次写入的一个字节的第一位标记后面的字节是否还是当前数据,所以每个字节实际可利用存储空间只有后7位。 数值 实际写入字节数 列表数据编码 初始化 写入 读取 常用类型编码 初始化 写入 写入字节数组,自动计算长度 缓冲区 数量 读取 从当前流中将 count 个字节读入字节数组,如果count小于0,则先读取字节数组长度。 要读取的字节数。 从当前流中读取 count 个字符,以字符数组的形式返回数据,并根据所使用的 Encoding 和从流中读取的特定字符,提升当前位置。 要读取的字符数。 二进制名值对 初始化 写入一个对象 目标对象 类型 尝试读取指定类型对象 写入名值对 读取原始名值对 读取原始名值对 数据流 编码 从原始名值对读取数据 获取成员 内部对象处理器。对于其它处理器无法支持的类型,一律由该处理器解决 实例化 写入对象 目标对象 类型 尝试读取指定类型对象 字段大小特性。 可以通过Size指定字符串或数组的固有大小,为0表示自动计算;也可以通过指定参考字段ReferenceName,然后从其中获取大小。 支持_Header._Questions形式的多层次引用字段 大小。使用时,作为偏移量;0表示自动计算大小 参考大小字段名 通过Size指定字符串或数组的固有大小,为0表示自动计算 指定参考字段ReferenceName,然后从其中获取大小 指定参考字段ReferenceName,然后从其中获取大小 在参考字段值基础上的增量,可以是正数负数 找到所引用的参考字段 目标对象 目标对象的成员 数值 设置目标对象的引用大小值 目标对象 获取目标对象的引用大小值 目标对象 二进制序列化接口 编码整数 小端字节序。默认false大端 使用指定大小的FieldSizeAttribute特性,默认false 要忽略的成员 处理器列表 写入字节 将字节数组部分写入当前流,不写入数组长度。 包含要写入的数据的字节数组。 buffer 中开始写入的起始点。 要写入的字节数。 写入大小 要写入的大小值 返回特性指定的固定长度,如果没有则返回-1 读取字节 从当前流中将 count 个字节读入字节数组 要读取的字节数。 读取大小 二进制读写处理器接口 二进制读写处理器基类 序列化访问器。接口实现者可以在这里完全自定义序列化行为 从数据流中读取消息 数据流 上下文 是否成功 把消息写入到数据流中 数据流 上下文 是否成功 访问器助手 支持访问器的对象转数据包 访问器 上下文 通过访问器读取 上下文 通过访问器转换数据包为实体对象 序列化接口 数据流 主对象 成员 文本编码 序列化属性而不是字段。默认true 写入一个对象 目标对象 类型 读取指定类型对象 读取指定类型对象 尝试读取指定类型对象 日志提供者 序列化处理器接口 宿主读写器 优先级 写入一个对象 目标对象 类型 尝试读取指定类型对象 序列化接口 数据流。默认实例化一个内存数据流 主对象 成员 字符串编码,默认Default 序列化属性而不是字段。默认true 实例化 获取流里面的数据 日志提供者 输出日志 读写处理器基类 宿主读写器 优先级 写入一个对象 目标对象 类型 尝试读取指定类型对象 输出日志 成员序列化访问器。接口实现者可以在这里完全自定义序列化行为 从数据流中读取消息 序列化 成员 是否成功 把消息写入到数据流中 序列化 成员 Json编码解码器 对象转Json Json转对象 Json编码解码器 对象转Json Json转对象 IJson序列化接口 是否缩进 处理器列表 写入字符串 写入 读取 读取字节 IJson读写处理器接口 获取对象的Json字符串表示形式。 返回null表示不支持 IJson读写处理器基类 获取对象的Json字符串表示形式。 返回null表示不支持 写入一个对象 目标对象 类型 是否处理成功 Json序列化接口 写入对象,得到Json字符串 是否缩进。默认false 是否写控制。默认true 是否驼峰命名。默认false 从Json字符串中读取对象 类型转换 Json助手 默认实现 写入对象,得到Json字符串 是否缩进 写入对象,得到Json字符串 是否缩进。默认false 是否写控制。默认true 是否驼峰命名。默认false 从Json字符串中读取对象 从Json字符串中读取对象 格式化Json文本 Json类型对象转换实体类 Json序列化 是否缩进 处理器列表 实例化 添加处理器 添加处理器 获取处理器 写入一个对象 目标对象 类型 写入字符串 写入 读取指定类型对象 读取指定类型对象 尝试读取指定类型对象 读取 读取字节 列表数据编码 初始化 获取对象的Json字符串表示形式。 返回null表示不支持 写入 读取 复合对象处理器 要忽略的成员 实例化 获取对象的Json字符串表示形式。 返回null表示不支持 写入对象 目标对象 类型 尝试读取指定类型对象 获取成员 Json基础类型处理器 实例化 获取对象的Json字符串表示形式。 返回null表示不支持 尝试读取指定类型对象 Json分析器 标识符 左大括号 右大括号 左方括号 右方括号 冒号 逗号 字符串 数字 布尔真 布尔真 空值 实例化 解码 读取一个Token Json读取器 是否使用UTC时间 读取Json到指定类型 读取Json到指定类型 Json字典或列表转为具体类型对象 Json对象 模板类型 目标对象 转为泛型列表 目标对象 转为数组 目标对象 转为泛型字典 目标对象 字典转复杂对象,反射属性赋值 目标对象 创建时间 Json写入器 使用UTC时间。默认false 使用小写名称 使用驼峰命名 写入空值。默认true 实例化 对象序列化为Json字符串 是否缩进。默认false 是否写控制。默认true 是否驼峰命名。默认false 根据小写和驼峰格式化名称 序列化助手 获取序列化名称 二进制序列化接口 处理器列表 使用注释 写入一个对象 目标对象 名称 类型 获取Xml写入器 获取Xml读取器 二进制读写处理器接口 Xml读写处理器基类 Xml序列化 深度 处理器列表 使用特性 使用注释 当前名称 实例化 添加处理器 添加处理器 写入一个对象 目标对象 名称 类型 写入开头 写入结尾 获取Xml写入器 读取指定类型对象 读取指定类型对象 尝试读取指定类型对象 读取开始 读取结束 获取Xml读取器 获取字符串 Xml复合对象处理器 实例化 写入对象 目标对象 类型 尝试读取 获取成员 Xml基础类型处理器 实例化 写入一个对象 目标对象 类型 是否处理成功 尝试读取 列表数据编码 初始化 写入 读取 核心设置 是否启用全局调试。默认启用 日志等级,只输出大于等于该级别的日志,All/Debug/Info/Warn/Error/Fatal,默认Info 文件日志目录 网络日志。本地子网日志广播255.255.255.255:514 日志文件格式 临时目录 插件目录 插件服务器。将从该网页上根据关键字分析链接并下载插件 加载完成后 获取插件目录 轻量级线程池。无等待和调度逻辑,直接创建线程竞争处理器资源 初始化线程池 带异常处理的线程池任务调度,不允许异常抛出,以免造成应用程序退出 带异常处理的线程池任务调度,不允许异常抛出,以免造成应用程序退出 静态实例 内部池 实例化 创建实例 把委托放入线程池执行 在线程池中异步执行任务 在线程池中异步执行任务 在线程池中异步执行任务 线程任务项 编号 线程 主机线程池 活跃 实例化 销毁 已重载。 执行委托 定时器调度器 创建指定名称的调度器 默认调度器 当前调度器 名称 定时器个数 最大耗时。超过时报警告日志,默认500ms 把定时器加入队列 从队列删除定时器 唤醒处理 调度主程序 检查定时器是否到期 处理每一个定时器 已重载。 是否开启调试,输出更多信息 不可重入的定时器。 为了避免系统的Timer可重入的问题,差别在于本地调用完成后才开始计算时间间隔。这实际上也是经常用到的。 因为挂载在静态列表上,必须从外部主动调用才能销毁定时器。 该定时器不能放入太多任务,否则适得其反! TimerX必须维持对象,否则很容易被GC回收。 所属调度器 获取/设置 回调 获取/设置 用户数据 获取/设置 下一次调用时间 获取/设置 调用次数 获取/设置 间隔周期。毫秒,设为0或-1则只调用一次 获取/设置 异步执行任务。默认false 获取/设置 绝对精确时间执行。默认false 调用中 平均耗时。毫秒 判断任务是否执行的委托。一般跟异步配合使用,避免频繁从线程池借出线程 当前定时器 实例化一个不可重入的定时器 委托 用户数据 多久之后开始。毫秒 间隔周期。毫秒 调度器 实例化一个绝对定时器 委托 用户数据 绝对开始时间 间隔周期。毫秒 调度器 销毁定时器 是否已设置下一次时间 设置下一次运行时间 小于等于0表示马上调度 延迟执行一个委托 当前时间。定时读取系统时间,避免频繁读取系统时间造成性能瓶颈 已重载 超链接 名称 全名 超链接 原始超链接 标题 版本 时间 原始Html 分析HTML中的链接 Html文本 基础Url,用于生成超链接的完整Url 用于基础过滤的过滤器 分解文件 已重载。 页面压缩模块 初始化模块,准备拦截请求。 网页压缩文件 是否可压缩文件 检查请求头,确认客户端是否支持压缩编码 添加压缩编码到响应头 全局错误处理模块 初始化模块,准备拦截请求。 是否需要处理 错误处理方法 页面执行时间模块 初始化模块,准备拦截请求。 初始化 执行时间字符串 输出运行时间 输出 OAuth 2.0 客户端 名称 验证服务器地址 令牌服务地址。可以不同于验证地址的内网直达地址 应用Key 安全码 验证地址 访问令牌地址 响应类型 验证服务器跳转回来子系统时的类型,默认code,此时还需要子系统服务端请求验证服务器换取AccessToken; 可选token,此时验证服务器直接返回AccessToken,子系统不需要再次请求。 作用域 授权码 访问令牌 刷新令牌 统一标识 过期时间 访问项 实例化 根据名称创建客户端 应用参数设置 应用参数设置 构建跳转验证地址 验证完成后调整的目标地址 用户状态数据 相对地址的基地址 根据授权码获取访问令牌 OpenID地址 根据授权码获取访问令牌 用户信息地址 用户ID 用户名 昵称 头像 获取用户信息 填充用户,登录成功并获取用户信息之后 注销地址 注销 完成后调整的目标地址 用户状态数据 相对地址的基地址 替换地址模版参数 获取名值字典 最后一次请求的响应内容 创建客户端 路径 从响应数据中获取信息 日志 写日志 配置 调试开关。默认true 应用地址。域名和端口,应用系统经过反向代理重定向时指定外部地址 配置项 已加载 获取 获取或添加 开放验证服务器配置项 服务地址 验证服务地址 令牌服务地址。可以不同于验证地址的内网直达地址 应用标识 密钥 授权范围 单点登录服务端 缓存 令牌提供者 令牌有效期。默认24小时 实例 验证用户身份 子系统需要验证访问者身份时,引导用户跳转到这里。 用户登录完成后,得到一个独一无二的code,并跳转回去子系统。 应用标识 回调地址 响应类型。默认code 授权域 用户状态数据 根据验证结果获取跳转回子系统的Url 根据Code获取令牌 解码令牌 日志 写日志 身份验证提供者 实例化 从响应数据中获取信息 身份验证提供者 实例化 从响应数据中获取信息 创建客户端 路径 身份验证提供者 实例化 从响应数据中获取信息 淘宝身份验证提供者 实例化 从响应数据中获取信息 身份验证提供者 实例化 从响应数据中获取信息 插件助手 加载插件 提供下载地址的多个目标页面 令牌提供者 密钥。签发方用私钥,验证方用公钥 读取密钥 文件 是否生成 编码用户和有效期得到令牌 用户 有效期 令牌解码得到用户和有效期 令牌 有效期 扩展的Web客户端 Cookie容器 可接受类型 可接受语言 引用页面 超时,默认15000毫秒 自动解压缩模式。 User-Agent 标头,指定有关客户端代理的信息 编码。网络时代,绝大部分使用utf8编码 保持连接 代理服务器地址 网页代理 实例化 初始化常用的东西 是否模拟ie 是否压缩 销毁 请求 响应 创建客户端会话 发送请求,获取响应 下载数据 下载字符串 下载文件 异步上传数据 异步上传表单 异步上传字符串 异步上传Json对象 获取指定地址的Html,自动处理文本编码 获取指定地址的Html,分析所有超链接 分析指定页面指定名称的链接,并下载到目标目录,返回目标文件 根据版本或时间降序排序选择 指定页面 页面上指定名称的链接 要下载到的目标目录 返回已下载的文件,无效时返回空 分析指定页面指定名称的链接,并下载到目标目录,解压Zip后返回目标文件 提供下载地址的多个目标页面 页面上指定名称的链接 要下载到的目标目录 是否覆盖目标同名文件 根据Http响应设置本地Cookie 从本地获取Cookie并设置到Http请求头 默认连接池 访问地址获取字符串 设置是否允许不安全头部 微软WebClient默认要求严格的Http头部,否则报错 日志 提供网页下载支持,在服务端把一个数据流作为附件传给浏览器,带有断点续传和限速的功能 数据流 文件名 内容类型 附件配置模式,是在浏览器直接打开,还是提示另存为 速度,每秒传输字节数,根据包大小,每响应一个包后睡眠指定毫秒数,0表示不限制 是否启用浏览器缓存 默认禁用 浏览器最大缓存时间 默认30天。通过Cache-Control头控制max-age,直接使用浏览器缓存,不会发出Http请求,对F5无效 文件数据最后修改时间,浏览器缓存时用 附件配置模式 不设置 内联模式,在浏览器直接打开 附件模式,提示另存为 分析模式 构造函数 构造函数 构造函数 检查浏览器缓存是否依然有效,如果有效则跳过Render 输出数据流 语音识别 系统名称。用于引导前缀 最后一次进入引导前缀的时间。 是否可用 销毁 当前实例 获取已注册的所有键值 注册要语音识别的关键字到委托 语音接口 语音识别事件 初始化 设置识别短语 识别事件参数 获取识别器分配的值,此值表示与给定输入匹配的可能性 获取语音识别器从识别的输入生成的规范化文本 实例化 支持Xml序列化的泛型字典类 读取Xml Xml读取器 写入Xml Xml写入器 Xml配置文件基类 标准用法:TConfig.Current 配置实体类通过特性指定配置文件路径以及自动更新时间。 Current将加载配置文件,如果文件不存在或者加载失败,将实例化一个对象返回。 考虑到自动刷新,不提供LoadFile和SaveFile等方法,可通过扩展方法ToXmlFileEntity和ToXmlFile实现。 用户也可以通过配置实体类的静态构造函数修改基类的来动态配置加载信息。 当前实例。通过置空可以使其重新加载。 一些设置。派生类可以在自己的静态构造函数中指定 是否调试 配置文件路径 重新加载时间。单位:毫秒 没有配置文件时是否保存新配置。默认true 配置文件 最后写入时间 过期时间。如果在这个时间之后再次访问,将检查文件修改时间 是否已更新。通过文件写入时间判断 设置过期重新加载配置的时间 是否新的配置文件 销毁 加载指定配置文件 从配置文件中读取完成后触发 保存到配置文件中去 保存到配置文件中去 异步保存 新创建配置文件时执行 Xml配置文件特性 配置文件名 重新加载时间。单位:毫秒 指定配置文件名 指定配置文件名和重新加载时间(毫秒) Xml辅助类 序列化为Xml字符串 要序列化为Xml的对象 编码 是否附加注释,附加成员的Description和DisplayName注释 是否使用特性输出 Xml字符串 序列化为Xml数据流 要序列化为Xml的对象 目标数据流 编码 是否附加注释,附加成员的Description和DisplayName注释 是否使用特性输出 序列化为Xml文件 要序列化为Xml的对象 目标Xml文件 编码 是否附加注释,附加成员的Description和DisplayName注释 Xml字符串 字符串转为Xml实体对象 实体类型 Xml字符串 Xml实体对象 字符串转为Xml实体对象 Xml字符串 实体类型 Xml实体对象 数据流转为Xml实体对象 实体类型 数据流 编码 Xml实体对象 数据流转为Xml实体对象 数据流 实体类型 编码 Xml实体对象 Xml文件转为Xml实体对象 实体类型 Xml文件 编码 Xml实体对象 简单Xml转为字符串字典 字符串字典转为Xml 高德地图 参考地址 http://lbs.amap.com/api/webservice/guide/api/georegeo/#geo 高德地图 远程调用 目标Url 结果字段 查询地址的经纬度坐标 查询地址获取坐标 地址 城市 是否格式化地址。高德地图默认已经格式化地址 根据坐标获取地址 http://lbs.amap.com/api/webservice/guide/api/georegeo/#regeo 根据坐标获取地址 计算距离和驾车时间 http://lbs.amap.com/api/webservice/guide/api/direction type: 0:直线距离 1:驾车导航距离(仅支持国内坐标)。 必须指出,当为1时会考虑路况,故在不同时间请求返回结果可能不同。 此策略和driving接口的 strategy = 4策略一致 2:公交规划距离(仅支持同城坐标) 3:步行规划距离(仅支持5km之间的距离) distance 路径距离,单位:米 duration 预计行驶时间,单位:秒 路径计算的方式和方法 行政区划 http://lbs.amap.com/api/webservice/guide/api/district 查询关键字 设置显示下级行政区级数 按照指定行政区划进行过滤,填入后则只返回该省/直辖市信息 是否无效Key。可能禁用或超出限制 百度地图 参考手册 http://lbsyun.baidu.com/index.php?title=webapi/guide/webservice-geocoding 高德地图 远程调用 目标Url 结果字段 查询地址的经纬度坐标 查询地址获取坐标 地址 城市 是否格式化地址 根据坐标获取地址 根据坐标获取地址 计算距离和驾车时间 http://lbsyun.baidu.com/index.php?title=webapi/route-matrix-api-v2 路径计算的方式和方法 行政区划区域检索 http://lbsyun.baidu.com/index.php?title=webapi/guide/webservice-placeapi 是否无效Key。可能禁用或超出限制 驾车距离和时间 距离。单位千米 路线耗时。单位秒 地图提供者接口 应用密钥 异步获取字符串 查询地址的经纬度坐标 查询地址获取坐标 地址 城市 是否格式化地址 根据坐标获取地址 根据坐标获取地址 计算距离和驾车时间 路径计算的方式和方法 日志 地图提供者 应用密钥。多个key逗号分隔 应用密码参数名 最后密钥 坐标系 最后网址 最后响应 最后结果 收到异常响应时是否抛出异常 销毁 异步获取字符串 远程调用 目标Url 结果字段 申请密钥 移除不可用密钥 是否无效Key。可能禁用或超出限制 日志 写日志 集合扩展 集合转为数组 集合转为数组 集合转为数组 目标匿名参数对象转为字典 合并字典参数 字典 目标对象 是否覆盖同名参数 排除项 转为可空字典 从队列里面获取指定个数元素 消费集合 元素个数 从消费集合里面获取指定个数元素 消费集合 元素个数 工具类 采用静态架构,允许外部重载工具类的各种实现。 所有类型转换均支持默认值,默认值为该default(T),在转换失败时返回默认值。 类型转换提供者 重载默认提供者并赋值给可改变所有类型转换的行为 转为整数,转换失败时返回默认值。支持字符串、全角、字节数组(小端)、时间(Unix秒) Int16/UInt32/Int64等,可以先转为最常用的Int32后再二次处理 待转换对象 默认值。待转换对象无效时使用 转为长整数,转换失败时返回默认值。支持字符串、全角、字节数组(小端)、时间(Unix毫秒) 待转换对象 默认值。待转换对象无效时使用 转为浮点数,转换失败时返回默认值。支持字符串、全角、字节数组(小端) Single可以先转为最常用的Double后再二次处理 待转换对象 默认值。待转换对象无效时使用 转为布尔型,转换失败时返回默认值。支持大小写True/False、0和非零 待转换对象 默认值。待转换对象无效时使用 转为时间日期,转换失败时返回最小时间。支持字符串、整数(Unix秒) 待转换对象 转为时间日期,转换失败时返回默认值 不是常量无法做默认值 待转换对象 默认值。待转换对象无效时使用 时间日期转为yyyy-MM-dd HH:mm:ss完整字符串 最常用的时间日期格式,可以无视各平台以及系统自定义的时间格式 待转换对象 时间日期转为yyyy-MM-dd HH:mm:ss完整字符串,支持指定最小时间的字符串 最常用的时间日期格式,可以无视各平台以及系统自定义的时间格式 待转换对象 字符串空值时(DateTime.MinValue)显示的字符串,null表示原样显示最小时间,String.Empty表示不显示 时间日期转为指定格式字符串 待转换对象 格式化字符串 字符串空值时显示的字符串,null表示原样显示最小时间,String.Empty表示不显示 获取内部真实异常 获取异常消息 异常 默认转换 转为整数,转换失败时返回默认值。支持字符串、全角、字节数组(小端)、时间(Unix秒) 待转换对象 默认值。待转换对象无效时使用 转为长整数。支持字符串、全角、字节数组(小端)、时间(Unix毫秒) 待转换对象 默认值。待转换对象无效时使用 转为浮点数 待转换对象 默认值。待转换对象无效时使用 转为布尔型。支持大小写True/False、0和非零 待转换对象 默认值。待转换对象无效时使用 转为时间日期,转换失败时返回最小时间。支持字符串、整数(Unix秒) 待转换对象 默认值。待转换对象无效时使用 全角为半角 全角半角的关系是相差0xFEE0 时间日期转为yyyy-MM-dd HH:mm:ss完整字符串 待转换对象 字符串空值时显示的字符串,null表示原样显示最小时间,String.Empty表示不显示 时间日期转为指定格式字符串 待转换对象 格式化字符串 字符串空值时显示的字符串,null表示原样显示最小时间,String.Empty表示不显示 获取内部真实异常 获取异常消息 异常 字节单位字符串 数值 格式化字符串 数据位助手 设置数据位 数值 设置数据位 数值 设置数据位 数值 获取数据位 数值 获取数据位 数值 获取数据位 数值 并行字典扩展 从并行字典中删除 网络结点扩展 枚举类型助手类 枚举变量是否包含指定标识 枚举变量 要判断的标识 设置标识位 数值 获取枚举字段的注释 数值 获取枚举类型的所有字段注释 获取枚举类型的所有字段注释 扩展List,支持遍历中修改元素 线程安全,搜索并返回第一个,支持遍历中修改元素 实体列表 条件 线程安全,搜索并返回第一个,支持遍历中修改元素 实体列表 条件 字符串助手类 忽略大小写的字符串相等比较,判断是否以任意一个待比较字符串相等 字符串 待比较字符串数组 忽略大小写的字符串开始比较,判断是否以任意一个待比较字符串开始 字符串 待比较字符串数组 忽略大小写的字符串结束比较,判断是否以任意一个待比较字符串结束 字符串 待比较字符串数组 指示指定的字符串是 null 还是 String.Empty 字符串 字符串 是否空或者空白字符串 字符串 拆分字符串,过滤空格,无效时返回空数组 字符串 分组分隔符,默认逗号分号 拆分字符串成为整型数组,默认逗号分号分隔,无效时返回空数组 过滤空格、过滤无效、不过滤重复 字符串 分组分隔符,默认逗号分号 拆分字符串成为不区分大小写的可空名值字典。逗号分号分组,等号分隔 字符串 名值分隔符,默认等于号 分组分隔符,默认逗号分号 拆分字符串成为不区分大小写的可空名值字典。逗号分组,等号分隔 字符串 名值分隔符,默认等于号 分组分隔符,默认分号 去掉括号 在.netCore需要区分该部分内容 把一个列表组合成为一个字符串,默认逗号分隔 组合分隔符,默认逗号 把一个列表组合成为一个字符串,默认逗号分隔 组合分隔符,默认逗号 把对象转为字符串的委托 把一个列表组合成为一个字符串,默认逗号分隔 组合分隔符,默认逗号 把对象转为字符串的委托 追加分隔符字符串,忽略开头,常用于拼接 字符串构造者 分隔符 字符串转数组 字符串 编码,默认utf-8无BOM 格式化字符串。特别支持无格式化字符串的时间参数 格式字符串 参数 确保字符串以指定的另一字符串开始,不区分大小写 字符串 确保字符串以指定的另一字符串结束,不区分大小写 字符串 从当前字符串开头移除另一字符串,不区分大小写,循环多次匹配前缀 当前字符串 另一字符串 从当前字符串结尾移除另一字符串,不区分大小写,循环多次匹配后缀 当前字符串 另一字符串 从字符串中检索子字符串,在指定头部字符串之后,指定尾部字符串之前 常用于截取xml某一个元素等操作 目标字符串 头部字符串,在它之后 尾部字符串,在它之前 搜索的开始位置 位置数组,两个元素分别记录头尾位置 根据最大长度截取字符串,并允许以指定空白填充末尾 字符串 截取后字符串的最大允许长度,包含后面填充 需要填充在后面的字符串,比如几个圆点 从当前字符串开头移除另一字符串以及之前的部分 当前字符串 另一字符串 从当前字符串结尾移除另一字符串以及之后的部分 当前字符串 另一字符串 编辑距离搜索,从词组中找到最接近关键字的若干匹配项 算法代码由@Aimeast 独立完成。http://www.cnblogs.com/Aimeast/archive/2011/09/05/2167844.html 关键字 词组 编辑距离 又称Levenshtein距离(也叫做Edit Distance),是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。 许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。 算法代码由@Aimeast 独立完成。http://www.cnblogs.com/Aimeast/archive/2011/09/05/2167844.html 最长公共子序列搜索,从词组中找到最接近关键字的若干匹配项 算法代码由@Aimeast 独立完成。http://www.cnblogs.com/Aimeast/archive/2011/09/05/2167844.html 最长公共子序列问题是寻找两个或多个已知数列最长的子序列。 一个数列 S,如果分别是两个或多个已知数列的子序列,且是所有符合此条件序列中最长的,则 S 称为已知序列的最长公共子序列。 The longest common subsequence (LCS) problem is to find the longest subsequence common to all sequences in a set of sequences (often just two). Note that subsequence is different from a substring, see substring vs. subsequence. It is a classic computer science problem, the basis of diff (a file comparison program that outputs the differences between two files), and has applications in bioinformatics. 算法代码由@Aimeast 独立完成。http://www.cnblogs.com/Aimeast/archive/2011/09/05/2167844.html 多个关键字。长度必须大于0,必须按照字符串长度升序排列。 根据列表项成员计算距离 在列表项中进行模糊搜索 模糊匹配 模糊匹配 模糊匹配 列表项 关键字 匹配字符串选择 获取个数 权重阀值 调用语音引擎说出指定话 异步调用语音引擎说出指定话。可能导致后来的调用打断前面的语音 启用语音提示 语音提示操作 停止所有语音播报 以隐藏窗口执行命令行 文件名 命令参数 等待毫秒数 进程输出内容。默认为空时输出到日志 进程退出时执行 进程退出代码 IO工具类 压缩数据流 输入流 输出流。如果不指定,则内部实例化一个内存流 返回输出流,注意此时指针位于末端 解压缩数据流 输入流 输出流。如果不指定,则内部实例化一个内存流 返回输出流,注意此时指针位于末端 压缩字节数组 字节数组 解压缩字节数组 字节数组 压缩数据流 输入流 输出流。如果不指定,则内部实例化一个内存流 返回输出流,注意此时指针位于末端 解压缩数据流 输入流 输出流。如果不指定,则内部实例化一个内存流 返回输出流,注意此时指针位于末端 复制数据流 源数据流 目的数据流 缓冲区大小,也就是每次复制的大小 最大复制字节数 返回复制的总字节数 把一个数据流写入到另一个数据流 目的数据流 源数据流 缓冲区大小,也就是每次复制的大小 最大复制字节数 把一个字节数组写入到一个数据流 目的数据流 源数据流 写入字节数组,先写入压缩整数表示的长度 读取字节数组,先读取压缩整数表示的长度 写入Unix格式时间,1970年以来秒数,绝对时间,非UTC 读取Unix格式时间,1970年以来秒数,绝对时间,非UTC 复制数组 源数组 起始位置 复制字节数 返回复制的总字节数 向字节数组写入一片数据 目标数组 目标偏移 源数组 源数组偏移 数量 返回实际写入的字节个数 合并两个数组 源数组 目标数组 起始位置 字节数 数据流转为字节数组 针对MemoryStream进行优化。内存流的Read实现是一个个字节复制,而ToArray是调用内部内存复制方法 如果要读完数据,又不支持定位,则采用内存流搬运 如果指定长度超过数据流长度,就让其报错,因为那是调用者所期望的值 数据流 长度,0表示读到结束 数据流转为字节数组,从0开始,无视数据流的当前位置 数据流 从数据流中读取字节数组,直到遇到指定字节数组 数据流 字节数组 字节数组中的偏移 字节数组中的查找长度 未找到时返回空,0位置范围大小为0的字节数组 从数据流中读取字节数组,直到遇到指定字节数组 数据流 从数据流中读取一行,直到遇到换行 数据流 未找到返回null,0位置返回String.Empty 流转换为字符串 目标流 编码格式 字节数组转换为字符串 字节数组 编码格式 字节数组中的偏移 字节数组中的查找长度 从字节数据指定位置读取一个无符号16位整数 偏移 是否小端字节序 从字节数据指定位置读取一个无符号32位整数 偏移 是否小端字节序 从字节数据指定位置读取一个无符号64位整数 偏移 是否小端字节序 向字节数组的指定位置写入一个无符号16位整数 数字 偏移 是否小端字节序 向字节数组的指定位置写入一个无符号32位整数 数字 偏移 是否小端字节序 向字节数组的指定位置写入一个无符号64位整数 数字 偏移 是否小端字节序 整数转为字节数组,注意大小端字节序 整数转为字节数组,注意大小端字节序 整数转为字节数组,注意大小端字节序 整数转为字节数组,注意大小端字节序 整数转为字节数组,注意大小端字节序 整数转为字节数组,注意大小端字节序 以压缩格式读取32位整数 数据流 以压缩格式读取32位整数 数据流 尝试读取压缩编码整数 以7位压缩格式写入32位整数,小于7位用1个字节,小于14位用2个字节。 由每次写入的一个字节的第一位标记后面的字节是否还是当前数据,所以每个字节实际可利用存储空间只有后7位。 数据流 数值 实际写入字节数 获取压缩编码整数 在数据流中查找字节数组的位置,流指针会移动到结尾 数据流 字节数组 字节数组中的偏移 字节数组中的查找长度 在字节数组中查找另一个字节数组的位置,不存在则返回-1 字节数组 另一个字节数组 偏移 查找长度 在字节数组中查找另一个字节数组的位置,不存在则返回-1 字节数组 源数组起始位置 查找长度 另一个字节数组 偏移 查找长度 比较两个字节数组大小。相等返回0,不等则返回不等的位置,如果位置为0,则返回1。 缓冲区 比较两个字节数组大小。相等返回0,不等则返回不等的位置,如果位置为0,则返回1。 数量 缓冲区 偏移 字节数组分割 一个数据流是否以另一个数组开头。如果成功,指针移到目标之后,否则保持指针位置不变。 缓冲区 一个数据流是否以另一个数组结尾。如果成功,指针移到目标之后,否则保持指针位置不变。 缓冲区 一个数组是否以另一个数组开头 缓冲区 一个数组是否以另一个数组结尾 缓冲区 倒序、更换字节序 字节数组 把字节数组编码为十六进制字符串 字节数组 偏移 数量。超过实际数量时,使用实际数量 把字节数组编码为十六进制字符串,带有分隔符和分组功能 字节数组 分隔符 分组大小,为0时对每个字节应用分隔符,否则对每个分组使用 最大显示多少个字节。默认-1显示全部 解密 Hex编码的字符串 起始位置 长度 字节数组转为Base64编码 是否换行显示 字节数组转为Url改进型Base64编码 Base64字符串转为字节数组 路径操作帮助 基础目录。GetFullPath依赖于此,默认为当前应用程序域基础目录 获取文件或目录的全路径,过滤相对目录 不确保目录后面一定有分隔符,是否有分隔符由原始路径末尾决定 文件或目录 获取文件或目录基于应用程序域基目录的全路径,过滤相对目录 不确保目录后面一定有分隔符,是否有分隔符由原始路径末尾决定 文件或目录 获取文件或目录基于当前目录的全路径,过滤相对目录 不确保目录后面一定有分隔符,是否有分隔符由原始路径末尾决定 文件或目录 确保目录存在,若不存在则创建 斜杠结尾的路径一定是目录,无视第二参数; 默认是文件,这样子只需要确保上一层目录存在即可,否则如果把文件当成了目录,目录的创建会导致文件无法创建。 文件路径或目录路径,斜杠结尾的路径一定是目录,无视第二参数 该路径是否是否文件路径。文件路径需要取目录部分 合并多段路径 文件路径作为文件信息 从文件中读取数据 把数据写入文件指定位置 读取所有文本,自动检测编码 性能较File.ReadAllText略慢,可通过提前检测BOM编码来优化 把文本写入文件,自动检测编码 复制到目标文件,目标文件必须已存在,且源文件较新 源文件 目标文件 打开并读取 返回类型 文件信息 要对文件流操作的委托 打开并写入 返回类型 文件信息 要对文件流操作的委托 解压缩 是否覆盖目标同名文件 压缩文件 路径作为目录信息 获取目录内所有符合条件的文件,支持多文件扩展匹配 目录 文件扩展列表。比如*.exe;*.dll;*.config 是否包含所有子孙目录文件 复制目录中的文件 源目录 目标目录 文件扩展列表。比如*.exe;*.dll;*.config 是否包含所有子孙目录文件 复制每一个文件之前的回调 对比源目录和目标目录,复制双方都存在且源目录较新的文件 源目录 目标目录 文件扩展列表。比如*.exe;*.dll;*.config 是否包含所有子孙目录文件 复制每一个文件之前的回调 从多个目标目录复制较新文件到当前目录 当前目录 多个目标目录 文件扩展列表。比如*.exe;*.dll;*.config 是否包含所有子孙目录文件 压缩 模型扩展 获取指定类型的服务对象 对象容器助手。扩展方法专用 注册类型和名称 接口类型 实现类型 对象容器 标识 优先级 注册类型指定名称的实例 接口类型 对象容器 实例 标识 优先级 解析类型指定名称的实例 接口类型 对象容器 标识 解析类型指定名称的实例 接口类型 对象容器 标识 遍历所有程序集的所有类型,自动注册实现了指定接口或基类的类型。如果没有注册任何实现,则默认注册第一个排除类型 自动注册一般用于单实例功能扩展型接口 接口类型 要排除的类型,一般是内部默认实现 对象容器 解析接口指定名称的实现类型 接口类型 对象容器 标识 网络工具类 设置超时检测时间和检测间隔 要设置的Socket对象 是否启用Keep-Alive 多长时间后开始第一次探测(单位:毫秒) 探测时间间隔(单位:毫秒) 分析地址,根据IP或者域名得到IP地址,缓存60秒,异步更新 分析网络终结点 地址,可以不带端口 地址不带端口时指定的默认端口 针对IPv4和IPv6获取合适的Any地址 除了Any地址以为,其它地址不具备等效性 是否Any地址,同时处理IPv4和IPv6 是否Any结点 是否IPv4地址 是否本地地址 获取相对于指定远程地址的本地地址 获取相对于指定远程地址的本地地址 指定地址的指定端口是否已被使用,似乎没办法判断IPv6地址 检查该协议的地址端口是否已经呗使用 获取活动的接口信息 获取可用的DHCP地址 获取可用的DNS地址 获取可用的网关地址 获取可用的IP地址 获取本机可用IP地址,缓存60秒,异步更新 获取可用的多播地址 获取以太网MAC地址 获取本地第一个IPv4地址 获取本地第一个IPv6地址 唤醒指定MAC地址的计算机 根据IP地址获取MAC地址 获取IP地址的物理地址位置 根据字符串形式IP地址转为物理地址 IP地址提供者接口 获取IP地址的物理地址位置 根据本地网络标识创建客户端 根据远程网络标识创建客户端 特性辅助类 获取自定义属性,带有缓存功能,避免因.Net内部GetCustomAttributes没有缓存而带来的损耗 获取成员绑定的显示名,优先DisplayName,然后Description 获取成员绑定的显示名,优先DisplayName,然后Description 获取自定义属性的值。可用于ReflectionOnly加载的程序集 获取自定义属性的值。可用于ReflectionOnly加载的程序集 目标对象 是否递归 安全算法 MD5散列 MD5散列 字符串编码,默认Default MD5散列 字符串编码,默认Default Crc散列 Crc16散列 SHA128 SHA256 SHA384 SHA512 对称加密算法扩展 注意:CryptoStream会把 outstream 数据流关闭 对称加密算法扩展 算法 数据 密码 模式。.Net默认CBC,Java默认ECB 填充算法。默认PKCS7,等同Java的PKCS5 对称解密算法扩展 注意:CryptoStream会把 instream 数据流关闭 对称解密算法扩展 算法 数据 密码 模式。.Net默认CBC,Java默认ECB 填充算法。默认PKCS7,等同Java的PKCS5 RC4对称加密算法 控件助手 执行无参委托 执行单一参数无返回值的委托 执行二参数无返回值的委托 附加文本到文本控件末尾。主要解决非UI线程以及滚动控件等问题 控件 消息 最大行数。超过该行数讲清空控件 滚动控件的滚动条 指定控件 是否底端,或者顶端 处理回车,移到行首 设置默认样式,包括字体、前景色、背景色 控件 字体大小 设置字体大小 采用默认着色方案进行着色 文本控件 开始位置 改变C++类名方法名颜色 着色文本控件的内容 文本控件 正则表达式 开始位置 颜色数组 着色文本控件的内容 文本控件 正则表达式 开始位置 颜色数组 当前Dpi 修正ListView的Dpi 修正窗体的Dpi ================================================ FILE: DLL/NewLife.RocketMQ.xml ================================================ NewLife.RocketMQ 代理客户端 服务器地址 实例化代理客户端 启动 注销客户端 心跳 获取运行时信息 集群客户端 维护到一个集群的客户端连接,内部采用负载均衡调度算法。 编号 名称 超时。默认3000ms 服务器地址集合 配置 实例化 销毁 开始 确保创建连接 发送命令 发送指定类型的命令 建立命令时,处理头部 收到命令时 收到命令 日志 写日志 权限 写入 读取 代理信息 名称 地址集合 权限 读队列数 写队列数 主题同步标记 相等比较 计算哈希 带权重负载均衡算法 权重集合 最小权重 状态值 次数 实例化 根据权重选择,并返回该项是第几次选中 根据权重选择 消费者 数据 消费间隔。默认15_000ms 持久化消费偏移间隔。默认5_000ms 拉取的批大小。默认32 启动时间 从最后偏移开始消费。默认true 消费委托 销毁 启动 从指定队列拉取消息 查询指定队列的偏移量 查询“队列”最大偏移量,不是消费提交的最后偏移量 获取最小偏移量 根据时间戳查询偏移 更新队列的偏移 获取消费者下所有消费者 启动消费者时自动开始调度。默认true 开始调度 拉取到一批消息 当前所需要消费的队列。由均衡算法产生 相等比较 计算哈希 重新平衡消费队列 收到命令 业务基类 名称服务器地址 消费组 主题 本地IP地址 实例名 拉取名称服务器间隔。默认30_000ms Broker心跳间隔。默认30_000ms 单元名称 单元模式 是否可用 代理集合 名称服务器 获取名称服务器地址的http地址 访问令牌 访问密钥 阿里云MQ通道 客户端标识 实例化 销毁 友好字符串 开始 获取代理客户端 Broker客户端集合 收到命令 日志 写日志 连接名称服务器的客户端 Broker集合 代理改变时触发 实例化 启动 获取主题的路由信息,含登录验证 生产者 启动 发送消息 发布消息 创建主题 选择队列 命令 头部 主体 是否响应 从数据流中读取 读取Body作为Json返回 写入命令到数据流 命令转字节数组 创建响应 友好字符串 头部 请求/响应码 扩展字段 这个字段不通的请求/响应不一样,完全自定义。数据结构上是java的hashmap。 在Java的每个RemotingCammand中,其实都带有一个CommandCustomHeader的属性成员,可以认为他是一个强类型的extFields, 再最后传输的时候,这个CommandCustomHeader会被忽略,而传输前会把其中的所有字段全部都原封不动塞到extFields中,以作传输。 标识 第0位标识是这次通信是request还是response,0标识request, 1 标识response。 第1位标识是否是oneway请求,1标识oneway。应答方在处理oneway请求的时候,不会做出响应,请求方也无需等待应答方响应。 由于要支持多语言,所以这一字段可以给通信双方知道对方通信层锁使用的开发语言 这里必须是JAVA,不能是CSharp,甚至Java都不行 请求标识码。在Java版的通信层中,这个只是一个不断自增的整形,为了收到应答方响应的的时候找到对应的请求。 序列化类型 给通信层知道对方的版本号,响应方可以以此做兼容老版本等的特殊操作 附带的文本信息。常见的如存放一些broker/nameserver返回的一些异常信息,方便开发人员定位问题。 获取扩展字段。如果为空则创建 心跳数据 客户端编号 消费数据集 生产者数据集 生产者数据 组名 消费者数据 从哪里开始消费 消费类型 组名 消息模型。广播/集群 订阅数据集 单元模式 订阅者数据 主题 表达式类型 子字符串 标签集合 代码集合 过滤模式 过滤源 子版本 消息 主题 标签 标记 消息体 消息体。字符串格式 等待存储消息 延迟时间等级 友好字符串 获取属性 设置数据 消息扩展 队列编号 存储大小 CRC校验 队列偏移 提交日志偏移 系统标记 生产时间 生产主机 存储时间 存储主机 重新消费次数 准备事务偏移 属性 消息编号 友好字符串 从数据流中读取 读取所有消息 写入命令到数据流 消息队列 主题 代理名称 队列编号 相等比较 计算哈希 友好字符串 编码器 实例化编码器 编码 加入队列 解码 连接关闭时,清空粘包编码器 是否匹配响应 拉取信息请求头 消费组 主题 订阅表达式 挂起超时时间。默认20_000ms 子版本 队列 队列偏移 最大消息数 提交偏移 系统标记 获取属性字典 拉取状态 已发现 没有新的消息 没有匹配消息 偏移量非法 未知类型 拉取结果 状态 最小偏移 最大偏移 下一轮拉取偏移 消息 友好字符串 读取数据 查询结果 最后更新时间 消息列表 请求代码 发消息 收消息 查询消费偏移 用于向brokers查询所有的topic和它们的配置 获取代理配置 获取代理运行时信息,包括broker版本、磁盘容量、系统负载等 获取topic/队列偏移量的最大值 获取topic/队列偏移量的最小值 发送心跳 注销 当consumer客户端无法处理消息时,将这些消息发送回brokers,以便将来将这些消息重新发送给consumers 查询每个consumer group的存活成员 当broker得知一个consumer宕机时,它会通知其他工作的consumers尽快重新平衡 获取所有的消费的偏移量 获取延迟topic的偏移量 获取topic路由信息 获取群集信息 创建新的consumer group或更新现有的consumer group以更改属性 查询所有已知的consumer group配置 查询topic相关的统计信息 查询所有topic 要求broker从consumer客户端按给定的时间戳重置偏移量 update the config of name server get config from name server 批处理模式发送消息 响应码 响应异常 响应代码 实例化响应异常 发送消息请求头 生产组 主题 默认主题 默认主题队列数 队列编号 系统标记 生产时间。毫秒 标记 属性。Tags/Keys等 重新消费次数 单元模式 获取属性字典 发送状态 成功 刷盘超时 刷从机超时 从机不可用 发送结果 状态 消息编号 队列 队列偏移 事务编号 偏移消息编号 区域 读取结果 服务状态 刚刚建立 运行中 已经关闭 启动失败 ================================================ FILE: Doc/Changelog.md ================================================ # NewLife.RocketMQ 更新日志 2026 ## v3.0.2026.0304 (2026-03-04) ### 架构优化 * 重构 RocketMQ gRPC 协议为 SpanReader/SpanWriter 实现,提升性能 * 重构架构文档,优化编解码器实现 * 优化项目文件描述及标题信息 ### 文档完善 * 文档体系重构:新增架构与需求文档,优化 Readme * 新增架构设计文档和功能分析文档 ### 依赖升级 * 升级 NewLife.Core 依赖到最新版本 --- ## v3.0.2026.0228 (2026-02-28) ### 问题修复 * 修复逻辑缺陷并补充单元测试 * 新增客户端拉取超时机制,防止 RocketMQ 4.9.8 消费者线程卡死 --- ## v3.0.2026.0216 (2026-02-16) ### 重大更新:v3.0 云适配重构 * 云适配重构与功能全面单元测试覆盖 * 完善兼容性梳理与优化进展 ### 新增特性 * 新增 VIP 通道支持 * 新增批量消息确认机制 * 新增 5.x MsgId 支持 * 新增 gRPC Telemetry 遥测支持 * 完整实现**事务消息**功能(PR #108) - 增加 RocketMQ 事务消息发布与结束接口 - 完善事务消息实现并通过审查与安全扫描 * 完整实现**请求-应答模式** (Request-Reply) 功能(PR #107) - 完善 RocketMQ 5.0 特性支持 - 通过单元测试和文档验证 ### 文档完善 * 新增功能分析与架构设计文档 * Phase 6 文档总结:兼容性梳理与优化进展 ### 依赖升级 * 升级 NuGet 依赖包 --- ## v3.0.2026.0211 (2026-02-11) ### 依赖升级 * 升级 NewLife.Core 依赖到最新版本 ================================================ FILE: Doc/RequestReply_Guide.md ================================================ # RocketMQ Request-Reply 使用指南 ## 概述 从 RocketMQ 4.6.0 版本开始,引入了 Request-Reply 特性,该特性允许生产者在发送消息后同步或异步等待消费者消费完消息并返回响应消息,实现类似 RPC 调用的效果。 NewLife.RocketMQ 现已支持此特性,并兼容 RocketMQ 5.0+。 ## 主要特性 - **同步请求**:发送请求后阻塞等待响应 - **异步请求**:发送请求后异步等待响应 - **超时控制**:支持设置请求超时时间 - **自动关联**:自动管理请求和响应的关联 - **简单易用**:API 设计简洁,易于集成 ## 使用示例 ### 1. 生产者端 - 发送请求 #### 同步请求 ```csharp using NewLife.RocketMQ; // 创建生产者 var producer = new Producer { Topic = "request_topic", NameServerAddress = "127.0.0.1:9876", RequestTimeout = 3000 // 设置默认超时时间为3秒 }; producer.Start(); try { // 发送请求并同步等待响应 var requestBody = "这是一个请求消息"; var response = producer.Request(requestBody, timeout: 5000); Console.WriteLine($"收到响应: {response.BodyString}"); } finally { producer.Stop(); producer.Dispose(); } ``` #### 异步请求 ```csharp using NewLife.RocketMQ; // 创建生产者 var producer = new Producer { Topic = "request_topic", NameServerAddress = "127.0.0.1:9876", RequestTimeout = 3000 }; producer.Start(); try { // 异步发送请求并等待响应 var requestBody = "这是一个异步请求消息"; var response = await producer.RequestAsync(requestBody, timeout: 5000); Console.WriteLine($"收到响应: {response.BodyString}"); } finally { producer.Stop(); producer.Dispose(); } ``` ### 2. 消费者端 - 处理请求并发送回复 #### 同步处理 ```csharp using NewLife.RocketMQ; // 创建消费者 var consumer = new Consumer { Topic = "request_topic", Group = "request_consumer_group", NameServerAddress = "127.0.0.1:9876", FromLastOffset = false }; // 设置消息处理回调 consumer.OnConsume = (queue, messages) => { foreach (var message in messages) { Console.WriteLine($"收到请求: {message.BodyString}"); // 检查是否是请求消息 if (!String.IsNullOrEmpty(message.CorrelationId)) { // 处理业务逻辑 var result = ProcessRequest(message.BodyString); // 发送回复 consumer.SendReply(message, result); Console.WriteLine($"已发送回复: {result}"); } } return true; }; consumer.Start(); // 保持运行 Console.WriteLine("消费者已启动,按任意键退出..."); Console.ReadKey(); consumer.Stop(); consumer.Dispose(); string ProcessRequest(string request) { // 实现你的业务逻辑 return $"处理结果: {request}"; } ``` #### 异步处理 ```csharp using NewLife.RocketMQ; // 创建消费者 var consumer = new Consumer { Topic = "request_topic", Group = "request_consumer_group", NameServerAddress = "127.0.0.1:9876", FromLastOffset = false }; // 设置异步消息处理回调 consumer.OnConsumeAsync = async (queue, messages, cancellationToken) => { foreach (var message in messages) { Console.WriteLine($"收到请求: {message.BodyString}"); // 检查是否是请求消息 if (!String.IsNullOrEmpty(message.CorrelationId)) { // 异步处理业务逻辑 var result = await ProcessRequestAsync(message.BodyString, cancellationToken); // 异步发送回复 await consumer.SendReplyAsync(message, result, cancellationToken); Console.WriteLine($"已发送回复: {result}"); } } return true; }; consumer.Start(); // 保持运行 Console.WriteLine("消费者已启动,按任意键退出..."); Console.ReadKey(); consumer.Stop(); consumer.Dispose(); async Task ProcessRequestAsync(string request, CancellationToken ct) { // 实现你的异步业务逻辑 await Task.Delay(100, ct); // 模拟异步操作 return $"处理结果: {request}"; } ``` ### 3. 超时处理 ```csharp using NewLife.RocketMQ; var producer = new Producer { Topic = "request_topic", NameServerAddress = "127.0.0.1:9876", RequestTimeout = 1000 // 默认超时1秒 }; producer.Start(); try { var response = await producer.RequestAsync("请求消息"); Console.WriteLine($"收到响应: {response.BodyString}"); } catch (TimeoutException ex) { Console.WriteLine($"请求超时: {ex.Message}"); } finally { producer.Stop(); producer.Dispose(); } ``` ## API 参考 ### Producer 类 #### 属性 - `RequestTimeout`:请求超时时间(毫秒),默认 3000ms #### 方法 - `MessageExt Request(Message message, Int32 timeout = -1)` - 发送请求消息,同步等待响应 - 参数: - `message`:请求消息 - `timeout`:超时时间(毫秒),-1 表示使用默认超时时间 - 返回:响应消息 - 异常:`TimeoutException` - 请求超时 - `MessageExt Request(Object body, Int32 timeout = -1)` - 发送请求消息,同步等待响应(简化版本) - 参数: - `body`:消息体内容 - `timeout`:超时时间(毫秒) - 返回:响应消息 - `Task RequestAsync(Message message, Int32 timeout = -1, CancellationToken cancellationToken = default)` - 异步发送请求消息并等待响应 - 参数: - `message`:请求消息 - `timeout`:超时时间(毫秒) - `cancellationToken`:取消令牌 - 返回:响应消息 - 异常:`TimeoutException` - 请求超时 - `Task RequestAsync(Object body, Int32 timeout = -1, CancellationToken cancellationToken = default)` - 异步发送请求消息并等待响应(简化版本) ### Consumer 类 #### 方法 - `SendResult SendReply(MessageExt requestMessage, Object replyBody)` - 发送回复消息 - 参数: - `requestMessage`:原始请求消息 - `replyBody`:回复消息内容 - 返回:发送结果 - 异常: - `ArgumentNullException` - 参数为空 - `InvalidOperationException` - 请求消息缺少必要属性 - `Task SendReplyAsync(MessageExt requestMessage, Object replyBody, CancellationToken cancellationToken = default)` - 异步发送回复消息 - 参数: - `requestMessage`:原始请求消息 - `replyBody`:回复消息内容 - `cancellationToken`:取消令牌 - 返回:发送结果 ### Message 类新增属性 - `ReplyToClient`:回复地址,指示回复消息应发送到的客户端ID - `CorrelationId`:关联ID,用于将回复消息与请求消息关联 - `MessageType`:消息类型,用于区分普通消息和回复消息("REQUEST"/"REPLY") - `RequestTimeout`:请求超时时间(毫秒) ## 注意事项 1. **版本要求**:需要 RocketMQ 服务器版本 4.6.0 或更高 2. **超时设置**:合理设置超时时间,避免长时间阻塞 3. **异常处理**:务必捕获 `TimeoutException` 处理超时情况 4. **资源释放**:使用完毕后及时释放 Producer 和 Consumer 资源 5. **Topic 规划**:建议为 Request-Reply 使用独立的 Topic 6. **消费者处理**:消费者必须检查 `CorrelationId` 属性来判断是否为请求消息 ## 兼容性 - 支持 .NET Framework 4.5+ - 支持 .NET Standard 2.0+ - 支持 .NET Core 2.0+ - 支持 .NET 5.0+ - 兼容 RocketMQ 4.6.0 或以上版本 - 兼容 RocketMQ 5.0 或以上版本 ## 性能建议 1. 复用 Producer 和 Consumer 实例,避免频繁创建销毁 2. 合理设置超时时间,避免资源浪费 3. 对于高并发场景,建议使用异步 API 4. 监控回复消息的处理时间,及时优化业务逻辑 ## 故障排查 ### 请求超时 - 检查消费者是否正常运行 - 检查网络连接是否正常 - 检查消费者处理逻辑是否耗时过长 - 适当增加超时时间 ### 收不到回复 - 确认消费者正确调用了 `SendReply` 或 `SendReplyAsync` - 检查消费者日志,确认是否有异常 - 确认消息的 `CorrelationId` 属性正确设置 - 检查 Topic 配置是否正确 ## 更多示例 更多使用示例请参考项目源码中的单元测试:`XUnitTestRocketMQ/RequestReplyTests.cs` ================================================ FILE: Doc/架构设计.md ================================================ # NewLife.RocketMQ 架构 ## 1. 架构概览 NewLife.RocketMQ 采用**分层清晰、职责单一、可扩展**的架构设计,纯 C# 实现,零外部依赖。 ### 1.1 设计理念 - **纯托管实现**:完全使用 C# 实现,无需 Java、gRPC、Protobuf 第三方库 - **双协议支持**:同时支持 Remoting 协议(4.x)和 gRPC 协议(5.x) - **云厂商适配**:统一 `ICloudProvider` 接口,轻松接入各云服务商 - **高性能优化**:连接复用、对象池、VIP 通道、消息压缩等优化手段 - **可测试性**:30+ 测试类覆盖核心功能和边缘场景 ### 1.2 层次结构 ``` ┌──────────────────────────────────────────────────────────┐ │ 业务层 (MqBase) │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ Producer │ │ Consumer │ │ │ │ 生产者业务逻辑 │ │ 消费者业务逻辑 │ │ │ └─────────────────┘ └─────────────────┘ │ └──────────────────────────────────────────────────────────┘ │ ┌──────────────────────────────────────────────────────────┐ │ 通信层 (Client) │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ ClusterClient │ │ GrpcClient │ │ │ │ TCP 长连接管理 │ │ HTTP/2 gRPC │ │ │ └─────────────────┘ └─────────────────┘ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ NameClient │ │GrpcMessaging │ │ │ │ 路由发现 │ │Service │ │ │ └─────────────────┘ └─────────────────┘ │ │ ┌─────────────────┐ │ │ │ BrokerClient │ │ │ │ 心跳/注销 │ │ │ └─────────────────┘ │ └──────────────────────────────────────────────────────────┘ │ ┌──────────────────────────────────────────────────────────┐ │ 协议层 (Protocol) │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ Command │ │ GrpcModels │ │ │ │ Remoting 帧 │ │ gRPC 消息模型 │ │ │ └─────────────────┘ └─────────────────┘ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ MessageExt │ │ ProtoWriter │ │ │ │ 消息模型 │ │ Protobuf 编码 │ │ │ └─────────────────┘ └─────────────────┘ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ RequestCode │ │ ProtoReader │ │ │ │ 请求码枚举 │ │ Protobuf 解码 │ │ │ └─────────────────┘ └─────────────────┘ │ └──────────────────────────────────────────────────────────┘ │ ┌──────────────────────────────────────────────────────────┐ │ 传输层 (Transport) │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ NewLife.Net │ │ HttpClient │ │ │ │ TCP Socket │ │ HTTP/2 │ │ │ └─────────────────┘ └─────────────────┘ │ └──────────────────────────────────────────────────────────┘ │ ┌──────────────────────────────────────────────────────────┐ │ 云厂商适配层 (CloudProvider) │ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │Aliyun │ │Huawei │ │Tencent │ │ ACL │ │ │ │Provider│ │Provider│ │Provider│ │Provider│ │ │ └────────┘ └────────┘ └────────┘ └────────┘ │ └──────────────────────────────────────────────────────────┘ ``` ### 1.3 代码组织结构 ``` MqBase (业务基类,NameServer连接/Broker管理/Topic与消费组CRUD/消息查询) ├── Producer (生产者:普通/异步/单向/延迟/事务/批量/Request-Reply/gRPC) └── Consumer (消费者:Pull/调度/Rebalance/多Topic/顺序/重试/Pop/gRPC) ClusterClient (集群客户端/通信层,Remoting协议) ├── NameClient (名称服务器客户端:路由发现/定时轮询/多Topic路由/Broker主从解析) └── BrokerClient (Broker客户端:心跳/注销/命令收发) Grpc/ (gRPC传输层,RocketMQ 5.x Proxy协议,netstandard2.1+) ├── GrpcClient (HTTP/2 gRPC客户端,帧编解码,Unary + Server Streaming) ├── GrpcMessagingService (消息服务:路由/发送/接收/确认/心跳/事务/延迟/死信) ├── ProtoWriter / ProtoReader (轻量级Protobuf编解码器,无外部依赖) ├── GrpcModels (Resource/Endpoints/Message/SystemProperties/MessageQueue等) ├── GrpcServiceMessages (Request/Response消息类型,约25个,含Telemetry) └── GrpcEnums (GrpcCode/GrpcMessageType/GrpcClientType/AddressScheme等) Protocol/ ├── Command (命令帧,Remoting协议编解码) ├── MqCodec (网络编解码器) ├── Message / MessageExt (消息模型,含批量解码/ZLIB解压/IPv4+IPv6/5.x MessageId) ├── RequestCode (约60个指令码) / ResponseCode (约20个响应码) ├── MQVersion (V3.0 ~ V5.9.9 + HIGHER_VERSION,约450个版本) └── 各类 Header / State / Enum CloudProvider/ ├── ICloudProvider (统一云厂商接口) ├── AliyunProvider (阿里云适配) ├── AclProvider (Apache ACL适配) ├── HuaweiProvider (华为云适配) └── TencentProvider (腾讯云适配) ``` --- ## 2. 数据模型 ### 2.1 消息模型 | 类型 | 说明 | 字段 | |------|------|------| | Message | 基础消息 | Topic, Tags, Keys, Body, Properties, DelayTimeLevel | | MessageExt | 扩展消息(接收端) | MsgId, QueueId, QueueOffset, BornTimestamp, StoreTimestamp, BornHost, StoreHost, SysFlag, ReconsumeTimes | | GrpcMessage | gRPC 消息(5.x) | Topic(GrpcResource), UserProperties, SystemProperties(GrpcSystemProperties), Body | ### 2.2 路由模型 | 类型 | 说明 | 字段 | |------|------|------| | BrokerInfo | Broker 信息 | BrokerName, MasterAddress, SlaveAddresses, Weight | | MessageQueue | 消息队列 | BrokerName, QueueId, ReadQueueNums, WriteQueueNums | ### 2.3 协议帧模型 **Remoting 协议帧**: ``` ┌────────┬────────────┬────────────┬─────────────────┐ │ Length │HeaderLength│ Header │ Body │ │ 4 bytes│ 4 bytes │ N bytes │ M bytes │ └────────┴────────────┴────────────┴─────────────────┘ Length = 4 + N + M HeaderLength 高 8 位:SerializeType (0=JSON, 1=ROCKETMQ) HeaderLength 低 24 位:实际 Header 长度 ``` **gRPC 消息帧**: ``` ┌────────┬────────────┬──────────────────────────────┐ │ Comp │ Length │ Body │ │ 1 byte │ 4 bytes │ N bytes │ └────────┴────────────┴──────────────────────────────┘ Comp: 0=不压缩, 1=gzip Length: 大端序,Protobuf 消息体长度 ``` --- ## 3. 接口设计 ### 3.1 生产者 API | 接口 | 方法 | 签名 | 入参 | 出参 | 说明 | |------|------|------|------|------|------| | Producer | Publish | `SendResult Publish(Object message, String tags)` | message: 消息体, tags: 标签 | SendResult | 同步发送 | | Producer | PublishAsync | `Task PublishAsync(Object message, String tags)` | 同上 | Task\ | 异步发送 | | Producer | PublishOneway | `void PublishOneway(Object message, String tags)` | 同上 | 无 | 单向发送 | | Producer | PublishBatch | `SendResult PublishBatch(IList messages)` | messages: 消息列表 | SendResult | 批量发送 | | Producer | PublishDelay | `SendResult PublishDelay(Object message, DelayTimeLevels delay)` | message, delay: 延迟等级 | SendResult | 延迟消息 | | Producer | PublishDelayViaGrpcAsync | `Task PublishDelayViaGrpcAsync(String body, DateTime deliveryTime)` | body, deliveryTime: 投递时间 | Task | gRPC 任意延迟 | | Producer | PublishTransaction | `SendResult PublishTransaction(Object message)` | message: 消息体 | SendResult | 事务半消息 | | Producer | EndTransaction | `void EndTransaction(SendResult result, TransactionState state)` | result, state: 提交/回滚 | 无 | 结束事务 | | Producer | Request | `MessageExt Request(Object message, Int32 timeout)` | message, timeout: 超时 | MessageExt | Request-Reply | ### 3.2 消费者 API | 接口 | 方法 | 签名 | 入参 | 出参 | 说明 | |------|------|------|------|------|------| | Consumer | OnConsume | `Func` | 委托 | — | 消费回调 | | Consumer | Pull | `PullResult Pull(MessageQueue queue, Int64 offset, Int32 maxNums)` | queue, offset, maxNums | PullResult | 拉取消息 | | Consumer | PopMessageAsync | `Task> PopMessageAsync(...)` | timeout 等 | IList\ | Pop 消费 | | Consumer | AckMessageAsync | `Task AckMessageAsync(MessageExt msg)` | msg: 消息 | Task | 确认消费 | | Consumer | BatchAckMessageAsync | `Task BatchAckMessageAsync(IList msgs)` | msgs: 消息列表 | Task | 批量确认 | ### 3.3 云厂商适配接口 | 接口 | 方法 | 签名 | 入参 | 出参 | 说明 | |------|------|------|------|------|------| | ICloudProvider | TransformTopic | `String TransformTopic(String topic)` | topic: 原始主题名 | 转换后主题名 | 主题名转换 | | ICloudProvider | TransformGroup | `String TransformGroup(String group)` | group: 原始组名 | 转换后组名 | 消费组名转换 | | ICloudProvider | GetNameServerAddress | `String GetNameServerAddress()` | 无 | NameServer 地址 | 获取 NameServer | ### 3.4 核心 RequestCode | 分类 | RequestCode | 值 | 说明 | |------|------------|:--:|------| | 消息发送 | SEND_MESSAGE_V2 | 310 | 发送消息V2 | | | SEND_BATCH_MESSAGE | 320 | 批量发送 | | | SEND_REPLY_MESSAGE_V2 | 325 | 回复消息 | | 消息拉取 | PULL_MESSAGE | 11 | 拉取消息 | | | POP_MESSAGE | 200050 | Pop 消费 | | | ACK_MESSAGE | 200051 | Pop 确认 | | | BATCH_ACK_MESSAGE | 200151 | 批量 Pop 确认 | | 事务消息 | END_TRANSACTION | 37 | 结束事务 | | | CHECK_TRANSACTION_STATE | 39 | 事务回查 | | 偏移管理 | QUERY_CONSUMER_OFFSET | 14 | 查询偏移 | | | UPDATE_CONSUMER_OFFSET | 15 | 更新偏移 | | | SEARCH_OFFSET_BY_TIMESTAMP | 29 | 按时间戳搜索 | | 顺序消费 | LOCK_BATCH_MQ | 41 | 锁定队列 | | | UNLOCK_BATCH_MQ | 42 | 解锁队列 | | 路由管理 | GET_ROUTEINTO_BY_TOPIC | 105 | Topic 路由 | | | GET_BROKER_CLUSTER_INFO | 106 | 集群信息 | | 心跳 | HEART_BEAT | 34 | 心跳 | | | UNREGISTER_CLIENT | 35 | 注销客户端 | ### 3.5 gRPC RPC 方法 | 方法 | 类型 | 说明 | |------|------|------| | QueryRoute | Unary | 查询主题路由 | | SendMessage | Unary | 发送消息(普通/延迟/FIFO/事务) | | QueryAssignment | Unary | 查询队列分配 | | ReceiveMessage | Server Streaming | 接收消息(长轮询) | | AckMessage | Unary | 确认消息消费 | | Heartbeat | Unary | 心跳 | | EndTransaction | Unary | 结束事务 | | ForwardToDeadLetterQueue | Unary | 转发到死信队列 | | ChangeInvisibleDuration | Unary | 修改不可见时间 | | NotifyClientTermination | Unary | 通知客户端终止 | | Telemetry | Bidirectional Streaming | 客户端资源上报 | --- ## 4. 技术选型 | 领域 | 选型 | 理由 | |------|------|------| | 网络通信(Remoting) | NewLife.Net | 高性能 TCP 库,单机千万级吞吐,内置连接池 | | 网络通信(gRPC) | System.Net.Http.HttpClient | .NET 原生 HTTP/2 支持,无需第三方库 | | 序列化(Remoting) | NewLife.Serialization | 内置 JSON 序列化,无外部依赖 | | 序列化(gRPC) | 自研 ProtoWriter/ProtoReader | 轻量级 Protobuf 编解码,零外部依赖 | | 加密签名 | System.Security.Cryptography | .NET 内置 HMAC-SHA1 | | SSL/TLS | System.Net.Security.SslStream | .NET 内置 SSL/TLS 支持 | | 日志 | NewLife.Log.ILog | NewLife 核心日志组件 | | 追踪 | NewLife.Model.ITracer | NewLife APM 追踪组件 | | 负载均衡 | 自研 WeightRoundRobin | 加权轮询算法,支持 Broker 权重 | | 压缩 | System.IO.Compression.DeflateStream | .NET 内置 ZLIB 压缩 | --- ## 5. 关键设计决策 | 决策点 | 方案 | 备选方案 | 选择理由 | |--------|------|---------|---------| | Protobuf 编解码 | 自研 ProtoWriter/ProtoReader | Google.Protobuf / protobuf-net | 零外部依赖,减少包体积和部署复杂度 | | gRPC 通信 | 原生 HttpClient + HTTP/2 | Grpc.Net.Client | 零外部依赖,仅需 netstandard2.1+ | | 连接管理 | ConcurrentDictionary 连接池 | 第三方连接池库 | 简单高效,满足 Broker 少连接场景 | | 消费者负载均衡 | 客户端平均分配 | 服务端 Rebalance | 兼容 4.x,5.x 服务端 Rebalance 需 Broker 配合 | | 云厂商适配 | ICloudProvider 接口 + 策略模式 | 硬编码各厂商逻辑 | 易扩展,新增厂商只需实现接口 | | 多目标框架 | net45/net461/netstandard2.0/netstandard2.1 | 仅 netstandard2.0 | 覆盖企业遗留系统和最新平台 | | 消息压缩 | 发送端 ZLIB 自动压缩 | 不压缩 / 手动压缩 | 大消息体自动优化带宽 | | VIP 通道 | BrokerPort - 2 优先级连接 | 仅标准端口 | 提升高优先级消息性能 | --- ## 6. 任务分解 所有任务已按迭代完成,详见需求文档「6. 功能清单与迭代计划」。 ### 已完成批次 | 批次 | 任务范围 | 状态 | |------|---------|:----:| | 批次 1 | T001~T008:核心通信与基础消息 | ✅ | | 批次 2 | T009~T016:生产可靠性增强 | ✅ | | 批次 3 | T017~T027:功能完善 | ✅ | | 批次 4 | T028~T032:云厂商适配 | ✅ | | 批次 5 | T033~T038:gRPC 协议与 5.x 新特性 | ✅ | | 批次 6 | T039~T044:功能增强与优化 | ✅ | --- ## 7. 风险与缓解 | 风险 | 影响 | 缓解措施 | |------|------|---------| | 各云厂商 5.x gRPC 接入未验证 | 阿里云 Serverless / 华为云 5.x / 腾讯云可能不兼容 | 客户端已具备完整 gRPC 能力,待实际环境逐步验证 | | Language 标识为 DOTNET | 部分 Broker 可能不识别 | DOTNET 为合理标识,暂无实际影响报告 | | 自研 Protobuf 编解码与官方不一致 | 极端数据格式可能解析失败 | 30+ 测试覆盖核心编解码场景,持续补充边界用例 | | RocketMQ 新版本协议变更 | 新增 RequestCode / Header 字段 | MQVersion 枚举已扩展到 V5.9.9 + HIGHER_VERSION,跟进社区更新 | | 阿里云公网版 BrokerName 不匹配 | 消费者偏移匹配可能失败 | 已用容错代码处理,不影响核心消费流程 | | 连接泄漏或超时 | 长时间运行可能资源不足 | 心跳保活 30s + 连接池管理 + Dispose 释放 | --- ## 附录 A:核心文件清单 | 文件 | 行数 | 说明 | |------|:----:|------| | MqBase.cs | ~1500 | 业务基类 | | Producer.cs | ~1200 | 生产者实现 | | Consumer.cs | ~1800 | 消费者实现 | | ClusterClient.cs | ~423 | TCP 连接管理 | | NameClient.cs | ~243 | 路由发现 | | BrokerClient.cs | ~142 | 心跳/注销 | | Command.cs | ~333 | Remoting 帧编解码 | | MessageExt.cs | ~244 | 消息扩展模型 | | RequestCode.cs | ~277 | 60+ 请求码 | | MQVersion.cs | ~909 | 450+ 协议版本 | | GrpcClient.cs | ~310 | gRPC HTTP/2 客户端 | | ProtoWriter.cs | ~343 | Protobuf 编码器 | | ProtoReader.cs | ~308 | Protobuf 解码器 | | GrpcModels.cs | ~520 | gRPC 消息模型 | | GrpcServiceMessages.cs | ~883 | gRPC 服务消息 | | GrpcMessagingService.cs | ~417 | 11 个 RPC 方法 | ## 附录 B:双协议特性对比 | 特性 | Remoting 协议 | gRPC 协议 | |------|--------------|-----------| | 传输层 | TCP 长连接(NewLife.Net) | HTTP/2(HttpClient) | | 编解码 | 自定义二进制帧 + JSON | Protobuf(自研编解码器) | | 支持版本 | RocketMQ 4.x / 5.x Broker | RocketMQ 5.x Proxy | | 目标框架 | net45+ | netstandard2.1+ / net5+ | | 连接复用 | 单连接 Opaque 复用 | HTTP/2 多路复用 | | SSL/TLS | 支持 | 原生支持(HTTPS) | | VIP 通道 | 支持(BrokerPort - 2) | N/A | | 签名认证 | HMAC-SHA1 | HTTP Header | | 消息压缩 | ZLIB(SysFlag 标记) | gzip(HTTP Content-Encoding) | ## 附录 C:各厂商 RocketMQ 产品对比 | 厂商 | 产品 | 协议 | 认证方式 | NameServer 发现 | 适配器 | |------|------|------|---------|----------------|--------| | Apache | RocketMQ 4.x | Remoting (TCP) | ACL (AccessKey) | 直连/HTTP | AclProvider | | Apache | RocketMQ 5.x | Remoting + gRPC | ACL | 直连/HTTP/Proxy | AclProvider | | 阿里云 | 消息队列 RocketMQ 4.x | Remoting | AK/SK + HMAC-SHA1 | HTTP 接口 | AliyunProvider | | 阿里云 | 消息队列 RocketMQ 5.x | gRPC 为主 | AK/SK | SDK/HTTP | AliyunProvider | | 华为云 | DMS for RocketMQ | Remoting | SASL / AK/SK | 实例地址 | HuaweiProvider | | 腾讯云 | TDMQ RocketMQ | Remoting | HMAC-SHA1 | VPC 内网 | TencentProvider | ## 附录 D:测试覆盖 测试框架:xUnit,目标框架 net10.0,共 30 个测试文件。 | 分类 | 测试文件 | 覆盖功能 | |------|---------|---------| | 核心功能 | ProducerTests, ConsumerTests, CommandTests, MessageTests, NameClientTests | 基础收发、协议编解码、路由发现 | | 高级特性 | TransactionCheckTests, BatchMessageTests, RetryTests, OrderConsumeTests, PopConsumeTests | 事务回查、批量消息、重试、顺序消费、Pop | | 协议兼容 | IPv6Tests, MessageId5xTests, MQVersionTests, ProtoTests | IPv6、5.x MessageId、协议版本、Protobuf | | 云厂商 | AliyunTests, AliyunIssuesTests, CloudProviderTests | 阿里云适配、云厂商接口 | | 性能优化 | CompressionTests, ConcurrentConsumeTests, VipChannelTests | 压缩、并发控制、VIP 通道 | | 管理功能 | ManagementTests, ConsumeStatsTests, QueryMessageTests | 管理 API、消费统计、消息查询 | | 扩展功能 | MultiTopicTests, RequestReplyTests, MessageTraceTests, SQL92FilterTests | 多 Topic、Request-Reply、轨迹、SQL92 | ================================================ FILE: Doc/需求文档.md ================================================ # NewLife.RocketMQ 需求 ## 1. 背景与目标 ### 1.1 背景与痛点 Apache RocketMQ 是国内使用最广泛的分布式消息中间件之一,广泛应用于电商交易、金融支付、物联网、大数据等领域。然而 .NET 生态中缺乏成熟、轻量、功能完整的 RocketMQ 客户端: - **官方 C# 客户端(rocketmq-client-csharp)**:仅支持 gRPC 协议(5.x),依赖 Google.Protobuf 和 Grpc.Net.Client 等第三方库,不支持 Remoting 协议(4.x),且更新缓慢(最新 commit 10个月前) - **社区第三方客户端**:功能不完整,多数已停止维护,缺乏云厂商适配 - **.NET Framework 兼容**:大量企业 .NET Framework 4.5+ 项目无法使用现有客户端 ### 1.2 目标 打造企业级**纯托管 .NET RocketMQ 客户端**,实现以下可衡量目标: - 完整支持 RocketMQ Remoting 协议(4.x/5.x Broker)和 gRPC Proxy 协议(5.x Proxy) - 零外部依赖(无需 Java、gRPC、Protobuf 第三方库),NuGet 一键安装 - 覆盖生产者、消费者全部核心功能及企业级特性(重试、死信、事务、顺序消费等) - 统一云厂商适配接口,支持阿里云、华为云、腾讯云及 Apache ACL 认证 - 兼容 .NET Framework 4.5+ 到 .NET 10 全版本 ## 2. 用户角色 | 角色 | 说明 | 核心诉求 | |------|------|---------| | .NET 开发者 | 使用 .NET 平台开发业务系统的开发人员 | 简单易用、功能完整、文档清晰的 RocketMQ 客户端 | | 架构师 | 负责技术选型和系统架构设计 | 高性能、高可靠、可扩展、多云适配的消息中间件方案 | | 运维工程师 | 负责系统部署与运维 | 零外部依赖、易于部署、可观测性(日志/轨迹/监控) | | 云平台用户 | 使用阿里云/华为云/腾讯云消息队列服务 | 无缝接入云厂商 RocketMQ 实例,支持认证和路由转换 | ## 3. 功能需求 ### 3.1 Remoting 协议通信层 - **描述**:实现 RocketMQ 4.x/5.x Broker 的 TCP 私有协议通信 - **用户故事**:作为 .NET 开发者,我希望通过 Remoting 协议连接 RocketMQ 4.x/5.x Broker,以便使用成熟稳定的通信方式 - **验收条件**(AC): - [x] 支持 Remoting 二进制帧编解码(Length + HeaderLength + Header + Body) - [x] 支持 JSON 和 ROCKETMQ 两种序列化格式 - [x] 支持 TCP 长连接管理与复用(ConcurrentDictionary 连接池) - [x] 支持 Opaque 请求-响应匹配 - [x] 支持 HMAC-SHA1 统一签名 - [x] 支持 SSL/TLS 加密传输 - [x] 支持 VIP 通道(BrokerPort - 2) - [x] 实现 60+ RequestCode 指令 - **优先级**:Must - **完成状态**:✅ 已完成 ### 3.2 gRPC Proxy 协议通信层 - **描述**:实现 RocketMQ 5.x Proxy 的 gRPC 协议通信(HTTP/2 + Protobuf) - **用户故事**:作为 .NET 开发者,我希望通过 gRPC 协议连接 RocketMQ 5.x Proxy,以便使用 5.x 新架构的特性 - **验收条件**(AC): - [x] 自研轻量级 Protobuf 编解码器(ProtoWriter/ProtoReader),无外部依赖 - [x] 支持 gRPC 帧格式(Comp + Length + Body) - [x] 支持 HTTP/2 多路复用 - [x] 支持 Unary(请求-响应)调用 - [x] 支持 Server Streaming(服务端流式)调用 - [x] 实现 11 个核心 RPC 方法(QueryRoute/SendMessage/ReceiveMessage/AckMessage/Heartbeat/EndTransaction/ForwardToDeadLetterQueue/ChangeInvisibleDuration/NotifyClientTermination/Telemetry/QueryAssignment) - [x] 仅在 netstandard2.1+ / net5+ 目标框架可用 - **优先级**:Must - **完成状态**:✅ 已完成 ### 3.3 NameServer 路由发现 - **描述**:实现 Topic 路由发现与定时轮询更新 - **用户故事**:作为 .NET 开发者,我希望客户端自动发现 Topic 路由并定时刷新,以便无需手动管理 Broker 连接 - **验收条件**(AC): - [x] 支持 Topic 路由查询(GET_ROUTEINTO_BY_TOPIC) - [x] 支持定时轮询路由更新(30s 间隔) - [x] 支持多 Topic 路由管理 - [x] 支持 Broker 主从地址解析 - [x] 支持 HTTP 方式获取 NameServer 地址(阿里云) - **优先级**:Must - **完成状态**:✅ 已完成 ### 3.4 生产者核心功能 - **描述**:实现消息生产者全部核心功能 - **用户故事**:作为 .NET 开发者,我希望使用丰富的消息发送方式,以便满足各种业务场景 - **验收条件**(AC): - [x] 同步发送(Publish) - [x] 异步发送(PublishAsync) - [x] 单向发送(PublishOneway) - [x] 批量消息发送(PublishBatch / SEND_BATCH_MESSAGE) - [x] 延迟消息发送(PublishDelay,18 级预设延迟) - [x] 任意时间延迟消息(PublishDelayViaGrpcAsync,gRPC 协议) - [x] 事务消息(PublishTransaction / EndTransaction) - [x] 事务回查回调(OnCheckTransaction,同步/异步委托) - [x] 顺序消息(指定 MessageQueue 发送) - [x] Request-Reply 模式(Request / RequestAsync) - [x] 消息压缩(CompressOverBytes 阈值自动 ZLIB 压缩) - [x] 发送重试(RetryTimesWhenSendFailed) - [x] 发送端钩子(ISendMessageHook) - [x] 消息轨迹追踪(AsyncTraceDispatcher / MessageTraceHook) - [x] 加权轮询负载均衡(ILoadBalance / WeightRoundRobin) - [x] gRPC 发送(SendMessageViaGrpcAsync / PublishTransactionViaGrpcAsync) - **优先级**:Must - **完成状态**:✅ 已完成 ### 3.5 消费者核心功能 - **描述**:实现消息消费者全部核心功能 - **用户故事**:作为 .NET 开发者,我希望使用灵活的消息消费方式,以便满足不同业务消费模式需求 - **验收条件**(AC): - [x] Pull 模式消费(长轮询拉取) - [x] 消费调度(自动分配队列并启动消费线程) - [x] 集群消费模式(Clustering,平均分配 Rebalance) - [x] 广播消费模式(Broadcasting,本地 JSON 文件持久化偏移) - [x] Tag 过滤 - [x] SQL92 表达式过滤 - [x] 多 Topic 订阅(Topics 属性,按 Topic 分别 Rebalance) - [x] 消费重试(EnableRetry + MaxReconsumeTimes,自动回退到 RETRY Topic) - [x] 死信队列(超过最大重试次数自动进入 %DLQ% Topic) - [x] 消费回退(SendMessageBackAsync / CONSUMER_SEND_MSG_BACK) - [x] 顺序消费(LockBatchMQAsync / UnlockBatchMQAsync / OrderConsume) - [x] Pop 消费模式(PopMessageAsync / AckMessageAsync / ChangeInvisibleTimeAsync) - [x] 批量确认 Pop 消息(BatchAckMessageAsync / BATCH_ACK_MESSAGE) - [x] 按时间戳消费(SearchOffset / SEARCH_OFFSET_BY_TIMESTAMP) - [x] 消费限流(MaxConcurrentConsume 信号量控制) - [x] 消费者变更通知(NOTIFY_CONSUMER_IDS_CHANGED 触发重平衡) - [x] Request-Reply 回复(SendReply / SendReplyAsync) - [x] 消费端钩子(IConsumeMessageHook) - [x] gRPC 消费(ReceiveMessageViaGrpcAsync / AckMessageViaGrpcAsync / HeartbeatViaGrpcAsync) - **优先级**:Must - **完成状态**:✅ 已完成 ### 3.6 管理与运维功能 - **描述**:实现 RocketMQ 管理 API - **用户故事**:作为运维工程师,我希望通过客户端管理 Topic、消费组、查询消息,以便方便运维操作 - **验收条件**(AC): - [x] Topic 创建/更新/删除(CreateTopic / DeleteTopic) - [x] 消费组创建/更新/删除(CreateSubscriptionGroup / DeleteSubscriptionGroup) - [x] 消息查询(按 ID:ViewMessage / 按 Key:QueryMessageByKey) - [x] 消费统计查询(GetConsumeStats / GetTopicStatsInfo) - [x] 集群信息查询(GetClusterInfo) - [x] 消费者连接列表查询(GetConsumerConnectionList) - [x] 偏移量管理与重置(QueryOffset / UpdateOffset / ResetConsumerOffset) - [x] Broker 运行信息(GetRuntimeInfo) - [x] 消息过滤服务器注册(RegisterFilterServer) - **优先级**:Should - **完成状态**:✅ 已完成 ### 3.7 云厂商适配 - **描述**:统一云厂商适配接口,支持多云平台接入 - **用户故事**:作为云平台用户,我希望通过统一接口接入不同云厂商的 RocketMQ 实例,以便减少适配工作量 - **验收条件**(AC): - [x] 统一 ICloudProvider 接口(SignatureTransformTopic/TransformGroup/GetNameServerAddress) - [x] 阿里云适配(AliyunProvider:实例 ID 前缀路由 + HTTP NameServer 发现 + HMAC-SHA1 签名) - [x] 华为云适配(HuaweiProvider:SSL/TLS + 实例 ID 路由) - [x] 腾讯云适配(TencentProvider:Namespace 前缀路由) - [x] Apache ACL 适配(AclProvider:HMAC-SHA1 签名,不转换 Topic/Group) - [x] 旧版参数兼容(AliyunOptions / AclOptions 自动桥接到新适配器) - **优先级**:Must - **完成状态**:✅ 已完成 ### 3.8 消息编解码 - **描述**:完整实现消息二进制编解码 - **用户故事**:作为 .NET 开发者,我希望客户端正确处理各种消息格式,以便与不同版本的 RocketMQ 兼容 - **验收条件**(AC): - [x] 标准 4.x 消息二进制格式编解码 - [x] ZLIB 消息解压缩(SysFlag 第 0 位标识) - [x] IPv4/IPv6 地址自动识别和解析(SysFlag 第 2 位标识) - [x] 批量消息解码(DecodeBatch,SysFlag 第 4 位标识) - [x] 5.x MessageId 新格式编解码(CreateMessageId5x / TryParseMessageId5x / IsMessageId5x) - [x] 消息属性分隔符与 Java 官方一致(\x01 和 \x02) - **优先级**:Must - **完成状态**:✅ 已完成 ### 3.9 可观测性 - **描述**:提供日志、轨迹、监控等可观测手段 - **用户故事**:作为运维工程师,我希望能追踪消息链路和监控客户端运行状态,以便快速定位问题 - **验收条件**(AC): - [x] 结构化日志(ILog) - [x] 消息轨迹追踪(AsyncTraceDispatcher / MessageTraceHook) - [x] 性能追踪集成(Tracer / NewSpan) - [x] 客户端资源上报(gRPC Telemetry) - [x] 消费者运行信息上报(GetConsumerRunningInfo) - **优先级**:Should - **完成状态**:✅ 已完成 ## 4. 非功能需求 ### 4.1 性能 - 基于 NewLife.Net 高性能网络层,支持连接复用(单连接 Opaque 复用 / HTTP/2 多路复用) - 支持 VIP 通道(BrokerPort - 2 优先级连接) - 支持消息压缩(发送端 ZLIB,超阈值自动压缩) - 支持消费限流(信号量控制最大并发) - 对象池和内存池优化(Pool.StringBuilder、ArrayPool 等) ### 4.2 安全 - 支持 HMAC-SHA1 签名认证(统一由 ICloudProvider 实现) - 支持 SSL/TLS 加密传输(SslProtocol + Certificate 配置) - 支持 Apache ACL 权限控制 - 不暴露内部实现路径和敏感凭据 ### 4.3 兼容性 - 目标框架:net45 / net461 / netstandard2.0 / netstandard2.1(gRPC 功能 netstandard2.1+) - RocketMQ 服务端兼容:4.0 ~ 5.x(Remoting 协议)/ 5.x Proxy(gRPC 协议) - 云厂商兼容:阿里云 4.x/5.x、华为云 DMS、腾讯云 TDMQ - 新增 API 评估各框架降级实现 ## 5. 边界与约束 ### 5.1 不做什么(明确排除项) - **Compaction Topic**:RocketMQ 5.1+ KV 语义 Topic,属于小众特性,暂不实现 - **服务端 Rebalance**:需 RocketMQ 5.0+ Broker 端深度配合,属于 Broker 端特性,客户端暂不实现 - **SASL 认证**:华为云可能使用的 SASL/PLAIN 或 SASL/SCRAM 认证,待有明确需求再实现 - **非 .NET 平台支持**:仅支持 .NET 平台 ### 5.2 已知限制 - Controller 模式对客户端透明,无需特殊适配 - 阿里云 Serverless 实例仅支持 gRPC 接入,客户端已具备能力但待实际环境验证 - 华为云/腾讯云 gRPC 接入待实际环境验证 - Language 标识为 DOTNET,不在 Java 官方枚举中,部分 Broker 可能不识别 ## 6. 功能清单与迭代计划 ### 迭代 1:核心通信与基础消息(Must)— ✅ 已完成 | 编号 | 功能点 | 验收条件 | 前置依赖 | 完成状态 | |------|--------|---------|---------|:-------:| | F001 | Remoting 协议帧编解码 | 正确编解码二进制帧 | 无 | ✅ | | F002 | TCP 连接管理与复用 | 支持连接池、Opaque 匹配 | F001 | ✅ | | F003 | NameServer 路由发现 | 30s 定时轮询、多 Topic 路由 | F002 | ✅ | | F004 | Broker 心跳机制 | 30s 心跳、注销 | F002 | ✅ | | F005 | 消息发送(同步/异步/单向) | 三种发送模式均可用 | F003 | ✅ | | F006 | Pull 消费 | 长轮询拉取消息 | F003 | ✅ | | F007 | 消费调度与 Rebalance | 自动分配队列、平均分配算法 | F006 | ✅ | | F008 | 偏移量管理 | 查询/更新/搜索偏移 | F006 | ✅ | ### 迭代 2:生产可靠性增强(Must)— ✅ 已完成 | 编号 | 功能点 | 验收条件 | 前置依赖 | 完成状态 | |------|--------|---------|---------|:-------:| | F009 | 延迟消息(18 级定时) | 支持预设延迟等级 | F005 | ✅ | | F010 | 事务消息(半消息) | 发送/提交/回滚 | F005 | ✅ | | F011 | 事务回查回调 | 响应 CHECK_TRANSACTION_STATE | F010 | ✅ | | F012 | 批量消息发送 | SEND_BATCH_MESSAGE | F005 | ✅ | | F013 | 顺序消息发送 | 指定 MessageQueue | F005 | ✅ | | F014 | 消费重试 | RETRY Topic + 自动回退 | F007 | ✅ | | F015 | 死信队列 | 超过重试次数进入 %DLQ% | F014 | ✅ | | F016 | 顺序消费锁定 | LOCK/UNLOCK_BATCH_MQ | F007 | ✅ | ### 迭代 3:功能完善(Should)— ✅ 已完成 | 编号 | 功能点 | 验收条件 | 前置依赖 | 完成状态 | |------|--------|---------|---------|:-------:| | F017 | Tag 过滤 | 支持 Tag 表达式 | F006 | ✅ | | F018 | SQL92 过滤 | ExpressionType=SQL92 | F006 | ✅ | | F019 | 消息压缩 | 发送端 ZLIB 压缩 | F005 | ✅ | | F020 | IPv6 支持 | 自动识别 IPv4/IPv6 | F006 | ✅ | | F021 | 多 Topic 订阅 | Topics 属性分别 Rebalance | F007 | ✅ | | F022 | 广播模式本地偏移 | OffsetStorePath JSON 持久化 | F008 | ✅ | | F023 | Request-Reply 模式 | 同步/异步请求回复 | F005 | ✅ | | F024 | 消费限流 | MaxConcurrentConsume 信号量 | F007 | ✅ | | F025 | Pop 消费模式 | Pop/Ack/ChangeInvisibleTime | F006 | ✅ | | F026 | 消息轨迹 | AsyncTraceDispatcher 分发 | F005 | ✅ | | F027 | 消息查询 | 按 ID / 按 Key 查询 | F003 | ✅ | ### 迭代 4:云厂商适配(Must)— ✅ 已完成 | 编号 | 功能点 | 验收条件 | 前置依赖 | 完成状态 | |------|--------|---------|---------|:-------:| | F028 | ICloudProvider 统一接口 | 7 个方法/属性定义 | 无 | ✅ | | F029 | 阿里云适配 | 实例 ID 路由 + HTTP NameServer | F028 | ✅ | | F030 | 华为云适配 | SSL/TLS + 实例 ID 路由 | F028 | ✅ | | F031 | 腾讯云适配 | Namespace 前缀路由 | F028 | ✅ | | F032 | Apache ACL 适配 | HMAC-SHA1 签名 | F028 | ✅ | ### 迭代 5:gRPC 协议与 5.x 新特性(Must)— ✅ 已完成 | 编号 | 功能点 | 验收条件 | 前置依赖 | 完成状态 | |------|--------|---------|---------|:-------:| | F033 | Protobuf 编解码器 | ProtoWriter/ProtoReader 自研 | 无 | ✅ | | F034 | gRPC 客户端 | HTTP/2 Unary + Server Streaming | F033 | ✅ | | F035 | gRPC 消息服务 | 11 个 RPC 方法实现 | F034 | ✅ | | F036 | 任意时间延迟消息 | PublishDelayViaGrpcAsync | F035 | ✅ | | F037 | 5.x MessageId | 新格式编解码 | 无 | ✅ | | F038 | 客户端资源上报 | gRPC Telemetry | F035 | ✅ | ### 迭代 6:功能增强与优化(Should)— ✅ 已完成 | 编号 | 功能点 | 验收条件 | 前置依赖 | 完成状态 | |------|--------|---------|---------|:-------:| | F039 | VIP 通道 | VipChannelEnabled 属性 | F002 | ✅ | | F040 | 批量确认 Pop 消息 | BatchAckMessageAsync | F025 | ✅ | | F041 | 消费统计完整 API | GetConsumeStats / GetTopicStatsInfo | F003 | ✅ | | F042 | 消息过滤服务器注册 | RegisterFilterServer | F003 | ✅ | | F043 | Broker 主从切换 | 消费失败自动切换从节点 | F003 | ✅ | | F044 | 管理功能 | Topic/消费组 CRUD、偏移重置 | F003 | ✅ | ### 待验证功能(各云厂商 5.x 环境) | 编号 | 功能点 | 状态 | 说明 | |------|--------|:----:|------| | F045 | 阿里云 5.x gRPC 接入 | ⚠️ 待验证 | 客户端已具备能力,待实际环境测试 | | F046 | 阿里云 Serverless 实例 | ⚠️ 待验证 | 仅支持 gRPC 接入 | | F047 | 华为云 5.x gRPC 接入 | ⚠️ 待验证 | HuaweiProvider + gRPC 理论可行 | | F048 | 腾讯云 TDMQ 验证 | ⚠️ 待验证 | TencentProvider 待生产环境验证 | ### 暂不实现功能 | 编号 | 功能点 | 原因 | |------|--------|------| | F049 | 服务端 Rebalance | Broker 端特性,客户端需深度配合 | | F050 | Compaction Topic | 5.1+ 小众特性,Broker 端支持 | | F051 | SASL 认证 | 华为云特有,待有明确需求 | ## 7. 竞品分析 ### 7.1 .NET 生态 RocketMQ 客户端对比 | 维度 | NewLife.RocketMQ | Apache rocketmq-client-csharp | 其他社区客户端 | |------|:----------------:|:---------------------------:|:------------:| | **协议支持** | Remoting + gRPC 双协议 | 仅 gRPC(5.x) | 仅 Remoting(部分) | | **4.x 兼容** | ✅ 完整支持 | ❌ 不支持 | ⚠️ 部分支持 | | **5.x 支持** | ✅ Remoting + gRPC | ✅ gRPC | ❌ 不支持 | | **外部依赖** | ✅ 零依赖 | ❌ Google.Protobuf / Grpc.Net.Client / NLog 等 | ⚠️ 部分依赖 | | **Protobuf 实现** | 自研 ProtoWriter/ProtoReader | Google.Protobuf | 无 | | **目标框架** | net45 ~ netstandard2.1 | .NET 5+ / .NET Core 3.1 | 不一致 | | **.NET Framework 支持** | ✅ 4.5+ | ❌ 不支持 | ⚠️ 部分 | | **多云适配** | ✅ 统一 ICloudProvider 接口 | ❌ 需自行适配 | ❌ 无 | | **阿里云适配** | ✅ AliyunProvider | ❌ | ❌ | | **华为云适配** | ✅ HuaweiProvider | ❌ | ❌ | | **腾讯云适配** | ✅ TencentProvider | ❌ | ❌ | | **事务消息** | ✅ 发送+回查 | ✅ 发送+回查 | ⚠️ 部分 | | **消费重试/死信** | ✅ 完整 | ✅ 内置 | ❌ 无 | | **顺序消费** | ✅ 锁定机制 | ✅ FIFO | ❌ 无 | | **批量消息** | ✅ 发送+解码 | ❌ 不支持 | ❌ 无 | | **Pop 消费** | ✅ Pop/Ack/BatchAck | ✅ SimpleConsumer | ❌ 无 | | **Request-Reply** | ✅ 同步/异步 | ❌ 不支持 | ❌ 无 | | **消息轨迹** | ✅ MessageTraceHook | ⚠️ OpenTelemetry 集成 | ❌ 无 | | **VIP 通道** | ✅ | N/A(gRPC) | ❌ 无 | | **消息压缩** | ✅ ZLIB 自动压缩 | ✅ ZLib | ❌ 无 | | **管理 API** | ✅ Topic/消费组 CRUD、消息查询、偏移重置 | ❌ 无 | ❌ 无 | | **测试覆盖** | 30+ 测试类 | 有单元测试 | 少量或无 | | **维护活跃度** | ✅ 持续维护(新生命团队) | ⚠️ 更新较慢(最新 10 个月前) | ❌ 多数已停止 | | **文档语言** | 中文为主 | 中英文 | 不一致 | | **NuGet 包名** | NewLife.RocketMQ | RocketMQ.Client | — | | **开源协议** | MIT | Apache 2.0 | 不一致 | ### 7.2 跨语言 RocketMQ 客户端对比 | 维度 | NewLife.RocketMQ (.NET) | 官方 Java 客户端 | 官方 Go 客户端 | 官方 C++ 客户端 | |------|:----------------------:|:----------------:|:--------------:|:--------------:| | **语言生态** | .NET 原生 | Java 原生 | Go 原生 | C++ 原生 | | **部署复杂度** | ✅ 单一 DLL,零依赖 | ⚠️ 需要 JRE | ✅ 单一二进制 | ⚠️ 需编译 | | **4.x Remoting** | ✅ 完整 | ✅ 完整 | ❌ 仅 5.x gRPC | ❌ 仅 5.x gRPC | | **5.x gRPC** | ✅ 自研编解码 | ✅ 官方实现 | ✅ 官方实现 | ✅ 官方实现 | | **消息类型** | Normal/Delay/FIFO/Transaction | 全部 | 全部 | 全部 | | **消费者类型** | Pull/Push(模拟)/Pop/Simple | 全部 | SimpleConsumer/PushConsumer | SimpleConsumer | | **功能完整度** | ✅ 4.x 100%,5.x ~90% | ✅ 100% | ✅ 5.x 功能完整 | ✅ 5.x 功能完整 | | **消息批量** | ✅ 发送+解码 | ✅ 发送+解码 | ❌ | ❌ | | **管理 API** | ✅ 完整 | ✅ 完整 | ❌ 无 | ❌ 无 | | **社区活跃度** | ⚠️ 新生命团队维护 | ✅ Apache 官方 | ✅ Apache 官方 | ✅ Apache 官方 | ### 7.3 与同类消息队列 .NET 客户端对比 | 维度 | NewLife.RocketMQ | Confluent.Kafka | RabbitMQ.Client | NewLife.Redis (队列) | |------|:----------------:|:--------------:|:--------------:|:------------------:| | **消息中间件** | RocketMQ | Kafka | RabbitMQ | Redis Stream/List | | **协议** | Remoting + gRPC | Kafka 协议 | AMQP | RESP | | **外部依赖** | ✅ 零依赖 | ⚠️ librdkafka | ✅ 零依赖 | ✅ 零依赖 | | **事务消息** | ✅ | ✅(幂等生产者) | ⚠️ 确认机制 | ❌ | | **延迟消息** | ✅ 18 级 + 任意时间 | ❌ 不支持 | ✅ TTL + 死信 | ✅ 延迟队列 | | **顺序消息** | ✅ 队列锁定 | ✅ 分区有序 | ❌ 不保证 | ❌ 不保证 | | **消费重试** | ✅ 内置 | ❌ 需自行实现 | ✅ 死信交换器 | ❌ 需自行实现 | | **消息轨迹** | ✅ 内置 | ⚠️ 需集成 | ⚠️ 需集成 | ❌ | | **多云适配** | ✅ 统一接口 | ⚠️ Confluent Cloud | ❌ | ❌ | | **消息规模** | 万亿级 | 万亿级 | 百亿级 | 十亿级 | | **适用场景** | 企业级业务消息 | 大数据流处理 | 微服务解耦 | 轻量级队列/缓存 | ### 7.4 核心竞争优势总结 1. **零外部依赖**:纯 C# 实现,无需 Java、gRPC、Protobuf 第三方库,部署运维成本最低 2. **双协议支持**:唯一同时支持 Remoting(4.x)和 gRPC(5.x)的 .NET 客户端,向后兼容且面向未来 3. **最广框架覆盖**:.NET Framework 4.5+ 到 .NET 10,覆盖企业遗留系统和最新平台 4. **统一多云适配**:唯一内置阿里云/华为云/腾讯云/Apache ACL 四家适配器的客户端 5. **生产级特性**:完整的企业级功能(事务回查、消费重试、死信队列、顺序消费、Pop 消费、消息轨迹等) 6. **管理 API 完整**:Topic/消费组 CRUD、消息查询、消费统计、偏移重置等运维功能 7. **持续维护**:新生命团队持续迭代更新,紧跟 RocketMQ 社区演进 ## 8. 验收记录 ### 功能验收 | 编号 | 功能点 | 验收条件 | 状态 | 备注 | |------|--------|---------|:----:|------| | F001~F008 | 核心通信与基础消息 | 协议编解码、连接管理、路由发现、消费调度 | ✅ 通过 | — | | F009~F016 | 生产可靠性增强 | 延迟/事务/批量/顺序/重试/死信 | ✅ 通过 | — | | F017~F027 | 功能完善 | 过滤/压缩/IPv6/多 Topic/Pop/轨迹/查询 | ✅ 通过 | — | | F028~F032 | 云厂商适配 | 四家适配器全部实现 | ✅ 通过 | 云厂商 5.x 环境待验证 | | F033~F038 | gRPC 协议与 5.x 新特性 | Protobuf 编解码/gRPC 通信/11 个 RPC | ✅ 通过 | netstandard2.1+ | | F039~F044 | 功能增强与优化 | VIP 通道/批量 Ack/管理 API | ✅ 通过 | — | ### 遗留问题 | 问题 | 影响 | 后续计划 | |------|------|---------| | 阿里云公网版 BrokerName 不匹配 | 消费者状态中偏移匹配需容错 | 已用 `?? new OffsetWrapperModel()` 容错处理 | | 各云厂商 5.x gRPC 接入未验证 | 阿里云 Serverless/华为云 5.x/腾讯云 | 客户端已具备能力,待实际环境测试 | | Language 标识为 DOTNET | 部分 Broker 可能不识别 | 暂无影响,DOTNET 为合理标识 | ### 经验总结 - **做得好的**:零依赖的 Protobuf 编解码器设计、统一云厂商适配接口、50+ 测试类高覆盖率 - **待改进的**:云厂商 5.x 环境的实际验证、性能基准测试数据补充 ### 单元测试覆盖统计 | 测试类 | 测试数 | 覆盖目标 | 状态 | |--------|:------:|---------|:----:| | CommandTests | 16 | Command 协议帧编解码 | ✅ 通过 | | SpanRefactorTests | 20+ | 二进制协议序列化、MessageExt 5.x ID | ✅ 通过 | | ProtoTests | 30+ | Protobuf 编解码(Varint/Fixed/Map/嵌套/gRPC) | ✅ 通过 | | NameClientTests | 8 | 路由发现解析、缓存、主从地址 | ✅ 通过 | | BatchMessageTests | 10 | 批量消息编解码、SysFlag、自动展开 | ✅ 通过 | | MessageTests | 5 | 消息体设置、属性解析 | ✅ 通过 | | MessageExtendedTests | 20 | Request-Reply 属性、延迟/等待/事务/UserProperty | ✅ 通过 | | MessageId5xTests | 11 | 5.x 消息ID生成与解析 | ✅ 通过 | | IPv6Tests | 5 | IPv6 消息解码、SysFlag 位判断 | ✅ 通过 | | CloudProviderTests | 25 | Aliyun/ACL/Huawei/Tencent 适配器 | ✅ 通过 | | HeaderTests | 9 | 协议头默认值、GetExtFields、CreateException | ✅ 通过 | | SendResultTests | 8 | 发送结果 Read 方法、ToString、枚举值 | ✅ 通过 | | PullResultTests | 8 | 拉取结果 Read 方法、ToString、枚举值 | ✅ 通过 | | MessageQueueTests | 10 | 消息队列 Equals/GetHashCode/ToString | ✅ 通过 | | ResponseExceptionTests | 5 | 响应异常构造与捕获 | ✅ 通过 | | RequestHeaderTests | 7 | 三种请求头 GetProperties 反射映射 | ✅ 通过 | | WeightRoundRobinTests | 8 | 负载均衡算法(等/不等权重分配、初始化) | ✅ 通过 | | BrokerInfoTests | 9 | 代理信息属性、Equals/GetHashCode、Permissions | ✅ 通过 | | ProtocolDataTests | 12 | HeartbeatData/ProducerData/ConsumerData/SubscriptionData/QueryResult | ✅ 通过 | | TraceModelTests | 9 | TraceContext/TraceBean/SendMessageContext/ConsumeMessageContext | ✅ 通过 | | ModelTests | 11 | 枚举值验证(DelayTimeLevels/RequestCode/ResponseCode 等) | ✅ 通过 | | MqBasePropertyTests | 15 | MqBase 默认值、ClientId、属性设置、Active 状态 | ✅ 通过 | | ConsumerStatesModelTests | 7 | ConsumerStatesModel/MessageQueueModel/OffsetWrapperModel | ✅ 通过 | | MqSettingTests | 1 | MqSetting 配置属性 | ✅ 通过 | | MQVersionTests | 1 | MQVersion 版本转换 | ✅ 通过 | | MQVersionUpdateTests | 4 | 版本枚举 5.x、默认版本 | ✅ 通过 | | VipChannelTests | 6 | VIP 通道启用/禁用 | ✅ 通过 | | SQL92FilterTests | 5 | SQL92 过滤表达式类型 | ✅ 通过 | | CompressionTests | 3 | 消息压缩阈值 | ✅ 通过 | | BroadcastOffsetTests | 4 | 广播模式消息模型与偏移存储 | ✅ 通过 | | MultiTopicTests | 6 | 多主题消费 | ✅ 通过 | | RetryTests | 7 | 重试机制参数 | ✅ 通过 | | ConcurrentConsumeTests | 3 | 并发消费参数 | ✅ 通过 | | TransactionCheckTests | 3 | 事务回查回调 | ✅ 通过 | | OrderConsumeTests | 6 | 顺序消费与队列锁 | ✅ 通过 | | PopConsumeTests | 5 | Pop 消费异常处理与请求码 | ✅ 通过 | | BatchAckTests | 6 | 批量确认异常处理与请求码 | ✅ 通过 | | QueryMessageTests | 3 | 消息查询参数校验 | ✅ 通过 | | RequestReplyTests | 1 | Request-Reply 消息属性 | ✅ 通过 | | BrokerFailoverTests | 5 | Broker 主从切换 | ✅ 通过 | | ConsumeStatsTests | 2 | 消费统计请求码 | ✅ 通过 | | **合计** | **344** | — | **✅ 全部通过** | > 另有 30 个集成测试标记为 Skip(需要 RocketMQ 服务器支持),包括 ProducerTests、ConsumerTests、AliyunTests、ManagementTests 等。 ## 9. 术语表 | 术语 | 定义 | |------|------| | Remoting 协议 | RocketMQ 4.x 的经典 TCP 私有协议,采用二进制帧格式 + JSON/二进制序列化 | | gRPC 协议 | RocketMQ 5.x 引入的新协议,基于 HTTP/2 + Protobuf | | Proxy | RocketMQ 5.x 架构中的代理层,接收 gRPC 请求并转发到 Broker | | NameServer | RocketMQ 的名称服务器,提供 Topic 路由发现 | | Broker | RocketMQ 的消息存储和转发服务器 | | Rebalance | 消费者负载均衡,将队列均匀分配给消费者 | | Pop 消费 | RocketMQ 4.9+ 引入的新消费模式,不自动提交偏移,需手动确认 | | VIP 通道 | 使用 BrokerPort - 2 的优先级端口,提供更高性能的连接 | | ICloudProvider | 统一云厂商适配接口,封装签名、路由转换、NameServer 发现等逻辑 | | HMAC-SHA1 | 基于哈希的消息认证码,用于请求签名 | | Opaque | 请求唯一标识符,用于 TCP 连接上的请求-响应匹配 | | SysFlag | 消息系统标志位,标识压缩、IPv6、批量等特征 | | 死信队列(DLQ) | Dead Letter Queue,超过最大重试次数后消息进入的队列 | ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 新生命开发团队 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: NewLife.RocketMQ/.github/copilot-instructions.md ================================================ # NewLife Copilot 协作指令 本说明适用于新生命团队(NewLife)及其全部开源/衍生项目,规范 Copilot 及类似智能助手在 C#/.NET 项目中的协作行为。 > 目标:把"每次请求必须携带的通用规则"控制在可接受体积;组件/业务专项流程放在 `.github/instructions/`,按需读取。 --- ## 1. 核心原则 | 原则 | 说明 | |------|------| | **提效** | 减少机械样板,聚焦业务/核心算法 | | **一致** | 风格、结构、命名、API 行为稳定 | | **可控** | 限制改动影响面,可审计,兼容友好 | | **可靠** | 先检索再生成,不虚构,不破坏现有合约 | | **主动** | 发现问题主动修复,不回避合理优化 | --- ## 2. 适用范围 - 含 NewLife 组件或衍生的全部 C#/.NET 仓库 - 不含纯前端/非 .NET/市场文案 - 存在本文件 → 必须遵循 --- ## 3. 组件专用指令索引(按需加载) 以下专用指令**仅在相关任务时**才需要读取,避免每次请求都携带大段流程/示例。 ### 3.1 XCode / Cube(数据库 & Web 快速开发) 当任务涉及以下任一信号时,请**先搜索并检查当前仓库** `.github/instructions/xcode.instructions.md` **是否存在**,若存在则读取并遵循: - 需求包含:XCode/Cube/魔方/实体生成/模型 XML/数据类库/数据库 CRUD/Controller 生成/`xcodetool`/`xcode` 命令 - 解决方案/项目中出现:`NewLife.XCode` 包引用 - 存在:`Model.xml`、`*.xcode.xml`、`*.Data.csproj`(或项目名以 `.Data` 结尾) - 代码出现命名空间/类型:`XCode.*`、`Entity`(XCode 实体基类)、XCode 相关特性/接口 - **用户提到修改任意 `.xml` 文件**(如 `member.xml`、`area.xml` 等配置文件),应**主动搜索** `xcode.instructions.md` 判断是否需要引入 **主动检测策略**:当用户提及 XML 文件修改时,即使未明确提到 XCode 关键字,也应先用 `file_search` 搜索 `xcode.instructions.md`,若存在则读取,以确定该 XML 文件是否属于 XCode/Cube 体系。 未满足以上条件时,**不要**引入 XCode/Cube 初始化流程,避免干扰其它仓库的常规开发。 --- ## 4. 工作流 ``` 需求分类 → 检索 → 评估 → 设计 → 实施 → 验证 → 说明 ``` 1. **需求分类**:功能/修复/性能/重构/文档 2. **检索**:相关类型、目录、方法、已有扩展/工具(**优先复用**) 3. **评估**:是否公共 API?是否性能热点?**是否存在潜在问题?** 4. **设计**:列出改动点 + 兼容/降级策略 5. **实施**: - 完成用户请求的核心任务 - **顺带修复**发现的明显缺陷(资源泄漏、空引用、逻辑错误) - **顺带优化**可简化的重复代码 - 保留原注释与结构,除非注释本身有误 6. **验证**: - 代码变更:必须编译通过;运行相关单元测试(未找到需说明) - 仅文档变更(未修改任何代码文件):可跳过编译与单元测试 7. **说明**:变更摘要/影响范围/风险点 ### 4.1 主动优化原则 当用户请求分析或优化代码时,**应主动**: | 类型 | 行动 | |------|------| | **架构梳理** | 梳理代码架构并进行重构,让代码结构更清晰易懂 | | **语法现代化** | 使用最新的 C# 语法来简化代码,提升可读性 | | **缺陷修复** | 资源泄漏、空引用风险、并发问题、逻辑错误 → 直接修复,让代码更健壮 | | **性能优化** | 无用分配、重复计算、可池化资源 → 通过缓存减少耗时的重复计算 | | **代码简化** | 重复代码提取、冗余判断合并、现代语法替换 → 在不影响可读性前提下简化 | | **注释完善** | 补充类、接口、属性、方法头部的注释,以及方法内部重要代码的注释 | | **架构参考** | 参考网络上同类功能的优秀架构,给出架构调整建议 | **架构调整策略**: - **改动较小**:直接调整,完成后说明变更内容 - **改动较大**:先列出调整方案,询问用户意见,待确认后再修改 **不应过度保守**: - ❌ 仅添加注释而忽略明显的代码问题 - ❌ 发现资源泄漏却不修复 - ❌ 看到重复代码却不提取 - ❌ 用户要求优化时只做表面工作 **保持谨慎的场景**: - 公共 API 签名变更 → 需说明兼容性影响 - 性能关键路径 → 需有依据或说明推理 - 大范围重构 → 需先与用户确认范围 ### 4.2 防御性注释规则 在旧有代码中,经常可以看到**被注释掉的代码**,这些注释代码前面通常带有说明文字。 **这些是防御性注释**: - 记录了过去曾经踩过的坑 - 目的是告诉后来人不要按照注释代码去写,否则会有问题 - **禁止删除此类防御性注释**,用于警示后人 **识别特征**: ```csharp // 曾经尝试过 xxx 方案,但会导致 yyy 问题 // var result = DoSomethingWrong(); // 不要使用 xxx,否则会造成 yyy // await client.SendAsync(data); // 这里不能用 xxx,因为 yyy // stream.Flush(); ``` **处理原则**: - ✅ 保留这类带说明的注释代码 - ✅ 可以补充更详细的说明,解释为什么不能这样做 - ❌ 不要删除这类防御性注释 - ❌ 不要尝试"恢复"这些被注释的代码 --- ## 5. 编码规范 ### 5.1 基础规范 | 项目 | 规范 | |------|------| | 语言版本 | `latest`,所有目标框架均使用最新 C# 语法 | | 命名空间 | file-scoped namespace | | 类型名 | **必须**使用 .NET 正式名 `String`/`Int32`/`Boolean` 等,避免 `string`/`int`/`bool` | | 兼容性 | 代码需兼容 .NET 4.5+;**禁止**使用 `ArgumentNullException.ThrowIfNull`,改用 `if (value == null) throw new ArgumentNullException(nameof(value));` | | 单文件 | 每文件一个主要公共类型;较大平台差异使用 `partial` | ### 5.2 命名规范 | 成员类型 | 命名规则 | 示例 | |---------|---------|------| | 类型/公共成员 | PascalCase | `UserService`、`GetName()` | | 参数/局部变量 | camelCase | `userName`、`count` | | 私有字段(实例/静态) | `_camelCase` | `_cache`、`_instance` | | 属性/方法(实例/静态) | PascalCase | `Name`、`Default`、`Create()` | | 扩展方法类 | `xxxHelper` 或 `xxxExtensions` | `StringHelper`、`CollectionExtensions` | ### 5.3 代码风格 ```csharp // ✅ 单行 if:单语句且整行不过长时同行 if (value == null) return; if (key == null) throw new ArgumentNullException(nameof(key)); // ✅ 单行 if:语句较长时另起一行 if (value == null) throw new ArgumentNullException(nameof(value), "Value cannot be null"); // ✅ 多分支单语句:不加花括号 if (count > 0) DoSomething(); else DoOther(); // ✅ 循环必须保留花括号(即使单语句) foreach (var item in list) { Process(item); } ``` ### 5.4 Region 组织结构 较长的类使用 `#region` 分段组织,顺序为:`属性` → `静态`(如有)→ `构造` → `方法` → `辅助`(如有)→ `日志`。 **日志 Region 规则**: - 类代码中如果带有 `ILog Log { get; set; }` 和 `WriteLog` 方法 - **必须放在类代码的最后** - **必须用名为"日志"的 region 包裹** - 不要放在"辅助" region 中,应单独作为"日志" region ### 5.5 现代 C# 语法 优先使用最新语法(switch 表达式、模式匹配、目标类型 `new`、record 等),即使目标框架是 net45。 ### 5.6 集合表达式 优先使用集合表达式 `[]` 初始化集合:`List Tags { get; set; } = [];` ### 5.7 Null 条件运算符 优先使用 `?.` / `??` 简化空值检查:`span?.AppendTag("test");` `var name = user?.Profile?.Name ?? "";` --- ## 6. 多目标框架 NewLife 支持 `net45` 到 `net10`,常用条件符号:`NETFRAMEWORK`、`NETSTANDARD2_0`、`NETCOREAPP`、`NET5_0_OR_GREATER`、`NET6_0_OR_GREATER`、`NET8_0_OR_GREATER`。 新增 API 时需评估各框架兼容性,必要时提供降级实现。 --- ## 7. 文档注释 | 规则 | 说明 | |------|------| | `` | **必须同一行闭合**,简短描述方法用途 | | `` | **必须为每个参数添加**,无论方法可见性如何 | | `` | 有返回值时必须添加 | | `` | 复杂方法可增加详细说明(可多行) | | 覆盖范围 | `public`/`protected` 成员必须注释;`private`/`internal` 建议添加 | | `[Obsolete]` | 必须包含迁移建议 | **正确示例**:`/// 获取名称` `/// 编号` **禁止**:`` 拆成多行;缺少 ``;有参数但无 param 标签。 --- ## 8. 异步与性能 | 规范 | 说明 | |------|------| | 方法命名 | 异步方法后缀 `Async` | | ConfigureAwait | 库内部默认 `ConfigureAwait(false)` | | 高频路径 | 优先对象池/`ArrayPool`/`Span`,避免多余分配 | | 反射/Linq | 仅用于非热点路径;热点使用手写循环/缓存 | | 池化资源 | 明确获取/归还;异常分支不遗失归还 | **内置工具优先**:`Pool.StringBuilder`、`Runtime.TickCount64`、`ToInt()`/`ToBoolean()` 等扩展方法。 --- ## 9. 日志与追踪 规则:若类包含 `ILog Log` 与 `WriteLog`,必须放在类末尾,并用名为"日志"的 `#region` 包裹;关键过程可使用 `Tracer?.NewSpan()` 埋点。 --- ## 10. 错误处理 - **精准异常类型**:`ArgumentNullException`/`InvalidOperationException` 等 - **参数校验**:空/越界/格式 - **TryXxx 模式**:不用异常作常规分支 - **类型转换**:优先使用 `ToInt()`/`ToBoolean()` 等扩展方法 - **对外异常**:不暴露内部实现/路径 --- ## 11. 测试规范 | 项目 | 规范 | |------|------| | 框架 | xUnit | | 命名 | `{ClassName}Tests` | | 描述 | `[DisplayName("中文描述意图")]` | | IO | 使用临时目录;端口用 0/随机 | | 覆盖 | 正常/边界/异常/并发(必要时) | ### 测试执行策略 1. 优先检索 `{ClassName}` 引用,若落入测试项目则运行 2. 未命中则查找 `{ClassName}Tests.cs` 3. **未发现相关测试需明确说明**,不自动创建测试项目 --- ## 12. NuGet 发布规范 | 类型 | 命名规则 | 示例 | |------|---------|------| | 正式版 | `{主版本}.{子版本}.{年}.{月日}` | `11.9.2025.0701` | | 测试版 | `{主版本}.{子版本}.{年}.{月日}-beta{时分}` | `11.9.2025.0701-beta0906` | - **正式版**:每月月初发布 - **测试版**:提交代码到 GitHub 时自动发布 --- ## 13. Markdown 文档规范 | 项目 | 规范 | |------|------| | 文件编码 | **必须** UTF-8,**禁止** GB2312/GBK/UTF-8 BOM | | 默认存放 | 代码库根目录下的 `Doc` 目录 | | 文件命名 | 优先**中文文件名**,简洁描述内容 | **注意**:已有文件**必须先读取**再增量修改,**禁止直接覆盖**。 --- ## 14. Copilot 行为守则 ### 必须 - 简体中文回复 - 输出前检索现有实现,**禁止重复造轮子** - 先列方案再实现 - 标记不确定上下文为"需查看文件" - **发现明显缺陷时主动修复**(资源泄漏、空引用、逻辑错误) - **用户要求优化时深入分析**,不做表面工作 ### 鼓励 - 提取重复代码为公共方法 - 简化冗余的条件判断 - 使用现代 C# 语法改进可读性 - 补充缺失的资源释放逻辑 - 修正错误或过时的注释 ### 禁止 - 虚构 API/文件/类型 - 伪造测试结果/性能数据 - 擅自删除公共/受保护成员 - 擅自删除已有代码注释(除非注释本身错误) - **删除防御性注释**(带说明的注释代码,记录历史踩坑经验) - 仅删除空白行制造"格式优化"提交 - 删除循环体的花括号 - 将 `` 拆成多行 - 将 `String`/`Int32` 改为 `string`/`int` - 新增外部依赖(除非说明理由并给出权衡) - 在热点路径添加未缓存反射/复杂 Linq - 输出敏感凭据/内部地址 - **发现问题却视而不见** - **用户要求优化时仅做注释/测试等表面工作** --- ## 15. 变更说明模板 提交或答复需包含: ```markdown ## 概述 做了什么 / 为什么 ## 影响 - 公共 API:是/否 - 性能影响:无/有(说明) ## 兼容性 降级策略 / 条件编译点 ## 风险 潜在回归 / 性能开销 ## 后续 是否补测试 / 文档 ``` --- ## 16. 术语说明 | 术语 | 定义 | |------|------| | **热点路径** | 经性能分析或高频调用栈确认的关键执行段 | | **基线** | 变更前的功能/性能参考数据 | | **顺带修复** | 在完成主任务过程中,修复发现的相关问题 | | **防御性注释** | 被注释掉的代码,前面带有说明,记录历史踩坑经验,用于警示后人 | --- ## 17. 代码优化检查清单 当进行代码优化时,按以下清单逐项检查: ### 架构与结构 - [ ] 代码架构是否清晰?是否需要重构? - [ ] 类的职责是否单一?是否需要拆分? - [ ] 是否有重复代码可以提取为公共方法? - [ ] Region 组织是否符合规范(属性→静态→构造→方法→辅助→日志)? ### 语法现代化 - [ ] 是否可以使用更简洁的 C# 语法?(switch 表达式、模式匹配等) - [ ] 集合初始化是否使用了集合表达式 `[]`? - [ ] 是否可以使用 null 条件运算符 `?.` 简化代码? ### 健壮性 - [ ] 是否存在空引用风险? - [ ] 资源是否正确释放?(IDisposable、流、连接等) - [ ] 异常处理是否完善? - [ ] 并发场景是否线程安全? ### 性能 - [ ] 是否存在可以缓存的重复计算? - [ ] 是否有不必要的对象分配? - [ ] 热点路径是否避免了反射和复杂 Linq? - [ ] 是否使用了对象池/ArrayPool 等池化技术? ### 注释与文档 - [ ] 类、接口是否有 `` 注释? - [ ] 公共方法是否有完整的参数和返回值注释? - [ ] 方法内重要逻辑是否有注释说明? - [ ] 防御性注释是否保留? ### 日志 - [ ] `ILog Log` 和 `WriteLog` 是否放在类的最后? - [ ] 是否用名为"日志"的 region 包裹? --- (完) ================================================ FILE: NewLife.RocketMQ/AclOptions.cs ================================================ namespace NewLife.RocketMQ { /// /// 支持 Apache RocketMQ ACL机制 /// public class AclOptions { /// Acl访问令牌 public String AccessKey { get; set; } /// Acl访问密钥 public String SecretKey { get; set; } /// 通道 public String OnsChannel { get; set; } = "LOCAL"; } } ================================================ FILE: NewLife.RocketMQ/AclProvider.cs ================================================ namespace NewLife.RocketMQ; /// Apache RocketMQ ACL 适配器 public class AclProvider : ICloudProvider { /// 提供者名称 public String Name => "ACL"; /// 访问令牌 public String AccessKey { get; set; } /// 访问密钥 public String SecretKey { get; set; } /// 通道标识。默认空 public String OnsChannel { get; set; } = ""; /// 转换主题名。ACL模式不转换 public String TransformTopic(String topic) => topic; /// 转换消费组名。ACL模式不转换 public String TransformGroup(String group) => group; /// 获取 NameServer 地址。ACL模式不从HTTP获取 public String GetNameServerAddress() => null; /// 从旧版 AclOptions 创建 /// 旧版ACL选项 /// public static AclProvider FromOptions(AclOptions options) { if (options == null) return null; return new AclProvider { AccessKey = options.AccessKey, SecretKey = options.SecretKey, OnsChannel = options.OnsChannel ?? "", }; } } ================================================ FILE: NewLife.RocketMQ/AliyunOptions.cs ================================================ using System; namespace NewLife.RocketMQ { /// /// 阿里云选项 /// public class AliyunOptions { #region 阿里云属性 /// 获取名称服务器地址的http地址。阿里云专用 public String Server { get; set; } = "http://onsaddr-internet.aliyun.com/rocketmq/nsaddr4client-internet"; /// 访问令牌。阿里云专用 public String AccessKey { get; set; } /// 访问密钥。阿里云专用 public String SecretKey { get; set; } /// 实例ID。阿里云专用 public String InstanceId { get; set; } /// 阿里云MQ通道。阿里云专用 public String OnsChannel { get; set; } = "ALIYUN"; #endregion } } ================================================ FILE: NewLife.RocketMQ/AliyunProvider.cs ================================================ namespace NewLife.RocketMQ; /// 阿里云 RocketMQ 适配器 public class AliyunProvider : ICloudProvider { /// 提供者名称 public String Name => "Aliyun"; /// 访问令牌 public String AccessKey { get; set; } /// 访问密钥 public String SecretKey { get; set; } /// 通道标识。默认ALIYUN public String OnsChannel { get; set; } = "ALIYUN"; /// 实例ID。MQ_INST_xxx public String InstanceId { get; set; } /// NameServer HTTP 发现地址 public String Server { get; set; } /// 转换主题名。阿里云需要加实例ID前缀 /// 原始主题名 /// public String TransformTopic(String topic) { var ins = InstanceId; if (!String.IsNullOrEmpty(ins) && !topic.StartsWith(ins)) return $"{ins}%{topic}"; return topic; } /// 转换消费组名。阿里云需要加实例ID前缀 /// 原始消费组名 /// public String TransformGroup(String group) { var ins = InstanceId; if (!String.IsNullOrEmpty(ins) && !group.StartsWith(ins)) return $"{ins}%{group}"; return group; } /// 获取 NameServer 地址。从阿里云 HTTP 接口获取 /// public String GetNameServerAddress() { var addr = Server; if (String.IsNullOrEmpty(addr) || !addr.StartsWith("http", StringComparison.OrdinalIgnoreCase)) return null; var http = new System.Net.Http.HttpClient(); var html = http.GetStringAsync(addr).ConfigureAwait(false).GetAwaiter().GetResult(); return String.IsNullOrWhiteSpace(html) ? null : html.Trim(); } /// 从旧版 AliyunOptions 创建 /// 旧版阿里云选项 /// public static AliyunProvider FromOptions(AliyunOptions options) { if (options == null) return null; return new AliyunProvider { AccessKey = options.AccessKey, SecretKey = options.SecretKey, OnsChannel = options.OnsChannel ?? "ALIYUN", InstanceId = options.InstanceId, Server = options.Server, }; } } ================================================ FILE: NewLife.RocketMQ/BrokerClient.cs ================================================ using NewLife.Log; using NewLife.Net; using NewLife.RocketMQ.Protocol; using NewLife.Threading; namespace NewLife.RocketMQ; /// 代理客户端 public class BrokerClient : ClusterClient { #region 属性 /// 服务器地址 private readonly String[] _Servers; #endregion #region 构造 /// 实例化代理客户端 /// public BrokerClient(String[] servers) => _Servers = servers; #endregion #region 方法 /// 启动 protected override void OnStart() { //Servers = _Servers.Select(e => new NetUri(e)).ToArray(); var list = new List(); foreach (var item in _Servers) { var uri = new NetUri(item); if (uri.Type == NetType.Unknown) uri.Type = NetType.Tcp; // VIP通道使用端口-2 if (Config != null && Config.VipChannelEnabled && uri.Port > 2) uri.Port -= 2; list.Add(uri); } Servers = list.ToArray(); base.OnStart(); // 心跳 StartPing(); } #endregion #region 注销 /// 注销客户端 /// public virtual Command UnRegisterClient(String group) { if (group.IsNullOrEmpty()) group = "CLIENT_INNER_PRODUCER"; return Invoke(RequestCode.UNREGISTER_CLIENT, new { ClientId = Id, ProducerGroup = group, ConsumerGroup = group, }); } /// protected override void Dispose(Boolean disposing) { if (disposing) _timer?.Dispose(); base.Dispose(disposing); } #endregion #region 心跳 private TimerX _timer; private void StartPing() { if (_timer == null) { var period = Config.HeartbeatBrokerInterval; _timer = new TimerX(OnPing, null, 100, period) { Async = true }; } } private void OnPing(Object state) { DefaultSpan.Current = null; Ping(); } /// 心跳 public void Ping() { using var span = Tracer?.NewSpan($"mq:{Name}:Ping"); try { var cfg = Config; var body = new HeartbeatData { ClientID = Id }; // 生产者 和 消费者 略有不同 if (cfg is Producer pd) { body.ProducerDataSet = [ new ProducerData { GroupName = pd.Group }, new ProducerData { GroupName = "CLIENT_INNER_PRODUCER" }, ]; body.ConsumerDataSet = []; } else if (cfg is Consumer cm) { body.ProducerDataSet = [new ProducerData { GroupName = "CLIENT_INNER_PRODUCER" }]; body.ConsumerDataSet = cm.Data.ToArray(); } span?.AppendTag(body); // 心跳忽略错误。有时候报40错误 Invoke(RequestCode.HEART_BEAT, body, null, true); } catch (Exception ex) { span?.SetError(ex, null); if (ex.GetTrue() is not TaskCanceledException) throw; } } #endregion #region 运行信息 /// 获取运行时信息 /// public IDictionary GetRuntimeInfo() { var rs = Invoke(RequestCode.GET_BROKER_RUNTIME_INFO, null); if (rs == null || rs.Payload == null) return null; var dic = rs.ReadBodyAsJson(); return dic?["table"] as IDictionary; } #endregion } ================================================ FILE: NewLife.RocketMQ/ClusterClient.cs ================================================ using System.Net.Sockets; using System.Security.Cryptography; using NewLife.Data; using NewLife.Log; using NewLife.Net; using NewLife.RocketMQ.Client; using NewLife.RocketMQ.Protocol; using NewLife.Serialization; namespace NewLife.RocketMQ; /// 集群客户端 /// /// 维护到一个集群的客户端连接,内部采用负载均衡调度算法。 /// public abstract class ClusterClient : DisposeBase { #region 属性 /// 编号 public String Id { get; set; } /// 名称 public String Name { get; set; } /// 超时。默认3000ms public Int32 Timeout { get; set; } = 3_000; /// 服务器地址集合 public NetUri[] Servers { get; set; } /// 配置 public MqBase Config { get; set; } /// 性能跟踪 public ITracer Tracer { get; set; } private ISocketClient _Client; private SerializeType _serializeType = SerializeType.JSON; #endregion #region 构造 /// 实例化 public ClusterClient() { //_Pool = new MyPool { Client = this }; } /// 销毁 /// protected override void Dispose(Boolean disposing) { base.Dispose(disposing); //_Pool.TryDispose(); _Client.TryDispose(); } #endregion #region 方法 /// 开始 public void Start() { using var span = Tracer?.NewSpan($"mq:{Name}:Start", Servers); OnStart(); } /// 开始 protected virtual void OnStart() { WriteLog("集群地址:{0}", Servers.Join(";")); if (Config != null) _serializeType = Config.SerializeType; EnsureCreate(); } /// 确保创建连接 protected void EnsureCreate() { var client = _Client; if (client != null && client.Active && !client.Disposed) return; lock (this) { client = _Client; if (client != null && client.Active && !client.Disposed) return; _Client = null; foreach (var uri in Servers) { WriteLog("正在连接[{0}]", uri); if (uri.Type == NetType.Unknown) uri.Type = NetType.Tcp; client = uri.CreateRemote(); client.Timeout = Timeout; client.Log = Log; if (Log != null && Log.Level <= LogLevel.Debug) client.Tracer = Tracer; client.Add(new MqCodec { Timeout = Timeout }); // 关闭Tcp延迟以合并小包的算法,降低延迟 if (client is TcpSession tcp) { tcp.SslProtocol = Config.SslProtocol; tcp.Certificate = Config.Certificate; tcp.NoDelay = true; } try { if (client.Open()) { client.Received += Client_Received; _Client = client; break; } } catch { } } if (_Client == null) throw new XException("[{0}]集群所有地址[{1}]连接失败!", Name, Servers.Length); } } private Int32 g_id; /// 发送命令 /// /// /// 取消通知 /// protected virtual async Task SendAsync(Command cmd, Boolean waitResult, CancellationToken cancellationToken = default) { if (cmd.Header.Opaque == 0) cmd.Header.Opaque = Interlocked.Increment(ref g_id); if (Log != null && Log.Level <= LogLevel.Debug) WriteLog("=> {0}", cmd); var code = (RequestCode)cmd.Header.Code; using var span = Tracer?.NewSpan($"mq:{Name}:SendAsync:{code}"); // 签名 SetSignature(cmd); EnsureCreate(); var client = _Client; try { if (span is DefaultSpan ds && ds.TraceFlag > 0) { span.AppendTag(cmd); span.AppendTag(cmd.Payload?.ToStr()); } if (waitResult) { var rs = await client.SendMessageAsync(cmd, cancellationToken).ConfigureAwait(false); if (Log != null && Log.Level <= LogLevel.Debug) WriteLog("<= {0}", rs as Command); var result = rs as Command; if (rs != null && span is DefaultSpan ds2 && ds2.TraceFlag > 0) { span.AppendTag(Environment.NewLine); span.AppendTag(rs); span.AppendTag(result?.Payload?.ToStr()); } return result; } else { var row = client.SendMessage(cmd); return new Command { Reply = true, Header = new Header() { Code = (Int32)ResponseCode.SUCCESS } }; } } catch (Exception ex) { // 拉取消息超时,不记录错误日志 if (code == RequestCode.PULL_MESSAGE && ex is TaskCanceledException) span?.AppendTag(ex.Message); else span?.SetError(ex, null); // 销毁,下次使用另一个地址 if (ex is SocketException or IOException) client.TryDispose(); throw; } } private void SetSignature(Command cmd) { // 各云厂商和Apache ACL统一使用HMAC-SHA1签名: // 将扩展字段按ASCII排序拼接值,再加上body,计算HmacSHA1 String accessKey; String secretKey; String onsChannel; // 优先使用新的 CloudProvider 接口 var provider = Config.CloudProvider; if (provider != null && !provider.AccessKey.IsNullOrEmpty()) { accessKey = provider.AccessKey; secretKey = provider.SecretKey; onsChannel = provider.OnsChannel; } else { // 兼容旧版:依次检查 Aliyun / AclOptions #pragma warning disable CS0618 var aliyun = Config.Aliyun; if (aliyun != null && !aliyun.AccessKey.IsNullOrEmpty()) { accessKey = aliyun.AccessKey; secretKey = aliyun.SecretKey; onsChannel = aliyun.OnsChannel; } else { var acl = Config.AclOptions; if (acl == null || acl.AccessKey.IsNullOrEmpty()) return; accessKey = acl.AccessKey; secretKey = acl.SecretKey; onsChannel = acl.OnsChannel; } #pragma warning restore CS0618 } var sha = new HMACSHA1(secretKey.GetBytes()); var ms = new MemoryStream(); var dic = cmd.Header.GetExtFields(); dic["AccessKey"] = accessKey; dic["OnsChannel"] = onsChannel; // 按照 asscii 排序已有 key var comparer = Comparer.Create(string.CompareOrdinal); foreach (var item in dic.OrderBy(e => e.Key, comparer).ToDictionary(e => e.Key, e => e.Value)) { if (item.Value != null) { ms.Write(item.Value.GetBytes()); } } // Body cmd.Payload?.CopyTo(ms); var sign = sha.ComputeHash(ms.ToArray()); dic["Signature"] = sign.ToBase64(); } /// 发送指定类型的命令 /// /// /// /// /// public virtual Command Invoke(RequestCode request, Object body, Object extFields = null, Boolean ignoreError = false) { var cmd = CreateCommand(request, body, extFields); // 避免UI死锁 var rs = SendAsync(cmd, true).ConfigureAwait(false).GetAwaiter().GetResult(); // 判断异常响应 if (!ignoreError && rs.Header != null && rs.Header.Code != 0) throw rs.Header.CreateException(); return rs; } /// 发送指定类型的命令 public virtual async Task InvokeAsync(RequestCode request, Object body, Object extFields = null, Boolean ignoreError = false, CancellationToken cancellationToken = default) { var cmd = CreateCommand(request, body, extFields); var rs = await SendAsync(cmd, true, cancellationToken).ConfigureAwait(false); // 判断异常响应 if (!ignoreError && rs.Header != null && rs.Header.Code != 0) { throw rs.Header.CreateException(); } return rs; } /// 发送指定类型的命令 /// /// /// /// public virtual Command InvokeOneway(RequestCode request, Object body, Object extFields = null) { var cmd = CreateCommand(request, body, extFields); cmd.OneWay = true; // 避免UI死锁 var rs = Task.Run(() => SendAsync(cmd, false)).Result; return rs; } /// 创建命令 /// /// /// /// public virtual Command CreateCommand(RequestCode request, Object body, Object extFields) { var header = new Header { Code = (Int32)request, SerializeTypeCurrentRPC = _serializeType + "", Remark = request + "", }; if (Config != null && Config.Version > 0) header.Version = Config.Version; var cmd = new Command { Header = header, }; // 主体 if (body is IPacket pk) cmd.Payload = pk; else if (body is Byte[] buf) cmd.Payload = (ArrayPacket)buf; else if (body != null) cmd.Payload = (ArrayPacket)Config.JsonHost.Write(body, false, false, false).GetBytes(); if (extFields != null) { var dic = header.GetExtFields(); foreach (var item in extFields.ToDictionary()) { if (item.Value != null && item.Value.GetType() == typeof(Boolean)) dic[item.Key] = item.Value.ToBoolean() ? "true" : "false"; else dic[item.Key] = item.Value + ""; } } OnBuild(header); return cmd; } /// 建立命令时,处理头部 /// protected virtual void OnBuild(Header header) { header.Language = "DOTNET"; } #endregion #region 接收数据 private void Client_Received(Object sender, ReceivedEventArgs e) { if (e.Message is not Command cmd) return; if (cmd.Reply) { //if (cmd.Header != null) _serializeType = cmd.Header.SerializeTypeCurrentRPC.ToEnum(SerializeType.JSON); return; } var rs = OnReceive(cmd); if (rs != null) { var ss = sender as ISocketRemote; ss.SendMessage(rs); } } /// 收到命令时 public event EventHandler> Received; /// 收到命令 /// protected virtual Command OnReceive(Command cmd) { var code = !cmd.Reply ? (RequestCode)cmd.Header.Code + "" : (ResponseCode)cmd.Header.Code + ""; WriteLog("收到:Code={0} {1}", code, cmd.Header.ToJson()); using var span = Tracer?.NewSpan($"mq:{Name}:Receive:{code}", cmd); span?.AppendTag(cmd.Payload?.ToStr()); try { if (Received == null) return null; var e = new EventArgs(cmd); Received.Invoke(this, e); return e.Arg; } catch (Exception ex) { span?.SetError(ex, null); throw; } } #endregion #region 日志 /// 日志 public ILog Log { get; set; } /// 写日志 /// /// public void WriteLog(String format, params Object[] args) => Log?.Info($"[{Name}]{format}", args); #endregion } ================================================ FILE: NewLife.RocketMQ/Common/BrokerInfo.cs ================================================ namespace NewLife.RocketMQ; /// 权限 [Flags] public enum Permissions { /// 写入 Write = 2, /// 读取 Read = 4, } /// 代理信息 public class BrokerInfo { #region 属性 /// 名称 public String Name { get; set; } /// 集群 public String Cluster { get; set; } /// 地址集合 public String[] Addresses { get; set; } /// 主节点地址(BrokerId=0) public String MasterAddress { get; set; } /// 从节点地址集合(BrokerId>0) public String[] SlaveAddresses { get; set; } /// 权限 public Permissions Permission { get; set; } /// 读队列数 public Int32 ReadQueueNums { get; set; } /// 写队列数 public Int32 WriteQueueNums { get; set; } /// 主题同步标记 public Int32 TopicSynFlag { get; set; } /// 是否主节点 public Boolean IsMaster { get; set; } #endregion #region 相等 /// 相等比较 /// /// public override Boolean Equals(Object obj) { var x = this; if (obj is not BrokerInfo y) return false; return x.Name == y.Name && (x.Addresses == y.Addresses || x.Addresses != null && y.Addresses != null && x.Addresses.SequenceEqual(y.Addresses)) && x.Permission == y.Permission && x.TopicSynFlag == y.TopicSynFlag && x.ReadQueueNums == y.ReadQueueNums && x.WriteQueueNums == y.WriteQueueNums; } /// 计算哈希 /// public override Int32 GetHashCode() { var obj = this; return obj.Name.GetHashCode() ^ obj.Addresses.GetHashCode() ^ obj.Permission.GetHashCode() ^ obj.TopicSynFlag ^ obj.ReadQueueNums ^ obj.WriteQueueNums; } #endregion } ================================================ FILE: NewLife.RocketMQ/Common/ILoadBalance.cs ================================================ using System; namespace NewLife.RocketMQ.Common { /// 负载均衡接口 public interface ILoadBalance { /// 已就绪 Boolean Ready { get; set; } /// 设置每个选项的权重数据 /// void Set(Int32[] weights); /// 根据权重选择,并返回该项是第几次选中 /// 第几次选中 /// Int32 Get(out Int32 times); } } ================================================ FILE: NewLife.RocketMQ/Common/WeightRoundRobin.cs ================================================ namespace NewLife.RocketMQ.Common; /// 带权重负载均衡算法 public class WeightRoundRobin : ILoadBalance { #region 属性 /// 已就绪 public Boolean Ready { get; set; } /// 权重集合 public Int32[] Weights { get; private set; } /// 最小权重 private Int32 minWeight; /// 状态值 private Int32[] _states; /// 次数 private Int32[] _times; #endregion #region 方法 /// 设置每个选项的权重数据 /// public void Set(Int32[] weights) { if (weights == null) throw new ArgumentNullException(nameof(weights)); if (Weights != null && Weights.SequenceEqual(weights)) return; Weights = weights; minWeight = weights.Min(); _states = new Int32[weights.Length]; _times = new Int32[weights.Length]; Ready = true; } /// 根据权重选择,并返回该项是第几次选中 /// 第几次选中 /// public Int32 Get(out Int32 times) { times = 1; var ts = _states; if (ts == null) return 0; // 选择状态最大值 var cur = GetMax(ts, out var idx); // 如果所有状态都不达标,则集体加盐 if (cur < minWeight) { for (var i = 0; i < Weights.Length; i++) { ts[i] += Weights[i]; } // 重新选择状态最大值 cur = GetMax(ts, out idx); } // 已选择,减状态 ts[idx] -= minWeight; times = ++_times[idx]; return idx; } /// 根据权重选择 /// public Int32 Get() => Get(out var n); private Int32 GetMax(Int32[] ds, out Int32 idx) { var n = Int32.MinValue; idx = 0; for (var i = 0; i < ds.Length; i++) { if (ds[i] > n) { n = ds[i]; idx = i; } } return n; } #endregion } ================================================ FILE: NewLife.RocketMQ/Consumer.cs ================================================ using System.Reflection; using System.Text; using System.Xml.Serialization; using NewLife.Data; using NewLife.Log; using NewLife.Reflection; using NewLife.RocketMQ.Client; using NewLife.RocketMQ.Models; using NewLife.RocketMQ.Protocol; using NewLife.RocketMQ.Protocol.ConsumerStates; using NewLife.Serialization; using NewLife.Threading; using NewLife.RocketMQ.MessageTrace; namespace NewLife.RocketMQ; /// 消费者 public class Consumer : MqBase { #region 属性 /// 数据 public IList Data { get; set; } /// 标签集合 public String[] Tags { get; set; } /// 多主题订阅列表。设置后一个Consumer可同时消费多个Topic,每个Topic使用相同的Tags和ExpressionType /// /// 设置 Topics 后,原 Topic 属性作为默认主题,Topics 中的所有主题都会被订阅。 /// 如果 Topics 未设置,则保持原有单 Topic 行为不变。 /// public String[] Topics { get; set; } /// 消费挂起超时。每次拉取消息,服务端如果没有消息时的挂起时间,默认15_000ms public Int32 SuspendTimeout { get; set; } = 15_000; /// 客户端拉取消息超时。默认0表示自动使用SuspendTimeout加10_000ms,防止服务端超时后无响应导致消费线程永久阻塞 public Int32 PullTimeout { get; set; } = 0; /// 拉取的批大小。默认32 public Int32 BatchSize { get; set; } = 32; /// 启动时间 private DateTime StartTime { get; set; } = DateTime.Now; /// 首次消费时的消费策略,默认值false,表示从头开始收,等同于Java版的COMSUME_FROM_FIRST_OFFSET public Boolean FromLastOffset { get; set; } = false; /// /// 订阅表达式 TAG /// public String Subscription { get; set; } = "*"; /// 表达式类型。TAG或SQL92,默认TAG。使用SQL92时Subscription填写SQL表达式 public String ExpressionType { get; set; } = "TAG"; /// 启动消费者时自动开始调度。默认true public Boolean AutoSchedule { get; set; } = true; /// 消息模型。广播/集群 public MessageModels MessageModel { get; set; } = MessageModels.Clustering; /// 消费类型。CONSUME_PASSIVELY/CONSUME_ACTIVELY public String ConsumeType { get; set; } = "CONSUME_PASSIVELY"; /// 最大重试次数。默认16次,超过后进入死信队列 public Int32 MaxReconsumeTimes { get; set; } = 16; /// 是否启用消费重试。默认true,消费失败时自动将消息发回Broker的RETRY Topic public Boolean EnableRetry { get; set; } = true; /// 重试延迟等级。默认0表示由Broker根据重试次数决定延迟,大于0时使用指定等级 public Int32 RetryDelayLevel { get; set; } /// 是否顺序消费。启用后消费前自动锁定队列,确保同一时刻只有一个消费者 public Boolean OrderConsume { get; set; } /// 最大并发消费数。0表示不限制,每个队列一个消费线程。大于0时使用信号量控制所有队列的总并发 public Int32 MaxConcurrentConsume { get; set; } /// 消费委托 public Func OnConsume; /// 异步消费委托 public Func> OnConsumeAsync; /// 消费事件 public event EventHandler Consumed; private readonly IList _consumeMessageHooks = new List(); private AsyncTraceDispatcher _traceDispatcher; /// 本地偏移存储路径。广播模式时使用,默认当前目录下的 .offsets/{Group}.json public String OffsetStorePath { get; set; } private SemaphoreSlim _concurrentSemaphore; /// 获取所有有效的订阅主题 private String[] GetEffectiveTopics() { var topics = Topics; if (topics != null && topics.Length > 0) return topics; return [Topic]; } #endregion #region 构造 /// 销毁 /// protected override void Dispose(Boolean disposing) { // 停止并保存偏移 Stop(); base.Dispose(disposing); _source.TryDispose(); _source = null; _timer.TryDispose(); _timer = null; } #endregion #region 方法 /// 启动 /// protected override void OnStart() { var allTopics = GetEffectiveTopics(); WriteLog("正在准备消费 {0}", allTopics.Join(",")); if (EnableMessageTrace) { _traceDispatcher = new AsyncTraceDispatcher(); _traceDispatcher.Start(NameServerAddress); _consumeMessageHooks.Add(new MessageTraceHook(_traceDispatcher)); } var list = Data; if (list == null) { // 为每个Topic建立订阅数据 var sds = allTopics.Select(t => new SubscriptionData { Topic = t, TagsSet = Tags }).ToArray(); var cd = new ConsumerData { GroupName = Group, ConsumeFromWhere = FromLastOffset ? "CONSUME_FROM_LAST_OFFSET" : "CONSUME_FROM_FIRST_OFFSET", MessageModel = MessageModel.ToString().ToUpper(), SubscriptionDataSet = sds, ConsumeType = ConsumeType, }; list = new[] { cd }; Data = list; } base.OnStart(); // 多Topic时,通知NameClient轮询额外主题的路由 if (allTopics.Length > 1 && _NameServer != null) { var extras = allTopics.Where(t => t != Topic).ToArray(); _NameServer.ExtraTopics = extras; // 首次获取额外主题的路由 foreach (var topic in extras) { try { _NameServer.GetRouteInfo(topic); } catch (Exception ex) { WriteLog("获取主题[{0}]路由失败:{1}", topic, ex.Message); } } } // 默认自动开始调度 if (AutoSchedule) StartSchedule(); } /// /// 停止 /// protected override void OnStop() { // 停止并保存偏移 StopSchedule(); PersistAll(_Queues).Wait(); base.OnStop(); } /// 创建Broker客户端,已重载,设置更大的超时时间 /// /// /// protected override BrokerClient CreateBroker(String name, String[] addrs) { var client = base.CreateBroker(name, addrs); if (client.Timeout < SuspendTimeout) client.Timeout = SuspendTimeout; return client; } #endregion #region 拉取消息 /// 从指定队列拉取消息 /// /// /// /// /// 取消通知 /// public async Task Pull(MessageQueue mq, Int64 offset, Int32 maxNums, Int32 msTimeout = -1, CancellationToken cancellationToken = default) { var header = new PullMessageRequestHeader { ConsumerGroup = Group, Topic = mq.Topic ?? Topic, Subscription = Subscription, ExpressionType = ExpressionType, QueueId = mq.QueueId, QueueOffset = offset, MaxMsgNums = maxNums, SysFlag = 6, SubVersion = StartTime.ToLong(), }; if (msTimeout >= 0) header.SuspendTimeoutMillis = msTimeout; var st = _Queues.FirstOrDefault(e => e.Queue == mq); if (st != null) header.CommitOffset = st.CommitOffset; var dic = header.GetProperties(); var bk = GetBroker(mq.BrokerName); var rs = await bk.InvokeAsync(RequestCode.PULL_MESSAGE, null, dic, true, cancellationToken).ConfigureAwait(false); if (rs?.Header == null) return null; var pr = new PullResult(); if (rs.Header.Code == 0) pr.Status = PullStatus.Found; else if (rs.Header.Code == (Int32)ResponseCode.PULL_NOT_FOUND) pr.Status = PullStatus.NoNewMessage; else if (rs.Header.Code == (Int32)ResponseCode.PULL_OFFSET_MOVED || rs.Header.Code == (Int32)ResponseCode.PULL_RETRY_IMMEDIATELY) pr.Status = PullStatus.OffsetIllegal; else { pr.Status = PullStatus.Unknown; Log.Warn("[{0}]{1} 序列编号:{2} 序列偏移量:{3}", (ResponseCode)rs.Header.Code, rs.Header.Remark, mq.QueueId, offset); } pr.Read(rs.Header?.ExtFields); // 读取内容 var pk = rs.Payload; if (pk != null) pr.Messages = MessageExt.ReadAll(pk).ToArray(); return pr; } #endregion #region 业务方法 /// 查询指定队列的偏移量 /// /// 取消通知 /// public async Task QueryOffset(MessageQueue mq, CancellationToken cancellationToken = default) { var bk = GetBroker(mq.BrokerName); var rs = await bk.InvokeAsync(RequestCode.QUERY_CONSUMER_OFFSET, null, new { consumerGroup = Group, topic = mq.Topic ?? Topic, queueId = mq.QueueId, }, true, cancellationToken).ConfigureAwait(false); var dic = rs.Header?.ExtFields; if (dic == null) return -1; return dic.TryGetValue("offset", out var str) ? str.ToLong() : -1; } /// /// 查询“队列”最大偏移量,不是消费提交的最后偏移量 /// /// /// 取消通知 /// public async Task QueryMaxOffset(MessageQueue mq, CancellationToken cancellationToken = default) { var bk = GetBroker(mq.BrokerName); var rs = await bk.InvokeAsync(RequestCode.GET_MAX_OFFSET, null, new { consumerGroup = Group, topic = mq.Topic ?? Topic, queueId = mq.QueueId, }, true, cancellationToken).ConfigureAwait(false); var dic = rs.Header?.ExtFields; if (dic == null) return -1; return dic.TryGetValue("offset", out var str) ? str.ToLong() : -1; } /// /// 获取最小偏移量 /// /// /// 取消通知 /// public async Task QueryMinOffset(MessageQueue mq, CancellationToken cancellationToken = default) { var bk = GetBroker(mq.BrokerName); var rs = await bk.InvokeAsync(RequestCode.GET_MIN_OFFSET, null, new { consumerGroup = Group, topic = mq.Topic ?? Topic, queueId = mq.QueueId, }, true, cancellationToken).ConfigureAwait(false); var dic = rs.Header?.ExtFields; if (dic == null) return -1; return dic.TryGetValue("offset", out var str) ? str.ToLong() : -1; } /// 根据时间戳查询偏移 /// 队列 /// 时间戳(毫秒) /// 取消通知 /// public async Task SearchOffset(MessageQueue mq, Int64 timestamp, CancellationToken cancellationToken = default) { var bk = GetBroker(mq.BrokerName); var rs = await bk.InvokeAsync(RequestCode.SEARCH_OFFSET_BY_TIMESTAMP, null, new { topic = mq.Topic ?? Topic, queueId = mq.QueueId, timestamp, }, true, cancellationToken).ConfigureAwait(false); var dic = rs.Header?.ExtFields; if (dic == null) return -1; return dic.TryGetValue("offset", out var str) ? str.ToLong() : -1; } /// 更新队列的偏移 /// /// /// 取消通知 /// public async Task UpdateOffset(MessageQueue mq, Int64 commitOffset, CancellationToken cancellationToken = default) { var bk = GetBroker(mq.BrokerName); var rs = await bk.InvokeAsync(RequestCode.UPDATE_CONSUMER_OFFSET, null, new { commitOffset, consumerGroup = Group, queueId = mq.QueueId, topic = mq.Topic ?? Topic, }, false, cancellationToken).ConfigureAwait(false); //var dic = rs?.Header?.ExtFields; //if (dic == null) return false; return true; } /// 获取消费者下所有消费者 /// public async Task> GetConsumers(String group = null) { if (group.IsNullOrEmpty()) group = Group; var header = new { consumerGroup = group, }; var cs = new HashSet(); // 在所有Broker上查询 foreach (var item in Brokers) { using var span = Tracer?.NewSpan($"mq:{Name}:GetConsumers", item.Name); try { var bk = GetBroker(item.Name); //bk.Ping(); var rs = await bk.InvokeAsync(RequestCode.GET_CONSUMER_LIST_BY_GROUP, null, header).ConfigureAwait(false); span?.AppendTag(rs.Payload?.ToStr()); //WriteLog(rs.Header.ExtFields?.ToJson()); var js = rs.ReadBodyAsJson(); if (js != null && js["consumerIdList"] is IList list) { foreach (String clientId in list) { if (!cs.Contains(clientId)) cs.Add(clientId); } } } catch (Exception ex) { if (ex is not ResponseException) span?.SetError(ex, null); //XTrace.WriteException(ex); WriteLog(ex.GetTrue().Message); } } return cs; } /// 获取消费者连接列表 /// 消费组名 /// public async Task> GetConsumerConnectionList(String group = null) { if (group.IsNullOrEmpty()) group = Group; foreach (var item in Brokers) { using var span = Tracer?.NewSpan($"mq:{Name}:GetConsumerConnectionList", item.Name); try { var bk = GetBroker(item.Name); var rs = await bk.InvokeAsync(RequestCode.GET_CONSUMER_CONNECTION_LIST, null, new { consumerGroup = group }).ConfigureAwait(false); if (rs?.Payload != null) return rs.ReadBodyAsJson(); } catch (Exception ex) { span?.SetError(ex, null); } } return null; } /// 重置消费偏移 /// 时间戳(毫秒),将偏移重置到该时间点 /// 消费组名 /// 取消通知 /// public async Task ResetConsumerOffset(Int64 timestamp, String group = null, CancellationToken cancellationToken = default) { if (group.IsNullOrEmpty()) group = Group; using var span = Tracer?.NewSpan($"mq:{Name}:ResetConsumerOffset", timestamp); try { foreach (var item in Brokers) { var bk = GetBroker(item.Name); await bk.InvokeAsync(RequestCode.INVOKE_BROKER_TO_RESET_OFFSET, null, new { topic = Topic, group, timestamp, isForce = false, }, true, cancellationToken).ConfigureAwait(false); } return true; } catch (Exception ex) { span?.SetError(ex, null); WriteLog("重置消费偏移失败:{0}", ex.Message); return false; } } #endregion #region 顺序消费锁定 /// 批量锁定队列。顺序消费时确保同一队列同一时刻只有一个消费者 /// 待锁定的队列集合 /// 取消通知 /// 成功锁定的队列集合 public async Task> LockBatchMQAsync(IList mqs, CancellationToken cancellationToken = default) { if (mqs == null || mqs.Count == 0) return []; var locked = new List(); var groups = mqs.GroupBy(e => e.BrokerName); foreach (var g in groups) { using var span = Tracer?.NewSpan($"mq:{Name}:LockBatchMQ", g.Key); try { var bk = GetBroker(g.Key); if (bk == null) continue; var body = new { consumerGroup = Group, clientId = ClientId, mqSet = g.Select(e => new { topic = e.Topic, brokerName = e.BrokerName, queueId = e.QueueId }).ToArray(), }; var rs = await bk.InvokeAsync(RequestCode.LOCK_BATCH_MQ, body, null, true, cancellationToken).ConfigureAwait(false); if (rs?.Payload != null) { var js = rs.ReadBodyAsJson(); if (js != null && js["lockOKMQSet"] is IList list) { foreach (IDictionary item in list) { locked.Add(new MessageQueue { Topic = item["topic"] + "", BrokerName = item["brokerName"] + "", QueueId = item["queueId"].ToInt(), }); } } } } catch (Exception ex) { span?.SetError(ex, null); WriteLog("锁定队列失败[{0}]:{1}", g.Key, ex.Message); } } return locked; } /// 批量解锁队列 /// 待解锁的队列集合 /// 取消通知 /// public async Task UnlockBatchMQAsync(IList mqs, CancellationToken cancellationToken = default) { if (mqs == null || mqs.Count == 0) return; var groups = mqs.GroupBy(e => e.BrokerName); foreach (var g in groups) { using var span = Tracer?.NewSpan($"mq:{Name}:UnlockBatchMQ", g.Key); try { var bk = GetBroker(g.Key); if (bk == null) continue; var body = new { consumerGroup = Group, clientId = ClientId, mqSet = g.Select(e => new { topic = e.Topic, brokerName = e.BrokerName, queueId = e.QueueId }).ToArray(), }; await bk.InvokeAsync(RequestCode.UNLOCK_BATCH_MQ, body, null, false, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { span?.SetError(ex, null); WriteLog("解锁队列失败[{0}]:{1}", g.Key, ex.Message); } } } #endregion #region 消费调度 private Task[] _tasks; private volatile Int32 _version; private CancellationTokenSource _source; /// 开始调度 public void StartSchedule() { if (_timer != null) return; lock (this) { if (_timer != null) return; // 快速检查消费组,均衡成功后改为30秒一次 _timer = new TimerX(CheckGroup, null, 100, 1_000) { Async = true }; } } private void DoSchedule() { var qs = _Queues; if (qs == null || qs.Length == 0) return; Interlocked.Increment(ref _version); // 关线程 //Stop(); StopSchedule(); // 如果有多个消费者,则等一段时间让大家停止消费,尽量避免重复消费 //if (_Consumers != null && _Consumers.Length > 1) Thread.Sleep(10_000); // 释放资源 if (_source != null) { _source.Cancel(); _source.TryDispose(); } var source = new CancellationTokenSource(); WriteLog("正在创建[{0}]个消费线程,Group={1},Topics={2}", qs.Length, Group, GetEffectiveTopics().Join(",")); // 初始化并发限流信号量 if (MaxConcurrentConsume > 0) { _concurrentSemaphore = new SemaphoreSlim(MaxConcurrentConsume, MaxConcurrentConsume); WriteLog("消费限流已启用,最大并发数={0}", MaxConcurrentConsume); } else { _concurrentSemaphore = null; } // 开线程 var tasks = new Task[qs.Length]; for (var i = 0; i < qs.Length; i++) { var queueStore = qs[i]; tasks[i] = Task.Factory.StartNew(async () => { await DoPull(queueStore, source.Token).ConfigureAwait(false); }, TaskCreationOptions.LongRunning); } _tasks = tasks; _source = source; } /// 停止 public void StopSchedule() { var ts = _tasks; if (ts != null && ts.Length > 0) { WriteLog("停止调度线程[{0}]", ts.Length); // 预留一点退出时间 Interlocked.Increment(ref _version); // 释放资源 if (_source != null) { _source.Cancel(); _source.TryDispose(); _source = null; } var timeout = TimeSpan.FromSeconds(3 * _tasks.Length); try { Task.WaitAll(_tasks, timeout); } catch { // 理论上不会遇到异常 // 但等待过程可能会遇到积压的 Task 异常,统统吃掉,从业务上也没有需要捕获的需要 } _tasks = null; } } private async Task DoPull(QueueStore st, CancellationToken cancellationToken) { var mq = st.Queue; WriteLog("开始消费[{0}],Group={1},{2},Offset={3},CommitOffset={4}", mq.Topic ?? Topic, Group, mq, st.Offset, st.CommitOffset); // 顺序消费时先锁定队列 if (OrderConsume) { var locked = await LockBatchMQAsync([mq], cancellationToken).ConfigureAwait(false); if (locked.Count == 0) { WriteLog("顺序消费锁定队列失败[{0}],跳过消费", mq); return; } } var currentVersion = _version; while (currentVersion == _version && !cancellationToken.IsCancellationRequested) { DefaultSpan.Current = null; try { var offset = st.Offset; // 客户端超时保护:防止服务端在SuspendTimeout后无响应导致消费线程永久阻塞 var pullTimeout = PullTimeout > 0 ? PullTimeout : SuspendTimeout + 10_000; using var pullCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); pullCts.CancelAfter(pullTimeout); var pr = await Pull(mq, offset, BatchSize, SuspendTimeout, pullCts.Token).ConfigureAwait(false); if (pr != null) { switch (pr.Status) { case PullStatus.Found: if (pr.Messages != null && pr.Messages.Length > 0) { DefaultSpan.Current = null; // 限流:等待信号量 if (_concurrentSemaphore != null) await _concurrentSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); // 性能埋点 using var span = Tracer?.NewSpan($"mq:{Name}:Consume", pr.Messages); try { // 触发消费 var rs = await Consume(mq, pr, cancellationToken).ConfigureAwait(false); // 更新偏移 if (rs) { st.Offset = pr.NextBeginOffset; // 提交消费进度 await UpdateOffset(mq, st.Offset, cancellationToken).ConfigureAwait(false); } } catch (Exception ex) { span?.SetError(ex, mq); throw; } finally { _concurrentSemaphore?.Release(); } } break; case PullStatus.NoNewMessage: break; case PullStatus.NoMatchedMessage: break; case PullStatus.OffsetIllegal: if (pr.NextBeginOffset >= 0) { WriteLog("无效的offset,可能历史消息已过期 [{0}@{1}] Offset={2:n0}, NextOffset={3:n0}", mq.BrokerName, mq.QueueId, st.Offset, pr.NextBeginOffset); st.Offset = pr.NextBeginOffset; } break; case PullStatus.Unknown: Log.Error("未知响应类型消息,序列[{1}]偏移量{0}", st.Offset, st.Queue.QueueId); break; default: break; } } } catch (ThreadAbortException) { break; } catch (ThreadInterruptedException) { break; } catch (TaskCanceledException) { } catch (AggregateException) { } catch (Exception ex) { Log?.Error(ex.GetMessage()); // 出现其他异常的情况下,等待一会,防止出现大量异常 await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); } } // 保存消费进度 if (st.Offset >= 0 && st.Offset != st.CommitOffset) { var rs = await UpdateOffset(mq, st.Offset, cancellationToken).ConfigureAwait(false); st.CommitOffset = st.Offset; } // 顺序消费结束时解锁队列 if (OrderConsume) { await UnlockBatchMQAsync([mq], cancellationToken).ConfigureAwait(false); } WriteLog("消费[{0}]结束", mq.Topic ?? Topic); } /// 拉取到一批消息 /// /// /// 取消通知 /// protected virtual async Task Consume(MessageQueue queue, PullResult result, CancellationToken cancellationToken) { if (Log != null && Log.Level <= LogLevel.Debug) WriteLog("{0}", result); var context = new ConsumeMessageContext { ConsumerGroup = Group, Mq = queue, MsgList = result.Messages.ToList(), }; foreach (var hook in _consumeMessageHooks) { try { hook.ExecuteHookBefore(context); } catch (Exception e) { if (Log.Enable) Log.Error(e.Message); } } Consumed?.Invoke(this, new ConsumeEventArgs { Queue = queue, Messages = result.Messages, Result = result }); var success = false; if (OnConsume != null) success = OnConsume(queue, result.Messages); if (OnConsumeAsync != null) success = await OnConsumeAsync(queue, result.Messages, cancellationToken).ConfigureAwait(false); // 消费失败且启用了重试时,将消息回退到RETRY Topic if (!success && EnableRetry && result.Messages != null) { foreach (var msg in result.Messages) { if (msg.ReconsumeTimes < MaxReconsumeTimes) { try { await SendMessageBackAsync(msg, RetryDelayLevel, MaxReconsumeTimes, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { WriteLog("消息回退失败[{0}]:{1}", msg.MsgId, ex.Message); } } else { // 超过最大重试次数,消息将进入死信队列 %DLQ%{ConsumerGroup} WriteLog("消息[{0}]超过最大重试次数[{1}],将进入死信队列", msg.MsgId, MaxReconsumeTimes); } } } context.Success = success; foreach (var hook in _consumeMessageHooks) { try { hook.ExecuteHookAfter(context); } catch (Exception e) { if (Log.Enable) Log.Error(e.Message); } } return success; } private async Task PersistAll(IEnumerable stores) { if (stores == null) return; // 广播模式保存到本地文件 if (MessageModel == MessageModels.Broadcasting) { SaveLocalOffsets(stores.ToArray()); return; } var ts = new List(); using var source = new CancellationTokenSource(5_000); foreach (var item in stores) { if (item.Offset >= 0 && item.Offset != item.CommitOffset) { var mq = item.Queue; WriteLog("队列[{0}@{1}]更新偏移[{2:n0}]", mq.BrokerName, mq.QueueId, item.Offset); ts.Add(Task.Run(() => UpdateOffset(item.Queue, item.Offset, source.Token))); item.CommitOffset = item.Offset; } } await Task.WhenAll(ts).ConfigureAwait(false); } #endregion #region 消费端负载均衡 /// 当前所需要消费的队列。由均衡算法产生 public MessageQueue[] Queues => _Queues?.Select(e => e.Queue).ToArray(); private QueueStore[] _Queues; //private String[] _Consumers; class QueueStore { [XmlIgnore] public MessageQueue Queue { get; set; } public Int64 Offset { get; set; } = -1; public Int64 CommitOffset { get; set; } = -1; #region 相等 /// 相等比较 /// /// public override Boolean Equals(Object obj) => obj is QueueStore y && Equals(Queue, y.Queue) && Offset == y.Offset; /// 计算哈希 /// public override Int32 GetHashCode() => (Queue == null ? 0 : Queue.GetHashCode()) ^ Offset.GetHashCode(); public override String ToString() => Queue?.ToString(); #endregion } /// 重新平衡消费队列 /// public async Task Rebalance() { /* * 1,获取消费组下所有消费组,排序 * 2,获取主题下所有队列,排序 * 3,各消费者平均分配队列,不采用环形,减少消费者到Broker连接数 */ if (_Queues == null) WriteLog("准备从所有Broker服务器上获取消费者列表,以确定当前消费者应该负责消费的queue分片"); var cs = await GetConsumers(Group).ConfigureAwait(false); if (cs.Count == 0) return false; // 为所有订阅主题构建队列列表,按Topic分别分配 var allTopics = GetEffectiveTopics(); var qs = new List(); foreach (var topic in allTopics) { // 获取该Topic对应的Broker信息 var topicBrokers = _NameServer?.GetTopicBrokers(topic) ?? Brokers; if (topicBrokers == null) continue; foreach (var br in topicBrokers) { if (br.Permission.HasFlag(Permissions.Read)) { for (var i = 0; i < br.ReadQueueNums; i++) { qs.Add(new MessageQueue { Topic = topic, BrokerName = br.Name, QueueId = i, }); } } } } var cs2 = cs.OrderBy(e => e).ToList(); if (_Queues == null) WriteLog("消费者列表[{0}]:{1}", cs2.Count, cs2.Join()); // 集群模式需要分配Queue,而广播模式不需要 if (MessageModel == MessageModels.Clustering) { // 排序,计算索引。如果当前节点不在消费者列表里,则跳过 var cid = ClientId; var idx = cs2.IndexOf(cid); if (idx < 0 || idx >= cs2.Count) return false; // 先分糖,每人多少个。你一个我一个,一圈又一圈的分。 // 如果无法均分,前面的消费者会比后面的消费者多拿一个,并且最多只会多一个 var ds = new Int32[cs2.Count]; for (Int32 i = 0, k = 0; i < qs.Count; i++) { ds[k++]++; if (k >= ds.Length) k = 0; } // 我的前面分了多少,就是前面的桶内数字之和 var start = ds.Take(idx).Sum(); // 跳过前面,取我的糖 qs = qs.Skip(start).Take(ds[idx]).ToList(); } var rs = new List(); foreach (var item in qs) { rs.Add(new QueueStore { Queue = item }); } // 如果序列相等则返回false var ori = _Queues; if (ori != null) { var q1 = ori.Select(e => e.Queue).ToArray(); var q2 = rs.Select(e => e.Queue).ToArray(); if (q1.SequenceEqual(q2)) return false; await PersistAll(ori).ConfigureAwait(false); } var dic = qs.GroupBy(e => e.BrokerName).ToDictionary(e => e.Key, e => e.Join(",", x => x.QueueId)); var str = dic.Join(";", e => $"{e.Key}[{e.Value}]"); WriteLog("消费重新平衡,当前消费者负责queue分片:{0}", str); using var span = Tracer?.NewSpan($"mq:{Name}:Rebalance", str); _Queues = rs.ToArray(); await InitOffsetAsync().ConfigureAwait(false); //_Consumers = cs2.ToArray(); return true; } private TimerX _timer; private DateTime _nextCheck; private Boolean _checking; /// 检查消费组,如果消费者有变化,则需要重新平衡,重新分配各消费者所处理的队列 /// private async Task CheckGroup(Object state = null) { if (_checking) return; // 避免多次平衡同时进行 var now = TimerX.Now; if (now < _nextCheck) return; //lock (this) //{ if (_checking) return; _checking = true; using var span = Tracer?.NewSpan($"mq:{Name}:CheckGroup"); try { var rs = await Rebalance().ConfigureAwait(false); if (!rs) return; if (AutoSchedule) DoSchedule(); if (_timer != null) _timer.Period = 60_000; _nextCheck = now.AddSeconds(3); } catch (Exception ex) { span?.SetError(ex, null); XTrace.WriteException(ex); } finally { _checking = false; } //} } private async Task InitOffsetAsync(CancellationToken cancellationToken = default) { var qs = _Queues; if (qs == null || qs.Length == 0) return; // 广播模式优先从本地加载偏移 if (MessageModel == MessageModels.Broadcasting) { var localOffsets = LoadLocalOffsets(); foreach (var store in qs) { if (store.Offset >= 0) continue; var key = $"{store.Queue.Topic ?? Topic}@{store.Queue.BrokerName}@{store.Queue.QueueId}"; // 兼容旧格式(不含Topic前缀) if (!localOffsets.TryGetValue(key, out var offset) || offset < 0) { var oldKey = $"{store.Queue.BrokerName}@{store.Queue.QueueId}"; localOffsets.TryGetValue(oldKey, out offset); } if (offset >= 0) { store.Offset = store.CommitOffset = offset; WriteLog("从本地加载offset[{0}@{1}] Offset={2:n0}", store.Queue.BrokerName, store.Queue.QueueId, store.Offset); } else { // 本地没有,从服务端查询 var off = FromLastOffset ? await QueryMaxOffset(store.Queue, cancellationToken).ConfigureAwait(false) : await QueryMinOffset(store.Queue, cancellationToken).ConfigureAwait(false); store.Offset = store.CommitOffset = off; WriteLog("初始化offset[{0}@{1}] Offset={2:n0}(广播模式)", store.Queue.BrokerName, store.Queue.QueueId, store.Offset); } } return; } var offsetTables = new Dictionary(); // 获取当前消费者分配到的服务器及服务器队列,按Topic+Broker分组查询 var topicBrokerGroups = qs.Select(t => new { t.Queue.Topic, t.Queue.BrokerName }) .Distinct() .GroupBy(t => t.Topic); foreach (var topicGroup in topicBrokerGroups) { var queryTopic = topicGroup.Key ?? Topic; foreach (var tb in topicGroup) { var broker = GetBroker(tb.BrokerName); var command = await broker.InvokeAsync(RequestCode.GET_CONSUME_STATS, null, new { consumerGroup = Group, topic = queryTopic }, true, cancellationToken).ConfigureAwait(false); var consumerStates = ConsumerStatesSpecialJsonHandler(command.Payload); //foreach (var (key, value) in consumerStates.OffsetTable) offsetTables.Add(key, value); foreach (var item in consumerStates.OffsetTable) { // 避免重复添加(同一个Broker上不同Topic的key可能相同) if (!offsetTables.ContainsKey(item.Key)) offsetTables.Add(item.Key, item.Value); } } } // 表示没消费过 var neverConsumed = offsetTables.All(t => t.Value.ConsumerOffset == 0); foreach (var store in qs) { if (store.Offset >= 0) continue; // 先按BrokerName精确匹配,如果找不到(阿里云公网版BrokerName可能不一致),则按QueueId模糊匹配 var item = offsetTables.FirstOrDefault(t => t.Key.BrokerName == store.Queue.BrokerName && t.Key.QueueId == store.Queue.QueueId); if (item.Value == null) { // 阿里云公网版RocketMQ,消费者状态返回的是真正brokerName,而前面Broker得到的是网关名 item = offsetTables.FirstOrDefault(t => t.Key.QueueId == store.Queue.QueueId); } var offsetTable = item.Value ?? new OffsetWrapperModel(); if (neverConsumed) { var offset = 0L; if (FromLastOffset) { offset = offsetTable.BrokerOffset; if (offset <= 0) offset = await QueryMaxOffset(store.Queue, cancellationToken).ConfigureAwait(false); } else { offset = await QueryMinOffset(store.Queue, cancellationToken).ConfigureAwait(false); } store.Offset = store.CommitOffset = offset; await UpdateOffset(store.Queue, offset, cancellationToken).ConfigureAwait(false); } else { var offset = offsetTable.ConsumerOffset; if (offset <= 0) offset = await QueryOffset(store.Queue, cancellationToken).ConfigureAwait(false); store.Offset = store.CommitOffset = offset; } WriteLog("初始化offset[{0}@{1}] Offset={2:n0}", store.Queue.BrokerName, store.Queue.QueueId, store.Offset); } } /// /// 消费者状态信息特殊Json处理 /// /// 负载数据 /// private ConsumerStatesModel ConsumerStatesSpecialJsonHandler(IPacket payload) { #region // Apache RocketMQ 获取Consumer_States返回结果 // 返回消息格式不是正常的JSON需要特殊处理 // { // "consumeTps":0.0, // "offsetTable":{ // {"brokerName":"wh-sr-11-26","queueId":0,"topic":"mip_topic_0"}:{"brokerOffset":5,"consumerOffset":35,"lastTimestamp":0}, // {"brokerName":"wh-sr-11-26","queueId":1,"topic":"mip_topic_0"}:{"brokerOffset":5,"consumerOffset":35,"lastTimestamp":0} // } // } // 阿里版本 RocketMQ 获取Consumer_States返回结果 // 返回消息格式不是正常的JSON需要特殊处理 // { // "consumeTps":0.0, // "offsetTable":{ // { "brokerName":"cn-qingdao-public-share-05-2","mainQueue":false,"queueGroupId":-1,"queueId":5,"topic":"mip_topic_0"}: // { "brokerOffset":4,"consumerOffset":4,"earliestUnPulledTimestamp":0,"earliestUnconsumedTimestamp":0, // "inFlightMsgCountEstimatedAccumulation":0,"lagEstimatedAccumulation":0,"lastTimestamp":1647746454641,"pullOffset":4} // } // } #endregion var cmdStr = payload.ToStr(); cmdStr = cmdStr[1..^1]; var indexOf = cmdStr.IndexOf('{') + 1; var lastIndexOf = cmdStr.LastIndexOf('}'); cmdStr = cmdStr[indexOf..lastIndexOf]; var offsetArr = cmdStr.Split('}'); var offsetNew = (from offset in offsetArr where !String.IsNullOrWhiteSpace(offset) select String.Concat(offset.Trim(',').Trim(':'), "}")).ToList(); var consumerStatesModel = new ConsumerStatesModel() { OffsetTable = [] }; for (var i = 0; i < offsetNew.Count / 2; i++) { var list = offsetNew.Skip(i * 2).Take(2).ToList(); var messageQueue = list[0].ToJsonEntity(); var offsetWrapper = list[1].ToJsonEntity(); consumerStatesModel.OffsetTable.Add(messageQueue, offsetWrapper); } if (consumerStatesModel.OffsetTable.Count == 0) { WriteLog("无法解析消费者状态,可能是服务端版本不兼容,响应如下:"); WriteLog(cmdStr); } return consumerStatesModel; } /// 获取本地偏移存储路径 private String GetOffsetFilePath() { var path = OffsetStorePath; if (String.IsNullOrEmpty(path)) path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ".offsets", $"{Group}.json"); return path; } /// 从本地文件加载偏移。广播模式使用 private Dictionary LoadLocalOffsets() { var file = GetOffsetFilePath(); if (!File.Exists(file)) return []; try { var json = File.ReadAllText(file); if (String.IsNullOrWhiteSpace(json)) return []; return JsonHelper.Default.Read>(json) ?? []; } catch { return []; } } /// 保存偏移到本地文件。广播模式使用 private void SaveLocalOffsets(QueueStore[] stores) { if (stores == null || stores.Length == 0) return; var dic = new Dictionary(); foreach (var st in stores) { if (st.Offset >= 0) { var key = $"{st.Queue.Topic ?? Topic}@{st.Queue.BrokerName}@{st.Queue.QueueId}"; dic[key] = st.Offset; } } var file = GetOffsetFilePath(); var dir = Path.GetDirectoryName(file); if (!String.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); File.WriteAllText(file, JsonHelper.Default.Write(dic)); } #endregion #region 下行指令 /// 收到命令 /// protected override Command OnReceive(Command cmd) { if (!cmd.Reply) { var code = (RequestCode)cmd.Header.Code; switch (code) { case RequestCode.NOTIFY_CONSUMER_IDS_CHANGED: NotifyConsumerIdsChanged(cmd); break; case RequestCode.RESET_CONSUMER_CLIENT_OFFSET: ResetOffset(cmd); break; case RequestCode.GET_CONSUMER_STATUS_FROM_CLIENT: GetConsumeStatus(cmd); break; case RequestCode.GET_CONSUMER_RUNNING_INFO: return GetConsumerRunningInfo(cmd); default: break; } } return null; } private void NotifyConsumerIdsChanged(Command cmd) => _timer?.SetNext(-1); private void ResetOffset(Command cmd) { var js = cmd.Payload?.ToStr(); if (js.IsNullOrEmpty()) return; // 请求内容是一个奇怪的Json,Key是MessageQueue对象,Value是偏移量 var ss = js.Split(",{"); foreach (var item in ss) { var name = item.Substring("\"brokerName\":", ",").Trim('\"'); var qid = item.Substring("\"queueId\":", ",").ToInt(); var offset = item.TrimEnd('}').Substring("}:", null).ToLong(); var mq = _Queues.FirstOrDefault(e => e.Queue.BrokerName == name & e.Queue.QueueId == qid); if (mq != null) mq.Offset = offset; } } private void GetConsumeStatus(Command cmd) { } private Command GetConsumerRunningInfo(Command cmd) { var ci = new ConsumerRunningInfo(); var dic = new Dictionary(); foreach (var pi in GetType().GetProperties()) { if (pi.DeclaringType == typeof(DisposeBase)) continue; if (pi.PropertyType.GetTypeCode() == TypeCode.Object) continue; var val = pi.GetValue(this, null); if (val != null) dic[pi.Name] = val + ""; } var asm = Assembly.GetExecutingAssembly(); dic["PROP_CLIENT_VERSION"] = asm.GetName().Version + ""; dic["PROP_CONSUMEORDERLY"] = OrderConsume ? "true" : "false"; dic["PROP_CONSUMER_START_TIMESTAMP"] = StartTime.ToInt() + ""; dic["PROP_CONSUME_TYPE"] = "CONSUME_PASSIVELY"; dic["PROP_NAMESERVER_ADDR"] = NameServerAddress; dic["PROP_THREADPOOL_CORE_SIZE"] = (_tasks?.Length ?? 1).ToString(); dic["messageModel"] = "CLUSTERING"; ci.Properties = dic; var sd = new SubscriptionData { Topic = Topic, }; ci.SubscriptionSet = GetEffectiveTopics().Select(t => new SubscriptionData { Topic = t }).ToArray(); var sb = new StringBuilder(); sb.Append('{'); if (_Queues != null) { sb.Append("\"mqTable\":{"); for (var i = 0; i < _Queues.Length; i++) { if (i > 0) sb.Append(','); var item = _Queues[i]; sb.Append(JsonHost.Write(item.Queue, false, false, true)); sb.Append(':'); sb.Append(JsonHost.Write(item, false, false, true)); } sb.Append('}'); } { sb.Append(','); sb.Append("\"properties\":"); sb.Append(JsonHost.Write(ci.Properties)); } { sb.Append(','); sb.Append("\"subscriptionSet\":"); sb.Append(JsonHost.Write(ci.SubscriptionSet, false, false, true)); } sb.Append('}'); var rs = cmd.CreateReply() as Command; rs.Header.Language = "DOTNET"; rs.Payload = (ArrayPacket)sb.ToString().GetBytes(); return rs; } #endregion #region 消费重试 /// 将消费失败的消息发回Broker,进入RETRY Topic延迟后重新消费 /// 消费失败的消息 /// 延迟等级。0=由Broker决定,大于0指定等级(1~18) /// 最大重试次数。-1表示使用默认值 /// 取消通知 /// 是否成功 public async Task SendMessageBackAsync(MessageExt msg, Int32 delayLevel = 0, Int32 maxReconsumeTimes = -1, CancellationToken cancellationToken = default) { if (msg == null) throw new ArgumentNullException(nameof(msg)); using var span = Tracer?.NewSpan($"mq:{Name}:SendMessageBack", msg.MsgId); try { var bk = GetBroker(msg.StoreHost?.Split(':').FirstOrDefault() ?? Brokers?.FirstOrDefault()?.Name); if (bk == null) { // 尝试在所有Broker上发送 bk = Clients?.FirstOrDefault(); } if (bk == null) return false; var header = new { group = Group, offset = msg.CommitLogOffset, delayLevel = delayLevel > 0 ? delayLevel : RetryDelayLevel, originMsgId = msg.MsgId ?? "", originTopic = msg.Topic ?? Topic, unitMode = UnitMode, maxReconsumeTimes = maxReconsumeTimes >= 0 ? maxReconsumeTimes : MaxReconsumeTimes, }; await bk.InvokeAsync(RequestCode.CONSUMER_SEND_MSG_BACK, null, header, true, cancellationToken).ConfigureAwait(false); return true; } catch (Exception ex) { span?.SetError(ex, null); WriteLog("消息回退失败:{0}", ex.Message); return false; } } /// 将消费失败的消息发回Broker,进入RETRY Topic延迟后重新消费 /// 消费失败的消息 /// 延迟等级 /// 最大重试次数 /// 是否成功 public Boolean SendMessageBack(MessageExt msg, Int32 delayLevel = 0, Int32 maxReconsumeTimes = -1) => SendMessageBackAsync(msg, delayLevel, maxReconsumeTimes).ConfigureAwait(false).GetAwaiter().GetResult(); #endregion #region Pop消费模式 /// Pop方式拉取消息。5.0新增的轻量消费模式,无需客户端Rebalance /// Broker名称 /// 队列编号。-1表示由Broker自动分配 /// 最大拉取数 /// 不可见时间(毫秒),消息被拉取后在此时间内不会被其他消费者看到 /// 长轮询等待时间(毫秒) /// 取消通知 /// 拉取结果 public async Task PopMessageAsync(String brokerName, Int32 queueId = -1, Int32 maxNums = 32, Int64 invisibleTime = 60_000, Int32 pollTime = 15_000, CancellationToken cancellationToken = default) { if (String.IsNullOrEmpty(brokerName)) throw new ArgumentNullException(nameof(brokerName)); using var span = Tracer?.NewSpan($"mq:{Name}:PopMessage", brokerName); try { var bk = GetBroker(brokerName); if (bk == null) return null; var header = new { consumerGroup = Group, topic = Topic, queueId, maxMsgNums = maxNums, invisibleTime, pollTime, bornTime = DateTime.Now.ToLong(), initMode = FromLastOffset ? 1 : 0, expType = ExpressionType, exp = Subscription, }; var rs = await bk.InvokeAsync(RequestCode.POP_MESSAGE, null, header, true, cancellationToken).ConfigureAwait(false); if (rs?.Header == null) return null; var pr = new PullResult(); if (rs.Header.Code == 0) pr.Status = PullStatus.Found; else if (rs.Header.Code == (Int32)ResponseCode.PULL_NOT_FOUND) pr.Status = PullStatus.NoNewMessage; else pr.Status = PullStatus.Unknown; pr.Read(rs.Header?.ExtFields); if (rs.Payload != null) pr.Messages = MessageExt.ReadAll(rs.Payload).ToArray(); return pr; } catch (Exception ex) { span?.SetError(ex, null); throw; } } /// 确认Pop消息消费完成 /// Broker名称 /// Pop检查点信息,即消息属性中的POP_CK字段值 /// 消息在Queue中的偏移量 /// 队列编号 /// 取消通知 /// public async Task AckMessageAsync(String brokerName, String extraInfo, Int64 offset, Int32 queueId = -1, CancellationToken cancellationToken = default) { using var span = Tracer?.NewSpan($"mq:{Name}:AckMessage", offset); try { var bk = GetBroker(brokerName); if (bk == null) return false; var header = new { consumerGroup = Group, topic = Topic, extraInfo, offset, queueId, }; await bk.InvokeAsync(RequestCode.ACK_MESSAGE, null, header, true, cancellationToken).ConfigureAwait(false); return true; } catch (Exception ex) { span?.SetError(ex, null); WriteLog("确认Pop消息失败:{0}", ex.Message); return false; } } /// 确认Pop消息消费完成。自动从消息属性中提取Pop检查点信息(POP_CK) /// Broker名称 /// 通过Pop方式拉取的消息 /// 取消通知 /// public Task AckMessageAsync(String brokerName, MessageExt msg, CancellationToken cancellationToken = default) { if (msg == null) throw new ArgumentNullException(nameof(msg)); if (String.IsNullOrEmpty(msg.PopCheckPoint)) throw new ArgumentException("消息不含Pop检查点信息(POP_CK属性缺失),请确认该消息是通过Pop方式拉取的。", nameof(msg)); return AckMessageAsync(brokerName, msg.PopCheckPoint, msg.QueueOffset, msg.QueueId, cancellationToken); } /// 修改Pop消息不可见时间,延长消费处理窗口 /// Broker名称 /// Pop检查点信息,即消息属性中的POP_CK字段值 /// 消息在Queue中的偏移量 /// 新的不可见时间(毫秒) /// 队列编号 /// 取消通知 /// public async Task ChangeInvisibleTimeAsync(String brokerName, String extraInfo, Int64 offset, Int64 invisibleTime, Int32 queueId = -1, CancellationToken cancellationToken = default) { using var span = Tracer?.NewSpan($"mq:{Name}:ChangeInvisibleTime", offset); try { var bk = GetBroker(brokerName); if (bk == null) return false; var header = new { consumerGroup = Group, topic = Topic, extraInfo, offset, invisibleTime, queueId, }; await bk.InvokeAsync(RequestCode.CHANGE_MESSAGE_INVISIBLETIME, null, header, true, cancellationToken).ConfigureAwait(false); return true; } catch (Exception ex) { span?.SetError(ex, null); WriteLog("修改不可见时间失败:{0}", ex.Message); return false; } } /// 修改Pop消息不可见时间,延长消费处理窗口。自动从消息属性中提取Pop检查点信息(POP_CK) /// Broker名称 /// 通过Pop方式拉取的消息 /// 新的不可见时间(毫秒) /// 取消通知 /// public Task ChangeInvisibleTimeAsync(String brokerName, MessageExt msg, Int64 invisibleTime, CancellationToken cancellationToken = default) { if (msg == null) throw new ArgumentNullException(nameof(msg)); if (String.IsNullOrEmpty(msg.PopCheckPoint)) throw new ArgumentException("消息不含Pop检查点信息(POP_CK属性缺失),请确认该消息是通过Pop方式拉取的。", nameof(msg)); return ChangeInvisibleTimeAsync(brokerName, msg.PopCheckPoint, msg.QueueOffset, invisibleTime, msg.QueueId, cancellationToken); } /// 批量确认Pop消息消费完成 /// Broker名称 /// 批量确认条目列表,每个条目包含extraInfo和offset /// 取消通知 /// 成功确认的数量 public async Task BatchAckMessageAsync(String brokerName, IList<(String extraInfo, Int64 offset)> ackEntries, CancellationToken cancellationToken = default) { if (String.IsNullOrEmpty(brokerName)) throw new ArgumentNullException(nameof(brokerName)); if (ackEntries == null || ackEntries.Count == 0) return 0; using var span = Tracer?.NewSpan($"mq:{Name}:BatchAckMessage", ackEntries.Count); try { var bk = GetBroker(brokerName); if (bk == null) return 0; // 构造批量确认请求体:JSON数组格式 var entries = ackEntries.Select(e => new { extraInfo = e.extraInfo, offset = e.offset }).ToArray(); var body = JsonHost.Write(entries, false, false, false).GetBytes(); var header = new { consumerGroup = Group, topic = Topic, }; await bk.InvokeAsync(RequestCode.BATCH_ACK_MESSAGE, body, header, true, cancellationToken).ConfigureAwait(false); return ackEntries.Count; } catch (Exception ex) { span?.SetError(ex, null); WriteLog("批量确认Pop消息失败:{0}", ex.Message); return 0; } } #endregion #if NETSTANDARD2_1_OR_GREATER #region gRPC消费模式 /// 通过gRPC协议接收消息。需要先设置GrpcProxyAddress /// gRPC消息队列 /// 过滤表达式。默认Tag=* /// 批量大小。默认32 /// 不可见时间。默认30秒 /// 长轮询超时。默认20秒 /// 取消通知 /// gRPC消息列表 public async Task> ReceiveMessageViaGrpcAsync( Grpc.GrpcMessageQueue queue, Grpc.GrpcFilterExpression filterExpression = null, Int32 batchSize = 32, TimeSpan? invisibleDuration = null, TimeSpan? longPollingTimeout = null, CancellationToken cancellationToken = default) { if (_GrpcService == null) throw new InvalidOperationException("gRPC service not initialized. Set GrpcProxyAddress first."); using var span = Tracer?.NewSpan($"mq:{Name}:ReceiveMessage:grpc"); try { return await _GrpcService.ReceiveMessageAsync( Group, queue, filterExpression, batchSize, invisibleDuration, longPollingTimeout, cancellationToken ).ConfigureAwait(false); } catch (Exception ex) { span?.SetError(ex, null); throw; } } /// 通过gRPC协议确认消息 /// 主题 /// 确认条目列表 /// 取消通知 /// 确认结果 public async Task AckMessageViaGrpcAsync( String topic, IList entries, CancellationToken cancellationToken = default) { if (_GrpcService == null) throw new InvalidOperationException("gRPC service not initialized. Set GrpcProxyAddress first."); using var span = Tracer?.NewSpan($"mq:{Name}:AckMessage:grpc"); try { return await _GrpcService.AckMessageAsync(topic, Group, entries, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { span?.SetError(ex, null); throw; } } /// 通过gRPC协议查询队列分配 /// 主题。默认使用当前Topic /// 取消通知 /// 分配结果 public async Task QueryAssignmentViaGrpcAsync(String topic = null, CancellationToken cancellationToken = default) { if (_GrpcService == null) throw new InvalidOperationException("gRPC service not initialized. Set GrpcProxyAddress first."); return await _GrpcService.QueryAssignmentAsync(topic ?? Topic, Group, cancellationToken).ConfigureAwait(false); } /// 通过gRPC协议修改消息不可见时间 /// 主题 /// 收据句柄 /// 消息ID /// 新的不可见时间 /// 取消通知 /// public async Task ChangeInvisibleDurationViaGrpcAsync( String topic, String receiptHandle, String messageId, TimeSpan invisibleDuration, CancellationToken cancellationToken = default) { if (_GrpcService == null) throw new InvalidOperationException("gRPC service not initialized. Set GrpcProxyAddress first."); return await _GrpcService.ChangeInvisibleDurationAsync(topic, Group, receiptHandle, messageId, invisibleDuration, cancellationToken).ConfigureAwait(false); } /// 通过gRPC协议发送心跳 /// 取消通知 /// public async Task HeartbeatViaGrpcAsync(CancellationToken cancellationToken = default) { if (_GrpcService == null) throw new InvalidOperationException("gRPC service not initialized. Set GrpcProxyAddress first."); return await _GrpcService.HeartbeatAsync(Group, Grpc.GrpcClientType.SIMPLE_CONSUMER, cancellationToken).ConfigureAwait(false); } #endregion #endif #region Request-Reply 请求响应模式 /// 发送回复消息 /// 原始请求消息 /// 回复消息体 /// 发送结果 public virtual SendResult SendReply(MessageExt requestMessage, Object replyBody) { if (requestMessage == null) throw new ArgumentNullException(nameof(requestMessage)); if (replyBody == null) throw new ArgumentNullException(nameof(replyBody)); // 检查是否有回复地址 var replyToClient = requestMessage.ReplyToClient; if (String.IsNullOrEmpty(replyToClient)) { throw new InvalidOperationException("Request message does not have ReplyToClient property"); } var correlationId = requestMessage.CorrelationId; if (String.IsNullOrEmpty(correlationId)) { throw new InvalidOperationException("Request message does not have CorrelationId property"); } // 创建回复消息 var replyMessage = new Message { Topic = requestMessage.Topic, CorrelationId = correlationId, MessageType = "REPLY" }; replyMessage.SetBody(replyBody); // 使用临时Producer发送回复 using var producer = new Producer { NameServerAddress = NameServerAddress, Topic = requestMessage.Topic, Group = Group + "_REPLY", ClientIP = ClientIP, InstanceName = InstanceName, Log = Log, Tracer = Tracer }; producer.Start(); return producer.Publish(replyMessage); } /// 发送回复消息(异步) /// 原始请求消息 /// 回复消息体 /// 取消令牌 /// 发送结果 public virtual async Task SendReplyAsync(MessageExt requestMessage, Object replyBody, CancellationToken cancellationToken = default) { if (requestMessage == null) throw new ArgumentNullException(nameof(requestMessage)); if (replyBody == null) throw new ArgumentNullException(nameof(replyBody)); // 检查是否有回复地址 var replyToClient = requestMessage.ReplyToClient; if (String.IsNullOrEmpty(replyToClient)) { throw new InvalidOperationException("Request message does not have ReplyToClient property"); } var correlationId = requestMessage.CorrelationId; if (String.IsNullOrEmpty(correlationId)) { throw new InvalidOperationException("Request message does not have CorrelationId property"); } // 创建回复消息 var replyMessage = new Message { Topic = requestMessage.Topic, CorrelationId = correlationId, MessageType = "REPLY" }; replyMessage.SetBody(replyBody); // 使用临时Producer发送回复 using var producer = new Producer { NameServerAddress = NameServerAddress, Topic = requestMessage.Topic, Group = Group + "_REPLY", ClientIP = ClientIP, InstanceName = InstanceName, Log = Log, Tracer = Tracer }; producer.Start(); return await producer.PublishAsync(replyMessage, null, cancellationToken).ConfigureAwait(false); } #endregion } ================================================ FILE: NewLife.RocketMQ/Grpc/GrpcClient.cs ================================================ #if NETSTANDARD2_1_OR_GREATER using System.Net; using System.Net.Http; using System.Net.Http.Headers; using NewLife.Log; namespace NewLife.RocketMQ.Grpc; /// 轻量级gRPC客户端。基于HttpClient HTTP/2实现gRPC协议 /// /// gRPC协议格式: /// - 传输层: HTTP/2 /// - 请求: POST /package.Service/Method /// - 内容类型: application/grpc /// - 消息帧: [1字节压缩标志][4字节大端序长度][Protobuf消息体] /// /// 不依赖任何gRPC包,完全使用 HttpClient + 手工帧编码实现。 /// public class GrpcClient : IDisposable { #region 属性 /// 目标地址。格式如 http://host:port public String Address { get; set; } /// 超时时间(毫秒)。默认30000 public Int32 Timeout { get; set; } = 30_000; /// 访问令牌。用于认证 public String AccessKey { get; set; } /// 访问密钥 public String SecretKey { get; set; } /// 客户端标识 public String ClientId { get; set; } /// 命名空间 public String Namespace { get; set; } /// 日志 public ILog Log { get; set; } = Logger.Null; /// 性能追踪 public ITracer Tracer { get; set; } private HttpClient _client; #endregion #region 构造 /// 实例化gRPC客户端 public GrpcClient() { } /// 实例化gRPC客户端 /// 目标地址 public GrpcClient(String address) => Address = address; /// 释放 public void Dispose() { _client?.Dispose(); _client = null; } #endregion #region 方法 /// 确保HttpClient已创建 private HttpClient EnsureClient() { if (_client != null) return _client; lock (this) { if (_client != null) return _client; var handler = new HttpClientHandler { // gRPC通常不需要验证服务端证书(内网) ServerCertificateCustomValidationCallback = (_, _, _, _) => true, }; var client = new HttpClient(handler) { BaseAddress = new Uri(Address), Timeout = TimeSpan.FromMilliseconds(Timeout), }; _client = client; return client; } } /// Unary调用。发送一个请求,接收一个响应 /// 服务名。如 apache.rocketmq.v2.MessagingService /// 方法名。如 SendMessage /// 请求消息 /// 取消通知 /// 响应数据 public async Task UnaryCallAsync(String service, String method, Byte[] request, CancellationToken cancellationToken = default) { var client = EnsureClient(); var path = $"/{service}/{method}"; using var span = Tracer?.NewSpan($"grpc:{method}", path); try { // 构造gRPC帧:[1字节压缩标志=0][4字节大端序长度][数据] var frame = FrameEncode(request); var httpRequest = new HttpRequestMessage(HttpMethod.Post, path); httpRequest.Version = new Version(2, 0); // gRPC 必需的 headers httpRequest.Content = new ByteArrayContent(frame); httpRequest.Content.Headers.ContentType = new MediaTypeHeaderValue("application/grpc"); httpRequest.Headers.Add("te", "trailers"); // 认证和元数据 SetMetadata(httpRequest); var response = await client.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); // 读取响应 var responseData = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); // 检查 grpc-status CheckGrpcStatus(response); // 解码gRPC帧 return FrameDecode(responseData); } catch (Exception ex) { span?.SetError(ex, null); throw; } } /// Server Streaming调用。发送一个请求,流式接收多个响应 /// 服务名 /// 方法名 /// 请求消息 /// 取消通知 /// 响应数据流 public async IAsyncEnumerable ServerStreamingCallAsync(String service, String method, Byte[] request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) { var client = EnsureClient(); var path = $"/{service}/{method}"; using var span = Tracer?.NewSpan($"grpc:{method}:stream", path); try { var frame = FrameEncode(request); var httpRequest = new HttpRequestMessage(HttpMethod.Post, path); httpRequest.Version = new Version(2, 0); httpRequest.Content = new ByteArrayContent(frame); httpRequest.Content.Headers.ContentType = new MediaTypeHeaderValue("application/grpc"); httpRequest.Headers.Add("te", "trailers"); SetMetadata(httpRequest); var response = await client.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); // 流式读取多个gRPC帧 var headerBuf = new Byte[5]; while (!cancellationToken.IsCancellationRequested) { // 读取5字节帧头 var read = await ReadFullAsync(stream, headerBuf, 0, 5, cancellationToken).ConfigureAwait(false); if (read < 5) break; // 解析帧头 var compressed = headerBuf[0] != 0; var length = (Int32)((headerBuf[1] << 24) | (headerBuf[2] << 16) | (headerBuf[3] << 8) | headerBuf[4]); if (length <= 0) continue; // 读取消息体 var body = new Byte[length]; read = await ReadFullAsync(stream, body, 0, length, cancellationToken).ConfigureAwait(false); if (read < length) break; yield return body; } } finally { span?.Dispose(); } } #endregion #region 辅助 /// 设置gRPC元数据(认证等) /// 请求 private void SetMetadata(HttpRequestMessage request) { if (!String.IsNullOrEmpty(ClientId)) request.Headers.TryAddWithoutValidation("x-mq-client-id", ClientId); if (!String.IsNullOrEmpty(Namespace)) request.Headers.TryAddWithoutValidation("x-mq-namespace", Namespace); // RocketMQ 5.x gRPC 使用 authorization header if (!String.IsNullOrEmpty(AccessKey) && !String.IsNullOrEmpty(SecretKey)) { var timestamp = DateTime.UtcNow.ToString("yyyyMMddTHHmmssZ"); request.Headers.TryAddWithoutValidation("x-mq-date-time", timestamp); request.Headers.TryAddWithoutValidation("authorization", $"MQv2-HMAC-SHA1 Credential={AccessKey}, SignedHeaders=x-mq-date-time, Signature={ComputeSignature(timestamp)}"); } request.Headers.TryAddWithoutValidation("x-mq-language", "DOTNET"); request.Headers.TryAddWithoutValidation("x-mq-protocol", "grpc"); } /// 计算签名 /// 时间戳 /// private String ComputeSignature(String dateTime) { using var hmac = new System.Security.Cryptography.HMACSHA1(System.Text.Encoding.UTF8.GetBytes(SecretKey)); var data = System.Text.Encoding.UTF8.GetBytes(dateTime); return Convert.ToBase64String(hmac.ComputeHash(data)); } /// gRPC帧编码。[1字节压缩标志][4字节大端序长度][数据] /// 数据 /// public static Byte[] FrameEncode(Byte[] data) { var len = data?.Length ?? 0; var frame = new Byte[5 + len]; frame[0] = 0; // 不压缩 frame[1] = (Byte)(len >> 24); frame[2] = (Byte)(len >> 16); frame[3] = (Byte)(len >> 8); frame[4] = (Byte)len; if (len > 0) Buffer.BlockCopy(data, 0, frame, 5, len); return frame; } /// gRPC帧解码。跳过5字节帧头返回消息体 /// 帧数据 /// public static Byte[] FrameDecode(Byte[] frame) { if (frame == null || frame.Length < 5) return []; var length = (frame[1] << 24) | (frame[2] << 16) | (frame[3] << 8) | frame[4]; if (length <= 0 || frame.Length < 5 + length) return []; var data = new Byte[length]; Buffer.BlockCopy(frame, 5, data, 0, length); return data; } /// 检查gRPC响应状态 /// HTTP响应 private static void CheckGrpcStatus(HttpResponseMessage response) { // grpc-status 可能在 trailer 或 header 中 var grpcStatus = -1; String grpcMessage = null; // TrailingHeaders 在 .NET 5+ 可用,低版本 try-catch 处理 try { #if NET5_0_OR_GREATER if (response.TrailingHeaders.TryGetValues("grpc-status", out var statusValues)) { var statusStr = statusValues.FirstOrDefault(); if (statusStr != null) Int32.TryParse(statusStr, out grpcStatus); } if (response.TrailingHeaders.TryGetValues("grpc-message", out var msgValues)) { grpcMessage = msgValues.FirstOrDefault(); } #endif } catch { } // 也检查普通 header(有些服务器在 header 中返回) if (grpcStatus < 0 && response.Headers.TryGetValues("grpc-status", out var headerStatusValues)) { var statusStr = headerStatusValues.FirstOrDefault(); if (statusStr != null) Int32.TryParse(statusStr, out grpcStatus); } // grpc-status: 0 = OK if (grpcStatus > 0) { throw new InvalidOperationException($"gRPC error {grpcStatus}: {grpcMessage ?? "Unknown error"}"); } } /// 确保从流中读取指定长度的数据 private static async Task ReadFullAsync(Stream stream, Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken) { var totalRead = 0; while (totalRead < count) { var read = await stream.ReadAsync(buffer, offset + totalRead, count - totalRead, cancellationToken).ConfigureAwait(false); if (read == 0) break; totalRead += read; } return totalRead; } #endregion } #endif ================================================ FILE: NewLife.RocketMQ/Grpc/GrpcEnums.cs ================================================ namespace NewLife.RocketMQ.Grpc; /// gRPC响应状态码 /// 参考 apache.rocketmq.v2.Code public enum GrpcCode { /// 成功 OK = 20000, /// 多个结果 MULTIPLE_RESULTS = 30000, /// 错误请求 BAD_REQUEST = 40000, /// 非法访问点 ILLEGAL_ACCESS_POINT = 40001, /// 非法主题 ILLEGAL_TOPIC = 40002, /// 非法消费组 ILLEGAL_CONSUMER_GROUP = 40003, /// 非法消息标签 ILLEGAL_MESSAGE_TAG = 40004, /// 非法消息Key ILLEGAL_MESSAGE_KEY = 40005, /// 非法消息分组 ILLEGAL_MESSAGE_GROUP = 40006, /// 非法消息属性Key ILLEGAL_MESSAGE_PROPERTY_KEY = 40007, /// 无效事务ID INVALID_TRANSACTION_ID = 40008, /// 非法消息ID ILLEGAL_MESSAGE_ID = 40009, /// 非法过滤表达式 ILLEGAL_FILTER_EXPRESSION = 40010, /// 非法不可见时间 ILLEGAL_INVISIBLE_TIME = 40011, /// 非法投递时间 ILLEGAL_DELIVERY_TIME = 40012, /// 无效收据句柄 INVALID_RECEIPT_HANDLE = 40013, /// 消息属性与类型冲突 MESSAGE_PROPERTY_CONFLICT_WITH_TYPE = 40014, /// 未认证 UNAUTHORIZED = 40100, /// 禁止 FORBIDDEN = 40300, /// 未找到 NOT_FOUND = 40400, /// 消息未找到 MESSAGE_NOT_FOUND = 40401, /// 主题未找到 TOPIC_NOT_FOUND = 40402, /// 消费组未找到 CONSUMER_GROUP_NOT_FOUND = 40403, /// 请求超时 REQUEST_TIMEOUT = 40800, /// 消息体过大 PAYLOAD_TOO_LARGE = 41300, /// 前置条件失败 PRECONDITION_FAILED = 42800, /// 请求过于频繁 TOO_MANY_REQUESTS = 42900, /// 内部错误 INTERNAL_ERROR = 50000, /// 内部服务器错误 INTERNAL_SERVER_ERROR = 50001, /// 不支持 UNSUPPORTED = 50100, /// 消费消息失败 FAILED_TO_CONSUME_MESSAGE = 60000, } /// 消息类型 public enum GrpcMessageType { /// 普通消息 NORMAL = 0, /// 顺序消息(FIFO) FIFO = 1, /// 延迟消息 DELAY = 2, /// 事务消息 TRANSACTION = 3, } /// 客户端类型 public enum GrpcClientType { /// 生产者 PRODUCER = 1, /// 推送消费者 PUSH_CONSUMER = 2, /// 简单消费者 SIMPLE_CONSUMER = 3, /// Pull消费者 PULL_CONSUMER = 4, } /// 地址类型 public enum AddressScheme { /// IPv4 IPv4 = 0, /// IPv6 IPv6 = 1, /// 域名 DOMAIN_NAME = 2, } /// 队列权限 public enum GrpcPermission { /// 无权限 NONE = 0, /// 只读 READ = 1, /// 只写 WRITE = 2, /// 读写 READ_WRITE = 3, } /// 过滤类型 public enum GrpcFilterType { /// Tag过滤 TAG = 1, /// SQL过滤 SQL = 2, } /// 摘要类型 public enum GrpcDigestType { /// CRC32 CRC32 = 0, /// MD5 MD5 = 1, /// SHA1 SHA1 = 2, } /// 消息编码 public enum GrpcEncoding { /// 不压缩 IDENTITY = 0, /// GZIP压缩 GZIP = 1, } /// 事务来源 public enum GrpcTransactionSource { /// 服务端回查 SOURCE_SERVER_CHECK = 0, /// 客户端提交 SOURCE_CLIENT = 1, } /// 事务决议 public enum GrpcTransactionResolution { /// 提交 COMMIT = 0, /// 回滚 ROLLBACK = 1, } ================================================ FILE: NewLife.RocketMQ/Grpc/GrpcMessagingService.cs ================================================ #if NETSTANDARD2_1_OR_GREATER using NewLife.Buffers; using NewLife.Log; using NewLife.Serialization; namespace NewLife.RocketMQ.Grpc; /// RocketMQ 5.x gRPC 消息服务客户端 /// /// 封装 apache.rocketmq.v2.MessagingService 的 gRPC 调用, /// 提供路由查询、消息发送/接收、确认、事务、心跳等功能。 /// /// 对应 RocketMQ 5.x Proxy 的 gRPC API。 /// public class GrpcMessagingService : IDisposable { #region 常量 private const String ServiceName = "apache.rocketmq.v2.MessagingService"; #endregion #region 属性 /// gRPC客户端 public GrpcClient Client { get; set; } /// 命名空间 public String Namespace { get; set; } /// 日志 public ILog Log { get; set; } = Logger.Null; /// 性能追踪 public ITracer Tracer { get; set; } #endregion #region 构造 /// 实例化gRPC消息服务客户端 public GrpcMessagingService() { } /// 实例化gRPC消息服务客户端 /// Proxy地址。如 http://host:8081 public GrpcMessagingService(String address) { Client = new GrpcClient(address) { Log = Log, Tracer = Tracer }; } /// 释放 public void Dispose() => Client?.Dispose(); #endregion #region 路由 /// 查询主题路由 /// 主题名 /// 取消通知 /// 路由信息(消息队列列表) public async Task QueryRouteAsync(String topic, CancellationToken cancellationToken = default) { var request = new QueryRouteRequest { Topic = new GrpcResource { ResourceNamespace = Namespace, Name = topic }, }; return await InvokeAsync("QueryRoute", request, cancellationToken).ConfigureAwait(false); } #endregion #region 发送消息 /// 发送消息 /// 主题 /// 消息体 /// 标签 /// 消息Key列表 /// 用户属性 /// 消息分组(FIFO消息) /// 定时投递时间(延迟消息) /// 取消通知 /// 发送结果 public async Task SendMessageAsync( String topic, Byte[] body, String tag = null, IList keys = null, IDictionary properties = null, String messageGroup = null, DateTime? deliveryTimestamp = null, CancellationToken cancellationToken = default) { var sysProps = new GrpcSystemProperties { Tag = tag, MessageType = GrpcMessageType.NORMAL, BornTimestamp = DateTime.UtcNow, BornHost = Environment.MachineName, }; if (keys != null && keys.Count > 0) sysProps.Keys = new List(keys); // 根据参数判断消息类型 if (!String.IsNullOrEmpty(messageGroup)) { sysProps.MessageType = GrpcMessageType.FIFO; sysProps.MessageGroup = messageGroup; } else if (deliveryTimestamp != null) { sysProps.MessageType = GrpcMessageType.DELAY; sysProps.DeliveryTimestamp = deliveryTimestamp; } var msg = new GrpcMessage { Topic = new GrpcResource { ResourceNamespace = Namespace, Name = topic }, SystemProperties = sysProps, Body = body, }; if (properties != null) { foreach (var kv in properties) { msg.UserProperties[kv.Key] = kv.Value; } } var request = new SendMessageRequest(); request.Messages.Add(msg); return await InvokeAsync("SendMessage", request, cancellationToken).ConfigureAwait(false); } /// 发送事务消息(半消息) /// 主题 /// 消息体 /// 标签 /// 消息Key列表 /// 取消通知 /// 发送结果 public async Task SendTransactionMessageAsync( String topic, Byte[] body, String tag = null, IList keys = null, CancellationToken cancellationToken = default) { var sysProps = new GrpcSystemProperties { Tag = tag, MessageType = GrpcMessageType.TRANSACTION, BornTimestamp = DateTime.UtcNow, BornHost = Environment.MachineName, }; if (keys != null && keys.Count > 0) sysProps.Keys = new List(keys); var msg = new GrpcMessage { Topic = new GrpcResource { ResourceNamespace = Namespace, Name = topic }, SystemProperties = sysProps, Body = body, }; var request = new SendMessageRequest(); request.Messages.Add(msg); return await InvokeAsync("SendMessage", request, cancellationToken).ConfigureAwait(false); } #endregion #region 接收消息 /// 查询队列分配 /// 主题 /// 消费组 /// 取消通知 /// 分配结果 public async Task QueryAssignmentAsync(String topic, String group, CancellationToken cancellationToken = default) { var request = new QueryAssignmentRequest { Topic = new GrpcResource { ResourceNamespace = Namespace, Name = topic }, Group = new GrpcResource { ResourceNamespace = Namespace, Name = group }, }; return await InvokeAsync("QueryAssignment", request, cancellationToken).ConfigureAwait(false); } /// 接收消息(Server Streaming) /// 消费组 /// 消息队列 /// 过滤表达式 /// 批量大小 /// 不可见时间 /// 长轮询超时 /// 取消通知 /// 消息列表 public async Task> ReceiveMessageAsync( String group, GrpcMessageQueue queue, GrpcFilterExpression filterExpression = null, Int32 batchSize = 32, TimeSpan? invisibleDuration = null, TimeSpan? longPollingTimeout = null, CancellationToken cancellationToken = default) { var request = new ReceiveMessageRequest { Group = new GrpcResource { ResourceNamespace = Namespace, Name = group }, MessageQueue = queue, FilterExpression = filterExpression ?? new GrpcFilterExpression { Type = GrpcFilterType.TAG, Expression = "*" }, BatchSize = batchSize, InvisibleDuration = invisibleDuration ?? TimeSpan.FromSeconds(30), LongPollingTimeout = longPollingTimeout ?? TimeSpan.FromSeconds(20), }; var requestData = ProtoExtensions.Serialize(request); var messages = new List(); await foreach (var data in Client.ServerStreamingCallAsync(ServiceName, "ReceiveMessage", requestData, cancellationToken).ConfigureAwait(false)) { var response = new ReceiveMessageResponse(); var reader = new SpanReader(data); response.Read(ref reader); // 检查状态 if (response.Status != null && response.Status.Code != GrpcCode.OK) { // 消息未找到不算错误 if (response.Status.Code == GrpcCode.MESSAGE_NOT_FOUND) break; throw new InvalidOperationException($"ReceiveMessage error: {response.Status}"); } if (response.Message != null) messages.Add(response.Message); } return messages; } #endregion #region 确认消息 /// 确认消息 /// 主题 /// 消费组 /// 确认条目列表 /// 取消通知 /// 确认结果 public async Task AckMessageAsync( String topic, String group, IList entries, CancellationToken cancellationToken = default) { var request = new AckMessageRequest { Group = new GrpcResource { ResourceNamespace = Namespace, Name = group }, Topic = new GrpcResource { ResourceNamespace = Namespace, Name = topic }, Entries = new List(entries), }; return await InvokeAsync("AckMessage", request, cancellationToken).ConfigureAwait(false); } #endregion #region 心跳 /// 发送心跳 /// 消费组 /// 客户端类型 /// 取消通知 /// 心跳结果 public async Task HeartbeatAsync(String group, GrpcClientType clientType, CancellationToken cancellationToken = default) { var request = new HeartbeatRequest { Group = new GrpcResource { ResourceNamespace = Namespace, Name = group }, ClientType = clientType, }; return await InvokeAsync("Heartbeat", request, cancellationToken).ConfigureAwait(false); } #endregion #region 事务 /// 结束事务 /// 主题 /// 消息ID /// 事务ID /// 事务决议 /// 取消通知 /// 结束事务结果 public async Task EndTransactionAsync( String topic, String messageId, String transactionId, GrpcTransactionResolution resolution, CancellationToken cancellationToken = default) { var request = new GrpcEndTransactionRequest { Topic = new GrpcResource { ResourceNamespace = Namespace, Name = topic }, MessageId = messageId, TransactionId = transactionId, Source = GrpcTransactionSource.SOURCE_CLIENT, Resolution = resolution, }; return await InvokeAsync("EndTransaction", request, cancellationToken).ConfigureAwait(false); } #endregion #region 死信队列 /// 转发消息到死信队列 /// 主题 /// 消费组 /// 收据句柄 /// 消息ID /// 当前投递尝试次数 /// 最大投递尝试次数 /// 取消通知 /// public async Task ForwardToDeadLetterQueueAsync( String topic, String group, String receiptHandle, String messageId, Int32 deliveryAttempt, Int32 maxDeliveryAttempts, CancellationToken cancellationToken = default) { var request = new ForwardMessageToDeadLetterQueueRequest { Group = new GrpcResource { ResourceNamespace = Namespace, Name = group }, Topic = new GrpcResource { ResourceNamespace = Namespace, Name = topic }, ReceiptHandle = receiptHandle, MessageId = messageId, DeliveryAttempt = deliveryAttempt, MaxDeliveryAttempts = maxDeliveryAttempts, }; return await InvokeAsync( "ForwardMessageToDeadLetterQueue", request, cancellationToken).ConfigureAwait(false); } #endregion #region 修改不可见时间 /// 修改消息不可见时间 /// 主题 /// 消费组 /// 收据句柄 /// 消息ID /// 新的不可见时间 /// 取消通知 /// public async Task ChangeInvisibleDurationAsync( String topic, String group, String receiptHandle, String messageId, TimeSpan invisibleDuration, CancellationToken cancellationToken = default) { var request = new ChangeInvisibleDurationRequest { Group = new GrpcResource { ResourceNamespace = Namespace, Name = group }, Topic = new GrpcResource { ResourceNamespace = Namespace, Name = topic }, ReceiptHandle = receiptHandle, MessageId = messageId, InvisibleDuration = invisibleDuration, }; return await InvokeAsync( "ChangeInvisibleDuration", request, cancellationToken).ConfigureAwait(false); } #endregion #region 通知终止 /// 通知服务端客户端即将终止 /// 消费组 /// 取消通知 /// public async Task NotifyClientTerminationAsync(String group = null, CancellationToken cancellationToken = default) { var request = new NotifyClientTerminationRequest(); if (!String.IsNullOrEmpty(group)) request.Group = new GrpcResource { ResourceNamespace = Namespace, Name = group }; return await InvokeAsync( "NotifyClientTermination", request, cancellationToken).ConfigureAwait(false); } #endregion #region 客户端资源上报 /// 上报客户端设置(Telemetry)。向Proxy上报客户端资源信息,包括设置、主题订阅等 /// 客户端设置 /// 取消通知 /// 服务端返回的Telemetry命令 public async Task TelemetryAsync(GrpcSettings settings, CancellationToken cancellationToken = default) { var request = new TelemetryCommand { Settings = settings }; return await InvokeAsync("Telemetry", request, cancellationToken).ConfigureAwait(false); } #endregion #region 辅助 /// 通用Unary调用封装 /// 请求类型 /// 响应类型 /// 方法名 /// 请求消息 /// 取消通知 /// 响应消息 private async Task InvokeAsync(String method, TRequest request, CancellationToken cancellationToken) where TRequest : ISpanSerializable where TResponse : ISpanSerializable, new() { var requestData = ProtoExtensions.Serialize(request); var responseData = await Client.UnaryCallAsync(ServiceName, method, requestData, cancellationToken).ConfigureAwait(false); var response = new TResponse(); if (responseData != null && responseData.Length > 0) { var reader = new SpanReader(responseData); response.Read(ref reader); } return response; } #endregion } #endif ================================================ FILE: NewLife.RocketMQ/Grpc/GrpcModels.cs ================================================ using NewLife.Buffers; using NewLife.Serialization; namespace NewLife.RocketMQ.Grpc; /// gRPC状态 public class GrpcStatus : ISpanSerializable { /// 状态码 public GrpcCode Code { get; set; } /// 状态消息 public String Message { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteEnum(1, (Int32)Code); writer.WriteString(2, Message); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Code = (GrpcCode)reader.ReadEnum(); break; case 2: Message = reader.ReadProtoString(); break; default: reader.SkipField(wt); break; } } } /// 已重载 public override String ToString() => $"{Code} {Message}"; } /// 资源描述(Topic/Group) public class GrpcResource : ISpanSerializable { /// 命名空间 public String ResourceNamespace { get; set; } /// 名称 public String Name { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteString(1, ResourceNamespace); writer.WriteString(2, Name); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: ResourceNamespace = reader.ReadProtoString(); break; case 2: Name = reader.ReadProtoString(); break; default: reader.SkipField(wt); break; } } } /// 已重载 public override String ToString() => String.IsNullOrEmpty(ResourceNamespace) ? Name : $"{ResourceNamespace}%{Name}"; } /// 地址 public class GrpcAddress : ISpanSerializable { /// 主机 public String Host { get; set; } /// 端口 public Int32 Port { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteString(1, Host); writer.WriteInt32(2, Port); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Host = reader.ReadProtoString(); break; case 2: Port = reader.ReadProtoInt32(); break; default: reader.SkipField(wt); break; } } } /// 已重载 public override String ToString() => $"{Host}:{Port}"; } /// 端点集合 public class GrpcEndpoints : ISpanSerializable { /// 地址类型 public AddressScheme Scheme { get; set; } /// 地址列表 public List Addresses { get; set; } = []; /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteEnum(1, (Int32)Scheme); writer.WriteRepeatedMessage(2, Addresses); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Scheme = (AddressScheme)reader.ReadEnum(); break; case 2: Addresses.Add(reader.ReadProtoMessage()); break; default: reader.SkipField(wt); break; } } } } /// Broker信息 public class GrpcBroker : ISpanSerializable { /// Broker名称 public String Name { get; set; } /// Broker ID public Int32 Id { get; set; } /// 端点 public GrpcEndpoints Endpoints { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteString(1, Name); writer.WriteInt32(2, Id); writer.WriteMessage(3, Endpoints); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Name = reader.ReadProtoString(); break; case 2: Id = reader.ReadProtoInt32(); break; case 3: Endpoints = reader.ReadProtoMessage(); break; default: reader.SkipField(wt); break; } } } /// 已重载 public override String ToString() => $"{Name}#{Id}"; } /// gRPC消息队列 public class GrpcMessageQueue : ISpanSerializable { /// 主题 public GrpcResource Topic { get; set; } /// 队列ID public Int32 Id { get; set; } /// 权限 public GrpcPermission Permission { get; set; } /// Broker public GrpcBroker Broker { get; set; } /// 接受的消息类型 public List AcceptMessageTypes { get; set; } = []; /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Topic); writer.WriteInt32(2, Id); writer.WriteEnum(3, (Int32)Permission); writer.WriteMessage(4, Broker); if (AcceptMessageTypes.Count > 0) { var enums = new List(AcceptMessageTypes.Count); foreach (var t in AcceptMessageTypes) { enums.Add((Int32)t); } writer.WritePackedEnum(5, enums); } } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Topic = reader.ReadProtoMessage(); break; case 2: Id = reader.ReadProtoInt32(); break; case 3: Permission = (GrpcPermission)reader.ReadEnum(); break; case 4: Broker = reader.ReadProtoMessage(); break; case 5: if (wt == 0) // 非packed AcceptMessageTypes.Add((GrpcMessageType)reader.ReadEnum()); else if (wt == 2) // packed { var data = reader.ReadProtoBytes(); var sub = new SpanReader(data); while (sub.Available > 0) AcceptMessageTypes.Add((GrpcMessageType)sub.ReadEnum()); } break; default: reader.SkipField(wt); break; } } } } /// 过滤表达式 public class GrpcFilterExpression : ISpanSerializable { /// 过滤类型 public GrpcFilterType Type { get; set; } /// 表达式 public String Expression { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteEnum(1, (Int32)Type); writer.WriteString(2, Expression); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Type = (GrpcFilterType)reader.ReadEnum(); break; case 2: Expression = reader.ReadProtoString(); break; default: reader.SkipField(wt); break; } } } } /// 摘要 public class GrpcDigest : ISpanSerializable { /// 摘要类型 public GrpcDigestType Type { get; set; } /// 校验值 public String Checksum { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteEnum(1, (Int32)Type); writer.WriteString(2, Checksum); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Type = (GrpcDigestType)reader.ReadEnum(); break; case 2: Checksum = reader.ReadProtoString(); break; default: reader.SkipField(wt); break; } } } } /// 消息系统属性 public class GrpcSystemProperties : ISpanSerializable { /// 标签 public String Tag { get; set; } /// Keys public List Keys { get; set; } = []; /// 消息ID public String MessageId { get; set; } /// 消息体摘要 public GrpcDigest BodyDigest { get; set; } /// 消息体编码 public GrpcEncoding BodyEncoding { get; set; } /// 消息类型 public GrpcMessageType MessageType { get; set; } /// 消息出生时间 public DateTime? BornTimestamp { get; set; } /// 消息出生主机 public String BornHost { get; set; } /// 消息存储时间 public DateTime? StoreTimestamp { get; set; } /// 消息存储主机 public String StoreHost { get; set; } /// 投递时间(延迟消息) public DateTime? DeliveryTimestamp { get; set; } /// 收据句柄 public String ReceiptHandle { get; set; } /// 队列ID public Int32 QueueId { get; set; } /// 队列偏移 public Int64 QueueOffset { get; set; } /// 不可见时间 public TimeSpan? InvisibleDuration { get; set; } /// 投递尝试次数 public Int32 DeliveryAttempt { get; set; } /// 消息分组(FIFO) public String MessageGroup { get; set; } /// 追踪上下文 public String TraceContext { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteString(1, Tag); writer.WriteRepeatedString(2, Keys); writer.WriteString(3, MessageId); writer.WriteMessage(4, BodyDigest); writer.WriteEnum(5, (Int32)BodyEncoding); writer.WriteEnum(6, (Int32)MessageType); writer.WriteTimestamp(7, BornTimestamp); writer.WriteString(8, BornHost); writer.WriteTimestamp(9, StoreTimestamp); writer.WriteString(10, StoreHost); writer.WriteTimestamp(11, DeliveryTimestamp); writer.WriteString(12, ReceiptHandle); writer.WriteInt32(13, QueueId); writer.WriteInt64(14, QueueOffset); writer.WriteDuration(15, InvisibleDuration); writer.WriteInt32(16, DeliveryAttempt); writer.WriteString(17, MessageGroup); writer.WriteString(18, TraceContext); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Tag = reader.ReadProtoString(); break; case 2: Keys.Add(reader.ReadProtoString()); break; case 3: MessageId = reader.ReadProtoString(); break; case 4: BodyDigest = reader.ReadProtoMessage(); break; case 5: BodyEncoding = (GrpcEncoding)reader.ReadEnum(); break; case 6: MessageType = (GrpcMessageType)reader.ReadEnum(); break; case 7: BornTimestamp = reader.ReadTimestamp(); break; case 8: BornHost = reader.ReadProtoString(); break; case 9: StoreTimestamp = reader.ReadTimestamp(); break; case 10: StoreHost = reader.ReadProtoString(); break; case 11: DeliveryTimestamp = reader.ReadTimestamp(); break; case 12: ReceiptHandle = reader.ReadProtoString(); break; case 13: QueueId = reader.ReadProtoInt32(); break; case 14: QueueOffset = reader.ReadProtoInt64(); break; case 15: InvisibleDuration = reader.ReadDuration(); break; case 16: DeliveryAttempt = reader.ReadProtoInt32(); break; case 17: MessageGroup = reader.ReadProtoString(); break; case 18: TraceContext = reader.ReadProtoString(); break; default: reader.SkipField(wt); break; } } } } /// gRPC消息 public class GrpcMessage : ISpanSerializable { /// 主题 public GrpcResource Topic { get; set; } /// 用户属性 public Dictionary UserProperties { get; set; } = []; /// 系统属性 public GrpcSystemProperties SystemProperties { get; set; } /// 消息体 public Byte[] Body { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Topic); writer.WriteMap(2, UserProperties); writer.WriteMessage(3, SystemProperties); writer.WriteBytes(4, Body); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Topic = reader.ReadProtoMessage(); break; case 2: var (k, v) = reader.ReadMapEntry(); if (k != null) UserProperties[k] = v; break; case 3: SystemProperties = reader.ReadProtoMessage(); break; case 4: Body = reader.ReadProtoBytes(); break; default: reader.SkipField(wt); break; } } } } /// 队列分配 public class GrpcAssignment : ISpanSerializable { /// 消息队列 public GrpcMessageQueue MessageQueue { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) => writer.WriteMessage(1, MessageQueue); /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: MessageQueue = reader.ReadProtoMessage(); break; default: reader.SkipField(wt); break; } } } } ================================================ FILE: NewLife.RocketMQ/Grpc/GrpcServiceMessages.cs ================================================ using NewLife.Buffers; using NewLife.Serialization; namespace NewLife.RocketMQ.Grpc; #region 路由查询 /// 查询路由请求 public class QueryRouteRequest : ISpanSerializable { /// 主题 public GrpcResource Topic { get; set; } /// 客户端端点 public GrpcEndpoints Endpoints { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Topic); writer.WriteMessage(2, Endpoints); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Topic = reader.ReadProtoMessage(); break; case 2: Endpoints = reader.ReadProtoMessage(); break; default: reader.SkipField(wt); break; } } } } /// 查询路由响应 public class QueryRouteResponse : ISpanSerializable { /// 状态 public GrpcStatus Status { get; set; } /// 消息队列列表 public List MessageQueues { get; set; } = []; /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Status); writer.WriteRepeatedMessage(2, MessageQueues); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Status = reader.ReadProtoMessage(); break; case 2: MessageQueues.Add(reader.ReadProtoMessage()); break; default: reader.SkipField(wt); break; } } } } #endregion #region 发送消息 /// 发送消息请求 public class SendMessageRequest : ISpanSerializable { /// 消息列表 public List Messages { get; set; } = []; /// 写入 /// 编码器 public void Write(ref SpanWriter writer) => writer.WriteRepeatedMessage(1, Messages); /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Messages.Add(reader.ReadProtoMessage()); break; default: reader.SkipField(wt); break; } } } } /// 发送结果条目 public class SendResultEntry : ISpanSerializable { /// 状态 public GrpcStatus Status { get; set; } /// 消息ID public String MessageId { get; set; } /// 事务ID public String TransactionId { get; set; } /// 偏移量 public Int64 Offset { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Status); writer.WriteString(2, MessageId); writer.WriteString(3, TransactionId); writer.WriteInt64(4, Offset); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Status = reader.ReadProtoMessage(); break; case 2: MessageId = reader.ReadProtoString(); break; case 3: TransactionId = reader.ReadProtoString(); break; case 4: Offset = reader.ReadProtoInt64(); break; default: reader.SkipField(wt); break; } } } } /// 发送消息响应 public class SendMessageResponse : ISpanSerializable { /// 状态 public GrpcStatus Status { get; set; } /// 结果条目 public List Entries { get; set; } = []; /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Status); writer.WriteRepeatedMessage(2, Entries); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Status = reader.ReadProtoMessage(); break; case 2: Entries.Add(reader.ReadProtoMessage()); break; default: reader.SkipField(wt); break; } } } } #endregion #region 队列分配 /// 查询队列分配请求 public class QueryAssignmentRequest : ISpanSerializable { /// 主题 public GrpcResource Topic { get; set; } /// 消费组 public GrpcResource Group { get; set; } /// 客户端端点 public GrpcEndpoints Endpoints { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Topic); writer.WriteMessage(2, Group); writer.WriteMessage(3, Endpoints); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Topic = reader.ReadProtoMessage(); break; case 2: Group = reader.ReadProtoMessage(); break; case 3: Endpoints = reader.ReadProtoMessage(); break; default: reader.SkipField(wt); break; } } } } /// 查询队列分配响应 public class QueryAssignmentResponse : ISpanSerializable { /// 状态 public GrpcStatus Status { get; set; } /// 分配结果 public List Assignments { get; set; } = []; /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Status); writer.WriteRepeatedMessage(2, Assignments); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Status = reader.ReadProtoMessage(); break; case 2: Assignments.Add(reader.ReadProtoMessage()); break; default: reader.SkipField(wt); break; } } } } #endregion #region 接收消息 /// 接收消息请求(Server Streaming) public class ReceiveMessageRequest : ISpanSerializable { /// 消费组 public GrpcResource Group { get; set; } /// 消息队列 public GrpcMessageQueue MessageQueue { get; set; } /// 过滤表达式 public GrpcFilterExpression FilterExpression { get; set; } /// 批量大小 public Int32 BatchSize { get; set; } /// 不可见时间 public TimeSpan? InvisibleDuration { get; set; } /// 自动续租 public Boolean AutoRenew { get; set; } /// 长轮询超时 public TimeSpan? LongPollingTimeout { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Group); writer.WriteMessage(2, MessageQueue); writer.WriteMessage(3, FilterExpression); writer.WriteInt32(4, BatchSize); writer.WriteDuration(5, InvisibleDuration); writer.WriteBool(6, AutoRenew); writer.WriteDuration(7, LongPollingTimeout); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Group = reader.ReadProtoMessage(); break; case 2: MessageQueue = reader.ReadProtoMessage(); break; case 3: FilterExpression = reader.ReadProtoMessage(); break; case 4: BatchSize = reader.ReadProtoInt32(); break; case 5: InvisibleDuration = reader.ReadDuration(); break; case 6: AutoRenew = reader.ReadBool(); break; case 7: LongPollingTimeout = reader.ReadDuration(); break; default: reader.SkipField(wt); break; } } } } /// 接收消息响应(oneof: status/message/delivery_timestamp) public class ReceiveMessageResponse : ISpanSerializable { /// 状态(oneof content = 1) public GrpcStatus Status { get; set; } /// 消息(oneof content = 2) public GrpcMessage Message { get; set; } /// 投递时间戳(oneof content = 3) public DateTime? DeliveryTimestamp { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Status); writer.WriteMessage(2, Message); writer.WriteTimestamp(3, DeliveryTimestamp); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Status = reader.ReadProtoMessage(); break; case 2: Message = reader.ReadProtoMessage(); break; case 3: DeliveryTimestamp = reader.ReadTimestamp(); break; default: reader.SkipField(wt); break; } } } } #endregion #region 确认消息 /// 确认消息条目 public class AckMessageEntry : ISpanSerializable { /// 消息ID public String MessageId { get; set; } /// 收据句柄 public String ReceiptHandle { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteString(1, MessageId); writer.WriteString(2, ReceiptHandle); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: MessageId = reader.ReadProtoString(); break; case 2: ReceiptHandle = reader.ReadProtoString(); break; default: reader.SkipField(wt); break; } } } } /// 确认消息请求 public class AckMessageRequest : ISpanSerializable { /// 消费组 public GrpcResource Group { get; set; } /// 主题 public GrpcResource Topic { get; set; } /// 确认条目 public List Entries { get; set; } = []; /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Group); writer.WriteMessage(2, Topic); writer.WriteRepeatedMessage(3, Entries); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Group = reader.ReadProtoMessage(); break; case 2: Topic = reader.ReadProtoMessage(); break; case 3: Entries.Add(reader.ReadProtoMessage()); break; default: reader.SkipField(wt); break; } } } } /// 确认消息结果条目 public class AckMessageResultEntry : ISpanSerializable { /// 消息ID public String MessageId { get; set; } /// 收据句柄 public String ReceiptHandle { get; set; } /// 状态 public GrpcStatus Status { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteString(1, MessageId); writer.WriteString(2, ReceiptHandle); writer.WriteMessage(3, Status); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: MessageId = reader.ReadProtoString(); break; case 2: ReceiptHandle = reader.ReadProtoString(); break; case 3: Status = reader.ReadProtoMessage(); break; default: reader.SkipField(wt); break; } } } } /// 确认消息响应 public class AckMessageResponse : ISpanSerializable { /// 状态 public GrpcStatus Status { get; set; } /// 结果条目 public List Entries { get; set; } = []; /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Status); writer.WriteRepeatedMessage(2, Entries); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Status = reader.ReadProtoMessage(); break; case 2: Entries.Add(reader.ReadProtoMessage()); break; default: reader.SkipField(wt); break; } } } } #endregion #region 心跳 /// 心跳请求 public class HeartbeatRequest : ISpanSerializable { /// 消费组 public GrpcResource Group { get; set; } /// 客户端类型 public GrpcClientType ClientType { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Group); writer.WriteEnum(2, (Int32)ClientType); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Group = reader.ReadProtoMessage(); break; case 2: ClientType = (GrpcClientType)reader.ReadEnum(); break; default: reader.SkipField(wt); break; } } } } /// 心跳响应 public class HeartbeatResponse : ISpanSerializable { /// 状态 public GrpcStatus Status { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) => writer.WriteMessage(1, Status); /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Status = reader.ReadProtoMessage(); break; default: reader.SkipField(wt); break; } } } } #endregion #region 结束事务 /// 结束事务请求 public class GrpcEndTransactionRequest : ISpanSerializable { /// 主题 public GrpcResource Topic { get; set; } /// 消息ID public String MessageId { get; set; } /// 事务ID public String TransactionId { get; set; } /// 事务来源 public GrpcTransactionSource Source { get; set; } /// 事务决议 public GrpcTransactionResolution Resolution { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Topic); writer.WriteString(2, MessageId); writer.WriteString(3, TransactionId); writer.WriteEnum(4, (Int32)Source); writer.WriteEnum(5, (Int32)Resolution); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Topic = reader.ReadProtoMessage(); break; case 2: MessageId = reader.ReadProtoString(); break; case 3: TransactionId = reader.ReadProtoString(); break; case 4: Source = (GrpcTransactionSource)reader.ReadEnum(); break; case 5: Resolution = (GrpcTransactionResolution)reader.ReadEnum(); break; default: reader.SkipField(wt); break; } } } } /// 结束事务响应 public class GrpcEndTransactionResponse : ISpanSerializable { /// 状态 public GrpcStatus Status { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) => writer.WriteMessage(1, Status); /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Status = reader.ReadProtoMessage(); break; default: reader.SkipField(wt); break; } } } } #endregion #region 死信队列 /// 转发消息到死信队列请求 public class ForwardMessageToDeadLetterQueueRequest : ISpanSerializable { /// 消费组 public GrpcResource Group { get; set; } /// 主题 public GrpcResource Topic { get; set; } /// 收据句柄 public String ReceiptHandle { get; set; } /// 消息ID public String MessageId { get; set; } /// 投递尝试次数 public Int32 DeliveryAttempt { get; set; } /// 最大投递尝试次数 public Int32 MaxDeliveryAttempts { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Group); writer.WriteMessage(2, Topic); writer.WriteString(3, ReceiptHandle); writer.WriteString(4, MessageId); writer.WriteInt32(5, DeliveryAttempt); writer.WriteInt32(6, MaxDeliveryAttempts); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Group = reader.ReadProtoMessage(); break; case 2: Topic = reader.ReadProtoMessage(); break; case 3: ReceiptHandle = reader.ReadProtoString(); break; case 4: MessageId = reader.ReadProtoString(); break; case 5: DeliveryAttempt = reader.ReadProtoInt32(); break; case 6: MaxDeliveryAttempts = reader.ReadProtoInt32(); break; default: reader.SkipField(wt); break; } } } } /// 转发消息到死信队列响应 public class ForwardMessageToDeadLetterQueueResponse : ISpanSerializable { /// 状态 public GrpcStatus Status { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) => writer.WriteMessage(1, Status); /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Status = reader.ReadProtoMessage(); break; default: reader.SkipField(wt); break; } } } } #endregion #region 修改不可见时间 /// 修改不可见时间请求 public class ChangeInvisibleDurationRequest : ISpanSerializable { /// 消费组 public GrpcResource Group { get; set; } /// 主题 public GrpcResource Topic { get; set; } /// 收据句柄 public String ReceiptHandle { get; set; } /// 不可见时间 public TimeSpan? InvisibleDuration { get; set; } /// 消息ID public String MessageId { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Group); writer.WriteMessage(2, Topic); writer.WriteString(3, ReceiptHandle); writer.WriteDuration(4, InvisibleDuration); writer.WriteString(5, MessageId); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Group = reader.ReadProtoMessage(); break; case 2: Topic = reader.ReadProtoMessage(); break; case 3: ReceiptHandle = reader.ReadProtoString(); break; case 4: InvisibleDuration = reader.ReadDuration(); break; case 5: MessageId = reader.ReadProtoString(); break; default: reader.SkipField(wt); break; } } } } /// 修改不可见时间响应 public class ChangeInvisibleDurationResponse : ISpanSerializable { /// 状态 public GrpcStatus Status { get; set; } /// 新收据句柄 public String ReceiptHandle { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Status); writer.WriteString(2, ReceiptHandle); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Status = reader.ReadProtoMessage(); break; case 2: ReceiptHandle = reader.ReadProtoString(); break; default: reader.SkipField(wt); break; } } } } #endregion #region 通知终止 /// 通知客户端终止请求 public class NotifyClientTerminationRequest : ISpanSerializable { /// 消费组 public GrpcResource Group { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) => writer.WriteMessage(1, Group); /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Group = reader.ReadProtoMessage(); break; default: reader.SkipField(wt); break; } } } } /// 通知客户端终止响应 public class NotifyClientTerminationResponse : ISpanSerializable { /// 状态 public GrpcStatus Status { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) => writer.WriteMessage(1, Status); /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Status = reader.ReadProtoMessage(); break; default: reader.SkipField(wt); break; } } } } #endregion #region 客户端资源上报(Telemetry) /// Telemetry命令。客户端向Proxy上报资源信息(设置、主题订阅等) public class TelemetryCommand : ISpanSerializable { /// 状态 public GrpcStatus Status { get; set; } /// 客户端设置 public GrpcSettings Settings { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Status); writer.WriteMessage(2, Settings); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Status = reader.ReadProtoMessage(); break; case 2: Settings = reader.ReadProtoMessage(); break; default: reader.SkipField(wt); break; } } } } /// gRPC客户端设置。用于Telemetry上报客户端配置信息 public class GrpcSettings : ISpanSerializable { /// 客户端类型 public GrpcClientType ClientType { get; set; } /// 访问点 public GrpcEndpoints AccessPoint { get; set; } /// 请求超时(Duration,秒) public TimeSpan? RequestTimeout { get; set; } /// 发布设置(生产者) public GrpcPublishingSettings Publishing { get; set; } /// 订阅设置(消费者) public GrpcSubscriptionSettings Subscription { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteEnum(1, (Int32)ClientType); writer.WriteMessage(2, AccessPoint); if (RequestTimeout != null) writer.WriteDuration(3, RequestTimeout.Value); writer.WriteMessage(4, Publishing); writer.WriteMessage(5, Subscription); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: ClientType = (GrpcClientType)reader.ReadEnum(); break; case 2: AccessPoint = reader.ReadProtoMessage(); break; case 3: RequestTimeout = reader.ReadDuration(); break; case 4: Publishing = reader.ReadProtoMessage(); break; case 5: Subscription = reader.ReadProtoMessage(); break; default: reader.SkipField(wt); break; } } } } /// 发布设置 public class GrpcPublishingSettings : ISpanSerializable { /// 发布主题列表 public List Topics { get; set; } = []; /// 写入 /// 编码器 public void Write(ref SpanWriter writer) => writer.WriteRepeatedMessage(1, Topics); /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Topics.Add(reader.ReadProtoMessage()); break; default: reader.SkipField(wt); break; } } } } /// 订阅设置 public class GrpcSubscriptionSettings : ISpanSerializable { /// 消费组 public GrpcResource Group { get; set; } /// 订阅列表 public List Subscriptions { get; set; } = []; /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Group); writer.WriteRepeatedMessage(2, Subscriptions); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Group = reader.ReadProtoMessage(); break; case 2: Subscriptions.Add(reader.ReadProtoMessage()); break; default: reader.SkipField(wt); break; } } } } /// 订阅条目 public class GrpcSubscriptionEntry : ISpanSerializable { /// 主题 public GrpcResource Topic { get; set; } /// 过滤表达式 public GrpcFilterExpression Expression { get; set; } /// 写入 /// 编码器 public void Write(ref SpanWriter writer) { writer.WriteMessage(1, Topic); writer.WriteMessage(2, Expression); } /// 读取 /// 解码器 public void Read(ref SpanReader reader) { while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; switch (fn) { case 1: Topic = reader.ReadProtoMessage(); break; case 2: Expression = reader.ReadProtoMessage(); break; default: reader.SkipField(wt); break; } } } } #endregion ================================================ FILE: NewLife.RocketMQ/Grpc/ProtoExtensions.cs ================================================ using System.Text; using NewLife.Buffers; using NewLife.Serialization; namespace NewLife.RocketMQ.Grpc; /// Protobuf编解码扩展方法。为 SpanWriter/SpanReader 提供 Protocol Buffers 二进制格式的读写支持 /// /// 实现 Protocol Buffers 编码规范,支持 varint/fixed/length-delimited 三种线路类型。 /// 参考:https://protobuf.dev/programming-guides/encoding/ /// public static class ProtoExtensions { #region SpanWriter 写入扩展 /// 写入varint编码的无符号64位整数 public static void WriteRawVarint(ref this SpanWriter writer, UInt64 value) { while (value > 0x7F) { writer.Write((Byte)(value | 0x80)); value >>= 7; } writer.Write((Byte)value); } /// 写入field tag(字段编号 + 线路类型) /// 写入器 /// 字段编号 /// 线路类型。0=varint, 1=64bit, 2=length-delimited, 5=32bit public static void WriteTag(ref this SpanWriter writer, Int32 fieldNumber, Int32 wireType) => WriteRawVarint(ref writer, (UInt64)((fieldNumber << 3) | wireType)); /// 写入固定4字节(小端序) public static void WriteRawFixed32(ref this SpanWriter writer, UInt32 value) { writer.Write((Byte)(value & 0xFF)); writer.Write((Byte)((value >> 8) & 0xFF)); writer.Write((Byte)((value >> 16) & 0xFF)); writer.Write((Byte)((value >> 24) & 0xFF)); } /// 写入固定8字节(小端序) public static void WriteRawFixed64(ref this SpanWriter writer, UInt64 value) { writer.Write((Byte)(value & 0xFF)); writer.Write((Byte)((value >> 8) & 0xFF)); writer.Write((Byte)((value >> 16) & 0xFF)); writer.Write((Byte)((value >> 24) & 0xFF)); writer.Write((Byte)((value >> 32) & 0xFF)); writer.Write((Byte)((value >> 40) & 0xFF)); writer.Write((Byte)((value >> 48) & 0xFF)); writer.Write((Byte)((value >> 56) & 0xFF)); } /// 写入int32字段(varint编码) public static void WriteInt32(ref this SpanWriter writer, Int32 fieldNumber, Int32 value) { if (value == 0) return; WriteTag(ref writer, fieldNumber, 0); if (value >= 0) WriteRawVarint(ref writer, (UInt64)value); else WriteRawVarint(ref writer, (UInt64)(Int64)value); // 负数按10字节varint编码 } /// 写入int64字段(varint编码) public static void WriteInt64(ref this SpanWriter writer, Int32 fieldNumber, Int64 value) { if (value == 0) return; WriteTag(ref writer, fieldNumber, 0); WriteRawVarint(ref writer, (UInt64)value); } /// 写入uint32字段(varint编码) public static void WriteUInt32(ref this SpanWriter writer, Int32 fieldNumber, UInt32 value) { if (value == 0) return; WriteTag(ref writer, fieldNumber, 0); WriteRawVarint(ref writer, value); } /// 写入uint64字段(varint编码) public static void WriteUInt64(ref this SpanWriter writer, Int32 fieldNumber, UInt64 value) { if (value == 0) return; WriteTag(ref writer, fieldNumber, 0); WriteRawVarint(ref writer, value); } /// 写入sint32字段(ZigZag + varint编码) public static void WriteSInt32(ref this SpanWriter writer, Int32 fieldNumber, Int32 value) { if (value == 0) return; WriteTag(ref writer, fieldNumber, 0); WriteRawVarint(ref writer, (UInt32)((value << 1) ^ (value >> 31))); } /// 写入sint64字段(ZigZag + varint编码) public static void WriteSInt64(ref this SpanWriter writer, Int32 fieldNumber, Int64 value) { if (value == 0) return; WriteTag(ref writer, fieldNumber, 0); WriteRawVarint(ref writer, (UInt64)((value << 1) ^ (value >> 63))); } /// 写入bool字段 public static void WriteBool(ref this SpanWriter writer, Int32 fieldNumber, Boolean value) { if (!value) return; WriteTag(ref writer, fieldNumber, 0); writer.Write((Byte)1); } /// 写入enum字段(varint编码) public static void WriteEnum(ref this SpanWriter writer, Int32 fieldNumber, Int32 value) { if (value == 0) return; WriteTag(ref writer, fieldNumber, 0); WriteRawVarint(ref writer, (UInt64)(UInt32)value); } /// 写入string字段(length-delimited UTF-8) public static void WriteString(ref this SpanWriter writer, Int32 fieldNumber, String value) { if (String.IsNullOrEmpty(value)) return; WriteTag(ref writer, fieldNumber, 2); var bytes = Encoding.UTF8.GetBytes(value); WriteRawVarint(ref writer, (UInt64)bytes.Length); writer.Write(bytes); } /// 写入bytes字段(length-delimited) public static void WriteBytes(ref this SpanWriter writer, Int32 fieldNumber, Byte[] value) { if (value == null || value.Length == 0) return; WriteTag(ref writer, fieldNumber, 2); WriteRawVarint(ref writer, (UInt64)value.Length); writer.Write(value); } /// 写入嵌套消息字段(length-delimited)。使用重试缓冲区处理未知大小的子消息 public static void WriteMessage(ref this SpanWriter writer, Int32 fieldNumber, ISpanSerializable message) { if (message == null) return; // 使用重试缓冲区计算子消息长度 var size = 4096; while (true) { var temp = new Byte[size]; var sub = new SpanWriter(temp); try { message.Write(ref sub); var len = sub.WrittenCount; WriteTag(ref writer, fieldNumber, 2); WriteRawVarint(ref writer, (UInt64)len); writer.Write(new ReadOnlySpan(temp, 0, len)); return; } catch (InvalidOperationException) { size = checked(size * 2); } } } /// 写入fixed32字段(4字节小端序) public static void WriteFixed32(ref this SpanWriter writer, Int32 fieldNumber, UInt32 value) { if (value == 0) return; WriteTag(ref writer, fieldNumber, 5); WriteRawFixed32(ref writer, value); } /// 写入fixed64字段(8字节小端序) public static void WriteFixed64(ref this SpanWriter writer, Int32 fieldNumber, UInt64 value) { if (value == 0) return; WriteTag(ref writer, fieldNumber, 1); WriteRawFixed64(ref writer, value); } /// 写入float字段(fixed32小端序) public static void WriteFloat(ref this SpanWriter writer, Int32 fieldNumber, Single value) { if (value == 0) return; WriteTag(ref writer, fieldNumber, 5); WriteRawFixed32(ref writer, BitConverter.ToUInt32(BitConverter.GetBytes(value), 0)); } /// 写入double字段(fixed64小端序) public static void WriteDouble(ref this SpanWriter writer, Int32 fieldNumber, Double value) { if (value == 0) return; WriteTag(ref writer, fieldNumber, 1); WriteRawFixed64(ref writer, (UInt64)BitConverter.DoubleToInt64Bits(value)); } /// 写入map字段。map编码为 repeated message { key=1, value=2 } public static void WriteMap(ref this SpanWriter writer, Int32 fieldNumber, IDictionary map) { if (map == null || map.Count == 0) return; foreach (var kv in map) { var entryBuf = new Byte[512]; var entry = new SpanWriter(entryBuf); WriteString(ref entry, 1, kv.Key); WriteString(ref entry, 2, kv.Value); var len = entry.WrittenCount; WriteTag(ref writer, fieldNumber, 2); WriteRawVarint(ref writer, (UInt64)len); writer.Write(new ReadOnlySpan(entryBuf, 0, len)); } } /// 写入repeated string字段 public static void WriteRepeatedString(ref this SpanWriter writer, Int32 fieldNumber, IList values) { if (values == null || values.Count == 0) return; foreach (var value in values) WriteString(ref writer, fieldNumber, value); } /// 写入repeated enum字段(packed编码) public static void WritePackedEnum(ref this SpanWriter writer, Int32 fieldNumber, IList values) { if (values == null || values.Count == 0) return; var subBuf = new Byte[values.Count * 5]; // varint最多5字节 var sub = new SpanWriter(subBuf); foreach (var v in values) WriteRawVarint(ref sub, (UInt64)(UInt32)v); var len = sub.WrittenCount; WriteTag(ref writer, fieldNumber, 2); WriteRawVarint(ref writer, (UInt64)len); writer.Write(new ReadOnlySpan(subBuf, 0, len)); } /// 写入repeated message字段 public static void WriteRepeatedMessage(ref this SpanWriter writer, Int32 fieldNumber, IList messages) where T : ISpanSerializable { if (messages == null || messages.Count == 0) return; foreach (var msg in messages) WriteMessage(ref writer, fieldNumber, msg); } /// 写入google.protobuf.Timestamp(seconds=1, nanos=2) public static void WriteTimestamp(ref this SpanWriter writer, Int32 fieldNumber, DateTime? value) { if (value == null || value.Value == DateTime.MinValue) return; var utc = value.Value.Kind == DateTimeKind.Utc ? value.Value : value.Value.ToUniversalTime(); var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); var ts = utc - epoch; var subBuf = new Byte[32]; var sub = new SpanWriter(subBuf); WriteInt64(ref sub, 1, (Int64)ts.TotalSeconds); var nanos = (Int32)((ts.Ticks % TimeSpan.TicksPerSecond) * 100); WriteInt32(ref sub, 2, nanos); var len = sub.WrittenCount; WriteTag(ref writer, fieldNumber, 2); WriteRawVarint(ref writer, (UInt64)len); writer.Write(new ReadOnlySpan(subBuf, 0, len)); } /// 写入google.protobuf.Duration(seconds=1, nanos=2) public static void WriteDuration(ref this SpanWriter writer, Int32 fieldNumber, TimeSpan? value) { if (value == null || value.Value == TimeSpan.Zero) return; var subBuf = new Byte[32]; var sub = new SpanWriter(subBuf); WriteInt64(ref sub, 1, (Int64)value.Value.TotalSeconds); var nanos = (Int32)((value.Value.Ticks % TimeSpan.TicksPerSecond) * 100); WriteInt32(ref sub, 2, nanos); var len = sub.WrittenCount; WriteTag(ref writer, fieldNumber, 2); WriteRawVarint(ref writer, (UInt64)len); writer.Write(new ReadOnlySpan(subBuf, 0, len)); } #endregion #region SpanReader 读取扩展 /// 读取varint编码的无符号64位整数 public static UInt64 ReadRawVarint(ref this SpanReader reader) { var result = 0UL; var shift = 0; while (shift < 64) { if (reader.Available <= 0) throw new EndOfStreamException("读取varint时到达数据末尾"); var b = reader.ReadByte(); result |= (UInt64)(b & 0x7F) << shift; if ((b & 0x80) == 0) return result; shift += 7; } throw new InvalidDataException("Varint编码超长"); } /// 读取field tag,返回字段编号和线路类型 public static (Int32 FieldNumber, Int32 WireType) ReadTag(ref this SpanReader reader) { if (reader.Available <= 0) return (0, 0); var tag = (UInt32)ReadRawVarint(ref reader); return ((Int32)(tag >> 3), (Int32)(tag & 0x07)); } /// 读取int32值(varint解码) public static Int32 ReadProtoInt32(ref this SpanReader reader) => (Int32)ReadRawVarint(ref reader); /// 读取int64值(varint解码) public static Int64 ReadProtoInt64(ref this SpanReader reader) => (Int64)ReadRawVarint(ref reader); /// 读取uint32值(varint解码) public static UInt32 ReadProtoUInt32(ref this SpanReader reader) => (UInt32)ReadRawVarint(ref reader); /// 读取sint32值(ZigZag解码) public static Int32 ReadSInt32(ref this SpanReader reader) { var n = (UInt32)ReadRawVarint(ref reader); return (Int32)((n >> 1) ^ -(Int32)(n & 1)); } /// 读取sint64值(ZigZag解码) public static Int64 ReadSInt64(ref this SpanReader reader) { var n = ReadRawVarint(ref reader); return (Int64)(n >> 1) ^ -((Int64)(n & 1)); } /// 读取bool值 public static Boolean ReadBool(ref this SpanReader reader) => ReadRawVarint(ref reader) != 0; /// 读取enum值(varint解码) public static Int32 ReadEnum(ref this SpanReader reader) => (Int32)ReadRawVarint(ref reader); /// 读取string值(length-delimited UTF-8) public static String ReadProtoString(ref this SpanReader reader) { var len = (Int32)ReadRawVarint(ref reader); if (len == 0) return ""; var buf = reader.ReadBytes(len).ToArray(); return Encoding.UTF8.GetString(buf); } /// 读取bytes值(length-delimited) public static Byte[] ReadProtoBytes(ref this SpanReader reader) { var len = (Int32)ReadRawVarint(ref reader); if (len == 0) return []; return reader.ReadBytes(len).ToArray(); } /// 读取fixed32值(小端序4字节) public static UInt32 ReadFixed32(ref this SpanReader reader) { var span = reader.ReadBytes(4); return (UInt32)(span[0] | (span[1] << 8) | (span[2] << 16) | (span[3] << 24)); } /// 读取fixed64值(小端序8字节) public static UInt64 ReadFixed64(ref this SpanReader reader) { var span = reader.ReadBytes(8); return (UInt64)span[0] | ((UInt64)span[1] << 8) | ((UInt64)span[2] << 16) | ((UInt64)span[3] << 24) | ((UInt64)span[4] << 32) | ((UInt64)span[5] << 40) | ((UInt64)span[6] << 48) | ((UInt64)span[7] << 56); } /// 读取float值(小端序4字节) public static Single ReadFloat(ref this SpanReader reader) { var bytes = reader.ReadBytes(4).ToArray(); return BitConverter.ToSingle(bytes, 0); } /// 读取double值(小端序8字节) public static Double ReadProtoDouble(ref this SpanReader reader) { var bytes = reader.ReadBytes(8).ToArray(); return BitConverter.ToDouble(bytes, 0); } /// 读取嵌套消息 public static T ReadProtoMessage(ref this SpanReader reader) where T : ISpanSerializable, new() { var len = (Int32)ReadRawVarint(ref reader); if (len == 0) return new T(); var subData = reader.ReadBytes(len).ToArray(); var sub = new SpanReader(subData); var msg = new T(); msg.Read(ref sub); return msg; } /// 读取map字段中的一个entry(key=1 string, value=2 string) public static (String Key, String Value) ReadMapEntry(ref this SpanReader reader) { var len = (Int32)ReadRawVarint(ref reader); var subData = reader.ReadBytes(len).ToArray(); var sub = new SpanReader(subData); String key = null; String value = null; while (sub.Available > 0) { var (fn, wt) = ReadTag(ref sub); if (fn == 0) break; switch (fn) { case 1: key = ReadProtoString(ref sub); break; case 2: value = ReadProtoString(ref sub); break; default: SkipField(ref sub, wt); break; } } return (key, value); } /// 读取google.protobuf.Timestamp public static DateTime ReadTimestamp(ref this SpanReader reader) { var len = (Int32)ReadRawVarint(ref reader); if (len == 0) return DateTime.MinValue; var subData = reader.ReadBytes(len).ToArray(); var sub = new SpanReader(subData); var seconds = 0L; var nanos = 0; while (sub.Available > 0) { var (fn, wt) = ReadTag(ref sub); if (fn == 0) break; switch (fn) { case 1: seconds = ReadProtoInt64(ref sub); break; case 2: nanos = ReadProtoInt32(ref sub); break; default: SkipField(ref sub, wt); break; } } var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); return epoch.AddSeconds(seconds).AddTicks(nanos / 100); } /// 读取google.protobuf.Duration public static TimeSpan ReadDuration(ref this SpanReader reader) { var len = (Int32)ReadRawVarint(ref reader); if (len == 0) return TimeSpan.Zero; var subData = reader.ReadBytes(len).ToArray(); var sub = new SpanReader(subData); var seconds = 0L; var nanos = 0; while (sub.Available > 0) { var (fn, wt) = ReadTag(ref sub); if (fn == 0) break; switch (fn) { case 1: seconds = ReadProtoInt64(ref sub); break; case 2: nanos = ReadProtoInt32(ref sub); break; default: SkipField(ref sub, wt); break; } } return TimeSpan.FromSeconds(seconds) + TimeSpan.FromTicks(nanos / 100); } /// 跳过当前字段的值 public static void SkipField(ref this SpanReader reader, Int32 wireType) { switch (wireType) { case 0: // varint ReadRawVarint(ref reader); break; case 1: // 64-bit reader.ReadBytes(8); break; case 2: // length-delimited var len = (Int32)ReadRawVarint(ref reader); reader.ReadBytes(len); break; case 5: // 32-bit reader.ReadBytes(4); break; default: throw new InvalidDataException($"未知的线路类型: {wireType}"); } } #endregion #region 序列化辅助 /// 序列化 ISpanSerializable 消息为 Protobuf 字节数组。使用重试缓冲区处理未知大小的消息 /// 消息 /// 初始缓冲区大小 /// Protobuf 编码的字节数组 public static Byte[] Serialize(ISpanSerializable message, Int32 initialCapacity = 4096) { if (message == null) return []; var capacity = initialCapacity; while (true) { var buf = new Byte[capacity]; var writer = new SpanWriter(buf); try { message.Write(ref writer); return writer.WrittenSpan.ToArray(); } catch (InvalidOperationException) { capacity = checked(capacity * 2); } } } #endregion } ================================================ FILE: NewLife.RocketMQ/Helper.cs ================================================ namespace NewLife.RocketMQ; static class Helper { public static TEnum ToEnum(this String value, TEnum defaultValue = default) where TEnum : struct => Enum.TryParse(value, out var v) ? v : defaultValue; } ================================================ FILE: NewLife.RocketMQ/HuaweiProvider.cs ================================================ namespace NewLife.RocketMQ; /// 华为云 DMS RocketMQ 适配器 public class HuaweiProvider : ICloudProvider { /// 提供者名称 public String Name => "Huawei"; /// 访问令牌 public String AccessKey { get; set; } /// 访问密钥 public String SecretKey { get; set; } /// 通道标识。默认HUAWEI public String OnsChannel { get; set; } = "HUAWEI"; /// 实例ID public String InstanceId { get; set; } /// 是否启用SSL public Boolean EnableSsl { get; set; } /// 转换主题名。华为云不转换 /// 原始主题名 /// public String TransformTopic(String topic) => topic; /// 转换消费组名。华为云不转换 /// 原始消费组名 /// public String TransformGroup(String group) => group; /// 获取 NameServer 地址。华为云不从HTTP获取 /// public String GetNameServerAddress() => null; } ================================================ FILE: NewLife.RocketMQ/ICloudProvider.cs ================================================ using NewLife.RocketMQ.Protocol; namespace NewLife.RocketMQ; /// 云厂商适配器接口。统一各云厂商的签名认证和实例路由逻辑 public interface ICloudProvider { /// 提供者名称 String Name { get; } /// 访问令牌 String AccessKey { get; } /// 访问密钥 String SecretKey { get; } /// 通道标识 String OnsChannel { get; } /// 转换主题名。用于阿里云等需要加实例ID前缀的场景 /// 原始主题名 /// 转换后的主题名 String TransformTopic(String topic); /// 转换消费组名 /// 原始消费组名 /// 转换后的消费组名 String TransformGroup(String group); /// 获取 NameServer 地址。用于从 HTTP 接口获取地址的场景 /// NameServer 地址,null 表示无需特殊处理 String GetNameServerAddress(); } ================================================ FILE: NewLife.RocketMQ/MessageTrace/AsyncTraceDispatcher.cs ================================================ using System; using System.Collections.Concurrent; using System.Text; using System.Threading; using System.Threading.Tasks; using NewLife.Log; using NewLife.RocketMQ.Protocol; namespace NewLife.RocketMQ.MessageTrace { /// /// 异步轨迹分发器 /// internal class AsyncTraceDispatcher : IDisposable { private readonly Producer _traceProducer; private readonly BlockingCollection _traceQueue; private readonly CancellationTokenSource _cancellationTokenSource; private readonly Task _dispatchTask; /// 轨迹主题 public const String TraceTopic = "RMQ_SYS_TRACE_TOPIC"; internal AsyncTraceDispatcher() { // 初始化内部生产者 _traceProducer = new Producer { Topic = TraceTopic, // 使用一个独特的生产者组 Group = "T_P_G_RMQ_SYS_TRACE_TOPIC", Log = XTrace.Log, }; _traceQueue = new BlockingCollection(); _cancellationTokenSource = new CancellationTokenSource(); // 启动后台任务来处理轨迹消息 _dispatchTask = Task.Factory.StartNew(Dispatch, TaskCreationOptions.LongRunning); } public void Start(String nameServerAddress) { if (nameServerAddress.IsNullOrEmpty()) throw new ArgumentNullException(nameof(nameServerAddress)); _traceProducer.NameServerAddress = nameServerAddress; _traceProducer.Start(); } /// /// 添加轨迹上下文到队列 /// /// public void AddTrace(TraceContext context) { if (!_traceProducer.Active) return; try { _traceQueue.Add(context, _cancellationTokenSource.Token); } catch (OperationCanceledException) { // 忽略异常,因为这意味着分发器正在关闭 } catch (Exception ex) { XTrace.WriteException(ex); } } private void Dispatch() { while (!_cancellationTokenSource.IsCancellationRequested) { try { var context = _traceQueue.Take(_cancellationTokenSource.Token); if (context != null) { ProcessTrace(context); } } catch (OperationCanceledException) { break; // 退出循环 } catch (Exception ex) { XTrace.WriteException(ex); } } } private void ProcessTrace(TraceContext context) { var sb = new StringBuilder(); foreach (var bean in context.TraceBeans) { sb.Append(bean.Topic).Append("\x01"); sb.Append(bean.MsgId).Append("\x01"); sb.Append(bean.Tags).Append("\x01"); sb.Append(bean.Keys).Append("\x01"); sb.Append(bean.StoreHost).Append("\x01"); sb.Append(bean.BodyLength).Append("\x01"); sb.Append(context.CostTime).Append("\x01"); sb.Append(context.TraceType).Append("\x02"); } var body = sb.ToString().TrimEnd('\x02'); var keys = context.TraceBeans.Count > 0 ? context.TraceBeans[0].MsgId : ""; var msg = new Message { Topic = TraceTopic, Tags = context.TraceType.ToString(), Keys = keys, Body = Encoding.UTF8.GetBytes(body) }; try { _traceProducer.Publish(msg,null,3000); } catch (Exception ex) { XTrace.WriteException(ex); } } public void Dispose() { _cancellationTokenSource.Cancel(); _dispatchTask.Wait(1000); _traceProducer.Stop(); } } } ================================================ FILE: NewLife.RocketMQ/MessageTrace/MessageTraceHook.cs ================================================ using System; using NewLife.RocketMQ.Protocol; using NewLife.Remoting; using NewLife.Net; using NewLife.RocketMQ.MessageTrace; using NewLife.Log; using System.Linq; using NewLife.Data; namespace NewLife.RocketMQ.MessageTrace { /// /// 消息轨迹钩子实现 /// internal class MessageTraceHook : ISendMessageHook, IConsumeMessageHook { private readonly AsyncTraceDispatcher _dispatcher; public MessageTraceHook(AsyncTraceDispatcher dispatcher) { _dispatcher = dispatcher; } public String HookName => "MessageTraceHook"; public void ExecuteHookBefore(SendMessageContext context) { if (context == null) return; context.TraceContext = new TraceContext { TraceType = TraceType.Pub, GroupName = context.ProducerGroup, RequestId = Guid.NewGuid().ToString("N") }; } public void ExecuteHookAfter(SendMessageContext context) { if (context.Message?.Topic?.Equals("RMQ_SYS_TRACE_TOPIC") == true) { return; } if (context?.SendResult == null || context.SendResult.Status != SendStatus.SendOK) return; var traceContext = new TraceContext { TraceType = TraceType.Pub, GroupName = context.ProducerGroup }; var traceBean = new TraceBean { Topic = context.Message.Topic, MsgId = context.SendResult.MsgId, Tags = context.Message.Tags, Keys = context.Message.Keys, StoreHost = context.BrokerAddr, BodyLength = context.Message.Body.Length, ClientHost = context.Mq.BrokerName, MsgType = context.MsgType }; traceContext.TraceBeans.Add(traceBean); traceContext.CostTime = (Int32)(DateTime.Now - context.BornHost).TotalMilliseconds; _dispatcher.AddTrace(traceContext); } public void ExecuteHookBefore(ConsumeMessageContext context) { if (context?.MsgList == null || context.MsgList.Count == 0) return; var traceContext = new TraceContext { TraceType = TraceType.SubBefore, GroupName = context.ConsumerGroup, RequestId = Guid.NewGuid().ToString("N") }; var traceBeans = context.MsgList.Select(msg => new TraceBean { Topic = msg.Topic, MsgId = msg.MsgId, Tags = msg.Tags, Keys = msg.Keys, StoreHost = msg.StoreHost + "", BodyLength = msg.Body.Length, ClientHost = context.Mq.BrokerName, MsgType = context.MsgType }).ToList(); foreach (var traceBean in traceBeans) traceContext.TraceBeans.Add(traceBean); _dispatcher.AddTrace(traceContext); } public void ExecuteHookAfter(ConsumeMessageContext context) { if (context?.MsgList == null || context.MsgList.Count == 0) return; var subBeforeTraceContext = context.TraceContext; var subAfterTraceContext = new TraceContext { TraceType = TraceType.SubAfter, GroupName = context.ConsumerGroup, RequestId = subBeforeTraceContext.RequestId, Success = context.Success, }; var traceBeans = context.MsgList.Select(msg => new TraceBean { Topic = msg.Topic, MsgId = msg.MsgId, Tags = msg.Tags, Keys = msg.Keys, StoreHost = msg.StoreHost + "", BodyLength = msg.Body.Length, ClientHost = context.Mq.BrokerName, MsgType = context.MsgType }).ToList(); foreach (var traceBean in traceBeans) subAfterTraceContext.TraceBeans.Add(traceBean); subAfterTraceContext.CostTime = (Int32)(DateTime.Now - subBeforeTraceContext.TimeStamp).TotalMilliseconds; _dispatcher.AddTrace(subAfterTraceContext); } } } ================================================ FILE: NewLife.RocketMQ/MessageTrace/TraceModel.cs ================================================ using System; using System.Collections.Generic; using NewLife.RocketMQ.Protocol; namespace NewLife.RocketMQ.MessageTrace { /// /// /// public interface ISendMessageHook { /// /// /// /// void ExecuteHookBefore(SendMessageContext context); /// /// /// /// void ExecuteHookAfter(SendMessageContext context); } /// /// /// public interface IConsumeMessageHook { /// /// /// /// void ExecuteHookBefore(ConsumeMessageContext context); /// /// /// /// void ExecuteHookAfter(ConsumeMessageContext context); } /// /// /// public class SendMessageContext { /// /// /// public String ProducerGroup; /// /// /// public Message Message; /// /// /// public MessageQueue Mq; /// /// /// public String BrokerAddr; /// /// /// public SendResult SendResult; /// /// /// public Exception E; /// /// /// public Object MqTraceContext; /// /// /// public IDictionary Props; /// /// /// public TraceContext TraceContext; /// /// /// public String MsgType; /// /// /// public DateTime BornHost; } /// /// /// public class ConsumeMessageContext { /// /// /// public String ConsumerGroup; /// public List MsgList; /// /// /// public MessageQueue Mq; /// /// /// public Boolean Success; /// /// /// public IDictionary Props; /// /// /// public TraceContext TraceContext; /// /// /// public String MsgType; /// /// /// public DateTime BornHost; } /// /// 轨迹类型 /// public enum TraceType { Pub, SubBefore, SubAfter, } /// /// 轨迹上下文 /// public class TraceContext { /// 轨迹类型 public TraceType TraceType { get; set; } /// 时间戳 public DateTime TimeStamp { get; set; } /// 区域ID public String RegionId { get; set; } /// 分组名 public String GroupName { get; set; } /// 耗时 public int CostTime { get; set; } /// 是否成功 public bool Success { get; set; } /// 请求ID,例如消息ID public String RequestId { get; set; } /// 轨迹豆 public IList TraceBeans { get; set; } = new List(); } /// /// 轨迹豆,包含轨迹上下文信息 /// public class TraceBean { /// 主题 public String Topic { get; set; } /// 消息ID public String MsgId { get; set; } /// 偏移消息ID public String OffsetMsgId { get; set; } /// 标签 public String Tags { get; set; } /// public String Keys { get; set; } /// 存储主机 public String StoreHost { get; set; } /// 消息体长度 public int BodyLength { get; set; } /// 客户端主机 public String ClientHost { get; set; } /// 消息类型 public String MsgType { get; set; } /// 存储时间 public long StoreTime { get; set; } } } ================================================ FILE: NewLife.RocketMQ/Models/ConsumeEventArgs.cs ================================================ using NewLife.RocketMQ.Protocol; namespace NewLife.RocketMQ.Models { /// 消费事件参数 public class ConsumeEventArgs : EventArgs { /// 队列 public MessageQueue Queue { get; set; } /// 消息集合 public MessageExt[] Messages { get; set; } /// 结果 public PullResult Result { get; set; } } } ================================================ FILE: NewLife.RocketMQ/Models/ConsumeFromWheres.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NewLife.RocketMQ.Models { public enum ConsumeFromWheres { CONSUME_FROM_LAST_OFFSET = 0, CONSUME_FROM_FIRST_OFFSET = 1, CONSUME_FROM_TIMESTAMP = 2, CONSUME_FROM_MIN_OFFSET = 3, CONSUME_FROM_MAX_OFFSET = 4, } } ================================================ FILE: NewLife.RocketMQ/Models/ConsumeTypes.cs ================================================ namespace NewLife.RocketMQ.Models; /// 消费类型 public enum ConsumeTypes { /// 拉取。 Pull, /// 推送。 Push, /// 弹出。 Pop, } ================================================ FILE: NewLife.RocketMQ/Models/DelayTimeLevels.cs ================================================ namespace NewLife.RocketMQ.Models; #pragma warning disable CS1591 // 缺少对公共可见类型或成员的 XML 注释 /// 延迟消息的18个等级 public enum DelayTimeLevels { S1 =1, S5 =2, S10=3, S30=4, Min1=5, Min2=6, Min3=7, Min4=8, Min5=9, Min6=10, Min7=11, Min8=12, Min9=13, Min10=14, Min20=15, Min30=16, Hour1=17, Hour2=18, } #pragma warning restore CS1591 // 缺少对公共可见类型或成员的 XML 注释 ================================================ FILE: NewLife.RocketMQ/Models/MessageModels.cs ================================================ namespace NewLife.RocketMQ.Models; /// 消息模型。广播/集群 public enum MessageModels { /// 集群。消费组内各消费者分享数据 Clustering, /// 广播。消费组内各消费者各自消费全部 Broadcasting, } ================================================ FILE: NewLife.RocketMQ/MqBase.cs ================================================ using System.Collections.Concurrent; using System.Diagnostics; using System.Reflection; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Xml.Serialization; using NewLife.Log; using NewLife.Net; using NewLife.RocketMQ.Protocol; using NewLife.Serialization; namespace NewLife.RocketMQ.Client; /// 业务基类 public abstract class MqBase : DisposeBase { #region 属性 /// 名称 public String Name { get; set; } /// 名称服务器地址 public String NameServerAddress { get; set; } /// 消费组 /// 阿里云目前需要在Group前面带上实例ID并用【%】连接,组成路由Group[用来路由到实例Group] public String Group { get; set; } = "DEFAULT_PRODUCER"; /// rocketmq 默认主题 public static String DefaultTopic { get; } = "TBW102"; /// 主题 /// 阿里云目前需要在Topic前面带上实例ID并用【%】连接,组成路由Topic[用来路由到实例Topic] public String Topic { get; set; } = DefaultTopic; /// 默认的主题队列数量 public Int32 DefaultTopicQueueNums { get; set; } = 4; /// 本地IP地址 public String ClientIP { get; set; } = NetHelper.MyIP() + ""; ///// 本地端口 //public Int32 ClientPort { get; set; } /// 实例名 public String InstanceName { get; set; } = "DEFAULT"; ///// 客户端回调执行线程数。默认CPU数 //public Int32 ClientCallbackExecutorThreads { get; set; } = Environment.ProcessorCount; /// 拉取名称服务器间隔。默认30_000ms public Int32 PollNameServerInterval { get; set; } = 30_000; /// Broker心跳间隔。默认30_000ms public Int32 HeartbeatBrokerInterval { get; set; } = 30_000; /// 单元名称 public String UnitName { get; set; } /// 单元模式 public Boolean UnitMode { get; set; } /// 序列化类型。默认Json,支持RocketMQ二进制 public SerializeType SerializeType { get; set; } = SerializeType.JSON; /// 让通信层知道对方的版本号,响应方可以以此做兼容老版本等的特殊操作 public MQVersion Version { get; set; } = MQVersion.V4_9_7; /// SSL协议。默认None public SslProtocols SslProtocol { get; set; } = SslProtocols.None; /// X509证书。用于SSL连接时验证证书指纹,可以直接加载pem证书文件,未指定时不验证证书 /// /// 可以使用pfx证书文件,也可以使用pem证书文件。 /// 服务端必须指定证书。 /// /// /// var cert = new X509Certificate2("file", "pass"); /// public X509Certificate? Certificate { get; set; } /// 是否使用外部代理。有些RocketMQ的Broker部署在网关外部,需要使用映射地址,默认false public Boolean ExternalBroker { get; set; } /// 是否启用VIP通道。启用后使用Broker端口-2作为VIP通道连接,获得更高优先级,默认false /// /// RocketMQ的VIP通道使用Broker监听端口减2的端口,例如Broker端口为10911时VIP端口为10909。 /// VIP通道在高负载场景下可获得更高的处理优先级。 /// public Boolean VipChannelEnabled { get; set; } /// 是否可用 public Boolean Active { get; private set; } /// 代理集合 public IList Brokers => _NameServer?.Brokers.OrderBy(t => t.Name).ToList(); /// 云厂商适配器。用于统一签名认证和实例路由逻辑 /// /// 支持阿里云(AliyunProvider)、腾讯云(TencentProvider)、Apache ACL(AclProvider)等。 /// 设置后自动处理签名、实例ID路由等厂商特有逻辑。 /// public ICloudProvider CloudProvider { get; set; } /// 阿里云选项。使用阿里云RocketMQ的参数有些不一样 [Obsolete("请使用 CloudProvider = new AliyunProvider { ... } 替代")] public AliyunOptions Aliyun { get => _aliyunOptions; set { _aliyunOptions = value; // 自动同步到 CloudProvider if (value != null && !value.AccessKey.IsNullOrEmpty()) CloudProvider ??= AliyunProvider.FromOptions(value); } } private AliyunOptions _aliyunOptions; /// Apache RocketMQ ACL 客户端配置。在Borker服务器配置设置为AclEnable = true 时配置生效。 [Obsolete("请使用 CloudProvider = new AclProvider { ... } 替代")] public AclOptions AclOptions { get => _aclOptions; set { _aclOptions = value; if (value != null && !value.AccessKey.IsNullOrEmpty()) CloudProvider ??= AclProvider.FromOptions(value); } } private AclOptions _aclOptions; /// Json序列化主机 public IJsonHost JsonHost { get; set; } = JsonHelper.Default; /// 性能追踪器 public ITracer Tracer { get; set; } = DefaultTracer.Instance; /// 是否启用消息轨迹 public Boolean EnableMessageTrace { get; set; } #if NETSTANDARD2_1_OR_GREATER /// gRPC Proxy地址。设置后使用gRPC协议连接RocketMQ 5.x /// /// 格式如 http://host:8081 或 https://host:8081。 /// 设置此属性后,将使用gRPC协议替代Remoting协议,支持RocketMQ 5.x新特性。 /// public String GrpcProxyAddress { get; set; } /// gRPC消息服务客户端 protected Grpc.GrpcMessagingService _GrpcService; #endif private String _group; private String _topic; /// 名称服务器 protected NameClient _NameServer; #endregion #region 扩展属性 /// 客户端标识 public String ClientId { get { var str = $"{ClientIP}@{InstanceName}"; if (!UnitName.IsNullOrEmpty()) str += "@" + UnitName; return str; } } #endregion #region 构造 static MqBase() { // 输出当前版本 Assembly.GetExecutingAssembly().WriteVersion(); XTrace.WriteLine("RocketMQ文档:https://newlifex.com/core/rocketmq"); } /// 实例化 public MqBase() { InstanceName = Process.GetCurrentProcess().Id + ""; // 设置UnitName,避免一个进程多实例时冲突 //UnitName = Rand.Next() + ""; } /// 销毁 /// protected override void Dispose(Boolean disposing) { base.Dispose(disposing); #if NETSTANDARD2_1_OR_GREATER _GrpcService.TryDispose(); #endif _NameServer.TryDispose(); //foreach (var item in _Brokers) //{ // item.Value.TryDispose(); //} Stop(); } /// 友好字符串 /// public override String ToString() => _group; #endregion #region 基础方法 /// 应用配置 /// public virtual void Configure(MqSetting setting) { if (!setting.NameServer.IsNullOrEmpty()) NameServerAddress = setting.NameServer; if (!setting.Topic.IsNullOrEmpty()) Topic = setting.Topic; if (!setting.Group.IsNullOrEmpty()) Group = setting.Group; // 兼容旧版配置方式 #pragma warning disable CS0618 Aliyun ??= new AliyunOptions(); if (!setting.Server.IsNullOrEmpty()) Aliyun.Server = setting.Server; if (!setting.AccessKey.IsNullOrEmpty()) Aliyun.AccessKey = setting.AccessKey; if (!setting.SecretKey.IsNullOrEmpty()) Aliyun.SecretKey = setting.SecretKey; #pragma warning restore CS0618 } /// 开始 /// public Boolean Start() { if (Active) return true; _group = Group; _topic = Topic; if (Name.IsNullOrEmpty()) Name = Topic; // 解析阿里云实例ID(兼容旧版 AliyunOptions) if (CloudProvider is AliyunProvider ap) { var ns = NameServerAddress; if (ap.InstanceId.IsNullOrEmpty() && !ns.IsNullOrEmpty() && ns.Contains("MQ_INST_")) { ap.InstanceId = ns.Substring("://", "."); } } #pragma warning disable CS0618 else if (_aliyunOptions != null && !_aliyunOptions.AccessKey.IsNullOrEmpty()) { var ns = NameServerAddress; if (_aliyunOptions.InstanceId.IsNullOrEmpty() && !ns.IsNullOrEmpty() && ns.Contains("MQ_INST_")) { _aliyunOptions.InstanceId = ns.Substring("://", "."); } } #pragma warning restore CS0618 using var span = Tracer?.NewSpan($"mq:{Name}:Start"); try { // 通过 CloudProvider 转换 Topic/Group var provider = CloudProvider; if (provider != null) { Topic = provider.TransformTopic(Topic); Group = provider.TransformGroup(Group); } #pragma warning disable CS0618 else { // 兼容旧版:阿里云实例ID前缀 var ins = _aliyunOptions?.InstanceId; if (!ins.IsNullOrEmpty()) { if (!Topic.StartsWith(ins)) Topic = $"{ins}%{Topic}"; if (!Group.StartsWith(ins)) Group = $"{ins}%{Group}"; } } #pragma warning restore CS0618 OnStart(); } catch (Exception ex) { span?.SetError(ex, null); throw; } return Active = true; } /// 开始 protected virtual void OnStart() { #if NETSTANDARD2_1_OR_GREATER // 使用gRPC协议时,初始化gRPC客户端 if (!GrpcProxyAddress.IsNullOrEmpty()) { WriteLog("使用gRPC协议连接Proxy[{0}]", GrpcProxyAddress); var svc = new Grpc.GrpcMessagingService(GrpcProxyAddress) { Namespace = CloudProvider is AliyunProvider ap ? ap.InstanceId : null, Log = Log, Tracer = Tracer, }; // 设置认证信息 if (CloudProvider != null && !CloudProvider.AccessKey.IsNullOrEmpty()) { svc.Client.AccessKey = CloudProvider.AccessKey; svc.Client.SecretKey = CloudProvider.SecretKey; } svc.Client.ClientId = ClientId; // 查询路由验证连通性 var route = svc.QueryRouteAsync(Topic).ConfigureAwait(false).GetAwaiter().GetResult(); if (route.Status?.Code != Grpc.GrpcCode.OK) throw new InvalidOperationException($"gRPC QueryRoute failed: {route.Status}"); WriteLog("gRPC路由查询成功,发现[{0}]个队列", route.MessageQueues.Count); _GrpcService = svc; return; } #endif if (NameServerAddress.IsNullOrEmpty()) { // 通过 CloudProvider 获取 NameServer 地址 var addr = CloudProvider?.GetNameServerAddress(); if (!addr.IsNullOrEmpty()) { NameServerAddress = addr; } #pragma warning disable CS0618 else { // 兼容旧版:从阿里云 HTTP 接口获取 var server = _aliyunOptions?.Server; if (!server.IsNullOrEmpty() && server.StartsWithIgnoreCase("http")) { var http = new System.Net.Http.HttpClient(); var html = http.GetStringAsync(server).ConfigureAwait(false).GetAwaiter().GetResult(); if (!html.IsNullOrWhiteSpace()) NameServerAddress = html.Trim(); } } #pragma warning restore CS0618 } WriteLog("正在从名称服务器[{0}]查找该Topic所在Broker服务器地址列表", NameServerAddress); var client = new NameClient(ClientId, this) { Name = Name, Tracer = Tracer, Log = Log }; client.Start(); // 阻塞获取Broker地址,确保首次使用之前已经获取到Broker地址 var rs = client.GetRouteInfo(Topic); DefaultTopicQueueNums = Math.Min(DefaultTopicQueueNums, rs.Where(e => e.Permission.HasFlag(Permissions.Write) && e.WriteQueueNums > 0).Select(e => e.WriteQueueNums).First()); foreach (var item in rs) { WriteLog("发现Broker[{0}]: {1}, reads={2}, writes={3}", item.Name, item.Addresses.Join(), item.ReadQueueNums, item.WriteQueueNums); } _NameServer = client; } /// 停止 /// public void Stop() { if (!Active) return; using var span = Tracer?.NewSpan($"mq:{Name}:Stop"); try { OnStop(); } catch (Exception ex) { span?.SetError(ex, null); throw; } Active = false; } /// 停止 protected virtual void OnStop() { foreach (var item in _Brokers) { try { item.Value.UnRegisterClient(Group); item.Value.TryDispose(); } catch (Exception ex) { XTrace.WriteException(ex); } } _Brokers.Clear(); } #endregion #region 收发信息 private readonly ConcurrentDictionary _Brokers = new(); /// 获取代理客户端 /// /// protected BrokerClient GetBroker(String name) { if (String.IsNullOrEmpty(name)) throw new ArgumentException($"“{nameof(name)}”不能为 null 或空。", nameof(name)); if (_Brokers.TryGetValue(name, out var client)) return client; var bk = Brokers?.FirstOrDefault(e => name == null || e.Name == name); if (bk == null) return null; lock (_Brokers) { if (_Brokers.TryGetValue(name, out client)) return client; var addrs = bk.Addresses.ToArray(); if (ExternalBroker) { // broker可能在内网,转为公网地址 var uri = new NetUri(NameServerAddress.Split(";").FirstOrDefault()); var ext = uri.Host; if (ext.IsNullOrEmpty()) ext = uri.Address.ToString(); for (var i = 0; i < addrs.Length; i++) { var addr = addrs[i]; if (addr.StartsWithIgnoreCase("127.", "10.", "192.", "172.") && !ext.IsNullOrEmpty()) { var p = addr.IndexOf(':'); addrs[i] = p > 0 ? ext + addr[p..] : ext; } } } // 实例化客户端 client = CreateBroker(bk.Name, addrs); client.Start(); // 尝试添加 _Brokers.TryAdd(name, client); return client; } } /// 创建Broker客户端通信 /// /// /// protected virtual BrokerClient CreateBroker(String name, String[] addrs) { var client = new BrokerClient(addrs) { Id = ClientId, Name = name, Config = this, Tracer = Tracer, Log = ClientLog, }; client.Received += (s, e) => { e.Arg = OnReceive(e.Arg); }; return client; } /// Broker客户端集合 public ICollection Clients => _Brokers.Values; /// 收到命令 /// protected virtual Command OnReceive(Command cmd) => null; #endregion #region 业务方法 /// 更新或创建主题。重复执行时为更新 /// 主题 /// 队列数 /// public virtual Int32 CreateTopic(String topic, Int32 queueNum, Int32 topicSysFlag = 0) { var header = new { topic, defaultTopic = Topic, readQueueNums = queueNum, writeQueueNums = queueNum, perm = 7, topicFilterType = "SINGLE_TAG", topicSysFlag, order = false, }; var count = 0; using var span = Tracer?.NewSpan($"mq:{Name}:CreateTopic", header); try { // 在所有Broker上创建Topic foreach (var item in Brokers) { WriteLog("在Broker[{0}]上创建主题:{1}", item.Name, topic); try { var bk = GetBroker(item.Name); var rs = bk.Invoke(RequestCode.UPDATE_AND_CREATE_TOPIC, null, header); if (rs != null && rs.Header.Code == (Int32)ResponseCode.SUCCESS) count++; } catch (Exception ex) { XTrace.WriteException(ex); } } } catch (Exception ex) { span?.SetError(ex, null); throw; } return count; } /// 删除主题 /// 主题 public virtual Int32 DeleteTopic(String topic) { var count = 0; using var span = Tracer?.NewSpan($"mq:{Name}:DeleteTopic", topic); try { // 从所有Broker上删除 foreach (var item in Brokers) { WriteLog("在Broker[{0}]上删除主题:{1}", item.Name, topic); try { var bk = GetBroker(item.Name); var rs = bk.Invoke(RequestCode.DELETE_TOPIC_IN_BROKER, null, new { topic }); if (rs != null && rs.Header.Code == (Int32)ResponseCode.SUCCESS) count++; } catch (Exception ex) { XTrace.WriteException(ex); } } // 从NameServer上删除 try { _NameServer?.Invoke(RequestCode.DELETE_TOPIC_IN_NAMESRV, null, new { topic }); } catch (Exception ex) { XTrace.WriteException(ex); } } catch (Exception ex) { span?.SetError(ex, null); throw; } return count; } /// 创建或更新消费组 /// 消费组名 /// 是否允许广播消费 /// 最大重试次数 /// 重试队列数 public virtual Int32 CreateSubscriptionGroup(String groupName, Boolean consumeBroadcastEnable = true, Int32 retryMaxTimes = 16, Int32 retryQueueNums = 1) { var count = 0; using var span = Tracer?.NewSpan($"mq:{Name}:CreateSubscriptionGroup", groupName); try { var header = new { groupName, consumeBroadcastEnable, consumeEnable = true, retryMaxTimes, retryQueueNums, }; foreach (var item in Brokers) { WriteLog("在Broker[{0}]上创建消费组:{1}", item.Name, groupName); try { var bk = GetBroker(item.Name); var rs = bk.Invoke(RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP, null, header); if (rs != null && rs.Header.Code == (Int32)ResponseCode.SUCCESS) count++; } catch (Exception ex) { XTrace.WriteException(ex); } } } catch (Exception ex) { span?.SetError(ex, null); throw; } return count; } /// 删除消费组 /// 消费组名 public virtual Int32 DeleteSubscriptionGroup(String groupName) { var count = 0; using var span = Tracer?.NewSpan($"mq:{Name}:DeleteSubscriptionGroup", groupName); try { foreach (var item in Brokers) { WriteLog("在Broker[{0}]上删除消费组:{1}", item.Name, groupName); try { var bk = GetBroker(item.Name); var rs = bk.Invoke(RequestCode.DELETE_SUBSCRIPTIONGROUP, null, new { groupName }); if (rs != null && rs.Header.Code == (Int32)ResponseCode.SUCCESS) count++; } catch (Exception ex) { XTrace.WriteException(ex); } } } catch (Exception ex) { span?.SetError(ex, null); throw; } return count; } /// 按消息ID查看消息 /// 消息编号 /// public virtual MessageExt ViewMessage(String msgId) { using var span = Tracer?.NewSpan($"mq:{Name}:ViewMessage", msgId); try { foreach (var item in Brokers) { try { var bk = GetBroker(item.Name); var rs = bk.Invoke(RequestCode.VIEW_MESSAGE_BY_ID, null, new { offset = msgId }, true); if (rs?.Payload != null) { var msgs = MessageExt.ReadAll(rs.Payload); if (msgs?.Count > 0) return msgs[0]; } } catch { } } } catch (Exception ex) { span?.SetError(ex, null); throw; } return null; } /// 获取集群信息 /// public virtual IDictionary GetClusterInfo() { using var span = Tracer?.NewSpan($"mq:{Name}:GetClusterInfo"); try { var rs = _NameServer?.Invoke(RequestCode.GET_BROKER_CLUSTER_INFO, null); if (rs?.Payload != null) return rs.ReadBodyAsJson(); } catch (Exception ex) { span?.SetError(ex, null); throw; } return null; } /// 获取消费统计信息 /// 消费组名 /// 主题。默认使用当前Topic /// 消费统计数据的JSON字典 public virtual IDictionary GetConsumeStats(String group, String topic = null) { if (String.IsNullOrEmpty(group)) group = Group; if (String.IsNullOrEmpty(topic)) topic = Topic; using var span = Tracer?.NewSpan($"mq:{Name}:GetConsumeStats", group); try { foreach (var item in Brokers) { try { var bk = GetBroker(item.Name); var rs = bk.Invoke(RequestCode.GET_CONSUME_STATS, null, new { consumerGroup = group, topic, }, true); if (rs?.Payload != null) return rs.ReadBodyAsJson(); } catch { } } } catch (Exception ex) { span?.SetError(ex, null); throw; } return null; } /// 获取Topic统计信息 /// 主题。默认使用当前Topic /// 主题统计数据的JSON字典 public virtual IDictionary GetTopicStatsInfo(String topic = null) { if (String.IsNullOrEmpty(topic)) topic = Topic; using var span = Tracer?.NewSpan($"mq:{Name}:GetTopicStatsInfo", topic); try { foreach (var item in Brokers) { try { var bk = GetBroker(item.Name); var rs = bk.Invoke(RequestCode.GET_TOPIC_STATS_INFO, null, new { topic }, true); if (rs?.Payload != null) return rs.ReadBodyAsJson(); } catch { } } } catch (Exception ex) { span?.SetError(ex, null); throw; } return null; } /// 按Key查询消息 /// 主题 /// 消息Key /// 最大返回数量 /// 起始时间戳(毫秒) /// 结束时间戳(毫秒) /// 匹配的消息列表 public virtual IList QueryMessageByKey(String topic, String key, Int32 maxNum = 32, Int64 beginTimestamp = 0, Int64 endTimestamp = 0) { if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); if (String.IsNullOrEmpty(topic)) topic = Topic; using var span = Tracer?.NewSpan($"mq:{Name}:QueryMessageByKey", key); try { foreach (var item in Brokers) { try { var bk = GetBroker(item.Name); var rs = bk.Invoke(RequestCode.QUERY_MESSAGE, null, new { topic, key, maxNum, beginTimestamp, endTimestamp, }, true); if (rs?.Payload != null) { var msgs = MessageExt.ReadAll(rs.Payload); if (msgs?.Count > 0) return msgs; } } catch { } } } catch (Exception ex) { span?.SetError(ex, null); throw; } return []; } /// 注册消息过滤服务器。将一个外部过滤服务器注册到Broker上,用于服务端消息过滤 /// 过滤服务器地址,格式如 ip:port /// 注册成功的Broker数量 public virtual Int32 RegisterFilterServer(String filterServerAddr) { if (String.IsNullOrEmpty(filterServerAddr)) throw new ArgumentNullException(nameof(filterServerAddr)); var count = 0; using var span = Tracer?.NewSpan($"mq:{Name}:RegisterFilterServer", filterServerAddr); try { foreach (var item in Brokers) { WriteLog("在Broker[{0}]上注册过滤服务器:{1}", item.Name, filterServerAddr); try { var bk = GetBroker(item.Name); var rs = bk.Invoke(RequestCode.REGISTER_FILTER_SERVER, null, new { filterServerAddr }, true); if (rs != null) count++; } catch (Exception ex) { WriteLog("注册过滤服务器失败[{0}]:{1}", item.Name, ex.Message); } } } catch (Exception ex) { span?.SetError(ex, null); throw; } return count; } #endregion #if NETSTANDARD2_1_OR_GREATER #region gRPC公共方法 /// 通过gRPC协议查询主题路由 /// 主题。默认使用当前Topic /// 取消通知 /// 路由查询结果 public async Task QueryRouteViaGrpcAsync(String topic = null, CancellationToken cancellationToken = default) { if (_GrpcService == null) throw new InvalidOperationException("gRPC service not initialized. Set GrpcProxyAddress first."); return await _GrpcService.QueryRouteAsync(topic ?? Topic, cancellationToken).ConfigureAwait(false); } /// 通过gRPC协议上报客户端资源信息(Telemetry) /// 客户端设置 /// 取消通知 /// 服务端返回的Telemetry命令 public async Task TelemetryViaGrpcAsync(Grpc.GrpcSettings settings, CancellationToken cancellationToken = default) { if (_GrpcService == null) throw new InvalidOperationException("gRPC service not initialized. Set GrpcProxyAddress first."); using var span = Tracer?.NewSpan($"mq:{Name}:Telemetry:grpc"); try { return await _GrpcService.TelemetryAsync(settings, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { span?.SetError(ex, null); throw; } } /// 通过gRPC协议通知客户端终止 /// 消费组 /// 取消通知 /// public async Task NotifyClientTerminationViaGrpcAsync(String group = null, CancellationToken cancellationToken = default) { if (_GrpcService == null) throw new InvalidOperationException("gRPC service not initialized. Set GrpcProxyAddress first."); using var span = Tracer?.NewSpan($"mq:{Name}:NotifyTermination:grpc"); try { return await _GrpcService.NotifyClientTerminationAsync(group ?? Group, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { span?.SetError(ex, null); throw; } } #endregion #endif #region 日志 /// 日志 public ILog Log { get; set; } = Logger.Null; /// 客户端日志。详细的指令收发日志,仅用于调试 public ILog ClientLog { get; set; } /// 写日志 /// /// public void WriteLog(String format, params Object[] args) => Log?.Info($"[{this}]" + format, args); #endregion } ================================================ FILE: NewLife.RocketMQ/MqSetting.cs ================================================ using System.ComponentModel; using NewLife.Configuration; namespace NewLife.RocketMQ; /// RocketMQ配置 [Config("RocketMQ")] public class MqSetting : Config { /// 名称服务器。将从该地址获取Broker [Description("名称服务器。将从该地址获取Broker")] public String NameServer { get; set; } /// 主题 [Description("主题")] public String Topic { get; set; } /// 消费组 [Description("消费组")] public String Group { get; set; } /// 获取名称服务器地址的http地址,阿里云专用 http://onsaddr-internet.aliyun.com/rocketmq/nsaddr4client-internet [Description("获取名称服务器地址的http地址,阿里云专用 http://onsaddr-internet.aliyun.com/rocketmq/nsaddr4client-internet")] public String Server { get; set; } /// 访问令牌,阿里云专用 [Description("访问令牌,阿里云专用")] public String AccessKey { get; set; } /// 访问密钥,阿里云专用 [Description("访问密钥,阿里云专用")] public String SecretKey { get; set; } } ================================================ FILE: NewLife.RocketMQ/NameClient.cs ================================================ using System.Collections.Concurrent; using NewLife.Data; using NewLife.Log; using NewLife.Net; using NewLife.RocketMQ.Client; using NewLife.RocketMQ.Protocol; using NewLife.Threading; namespace NewLife.RocketMQ; /// 连接名称服务器的客户端 public class NameClient : ClusterClient { #region 属性 /// Broker集合 public IList Brokers { get; private set; } = []; /// 代理改变时触发 public event EventHandler OnBrokerChange; /// 额外需要轮询路由的主题列表。多Topic订阅时使用 public String[] ExtraTopics { get; set; } private readonly ConcurrentDictionary> _topicBrokers = new(); #endregion #region 构造 /// 实例化 /// /// public NameClient(String id, MqBase config) { Id = id; Config = config; } #endregion #region 方法 /// protected override void Dispose(Boolean disposing) { if (disposing) _timer?.Dispose(); base.Dispose(disposing); } /// 启动 protected override void OnStart() { var cfg = Config; if (cfg.NameServerAddress.IsNullOrEmpty()) throw new ArgumentNullException(nameof(cfg.NameServerAddress), "未指定NameServer地址"); var ss = cfg.NameServerAddress.Split(";"); var list = new List(); foreach (var item in ss) { var uri = new NetUri(item); if (uri.Type == NetType.Unknown) uri.Type = NetType.Tcp; list.Add(uri); } Servers = list.ToArray(); base.OnStart(); _timer ??= new TimerX(DoWork, null, cfg.PollNameServerInterval, cfg.PollNameServerInterval) { Async = true }; } #endregion #region 命令 private TimerX _timer; private String _lastBrokers; private void DoWork(Object state) { var rs = GetRouteInfo(Config.Topic); var str = rs?.Join(",", e => $"{e.Name}={e.Addresses.Join()}"); if (str != _lastBrokers) { _lastBrokers = str; foreach (var item in rs) { WriteLog("发现Broker[{0}]: {1}, reads={2}, writes={3}", item.Name, item.Addresses.Join(), item.ReadQueueNums, item.WriteQueueNums); } } // 轮询额外主题的路由 var extras = ExtraTopics; if (extras != null && extras.Length > 0) { foreach (var topic in extras) { if (!String.IsNullOrEmpty(topic) && topic != Config.Topic) { try { GetRouteInfo(topic); } catch (Exception ex) { WriteLog("获取主题[{0}]路由失败:{1}", topic, ex.Message); } } } } } /// 获取指定主题的Broker信息(从缓存) /// 主题名 /// public IList GetTopicBrokers(String topic) { if (_topicBrokers.TryGetValue(topic, out var list)) return list; return Brokers; } /// 获取主题的路由信息,含登录验证 /// /// public IList GetRouteInfo(String topic) { using var span = Tracer?.NewSpan($"mq:{topic}:GetRouteInfo", topic); try { // 发送命令 var rs = Invoke(RequestCode.GET_ROUTEINTO_BY_TOPIC, null, new { topic }); span?.AppendTag(rs.Payload?.ToStr()); var js = rs.ReadBodyAsJson(); var list = new List(); // 解析broker集群地址 if (js["brokerDatas"] is IList bs) { foreach (IDictionary item in bs) { var name = item["brokerName"] + ""; var cluster = item["cluster"] + ""; if (item["brokerAddrs"] is IDictionary addrs) { // key==0为Master,key>0为Slave var addresses = addrs.Select(e => e.Value + "").ToArray(); var isMaster = addrs.ContainsKey("0"); var masterAddr = addrs.TryGetValue("0", out var ma) ? ma + "" : null; var slaveAddrs = addrs.Where(e => e.Key != "0").Select(e => e.Value + "").ToArray(); // 优先 Master 在前 var ordered = new List(); if (!String.IsNullOrEmpty(masterAddr)) ordered.Add(masterAddr); ordered.AddRange(slaveAddrs); list.Add(new BrokerInfo { Name = name, Cluster = cluster, Addresses = ordered.ToArray(), MasterAddress = masterAddr, SlaveAddresses = slaveAddrs, IsMaster = isMaster }); } } } // 解析队列集合 if (js["queueDatas"] is IList bs2) { foreach (IDictionary item in bs2) { var name = item["brokerName"] + ""; var bk = list.FirstOrDefault(e => e.Name == name); if (bk == null) list.Add(bk = new BrokerInfo { Name = name }); bk.Permission = (Permissions)item["perm"].ToInt(); bk.ReadQueueNums = item["readQueueNums"].ToInt(); bk.WriteQueueNums = item["writeQueueNums"].ToInt(); bk.TopicSynFlag = item["topicSysFlag"].ToInt(); } } // 如果完全相等,则直接返回。否则重新平衡队列 if (Brokers.SequenceEqual(list)) return list.OrderBy(t => t.Name).ToList(); Brokers = list; // 缓存每个主题的Broker信息 if (!String.IsNullOrEmpty(topic)) _topicBrokers[topic] = list; // 结果检查 if (list.Count == 0) { WriteLog("未能找到主题[{0}]的任何Broker信息,可能是Topic或NameServer错误,也可能是不支持的服务端版本。服务端返回如下:", topic); WriteLog(rs.Payload.ToStr()); } // 有改变,重新平衡队列 OnBrokerChange?.Invoke(this, EventArgs.Empty); return list.OrderBy(t => t.Name).ToList(); } catch (ResponseException ex) { if (!MqBase.DefaultTopic.Equals(topic) && ResponseCode.TOPIC_NOT_EXIST.Equals(ex.Code)) { WriteLog("未能找到主题[{0}],将读取默认主题[TBW102]的替代。", topic); var rs = GetRouteInfo(MqBase.DefaultTopic); if (rs != null && rs.Count() > 0) { if (rs[0].ReadQueueNums > Config.DefaultTopicQueueNums) { foreach (var item in rs) { item.WriteQueueNums = Config.DefaultTopicQueueNums; item.ReadQueueNums = Config.DefaultTopicQueueNums; } } } return rs; } else { span?.SetError(ex, null); throw; } } catch (Exception ex) { span?.SetError(ex, null); throw; } } #endregion } ================================================ FILE: NewLife.RocketMQ/NewLife.RocketMQ.csproj ================================================ net45;net461;netstandard2.0;netstandard2.1 RocketMQ纯托管客户端 企业级纯托管 RocketMQ 客户端,双协议支持 Remoting 4.x/5.x 和 gRPC 5.x Proxy,零外部依赖,无需 Java、gRPC、Protobuf 第三方库,统一适配阿里云、华为云、腾讯云及 Apache ACL,完整企业级特性(消费重试、死信队列、事务消息、顺序消费、Pop 消费),十亿级项目验证 新生命开发团队 ©2002-2026 新生命开发团队 3.0 $([System.DateTime]::Now.ToString(`yyyy.MMdd`)) $(VersionPrefix).$(VersionSuffix) $(Version) $(VersionPrefix).* false ..\Bin True enable latest True ..\Doc\newlife.snk latest CA2007 $(AssemblyName) $(Company) https://newlifex.com/core/rocketmq leaf.png https://github.com/NewLifeX/NewLife.RocketMQ git 新生命团队;X组件;NewLife;RocketMQ;纯托管;零依赖;Remoting;gRPC;阿里云;华为云;腾讯云;消息队列;MQ;分布式;事务消息;延迟消息;顺序消息;死信队列;$(AssemblyName) 修复Pop/Ack/ChangeInvisibleTime操作缺少queueId参数导致服务端异常;新增MessageExt便利方法简化消息属性访问;升级基础组件 MIT true true true snupkg Readme.MD all runtime; build; native; contentfiles; analyzers; buildtransitive True \ ================================================ FILE: NewLife.RocketMQ/Producer.cs ================================================ using System.Collections.Concurrent; using System.Globalization; using NewLife.Log; using NewLife.Reflection; using NewLife.RocketMQ.Client; using NewLife.RocketMQ.Common; using NewLife.RocketMQ.MessageTrace; using NewLife.RocketMQ.Models; using NewLife.RocketMQ.Protocol; namespace NewLife.RocketMQ; /// 生产者 public class Producer : MqBase { private const Int32 CommitLogOffsetHexLength = 16; #region 属性 /// 负载均衡。发布消息时,分发到各个队列的负载均衡算法,默认使用带权重的轮询 public ILoadBalance LoadBalance { get; set; } //public Int32 DefaultTopicQueueNums { get; set; } = 4; //public Int32 SendMsgTimeout { get; set; } = 3_000; //public Int32 CompressMsgBodyOverHowmuch { get; set; } = 4096; /// 发送消息失败时的重试次数。默认3次 public Int32 RetryTimesWhenSendFailed { get; set; } = 3; //public Int32 RetryTimesWhenSendAsyncFailed { get; set; } = 2; //public Boolean RetryAnotherBrokerWhenNotStoreOK { get; set; } /// 最大消息大小。默认4*1024*1024 public Int32 MaxMessageSize { get; set; } = 4 * 1024 * 1024; /// 消息体压缩阈值(字节)。超过该大小自动ZLIB压缩,默认4096。设为0则禁用压缩 public Int32 CompressOverBytes { get; set; } = 4096; private readonly IList _sendMessageHooks = new List(); private AsyncTraceDispatcher _traceDispatcher; /// 请求超时时间。默认3000ms public Int32 RequestTimeout { get; set; } = 3_000; /// 事务回查委托。Broker发起事务回查时调用,参数为消息和事务ID,返回事务状态 public Func OnCheckTransaction; /// 异步事务回查委托。Broker发起事务回查时调用,参数为消息、事务ID和取消令牌,返回事务状态 public Func> OnCheckTransactionAsync; private readonly ConcurrentDictionary> _requestCallbacks = new(); private Consumer _replyConsumer; private String _replyTopic; #endregion #region 基础方法 /// 启动 /// protected override void OnStart() { base.OnStart(); if (EnableMessageTrace) { _traceDispatcher = new AsyncTraceDispatcher(); _traceDispatcher.Start(NameServerAddress); _sendMessageHooks.Add(new MessageTraceHook(_traceDispatcher)); } LoadBalance ??= new WeightRoundRobin(); if (_NameServer != null) { _NameServer.OnBrokerChange += (s, e) => { _brokers = null; //_robin = null; LoadBalance.Ready = false; }; } // 初始化回复消息消费者 _replyTopic = $"{Topic}_REPLY_{ClientId}"; } /// 停止 protected override void OnStop() { // 停止回复消息消费者 if (_replyConsumer != null) { _replyConsumer.Stop(); _replyConsumer.Dispose(); _replyConsumer = null; } base.OnStop(); } #endregion #region 发布消息(普通/顺序) /// 发送消息 /// 消息体 /// 目标队列。指定时可实现顺序发布(通过SelectQueue获取),默认未指定并自动选择队列 /// /// public virtual SendResult Publish(Message message, MessageQueue queue, Int32 timeout = -1) { // 构造请求头 var header = CreateHeader(message); for (var i = 0; i <= RetryTimesWhenSendFailed; i++) { // 选择队列分片 var mq = queue ?? SelectQueue(); mq.Topic = Topic; header.QueueId = mq.QueueId; header.BrokerName = mq.BrokerName; // 性能埋点 using var span = Tracer?.NewSpan($"mq:{Name}:Publish", message.BodyString); span?.AppendTag($"queue={mq}"); SendMessageContext context = null; try { // 根据队列获取Broker客户端 var bk = GetBroker(mq.BrokerName); context = new SendMessageContext { ProducerGroup = Group, Message = message, Mq = mq, BrokerAddr = bk.Name, }; foreach (var hook in _sendMessageHooks) { try { hook.ExecuteHookBefore(context); } catch (Exception e) { if (Log.Enable) Log.Error(e.Message); } } var rs = bk.Invoke(RequestCode.SEND_MESSAGE_V2, message.Body, header.GetProperties(), true); // 包装结果 var result = new SendResult { Queue = mq, Header = rs.Header, Status = (ResponseCode)rs.Header.Code switch { ResponseCode.SUCCESS => SendStatus.SendOK, ResponseCode.FLUSH_DISK_TIMEOUT => SendStatus.FlushDiskTimeout, ResponseCode.FLUSH_SLAVE_TIMEOUT => SendStatus.FlushSlaveTimeout, ResponseCode.SLAVE_NOT_AVAILABLE => SendStatus.SlaveNotAvailable, _ => throw rs.Header.CreateException(), } }; result.Read(rs.Header?.ExtFields); context.SendResult = result; foreach (var hook in _sendMessageHooks) { try { hook.ExecuteHookAfter(context); } catch (Exception e) { if (Log.Enable) Log.Error(e.Message); } } span?.AppendTag($"Status={result.Status}"); span?.AppendTag($"MsgId={result.MsgId}"); span?.AppendTag($"OffsetMsgId={result.OffsetMsgId}"); span?.AppendTag($"QueueOffset={result.QueueOffset}"); span?.AppendTag($"TransactionId={result.TransactionId}"); if (Log != null && Log.Level <= LogLevel.Debug) WriteLog("{0}", result); return result; } catch (Exception ex) { if (context != null) { context.E = ex; foreach (var hook in _sendMessageHooks) { try { hook.ExecuteHookAfter(context); } catch (Exception e) { if (Log.Enable) Log.Error(e.Message); } } } // 如果网络异常,则延迟重发 if (i < RetryTimesWhenSendFailed) { Thread.Sleep(1000); continue; } span?.SetError(ex, message); throw; } } return null; } /// 发布消息 /// /// /// public virtual SendResult Publish(Object body, Int32 timeout = -1) => Publish(CreateMessage(body), null, timeout); /// 发布消息 /// /// /// /// public virtual SendResult Publish(Object body, String tags, Int32 timeout = -1) { var message = CreateMessage(body); message.Tags = tags; return Publish(message, null, timeout); } /// 发布消息 /// /// 传null则为空 /// 传null则为空 /// /// public virtual SendResult Publish(Object body, String tags, String keys, Int32 timeout = -1) { var message = CreateMessage(body); message.Tags = tags; message.Keys = keys; return Publish(message, null, timeout); } /// 发布事务消息(半消息) /// 消息体 /// 目标队列 /// /// public virtual SendResult PublishTransaction(Message message, MessageQueue queue = null, Int32 timeout = -1) { if (message is null) throw new ArgumentNullException(nameof(message)); message.Properties["TRAN_MSG"] = "true"; message.Properties["PGROUP"] = Group; return Publish(message, queue, timeout); } /// 发布事务消息(半消息) /// /// /// /// /// public virtual SendResult PublishTransaction(Object body, String tags = null, String keys = null, Int32 timeout = -1) { var message = CreateMessage(body); message.Tags = tags; message.Keys = keys; return PublishTransaction(message, null, timeout); } #endregion #region 异步发布消息 /// 发布消息 /// 消息体 /// 目标队列。指定时可实现顺序发布(通过SelectQueue获取),默认未指定并自动选择队列 /// public virtual async Task PublishAsync(Message message, MessageQueue queue, CancellationToken cancellationToken = default) { if (message is null) throw new ArgumentNullException(nameof(message)); #if NETSTANDARD2_1_OR_GREATER // gRPC模式 if (_GrpcService != null) return await PublishViaGrpcAsync(message, cancellationToken).ConfigureAwait(false); #endif // 构造请求头 var header = CreateHeader(message); for (var i = 0; i <= RetryTimesWhenSendFailed; i++) { // 选择队列分片 var mq = queue ?? SelectQueue(); mq.Topic = Topic; header.QueueId = mq.QueueId; // 性能埋点 using var span = Tracer?.NewSpan($"mq:{Name}:PublishAsync", message.BodyString); try { var bk = GetBroker(mq.BrokerName); var context = new SendMessageContext(); foreach (var hook in _sendMessageHooks) { try { hook.ExecuteHookBefore(context); } catch (Exception e) { if (Log.Enable) Log.Error(e.Message); } } var rs = await bk.InvokeAsync(RequestCode.SEND_MESSAGE_V2, message.Body, header.GetProperties(), true, cancellationToken).ConfigureAwait(false); foreach (var hook in _sendMessageHooks) { try { hook.ExecuteHookAfter(context); } catch (Exception e) { if (Log.Enable) Log.Error(e.Message); } } // 包装结果 var sendResult = new SendResult { Queue = mq, Header = rs.Header, Status = (ResponseCode)rs.Header.Code switch { ResponseCode.SUCCESS => SendStatus.SendOK, ResponseCode.FLUSH_DISK_TIMEOUT => SendStatus.FlushDiskTimeout, ResponseCode.FLUSH_SLAVE_TIMEOUT => SendStatus.FlushSlaveTimeout, ResponseCode.SLAVE_NOT_AVAILABLE => SendStatus.SlaveNotAvailable, _ => throw rs.Header.CreateException(), } }; sendResult.Read(rs.Header?.ExtFields); return sendResult; } catch (Exception ex) { // 如果网络异常,则延迟重发 if (i < RetryTimesWhenSendFailed) { await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); continue; } span?.SetError(ex, message); throw; } } return null; } /// 异步发布事务消息(半消息) /// 消息体 /// 目标队列 /// 取消令牌 /// public virtual Task PublishTransactionAsync(Message message, MessageQueue queue = null, CancellationToken cancellationToken = default) { if (message is null) throw new ArgumentNullException(nameof(message)); message.Properties["TRAN_MSG"] = "true"; message.Properties["PGROUP"] = Group; return PublishAsync(message, queue, cancellationToken); } /// 发布消息 /// /// public virtual Task PublishAsync(Object body) => PublishAsync(CreateMessage(body), null); /// 发布消息 /// /// 传null则为空 /// 传null则为空 /// public virtual Task PublishAsync(Object body, String tags, String keys) { var message = CreateMessage(body); message.Tags = tags; message.Keys = keys; return PublishAsync(message, null); } #if NETSTANDARD2_1_OR_GREATER /// 通过gRPC协议发送消息 /// 消息 /// 取消通知 /// private async Task PublishViaGrpcAsync(Message message, CancellationToken cancellationToken) { using var span = Tracer?.NewSpan($"mq:{Name}:PublishAsync:grpc", message.BodyString); try { var keys = message.Keys?.Split(',').Where(k => !String.IsNullOrEmpty(k)).ToList(); var rs = await _GrpcService.SendMessageAsync( Topic, message.Body, tag: message.Tags, keys: keys, properties: message.Properties.Count > 0 ? message.Properties : null, cancellationToken: cancellationToken ).ConfigureAwait(false); if (rs.Status?.Code != Grpc.GrpcCode.OK) throw new InvalidOperationException($"gRPC SendMessage failed: {rs.Status}"); var entry = rs.Entries.FirstOrDefault(); var result = new SendResult { Status = SendStatus.SendOK, MsgId = entry?.MessageId, TransactionId = entry?.TransactionId, QueueOffset = entry?.Offset ?? 0, }; return result; } catch (Exception ex) { span?.SetError(ex, message); throw; } } #endif #endregion #region 发布单向消息 /// 发送消息,不等结果 /// 消息体 /// 目标队列。指定时可实现顺序发布(通过SelectQueue获取),默认未指定并自动选择队列 /// public virtual SendResult PublishOneway(Message message, MessageQueue queue) { // 构造请求头 var header = CreateHeader(message); for (var i = 0; i <= RetryTimesWhenSendFailed; i++) { // 选择队列分片 var mq = queue ?? SelectQueue(); mq.Topic = Topic; header.QueueId = mq.QueueId; // 性能埋点 using var span = Tracer?.NewSpan($"mq:{Name}:PublishOneway", message.BodyString); try { // 根据队列获取Broker客户端 var bk = GetBroker(mq.BrokerName); var context = new SendMessageContext { ProducerGroup = Group, Message = message, Mq = mq, BrokerAddr = bk.Name, }; foreach (var hook in _sendMessageHooks) { try { hook.ExecuteHookBefore(context); } catch (Exception e) { if (Log.Enable) Log.Error(e.Message); } } var rs = bk.InvokeOneway(RequestCode.SEND_MESSAGE_V2, message.Body, header.GetProperties()); // 包装结果 var sendResult = new SendResult { Queue = mq, Header = rs.Header, Status = rs.Header.Code switch { -1 => SendStatus.SendError, _ => SendStatus.SendOK, } }; context.SendResult = sendResult; foreach (var hook in _sendMessageHooks) { try { hook.ExecuteHookAfter(context); } catch (Exception e) { if (Log.Enable) Log.Error(e.Message); } } if (Log != null && Log.Level <= LogLevel.Debug) WriteLog("{0}", sendResult); return sendResult; } catch (Exception ex) { // 如果网络异常,则延迟重发 if (i < RetryTimesWhenSendFailed) { Thread.Sleep(1000); continue; } span?.SetError(ex, message); throw; } } return null; } /// 发布消息,不等结果 /// /// /// public virtual void PublishOneway(Object body, String tags = null) { var message = CreateMessage(body); message.Tags = tags; PublishOneway(message, null); } #endregion #region 批量发布消息 /// 批量发布消息 /// 消息集合 /// 超时时间 /// public virtual SendResult PublishBatch(IList messages, Int32 timeout = -1) { if (messages == null || messages.Count == 0) throw new ArgumentException("消息集合不能为空", nameof(messages)); // 编码批量消息体 var ms = new MemoryStream(); var bn = new NewLife.Serialization.Binary { Stream = ms, IsLittleEndian = false }; foreach (var msg in messages) { msg.Topic ??= Topic; var body = msg.Body ?? new Byte[0]; var props = msg.GetProperties() ?? ""; var propsBytes = props.GetBytes(); // 按照 RocketMQ 批量消息编码格式 // TotalSize(4) + MagicCode(4) + BodyCRC(4) + Flag(4) + BodyLen(4) + Body + PropsLen(2) + Props var totalSize = 4 + 4 + 4 + 4 + 4 + body.Length + 2 + propsBytes.Length; bn.Write(totalSize); bn.Write(0); // MagicCode bn.Write(0); // BodyCRC bn.Write(msg.Flag); bn.Write(body.Length); ms.Write(body, 0, body.Length); bn.Write((Int16)propsBytes.Length); ms.Write(propsBytes, 0, propsBytes.Length); } var batchBody = ms.ToArray(); // 使用第一条消息的属性作为批量消息头 var firstMsg = messages[0]; var header = new SendMessageRequestHeader { ProducerGroup = Group, Topic = Topic, SysFlag = 0, BornTimestamp = DateTime.UtcNow.ToLong(), Flag = firstMsg.Flag, Properties = firstMsg.GetProperties(), ReconsumeTimes = 0, UnitMode = UnitMode, Batch = true, DefaultTopic = DefaultTopic, DefaultTopicQueueNums = DefaultTopicQueueNums }; // 选择队列分片 var mq = SelectQueue(); if (mq == null) return null; mq.Topic = Topic; header.QueueId = mq.QueueId; header.BrokerName = mq.BrokerName; using var span = Tracer?.NewSpan($"mq:{Name}:PublishBatch", messages.Count); try { var bk = GetBroker(mq.BrokerName); var rs = bk.Invoke(RequestCode.SEND_BATCH_MESSAGE, batchBody, header.GetProperties(), true); var result = new SendResult { Queue = mq, Header = rs.Header, Status = (ResponseCode)rs.Header.Code switch { ResponseCode.SUCCESS => SendStatus.SendOK, ResponseCode.FLUSH_DISK_TIMEOUT => SendStatus.FlushDiskTimeout, ResponseCode.FLUSH_SLAVE_TIMEOUT => SendStatus.FlushSlaveTimeout, ResponseCode.SLAVE_NOT_AVAILABLE => SendStatus.SlaveNotAvailable, _ => throw rs.Header.CreateException(), } }; result.Read(rs.Header?.ExtFields); return result; } catch (Exception ex) { span?.SetError(ex, null); throw; } } /// 批量发布字符串消息 /// 消息体集合 /// 标签 /// public virtual SendResult PublishBatch(IList bodies, String tags = null) { var messages = new List(); foreach (var body in bodies) { var msg = new Message(); msg.SetBody(body); if (tags != null) msg.Tags = tags; messages.Add(msg); } return PublishBatch(messages); } #endregion #region 发布延迟消息 /// 发布延迟消息 /// 消息体 /// 目标队列。指定时可实现顺序发布(通过SelectQueue获取),默认未指定并自动选择队列 /// 延迟时间等级。18级 /// public virtual SendResult PublishDelay(Message message, MessageQueue queue, DelayTimeLevels level) { // 构造请求头 message.DelayTimeLevel = (Int32)level; var header = CreateHeader(message); for (var i = 0; i <= RetryTimesWhenSendFailed; i++) { // 选择队列分片 var mq = queue ?? SelectQueue(); mq.Topic = Topic; header.QueueId = mq.QueueId; // 性能埋点 using var span = Tracer?.NewSpan($"mq:{Name}:PublishDelay", new { level, message.BodyString }); try { // 根据队列获取Broker客户端 var bk = GetBroker(mq.BrokerName); var context = new SendMessageContext { ProducerGroup = Group, Message = message, Mq = mq, BrokerAddr = bk.Name, }; foreach (var hook in _sendMessageHooks) { try { hook.ExecuteHookBefore(context); } catch (Exception e) { if (Log.Enable) Log.Error(e.Message); } } var rs = bk.InvokeOneway(RequestCode.SEND_MESSAGE_V2, message.Body, header.GetProperties()); // 包装结果 var sendResult = new SendResult { Queue = mq, Header = rs.Header, Status = rs.Header.Code switch { -1 => SendStatus.SendError, _ => SendStatus.SendOK, } }; context.SendResult = sendResult; foreach (var hook in _sendMessageHooks) { try { hook.ExecuteHookAfter(context); } catch (Exception e) { if (Log.Enable) Log.Error(e.Message); } } if (Log != null && Log.Level <= LogLevel.Debug) WriteLog("{0}", sendResult); return sendResult; } catch (Exception ex) { // 如果网络异常,则延迟重发 if (i < RetryTimesWhenSendFailed) { Thread.Sleep(1000); continue; } span?.SetError(ex, message); throw; } } return null; } /// 发布延迟消息 /// /// 延迟时间等级。18级 /// /// public virtual void PublishDelay(Object body, DelayTimeLevels level, String tags = null) { var message = CreateMessage(body); message.Tags = tags; PublishDelay(message, null, level); } #endregion #region 结束事务消息 /// 结束事务消息。提交或回滚事务 /// 发布事务消息返回结果 /// 事务状态 /// 是否来自事务回查 public virtual void EndTransaction(SendResult result, TransactionState state, Boolean fromTransactionCheck = false) { if (result is null) throw new ArgumentNullException(nameof(result)); if (result.Queue == null) throw new ArgumentNullException(nameof(result), "缺少队列信息"); if (result.Queue.BrokerName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(result), "缺少BrokerName"); var header = new EndTransactionRequestHeader { ProducerGroup = Group, TranStateTableOffset = result.QueueOffset, CommitLogOffset = GetCommitLogOffset(result.OffsetMsgId), CommitOrRollback = (Int32)state, FromTransactionCheck = fromTransactionCheck, MsgId = result.MsgId, TransactionId = result.TransactionId, }; var bk = GetBroker(result.Queue.BrokerName); bk.Invoke(RequestCode.END_TRANSACTION, null, header.GetProperties()); } /// 异步结束事务消息。提交或回滚事务 /// 发布事务消息返回结果 /// 事务状态 /// 是否来自事务回查 /// 取消令牌 /// public virtual async Task EndTransactionAsync(SendResult result, TransactionState state, Boolean fromTransactionCheck = false, CancellationToken cancellationToken = default) { if (result is null) throw new ArgumentNullException(nameof(result)); if (result.Queue == null) throw new ArgumentNullException(nameof(result), "缺少队列信息"); if (result.Queue.BrokerName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(result), "缺少BrokerName"); var header = new EndTransactionRequestHeader { ProducerGroup = Group, TranStateTableOffset = result.QueueOffset, CommitLogOffset = GetCommitLogOffset(result.OffsetMsgId), CommitOrRollback = (Int32)state, FromTransactionCheck = fromTransactionCheck, MsgId = result.MsgId, TransactionId = result.TransactionId, }; var bk = GetBroker(result.Queue.BrokerName); await bk.InvokeAsync(RequestCode.END_TRANSACTION, null, header.GetProperties(), false, cancellationToken).ConfigureAwait(false); } #endregion #region 辅助 /// /// 创建消息,设计于支持用户重载以改变消息序列化行为 /// /// /// protected virtual Message CreateMessage(Object body) { if (body is null) throw new ArgumentNullException(nameof(body)); if (body is Message) throw new ArgumentOutOfRangeException(nameof(body), "body不能是Message类型"); if (!body.GetType().IsBaseType()) body = JsonHost.Write(body); var msg = new Message(); msg.SetBody(body); return msg; } private SendMessageRequestHeader CreateHeader(Message message) { var max = MaxMessageSize; if (max > 0 && message.Body.Length > max) throw new InvalidOperationException($"主题[{Topic}]的数据包大小[{message.Body.Length}]超过最大限制[{max}],大key会拖累整个队列,可通过MaxMessageSize调节。"); // 消息压缩 var sysFlag = 0; var compressOver = CompressOverBytes; if (compressOver > 0 && message.Body != null && message.Body.Length > compressOver) { message.Body = message.Body.Compress(); sysFlag |= 1; // 第0位表示压缩 } // 构造请求头 var smrh = new SendMessageRequestHeader { ProducerGroup = Group, Topic = Topic, //QueueId = mq.QueueId, SysFlag = sysFlag, BornTimestamp = DateTime.UtcNow.ToLong(), Flag = message.Flag, Properties = message.GetProperties(), ReconsumeTimes = 0, UnitMode = UnitMode, DefaultTopic = DefaultTopic, DefaultTopicQueueNums = DefaultTopicQueueNums }; if (message.Properties.TryGetValue("TRAN_MSG", out var str) && str.ToBoolean()) smrh.SysFlag = (Int32)TransactionState.Prepared; return smrh; } private static Int64 GetCommitLogOffset(String offsetMsgId) { if (offsetMsgId.IsNullOrEmpty() || offsetMsgId.Length < CommitLogOffsetHexLength) return 0; // OffsetMsgId尾部16位是8字节(Int64)的CommitLogOffset十六进制表示 return Int64.TryParse(offsetMsgId.Substring(offsetMsgId.Length - CommitLogOffsetHexLength), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var rs) ? rs : 0; } #endregion #region 选择Broker队列 private IList _brokers; //private WeightRoundRobin _robin; /// 选择队列 /// public virtual MessageQueue SelectQueue() { var lb = LoadBalance; if (!lb.Ready) { // 只选择主节点且可写 var list = Brokers.Where(e => e.IsMaster && e.Permission.HasFlag(Permissions.Write) && e.WriteQueueNums > 0).ToList(); if (list.Count == 0) return null; var total = list.Sum(e => e.WriteQueueNums); if (total <= 0) return null; _brokers = list; //lb = new WeightRoundRobin(); lb.Set(list.Select(e => e.WriteQueueNums).ToArray()); } // 解锁解决冲突,让消息发送更均匀 lock (lb) { // 构造排序列表。希望能够均摊到各Broker var idx = lb.Get(out var times); var bk = _brokers[idx]; return new MessageQueue { BrokerName = bk.Name, QueueId = (times - 1) % bk.WriteQueueNums }; } } #endregion #region Request-Reply 请求响应模式 /// 发送请求消息,同步等待响应 /// 请求消息 /// 超时时间(毫秒),默认使用RequestTimeout /// 响应消息 public virtual MessageExt Request(Message message, Int32 timeout = -1) { if (message == null) throw new ArgumentNullException(nameof(message)); if (timeout <= 0) timeout = RequestTimeout; // 生成关联ID var correlationId = Guid.NewGuid().ToString("N"); message.CorrelationId = correlationId; message.ReplyToClient = ClientId; message.MessageType = "REQUEST"; message.RequestTimeout = timeout; // 注册回调 var tcs = new TaskCompletionSource(); _requestCallbacks[correlationId] = tcs; try { // 发送请求消息 var result = Publish(message, null, timeout); // 等待响应 if (tcs.Task.Wait(timeout)) { return tcs.Task.Result; } else { throw new TimeoutException($"Request timeout after {timeout}ms, correlationId={correlationId}"); } } finally { // 清理回调 _requestCallbacks.TryRemove(correlationId, out _); } } /// 发送请求消息,同步等待响应 /// 消息体 /// 超时时间(毫秒) /// 响应消息 public virtual MessageExt Request(Object body, Int32 timeout = -1) => Request(CreateMessage(body), timeout); /// 发送请求消息,异步等待响应 /// 请求消息 /// 超时时间(毫秒),默认使用RequestTimeout /// 取消令牌 /// 响应消息 public virtual async Task RequestAsync(Message message, Int32 timeout = -1, CancellationToken cancellationToken = default) { if (message == null) throw new ArgumentNullException(nameof(message)); if (timeout <= 0) timeout = RequestTimeout; // 生成关联ID var correlationId = Guid.NewGuid().ToString("N"); message.CorrelationId = correlationId; message.ReplyToClient = ClientId; message.MessageType = "REQUEST"; message.RequestTimeout = timeout; // 注册回调 var tcs = new TaskCompletionSource(); _requestCallbacks[correlationId] = tcs; try { // 发送请求消息 await PublishAsync(message, null, cancellationToken).ConfigureAwait(false); // 等待响应,使用兼容的方式 var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(timeout, cancellationToken)).ConfigureAwait(false); if (completedTask == tcs.Task) { return await tcs.Task.ConfigureAwait(false); } else { throw new TimeoutException($"Request timeout after {timeout}ms, correlationId={correlationId}"); } } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { throw new TimeoutException($"Request timeout after {timeout}ms, correlationId={correlationId}"); } finally { // 清理回调 _requestCallbacks.TryRemove(correlationId, out _); } } /// 发送请求消息,异步等待响应 /// 消息体 /// 超时时间(毫秒) /// 取消令牌 /// 响应消息 public virtual Task RequestAsync(Object body, Int32 timeout = -1, CancellationToken cancellationToken = default) => RequestAsync(CreateMessage(body), timeout, cancellationToken); /// 处理回复消息 /// 回复消息 internal void HandleReplyMessage(MessageExt message) { var correlationId = message.CorrelationId; if (String.IsNullOrEmpty(correlationId)) return; if (_requestCallbacks.TryGetValue(correlationId, out var tcs)) { tcs.TrySetResult(message); } } #endregion #region 事务回查 /// 收到命令 /// protected override Command OnReceive(Command cmd) { if (!cmd.Reply) { var code = (RequestCode)cmd.Header.Code; if (code == RequestCode.CHECK_TRANSACTION_STATE) return HandleCheckTransaction(cmd); } return null; } private Command HandleCheckTransaction(Command cmd) { using var span = Tracer?.NewSpan($"mq:{Name}:CheckTransaction"); try { var dic = cmd.Header?.ExtFields; var transactionId = dic != null && dic.TryGetValue("transactionId", out var tid) ? tid : null; var commitLogOffset = dic != null && dic.TryGetValue("commitLogOffset", out var clo) ? clo.ToLong() : 0L; var tranStateTableOffset = dic != null && dic.TryGetValue("tranStateTableOffset", out var tso) ? tso.ToLong() : 0L; var msgId = dic != null && dic.TryGetValue("msgId", out var mid) ? mid : null; // 从消息体中尝试解析消息 MessageExt msgExt = null; if (cmd.Payload != null) { var msgs = MessageExt.ReadAll(cmd.Payload); msgExt = msgs?.FirstOrDefault(); } msgExt ??= new MessageExt { MsgId = msgId, TransactionId = transactionId }; // 调用回查委托 var state = TransactionState.Rollback; if (OnCheckTransactionAsync != null) state = OnCheckTransactionAsync(msgExt, transactionId, default).ConfigureAwait(false).GetAwaiter().GetResult(); else if (OnCheckTransaction != null) state = OnCheckTransaction(msgExt, transactionId); else { WriteLog("收到事务回查但未设置OnCheckTransaction委托,事务ID={0},将默认回滚", transactionId); } // 构造EndTransaction请求 var header = new EndTransactionRequestHeader { ProducerGroup = Group, TranStateTableOffset = tranStateTableOffset, CommitLogOffset = commitLogOffset, CommitOrRollback = (Int32)state, FromTransactionCheck = true, MsgId = msgId, TransactionId = transactionId, }; var bk = Clients?.FirstOrDefault(); bk?.Invoke(RequestCode.END_TRANSACTION, null, header.GetProperties()); WriteLog("事务回查完成,事务ID={0},状态={1}", transactionId, state); } catch (Exception ex) { span?.SetError(ex, null); WriteLog("事务回查处理异常:{0}", ex.Message); } return null; } #endregion #if NETSTANDARD2_1_OR_GREATER #region gRPC扩展方法 /// 通过gRPC协议发送延迟消息(任意时间)。RocketMQ 5.x新特性 /// 消息体 /// 投递时间(UTC或本地时间) /// 标签 /// 消息Key列表 /// 取消通知 /// 发送结果 public async Task PublishDelayViaGrpcAsync( Object body, DateTime deliveryTimestamp, String tag = null, IList keys = null, CancellationToken cancellationToken = default) { if (_GrpcService == null) throw new InvalidOperationException("gRPC service not initialized. Set GrpcProxyAddress first."); var message = CreateMessage(body); using var span = Tracer?.NewSpan($"mq:{Name}:PublishDelay:grpc", new { deliveryTimestamp, body = message.BodyString }); try { var rs = await _GrpcService.SendMessageAsync( Topic, message.Body, tag: tag, keys: keys, deliveryTimestamp: deliveryTimestamp, cancellationToken: cancellationToken ).ConfigureAwait(false); if (rs.Status?.Code != Grpc.GrpcCode.OK) throw new InvalidOperationException($"gRPC SendMessage (delay) failed: {rs.Status}"); var entry = rs.Entries.FirstOrDefault(); return new SendResult { Status = SendStatus.SendOK, MsgId = entry?.MessageId, TransactionId = entry?.TransactionId, QueueOffset = entry?.Offset ?? 0, }; } catch (Exception ex) { span?.SetError(ex, null); throw; } } /// 通过gRPC协议发送事务消息(半消息)。RocketMQ 5.x gRPC API /// 消息体 /// 标签 /// 消息Key列表 /// 取消通知 /// 发送结果(包含TransactionId) public async Task PublishTransactionViaGrpcAsync( Object body, String tag = null, IList keys = null, CancellationToken cancellationToken = default) { if (_GrpcService == null) throw new InvalidOperationException("gRPC service not initialized. Set GrpcProxyAddress first."); var message = CreateMessage(body); using var span = Tracer?.NewSpan($"mq:{Name}:PublishTransaction:grpc", message.BodyString); try { var rs = await _GrpcService.SendTransactionMessageAsync( Topic, message.Body, tag: tag, keys: keys, cancellationToken: cancellationToken ).ConfigureAwait(false); if (rs.Status?.Code != Grpc.GrpcCode.OK) throw new InvalidOperationException($"gRPC SendTransactionMessage failed: {rs.Status}"); var entry = rs.Entries.FirstOrDefault(); return new SendResult { Status = SendStatus.SendOK, MsgId = entry?.MessageId, TransactionId = entry?.TransactionId, QueueOffset = entry?.Offset ?? 0, }; } catch (Exception ex) { span?.SetError(ex, null); throw; } } /// 通过gRPC协议结束事务 /// 消息ID /// 事务ID /// 是否提交。true提交,false回滚 /// 取消通知 /// public async Task EndTransactionViaGrpcAsync( String messageId, String transactionId, Boolean commit, CancellationToken cancellationToken = default) { if (_GrpcService == null) throw new InvalidOperationException("gRPC service not initialized. Set GrpcProxyAddress first."); return await _GrpcService.EndTransactionAsync( Topic, messageId, transactionId, commit ? Grpc.GrpcTransactionResolution.COMMIT : Grpc.GrpcTransactionResolution.ROLLBACK, cancellationToken ).ConfigureAwait(false); } /// 通过gRPC协议查询主题路由 /// 主题名。默认使用当前Topic /// 取消通知 /// 路由信息 public async Task QueryRouteViaGrpcAsync(String topic = null, CancellationToken cancellationToken = default) { if (_GrpcService == null) throw new InvalidOperationException("gRPC service not initialized. Set GrpcProxyAddress first."); return await _GrpcService.QueryRouteAsync(topic ?? Topic, cancellationToken).ConfigureAwait(false); } #endregion #endif } ================================================ FILE: NewLife.RocketMQ/Properties/PublishProfiles/FolderProfile.pubxml ================================================  FileSystem Release Any CPU netstandard2.0 ..\Bin\ ================================================ FILE: NewLife.RocketMQ/Protocol/Command.cs ================================================ using System.Runtime.Serialization; using System.Xml.Serialization; using NewLife.Buffers; using NewLife.Collections; using NewLife.Data; using NewLife.Log; using NewLife.Messaging; using NewLife.Serialization; namespace NewLife.RocketMQ.Protocol; /// 命令 /// /// Remoting 协议帧格式(大端序): /// [4字节 TotalLength] [4字节 OriHeaderLength] [HeaderData] [BodyData] /// 其中 OriHeaderLength 高8位为序列化类型,低24位为头部数据长度。 /// 使用 SpanReader/SpanWriter 进行高性能零拷贝编解码。 /// public class Command : IAccessor, IMessage { #region 属性 /// 头部 public Header Header { get; set; } /// 主体 [XmlIgnore, IgnoreDataMember] public IPacket Payload { get; set; } /// 原始Json [XmlIgnore, IgnoreDataMember] public String RawJson { get; private set; } #endregion #region 扩展属性 /// 是否响应 public Boolean Reply { get; set; } /// 是否单向 public Boolean OneWay { get; set; } /// 是否异常 Boolean IMessage.Error { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } #endregion #region 构造 /// 销毁。回收内存 public void Dispose() => Payload.TryDispose(); #endregion #region 读写 /// 从数据流中读取 /// 数据流 /// 上下文 /// public Boolean Read(Stream stream, Object context = null) { try { // 先读取8字节帧头(TotalLength + OriHeaderLength) var headBuf = new Byte[8]; if (stream.Read(headBuf, 0, 8) < 8) return false; var reader = new SpanReader(headBuf) { IsLittleEndian = false }; var len = reader.ReadInt32(); if (len < 4 || len > 4 * 1024 * 1024) return false; var oriHeaderLen = reader.ReadInt32(); var headerLen = oriHeaderLen & 0xFFFFFF; if (headerLen <= 0 || headerLen > 8 * 1024) return false; // 读取剩余数据(头部 + 主体) var bodyLen = len - 4 - headerLen; var dataBuf = new Byte[headerLen + (bodyLen > 0 ? bodyLen : 0)]; if (stream.Read(dataBuf, 0, dataBuf.Length) < dataBuf.Length) return false; // 读取序列化类型 var type = (SerializeType)((oriHeaderLen >> 24) & 0xFF); if (type == SerializeType.JSON) { var json = dataBuf.ToStr(null, 0, headerLen); RawJson = json; var header = json.ToJsonEntity
(); if (header.SerializeTypeCurrentRPC.IsNullOrEmpty()) header.SerializeTypeCurrentRPC = type + ""; Header = header; Reply = (header.Flag & 0b01) == 0b01; OneWay = (header.Flag & 0b10) == 0b10; // 读取主体 if (bodyLen > 0) Payload = new ArrayPacket(dataBuf, headerLen, bodyLen); } else if (type == SerializeType.ROCKETMQ) { var rd = new SpanReader(dataBuf, 0, headerLen) { IsLittleEndian = false }; var header = new Header { SerializeTypeCurrentRPC = type + "", Code = rd.ReadUInt16(), Language = ((LanguageCode)rd.ReadByte()) + "", Version = (MQVersion)rd.ReadUInt16(), Opaque = rd.ReadInt32(), Flag = rd.ReadInt32(), Remark = ReadStr(ref rd, false, headerLen), }; Reply = (header.Flag & 0b01) == 0b01; OneWay = (header.Flag & 0b10) == 0b10; // 读取扩展字段 var extFieldsLength = rd.ReadInt32(); if (extFieldsLength > 0) { if (extFieldsLength > headerLen) throw new Exception($"扩展字段长度[{extFieldsLength}]超过头部长度[{headerLen}]"); var extFields = header.GetExtFields(); var endPos = rd.Position + extFieldsLength; while (rd.Position < endPos) { var k = ReadStr(ref rd, true, extFieldsLength); var v = ReadStr(ref rd, false, extFieldsLength); extFields[k + ""] = v; } } Header = header; // 读取主体 if (bodyLen > 0) Payload = new ArrayPacket(dataBuf, headerLen, bodyLen); } else throw new NotSupportedException($"不支持[{type}]序列化"); } catch (Exception ex) { XTrace.WriteLine("序列化错误!{0}", ex.Message); return false; } return true; } /// 从SpanReader中读取字符串 /// 读取器 /// 是否使用2字节长度前缀 /// 长度上限 /// private static String ReadStr(ref SpanReader reader, Boolean useShortLength, Int32 limit) { var len = useShortLength ? reader.ReadInt16() : reader.ReadInt32(); if (len == 0) return null; if (len > limit) throw new Exception($"字符串长度[{len}]超过限制[{limit}]"); return reader.ReadBytes(len).ToArray().ToStr(); } /// 向SpanWriter写入字符串 /// 写入器 /// 是否使用2字节长度前缀 /// 字符串值 private static void WriteStr(ref SpanWriter writer, Boolean useShortLength, String value) { var buf = value?.GetBytes(); var len = buf?.Length ?? 0; if (useShortLength) writer.Write((Int16)len); else writer.Write(len); if (len > 0) writer.Write(buf); } /// 读取Body作为Json返回 /// public IDictionary ReadBodyAsJson() { var pk = Payload; if (pk == null || pk.Total == 0) return null; return new JsonParser(pk.ToStr()).Decode() as IDictionary; } /// 写入命令到数据流 /// 数据流 /// 上下文 /// public Boolean Write(Stream stream, Object context = null) { var header = Header; if (Reply) header.Flag |= 0b01; if (OneWay) header.Flag |= 0b10; var pk = Payload; // 区分不同的序列化类型 var type = header.SerializeTypeCurrentRPC.ToEnum(SerializeType.JSON); if (type == SerializeType.JSON) { // 计算头部 //var json = Header.ToJson(); var json = Header.ToJson(false, false, false); RawJson = json; var hs = json.GetBytes(); // 计算长度 var len = 4 + hs.Length; if (pk != null) len += pk.Total; // 使用SpanWriter写入8字节帧头(TotalLength + HeaderLength) var prefix = new Byte[8]; var writer = new SpanWriter(prefix) { IsLittleEndian = false }; writer.Write(len); writer.Write(hs.Length); stream.Write(prefix, 0, 8); // 写入头部JSON stream.Write(hs, 0, hs.Length); } else if (type == SerializeType.ROCKETMQ) { // 使用SpanWriter编码ROCKETMQ二进制头部 var hsBuf = new Byte[8 * 1024]; var writer = new SpanWriter(hsBuf) { IsLittleEndian = false }; writer.Write((UInt16)header.Code); writer.Write((Byte)header.Language.ToEnum(LanguageCode.JAVA)); writer.Write((UInt16)header.Version); writer.Write(header.Opaque); writer.Write(header.Flag); WriteStr(ref writer, false, header.Remark); if (header.ExtFields != null && header.ExtFields.Count > 0) { // 先编码扩展字段到临时缓冲区 var extBuf = new Byte[4 * 1024]; var extWriter = new SpanWriter(extBuf) { IsLittleEndian = false }; foreach (var item in header.ExtFields) { WriteStr(ref extWriter, true, item.Key); WriteStr(ref extWriter, false, item.Value); } var extLen = extWriter.Position; writer.Write(extLen); if (extLen > 0) writer.Write(new ReadOnlySpan(extBuf, 0, extLen)); } else { writer.Write(0); } // 计算长度 var hsLen = writer.Position; var oriHeaderLen = (hsLen & 0xFFFFFF) | ((Byte)type << 24); var len = 4 + hsLen; if (pk != null) len += pk.Total; // 使用SpanWriter写入帧头 var prefix = new Byte[8]; var prefixWriter = new SpanWriter(prefix) { IsLittleEndian = false }; prefixWriter.Write(len); prefixWriter.Write(oriHeaderLen); stream.Write(prefix, 0, 8); // 写入头部 stream.Write(hsBuf, 0, hsLen); } else throw new NotSupportedException($"不支持[{type}]序列化"); // 写入主体 if (pk != null && pk.Total > 0) pk.CopyTo(stream); return true; } /// 命令转字节数组 /// public IPacket ToPacket() { var ms = new MemoryStream(); Write(ms, null); ms.Position = 0; return new ArrayPacket(ms); } /// 创建响应 /// public IMessage CreateReply() { if (Header == null || Reply || OneWay) throw new Exception("不能创建响应命令"); var head = new Header { Opaque = Header.Opaque, SerializeTypeCurrentRPC = Header.SerializeTypeCurrentRPC, Version = Header.Version, }; var cmd = new Command { Reply = true, Header = head, }; return cmd; } Boolean IMessage.Read(IPacket pk) => Read(pk.GetStream()); #endregion #region 辅助 /// 友好字符串 /// public override String ToString() { var h = Header; if (h == null) return base.ToString(); var sb = Pool.StringBuilder.Get(); // 请求与响应 if (Reply) { sb.Append('#'); sb.Append((ResponseCode)h.Code); } else { sb.Append((RequestCode)h.Code); } var pk = Payload; sb.AppendFormat("({0})", h.Opaque); var ext = h.ExtFields; if (ext != null && ext.Count > 0) sb.AppendFormat("<{0}>", ext.ToJson()); if (pk != null && pk.Total > 0) { sb.AppendFormat("[{0}]", pk.Total); sb.Append(pk.ToStr(null, 0, 256)); } return sb.Return(true); } #endregion } ================================================ FILE: NewLife.RocketMQ/Protocol/ConsumerData.cs ================================================ namespace NewLife.RocketMQ.Protocol; /// 消费者数据 public class ConsumerData { #region 属性 /// 从哪里开始消费 public String ConsumeFromWhere { get; set; } = "CONSUME_FROM_LAST_OFFSET"; /// 消费类型 public String ConsumeType { get; set; } = "CONSUME_ACTIVELY"; /// 组名 public String GroupName { get; set; } /// 消息模型。广播/集群 public String MessageModel { get; set; } = "CLUSTERING"; /// 订阅数据集 public SubscriptionData[] SubscriptionDataSet { get; set; } /// 单元模式 public Boolean UnitMode { get; set; } #endregion } ================================================ FILE: NewLife.RocketMQ/Protocol/ConsumerRunningInfo.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NewLife.RocketMQ.Protocol { class ConsumerRunningInfo { #region 属性 public IDictionary Properties { get; set; } public SubscriptionData[] SubscriptionSet { get; set; } public String[] MqTable { get; set; } #endregion } } ================================================ FILE: NewLife.RocketMQ/Protocol/ConsumerStates/ConsumerStatesModel.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace NewLife.RocketMQ.Protocol.ConsumerStates { /// /// 消费者状态信息模型 /// public class ConsumerStatesModel { /// /// 消费Tps /// public double ConsumeTps { get; set; } /// /// 消费Offset信息Table /// public Dictionary OffsetTable { get; set; } } } ================================================ FILE: NewLife.RocketMQ/Protocol/ConsumerStates/MessageQueueModel.cs ================================================ namespace NewLife.RocketMQ.Protocol.ConsumerStates; /// /// 消息队列信息模型 /// public class MessageQueueModel { /// /// Broker服务器名称 /// public String BrokerName { get; set; } /// /// 队列编码 /// public Int32 QueueId { get; set; } /// /// 主题 /// public String Topic { get; set; } /// /// 阿里版本返回字段 /// public Boolean MainQueue { get; set; } /// /// 阿里版本返回字段 /// public Int32 QueueGroupId { get; set; } /// 已重载。 /// public override String ToString() => $"{BrokerName}[{QueueId}]"; } ================================================ FILE: NewLife.RocketMQ/Protocol/ConsumerStates/OffsetWrapperModel.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace NewLife.RocketMQ.Protocol.ConsumerStates { /// /// 消费点位信息模型 /// public class OffsetWrapperModel { /// /// 代理者位点 /// public Int64 BrokerOffset { get; set; } /// /// 消费者点位 /// public Int64 ConsumerOffset { get; set; } /// /// 上次时间 /// public Int64 LastTimestamp { get; set; } /// /// 阿里版拉取点位 /// public Int64 PullOffset { get; set; } } } ================================================ FILE: NewLife.RocketMQ/Protocol/EndTransactionRequestHeader.cs ================================================ using System.Reflection; using NewLife.Reflection; namespace NewLife.RocketMQ.Protocol; /// 结束事务请求头 public class EndTransactionRequestHeader { #region 属性 /// 生产组 public String ProducerGroup { get; set; } /// 事务状态表偏移 public Int64 TranStateTableOffset { get; set; } /// 提交日志偏移 public Int64 CommitLogOffset { get; set; } /// 提交或回滚标记 public Int32 CommitOrRollback { get; set; } /// 是否来自事务回查 public Boolean FromTransactionCheck { get; set; } /// 消息编号 public String MsgId { get; set; } /// 事务编号 public String TransactionId { get; set; } #endregion #region 方法 /// 获取属性字典 /// public IDictionary GetProperties() { var dic = new Dictionary(); foreach (var pi in GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (pi.GetIndexParameters().Length > 0) continue; var name = pi.Name; if (!name.IsNullOrEmpty()) name = Char.ToLowerInvariant(name[0]) + name.Substring(1); dic[name] = this.GetValue(pi); } return dic; } #endregion } ================================================ FILE: NewLife.RocketMQ/Protocol/Header.cs ================================================ using System.Xml.Serialization; namespace NewLife.RocketMQ.Protocol; /// 头部 public class Header { #region 属性 /// 请求/响应码 [XmlElement("code")] public Int32 Code { get; set; } /// 扩展字段 /// /// 这个字段在不同的请求/响应不一样,完全自定义。数据结构上是java的hashmap。 /// 在Java的每个RemotingCammand中,其实都带有一个CommandCustomHeader的属性成员,可以认为他是一个强类型的extFields, /// 在最后传输的时候,这个CommandCustomHeader会被忽略,而传输前会把其中的所有字段全部都原封不动塞到extFields中,以作传输。 /// [XmlElement("extFields")] public IDictionary ExtFields { get; set; } /// 标识 /// /// 第0位标识是这次通信是request还是response,0标识request, 1 标识response。 /// 第1位标识是否是oneway请求,1标识oneway。应答方在处理oneway请求的时候,不会做出响应,请求方也无需等待应答方响应。 /// [XmlElement("flag")] public Int32 Flag { get; set; } /// 由于要支持多语言,所以这一字段可以给通信双方知道对方通信层所使用的开发语言。 /// LanguageCode,JAVA/CPP/DOTNET/PYTHON/DELPHI/ERLANG/RUBY/OTHER/HTTP/GO/PHP/OMS [XmlElement("language")] public String Language { get; set; } = "CPP"; /// 请求标识码。在Java版的通信层中,这个只是一个不断自增的整型,为了收到应答方响应的的时候找到对应的请求。 [XmlElement("opaque")] public Int32 Opaque { get; set; } /// 序列化类型 [XmlElement("serializeTypeCurrentRPC")] public String SerializeTypeCurrentRPC { get; set; } = "JSON"; /// 让通信层知道对方的版本号,响应方可以以此做兼容老版本等的特殊操作 [XmlElement("version")] public MQVersion Version { get; set; } = MQVersion.V4_8_0; /// 附带的文本信息。常见的如存放一些broker/nameserver返回的一些异常信息,方便开发人员定位问题。 [XmlElement("remark")] public String Remark { get; set; } #endregion #region 方法 /// 获取扩展字段。如果为空则创建 /// public IDictionary GetExtFields() { ExtFields ??= new Dictionary(StringComparer.OrdinalIgnoreCase); return ExtFields; } /// 创建异常 /// public ResponseException CreateException() { var err = Remark; if (!err.IsNullOrEmpty()) { var p = err.IndexOf("Exception: "); if (p >= 0) err = err.Substring(p + "Exception: ".Length); p = err.IndexOf(", "); if (p > 0) err = err.Substring(0, p); } return new ResponseException((ResponseCode)Code, err); } #endregion } ================================================ FILE: NewLife.RocketMQ/Protocol/HeartbeatData.cs ================================================ namespace NewLife.RocketMQ.Protocol { /// 心跳数据 public class HeartbeatData { #region 属性 /// 客户端编号 public String ClientID { get; set; } /// 消费数据集 public ConsumerData[] ConsumerDataSet { get; set; } /// 生产者数据集 public ProducerData[] ProducerDataSet { get; set; } #endregion } } ================================================ FILE: NewLife.RocketMQ/Protocol/LanguageCode.cs ================================================ namespace NewLife.RocketMQ.Protocol; /// 语言类型 public enum LanguageCode : Byte { /// JAVA = 0, /// CPP = 1, /// DOTNET = 2, /// PYTHON = 3, /// DELPHI = 4, /// ERLANG = 5, /// RUBY = 6, /// OTHER = 7, /// HTTP = 8, /// GO = 9, /// PHP = 10, /// OMS = 11, /// RUST = 12, } ================================================ FILE: NewLife.RocketMQ/Protocol/MQVersion.cs ================================================ namespace NewLife.RocketMQ.Protocol; #pragma warning disable CS1591 // 缺少对公共可见类型或成员的 XML 注释 public enum MQVersion : Int32 { V3_0_0_SNAPSHOT, V3_0_0_ALPHA1, V3_0_0_BETA1, V3_0_0_BETA2, V3_0_0_BETA3, V3_0_0_BETA4, V3_0_0_BETA5, V3_0_0_BETA6_SNAPSHOT, V3_0_0_BETA6, V3_0_0_BETA7_SNAPSHOT, V3_0_0_BETA7, V3_0_0_BETA8_SNAPSHOT, V3_0_0_BETA8, V3_0_0_BETA9_SNAPSHOT, V3_0_0_BETA9, V3_0_0_FINAL, V3_0_1_SNAPSHOT, V3_0_1, V3_0_2_SNAPSHOT, V3_0_2, V3_0_3_SNAPSHOT, V3_0_3, V3_0_4_SNAPSHOT, V3_0_4, V3_0_5_SNAPSHOT, V3_0_5, V3_0_6_SNAPSHOT, V3_0_6, V3_0_7_SNAPSHOT, V3_0_7, V3_0_8_SNAPSHOT, V3_0_8, V3_0_9_SNAPSHOT, V3_0_9, V3_0_10_SNAPSHOT, V3_0_10, V3_0_11_SNAPSHOT, V3_0_11, V3_0_12_SNAPSHOT, V3_0_12, V3_0_13_SNAPSHOT, V3_0_13, V3_0_14_SNAPSHOT, V3_0_14, V3_0_15_SNAPSHOT, V3_0_15, V3_1_0_SNAPSHOT, V3_1_0, V3_1_1_SNAPSHOT, V3_1_1, V3_1_2_SNAPSHOT, V3_1_2, V3_1_3_SNAPSHOT, V3_1_3, V3_1_4_SNAPSHOT, V3_1_4, V3_1_5_SNAPSHOT, V3_1_5, V3_1_6_SNAPSHOT, V3_1_6, V3_1_7_SNAPSHOT, V3_1_7, V3_1_8_SNAPSHOT, V3_1_8, V3_1_9_SNAPSHOT, V3_1_9, V3_2_0_SNAPSHOT, V3_2_0, V3_2_1_SNAPSHOT, V3_2_1, V3_2_2_SNAPSHOT, V3_2_2, V3_2_3_SNAPSHOT, V3_2_3, V3_2_4_SNAPSHOT, V3_2_4, V3_2_5_SNAPSHOT, V3_2_5, V3_2_6_SNAPSHOT, V3_2_6, V3_2_7_SNAPSHOT, V3_2_7, V3_2_8_SNAPSHOT, V3_2_8, V3_2_9_SNAPSHOT, V3_2_9, V3_3_1_SNAPSHOT, V3_3_1, V3_3_2_SNAPSHOT, V3_3_2, V3_3_3_SNAPSHOT, V3_3_3, V3_3_4_SNAPSHOT, V3_3_4, V3_3_5_SNAPSHOT, V3_3_5, V3_3_6_SNAPSHOT, V3_3_6, V3_3_7_SNAPSHOT, V3_3_7, V3_3_8_SNAPSHOT, V3_3_8, V3_3_9_SNAPSHOT, V3_3_9, V3_4_1_SNAPSHOT, V3_4_1, V3_4_2_SNAPSHOT, V3_4_2, V3_4_3_SNAPSHOT, V3_4_3, V3_4_4_SNAPSHOT, V3_4_4, V3_4_5_SNAPSHOT, V3_4_5, V3_4_6_SNAPSHOT, V3_4_6, V3_4_7_SNAPSHOT, V3_4_7, V3_4_8_SNAPSHOT, V3_4_8, V3_4_9_SNAPSHOT, V3_4_9, V3_5_1_SNAPSHOT, V3_5_1, V3_5_2_SNAPSHOT, V3_5_2, V3_5_3_SNAPSHOT, V3_5_3, V3_5_4_SNAPSHOT, V3_5_4, V3_5_5_SNAPSHOT, V3_5_5, V3_5_6_SNAPSHOT, V3_5_6, V3_5_7_SNAPSHOT, V3_5_7, V3_5_8_SNAPSHOT, V3_5_8, V3_5_9_SNAPSHOT, V3_5_9, V3_6_1_SNAPSHOT, V3_6_1, V3_6_2_SNAPSHOT, V3_6_2, V3_6_3_SNAPSHOT, V3_6_3, V3_6_4_SNAPSHOT, V3_6_4, V3_6_5_SNAPSHOT, V3_6_5, V3_6_6_SNAPSHOT, V3_6_6, V3_6_7_SNAPSHOT, V3_6_7, V3_6_8_SNAPSHOT, V3_6_8, V3_6_9_SNAPSHOT, V3_6_9, V3_7_1_SNAPSHOT, V3_7_1, V3_7_2_SNAPSHOT, V3_7_2, V3_7_3_SNAPSHOT, V3_7_3, V3_7_4_SNAPSHOT, V3_7_4, V3_7_5_SNAPSHOT, V3_7_5, V3_7_6_SNAPSHOT, V3_7_6, V3_7_7_SNAPSHOT, V3_7_7, V3_7_8_SNAPSHOT, V3_7_8, V3_7_9_SNAPSHOT, V3_7_9, V3_8_1_SNAPSHOT, V3_8_1, V3_8_2_SNAPSHOT, V3_8_2, V3_8_3_SNAPSHOT, V3_8_3, V3_8_4_SNAPSHOT, V3_8_4, V3_8_5_SNAPSHOT, V3_8_5, V3_8_6_SNAPSHOT, V3_8_6, V3_8_7_SNAPSHOT, V3_8_7, V3_8_8_SNAPSHOT, V3_8_8, V3_8_9_SNAPSHOT, V3_8_9, V3_9_1_SNAPSHOT, V3_9_1, V3_9_2_SNAPSHOT, V3_9_2, V3_9_3_SNAPSHOT, V3_9_3, V3_9_4_SNAPSHOT, V3_9_4, V3_9_5_SNAPSHOT, V3_9_5, V3_9_6_SNAPSHOT, V3_9_6, V3_9_7_SNAPSHOT, V3_9_7, V3_9_8_SNAPSHOT, V3_9_8, V3_9_9_SNAPSHOT, V3_9_9, V4_0_0_SNAPSHOT, V4_0_0, V4_0_1_SNAPSHOT, V4_0_1, V4_0_2_SNAPSHOT, V4_0_2, V4_0_3_SNAPSHOT, V4_0_3, V4_0_4_SNAPSHOT, V4_0_4, V4_0_5_SNAPSHOT, V4_0_5, V4_0_6_SNAPSHOT, V4_0_6, V4_0_7_SNAPSHOT, V4_0_7, V4_0_8_SNAPSHOT, V4_0_8, V4_0_9_SNAPSHOT, V4_0_9, V4_1_0_SNAPSHOT, V4_1_0, V4_1_1_SNAPSHOT, V4_1_1, V4_1_2_SNAPSHOT, V4_1_2, V4_1_3_SNAPSHOT, V4_1_3, V4_1_4_SNAPSHOT, V4_1_4, V4_1_5_SNAPSHOT, V4_1_5, V4_1_6_SNAPSHOT, V4_1_6, V4_1_7_SNAPSHOT, V4_1_7, V4_1_8_SNAPSHOT, V4_1_8, V4_1_9_SNAPSHOT, V4_1_9, V4_2_0_SNAPSHOT, V4_2_0, V4_2_1_SNAPSHOT, V4_2_1, V4_2_2_SNAPSHOT, V4_2_2, V4_2_3_SNAPSHOT, V4_2_3, V4_2_4_SNAPSHOT, V4_2_4, V4_2_5_SNAPSHOT, V4_2_5, V4_2_6_SNAPSHOT, V4_2_6, V4_2_7_SNAPSHOT, V4_2_7, V4_2_8_SNAPSHOT, V4_2_8, V4_2_9_SNAPSHOT, V4_2_9, V4_3_0_SNAPSHOT, V4_3_0, V4_3_1_SNAPSHOT, V4_3_1, V4_3_2_SNAPSHOT, V4_3_2, V4_3_3_SNAPSHOT, V4_3_3, V4_3_4_SNAPSHOT, V4_3_4, V4_3_5_SNAPSHOT, V4_3_5, V4_3_6_SNAPSHOT, V4_3_6, V4_3_7_SNAPSHOT, V4_3_7, V4_3_8_SNAPSHOT, V4_3_8, V4_3_9_SNAPSHOT, V4_3_9, V4_4_0_SNAPSHOT, V4_4_0, V4_4_1_SNAPSHOT, V4_4_1, V4_4_2_SNAPSHOT, V4_4_2, V4_4_3_SNAPSHOT, V4_4_3, V4_4_4_SNAPSHOT, V4_4_4, V4_4_5_SNAPSHOT, V4_4_5, V4_4_6_SNAPSHOT, V4_4_6, V4_4_7_SNAPSHOT, V4_4_7, V4_4_8_SNAPSHOT, V4_4_8, V4_4_9_SNAPSHOT, V4_4_9, V4_5_0_SNAPSHOT, V4_5_0, V4_5_1_SNAPSHOT, V4_5_1, V4_5_2_SNAPSHOT, V4_5_2, V4_5_3_SNAPSHOT, V4_5_3, V4_5_4_SNAPSHOT, V4_5_4, V4_5_5_SNAPSHOT, V4_5_5, V4_5_6_SNAPSHOT, V4_5_6, V4_5_7_SNAPSHOT, V4_5_7, V4_5_8_SNAPSHOT, V4_5_8, V4_5_9_SNAPSHOT, V4_5_9, V4_6_0_SNAPSHOT, V4_6_0, V4_6_1_SNAPSHOT, V4_6_1, V4_6_2_SNAPSHOT, V4_6_2, V4_6_3_SNAPSHOT, V4_6_3, V4_6_4_SNAPSHOT, V4_6_4, V4_6_5_SNAPSHOT, V4_6_5, V4_6_6_SNAPSHOT, V4_6_6, V4_6_7_SNAPSHOT, V4_6_7, V4_6_8_SNAPSHOT, V4_6_8, V4_6_9_SNAPSHOT, V4_6_9, V4_7_0_SNAPSHOT, V4_7_0, V4_7_1_SNAPSHOT, V4_7_1, V4_7_2_SNAPSHOT, V4_7_2, V4_7_3_SNAPSHOT, V4_7_3, V4_7_4_SNAPSHOT, V4_7_4, V4_7_5_SNAPSHOT, V4_7_5, V4_7_6_SNAPSHOT, V4_7_6, V4_7_7_SNAPSHOT, V4_7_7, V4_7_8_SNAPSHOT, V4_7_8, V4_7_9_SNAPSHOT, V4_7_9, V4_8_0_SNAPSHOT, V4_8_0, V4_8_1_SNAPSHOT, V4_8_1, V4_8_2_SNAPSHOT, V4_8_2, V4_8_3_SNAPSHOT, V4_8_3, V4_8_4_SNAPSHOT, V4_8_4, V4_8_5_SNAPSHOT, V4_8_5, V4_8_6_SNAPSHOT, V4_8_6, V4_8_7_SNAPSHOT, V4_8_7, V4_8_8_SNAPSHOT, V4_8_8, V4_8_9_SNAPSHOT, V4_8_9, V4_9_0_SNAPSHOT, V4_9_0, V4_9_1_SNAPSHOT, V4_9_1, V4_9_2_SNAPSHOT, V4_9_2, V4_9_3_SNAPSHOT, V4_9_3, V4_9_4_SNAPSHOT, V4_9_4, V4_9_5_SNAPSHOT, V4_9_5, V4_9_6_SNAPSHOT, V4_9_6, V4_9_7_SNAPSHOT, V4_9_7, V4_9_8_SNAPSHOT, V4_9_8, V4_9_9_SNAPSHOT, V4_9_9, V5_0_0_SNAPSHOT, V5_0_0, V5_0_1_SNAPSHOT, V5_0_1, V5_0_2_SNAPSHOT, V5_0_2, V5_0_3_SNAPSHOT, V5_0_3, V5_0_4_SNAPSHOT, V5_0_4, V5_0_5_SNAPSHOT, V5_0_5, V5_0_6_SNAPSHOT, V5_0_6, V5_0_7_SNAPSHOT, V5_0_7, V5_0_8_SNAPSHOT, V5_0_8, V5_0_9_SNAPSHOT, V5_0_9, V5_1_0_SNAPSHOT, V5_1_0, V5_1_1_SNAPSHOT, V5_1_1, V5_1_2_SNAPSHOT, V5_1_2, V5_1_3_SNAPSHOT, V5_1_3, V5_1_4_SNAPSHOT, V5_1_4, V5_1_5_SNAPSHOT, V5_1_5, V5_1_6_SNAPSHOT, V5_1_6, V5_1_7_SNAPSHOT, V5_1_7, V5_1_8_SNAPSHOT, V5_1_8, V5_1_9_SNAPSHOT, V5_1_9, V5_2_0_SNAPSHOT, V5_2_0, V5_2_1_SNAPSHOT, V5_2_1, V5_2_2_SNAPSHOT, V5_2_2, V5_2_3_SNAPSHOT, V5_2_3, V5_2_4_SNAPSHOT, V5_2_4, V5_2_5_SNAPSHOT, V5_2_5, V5_2_6_SNAPSHOT, V5_2_6, V5_2_7_SNAPSHOT, V5_2_7, V5_2_8_SNAPSHOT, V5_2_8, V5_2_9_SNAPSHOT, V5_2_9, V5_3_0_SNAPSHOT, V5_3_0, V5_3_1_SNAPSHOT, V5_3_1, V5_3_2_SNAPSHOT, V5_3_2, V5_3_3_SNAPSHOT, V5_3_3, V5_3_4_SNAPSHOT, V5_3_4, V5_3_5_SNAPSHOT, V5_3_5, V5_3_6_SNAPSHOT, V5_3_6, V5_3_7_SNAPSHOT, V5_3_7, V5_3_8_SNAPSHOT, V5_3_8, V5_3_9_SNAPSHOT, V5_3_9, V5_4_0_SNAPSHOT, V5_4_0, V5_4_1_SNAPSHOT, V5_4_1, V5_4_2_SNAPSHOT, V5_4_2, V5_4_3_SNAPSHOT, V5_4_3, V5_4_4_SNAPSHOT, V5_4_4, V5_4_5_SNAPSHOT, V5_4_5, V5_4_6_SNAPSHOT, V5_4_6, V5_4_7_SNAPSHOT, V5_4_7, V5_4_8_SNAPSHOT, V5_4_8, V5_4_9_SNAPSHOT, V5_4_9, V5_5_0_SNAPSHOT, V5_5_0, V5_5_1_SNAPSHOT, V5_5_1, V5_5_2_SNAPSHOT, V5_5_2, V5_5_3_SNAPSHOT, V5_5_3, V5_5_4_SNAPSHOT, V5_5_4, V5_5_5_SNAPSHOT, V5_5_5, V5_5_6_SNAPSHOT, V5_5_6, V5_5_7_SNAPSHOT, V5_5_7, V5_5_8_SNAPSHOT, V5_5_8, V5_5_9_SNAPSHOT, V5_5_9, V5_6_0_SNAPSHOT, V5_6_0, V5_6_1_SNAPSHOT, V5_6_1, V5_6_2_SNAPSHOT, V5_6_2, V5_6_3_SNAPSHOT, V5_6_3, V5_6_4_SNAPSHOT, V5_6_4, V5_6_5_SNAPSHOT, V5_6_5, V5_6_6_SNAPSHOT, V5_6_6, V5_6_7_SNAPSHOT, V5_6_7, V5_6_8_SNAPSHOT, V5_6_8, V5_6_9_SNAPSHOT, V5_6_9, V5_7_0_SNAPSHOT, V5_7_0, V5_7_1_SNAPSHOT, V5_7_1, V5_7_2_SNAPSHOT, V5_7_2, V5_7_3_SNAPSHOT, V5_7_3, V5_7_4_SNAPSHOT, V5_7_4, V5_7_5_SNAPSHOT, V5_7_5, V5_7_6_SNAPSHOT, V5_7_6, V5_7_7_SNAPSHOT, V5_7_7, V5_7_8_SNAPSHOT, V5_7_8, V5_7_9_SNAPSHOT, V5_7_9, V5_8_0_SNAPSHOT, V5_8_0, V5_8_1_SNAPSHOT, V5_8_1, V5_8_2_SNAPSHOT, V5_8_2, V5_8_3_SNAPSHOT, V5_8_3, V5_8_4_SNAPSHOT, V5_8_4, V5_8_5_SNAPSHOT, V5_8_5, V5_8_6_SNAPSHOT, V5_8_6, V5_8_7_SNAPSHOT, V5_8_7, V5_8_8_SNAPSHOT, V5_8_8, V5_8_9_SNAPSHOT, V5_8_9, V5_9_0_SNAPSHOT, V5_9_0, V5_9_1_SNAPSHOT, V5_9_1, V5_9_2_SNAPSHOT, V5_9_2, V5_9_3_SNAPSHOT, V5_9_3, V5_9_4_SNAPSHOT, V5_9_4, V5_9_5_SNAPSHOT, V5_9_5, V5_9_6_SNAPSHOT, V5_9_6, V5_9_7_SNAPSHOT, V5_9_7, V5_9_8_SNAPSHOT, V5_9_8, V5_9_9_SNAPSHOT, V5_9_9, HIGHER_VERSION } #pragma warning restore CS1591 // 缺少对公共可见类型或成员的 XML 注释 ================================================ FILE: NewLife.RocketMQ/Protocol/Message.cs ================================================ using System.Runtime.Serialization; using System.Xml.Serialization; using NewLife.Collections; using NewLife.Data; using NewLife.Serialization; namespace NewLife.RocketMQ.Protocol; /// 消息 public class Message { #region 属性 /// 主题 public String Topic { get; set; } /// 标签 public String Tags { get => Properties.TryGetValue("TAGS", out var str) ? str : null; set => Properties["TAGS"] = value; } /// public String Keys { get => Properties.TryGetValue("KEYS", out var str) ? str : null; set => Properties["KEYS"] = value; } /// 标记 public Int32 Flag { get; set; } /// 消息体 [XmlIgnore, IgnoreDataMember] public Byte[] Body { get; set; } private String _BodyString; /// 消息体。字符串格式 public String BodyString { get => _BodyString ??= Body?.ToStr(); set => Body = (_BodyString = value)?.GetBytes(); } /// 等待存储消息 public Boolean WaitStoreMsgOK { get => Properties.TryGetValue("WAIT", out var str) ? str.ToBoolean() : true; set => Properties["WAIT"] = value.ToString(); } /// 延迟时间等级 public Int32 DelayTimeLevel { get => Properties.TryGetValue("DELAY", out var str) ? str.ToInt() : 0; set => Properties["DELAY"] = value.ToString(); } /// 事务标识 public String TransactionId { get => Properties.TryGetValue("UNIQ_KEY", out var str) ? str : null; set => Properties["UNIQ_KEY"] = value; } /// 回复地址。用于Request-Reply模式,指示回复消息应发送到的客户端ID public String ReplyToClient { get => Properties.TryGetValue("REPLY_TO_CLIENT", out var str) ? str : null; set => Properties["REPLY_TO_CLIENT"] = value; } /// 关联ID。用于Request-Reply模式,将回复消息与请求消息关联 public String CorrelationId { get => Properties.TryGetValue("CORRELATION_ID", out var str) ? str : null; set => Properties["CORRELATION_ID"] = value; } /// 消息类型。用于区分普通消息和回复消息 public String MessageType { get => Properties.TryGetValue("MSG_TYPE", out var str) ? str : null; set => Properties["MSG_TYPE"] = value; } /// 请求超时时间(毫秒)。用于Request-Reply模式 public Int32 RequestTimeout { get => Properties.TryGetValue("REQUEST_TIMEOUT", out var str) ? str.ToInt() : 0; set => Properties["REQUEST_TIMEOUT"] = value.ToString(); } /// 附加属性 public IDictionary Properties { get; set; } #endregion #region 构造 /// 实例化 public Message() { Properties = new NullableDictionary(StringComparer.OrdinalIgnoreCase); } /// 友好字符串 /// public override String ToString() => Body != null && Body.Length > 0 ? BodyString : base.ToString(); #endregion #region 方法 /// /// 设置消息体 /// /// public void SetBody(Object body) { _BodyString = null; if (body is IPacket pk) Body = pk.ReadBytes(); else if (body is Byte[] buf) Body = buf; else if (body is String str) { _BodyString = str; Body = str.GetBytes(); } else { str = body.ToJson(); _BodyString = str; Body = str.GetBytes(); } } /// 获取属性 /// public String GetProperties() { var sb = Pool.StringBuilder.Get(); if (Properties != null && Properties.Count > 0) { foreach (var item in Properties) { sb.AppendFormat("{0}\u0001{1}\u0002", item.Key, item.Value); } } return sb.Return(true); } /// 设置属性 /// /// public void PutUserProperty(String key, String value) { if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); if (String.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value)); Properties[key] = value; } /// 获取属性 /// /// public String GetUserProperty(String key) { Properties.TryGetValue(key, out var value); return value; } /// 分析字典属性 /// public IDictionary ParseProperties(String properties) { if (properties.IsNullOrEmpty()) return Properties; var dic = SplitAsDictionary(properties, "\u0001", "\u0002"); Properties = dic; if (TryGetAndRemove(dic, nameof(Tags), out var str)) Tags = str; if (TryGetAndRemove(dic, nameof(Keys), out str)) Keys = str; if (TryGetAndRemove(dic, "DELAY", out str)) DelayTimeLevel = str.ToInt(); if (TryGetAndRemove(dic, "WAIT", out str)) WaitStoreMsgOK = str.ToBoolean(); return Properties; } private static IDictionary SplitAsDictionary(String value, String nameValueSeparator, String separator) { var dic = new NullableDictionary(StringComparer.OrdinalIgnoreCase); if (value == null || value.IsNullOrWhiteSpace()) return dic; var ss = value.Split([separator], StringSplitOptions.RemoveEmptyEntries); if (ss == null || ss.Length <= 0) return dic; foreach (var item in ss) { // 如果分隔符是 \u0001,则必须使用Ordinal,否则无法分割直接返回0 var p = item.IndexOf(nameValueSeparator, StringComparison.Ordinal); if (p <= 0) continue; var key = item[..p].Trim(); var val = item[(p + nameValueSeparator.Length)..].Trim(); #if NETFRAMEWORK || NETSTANDARD2_0 if (!dic.ContainsKey(key)) dic.Add(key, val); #else dic.TryAdd(key, val); #endif } return dic; } private Boolean TryGetAndRemove(IDictionary dic, String key, out String value) { if (dic.TryGetValue(key, out var str)) { value = str; dic.Remove(key); return true; } value = null; return false; } #endregion } ================================================ FILE: NewLife.RocketMQ/Protocol/MessageExt.cs ================================================ using System.Net; using NewLife.Buffers; using NewLife.Collections; using NewLife.Data; using NewLife.Serialization; namespace NewLife.RocketMQ.Protocol; /// 消息扩展 /// /// RocketMQ 消息二进制格式(大端序): /// StoreSize(4) + MagicCode(4) + BodyCRC(4) + QueueId(4) + Flag(4) + /// QueueOffset(8) + CommitLogOffset(8) + SysFlag(4) + BornTimestamp(8) + /// BornHost(4/16+4) + StoreTimestamp(8) + StoreHost(4/16+4) + /// ReconsumeTimes(4) + PreparedTransactionOffset(8) + /// BodyLength(4) + Body + TopicLength(1) + Topic + /// PropertiesLength(2) + Properties /// 使用 SpanReader 进行高性能解码。 /// public class MessageExt : Message, IAccessor { #region 属性 /// 队列编号 public Int32 QueueId { get; set; } /// 存储大小 public Int32 StoreSize { get; set; } /// CRC校验 public Int32 BodyCRC { get; set; } /// 队列偏移 public Int64 QueueOffset { get; set; } /// 提交日志偏移 public Int64 CommitLogOffset { get; set; } /// 系统标记 public Int32 SysFlag { get; set; } /// 生产时间 public Int64 BornTimestamp { get; set; } /// 生产主机 public String BornHost { get; set; } /// 存储时间 public Int64 StoreTimestamp { get; set; } /// 存储主机 public String StoreHost { get; set; } /// 重新消费次数 public Int32 ReconsumeTimes { get; set; } /// 准备事务偏移 public Int64 PreparedTransactionOffset { get; set; } /// 消息编号 public String MsgId { get; set; } /// Pop检查点信息。Pop消费模式下由Broker在消息属性中返回,Ack/ChangeInvisibleTime操作时需传入此值 public String PopCheckPoint { get => Properties.TryGetValue("POP_CK", out var str) ? str : null; set => Properties["POP_CK"] = value; } #endregion #region 构造 /// 友好字符串 /// public override String ToString() => $"[{CommitLogOffset}]{base.ToString()}"; #endregion #region 读写 /// 从SpanReader中读取消息 /// SpanReader引用 /// public Boolean Read(ref SpanReader reader) { // 读取 StoreSize = reader.ReadInt32(); if (StoreSize <= 0) return false; var n = reader.ReadInt32(); // MagicCode BodyCRC = reader.ReadInt32(); QueueId = reader.ReadInt32(); Flag = reader.ReadInt32(); QueueOffset = reader.ReadInt64(); CommitLogOffset = reader.ReadInt64(); SysFlag = reader.ReadInt32(); // SysFlag第2位(0x04)标识IPv6地址。IPv4=4字节,IPv6=16字节 var isIPv6 = (SysFlag & 4) != 0; var ipLen = isIPv6 ? 16 : 4; BornTimestamp = reader.ReadInt64(); var buf = reader.ReadBytes(ipLen).ToArray(); var ip = new IPAddress(buf); var port = reader.ReadInt32(); BornHost = $"{ip}:{port}"; StoreTimestamp = reader.ReadInt64(); var buf2 = reader.ReadBytes(ipLen).ToArray(); var ip2 = new IPAddress(buf2); var port2 = reader.ReadInt32(); StoreHost = $"{ip2}:{port2}"; ReconsumeTimes = reader.ReadInt32(); PreparedTransactionOffset = reader.ReadInt64(); // 主体 var len = reader.ReadInt32(); Body = reader.ReadBytes(len).ToArray(); if ((SysFlag & 1) == 1) { /*uncompress*/ // ZLIB格式RFC1950,要去掉头部两个字节 Body = Body.ReadBytes(2, -1).Decompress(); //var gs = new MemoryStream(Body); //Body = gs.DecompressGZip().ReadBytes(); } // 主题 len = reader.ReadByte(); Topic = reader.ReadBytes(len).ToArray().ToStr(); var len2 = reader.ReadInt16(); var str = reader.ReadBytes(len2).ToArray().ToStr(); ParseProperties(str); // MsgId:IPv4为16字节(4+4+8),IPv6为28字节(16+4+8) var idLen = isIPv6 ? 28 : 16; var idBuf = new Byte[idLen]; var idWriter = new SpanWriter(idBuf) { IsLittleEndian = false }; idWriter.Write(buf); idWriter.Write(port); idWriter.Write(CommitLogOffset); MsgId = idBuf.ToHex(0, idLen); return true; } /// 从数据流中读取(向后兼容) /// 数据流 /// 上下文 /// public Boolean Read(Stream stream, Object context = null) { // 读取剩余数据到缓冲区 var remaining = (Int32)(stream.Length - stream.Position); if (remaining <= 0) return false; var buf = new Byte[remaining]; var n = stream.Read(buf, 0, remaining); var reader = new SpanReader(buf, 0, n) { IsLittleEndian = false }; var rs = Read(ref reader); // 将流位置设置到实际读取位置 stream.Position = stream.Length - remaining + reader.Position; return rs; } /// 读取所有消息 /// 消息数据包 /// public static IList ReadAll(IPacket body) { var buf = body.ReadBytes(); var reader = new SpanReader(buf) { IsLittleEndian = false }; var list = new List(); while (reader.FreeCapacity > 0) { var msg = new MessageExt(); if (!msg.Read(ref reader)) break; // SysFlag第4位(0x10)标识批量消息,Body内嵌多条子消息 if ((msg.SysFlag & 0x10) != 0 && msg.Body != null && msg.Body.Length > 0) { var batches = DecodeBatch(msg); list.AddRange(batches); } else { list.Add(msg); } } return list; } /// 解码批量消息。BatchMessage 的 Body 内嵌多条子消息 /// 父消息,批量消息的外层容器 /// public static IList DecodeBatch(MessageExt parent) { if (parent == null) throw new ArgumentNullException(nameof(parent)); if (parent.Body == null || parent.Body.Length == 0) return []; var list = new List(); var reader = new SpanReader(parent.Body) { IsLittleEndian = false }; while (reader.FreeCapacity > 0) { try { // 批量消息内部格式: // 4字节 TotalSize(含自身) // 4字节 MagicCode // 4字节 BodyCRC // 4字节 Flag // 4字节 Body长度 + Body // 1字节 Topic长度 + Topic // 2字节 Properties长度 + Properties var totalSize = reader.ReadInt32(); if (totalSize <= 0) break; var magicCode = reader.ReadInt32(); var bodyCrc = reader.ReadInt32(); var flag = reader.ReadInt32(); var bodyLen = reader.ReadInt32(); var body = reader.ReadBytes(bodyLen).ToArray(); var topicLen = reader.ReadByte(); var topic = reader.ReadBytes(topicLen).ToArray().ToStr(); var propsLen = reader.ReadInt16(); var propsStr = propsLen > 0 ? reader.ReadBytes(propsLen).ToArray().ToStr() : ""; var sub = new MessageExt { // 从父消息继承上下文信息 QueueId = parent.QueueId, CommitLogOffset = parent.CommitLogOffset, SysFlag = parent.SysFlag & ~0x10, // 清除批量标志 BornTimestamp = parent.BornTimestamp, BornHost = parent.BornHost, StoreTimestamp = parent.StoreTimestamp, StoreHost = parent.StoreHost, // 子消息自身信息 StoreSize = totalSize, BodyCRC = bodyCrc, Flag = flag, Body = body, Topic = topic, }; sub.ParseProperties(propsStr); // 使用 UNIQ_KEY 作为 MsgId(如果存在) sub.MsgId = sub.TransactionId ?? parent.MsgId; list.Add(sub); } catch { break; } } return list; } /// 写入命令到数据流 /// 数据流 /// 上下文 /// public Boolean Write(Stream stream, Object context = null) => true; #endregion #region 5.x MessageId /// 创建5.x格式的MessageId。格式:01{VERSION}{MAC_HEX}{PID_HEX}{COUNTER_HEX},共34个十六进制字符 /// 版本号,默认1 /// MAC地址字节数组(6字节),为空时使用随机字节 /// 进程ID /// 消息计数器 /// 5.x格式的MessageId(34字符十六进制字符串) public static String CreateMessageId5x(Byte version, Byte[] macBytes, Int32 processId, Int32 counter) { // 格式:01 + 1字节Version + 6字节MAC + 4字节PID + 4字节Counter = 16字节 = 32 hex + 前缀"01" = 34 hex var buf = new Byte[16]; var writer = new SpanWriter(buf) { IsLittleEndian = false }; // 固定前缀 01 writer.Write((Byte)0x01); // 版本 writer.Write(version); // MAC地址(6字节) if (macBytes == null || macBytes.Length < 6) { var rand = new Byte[6]; new Random().NextBytes(rand); writer.Write(rand); } else { writer.Write(new ReadOnlySpan(macBytes, 0, 6)); } // 进程ID(4字节,大端序) writer.Write(processId); // 计数器(4字节,大端序) writer.Write(counter); return buf.ToHex(0, 16); } /// 尝试解析5.x格式的MessageId /// MessageId字符串 /// 解析出的版本号 /// 解析出的MAC地址 /// 解析出的进程ID /// 解析出的计数器 /// 是否为有效的5.x格式MessageId public static Boolean TryParseMessageId5x(String messageId, out Byte version, out Byte[] macBytes, out Int32 processId, out Int32 counter) { version = 0; macBytes = null; processId = 0; counter = 0; if (String.IsNullOrEmpty(messageId) || messageId.Length != 32) return false; // 前两个hex字符必须为"01" if (!messageId.StartsWith("01", StringComparison.OrdinalIgnoreCase)) return false; try { var bytes = messageId.ToHex(); if (bytes == null || bytes.Length != 16) return false; var reader = new SpanReader(bytes) { IsLittleEndian = false }; // bytes[0] = 0x01(前缀),跳过 reader.ReadByte(); version = reader.ReadByte(); macBytes = reader.ReadBytes(6).ToArray(); // 大端序 processId = reader.ReadInt32(); counter = reader.ReadInt32(); return true; } catch { return false; } } /// 判断是否为5.x格式的MessageId /// MessageId字符串 /// 是否为5.x格式 public static Boolean IsMessageId5x(String messageId) { if (String.IsNullOrEmpty(messageId)) return false; // 5.x格式为32个十六进制字符,前缀为"01" return messageId.Length == 32 && messageId.StartsWith("01", StringComparison.OrdinalIgnoreCase); } #endregion } ================================================ FILE: NewLife.RocketMQ/Protocol/MessageQueue.cs ================================================ namespace NewLife.RocketMQ.Protocol; /// 消息队列 public class MessageQueue { #region 属性 /// 主题 public String Topic { get; set; } /// 代理名称 public String BrokerName { get; set; } /// 队列编号 public Int32 QueueId { get; set; } #endregion #region 相等 /// 相等比较 /// /// public override Boolean Equals(Object obj) { var x = this; if (obj is not MessageQueue y) return false; return x.Topic == y.Topic && x.BrokerName == y.BrokerName && x.QueueId == y.QueueId; } /// 计算哈希 /// public override Int32 GetHashCode() { var obj = this; return obj.Topic.GetHashCode() ^ obj.BrokerName.GetHashCode() ^ obj.QueueId; } #endregion #region 辅助 /// 友好字符串 /// public override String ToString() => $"{BrokerName}[{QueueId}]"; #endregion } ================================================ FILE: NewLife.RocketMQ/Protocol/MqCodec.cs ================================================ using NewLife.Data; using NewLife.Messaging; using NewLife.Model; using NewLife.Net.Handlers; namespace NewLife.RocketMQ.Protocol; /// 编码器 class MqCodec : MessageCodec { /// 实例化编码器 public MqCodec() => UserPacket = false; /// 编码 /// /// /// protected override Object Encode(IHandlerContext context, Command msg) { if (msg is Command cmd) return cmd.ToPacket(); return null; } /// 加入队列 /// /// /// protected override void AddToQueue(IHandlerContext context, Command msg) { if (!msg.Reply && !msg.OneWay) base.AddToQueue(context, msg); } /// 解码 /// /// /// protected override IEnumerable Decode(IHandlerContext context, IPacket pk) { var ss = context.Owner as IExtend; if (ss["Codec"] is not PacketCodec pc) ss["Codec"] = pc = new PacketCodec { GetLength = p => GetLength(p, 0, -4) }; var pks = pc.Parse(pk); foreach (var e in pks) { var msg = new Command(); if (msg.Read(e.GetStream())) yield return msg; } } /// 连接关闭时,清空粘包编码器 /// /// /// public override Boolean Close(IHandlerContext context, String reason) { if (context.Owner is IExtend ss) ss["Codec"] = null; return base.Close(context, reason); } /// 是否匹配响应 /// /// /// protected override Boolean IsMatch(Object request, Object response) { return request is Command req && req.Header != null && response is Command res && res.Header != null && req.Header.Opaque == res.Header.Opaque; } } ================================================ FILE: NewLife.RocketMQ/Protocol/ProducerData.cs ================================================ namespace NewLife.RocketMQ.Protocol { /// 生产者数据 public class ProducerData { #region 属性 /// 组名 public String GroupName { get; set; } = "CLIENT_INNER_PRODUCER"; #endregion } } ================================================ FILE: NewLife.RocketMQ/Protocol/PullMessageRequestHeader.cs ================================================ using NewLife.Reflection; namespace NewLife.RocketMQ.Protocol; /// 拉取信息请求头 public class PullMessageRequestHeader { #region 属性 /// 消费组 public String ConsumerGroup { get; set; } /// 主题 public String Topic { get; set; } ///// 表达式类型 //public String ExpressionType { get; set; } = "TAG"; /// 订阅表达式 public String Subscription { get; set; } = "*"; /// 表达式类型。TAG或SQL92,默认TAG public String ExpressionType { get; set; } = "TAG"; /// 挂起超时时间。默认20_000ms public Int32 SuspendTimeoutMillis { get; set; } = 20_000; /// 子版本 public Int64 SubVersion { get; set; } /// 队列 public Int32 QueueId { get; set; } /// 队列偏移 public Int64 QueueOffset { get; set; } /// 最大消息数 public Int32 MaxMsgNums { get; set; } /// 提交偏移 public Int64 CommitOffset { get; set; } /// 系统标记 public Int32 SysFlag { get; set; } #endregion #region 方法 /// 获取属性字典 /// public IDictionary GetProperties() { //var dic = new Dictionary(); var dic = new SortedList(StringComparer.Ordinal); foreach (var pi in GetType().GetProperties()) { //if (pi.GetIndexParameters().Length > 0) continue; //if (pi.GetCustomAttribute() != null) continue; var name = pi.Name; //var att = pi.GetCustomAttribute(); //if (att != null && !att.ElementName.IsNullOrEmpty()) name = att.ElementName; name = name.Substring(0, 1).ToLower() + name.Substring(1); dic[name] = this.GetValue(pi) + ""; } return dic; } #endregion } ================================================ FILE: NewLife.RocketMQ/Protocol/PullResult.cs ================================================ using System; using System.Collections.Generic; namespace NewLife.RocketMQ.Protocol { /// 拉取状态 public enum PullStatus { /// 已发现 Found = 0, /// 没有新的消息 NoNewMessage = 1, /// 没有匹配消息 NoMatchedMessage = 2, /// 偏移量非法 OffsetIllegal = 3, /// 未知类型 Unknown = 4 } /// 拉取结果 public class PullResult { #region 属性 /// 状态 public PullStatus Status { get; set; } /// 最小偏移 public Int64 MinOffset { get; set; } /// 最大偏移 public Int64 MaxOffset { get; set; } /// 下一轮拉取偏移 public Int64 NextBeginOffset { get; set; } /// 消息 public MessageExt[] Messages { get; set; } #endregion #region 方法 /// 友好字符串 /// public override String ToString() => $"{Status} ({MinOffset},{MaxOffset})[{((Messages == null) ? 0 : Messages.Length)}]"; /// 读取数据 /// public void Read(IDictionary dic) { if (dic == null) return; var dic2 = dic.ToNullable(StringComparer.OrdinalIgnoreCase); if (dic2.TryGetValue(nameof(MinOffset), out var str)) MinOffset = str.ToLong(); if (dic2.TryGetValue(nameof(MaxOffset), out str)) MaxOffset = str.ToLong(); if (dic2.TryGetValue(nameof(NextBeginOffset), out str)) NextBeginOffset = str.ToLong(); } #endregion } } ================================================ FILE: NewLife.RocketMQ/Protocol/QueryResult.cs ================================================ using System; using System.Collections.Generic; namespace NewLife.RocketMQ.Protocol { /// 查询结果 public class QueryResult { /// 最后更新时间 public Int32 IndexLastUpdateTimestamp { get; set; } /// 消息列表 public List MessageList { get; set; } } } ================================================ FILE: NewLife.RocketMQ/Protocol/RequestCode.cs ================================================ namespace NewLife.RocketMQ.Protocol; /// 请求代码 public enum RequestCode { /// 发消息 SEND_MESSAGE = 10, /// 收消息 PULL_MESSAGE = 11, /// 查询消息 QUERY_MESSAGE = 12, /// 查询Broker偏移 QUERY_BROKER_OFFSET = 13, /// 查询消费偏移 QUERY_CONSUMER_OFFSET = 14, /// 更新消费者偏移 UPDATE_CONSUMER_OFFSET = 15, /// 更新或创建Topic UPDATE_AND_CREATE_TOPIC = 17, /// 用于向brokers查询所有的topic和它们的配置 GET_ALL_TOPIC_CONFIG = 21, /// 获取Topic配置列表 GET_TOPIC_CONFIG_LIST = 22, /// 获取Topic名列表 GET_TOPIC_NAME_LIST = 23, /// 更新Broker配置 UPDATE_BROKER_CONFIG = 25, /// 获取代理配置 GET_BROKER_CONFIG = 26, /// 触发删除文件 TRIGGER_DELETE_FILES = 27, /// 获取代理运行时信息,包括broker版本、磁盘容量、系统负载等 GET_BROKER_RUNTIME_INFO = 28, /// 按时间戳搜索偏移 SEARCH_OFFSET_BY_TIMESTAMP = 29, /// 获取topic/队列偏移量的最大值 GET_MAX_OFFSET = 30, /// 获取topic/队列偏移量的最小值 GET_MIN_OFFSET = 31, /// 获取最早消息存储时间 GET_EARLIEST_MSG_STORETIME = 32, /// 按消息ID查看消息 VIEW_MESSAGE_BY_ID = 33, /// 发送心跳 HEART_BEAT = 34, /// 注销 UNREGISTER_CLIENT = 35, /// 当consumer客户端无法处理消息时,将这些消息发送回brokers,以便将来将这些消息重新发送给consumers CONSUMER_SEND_MSG_BACK = 36, /// 结束事务 END_TRANSACTION = 37, /// 查询每个consumer group的存活成员 GET_CONSUMER_LIST_BY_GROUP = 38, /// 检查事务状态 CHECK_TRANSACTION_STATE = 39, /// 当broker得知一个consumer宕机时,它会通知其他工作的consumers尽快重新平衡 NOTIFY_CONSUMER_IDS_CHANGED = 40, /// 锁定批 LOCK_BATCH_MQ = 41, /// 解锁批 UNLOCK_BATCH_MQ = 42, /// 获取所有的消费的偏移量 GET_ALL_CONSUMER_OFFSET = 43, /// 获取延迟topic的偏移量 GET_ALL_DELAY_OFFSET = 45, /// 检查客户端配置 CHECK_CLIENT_CONFIG = 46, /// 提交KV配置 PUT_KV_CONFIG = 100, /// 获取KV配置 GET_KV_CONFIG = 101, /// 删除KV配置 DELETE_KV_CONFIG = 102, /// 注册Broker REGISTER_BROKER = 103, /// 取消注册Broker UNREGISTER_BROKER = 104, /// 获取topic路由信息 GET_ROUTEINTO_BY_TOPIC = 105, /// 获取群集信息 GET_BROKER_CLUSTER_INFO = 106, /// 创建新的consumer group或更新现有的consumer group以更改属性 UPDATE_AND_CREATE_SUBSCRIPTIONGROUP = 200, /// 查询所有已知的consumer group配置 GET_ALL_SUBSCRIPTIONGROUP_CONFIG = 201, /// 查询topic相关的统计信息 GET_TOPIC_STATS_INFO = 202, /// 获取消费者连接列表 GET_CONSUMER_CONNECTION_LIST = 203, /// 获取生产者连接列表 GET_PRODUCER_CONNECTION_LIST = 204, /// 写Broker权限 WIPE_WRITE_PERM_OF_BROKER = 205, /// 查询所有topic GET_ALL_TOPIC_LIST_FROM_NAMESERVER = 206, /// 删除订阅组 DELETE_SUBSCRIPTIONGROUP = 207, /// 获取消费者状态 GET_CONSUME_STATS = 208, /// 挂起消费者 SUSPEND_CONSUMER = 209, /// 恢复消费者 RESUME_CONSUMER = 210, /// 重置消费者偏移 RESET_CONSUMER_OFFSET_IN_CONSUMER = 211, /// 重置Broker中的消费者偏移 RESET_CONSUMER_OFFSET_IN_BROKER = 212, /// 对齐消费者线程池 ADJUST_CONSUMER_THREAD_POOL = 213, /// 查询谁消费了指定消息 WHO_CONSUME_THE_MESSAGE = 214, /// 在Broker中删除Topic DELETE_TOPIC_IN_BROKER = 215, /// 在Name服务器中删除Topic DELETE_TOPIC_IN_NAMESRV = 216, /// 根据命名空间获取KV列表 GET_KVLIST_BY_NAMESPACE = 219, /// 重置消费者客户端偏移 RESET_CONSUMER_CLIENT_OFFSET = 220, /// 从客户端获取消费者状态 GET_CONSUMER_STATUS_FROM_CLIENT = 221, /// 要求broker从consumer客户端按给定的时间戳重置偏移量 INVOKE_BROKER_TO_RESET_OFFSET = 222, /// 要求Broker获取消费者状态 INVOKE_BROKER_TO_GET_CONSUMER_STATUS = 223, /// 查询谁消费了指定Topic QUERY_TOPIC_CONSUME_BY_WHO = 300, /// 获取集群中Topic GET_TOPICS_BY_CLUSTER = 224, /// 注册文件服务器 REGISTER_FILTER_SERVER = 301, /// 注册消息过滤类 REGISTER_MESSAGE_FILTER_CLASS = 302, /// 查询消费时间 QUERY_CONSUME_TIME_SPAN = 303, /// 从名称服务器查询系统Topic列表 GET_SYSTEM_TOPIC_LIST_FROM_NS = 304, /// 从Broker服务器查询系统Topic列表 GET_SYSTEM_TOPIC_LIST_FROM_BROKER = 305, /// 清理过滤消费队列 CLEAN_EXPIRED_CONSUMEQUEUE = 306, /// 获取消费者运行消息 GET_CONSUMER_RUNNING_INFO = 307, /// 查询协调器偏移 QUERY_CORRECTION_OFFSET = 308, /// 直接消费消息 CONSUME_MESSAGE_DIRECTLY = 309, /// 发送消息V2 SEND_MESSAGE_V2 = 310, /// 获取单元Topic列表 GET_UNIT_TOPIC_LIST = 311, /// GET_HAS_UNIT_SUB_TOPIC_LIST = 312, /// GET_HAS_UNIT_SUB_UNUNIT_TOPIC_LIST = 313, /// 克隆组偏移 CLONE_GROUP_OFFSET = 314, /// 查看Broker状态数据 VIEW_BROKER_STATS_DATA = 315, /// 清理未使用Topic CLEAN_UNUSED_TOPIC = 316, /// 获取消费状态 GET_BROKER_CONSUME_STATS = 317, /// update the config of name server UPDATE_NAMESRV_CONFIG = 318, /// get config from name server GET_NAMESRV_CONFIG = 319, /// 批处理模式发送消息 SEND_BATCH_MESSAGE = 320, /// 查询消费队列 QUERY_CONSUME_QUEUE = 321, /// 查询数据版本 QUERY_DATA_VERSION = 322, /// 请求消息(Request-Reply特性) REQUEST_MESSAGE = 323, /// 发送回复消息 SEND_REPLY_MESSAGE = 324, /// 发送回复消息V2 SEND_REPLY_MESSAGE_V2 = 325, /// Pop消费消息。5.0新增的轻量消费模式,无需Rebalance POP_MESSAGE = 200050, /// 确认Pop消息。确认消费完成 ACK_MESSAGE = 200051, /// 修改Pop消息不可见时间。延长处理窗口 CHANGE_MESSAGE_INVISIBLETIME = 200052, /// 批量确认Pop消息 BATCH_ACK_MESSAGE = 200151, } ================================================ FILE: NewLife.RocketMQ/Protocol/ResponseCode.cs ================================================ namespace NewLife.RocketMQ.Protocol; /// 响应码 public enum ResponseCode { /// 成功 SUCCESS = 0, /// 系统错误 SYSTEM_ERROR = 1, /// 系统忙 SYSTEM_BUSY = 2, /// 不支持的请求码 REQUEST_CODE_NOT_SUPPORTED = 3, /// 事务失败 TRANSACTION_FAILED = 4, /// 刷磁盘超时 FLUSH_DISK_TIMEOUT = 10, /// 从机不可用 SLAVE_NOT_AVAILABLE = 11, /// 刷从机超时 FLUSH_SLAVE_TIMEOUT = 12, /// 非法消息 MESSAGE_ILLEGAL = 13, /// 服务不可用 SERVICE_NOT_AVAILABLE = 14, /// 不支持的版本 VERSION_NOT_SUPPORTED = 15, /// 没有权限 NO_PERMISSION = 16, /// 主题不存在 TOPIC_NOT_EXIST = 17, /// 主题已存在 TOPIC_EXIST_ALREADY = 18, /// 拉取未发现 PULL_NOT_FOUND = 19, /// 请重试拉取 PULL_RETRY_IMMEDIATELY = 20, /// 拉取偏移被移动 PULL_OFFSET_MOVED = 21, /// 查询未发现 QUERY_NOT_FOUND = 22, /// 订阅分析失败 SUBSCRIPTION_PARSE_FAILED = 23, /// 订阅不存在 SUBSCRIPTION_NOT_EXIST = 24, /// 订阅不是最新 SUBSCRIPTION_NOT_LATEST = 25, /// 订阅组不存在 SUBSCRIPTION_GROUP_NOT_EXIST = 26, /// 过滤数据不存在 FILTER_DATA_NOT_EXIST = 27, /// 过滤数据不是最新 FILTER_DATA_NOT_LATEST = 28, /// 事务应该提交 TRANSACTION_SHOULD_COMMIT = 200, /// 事务应该回滚 TRANSACTION_SHOULD_ROLLBACK = 201, /// 事务状态未知 TRANSACTION_STATE_UNKNOW = 202, /// 事务状态组错误 TRANSACTION_STATE_GROUP_WRONG = 203, /// 未购买 NO_BUYER_ID = 204, /// 不在当前单元 NOT_IN_CURRENT_UNIT = 205, /// 消费者不在线 CONSUMER_NOT_ONLINE = 206, /// 消费消息超时 CONSUME_MSG_TIMEOUT = 207, /// 没有消息 NO_MESSAGE = 208, } ================================================ FILE: NewLife.RocketMQ/Protocol/ResponseException.cs ================================================ namespace NewLife.RocketMQ.Protocol; /// 响应异常 public class ResponseException : Exception { /// 响应代码 public ResponseCode Code { get; set; } /// 实例化响应异常 /// /// public ResponseException(ResponseCode code, String message) : base(code + ": " + message) => Code = code; } ================================================ FILE: NewLife.RocketMQ/Protocol/SendMessageRequestHeader.cs ================================================ using System.Reflection; using System.Xml.Serialization; using NewLife.Reflection; namespace NewLife.RocketMQ.Protocol; /// 发送消息请求头 public class SendMessageRequestHeader { #region 属性 /// 生产组 [XmlElement("a")] public String ProducerGroup { get; set; } /// 主题 [XmlElement("b")] public String Topic { get; set; } /// 默认主题 [XmlElement("c")] public String DefaultTopic { get; set; } /// 默认主题队列数 [XmlElement("d")] public Int32 DefaultTopicQueueNums { get; set; } /// 队列编号 [XmlElement("e")] public Int32 QueueId { get; set; } /// 系统标记 [XmlElement("f")] public Int32 SysFlag { get; set; } /// 生产时间。毫秒 [XmlElement("g")] public Int64 BornTimestamp { get; set; } /// 标记 [XmlElement("h")] public Int32 Flag { get; set; } /// 属性。Tags/Keys等 [XmlElement("i")] public String Properties { get; set; } /// 重新消费次数 [XmlElement("j")] public Int32 ReconsumeTimes { get; set; } /// 单元模式 [XmlElement("k")] public Boolean UnitMode { get; set; } /// 消费重试次数 [XmlElement("l")] public Int32 ConsumeRetryTimes { get; set; } /// 批操作 [XmlElement("m")] public Boolean Batch { get; set; } /// Broker名称 [XmlElement("n")] public String BrokerName { get; set; } #endregion #region 方法 /// 获取属性字典 /// public IDictionary GetProperties() { var dic = new Dictionary(); foreach (var pi in GetType().GetProperties()) { if (pi.GetIndexParameters().Length > 0) continue; if (pi.GetCustomAttribute() != null) continue; var name = pi.Name; var att = pi.GetCustomAttribute(); if (att != null && !att.ElementName.IsNullOrEmpty()) name = att.ElementName; dic[name] = this.GetValue(pi); } return dic; } #endregion } ================================================ FILE: NewLife.RocketMQ/Protocol/SendResult.cs ================================================ namespace NewLife.RocketMQ.Protocol; /// 发送状态 public enum SendStatus { /// 成功 SendOK = 0, /// 刷盘超时 FlushDiskTimeout = 1, /// 刷从机超时 FlushSlaveTimeout = 2, /// 从机不可用 SlaveNotAvailable = 3, /// 发送失败 SendError = 4, } /// 发送结果 public class SendResult { #region 属性 /// 头部 public Header Header { get; set; } /// 状态 public SendStatus Status { get; set; } /// 消息编号 public String MsgId { get; set; } /// 队列 public MessageQueue Queue { get; set; } /// 队列偏移 public Int64 QueueOffset { get; set; } /// 事务编号 public String TransactionId { get; set; } /// 偏移消息编号 public String OffsetMsgId { get; set; } /// 区域 public String RegionId { get; set; } #endregion #region 方法 /// 读取结果 /// public void Read(IDictionary dic) { if (dic == null) return; var dic2 = dic.ToNullable(StringComparer.OrdinalIgnoreCase); if (dic2.TryGetValue(nameof(MsgId), out var str)) MsgId = str; if (dic2.TryGetValue(nameof(OffsetMsgId), out str)) OffsetMsgId = str; if (dic2.TryGetValue(nameof(QueueOffset), out str)) QueueOffset = str.ToLong(); if (dic2.TryGetValue(nameof(TransactionId), out str)) TransactionId = str; if (dic2.TryGetValue(nameof(RegionId), out str)) RegionId = str; if (dic2.TryGetValue("MSG_REGION", out str)) RegionId = str; } /// /// 已重载。友好显示 /// /// public override String ToString() => $"SendStatus={Status} MsgId={MsgId} OffsetMsgId={OffsetMsgId} QueueOffset={QueueOffset} Queue={Queue}"; #endregion } ================================================ FILE: NewLife.RocketMQ/Protocol/SendStatus.cs ================================================ namespace NewLife.RocketMQ.Protocol { /// 发送状态 public enum SendStatus { SendOK = 0, FlushDiskTimeout = 1, FlushSlaveTimeout = 2, SlaveNotAvailable = 3, } } ================================================ FILE: NewLife.RocketMQ/Protocol/SerializeType.cs ================================================ namespace NewLife.RocketMQ.Protocol; /// 序列化类型 public enum SerializeType : Byte { /// Json序列化 JSON = 0, /// 二进制序列化 ROCKETMQ = 1, } ================================================ FILE: NewLife.RocketMQ/Protocol/ServiceState.cs ================================================ namespace NewLife.RocketMQ.Protocol { /// 服务状态 public enum ServiceState { /// 刚刚建立 CreateJust = 0, /// 运行中 Running = 1, /// 已经关闭 ShutdownAlready = 2, /// 启动失败 StartFailed = 3, } } ================================================ FILE: NewLife.RocketMQ/Protocol/SubscriptionData.cs ================================================ namespace NewLife.RocketMQ.Protocol { /// 订阅者数据 public class SubscriptionData { #region 属性 /// 主题 public String Topic { get; set; } /// 表达式类型 public String ExpressionType { get; set; } = "TAG"; /// 子字符串 public String SubString { get; set; } = "*"; /// 标签集合 public String[] TagsSet { get; set; } /// 代码集合 public String[] CodeSet { get; set; } /// 过滤模式 public Boolean ClassFilterMode { get; set; } /// 过滤源 public String FilterClassSource { get; set; } /// 子版本 public Int64 SubVersion { get; set; } = DateTime.Now.Ticks; #endregion } } ================================================ FILE: NewLife.RocketMQ/Protocol/TransactionState.cs ================================================ namespace NewLife.RocketMQ.Protocol; /// 事务状态 public enum TransactionState { /// 预备事务(半消息) Prepared = 4, /// 提交事务 Commit = 8, /// 回滚事务 Rollback = 12, } ================================================ FILE: NewLife.RocketMQ/TencentProvider.cs ================================================ namespace NewLife.RocketMQ; /// 腾讯云 TDMQ RocketMQ 适配器 /// /// 腾讯云 TDMQ 提供 RocketMQ 4.x 兼容实例,支持 Remoting 协议。 /// 签名方式与 Apache ACL 类似,使用 HMAC-SHA1。 /// VPC 内网访问直接配置 NameServer 地址即可。 /// public class TencentProvider : ICloudProvider { /// 提供者名称 public String Name => "Tencent"; /// 访问令牌。腾讯云 SecretId public String AccessKey { get; set; } /// 访问密钥。腾讯云 SecretKey public String SecretKey { get; set; } /// 通道标识。默认TENCENT public String OnsChannel { get; set; } = "TENCENT"; /// 命名空间。腾讯云 TDMQ 中的命名空间,用于资源隔离 public String Namespace { get; set; } /// 转换主题名。腾讯云有命名空间时拼接前缀 /// 原始主题名 /// public String TransformTopic(String topic) { var ns = Namespace; if (!String.IsNullOrEmpty(ns) && !topic.StartsWith(ns)) return $"{ns}%{topic}"; return topic; } /// 转换消费组名。腾讯云有命名空间时拼接前缀 /// 原始消费组名 /// public String TransformGroup(String group) { var ns = Namespace; if (!String.IsNullOrEmpty(ns) && !group.StartsWith(ns)) return $"{ns}%{group}"; return group; } /// 获取 NameServer 地址。腾讯云不从HTTP获取,使用 VPC 内网直连 public String GetNameServerAddress() => null; } ================================================ FILE: NewLife.RocketMQ.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.2.11430.68 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NewLife.RocketMQ", "NewLife.RocketMQ\NewLife.RocketMQ.csproj", "{146B7192-0194-4924-8371-75EC83B05185}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test", "Test\Test.csproj", "{8D0806BE-ED0A-4D11-847C-38A366A33474}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnitTestRocketMQ", "XUnitTestRocketMQ\XUnitTestRocketMQ.csproj", "{670E6004-4A15-4C91-AC70-FED96843B793}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{AF932CB1-8F4D-4317-8028-62F2CACEA9BD}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig .github\workflows\publish-beta.yml = .github\workflows\publish-beta.yml .github\workflows\publish.yml = .github\workflows\publish.yml Readme.MD = Readme.MD .github\workflows\test.yml = .github\workflows\test.yml Doc\架构设计.md = Doc\架构设计.md Doc\需求文档.md = Doc\需求文档.md EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {146B7192-0194-4924-8371-75EC83B05185}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {146B7192-0194-4924-8371-75EC83B05185}.Debug|Any CPU.Build.0 = Debug|Any CPU {146B7192-0194-4924-8371-75EC83B05185}.Release|Any CPU.ActiveCfg = Release|Any CPU {146B7192-0194-4924-8371-75EC83B05185}.Release|Any CPU.Build.0 = Release|Any CPU {8D0806BE-ED0A-4D11-847C-38A366A33474}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8D0806BE-ED0A-4D11-847C-38A366A33474}.Debug|Any CPU.Build.0 = Debug|Any CPU {8D0806BE-ED0A-4D11-847C-38A366A33474}.Release|Any CPU.ActiveCfg = Release|Any CPU {8D0806BE-ED0A-4D11-847C-38A366A33474}.Release|Any CPU.Build.0 = Release|Any CPU {670E6004-4A15-4C91-AC70-FED96843B793}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {670E6004-4A15-4C91-AC70-FED96843B793}.Debug|Any CPU.Build.0 = Debug|Any CPU {670E6004-4A15-4C91-AC70-FED96843B793}.Release|Any CPU.ActiveCfg = Release|Any CPU {670E6004-4A15-4C91-AC70-FED96843B793}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9B014964-38CE-4F1B-9D25-0500977CC49B} EndGlobalSection EndGlobal ================================================ FILE: Readme.MD ================================================ # NewLife.RocketMQ - 企业级纯托管 RocketMQ 客户端 ![GitHub top language](https://img.shields.io/github/languages/top/newlifex/newlife.rocketmq?logo=github) ![GitHub License](https://img.shields.io/github/license/newlifex/newlife.rocketmq?logo=github) ![Nuget Downloads](https://img.shields.io/nuget/dt/newlife.rocketmq?logo=nuget) ![Nuget](https://img.shields.io/nuget/v/newlife.rocketmq?logo=nuget) ![Nuget (with prereleases)](https://img.shields.io/nuget/vpre/newlife.rocketmq?label=dev%20nuget&logo=nuget) **纯托管企业级 RocketMQ 客户端**,支持 `.NET Framework 4.5+` / `.NET Standard 2.0+` / `.NET Core` / `.NET 5+`。 **完全使用 C# 实现,零外部依赖(无需 Java、gRPC、Protobuf 第三方库)。** --- ## 产品简介 NewLife.RocketMQ 是新生命团队开发的**企业级纯托管 RocketMQ 客户端**,专为 .NET 生态设计。它同时支持 RocketMQ **Remoting 协议(4.x/5.x Broker)** 和 **gRPC Proxy 协议(5.x Proxy)**,覆盖生产者、消费者全部核心功能及企业级特性,统一适配阿里云、华为云、腾讯云及 Apache ACL 认证体系。 **核心优势**: | 特性 | 说明 | |------|------| | **双协议支持** | Remoting(4.x 成熟稳定)+ gRPC(5.x 面向未来),自动路由 | | **零外部依赖** | 内置 Protobuf 编解码器(ProtoWriter/ProtoReader),无需 Java 或 gRPC 运行时 | | **多云适配** | 统一 `ICloudProvider` 接口,已内置阿里云/华为云/腾讯云/Apache ACL 四家适配器 | | **生产就绪** | 消费重试、死信队列、事务回查、顺序消费、Pop 消费等企业级特性完整支持 | | **最广框架覆盖** | .NET Framework 4.5+ 到 .NET 10,gRPC 功能在 .NET Standard 2.1+ 可用 | | **高性能** | 基于 NewLife.Net 高性能网络层,连接复用、VIP 通道、消息压缩、并发控制 | --- ## 安装 ```powershell # NuGet 包管理器 Install-Package NewLife.RocketMQ # .NET CLI dotnet add package NewLife.RocketMQ ``` ```xml ``` --- ## 快速入门 ### 发送消息 ```csharp using NewLife.RocketMQ; var producer = new Producer { Topic = "test_topic", NameServerAddress = "127.0.0.1:9876", Group = "producer_group" }; producer.Start(); // 同步发送 var result = producer.Publish("Hello RocketMQ!"); Console.WriteLine($"消息ID: {result.MsgId}"); // 异步发送 await producer.PublishAsync("异步消息"); // 批量发送 await producer.PublishBatch(new[] { "消息1", "消息2", "消息3" }); ``` ### 消费消息 ```csharp var consumer = new Consumer { Topic = "test_topic", Group = "consumer_group", NameServerAddress = "127.0.0.1:9876" }; consumer.OnConsume = (q, messages) => { foreach (var msg in messages) { Console.WriteLine($"收到消息: {msg.BodyString}"); } return true; // 返回 true 表示消费成功 }; consumer.Start(); ``` ### 延迟消息 ```csharp // 18 级预设延迟 producer.PublishDelay("延迟消息", DelayTimeLevels.s30); // gRPC 模式支持任意时间延迟(需 netstandard2.1+) producer.GrpcProxyAddress = "http://127.0.0.1:8081"; await producer.PublishDelayViaGrpcAsync("任意延迟", DateTime.Now.AddMinutes(30)); ``` ### 事务消息 ```csharp var producer = new Producer { Topic = "tx_topic", Group = "tx_group", NameServerAddress = "127.0.0.1:9876" }; // 事务回查回调 producer.OnCheckTransaction = (msg, transactionId) => { var success = CheckLocalTransaction(transactionId); return success ? TransactionState.Commit : TransactionState.Rollback; }; producer.Start(); // 发送半消息 → 执行本地事务 → 提交/回滚 var sendResult = producer.PublishTransaction("订单创建"); try { ExecuteLocalTransaction(sendResult.TransactionId); producer.EndTransaction(sendResult, TransactionState.Commit); } catch { producer.EndTransaction(sendResult, TransactionState.Rollback); } ``` ### 顺序消息 ```csharp // 相同 key 的消息进入同一队列 var queue = producer.SelectQueue("order_123"); producer.Publish("顺序消息1", queue); producer.Publish("顺序消息2", queue); // 消费端启用顺序消费 consumer.OrderConsume = true; ``` ### Request-Reply 模式 ```csharp // 生产者发送请求(同步/异步) var response = producer.Request("请求消息", timeout: 5000); var reply = await producer.RequestAsync("异步请求", timeout: 5000); // 消费者回复 consumer.OnConsume = (q, messages) => { foreach (var msg in messages) { if (!String.IsNullOrEmpty(msg.CorrelationId)) consumer.SendReply(msg, "处理结果"); } return true; }; ``` --- ## 消费者高级特性 ### 消费重试与死信队列 ```csharp var consumer = new Consumer { Topic = "test_topic", Group = "consumer_group", NameServerAddress = "127.0.0.1:9876", EnableRetry = true, // 启用消费重试 MaxReconsumeTimes = 3 // 最大重试次数,超过进入 %DLQ% 死信队列 }; consumer.OnConsume = (q, messages) => { foreach (var msg in messages) { try { ProcessMessage(msg); } catch { return false; } // 返回 false 触发重试 } return true; }; ``` ### Tag / SQL92 过滤 ```csharp // Tag 过滤 consumer.Tags = "TagA || TagB"; // SQL92 表达式过滤 consumer.ExpressionType = "SQL92"; consumer.Subscription = "age > 18 AND city = 'Shanghai'"; ``` ### 多 Topic 订阅 ```csharp consumer.Topics = "topic1;topic2;topic3"; ``` ### Pop 消费模式 ```csharp // Pop 消费(手动确认) var messages = await consumer.PopMessageAsync(timeout: 10000); foreach (var msg in messages) { try { ProcessMessage(msg); await consumer.AckMessageAsync(msg); } catch { await consumer.ChangeInvisibleTimeAsync(msg, 30000); // 延长处理时间 } } ``` ### 消费限流 / VIP 通道 / 消息压缩 ```csharp consumer.MaxConcurrentConsume = 10; // 最多同时处理 10 条消息 producer.VipChannelEnabled = true; // 启用 VIP 通道(BrokerPort - 2) producer.CompressOverBytes = 4096; // 消息体超过 4KB 自动 ZLIB 压缩 ``` --- ## 云厂商接入 ### 阿里云消息队列 RocketMQ ```csharp var producer = new Producer { Topic = "test_topic", NameServerAddress = "http://MQ_INST_xxx.aliyuncs.com:80", CloudProvider = new AliyunProvider { AccessKey = "你的AccessKey", SecretKey = "你的SecretKey", InstanceId = "MQ_INST_xxx" // 可选,自动从地址解析 } }; ``` ### 华为云 DMS for RocketMQ ```csharp var producer = new Producer { Topic = "test_topic", NameServerAddress = "华为云实例地址:9876", CloudProvider = new HuaweiProvider { AccessKey = "你的AK", SecretKey = "你的SK", InstanceId = "实例ID", EnableSsl = true } }; ``` ### 腾讯云 TDMQ RocketMQ ```csharp var producer = new Producer { Topic = "test_topic", NameServerAddress = "腾讯云实例地址:9876", CloudProvider = new TencentProvider { AccessKey = "腾讯云SecretId", SecretKey = "腾讯云SecretKey", Namespace = "命名空间" } }; ``` ### Apache RocketMQ ACL 认证 ```csharp var producer = new Producer { Topic = "test_topic", NameServerAddress = "127.0.0.1:9876", CloudProvider = new AclProvider { AccessKey = "RocketMQ AccessKey", SecretKey = "RocketMQ SecretKey" } }; ``` --- ## 架构总览 ``` MqBase (业务基类) ├── Producer (生产者) └── Consumer (消费者) 通信层 ├── Remoting 协议(4.x/5.x Broker) │ ├── ClusterClient (TCP 长连接,Opaque 复用) │ ├── NameClient (路由发现,30s 轮询) │ └── BrokerClient (心跳/注销) │ └── gRPC 协议(5.x Proxy,netstandard2.1+) ├── GrpcClient (HTTP/2,Unary + Server Streaming) ├── GrpcMessagingService (11 个 RPC 方法) └── ProtoWriter/ProtoReader (自研 Protobuf 编解码) 云厂商适配层 ├── AliyunProvider (阿里云:实例ID路由 + HTTP NameServer) ├── HuaweiProvider (华为云:SSL/TLS + 实例ID路由) ├── TencentProvider (腾讯云:Namespace 前缀路由) └── AclProvider (Apache ACL:HMAC-SHA1 签名) ``` 详见 [架构设计文档](Doc/NewLife.RocketMQ架构.md)、[需求文档](Doc/NewLife.RocketMQ需求.md)。 --- ## 功能特性一览 ### 生产者 | 功能 | 状态 | 说明 | |------|:----:|------| | 同步/异步/单向发送 | ✅ | Publish / PublishAsync / PublishOneway | | 批量消息发送 | ✅ | PublishBatch,合并多条消息为一个请求 | | 延迟消息 | ✅ | 18 级预设 + gRPC 任意时间延迟 | | 事务消息 | ✅ | 半消息 + 提交/回滚 + 回查回调 | | 顺序消息 | ✅ | 指定 MessageQueue 发送 | | Request-Reply | ✅ | 同步/异步请求回复 | | 消息压缩 | ✅ | CompressOverBytes 阈值自动 ZLIB | | 消息轨迹 | ✅ | AsyncTraceDispatcher + MessageTraceHook | ### 消费者 | 功能 | 状态 | 说明 | |------|:----:|------| | Pull 消费 / 消费调度 | ✅ | 长轮询拉取,自动分配队列 | | 集群消费 / 广播消费 | ✅ | Rebalance 平均分配 / 本地偏移持久化 | | Tag / SQL92 过滤 | ✅ | 表达式过滤 | | 多 Topic 订阅 | ✅ | Topics 属性,按 Topic 分别 Rebalance | | 消费重试 + 死信队列 | ✅ | EnableRetry + MaxReconsumeTimes | | 顺序消费 | ✅ | 队列锁定(OrderConsume) | | Pop 消费 | ✅ | Pop/Ack/BatchAck/ChangeInvisibleTime | | 消费限流 | ✅ | MaxConcurrentConsume 信号量控制 | ### 管理与运维 | 功能 | 状态 | 说明 | |------|:----:|------| | Topic/消费组 CRUD | ✅ | 创建/更新/删除 | | 消息查询 | ✅ | 按 ID / 按 Key | | 消费统计 / 集群信息 | ✅ | GetConsumeStats / GetClusterInfo | | 偏移量管理与重置 | ✅ | 查询/更新/重置 | ### 协议与兼容性 | 服务端版本 | Remoting | gRPC | 说明 | |-----------|:--------:|:----:|------| | RocketMQ 4.0 ~ 4.9 | ✅ | — | 完全兼容 | | RocketMQ 5.x(Broker) | ✅ | — | Remoting 向后兼容 | | RocketMQ 5.x(Proxy) | — | ✅ | 通过 GrpcProxyAddress 启用 | | 阿里云 4.x | ✅ | — | AliyunProvider 适配 | | 华为云 DMS | ✅ | — | HuaweiProvider 适配 | | 腾讯云 TDMQ | ✅ | — | TencentProvider 适配 | --- ## 与竞品对比 | 维度 | NewLife.RocketMQ | Apache rocketmq-client-csharp | 官方 Java 客户端 | |------|:----------------:|:---------------------------:|:----------------:| | 协议支持 | Remoting + gRPC | 仅 gRPC | Remoting + gRPC | | 4.x 兼容 | ✅ | ❌ | ✅ | | 外部依赖 | **零依赖** | Google.Protobuf / Grpc.Net 等 | 多个依赖 | | .NET Framework | ✅ 4.5+ | ❌ | N/A(Java) | | 多云适配 | ✅ 内置四家 | ❌ | ❌ | | 事务/重试/死信 | ✅ 完整 | ✅ | ✅ | | 管理 API | ✅ 完整 | ❌ | ✅ | | 维护活跃度 | ✅ 持续维护 | ⚠️ 更新较慢 | ✅ 官方维护 | --- ## 测试覆盖 30+ 测试类(xUnit),覆盖核心功能、高级特性、协议兼容、云厂商适配、性能优化等场景。 --- ## 参与贡献 欢迎提交 Issue 和 Pull Request! 1. Fork 本仓库 2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) 3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) 4. 推送到分支 (`git push origin feature/AmazingFeature`) 5. 提交 Pull Request --- ## 许可协议 本项目采用 [MIT License](LICENSE) 开源协议。 --- ## 新生命项目矩阵 各项目默认支持 net10.0/net9.0/netstandard2.1/netstandard2.0/net4.62/net4.5 | 项目 | 年份 | 说明 | | :--------------------------------------------------------------: | :---: | ------------------------------------------------------------------------------------------- | | 基础组件 | | 支撑其它中间件以及产品项目 | | [NewLife.Core](https://github.com/NewLifeX/X) | 2002 | 核心库,日志、配置、缓存、网络、序列化、APM性能追踪 | | [NewLife.XCode](https://github.com/NewLifeX/NewLife.XCode) | 2005 | 大数据中间件,单表百亿级,MySql/SQLite/SqlServer/Oracle/PostgreSql/达梦,自动分表,读写分离 | | [NewLife.Net](https://github.com/NewLifeX/NewLife.Net) | 2005 | 网络库,单机千万级吞吐率(2266万tps),单机百万级连接(400万Tcp长连接) | | [NewLife.Remoting](https://github.com/NewLifeX/NewLife.Remoting) | 2011 | 协议通信库,提供CS应用通信框架,支持Http/RPC通信框架,高吞吐,物联网设备低开销易接入 | | [NewLife.Cube](https://github.com/NewLifeX/NewLife.Cube) | 2010 | 魔方快速开发平台,集成了用户权限、SSO登录、OAuth服务端等,单表100亿级项目验证 | | [NewLife.Agent](https://github.com/NewLifeX/NewLife.Agent) | 2008 | 服务管理组件,把应用安装成为操作系统守护进程,Windows服务、Linux的Systemd | | [NewLife.Zero](https://github.com/NewLifeX/NewLife.Zero) | 2020 | Zero零代脚手架,基于NewLife组件生态的项目模板NewLife.Templates,Web、WebApi、Service | | 中间件 | | 对接知名中间件平台 | | [NewLife.Redis](https://github.com/NewLifeX/NewLife.Redis) | 2017 | Redis客户端,微秒级延迟,百万级吞吐,丰富的消息队列,百亿级数据量项目验证 | | [NewLife.RocketMQ](https://github.com/NewLifeX/NewLife.RocketMQ) | 2018 | RocketMQ纯托管客户端,支持Apache RocketMQ和阿里云消息队列,十亿级项目验证 | | [NewLife.MQTT](https://github.com/NewLifeX/NewLife.MQTT) | 2019 | 物联网消息协议,MqttClient/MqttServer,客户端支持阿里云物联网 | | [NewLife.IoT](https://github.com/NewLifeX/NewLife.IoT) | 2022 | IoT标准库,定义物联网领域的各种通信协议标准规范 | | [NewLife.Modbus](https://github.com/NewLifeX/NewLife.Modbus) | 2022 | ModbusTcp/ModbusRTU/ModbusASCII,基于IoT标准库实现,支持ZeroIoT平台和IoTEdge网关 | | [NewLife.Siemens](https://github.com/NewLifeX/NewLife.Siemens) | 2022 | 西门子PLC协议,基于IoT标准库实现,支持IoT平台和IoTEdge | | [NewLife.Map](https://github.com/NewLifeX/NewLife.Map) | 2022 | 地图组件库,封装百度地图、高德地图、腾讯地图、天地图 | | [NewLife.Audio](https://github.com/NewLifeX/NewLife.Audio) | 2023 | 音频编解码库,PCM/ADPCMA/G711A/G722U/WAV/AAC | | 产品平台 | | 产品平台级,编译部署即用,个性化自定义 | | [Stardust](https://github.com/NewLifeX/Stardust) | 2018 | 星尘,分布式服务平台,节点管理、APM监控中心、配置中心、注册中心、发布中心 | | [AntJob](https://github.com/NewLifeX/AntJob) | 2019 | 蚂蚁调度,分布式大数据计算平台(实时/离线),蚂蚁搬家分片思想,万亿级数据量项目验证 | | [NewLife.ERP](https://github.com/NewLifeX/NewLife.ERP) | 2021 | 企业ERP,产品管理、客户管理、销售管理、供应商管理 | | [CrazyCoder](https://github.com/NewLifeX/XCoder) | 2006 | 码神工具,众多开发者工具,网络、串口、加解密、正则表达式、Modbus、MQTT | | [EasyIO](https://github.com/NewLifeX/EasyIO) | 2023 | 简易文件存储,支持分布式系统中文件集中存储 | | [XProxy](https://github.com/NewLifeX/XProxy) | 2005 | 产品级反向代理,NAT代理、Http代理 | | [HttpMeter](https://github.com/NewLifeX/HttpMeter) | 2022 | Http压力测试工具 | | [GitCandy](https://github.com/NewLifeX/GitCandy) | 2015 | Git源代码管理系统 | | [SmartOS](https://github.com/NewLifeX/SmartOS) | 2014 | 嵌入式操作系统,完全独立自主,支持ARM Cortex-M芯片架构 | | [SmartA2](https://github.com/NewLifeX/SmartA2) | 2019 | 嵌入式工业计算机,物联网边缘网关,高性能.NET8主机,应用于工业、农业、交通、医疗 | | FIoT物联网平台 | 2020 | 物联网整体解决方案,建筑、环保、农业,软硬件及大数据分析一体化,单机十万级点位项目验证 | | UWB高精度室内定位 | 2020 | 厘米级(10~20cm)高精度室内定位,软硬件一体化,与其它系统联动,大型展厅项目验证 | --- ## 新生命开发团队 ![XCode](https://newlifex.com/logo.png) 新生命团队(NewLife)成立于2002年,是新时代物联网行业解决方案提供者,致力于提供软硬件应用方案咨询、系统架构规划与开发服务。 团队主导的80多个开源项目已被广泛应用于各行业,Nuget累计下载量高达**400余万次**。 团队开发的大数据中间件 **NewLife.XCode**、蚂蚁调度计算平台 **AntJob**、星尘分布式平台 **Stardust**、缓存队列组件 **NewLife.Redis** 以及物联网平台 **FIoT**,均成功应用于电力、高校、互联网、电信、交通、物流、工控、医疗、文博等行业,为客户提供了大量先进、可靠、安全、高质量、易扩展的产品和系统集成服务。 我们将不断通过服务的持续改进,成为客户长期信赖的合作伙伴,通过不断的创新和发展,成为国内优秀的 IoT 服务供应商。 **新生命团队始于2002年,部分开源项目具有20年以上漫长历史,源码库保留有2010年以来所有修改记录** - 网站: - 开源: - QQ群:1600800 / 1600838 - 微信公众号: ![智能大石头](https://newlifex.com/stone.jpg) ================================================ FILE: Test/Program.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using NewLife; using NewLife.Log; using NewLife.RocketMQ; using NewLife.RocketMQ.Common; using NewLife.RocketMQ.Protocol; using NewLife.Serialization; namespace Test; class Program { static void Main(String[] args) { XTrace.UseConsole(); Test1(); //TestAliyun(); Console.WriteLine("OK!"); Console.ReadKey(); } static void Test1() { //JsonHelper.Default = new SystemJson(); XTrace.WriteLine(""); XTrace.WriteLine("创建生产者……"); var producer = new Producer { Topic = "nx_test", NameServerAddress = "rocketmq.newlifex.com:9876", Version = MQVersion.V5_2_0, ExternalBroker = true, Log = XTrace.Log, }; //producer.Configure(MqSetting.Current); producer.Start(); //mq.CreateTopic("nx_test", 2); XTrace.WriteLine(""); XTrace.WriteLine("创建消费者……"); var consumer = new Consumer { Topic = producer.Topic, Group = "test", NameServerAddress = producer.NameServerAddress, ExternalBroker = true, FromLastOffset = false, //SkipOverStoredMsgCount = 0, //BatchSize = 20, Log = XTrace.Log, ClientLog = XTrace.Log, }; consumer.OnConsume = OnConsume; //consumer.Configure(MqSetting.Current); consumer.Start(); Thread.Sleep(1000); XTrace.WriteLine(""); XTrace.WriteLine("发布测试消息……"); for (var i = 0; i < 10; i++) { var str = "学无先后达者为师" + i; //var str = Rand.NextString(1337); var sr = producer.Publish(str, "TagA", null); //Console.WriteLine("[{0}] {1} {2} {3}", sr.Queue.BrokerName, sr.Queue.QueueId, sr.MsgId, sr.QueueOffset); // 阿里云发送消息不能过快,否则报错“服务不可用” Thread.Sleep(500); } Console.WriteLine("完成"); producer.Dispose(); } private static Consumer _consumer; static void Test2() { var consumer = new Consumer { Topic = "nx_test", Group = "test", NameServerAddress = "127.0.0.1:9876", FromLastOffset = false, //SkipOverStoredMsgCount = 0, //BatchSize = 20, Log = XTrace.Log, ClientLog = XTrace.Log, }; consumer.OnConsume = OnConsume; consumer.Configure(MqSetting.Current); consumer.Start(); _consumer = consumer; } static void TestAliyun() { // 2024-04-10 对接阿里云RocketMQ v4测试通过。 // 创建RocketMQ实例后,需要手工创建Topic和Group,并创建正确的AccessKey var consumer = new Consumer { Topic = "newlife_test_02", Group = "GID_newlife_Group02", NameServerAddress = "http://MQ_INST_1827694722767531_BXxCwUhm.mq-internet-access.mq-internet.aliyuncs.com:80", Aliyun = new AliyunOptions { AccessKey = "LTAI5tKTGShu31C61xRARVC4", SecretKey = "a9oPwph1IcMGanWckzUOwOf3Ork8LO", //InstanceId = "MQ_INST_1827694722767531_BXxCwUhm", }, FromLastOffset = true, //SkipOverStoredMsgCount = 0, BatchSize = 1, Log = XTrace.Log, ClientLog = XTrace.Log, }; consumer.OnConsume = OnConsume; consumer.Configure(MqSetting.Current); consumer.Start(); _consumer = consumer; } private static Boolean OnConsume(MessageQueue q, MessageExt[] ms) { Console.WriteLine("[{0}@{1}]收到消息[{2}]", q.BrokerName, q.QueueId, ms.Length); foreach (var item in ms.ToList()) { Console.WriteLine($"消息:主键【{item.Keys}】 Topic 【{item.Topic}】,产生时间【{item.BornTimestamp.ToDateTime().ToFullString()}】,内容【{item.Body.ToStr(null, 0, 64)}】"); } return true; } static void Test3() { var dic = new SortedList(StringComparer.Ordinal) { ["subscription"] = "aaa", ["subVersion"] = "ccc", }; Console.WriteLine(dic.Join(",", e => $"{e.Key}={e.Value}")); Console.WriteLine('s' > 'V'); Console.WriteLine(); var cmp = Comparer.Default; Console.WriteLine(cmp.Compare("s", "S")); Console.WriteLine(cmp.Compare("s", "v")); Console.WriteLine(cmp.Compare("s", "V")); Console.WriteLine(); var cmp2 = StringComparer.OrdinalIgnoreCase; Console.WriteLine(cmp2.Compare("s", "S")); Console.WriteLine(cmp2.Compare("s", "v")); Console.WriteLine(cmp2.Compare("s", "V")); Console.WriteLine(); cmp2 = StringComparer.Ordinal; Console.WriteLine(cmp2.Compare("s", "S")); Console.WriteLine(cmp2.Compare("s", "v")); Console.WriteLine(cmp2.Compare("s", "V")); //dic.Clear(); //dic = dic.OrderBy(e => e.Key).ToDictionary(e => e.Key, e => e.Value); //Console.WriteLine(dic.Join(",", e => $"{e.Key}={e.Value}")); var list = new List { new BrokerInfo { Name = "A", WriteQueueNums = 5 }, new BrokerInfo { Name = "B", WriteQueueNums = 7,Addresses=new[]{ "111","222"} }, new BrokerInfo { Name = "C", WriteQueueNums = 9 }, }; var list2 = new List { new BrokerInfo { Name = "A", WriteQueueNums = 5 }, new BrokerInfo { Name = "B", WriteQueueNums = 7 ,Addresses=new[]{ "111","222"}}, new BrokerInfo { Name = "C", WriteQueueNums = 9 }, }; Console.WriteLine(list[1].Equals(list2[1])); Console.WriteLine(list2.SequenceEqual(list)); var robin = new WeightRoundRobin(); robin.Set(list.Select(e => e.WriteQueueNums).ToArray()); var count = list.Sum(e => e.WriteQueueNums); for (var i = 0; i < count; i++) { var idx = robin.Get(out var times); var bk = list[idx]; Console.WriteLine("{0} {1} {2}", i, bk.Name, times - 1); } } static void Test4() { var a1 = File.ReadAllBytes("a1".GetFullPath()); var a2 = File.ReadAllBytes("a2".GetFullPath()); //var ms = new MemoryStream(a1); //ms.Position += 2; //var ds = new DeflateStream(ms, CompressionMode.Decompress); //var buf = ds.ReadBytes(); var buf = a1.ReadBytes(2, -1).Decompress(); var rs = a2.ToBase64() == buf.ToBase64(); Console.WriteLine(rs); Console.WriteLine(buf.ToStr()); } static void Test5() { var _consumers = new List(); var topics = new List() { "flow", "flow2" }; for (int i = 0; i < 2; i++) { var consumer = new Consumer { Topic = topics[i], //Group = "test", NameServerAddress = "172.19.177.185:9876", BatchSize = 1, Log = XTrace.Log, }; consumer.OnConsume = OnConsume; consumer.Start(); _consumers.Add(consumer); } } } ================================================ FILE: Test/Test.csproj ================================================ Exe net10.0 RocketMQ测试程序 NewLife.RocketMQ 功能测试与示例程序 ..\Bin\Test false ================================================ FILE: XUnitTestRocketMQ/.github/copilot-instructions.md ================================================ # NewLife Copilot 协作指令 本说明适用于新生命团队(NewLife)及其全部开源/衍生项目,规范 Copilot 及类似智能助手在 C#/.NET 项目中的协作行为。 > 目标:把"每次请求必须携带的通用规则"控制在可接受体积;组件/业务专项流程放在 `.github/instructions/`,按需读取。 --- ## 1. 核心原则 | 原则 | 说明 | |------|------| | **提效** | 减少机械样板,聚焦业务/核心算法 | | **一致** | 风格、结构、命名、API 行为稳定 | | **可控** | 限制改动影响面,可审计,兼容友好 | | **可靠** | 先检索再生成,不虚构,不破坏现有合约 | | **主动** | 发现问题主动修复,不回避合理优化 | --- ## 2. 适用范围 - 含 NewLife 组件或衍生的全部 C#/.NET 仓库 - 不含纯前端/非 .NET/市场文案 - 存在本文件 → 必须遵循 --- ## 3. 组件专用指令索引(按需加载) 以下专用指令**仅在相关任务时**才需要读取,避免每次请求都携带大段流程/示例。 ### 3.1 XCode / Cube(数据库 & Web 快速开发) 当任务涉及以下任一信号时,请**先搜索并检查当前仓库** `.github/instructions/xcode.instructions.md` **是否存在**,若存在则读取并遵循: - 需求包含:XCode/Cube/魔方/实体生成/模型 XML/数据类库/数据库 CRUD/Controller 生成/`xcodetool`/`xcode` 命令 - 解决方案/项目中出现:`NewLife.XCode` 包引用 - 存在:`Model.xml`、`*.xcode.xml`、`*.Data.csproj`(或项目名以 `.Data` 结尾) - 代码出现命名空间/类型:`XCode.*`、`Entity`(XCode 实体基类)、XCode 相关特性/接口 - **用户提到修改任意 `.xml` 文件**(如 `member.xml`、`area.xml` 等配置文件),应**主动搜索** `xcode.instructions.md` 判断是否需要引入 **主动检测策略**:当用户提及 XML 文件修改时,即使未明确提到 XCode 关键字,也应先用 `file_search` 搜索 `xcode.instructions.md`,若存在则读取,以确定该 XML 文件是否属于 XCode/Cube 体系。 未满足以上条件时,**不要**引入 XCode/Cube 初始化流程,避免干扰其它仓库的常规开发。 --- ## 4. 工作流 ``` 需求分类 → 检索 → 评估 → 设计 → 实施 → 验证 → 说明 ``` 1. **需求分类**:功能/修复/性能/重构/文档 2. **检索**:相关类型、目录、方法、已有扩展/工具(**优先复用**) 3. **评估**:是否公共 API?是否性能热点?**是否存在潜在问题?** 4. **设计**:列出改动点 + 兼容/降级策略 5. **实施**: - 完成用户请求的核心任务 - **顺带修复**发现的明显缺陷(资源泄漏、空引用、逻辑错误) - **顺带优化**可简化的重复代码 - 保留原注释与结构,除非注释本身有误 6. **验证**: - 代码变更:必须编译通过;运行相关单元测试(未找到需说明) - 仅文档变更(未修改任何代码文件):可跳过编译与单元测试 7. **说明**:变更摘要/影响范围/风险点 ### 4.1 主动优化原则 当用户请求分析或优化代码时,**应主动**: | 类型 | 行动 | |------|------| | **架构梳理** | 梳理代码架构并进行重构,让代码结构更清晰易懂 | | **语法现代化** | 使用最新的 C# 语法来简化代码,提升可读性 | | **缺陷修复** | 资源泄漏、空引用风险、并发问题、逻辑错误 → 直接修复,让代码更健壮 | | **性能优化** | 无用分配、重复计算、可池化资源 → 通过缓存减少耗时的重复计算 | | **代码简化** | 重复代码提取、冗余判断合并、现代语法替换 → 在不影响可读性前提下简化 | | **注释完善** | 补充类、接口、属性、方法头部的注释,以及方法内部重要代码的注释 | | **架构参考** | 参考网络上同类功能的优秀架构,给出架构调整建议 | **架构调整策略**: - **改动较小**:直接调整,完成后说明变更内容 - **改动较大**:先列出调整方案,询问用户意见,待确认后再修改 **不应过度保守**: - ❌ 仅添加注释而忽略明显的代码问题 - ❌ 发现资源泄漏却不修复 - ❌ 看到重复代码却不提取 - ❌ 用户要求优化时只做表面工作 **保持谨慎的场景**: - 公共 API 签名变更 → 需说明兼容性影响 - 性能关键路径 → 需有依据或说明推理 - 大范围重构 → 需先与用户确认范围 ### 4.2 防御性注释规则 在旧有代码中,经常可以看到**被注释掉的代码**,这些注释代码前面通常带有说明文字。 **这些是防御性注释**: - 记录了过去曾经踩过的坑 - 目的是告诉后来人不要按照注释代码去写,否则会有问题 - **禁止删除此类防御性注释**,用于警示后人 **识别特征**: ```csharp // 曾经尝试过 xxx 方案,但会导致 yyy 问题 // var result = DoSomethingWrong(); // 不要使用 xxx,否则会造成 yyy // await client.SendAsync(data); // 这里不能用 xxx,因为 yyy // stream.Flush(); ``` **处理原则**: - ✅ 保留这类带说明的注释代码 - ✅ 可以补充更详细的说明,解释为什么不能这样做 - ❌ 不要删除这类防御性注释 - ❌ 不要尝试"恢复"这些被注释的代码 --- ## 5. 编码规范 ### 5.1 基础规范 | 项目 | 规范 | |------|------| | 语言版本 | `latest`,所有目标框架均使用最新 C# 语法 | | 命名空间 | file-scoped namespace | | 类型名 | **必须**使用 .NET 正式名 `String`/`Int32`/`Boolean` 等,避免 `string`/`int`/`bool` | | 兼容性 | 代码需兼容 .NET 4.5+;**禁止**使用 `ArgumentNullException.ThrowIfNull`,改用 `if (value == null) throw new ArgumentNullException(nameof(value));` | | 单文件 | 每文件一个主要公共类型;较大平台差异使用 `partial` | ### 5.2 命名规范 | 成员类型 | 命名规则 | 示例 | |---------|---------|------| | 类型/公共成员 | PascalCase | `UserService`、`GetName()` | | 参数/局部变量 | camelCase | `userName`、`count` | | 私有字段(实例/静态) | `_camelCase` | `_cache`、`_instance` | | 属性/方法(实例/静态) | PascalCase | `Name`、`Default`、`Create()` | | 扩展方法类 | `xxxHelper` 或 `xxxExtensions` | `StringHelper`、`CollectionExtensions` | ### 5.3 代码风格 ```csharp // ✅ 单行 if:单语句且整行不过长时同行 if (value == null) return; if (key == null) throw new ArgumentNullException(nameof(key)); // ✅ 单行 if:语句较长时另起一行 if (value == null) throw new ArgumentNullException(nameof(value), "Value cannot be null"); // ✅ 多分支单语句:不加花括号 if (count > 0) DoSomething(); else DoOther(); // ✅ 循环必须保留花括号(即使单语句) foreach (var item in list) { Process(item); } ``` ### 5.4 Region 组织结构 较长的类使用 `#region` 分段组织,顺序为:`属性` → `静态`(如有)→ `构造` → `方法` → `辅助`(如有)→ `日志`。 **日志 Region 规则**: - 类代码中如果带有 `ILog Log { get; set; }` 和 `WriteLog` 方法 - **必须放在类代码的最后** - **必须用名为"日志"的 region 包裹** - 不要放在"辅助" region 中,应单独作为"日志" region ### 5.5 现代 C# 语法 优先使用最新语法(switch 表达式、模式匹配、目标类型 `new`、record 等),即使目标框架是 net45。 ### 5.6 集合表达式 优先使用集合表达式 `[]` 初始化集合:`List Tags { get; set; } = [];` ### 5.7 Null 条件运算符 优先使用 `?.` / `??` 简化空值检查:`span?.AppendTag("test");` `var name = user?.Profile?.Name ?? "";` --- ## 6. 多目标框架 NewLife 支持 `net45` 到 `net10`,常用条件符号:`NETFRAMEWORK`、`NETSTANDARD2_0`、`NETCOREAPP`、`NET5_0_OR_GREATER`、`NET6_0_OR_GREATER`、`NET8_0_OR_GREATER`。 新增 API 时需评估各框架兼容性,必要时提供降级实现。 --- ## 7. 文档注释 | 规则 | 说明 | |------|------| | `` | **必须同一行闭合**,简短描述方法用途 | | `` | **必须为每个参数添加**,无论方法可见性如何 | | `` | 有返回值时必须添加 | | `` | 复杂方法可增加详细说明(可多行) | | 覆盖范围 | `public`/`protected` 成员必须注释;`private`/`internal` 建议添加 | | `[Obsolete]` | 必须包含迁移建议 | **正确示例**:`/// 获取名称` `/// 编号` **禁止**:`` 拆成多行;缺少 ``;有参数但无 param 标签。 --- ## 8. 异步与性能 | 规范 | 说明 | |------|------| | 方法命名 | 异步方法后缀 `Async` | | ConfigureAwait | 库内部默认 `ConfigureAwait(false)` | | 高频路径 | 优先对象池/`ArrayPool`/`Span`,避免多余分配 | | 反射/Linq | 仅用于非热点路径;热点使用手写循环/缓存 | | 池化资源 | 明确获取/归还;异常分支不遗失归还 | **内置工具优先**:`Pool.StringBuilder`、`Runtime.TickCount64`、`ToInt()`/`ToBoolean()` 等扩展方法。 --- ## 9. 日志与追踪 规则:若类包含 `ILog Log` 与 `WriteLog`,必须放在类末尾,并用名为"日志"的 `#region` 包裹;关键过程可使用 `Tracer?.NewSpan()` 埋点。 --- ## 10. 错误处理 - **精准异常类型**:`ArgumentNullException`/`InvalidOperationException` 等 - **参数校验**:空/越界/格式 - **TryXxx 模式**:不用异常作常规分支 - **类型转换**:优先使用 `ToInt()`/`ToBoolean()` 等扩展方法 - **对外异常**:不暴露内部实现/路径 --- ## 11. 测试规范 | 项目 | 规范 | |------|------| | 框架 | xUnit | | 命名 | `{ClassName}Tests` | | 描述 | `[DisplayName("中文描述意图")]` | | IO | 使用临时目录;端口用 0/随机 | | 覆盖 | 正常/边界/异常/并发(必要时) | ### 测试执行策略 1. 优先检索 `{ClassName}` 引用,若落入测试项目则运行 2. 未命中则查找 `{ClassName}Tests.cs` 3. **未发现相关测试需明确说明**,不自动创建测试项目 --- ## 12. NuGet 发布规范 | 类型 | 命名规则 | 示例 | |------|---------|------| | 正式版 | `{主版本}.{子版本}.{年}.{月日}` | `11.9.2025.0701` | | 测试版 | `{主版本}.{子版本}.{年}.{月日}-beta{时分}` | `11.9.2025.0701-beta0906` | - **正式版**:每月月初发布 - **测试版**:提交代码到 GitHub 时自动发布 --- ## 13. Markdown 文档规范 | 项目 | 规范 | |------|------| | 文件编码 | **必须** UTF-8,**禁止** GB2312/GBK/UTF-8 BOM | | 默认存放 | 代码库根目录下的 `Doc` 目录 | | 文件命名 | 优先**中文文件名**,简洁描述内容 | **注意**:已有文件**必须先读取**再增量修改,**禁止直接覆盖**。 --- ## 14. Copilot 行为守则 ### 必须 - 简体中文回复 - 输出前检索现有实现,**禁止重复造轮子** - 先列方案再实现 - 标记不确定上下文为"需查看文件" - **发现明显缺陷时主动修复**(资源泄漏、空引用、逻辑错误) - **用户要求优化时深入分析**,不做表面工作 ### 鼓励 - 提取重复代码为公共方法 - 简化冗余的条件判断 - 使用现代 C# 语法改进可读性 - 补充缺失的资源释放逻辑 - 修正错误或过时的注释 ### 禁止 - 虚构 API/文件/类型 - 伪造测试结果/性能数据 - 擅自删除公共/受保护成员 - 擅自删除已有代码注释(除非注释本身错误) - **删除防御性注释**(带说明的注释代码,记录历史踩坑经验) - 仅删除空白行制造"格式优化"提交 - 删除循环体的花括号 - 将 `` 拆成多行 - 将 `String`/`Int32` 改为 `string`/`int` - 新增外部依赖(除非说明理由并给出权衡) - 在热点路径添加未缓存反射/复杂 Linq - 输出敏感凭据/内部地址 - **发现问题却视而不见** - **用户要求优化时仅做注释/测试等表面工作** --- ## 15. 变更说明模板 提交或答复需包含: ```markdown ## 概述 做了什么 / 为什么 ## 影响 - 公共 API:是/否 - 性能影响:无/有(说明) ## 兼容性 降级策略 / 条件编译点 ## 风险 潜在回归 / 性能开销 ## 后续 是否补测试 / 文档 ``` --- ## 16. 术语说明 | 术语 | 定义 | |------|------| | **热点路径** | 经性能分析或高频调用栈确认的关键执行段 | | **基线** | 变更前的功能/性能参考数据 | | **顺带修复** | 在完成主任务过程中,修复发现的相关问题 | | **防御性注释** | 被注释掉的代码,前面带有说明,记录历史踩坑经验,用于警示后人 | --- ## 17. 代码优化检查清单 当进行代码优化时,按以下清单逐项检查: ### 架构与结构 - [ ] 代码架构是否清晰?是否需要重构? - [ ] 类的职责是否单一?是否需要拆分? - [ ] 是否有重复代码可以提取为公共方法? - [ ] Region 组织是否符合规范(属性→静态→构造→方法→辅助→日志)? ### 语法现代化 - [ ] 是否可以使用更简洁的 C# 语法?(switch 表达式、模式匹配等) - [ ] 集合初始化是否使用了集合表达式 `[]`? - [ ] 是否可以使用 null 条件运算符 `?.` 简化代码? ### 健壮性 - [ ] 是否存在空引用风险? - [ ] 资源是否正确释放?(IDisposable、流、连接等) - [ ] 异常处理是否完善? - [ ] 并发场景是否线程安全? ### 性能 - [ ] 是否存在可以缓存的重复计算? - [ ] 是否有不必要的对象分配? - [ ] 热点路径是否避免了反射和复杂 Linq? - [ ] 是否使用了对象池/ArrayPool 等池化技术? ### 注释与文档 - [ ] 类、接口是否有 `` 注释? - [ ] 公共方法是否有完整的参数和返回值注释? - [ ] 方法内重要逻辑是否有注释说明? - [ ] 防御性注释是否保留? ### 日志 - [ ] `ILog Log` 和 `WriteLog` 是否放在类的最后? - [ ] 是否用名为"日志"的 region 包裹? --- (完) ================================================ FILE: XUnitTestRocketMQ/AliyunIssuesTests.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Threading; using NewLife; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// /// 修复Issues调用阿里云版RocketMQ相关问题 /// #35、#24 /// public class AliyunIssuesTests { private readonly String _testTopic = "newlife_test_01"; private readonly String _testGroup = "GID_newlife_Group01"; private static readonly AliyunOptions _aliyunOptions = new AliyunOptions() { AccessKey = "LTAIxxxxxxxxxxxxRARVC4", SecretKey = "a9oPwxxxxxxxxxxx3OrxxLO", Server = "http://onsaddr-internet.aliyun.com/rocketmq/nsaddr4client-internet", InstanceId = "MQ_INST_xxxxxxxxxxxx_AXxCwUhm" }; [Fact(Skip = "需要阿里云RocketMQ服务器支持")] public void ProducerForAliyun_Test() { var producer = new Producer() { Topic = _testTopic, Aliyun = _aliyunOptions, //NameServerAddress = "http://MQ_INST_xxxxxxxxxx_AXxCwUhm.mq-internet-access.mq-internet.aliyuncs.com:80", //如果不用上面的默认Server地址,直接将NameServerAddress设为你自己的TCP公网接收点地址也是可以的 }; producer.Start(); var pubResultList = new List(); for (var i = 0; i < 2; i++) { var message = "大家好才是真的好!"; var pubResult = producer.Publish(message, "newlife_test_tag"); pubResultList.Add(pubResult.Status == SendStatus.SendOK); } Assert.True(pubResultList.All(t => true)); producer.Dispose(); } [Fact(Skip = "需要阿里云RocketMQ服务器支持")] public void ConsumerForAliyun_Test() { var consumer = new Consumer() { Topic = _testTopic, Aliyun = _aliyunOptions, Group = _testGroup, FromLastOffset = true, BatchSize = 1, }; consumer.OnConsume = OnConsume; consumer.Start(); Thread.Sleep(3000); static Boolean OnConsume(MessageQueue q, MessageExt[] ms) { Console.WriteLine("[{0}@{1}]收到消息[{2}]", q.BrokerName, q.QueueId, ms.Length); foreach (var item in ms.ToList()) { Console.WriteLine($"消息:主键【{item.Keys}】,产生时间【{item.BornTimestamp.ToDateTime()}】,内容【{item.Body.ToStr()}】"); } return true; } } } ================================================ FILE: XUnitTestRocketMQ/AliyunTests.cs ================================================ using NewLife; using NewLife.Log; using NewLife.RocketMQ; using NewLife.RocketMQ.Client; using NewLife.RocketMQ.Protocol; using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using Xunit; namespace XUnitTestRocketMQ; public class AliyunTests { private static void SetConfig(MqBase mq) { //mq.Server = "http://onsaddr-internet.aliyun.com/rocketmq/nsaddr4client-internet"; mq.Configure(MqSetting.Current); mq.Log = XTrace.Log; } [Fact(Skip = "需要阿里云RocketMQ服务器支持")] public void CreateTopic() { var mq = new Producer { //Topic = "nx_test", }; SetConfig(mq); mq.Start(); // 创建topic时,start前不能指定topic,让其使用默认TBW102 Assert.Equal("TBW102", mq.Topic); mq.CreateTopic("nx_test", 2); } [Fact(Skip = "需要阿里云RocketMQ服务器支持")] static void ProduceTest() { using var mq = new Producer { Topic = "test1", }; SetConfig(mq); mq.Start(); for (var i = 0; i < 10; i++) { var str = "学无先后达者为师" + i; //var str = Rand.NextString(1337); var sr = mq.Publish(str, "TagA", null); } } [Fact(Skip = "需要阿里云RocketMQ服务器支持")] static async Task ProduceAsyncTest() { using var mq = new Producer { Topic = "test1", }; SetConfig(mq); mq.Start(); for (var i = 0; i < 10; i++) { var str = "学无先后达者为师" + i; //var str = Rand.NextString(1337); var sr = await mq.PublishAsync(str, "TagA", null); } } private static Consumer _consumer; [Fact(Skip = "需要阿里云RocketMQ服务器支持")] static void ConsumeTest() { var consumer = new Consumer { Topic = "test1", Group = "test", FromLastOffset = true, BatchSize = 20, }; SetConfig(consumer); consumer.OnConsume = OnConsume; consumer.Start(); _consumer = consumer; Thread.Sleep(3000); } private static Boolean OnConsume(MessageQueue q, MessageExt[] ms) { Console.WriteLine("[{0}@{1}]收到消息[{2}]", q.BrokerName, q.QueueId, ms.Length); foreach (var item in ms.ToList()) { Console.WriteLine($"消息:主键【{item.Keys}】,产生时间【{item.BornTimestamp.ToDateTime()}】,内容【{item.Body.ToStr()}】"); } return true; } } ================================================ FILE: XUnitTestRocketMQ/BasicTest.cs ================================================ using NewLife.Log; using NewLife.RocketMQ; using Xunit; // 所有测试用例放入一个汇编级集合,除非单独指定Collection特性 [assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] namespace XUnitTestRocketMQ; [Collection("Basic")] public class BasicTest { private static MqSetting _config; public static MqSetting GetConfig() { if (_config != null) return _config; lock (typeof(BasicTest)) { if (_config != null) return _config; var set = MqSetting.Current; if (set.IsNew) { set.NameServer = "rocketmq.newlifex.com:9876"; set.Save(); } XTrace.WriteLine("RocketMQ配置:{0}", set.NameServer); return _config = set; } } } ================================================ FILE: XUnitTestRocketMQ/BatchAckTests.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using System.Threading.Tasks; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// 批量确认Pop消息测试 public class BatchAckTests { [Fact] [DisplayName("BatchAckMessageAsync_Null的BrokerName抛出异常")] public async Task BatchAckMessageAsync_NullBrokerName_ThrowsException() { using var consumer = new Consumer(); await Assert.ThrowsAsync(() => consumer.BatchAckMessageAsync(null, new List<(String, Int64)> { ("extra", 0) })); } [Fact] [DisplayName("BatchAckMessageAsync_空BrokerName抛出异常")] public async Task BatchAckMessageAsync_EmptyBrokerName_ThrowsException() { using var consumer = new Consumer(); await Assert.ThrowsAsync(() => consumer.BatchAckMessageAsync("", new List<(String, Int64)> { ("extra", 0) })); } [Fact] [DisplayName("BatchAckMessageAsync_空条目列表返回0")] public async Task BatchAckMessageAsync_EmptyEntries_ReturnsZero() { using var consumer = new Consumer(); var result = await consumer.BatchAckMessageAsync("broker1", new List<(String, Int64)>()); Assert.Equal(0, result); } [Fact] [DisplayName("BatchAckMessageAsync_Null条目列表返回0")] public async Task BatchAckMessageAsync_NullEntries_ReturnsZero() { using var consumer = new Consumer(); var result = await consumer.BatchAckMessageAsync("broker1", null); Assert.Equal(0, result); } [Fact] [DisplayName("BatchAckMessageAsync_无Broker连接时返回0")] public async Task BatchAckMessageAsync_NoBroker_ReturnsZero() { using var consumer = new Consumer(); // 未Start,无Broker连接 var entries = new List<(String extraInfo, Int64 offset)> { ("extra1", 100), ("extra2", 200), }; var result = await consumer.BatchAckMessageAsync("nonexistent", entries); Assert.Equal(0, result); } [Fact] [DisplayName("RequestCode包含BATCH_ACK_MESSAGE")] public void RequestCode_ContainsBatchAckMessage() { Assert.Equal(200151, (Int32)RequestCode.BATCH_ACK_MESSAGE); } } ================================================ FILE: XUnitTestRocketMQ/BatchMessageTests.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Text; using NewLife; using NewLife.Data; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// 批量消息发送测试 [Collection("Basic")] public class BatchMessageTests { [Fact] [DisplayName("批量消息发送_消息列表为空时抛出异常")] public void PublishBatch_EmptyList_ThrowsException() { using var mq = new Producer { Topic = "nx_test" }; Assert.Throws(() => mq.PublishBatch(new List())); } [Fact] [DisplayName("批量消息发送_消息列表为null时抛出异常")] public void PublishBatch_NullList_ThrowsException() { using var mq = new Producer { Topic = "nx_test" }; Assert.Throws(() => mq.PublishBatch((IList)null)); } [Fact] [DisplayName("批量消息字符串重载_空列表抛出异常")] public void PublishBatch_StringOverload_EmptyList_ThrowsException() { using var mq = new Producer { Topic = "nx_test" }; Assert.Throws(() => mq.PublishBatch(new List())); } [Fact(Skip = "需要RocketMQ服务器")] [DisplayName("批量消息发送_发送多条消息")] public void PublishBatch_SendMultipleMessages() { var set = BasicTest.GetConfig(); using var mq = new Producer { Topic = "nx_test", NameServerAddress = set.NameServer, }; mq.Start(); var messages = new List(); for (var i = 0; i < 5; i++) { var msg = new Message(); msg.SetBody($"批量消息{i}"); msg.Tags = "TagBatch"; messages.Add(msg); } var result = mq.PublishBatch(messages); Assert.NotNull(result); Assert.Equal(SendStatus.SendOK, result.Status); } #region 批量消息解码辅助方法 /// 写入大端序Int32 private static void WriteBE32(MemoryStream ms, Int32 value) { ms.WriteByte((Byte)(value >> 24)); ms.WriteByte((Byte)(value >> 16)); ms.WriteByte((Byte)(value >> 8)); ms.WriteByte((Byte)value); } /// 写入大端序Int64 private static void WriteBE64(MemoryStream ms, Int64 value) { WriteBE32(ms, (Int32)(value >> 32)); WriteBE32(ms, (Int32)value); } /// 写入大端序Int16 private static void WriteBE16(MemoryStream ms, Int16 value) { ms.WriteByte((Byte)(value >> 8)); ms.WriteByte((Byte)value); } /// 构建批量消息Body(多条子消息的二进制数据) private static Byte[] BuildBatchBody(params (String body, String topic, String props)[] items) { var ms = new MemoryStream(); foreach (var (body, topic, props) in items) { var bodyBytes = body.GetBytes(); var topicBytes = topic.GetBytes(); var propsBytes = props.GetBytes(); // TotalSize = 4+4+4+4 + 4+bodyLen + 1+topicLen + 2+propsLen var totalSize = 4 + 4 + 4 + 4 + bodyBytes.Length + 1 + topicBytes.Length + 2 + propsBytes.Length; WriteBE32(ms, totalSize); // TotalSize WriteBE32(ms, 0); // MagicCode WriteBE32(ms, 0); // BodyCRC WriteBE32(ms, 0); // Flag WriteBE32(ms, bodyBytes.Length); // BodyLength ms.Write(bodyBytes, 0, bodyBytes.Length); ms.WriteByte((Byte)topicBytes.Length); // TopicLength ms.Write(topicBytes, 0, topicBytes.Length); WriteBE16(ms, (Int16)propsBytes.Length); // PropertiesLength if (propsBytes.Length > 0) ms.Write(propsBytes, 0, propsBytes.Length); } return ms.ToArray(); } /// 构建一条完整的外层消息二进制(带SysFlag=0x10标识批量) private static Byte[] BuildOuterMessage(Byte[] batchBody) { var ms = new MemoryStream(); var ipBytes = new Byte[] { 127, 0, 0, 1 }; var topic = "batch_topic"u8.ToArray(); var props = ""u8.ToArray(); var sysFlag = 0x10; // 批量消息标志 var storeSize = 4 + 4 + 4 + 4 + 4 + 8 + 8 + 4 + 8 + 4 + 4 + 8 + 4 + 4 + 4 + 8 + 4 + batchBody.Length + 1 + topic.Length + 2 + props.Length; WriteBE32(ms, storeSize); WriteBE32(ms, 0); // MagicCode WriteBE32(ms, 0); // BodyCRC WriteBE32(ms, 0); // QueueId WriteBE32(ms, 0); // Flag WriteBE64(ms, 0L); // QueueOffset WriteBE64(ms, 100L); // CommitLogOffset WriteBE32(ms, sysFlag); // SysFlag (batch) WriteBE64(ms, 1000L); // BornTimestamp ms.Write(ipBytes, 0, 4); // BornHost IP WriteBE32(ms, 9876); // BornHost Port WriteBE64(ms, 2000L); // StoreTimestamp ms.Write(ipBytes, 0, 4); // StoreHost IP WriteBE32(ms, 10911); // StoreHost Port WriteBE32(ms, 0); // ReconsumeTimes WriteBE64(ms, 0L); // PreparedTransactionOffset WriteBE32(ms, batchBody.Length); // BodyLength ms.Write(batchBody, 0, batchBody.Length); ms.WriteByte((Byte)topic.Length); ms.Write(topic, 0, topic.Length); WriteBE16(ms, (Int16)props.Length); return ms.ToArray(); } #endregion #region 批量消息解码测试 [Fact] [DisplayName("批量消息解码_解码2条子消息")] public void DecodeBatch_TwoMessages() { var batchBody = BuildBatchBody( ("hello", "topic1", ""), ("world", "topic2", "") ); var parent = new MessageExt { QueueId = 1, CommitLogOffset = 100, SysFlag = 0x10, BornTimestamp = 1000, BornHost = "127.0.0.1:9876", StoreTimestamp = 2000, StoreHost = "127.0.0.1:10911", Body = batchBody, Topic = "batch_topic", MsgId = "PARENT_ID", }; var msgs = MessageExt.DecodeBatch(parent); Assert.Equal(2, msgs.Count); Assert.Equal("hello", msgs[0].BodyString); Assert.Equal("topic1", msgs[0].Topic); Assert.Equal(1, msgs[0].QueueId); Assert.Equal(100, msgs[0].CommitLogOffset); Assert.Equal("127.0.0.1:9876", msgs[0].BornHost); Assert.Equal("world", msgs[1].BodyString); Assert.Equal("topic2", msgs[1].Topic); } [Fact] [DisplayName("批量消息解码_解码带Properties的子消息")] public void DecodeBatch_WithProperties() { var props = "TAGS\u0001TagA\u0002KEYS\u0001Key1\u0002"; var batchBody = BuildBatchBody( ("data", "my_topic", props) ); var parent = new MessageExt { SysFlag = 0x10, Body = batchBody, MsgId = "P1", }; var msgs = MessageExt.DecodeBatch(parent); Assert.Single(msgs); Assert.Equal("data", msgs[0].BodyString); Assert.Equal("my_topic", msgs[0].Topic); Assert.Equal("TagA", msgs[0].Tags); Assert.Equal("Key1", msgs[0].Keys); } [Fact] [DisplayName("批量消息解码_空Body返回空列表")] public void DecodeBatch_EmptyBody() { var parent = new MessageExt { SysFlag = 0x10, Body = [], MsgId = "P1", }; var msgs = MessageExt.DecodeBatch(parent); Assert.Empty(msgs); } [Fact] [DisplayName("批量消息解码_Null参数抛出异常")] public void DecodeBatch_NullParent_Throws() { Assert.Throws(() => MessageExt.DecodeBatch(null)); } [Fact] [DisplayName("批量消息ReadAll自动展开")] public void ReadAll_BatchMessage_AutoExpand() { var batchBody = BuildBatchBody( ("msg1", "t1", ""), ("msg2", "t2", ""), ("msg3", "t3", "") ); var data = BuildOuterMessage(batchBody); var pk = new ArrayPacket(data); var msgs = MessageExt.ReadAll(pk); Assert.Equal(3, msgs.Count); Assert.Equal("msg1", msgs[0].BodyString); Assert.Equal("msg2", msgs[1].BodyString); Assert.Equal("msg3", msgs[2].BodyString); Assert.Equal("t1", msgs[0].Topic); Assert.Equal("t2", msgs[1].Topic); Assert.Equal("t3", msgs[2].Topic); } [Fact] [DisplayName("SysFlag批量位判断")] public void SysFlag_Batch_Bit() { Assert.NotEqual(0, 0x10 & 0x10); // 批量 Assert.Equal(0, 0 & 0x10); // 普通 Assert.NotEqual(0, 0x11 & 0x10); // 压缩+批量 Assert.Equal(0, 0x01 & 0x10); // 仅压缩 } #endregion } ================================================ FILE: XUnitTestRocketMQ/BroadcastOffsetTests.cs ================================================ using System; using System.ComponentModel; using System.IO; using NewLife.RocketMQ; using NewLife.RocketMQ.Models; using Xunit; namespace XUnitTestRocketMQ; /// 广播模式本地偏移持久化测试 public class BroadcastOffsetTests { [Fact] [DisplayName("MessageModel_默认Clustering")] public void MessageModel_DefaultClustering() { using var consumer = new Consumer(); Assert.Equal(MessageModels.Clustering, consumer.MessageModel); } [Fact] [DisplayName("MessageModel_可设置为Broadcasting")] public void MessageModel_CanSetBroadcasting() { using var consumer = new Consumer { MessageModel = MessageModels.Broadcasting }; Assert.Equal(MessageModels.Broadcasting, consumer.MessageModel); } [Fact] [DisplayName("OffsetStorePath_默认为null")] public void OffsetStorePath_DefaultNull() { using var consumer = new Consumer(); Assert.Null(consumer.OffsetStorePath); } [Fact] [DisplayName("OffsetStorePath_可自定义")] public void OffsetStorePath_CanBeCustomized() { var path = Path.Combine(Path.GetTempPath(), "test_offsets.json"); using var consumer = new Consumer { OffsetStorePath = path }; Assert.Equal(path, consumer.OffsetStorePath); } } ================================================ FILE: XUnitTestRocketMQ/BrokerFailoverTests.cs ================================================ using System; using System.ComponentModel; using System.Linq; using NewLife.RocketMQ; using Xunit; namespace XUnitTestRocketMQ; /// Broker主从切换测试 public class BrokerFailoverTests { [Fact] [DisplayName("BrokerInfo_MasterAddress属性")] public void BrokerInfo_MasterAddress() { var info = new BrokerInfo { Name = "broker-a", MasterAddress = "192.168.1.1:10911", SlaveAddresses = ["192.168.1.2:10911"], Addresses = ["192.168.1.1:10911", "192.168.1.2:10911"], IsMaster = true, }; Assert.Equal("192.168.1.1:10911", info.MasterAddress); Assert.Single(info.SlaveAddresses); Assert.Equal("192.168.1.2:10911", info.SlaveAddresses[0]); } [Fact] [DisplayName("BrokerInfo_无Slave时SlaveAddresses为空")] public void BrokerInfo_NoSlave() { var info = new BrokerInfo { Name = "broker-a", MasterAddress = "192.168.1.1:10911", SlaveAddresses = [], Addresses = ["192.168.1.1:10911"], IsMaster = true, }; Assert.Empty(info.SlaveAddresses); } [Fact] [DisplayName("BrokerInfo_Addresses以Master在前")] public void BrokerInfo_MasterFirst() { var info = new BrokerInfo { Name = "broker-a", MasterAddress = "192.168.1.1:10911", SlaveAddresses = ["192.168.1.2:10911", "192.168.1.3:10911"], Addresses = ["192.168.1.1:10911", "192.168.1.2:10911", "192.168.1.3:10911"], IsMaster = true, }; Assert.Equal("192.168.1.1:10911", info.Addresses[0]); Assert.Equal(3, info.Addresses.Length); } [Fact] [DisplayName("BrokerInfo_IsMaster标记")] public void BrokerInfo_IsMaster_Flag() { var masterInfo = new BrokerInfo { Name = "broker-a", IsMaster = true }; var slaveInfo = new BrokerInfo { Name = "broker-b", IsMaster = false }; Assert.True(masterInfo.IsMaster); Assert.False(slaveInfo.IsMaster); } } ================================================ FILE: XUnitTestRocketMQ/BrokerInfoTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ; using Xunit; namespace XUnitTestRocketMQ; /// BrokerInfo代理信息测试 public class BrokerInfoTests { #region 属性 [Fact] [DisplayName("BrokerInfo_默认值")] public void BrokerInfo_Defaults() { var bi = new BrokerInfo(); Assert.Null(bi.Name); Assert.Null(bi.Cluster); Assert.Null(bi.Addresses); Assert.Null(bi.MasterAddress); Assert.Null(bi.SlaveAddresses); Assert.False(bi.IsMaster); } [Fact] [DisplayName("BrokerInfo_设置所有属性")] public void BrokerInfo_SetAllProperties() { var bi = new BrokerInfo { Name = "broker-a", Cluster = "DefaultCluster", Addresses = ["10.0.0.1:10911", "10.0.0.2:10911"], MasterAddress = "10.0.0.1:10911", SlaveAddresses = ["10.0.0.2:10911"], Permission = Permissions.Read | Permissions.Write, ReadQueueNums = 4, WriteQueueNums = 4, TopicSynFlag = 0, IsMaster = true, }; Assert.Equal("broker-a", bi.Name); Assert.Equal("DefaultCluster", bi.Cluster); Assert.Equal(2, bi.Addresses.Length); Assert.Equal("10.0.0.1:10911", bi.MasterAddress); Assert.Single(bi.SlaveAddresses); Assert.True(bi.IsMaster); Assert.Equal(4, bi.ReadQueueNums); Assert.Equal(4, bi.WriteQueueNums); } [Fact] [DisplayName("BrokerInfo_无Slave地址")] public void BrokerInfo_NoSlaves() { var bi = new BrokerInfo { Addresses = ["10.0.0.1:10911"], MasterAddress = "10.0.0.1:10911", SlaveAddresses = [], }; Assert.Empty(bi.SlaveAddresses); } [Fact] [DisplayName("BrokerInfo_IsMaster标志")] public void BrokerInfo_IsMasterFlag() { var bi1 = new BrokerInfo { IsMaster = true }; var bi2 = new BrokerInfo { IsMaster = false }; Assert.True(bi1.IsMaster); Assert.False(bi2.IsMaster); } #endregion #region 相等比较 [Fact] [DisplayName("BrokerInfo_相同属性相等")] public void BrokerInfo_SameProperties_Equal() { var bi1 = new BrokerInfo { Name = "broker-a", Addresses = ["10.0.0.1:10911"], ReadQueueNums = 4, WriteQueueNums = 4 }; var bi2 = new BrokerInfo { Name = "broker-a", Addresses = ["10.0.0.1:10911"], ReadQueueNums = 4, WriteQueueNums = 4 }; Assert.True(bi1.Equals(bi2)); } [Fact] [DisplayName("BrokerInfo_不同Name不相等")] public void BrokerInfo_DifferentName_NotEqual() { var bi1 = new BrokerInfo { Name = "broker-a", Addresses = ["10.0.0.1:10911"] }; var bi2 = new BrokerInfo { Name = "broker-b", Addresses = ["10.0.0.1:10911"] }; Assert.False(bi1.Equals(bi2)); } [Fact] [DisplayName("BrokerInfo_与非BrokerInfo不相等")] public void BrokerInfo_NonBrokerInfo_NotEqual() { var bi = new BrokerInfo { Name = "broker-a" }; Assert.False(bi.Equals("broker-a")); Assert.False(bi.Equals(null)); } [Fact] [DisplayName("BrokerInfo_相同属性哈希相同")] public void BrokerInfo_SameProperties_SameHash() { var bi1 = new BrokerInfo { Name = "broker-a", Addresses = ["10.0.0.1:10911"], ReadQueueNums = 4, WriteQueueNums = 4 }; var bi2 = new BrokerInfo { Name = "broker-a", Addresses = ["10.0.0.1:10911"], ReadQueueNums = 4, WriteQueueNums = 4 }; // 由于 Addresses 是不同的数组实例,GetHashCode 可能不相等 // 只验证各自的哈希值是稳定的 Assert.Equal(bi1.GetHashCode(), bi1.GetHashCode()); Assert.Equal(bi2.GetHashCode(), bi2.GetHashCode()); } [Fact] [DisplayName("Permissions_读写标记")] public void Permissions_ReadWriteFlags() { Assert.Equal(2, (Int32)Permissions.Write); Assert.Equal(4, (Int32)Permissions.Read); var rw = Permissions.Read | Permissions.Write; Assert.True(rw.HasFlag(Permissions.Read)); Assert.True(rw.HasFlag(Permissions.Write)); } #endregion } ================================================ FILE: XUnitTestRocketMQ/CloudProviderTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ; using Xunit; namespace XUnitTestRocketMQ; /// 云厂商适配器接口测试 public class CloudProviderTests { #region AliyunProvider [Fact] [DisplayName("AliyunProvider_默认通道为ALIYUN")] public void AliyunProvider_DefaultOnsChannel() { var provider = new AliyunProvider(); Assert.Equal("Aliyun", provider.Name); Assert.Equal("ALIYUN", provider.OnsChannel); } [Fact] [DisplayName("AliyunProvider_有InstanceId时转换Topic")] public void AliyunProvider_TransformTopic_WithInstanceId() { var provider = new AliyunProvider { InstanceId = "MQ_INST_123" }; var result = provider.TransformTopic("test_topic"); Assert.Equal("MQ_INST_123%test_topic", result); } [Fact] [DisplayName("AliyunProvider_无InstanceId时不转换Topic")] public void AliyunProvider_TransformTopic_WithoutInstanceId() { var provider = new AliyunProvider(); var result = provider.TransformTopic("test_topic"); Assert.Equal("test_topic", result); } [Fact] [DisplayName("AliyunProvider_已有前缀时不重复添加")] public void AliyunProvider_TransformTopic_AlreadyPrefixed() { var provider = new AliyunProvider { InstanceId = "MQ_INST_123" }; var result = provider.TransformTopic("MQ_INST_123%test_topic"); Assert.Equal("MQ_INST_123%test_topic", result); } [Fact] [DisplayName("AliyunProvider_有InstanceId时转换Group")] public void AliyunProvider_TransformGroup_WithInstanceId() { var provider = new AliyunProvider { InstanceId = "MQ_INST_123" }; var result = provider.TransformGroup("test_group"); Assert.Equal("MQ_INST_123%test_group", result); } [Fact] [DisplayName("AliyunProvider_无Server时GetNameServerAddress返回null")] public void AliyunProvider_GetNameServerAddress_NoServer() { var provider = new AliyunProvider(); var result = provider.GetNameServerAddress(); Assert.Null(result); } [Fact] [DisplayName("AliyunProvider_非HTTP地址时GetNameServerAddress返回null")] public void AliyunProvider_GetNameServerAddress_NonHttp() { var provider = new AliyunProvider { Server = "tcp://example.com" }; var result = provider.GetNameServerAddress(); Assert.Null(result); } [Fact] [DisplayName("AliyunProvider_FromOptions转换旧版选项")] public void AliyunProvider_FromOptions() { var options = new AliyunOptions { AccessKey = "ak", SecretKey = "sk", InstanceId = "inst1", OnsChannel = "ALIYUN", Server = "http://test.com", }; var provider = AliyunProvider.FromOptions(options); Assert.NotNull(provider); Assert.Equal("ak", provider.AccessKey); Assert.Equal("sk", provider.SecretKey); Assert.Equal("inst1", provider.InstanceId); Assert.Equal("ALIYUN", provider.OnsChannel); Assert.Equal("http://test.com", provider.Server); } [Fact] [DisplayName("AliyunProvider_FromOptions_Null返回Null")] public void AliyunProvider_FromOptions_Null() { var result = AliyunProvider.FromOptions(null); Assert.Null(result); } #endregion #region AclProvider [Fact] [DisplayName("AclProvider_默认属性")] public void AclProvider_Defaults() { var provider = new AclProvider(); Assert.Equal("ACL", provider.Name); Assert.Equal("", provider.OnsChannel); } [Fact] [DisplayName("AclProvider_不转换Topic和Group")] public void AclProvider_NoTransform() { var provider = new AclProvider(); Assert.Equal("test_topic", provider.TransformTopic("test_topic")); Assert.Equal("test_group", provider.TransformGroup("test_group")); } [Fact] [DisplayName("AclProvider_GetNameServerAddress返回null")] public void AclProvider_GetNameServerAddress_Null() { var provider = new AclProvider(); Assert.Null(provider.GetNameServerAddress()); } [Fact] [DisplayName("AclProvider_FromOptions转换旧版选项")] public void AclProvider_FromOptions() { var options = new AclOptions { AccessKey = "acl_ak", SecretKey = "acl_sk", OnsChannel = "LOCAL", }; var provider = AclProvider.FromOptions(options); Assert.NotNull(provider); Assert.Equal("acl_ak", provider.AccessKey); Assert.Equal("acl_sk", provider.SecretKey); Assert.Equal("LOCAL", provider.OnsChannel); } [Fact] [DisplayName("AclProvider_FromOptions_Null返回Null")] public void AclProvider_FromOptions_Null() { var result = AclProvider.FromOptions(null); Assert.Null(result); } #endregion #region HuaweiProvider [Fact] [DisplayName("HuaweiProvider_默认通道为HUAWEI")] public void HuaweiProvider_Defaults() { var provider = new HuaweiProvider(); Assert.Equal("Huawei", provider.Name); Assert.Equal("HUAWEI", provider.OnsChannel); Assert.False(provider.EnableSsl); } [Fact] [DisplayName("HuaweiProvider_不转换Topic和Group")] public void HuaweiProvider_NoTransform() { var provider = new HuaweiProvider(); Assert.Equal("test_topic", provider.TransformTopic("test_topic")); Assert.Equal("test_group", provider.TransformGroup("test_group")); } [Fact] [DisplayName("HuaweiProvider_GetNameServerAddress返回null")] public void HuaweiProvider_GetNameServerAddress_Null() { var provider = new HuaweiProvider(); Assert.Null(provider.GetNameServerAddress()); } #endregion #region TencentProvider [Fact] [DisplayName("TencentProvider_默认通道为TENCENT")] public void TencentProvider_Defaults() { var provider = new TencentProvider(); Assert.Equal("Tencent", provider.Name); Assert.Equal("TENCENT", provider.OnsChannel); } [Fact] [DisplayName("TencentProvider_有Namespace时转换Topic")] public void TencentProvider_TransformTopic_WithNamespace() { var provider = new TencentProvider { Namespace = "ns1" }; var result = provider.TransformTopic("test_topic"); Assert.Equal("ns1%test_topic", result); } [Fact] [DisplayName("TencentProvider_无Namespace时不转换Topic")] public void TencentProvider_TransformTopic_WithoutNamespace() { var provider = new TencentProvider(); var result = provider.TransformTopic("test_topic"); Assert.Equal("test_topic", result); } [Fact] [DisplayName("TencentProvider_有Namespace时转换Group")] public void TencentProvider_TransformGroup_WithNamespace() { var provider = new TencentProvider { Namespace = "ns1" }; var result = provider.TransformGroup("test_group"); Assert.Equal("ns1%test_group", result); } [Fact] [DisplayName("TencentProvider_已有前缀时不重复添加")] public void TencentProvider_TransformTopic_AlreadyPrefixed() { var provider = new TencentProvider { Namespace = "ns1" }; var result = provider.TransformTopic("ns1%test_topic"); Assert.Equal("ns1%test_topic", result); } [Fact] [DisplayName("TencentProvider_GetNameServerAddress返回null")] public void TencentProvider_GetNameServerAddress_Null() { var provider = new TencentProvider(); Assert.Null(provider.GetNameServerAddress()); } #endregion #region MqBase集成 [Fact] [DisplayName("MqBase_CloudProvider默认为null")] public void MqBase_CloudProvider_DefaultNull() { using var producer = new Producer(); Assert.Null(producer.CloudProvider); } [Fact] [DisplayName("MqBase_设置CloudProvider")] public void MqBase_CloudProvider_CanBeSet() { using var producer = new Producer { CloudProvider = new AliyunProvider { AccessKey = "ak", SecretKey = "sk", InstanceId = "MQ_INST_123", } }; Assert.NotNull(producer.CloudProvider); Assert.IsType(producer.CloudProvider); Assert.Equal("ak", producer.CloudProvider.AccessKey); } [Fact] [DisplayName("MqBase_设置旧版Aliyun自动同步到CloudProvider")] public void MqBase_LegacyAliyun_SyncsToCloudProvider() { #pragma warning disable CS0618 using var producer = new Producer { Aliyun = new AliyunOptions { AccessKey = "ak", SecretKey = "sk", } }; #pragma warning restore CS0618 Assert.NotNull(producer.CloudProvider); Assert.IsType(producer.CloudProvider); Assert.Equal("ak", producer.CloudProvider.AccessKey); } [Fact] [DisplayName("MqBase_设置旧版AclOptions自动同步到CloudProvider")] public void MqBase_LegacyAclOptions_SyncsToCloudProvider() { #pragma warning disable CS0618 using var producer = new Producer { AclOptions = new AclOptions { AccessKey = "acl_ak", SecretKey = "acl_sk", } }; #pragma warning restore CS0618 Assert.NotNull(producer.CloudProvider); Assert.IsType(producer.CloudProvider); Assert.Equal("acl_ak", producer.CloudProvider.AccessKey); } [Fact] [DisplayName("MqBase_显式设置CloudProvider不被旧版属性覆盖")] public void MqBase_ExplicitCloudProvider_NotOverridden() { var acl = new AclProvider { AccessKey = "explicit_ak", SecretKey = "sk" }; using var producer = new Producer { CloudProvider = acl, }; #pragma warning disable CS0618 // 设置旧版属性时,因为 CloudProvider 已有值,不会覆盖 producer.Aliyun = new AliyunOptions { AccessKey = "old_ak", SecretKey = "sk" }; #pragma warning restore CS0618 Assert.Same(acl, producer.CloudProvider); Assert.Equal("explicit_ak", producer.CloudProvider.AccessKey); } [Fact] [DisplayName("MqBase_腾讯云Provider集成测试")] public void MqBase_TencentProvider_Integration() { using var producer = new Producer { CloudProvider = new TencentProvider { AccessKey = "tencent_id", SecretKey = "tencent_key", Namespace = "ns_test", } }; Assert.NotNull(producer.CloudProvider); Assert.IsType(producer.CloudProvider); Assert.Equal("tencent_id", producer.CloudProvider.AccessKey); var tp = (TencentProvider)producer.CloudProvider; Assert.Equal("ns_test%my_topic", tp.TransformTopic("my_topic")); Assert.Equal("ns_test%my_group", tp.TransformGroup("my_group")); } #endregion } ================================================ FILE: XUnitTestRocketMQ/CommandTests.cs ================================================ using System; using System.IO; using System.Reflection; using NewLife; using NewLife.Data; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using NewLife.Serialization; using Xunit; namespace XUnitTestRocketMQ; public class CommandTests { [Fact] public void DecodeJson() { var data = """ 00 00 01 06 00 00 00 72 7b 22 63 6f 64 65 22 3a 33 34 2c 22 6c 61 6e 67 75 61 67 65 22 3a 22 44 4f 54 4e 45 54 22 2c 22 6f 70 61 71 75 65 22 3a 31 35 37 35 2c 22 73 65 72 69 61 6c 69 7a 65 54 79 70 65 43 75 72 72 65 6e 74 52 50 43 22 3a 22 4a 53 4f 4e 22 2c 22 76 65 72 73 69 6f 6e 22 3a 33 37 33 2c 22 72 65 6d 61 72 6b 22 3a 22 48 45 41 52 54 5f 42 45 41 54 22 7d 7b 22 43 6c 69 65 6e 74 49 44 22 3a 22 31 30 2e 37 2e 36 39 2e 32 30 35 40 36 33 32 38 34 22 2c 22 43 6f 6e 73 75 6d 65 72 44 61 74 61 53 65 74 22 3a 5b 5d 2c 22 50 72 6f 64 75 63 65 72 44 61 74 61 53 65 74 22 3a 5b 7b 22 47 72 6f 75 70 4e 61 6d 65 22 3a 22 44 45 46 41 55 4c 54 5f 50 52 4f 44 55 43 45 52 22 7d 2c 7b 22 47 72 6f 75 70 4e 61 6d 65 22 3a 22 43 4c 49 45 4e 54 5f 49 4e 4e 45 52 5f 50 52 4f 44 55 43 45 52 22 7d 5d 7d """; var ms = new MemoryStream(data.ToHex()); var cmd = new Command(); var rs = cmd.Read(ms); Assert.True(rs); var header = cmd.Header; Assert.NotNull(header); Assert.Equal((Int32)RequestCode.HEART_BEAT, header.Code); Assert.Equal(0, header.Flag); Assert.Equal(LanguageCode.DOTNET + "", header.Language); Assert.Equal(SerializeType.JSON + "", header.SerializeTypeCurrentRPC); Assert.Equal(MQVersion.V4_8_0, header.Version); //Assert.Empty(header.Remark); Assert.Equal("HEART_BEAT", header.Remark); var ext = header.GetExtFields(); Assert.Empty(ext); var pk = cmd.Payload; Assert.NotNull(pk); var dic = JsonParser.Decode(pk.ToStr()); Assert.Equal(3, dic.Count); } [Fact] public void DecodeJson2() { var data = """ 00 00 03 0c 00 00 01 4c 7b 22 63 6f 64 65 22 3a 33 31 30 2c 22 65 78 74 46 69 65 6c 64 73 22 3a 7b 22 61 22 3a 22 44 45 46 41 55 4c 54 5f 50 52 4f 44 55 43 45 52 22 2c 22 62 22 3a 22 49 4f 43 5f 49 4e 53 5f 50 41 52 53 45 5f 54 4f 50 49 43 22 2c 22 63 22 3a 22 54 42 57 31 30 32 22 2c 22 64 22 3a 22 30 22 2c 22 65 22 3a 22 36 22 2c 22 66 22 3a 22 30 22 2c 22 67 22 3a 22 31 36 39 33 34 35 39 38 32 32 35 37 33 22 2c 22 68 22 3a 22 30 22 2c 22 69 22 3a 22 54 41 47 53 01 54 30 32 30 30 02 4b 45 59 53 01 30 61 30 37 34 35 63 64 31 36 39 33 34 35 39 38 32 32 35 36 30 62 33 36 33 65 66 37 33 34 02 57 41 49 54 01 54 72 75 65 02 22 2c 22 6a 22 3a 22 30 22 2c 22 6b 22 3a 22 46 61 6c 73 65 22 7d 2c 22 6c 61 6e 67 75 61 67 65 22 3a 22 44 4f 54 4e 45 54 22 2c 22 6f 70 61 71 75 65 22 3a 31 35 37 36 2c 22 73 65 72 69 61 6c 69 7a 65 54 79 70 65 43 75 72 72 65 6e 74 52 50 43 22 3a 22 4a 53 4f 4e 22 2c 22 76 65 72 73 69 6f 6e 22 3a 33 37 33 2c 22 72 65 6d 61 72 6b 22 3a 22 53 45 4e 44 5f 4d 45 53 53 41 47 45 5f 56 32 22 7d 7b 22 4b 69 6e 64 22 3a 22 54 30 32 30 30 22 2c 22 56 65 72 73 69 6f 6e 22 3a 22 4a 54 32 30 31 33 22 2c 22 4d 6f 62 69 6c 65 22 3a 22 31 34 32 37 30 35 37 39 34 35 37 22 2c 22 53 65 71 75 65 6e 63 65 22 3a 35 31 35 34 2c 22 42 6f 64 79 22 3a 7b 22 41 6c 61 72 6d 22 3a 30 2c 22 53 74 61 74 75 73 22 3a 37 38 36 34 33 35 2c 22 4c 61 74 69 74 75 64 65 22 3a 33 36 38 37 31 33 30 30 2c 22 4c 6f 6e 67 69 74 75 64 65 22 3a 31 31 37 31 32 31 38 33 31 2c 22 41 6c 74 69 74 75 64 65 22 3a 31 36 2c 22 53 70 65 65 64 22 3a 30 2c 22 44 69 72 65 63 74 69 6f 6e 22 3a 31 31 35 2c 22 47 50 53 54 69 6d 65 22 3a 22 32 30 32 33 2d 30 38 2d 33 31 20 31 33 3a 33 30 3a 31 36 22 2c 22 41 64 64 69 74 69 6f 6e 61 6c 73 22 3a 7b 22 e9 87 8c e7 a8 8b 22 3a 32 36 35 30 34 30 39 2c 22 e9 80 9f e5 ba a6 22 3a 30 2c 22 e8 a7 86 e9 a2 91 e6 8a a5 e8 ad a6 22 3a 30 2c 22 e8 a7 86 e9 a2 91 e4 b8 a2 e5 a4 b1 22 3a 30 2c 22 e8 a7 86 e9 a2 91 e9 81 ae e6 8c a1 22 3a 30 2c 22 e5 ad 98 e5 82 a8 e6 95 85 e9 9a 9c 22 3a 30 2c 22 e5 bc 82 e5 b8 b8 e9 a9 be e9 a9 b6 22 3a 22 41 41 41 41 22 2c 22 e8 bd a6 e8 be 86 e4 bf a1 e5 8f b7 22 3a 30 2c 22 e6 a8 a1 e6 8b 9f e9 87 8f 22 3a 30 2c 22 e4 bf a1 e5 8f b7 e5 bc ba e5 ba a6 22 3a 34 2c 22 e5 8d ab e6 98 9f e6 95 b0 22 3a 33 32 2c 22 31 38 33 22 3a 30 2c 22 32 34 38 22 3a 30 2c 22 32 33 39 22 3a 30 7d 7d 7d """; var ms = new MemoryStream(data.ToHex()); var cmd = new Command(); var rs = cmd.Read(ms); Assert.True(rs); var header = cmd.Header; Assert.NotNull(header); Assert.Equal((Int32)RequestCode.SEND_MESSAGE_V2, header.Code); Assert.Equal(0, header.Flag); Assert.Equal(LanguageCode.DOTNET + "", header.Language); Assert.Equal(SerializeType.JSON + "", header.SerializeTypeCurrentRPC); Assert.Equal(MQVersion.V4_8_0, header.Version); Assert.Equal("SEND_MESSAGE_V2", header.Remark); var ext = header.GetExtFields(); Assert.NotEmpty(ext); Assert.Equal(11, ext.Count); var pk = cmd.Payload; Assert.NotNull(pk); var dic = JsonParser.Decode(pk.ToStr()); Assert.Equal(5, dic.Count); } [Fact] public void DecodeRocketMQ() { var data = """ 00 00 00 19 01 00 00 15 00 00 00 01 75 00 00 06 27 00 00 00 01 00 00 00 00 00 00 00 00 """; var ms = new MemoryStream(data.ToHex()); var cmd = new Command(); var rs = cmd.Read(ms); Assert.True(rs); var header = cmd.Header; Assert.NotNull(header); Assert.Equal((Int32)ResponseCode.SUCCESS, header.Code); Assert.Equal(1, header.Flag); Assert.Equal(LanguageCode.JAVA + "", header.Language); Assert.Equal(SerializeType.ROCKETMQ + "", header.SerializeTypeCurrentRPC); Assert.Equal(MQVersion.V4_8_0, header.Version); Assert.Null(header.Remark); var ext = header.GetExtFields(); Assert.Empty(ext); var pk = cmd.Payload; Assert.Null(pk); } [Fact] public void DecodeRocketMQ2() { var data = """ 00 00 00 99 01 00 00 95 00 00 00 01 75 00 00 06 28 00 00 00 01 00 00 00 00 00 00 00 80 00 07 71 75 65 75 65 49 64 00 00 00 01 36 00 08 54 52 41 43 45 5f 4f 4e 00 00 00 04 74 72 75 65 00 0a 4d 53 47 5f 52 45 47 49 4f 4e 00 00 00 0d 44 65 66 61 75 6c 74 52 65 67 69 6f 6e 00 05 6d 73 67 49 64 00 00 00 20 30 41 30 39 30 46 32 38 30 30 30 30 32 41 39 46 30 30 30 30 32 32 39 45 38 39 37 43 46 33 38 32 00 0b 71 75 65 75 65 4f 66 66 73 65 74 00 00 00 07 36 33 37 39 38 38 31 """; var ms = new MemoryStream(data.ToHex()); var cmd = new Command(); var rs = cmd.Read(ms); Assert.True(rs); var header = cmd.Header; Assert.NotNull(header); Assert.Equal((Int32)ResponseCode.SUCCESS, header.Code); Assert.Equal(1, header.Flag); Assert.Equal(LanguageCode.JAVA + "", header.Language); Assert.Equal(SerializeType.ROCKETMQ + "", header.SerializeTypeCurrentRPC); Assert.Equal(MQVersion.V4_8_0, header.Version); Assert.Null(header.Remark); var ext = header.GetExtFields(); Assert.NotEmpty(ext); Assert.Equal(5, ext.Count); var pk = cmd.Payload; Assert.Null(pk); } [Fact] public void GetRouteInfo_v520_Java() { var data = """ 00 00 00 8a 00 00 00 86 7b 22 63 6f 64 65 22 3a 31 30 35 2c 22 65 78 74 46 69 65 6c 64 73 22 3a 7b 22 74 6f 70 69 63 22 3a 22 54 65 5a 5f 54 65 73 74 5f 4c 6e 67 22 7d 2c 22 66 6c 61 67 22 3a 30 2c 22 6c 61 6e 67 75 61 67 65 22 3a 22 4a 41 56 41 22 2c 22 6f 70 61 71 75 65 22 3a 30 2c 22 73 65 72 69 61 6c 69 7a 65 54 79 70 65 43 75 72 72 65 6e 74 52 50 43 22 3a 22 4a 53 4f 4e 22 2c 22 76 65 72 73 69 6f 6e 22 3a 34 35 33 7d """; var ms = new MemoryStream(data.ToHex()); var cmd = new Command(); var rs = cmd.Read(ms); Assert.True(rs); Assert.False(cmd.Reply); Assert.Equal(""" {"code":105,"extFields":{"topic":"TeZ_Test_Lng"},"flag":0,"language":"JAVA","opaque":0,"serializeTypeCurrentRPC":"JSON","version":453} """, cmd.RawJson); var header = cmd.Header; Assert.NotNull(header); Assert.Equal((Int32)RequestCode.GET_ROUTEINTO_BY_TOPIC, header.Code); Assert.Equal(0, header.Flag); Assert.Equal(LanguageCode.JAVA + "", header.Language); Assert.Equal(SerializeType.JSON + "", header.SerializeTypeCurrentRPC); Assert.Equal(MQVersion.V5_2_0, header.Version); Assert.Null(header.Remark); var ext = header.GetExtFields(); Assert.Single(ext); Assert.Equal("TeZ_Test_Lng", ext["topic"]); var pk = cmd.Payload; Assert.Null(pk); } [Fact] public void DecodeRouteInfo_v520_Java() { var data = """ 00 00 01 6a 00 00 00 5f 7b 22 63 6f 64 65 22 3a 30 2c 22 66 6c 61 67 22 3a 31 2c 22 6c 61 6e 67 75 61 67 65 22 3a 22 4a 41 56 41 22 2c 22 6f 70 61 71 75 65 22 3a 30 2c 22 73 65 72 69 61 6c 69 7a 65 54 79 70 65 43 75 72 72 65 6e 74 52 50 43 22 3a 22 4a 53 4f 4e 22 2c 22 76 65 72 73 69 6f 6e 22 3a 34 35 33 7d 7b 22 62 72 6f 6b 65 72 44 61 74 61 73 22 3a 5b 7b 22 62 72 6f 6b 65 72 41 64 64 72 73 22 3a 7b 22 30 22 3a 22 31 30 2e 32 2e 33 2e 31 31 37 3a 31 30 39 31 31 22 7d 2c 22 62 72 6f 6b 65 72 4e 61 6d 65 22 3a 22 62 72 6f 6b 65 72 2d 61 22 2c 22 63 6c 75 73 74 65 72 22 3a 22 44 65 66 61 75 6c 74 43 6c 75 73 74 65 72 22 2c 22 65 6e 61 62 6c 65 41 63 74 69 6e 67 4d 61 73 74 65 72 22 3a 66 61 6c 73 65 7d 5d 2c 22 66 69 6c 74 65 72 53 65 72 76 65 72 54 61 62 6c 65 22 3a 7b 7d 2c 22 71 75 65 75 65 44 61 74 61 73 22 3a 5b 7b 22 62 72 6f 6b 65 72 4e 61 6d 65 22 3a 22 62 72 6f 6b 65 72 2d 61 22 2c 22 70 65 72 6d 22 3a 36 2c 22 72 65 61 64 51 75 65 75 65 4e 75 6d 73 22 3a 38 2c 22 74 6f 70 69 63 53 79 73 46 6c 61 67 22 3a 30 2c 22 77 72 69 74 65 51 75 65 75 65 4e 75 6d 73 22 3a 38 7d 5d 7d """; var ms = new MemoryStream(data.ToHex()); var cmd = new Command(); var rs = cmd.Read(ms); Assert.True(rs); Assert.True(cmd.Reply); Assert.Equal(""" {"code":0,"flag":1,"language":"JAVA","opaque":0,"serializeTypeCurrentRPC":"JSON","version":453} """, cmd.RawJson); var header = cmd.Header; Assert.NotNull(header); Assert.Equal(0, header.Code); Assert.Equal(1, header.Flag); Assert.Equal(LanguageCode.JAVA + "", header.Language); Assert.Equal(SerializeType.JSON + "", header.SerializeTypeCurrentRPC); Assert.Equal(MQVersion.V5_2_0, header.Version); Assert.Null(header.Remark); var ext = header.GetExtFields(); Assert.Empty(ext); var pk = cmd.Payload; Assert.NotNull(pk); var target = """ {"brokerDatas":[{"brokerAddrs":{"0":"10.2.3.117:10911"},"brokerName":"broker-a","cluster":"DefaultCluster","enableActingMaster":false}],"filterServerTable":{},"queueDatas":[{"brokerName":"broker-a","perm":6,"readQueueNums":8,"topicSysFlag":0,"writeQueueNums":8}]} """; var json = pk.ToStr(); Assert.Equal(target, json); var dic = cmd.ReadBodyAsJson(); Assert.True(dic.ContainsKey("brokerDatas")); Assert.True(dic.ContainsKey("queueDatas")); } [Fact] public void GetRouteInfo_v520_Dotnet() { var data = """ 00 00 00 a5 00 00 00 a1 7b 22 63 6f 64 65 22 3a 31 30 35 2c 22 65 78 74 46 69 65 6c 64 73 22 3a 7b 22 74 6f 70 69 63 22 3a 22 54 65 5a 5f 54 65 73 74 5f 4c 6e 67 22 7d 2c 22 6c 61 6e 67 75 61 67 65 22 3a 22 44 4f 54 4e 45 54 22 2c 22 6f 70 61 71 75 65 22 3a 31 2c 22 73 65 72 69 61 6c 69 7a 65 54 79 70 65 43 75 72 72 65 6e 74 52 50 43 22 3a 22 4a 53 4f 4e 22 2c 22 76 65 72 73 69 6f 6e 22 3a 33 37 33 2c 22 72 65 6d 61 72 6b 22 3a 22 47 45 54 5f 52 4f 55 54 45 49 4e 54 4f 5f 42 59 5f 54 4f 50 49 43 22 7d """; var ms = new MemoryStream(data.ToHex()); var cmd = new Command(); var rs = cmd.Read(ms); Assert.True(rs); Assert.False(cmd.Reply); Assert.Equal(""" {"code":105,"extFields":{"topic":"TeZ_Test_Lng"},"language":"DOTNET","opaque":1,"serializeTypeCurrentRPC":"JSON","version":373,"remark":"GET_ROUTEINTO_BY_TOPIC"} """, cmd.RawJson); var header = cmd.Header; Assert.NotNull(header); Assert.Equal((Int32)RequestCode.GET_ROUTEINTO_BY_TOPIC, header.Code); Assert.Equal(0, header.Flag); Assert.Equal(LanguageCode.DOTNET + "", header.Language); Assert.Equal(SerializeType.JSON + "", header.SerializeTypeCurrentRPC); Assert.Equal(MQVersion.V4_8_0, header.Version); //Assert.Null(header.Remark); Assert.Equal("GET_ROUTEINTO_BY_TOPIC", header.Remark); var ext = header.GetExtFields(); Assert.Single(ext); Assert.Equal("TeZ_Test_Lng", ext["topic"]); var pk = cmd.Payload; Assert.Null(pk); } [Fact] public void DecodeRouteInfo_v520_Dotnet() { var data = """ 00 00 01 68 00 00 00 5f 7b 22 63 6f 64 65 22 3a 30 2c 22 66 6c 61 67 22 3a 31 2c 22 6c 61 6e 67 75 61 67 65 22 3a 22 4a 41 56 41 22 2c 22 6f 70 61 71 75 65 22 3a 31 2c 22 73 65 72 69 61 6c 69 7a 65 54 79 70 65 43 75 72 72 65 6e 74 52 50 43 22 3a 22 4a 53 4f 4e 22 2c 22 76 65 72 73 69 6f 6e 22 3a 34 35 33 7d 7b 22 62 72 6f 6b 65 72 44 61 74 61 73 22 3a 5b 7b 22 62 72 6f 6b 65 72 41 64 64 72 73 22 3a 7b 30 3a 22 31 30 2e 32 2e 33 2e 31 31 37 3a 31 30 39 31 31 22 7d 2c 22 62 72 6f 6b 65 72 4e 61 6d 65 22 3a 22 62 72 6f 6b 65 72 2d 61 22 2c 22 63 6c 75 73 74 65 72 22 3a 22 44 65 66 61 75 6c 74 43 6c 75 73 74 65 72 22 2c 22 65 6e 61 62 6c 65 41 63 74 69 6e 67 4d 61 73 74 65 72 22 3a 66 61 6c 73 65 7d 5d 2c 22 66 69 6c 74 65 72 53 65 72 76 65 72 54 61 62 6c 65 22 3a 7b 7d 2c 22 71 75 65 75 65 44 61 74 61 73 22 3a 5b 7b 22 62 72 6f 6b 65 72 4e 61 6d 65 22 3a 22 62 72 6f 6b 65 72 2d 61 22 2c 22 70 65 72 6d 22 3a 36 2c 22 72 65 61 64 51 75 65 75 65 4e 75 6d 73 22 3a 38 2c 22 74 6f 70 69 63 53 79 73 46 6c 61 67 22 3a 30 2c 22 77 72 69 74 65 51 75 65 75 65 4e 75 6d 73 22 3a 38 7d 5d 7d """; var ms = new MemoryStream(data.ToHex()); var cmd = new Command(); var rs = cmd.Read(ms); Assert.True(rs); Assert.True(cmd.Reply); Assert.Equal(""" {"code":0,"flag":1,"language":"JAVA","opaque":1,"serializeTypeCurrentRPC":"JSON","version":453} """, cmd.RawJson); var header = cmd.Header; Assert.NotNull(header); Assert.Equal(0, header.Code); Assert.Equal(1, header.Flag); Assert.Equal(LanguageCode.JAVA + "", header.Language); Assert.Equal(SerializeType.JSON + "", header.SerializeTypeCurrentRPC); Assert.Equal(MQVersion.V5_2_0, header.Version); Assert.Null(header.Remark); var ext = header.GetExtFields(); Assert.Empty(ext); var pk = cmd.Payload; Assert.NotNull(pk); var target = """ {"brokerDatas":[{"brokerAddrs":{0:"10.2.3.117:10911"},"brokerName":"broker-a","cluster":"DefaultCluster","enableActingMaster":false}],"filterServerTable":{},"queueDatas":[{"brokerName":"broker-a","perm":6,"readQueueNums":8,"topicSysFlag":0,"writeQueueNums":8}]} """; var json = pk.ToStr(); Assert.Equal(target, json); var dic = cmd.ReadBodyAsJson(); Assert.True(dic.ContainsKey("brokerDatas")); Assert.True(dic.ContainsKey("queueDatas")); } [Fact] public void SendMessageV2_v520_Java() { var data = """ 00 00 04 09 00 00 01 81 7b 22 63 6f 64 65 22 3a 33 31 30 2c 22 65 78 74 46 69 65 6c 64 73 22 3a 7b 22 61 22 3a 22 52 30 31 5f 70 72 6f 64 75 63 65 72 5f 31 32 33 22 2c 22 62 22 3a 22 54 65 5a 5f 54 65 73 74 5f 4c 6e 67 22 2c 22 63 22 3a 22 54 42 57 31 30 32 22 2c 22 64 22 3a 22 34 22 2c 22 65 22 3a 22 30 22 2c 22 66 22 3a 22 30 22 2c 22 67 22 3a 22 31 37 34 39 38 38 33 37 37 38 36 38 36 22 2c 22 68 22 3a 22 30 22 2c 22 69 22 3a 22 55 4e 49 51 5f 4b 45 59 5c 75 30 30 30 31 32 34 30 30 44 44 30 32 31 30 30 38 30 30 31 35 32 42 32 42 37 44 30 34 31 39 44 36 44 44 35 45 39 45 46 43 31 38 42 34 41 41 43 32 34 36 32 31 32 41 37 44 30 30 30 30 5c 75 30 30 30 32 57 41 49 54 5c 75 30 30 30 31 74 72 75 65 5c 75 30 30 30 32 54 41 47 53 5c 75 30 30 30 31 2a 5c 75 30 30 30 32 22 2c 22 6a 22 3a 22 30 22 2c 22 6b 22 3a 22 66 61 6c 73 65 22 2c 22 6d 22 3a 22 66 61 6c 73 65 22 2c 22 6e 22 3a 22 62 72 6f 6b 65 72 2d 61 22 7d 2c 22 66 6c 61 67 22 3a 30 2c 22 6c 61 6e 67 75 61 67 65 22 3a 22 4a 41 56 41 22 2c 22 6f 70 61 71 75 65 22 3a 32 2c 22 73 65 72 69 61 6c 69 7a 65 54 79 70 65 43 75 72 72 65 6e 74 52 50 43 22 3a 22 4a 53 4f 4e 22 2c 22 76 65 72 73 69 6f 6e 22 3a 34 35 33 7d 5b 31 2e 30 2c 31 35 2e 30 2c 31 2e 30 31 33 32 35 2c 30 2e 35 2c 35 37 32 2e 35 2c 31 2e 30 31 33 32 35 2c 31 35 2e 30 2c 31 2e 30 35 30 37 30 38 31 37 32 2c 32 39 39 39 2e 39 34 32 37 31 35 2c 31 2e 30 32 35 31 38 35 37 34 37 2c 32 2e 31 31 35 37 35 37 34 39 37 2c 39 30 2e 30 2c 31 2e 30 35 30 37 30 38 31 37 32 2c 32 2e 31 31 35 37 35 37 34 39 37 2c 34 2e 32 36 30 33 39 33 30 34 31 2c 35 37 32 2e 35 2c 34 2e 32 36 30 33 39 33 30 34 31 2c 34 32 33 2e 30 2c 31 38 2e 31 39 2c 31 31 31 31 2e 30 2c 31 2e 30 2c 31 35 2e 30 2c 31 2e 30 31 33 32 35 2c 30 2e 35 2c 31 38 2e 36 30 38 32 35 38 31 39 2c 31 2e 30 31 33 32 35 2c 31 35 2e 30 2c 31 2e 30 35 30 37 30 38 31 37 32 2c 32 39 39 39 2e 39 34 32 37 31 35 2c 31 2e 30 32 35 31 38 35 37 34 37 2c 32 2e 31 31 35 37 35 37 34 39 37 2c 39 30 2e 30 2c 31 2e 30 35 30 37 30 38 31 37 32 2c 32 2e 31 31 35 37 35 37 34 39 37 2c 34 2e 32 36 30 33 39 33 30 34 31 2c 31 38 2e 36 30 38 32 35 38 31 39 2c 34 32 33 2e 30 2c 34 32 33 2e 30 2c 31 38 2e 31 39 2c 31 31 31 31 2e 30 2c 31 2e 30 2c 31 35 2e 30 2c 31 2e 30 31 33 32 35 2c 30 2e 35 2c 31 38 2e 36 30 38 32 35 38 31 39 2c 31 2e 30 31 33 32 35 2c 31 35 2e 30 2c 31 2e 30 35 30 37 30 38 31 37 32 2c 32 39 39 39 2e 39 34 32 37 31 35 2c 31 2e 30 32 35 31 38 35 37 34 37 2c 32 2e 31 31 35 37 35 37 34 39 37 2c 39 30 2e 30 2c 31 2e 30 35 30 37 30 38 31 37 32 2c 32 2e 31 31 35 37 35 37 34 39 37 2c 34 2e 32 36 30 33 39 33 30 34 31 2c 35 37 32 2e 35 2c 32 39 39 39 2e 39 34 32 37 31 35 2c 34 32 33 2e 30 2c 31 38 2e 31 39 2c 39 30 2e 30 2c 31 2e 30 2c 31 35 2e 30 2c 31 2e 30 31 33 32 35 2c 30 2e 35 2c 31 38 2e 36 30 38 32 35 38 31 39 2c 31 2e 30 31 33 32 35 2c 31 35 2e 30 2c 31 2e 30 35 30 37 30 38 31 37 32 2c 32 39 39 39 2e 39 34 32 37 31 35 2c 31 2e 30 32 35 31 38 35 37 34 37 2c 32 2e 31 31 35 37 35 37 34 39 37 2c 39 30 2e 30 2c 31 2e 30 35 30 37 30 38 31 37 32 2c 32 2e 31 31 35 37 35 37 34 39 37 2c 31 2e 30 32 35 31 38 35 37 34 37 5d """; var ms = new MemoryStream(data.ToHex()); var cmd = new Command(); var rs = cmd.Read(ms); Assert.True(rs); Assert.False(cmd.Reply); Assert.Equal(""" {"code":310,"extFields":{"a":"R01_producer_123","b":"TeZ_Test_Lng","c":"TBW102","d":"4","e":"0","f":"0","g":"1749883778686","h":"0","i":"UNIQ_KEY\u00012400DD02100800152B2B7D0419D6DD5E9EFC18B4AAC246212A7D0000\u0002WAIT\u0001true\u0002TAGS\u0001*\u0002","j":"0","k":"false","m":"false","n":"broker-a"},"flag":0,"language":"JAVA","opaque":2,"serializeTypeCurrentRPC":"JSON","version":453} """, cmd.RawJson); var header = cmd.Header; Assert.NotNull(header); Assert.Equal((Int32)RequestCode.SEND_MESSAGE_V2, header.Code); Assert.Equal(0, header.Flag); Assert.Equal(LanguageCode.JAVA + "", header.Language); Assert.Equal(SerializeType.JSON + "", header.SerializeTypeCurrentRPC); Assert.Equal(MQVersion.V5_2_0, header.Version); Assert.Null(header.Remark); var ext = header.GetExtFields(); Assert.Equal(13, ext.Count); Assert.Equal("R01_producer_123", ext["a"]); Assert.Equal("TeZ_Test_Lng", ext["b"]); Assert.Equal("TBW102", ext["c"]); Assert.Equal("UNIQ_KEY\u00012400DD02100800152B2B7D0419D6DD5E9EFC18B4AAC246212A7D0000\u0002WAIT\u0001true\u0002TAGS\u0001*\u0002", ext["i"]); var pk = cmd.Payload; Assert.NotNull(pk); var json = pk.ToStr(); Assert.NotEmpty(json); } [Fact] public void DecodeSendMessageV2_v520_Java() { var data = """ 00 00 01 36 00 00 01 32 7b 22 63 6f 64 65 22 3a 30 2c 22 65 78 74 46 69 65 6c 64 73 22 3a 7b 22 71 75 65 75 65 49 64 22 3a 22 30 22 2c 22 74 72 61 6e 73 61 63 74 69 6f 6e 49 64 22 3a 22 32 34 30 30 44 44 30 32 31 30 30 38 30 30 31 35 32 42 32 42 37 44 30 34 31 39 44 36 44 44 35 45 39 45 46 43 31 38 42 34 41 41 43 32 34 36 32 31 32 41 37 44 30 30 30 30 22 2c 22 6d 73 67 49 64 22 3a 22 30 41 30 32 30 33 37 35 30 30 30 30 32 41 39 46 30 30 30 30 30 30 30 30 30 31 45 43 31 38 46 43 22 2c 22 54 52 41 43 45 5f 4f 4e 22 3a 22 74 72 75 65 22 2c 22 4d 53 47 5f 52 45 47 49 4f 4e 22 3a 22 44 65 66 61 75 6c 74 52 65 67 69 6f 6e 22 2c 22 71 75 65 75 65 4f 66 66 73 65 74 22 3a 22 30 22 7d 2c 22 66 6c 61 67 22 3a 31 2c 22 6c 61 6e 67 75 61 67 65 22 3a 22 4a 41 56 41 22 2c 22 6f 70 61 71 75 65 22 3a 32 2c 22 73 65 72 69 61 6c 69 7a 65 54 79 70 65 43 75 72 72 65 6e 74 52 50 43 22 3a 22 4a 53 4f 4e 22 2c 22 76 65 72 73 69 6f 6e 22 3a 34 35 33 7d """; var ms = new MemoryStream(data.ToHex()); var cmd = new Command(); var rs = cmd.Read(ms); Assert.True(rs); Assert.True(cmd.Reply); Assert.Equal(""" {"code":0,"extFields":{"queueId":"0","transactionId":"2400DD02100800152B2B7D0419D6DD5E9EFC18B4AAC246212A7D0000","msgId":"0A02037500002A9F0000000001EC18FC","TRACE_ON":"true","MSG_REGION":"DefaultRegion","queueOffset":"0"},"flag":1,"language":"JAVA","opaque":2,"serializeTypeCurrentRPC":"JSON","version":453} """, cmd.RawJson); var header = cmd.Header; Assert.NotNull(header); Assert.Equal(0, header.Code); Assert.Equal(1, header.Flag); Assert.Equal(LanguageCode.JAVA + "", header.Language); Assert.Equal(SerializeType.JSON + "", header.SerializeTypeCurrentRPC); Assert.Equal(MQVersion.V5_2_0, header.Version); Assert.Null(header.Remark); var ext = header.GetExtFields(); Assert.Equal(6, ext.Count); Assert.Equal("0", ext["queueId"]); Assert.Equal("2400DD02100800152B2B7D0419D6DD5E9EFC18B4AAC246212A7D0000", ext["transactionId"]); Assert.Equal("0A02037500002A9F0000000001EC18FC", ext["msgId"]); Assert.Equal("true", ext["TRACE_ON"]); Assert.Equal("DefaultRegion", ext["MSG_REGION"]); Assert.Equal("0", ext["queueOffset"]); var result = new SendResult(); result.Read(ext); Assert.Equal("2400DD02100800152B2B7D0419D6DD5E9EFC18B4AAC246212A7D0000", result.TransactionId); Assert.Equal("0A02037500002A9F0000000001EC18FC", result.MsgId); Assert.Equal("DefaultRegion", result.RegionId); Assert.Equal(0, result.QueueOffset); var pk = cmd.Payload; Assert.Null(pk); } [Fact] public void SendMessageV2_v520_Dotnet() { var data = """ 00 00 01 31 00 00 01 1a 7b 22 63 6f 64 65 22 3a 33 31 30 2c 22 65 78 74 46 69 65 6c 64 73 22 3a 7b 22 61 22 3a 22 52 30 31 5f 70 72 6f 64 75 63 65 72 5f 31 32 33 22 2c 22 62 22 3a 22 54 65 5a 5f 54 65 73 74 5f 4c 6e 67 22 2c 22 63 22 3a 22 54 42 57 31 30 32 22 2c 22 64 22 3a 22 34 22 2c 22 65 22 3a 22 30 22 2c 22 66 22 3a 22 30 22 2c 22 67 22 3a 22 31 37 34 39 38 32 36 35 32 39 37 31 36 22 2c 22 68 22 3a 22 30 22 2c 22 69 22 3a 22 54 41 47 53 01 2a 02 57 41 49 54 01 54 72 75 65 02 22 2c 22 6a 22 3a 22 30 22 2c 22 6b 22 3a 22 46 61 6c 73 65 22 7d 2c 22 6c 61 6e 67 75 61 67 65 22 3a 22 44 4f 54 4e 45 54 22 2c 22 6f 70 61 71 75 65 22 3a 31 2c 22 73 65 72 69 61 6c 69 7a 65 54 79 70 65 43 75 72 72 65 6e 74 52 50 43 22 3a 22 4a 53 4f 4e 22 2c 22 76 65 72 73 69 6f 6e 22 3a 33 37 33 2c 22 72 65 6d 61 72 6b 22 3a 22 53 45 4e 44 5f 4d 45 53 53 41 47 45 5f 56 32 22 7d 32 30 32 35 2d 30 36 2d 31 33 20 32 32 3a 35 34 3a 31 32 """; var ms = new MemoryStream(data.ToHex()); var cmd = new Command(); var rs = cmd.Read(ms); Assert.True(rs); Assert.False(cmd.Reply); Assert.Equal(""" {"code":310,"extFields":{"a":"R01_producer_123","b":"TeZ_Test_Lng","c":"TBW102","d":"4","e":"0","f":"0","g":"1749826529716","h":"0","i":"TAGS*WAITTrue","j":"0","k":"False"},"language":"DOTNET","opaque":1,"serializeTypeCurrentRPC":"JSON","version":373,"remark":"SEND_MESSAGE_V2"} """, cmd.RawJson); var header = cmd.Header; Assert.NotNull(header); Assert.Equal((Int32)RequestCode.SEND_MESSAGE_V2, header.Code); Assert.Equal(0, header.Flag); Assert.Equal(LanguageCode.DOTNET + "", header.Language); Assert.Equal(SerializeType.JSON + "", header.SerializeTypeCurrentRPC); Assert.Equal(MQVersion.V4_8_0, header.Version); //Assert.Null(header.Remark); Assert.Equal("SEND_MESSAGE_V2", header.Remark); var ext = header.GetExtFields(); //Assert.Equal(13, ext.Count); Assert.Equal("R01_producer_123", ext["a"]); Assert.Equal("TeZ_Test_Lng", ext["b"]); Assert.Equal("TBW102", ext["c"]); var pk = cmd.Payload; Assert.NotNull(pk); var json = pk.ToStr(); Assert.Equal("2025-06-13 22:54:12", json); } [Fact] public void SendMessageV2_v520_Dotnet2() { var data = """ 00 00 01 45 00 00 01 2e 7b 22 43 6f 64 65 22 3a 33 31 30 2c 22 45 78 74 46 69 65 6c 64 73 22 3a 7b 22 61 22 3a 22 52 30 31 5f 70 72 6f 64 75 63 65 72 5f 31 32 33 22 2c 22 62 22 3a 22 54 65 5a 5f 54 65 73 74 5f 4c 6e 67 22 2c 22 63 22 3a 22 54 42 57 31 30 32 22 2c 22 64 22 3a 22 34 22 2c 22 65 22 3a 22 30 22 2c 22 66 22 3a 22 30 22 2c 22 67 22 3a 22 31 37 34 39 39 32 33 33 35 32 34 38 36 22 2c 22 68 22 3a 22 30 22 2c 22 69 22 3a 22 54 41 47 53 5c 75 30 30 30 31 2a 5c 75 30 30 30 32 57 41 49 54 5c 75 30 30 30 31 54 72 75 65 5c 75 30 30 30 32 22 2c 22 6a 22 3a 22 30 22 2c 22 6b 22 3a 22 46 61 6c 73 65 22 7d 2c 22 4c 61 6e 67 75 61 67 65 22 3a 22 44 4f 54 4e 45 54 22 2c 22 4f 70 61 71 75 65 22 3a 31 2c 22 53 65 72 69 61 6c 69 7a 65 54 79 70 65 43 75 72 72 65 6e 74 52 50 43 22 3a 22 4a 53 4f 4e 22 2c 22 56 65 72 73 69 6f 6e 22 3a 34 35 33 2c 22 52 65 6d 61 72 6b 22 3a 22 53 45 4e 44 5f 4d 45 53 53 41 47 45 5f 56 32 22 7d 32 30 32 35 2d 30 36 2d 31 35 20 30 31 3a 34 39 3a 30 39 """; var ms = new MemoryStream(data.ToHex()); var cmd = new Command(); var rs = cmd.Read(ms); Assert.True(rs); Assert.False(cmd.Reply); Assert.Equal(""" {"Code":310,"ExtFields":{"a":"R01_producer_123","b":"TeZ_Test_Lng","c":"TBW102","d":"4","e":"0","f":"0","g":"1749923352486","h":"0","i":"TAGS\u0001*\u0002WAIT\u0001True\u0002","j":"0","k":"False"},"Language":"DOTNET","Opaque":1,"SerializeTypeCurrentRPC":"JSON","Version":453,"Remark":"SEND_MESSAGE_V2"} """, cmd.RawJson); var header = cmd.Header; Assert.NotNull(header); Assert.Equal((Int32)RequestCode.SEND_MESSAGE_V2, header.Code); Assert.Equal(0, header.Flag); Assert.Equal(LanguageCode.DOTNET + "", header.Language); Assert.Equal(SerializeType.JSON + "", header.SerializeTypeCurrentRPC); Assert.Equal(MQVersion.V5_2_0, header.Version); //Assert.Null(header.Remark); Assert.Equal("SEND_MESSAGE_V2", header.Remark); var ext = header.GetExtFields(); //Assert.Equal(13, ext.Count); Assert.Equal("R01_producer_123", ext["a"]); Assert.Equal("TeZ_Test_Lng", ext["b"]); Assert.Equal("TBW102", ext["c"]); var pk = cmd.Payload; Assert.NotNull(pk); var json = pk.ToStr(); Assert.Equal("2025-06-15 01:49:09", json); } [Fact] public void HeartBeat_v520_Java() { var data = """ 00 00 01 3c 00 00 00 6f 7b 22 63 6f 64 65 22 3a 33 34 2c 22 65 78 74 46 69 65 6c 64 73 22 3a 7b 7d 2c 22 66 6c 61 67 22 3a 30 2c 22 6c 61 6e 67 75 61 67 65 22 3a 22 4a 41 56 41 22 2c 22 6f 70 61 71 75 65 22 3a 33 2c 22 73 65 72 69 61 6c 69 7a 65 54 79 70 65 43 75 72 72 65 6e 74 52 50 43 22 3a 22 4a 53 4f 4e 22 2c 22 76 65 72 73 69 6f 6e 22 3a 34 35 33 7d 7b 22 63 6c 69 65 6e 74 49 44 22 3a 22 31 30 2e 31 2e 35 2e 39 40 34 30 37 30 30 23 39 35 35 32 32 30 30 38 37 38 32 32 35 30 30 22 2c 22 63 6f 6e 73 75 6d 65 72 44 61 74 61 53 65 74 22 3a 5b 5d 2c 22 68 65 61 72 74 62 65 61 74 46 69 6e 67 65 72 70 72 69 6e 74 22 3a 30 2c 22 70 72 6f 64 75 63 65 72 44 61 74 61 53 65 74 22 3a 5b 7b 22 67 72 6f 75 70 4e 61 6d 65 22 3a 22 43 4c 49 45 4e 54 5f 49 4e 4e 45 52 5f 50 52 4f 44 55 43 45 52 22 7d 2c 7b 22 67 72 6f 75 70 4e 61 6d 65 22 3a 22 52 30 31 5f 70 72 6f 64 75 63 65 72 5f 31 32 33 22 7d 5d 2c 22 77 69 74 68 6f 75 74 53 75 62 22 3a 66 61 6c 73 65 7d """; var ms = new MemoryStream(data.ToHex()); var cmd = new Command(); var rs = cmd.Read(ms); Assert.True(rs); Assert.False(cmd.Reply); Assert.Equal(""" {"code":34,"extFields":{},"flag":0,"language":"JAVA","opaque":3,"serializeTypeCurrentRPC":"JSON","version":453} """, cmd.RawJson); var header = cmd.Header; Assert.NotNull(header); Assert.Equal((Int32)RequestCode.HEART_BEAT, header.Code); Assert.Equal(0, header.Flag); Assert.Equal(LanguageCode.JAVA + "", header.Language); Assert.Equal(SerializeType.JSON + "", header.SerializeTypeCurrentRPC); Assert.Equal(MQVersion.V5_2_0, header.Version); Assert.Null(header.Remark); var ext = header.GetExtFields(); Assert.Empty(ext); var pk = cmd.Payload; Assert.NotNull(pk); var json = pk.ToStr(); Assert.NotEmpty(json); Assert.Equal(""" {"clientID":"10.1.5.9@40700#955220087822500","consumerDataSet":[],"heartbeatFingerprint":0,"producerDataSet":[{"groupName":"CLIENT_INNER_PRODUCER"},{"groupName":"R01_producer_123"}],"withoutSub":false} """, json); } [Fact] public void DecodeHeartBeat_v520_Java() { var data = """ 00 00 00 aa 00 00 00 a6 7b 22 63 6f 64 65 22 3a 30 2c 22 65 78 74 46 69 65 6c 64 73 22 3a 7b 22 49 53 5f 53 55 50 50 4f 52 54 5f 48 45 41 52 54 5f 42 45 41 54 5f 56 32 22 3a 22 74 72 75 65 22 2c 22 49 53 5f 53 55 42 5f 43 48 41 4e 47 45 22 3a 22 74 72 75 65 22 7d 2c 22 66 6c 61 67 22 3a 31 2c 22 6c 61 6e 67 75 61 67 65 22 3a 22 4a 41 56 41 22 2c 22 6f 70 61 71 75 65 22 3a 33 2c 22 73 65 72 69 61 6c 69 7a 65 54 79 70 65 43 75 72 72 65 6e 74 52 50 43 22 3a 22 4a 53 4f 4e 22 2c 22 76 65 72 73 69 6f 6e 22 3a 34 35 33 7d """; var ms = new MemoryStream(data.ToHex()); var cmd = new Command(); var rs = cmd.Read(ms); Assert.True(rs); Assert.True(cmd.Reply); Assert.Equal(""" {"code":0,"extFields":{"IS_SUPPORT_HEART_BEAT_V2":"true","IS_SUB_CHANGE":"true"},"flag":1,"language":"JAVA","opaque":3,"serializeTypeCurrentRPC":"JSON","version":453} """, cmd.RawJson); var header = cmd.Header; Assert.NotNull(header); Assert.Equal(0, header.Code); Assert.Equal(1, header.Flag); Assert.Equal(LanguageCode.JAVA + "", header.Language); Assert.Equal(SerializeType.JSON + "", header.SerializeTypeCurrentRPC); Assert.Equal(MQVersion.V5_2_0, header.Version); Assert.Null(header.Remark); var ext = header.GetExtFields(); Assert.Equal(2, ext.Count); Assert.Equal("true", ext["IS_SUPPORT_HEART_BEAT_V2"]); Assert.Equal("true", ext["IS_SUB_CHANGE"]); var pk = cmd.Payload; Assert.Null(pk); } [Fact] public void CreateHeader_TransactionMessage_SetsPreparedFlag() { var producer = new Producer { Topic = "nx_test", Group = "nx_group" }; var message = new Message(); message.SetBody("hello"); message.Properties["TRAN_MSG"] = "true"; message.Properties["PGROUP"] = "nx_group"; var method = typeof(Producer).GetMethod("CreateHeader", BindingFlags.Instance | BindingFlags.NonPublic); Assert.NotNull(method); var header = (SendMessageRequestHeader)method.Invoke(producer, new Object[] { message }); Assert.Equal((Int32)TransactionState.Prepared, header.SysFlag); } [Fact] public void EndTransactionRequestHeader_ToProperties_UsesCamelCase() { var header = new EndTransactionRequestHeader { ProducerGroup = "nx_group", TranStateTableOffset = 11, CommitLogOffset = 22, CommitOrRollback = (Int32)TransactionState.Commit, FromTransactionCheck = false, MsgId = "msg_1", TransactionId = "tx_1", }; var ext = header.GetProperties(); Assert.Equal("nx_group", ext["producerGroup"]); Assert.Equal("11", ext["tranStateTableOffset"]?.ToString()); Assert.Equal("22", ext["commitLogOffset"]?.ToString()); Assert.Equal("8", ext["commitOrRollback"]?.ToString()); Assert.Equal("False", ext["fromTransactionCheck"]?.ToString()); Assert.Equal("msg_1", ext["msgId"]); Assert.Equal("tx_1", ext["transactionId"]); } } ================================================ FILE: XUnitTestRocketMQ/CompressionTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// 消息压缩功能测试 public class CompressionTests { [Fact] [DisplayName("压缩阈值_默认值为4096")] public void CompressOverBytes_DefaultValue() { using var producer = new Producer(); Assert.Equal(4096, producer.CompressOverBytes); } [Fact] [DisplayName("压缩阈值_可自定义设置")] public void CompressOverBytes_CanBeSet() { using var producer = new Producer { CompressOverBytes = 1024, }; Assert.Equal(1024, producer.CompressOverBytes); } [Fact] [DisplayName("压缩阈值_设为0禁用压缩")] public void CompressOverBytes_ZeroDisablesCompression() { using var producer = new Producer { CompressOverBytes = 0, }; Assert.Equal(0, producer.CompressOverBytes); } } ================================================ FILE: XUnitTestRocketMQ/ConcurrentConsumeTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ; using Xunit; namespace XUnitTestRocketMQ; /// 消费限流测试 public class ConcurrentConsumeTests { [Fact] [DisplayName("MaxConcurrentConsume_默认为0")] public void MaxConcurrentConsume_DefaultZero() { using var consumer = new Consumer(); Assert.Equal(0, consumer.MaxConcurrentConsume); } [Fact] [DisplayName("MaxConcurrentConsume_可设置正整数")] public void MaxConcurrentConsume_CanSetPositive() { using var consumer = new Consumer { MaxConcurrentConsume = 10 }; Assert.Equal(10, consumer.MaxConcurrentConsume); } [Fact] [DisplayName("MaxConcurrentConsume_设为1时串行消费")] public void MaxConcurrentConsume_Serial() { using var consumer = new Consumer { MaxConcurrentConsume = 1 }; Assert.Equal(1, consumer.MaxConcurrentConsume); } } ================================================ FILE: XUnitTestRocketMQ/ConsumeStatsTests.cs ================================================ using System; using System.ComponentModel; using NewLife.Log; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// 消费统计和过滤服务器测试 public class ConsumeStatsTests { [Fact(Skip = "需要RocketMQ服务器")] [DisplayName("GetConsumeStats_获取消费统计")] public void GetConsumeStats_Test() { var set = BasicTest.GetConfig(); using var mq = new Producer { Topic = "nx_test", NameServerAddress = set.NameServer, Log = XTrace.Log, }; mq.Start(); var stats = mq.GetConsumeStats("CID_nx_test"); // 不做严格断言,仅验证不抛出异常 } [Fact(Skip = "需要RocketMQ服务器")] [DisplayName("GetTopicStatsInfo_获取Topic统计")] public void GetTopicStatsInfo_Test() { var set = BasicTest.GetConfig(); using var mq = new Producer { Topic = "nx_test", NameServerAddress = set.NameServer, Log = XTrace.Log, }; mq.Start(); var stats = mq.GetTopicStatsInfo("nx_test"); // 不做严格断言,仅验证不抛出异常 } [Fact(Skip = "需要RocketMQ服务器")] [DisplayName("RegisterFilterServer_注册过滤服务器")] public void RegisterFilterServer_Test() { var set = BasicTest.GetConfig(); using var mq = new Producer { Topic = "nx_test", NameServerAddress = set.NameServer, Log = XTrace.Log, }; mq.Start(); var count = mq.RegisterFilterServer("127.0.0.1:9999"); Assert.True(count >= 0); } [Fact] [DisplayName("RegisterFilterServer_空地址抛出异常")] public void RegisterFilterServer_EmptyAddress_ThrowsException() { using var mq = new Producer(); Assert.Throws(() => mq.RegisterFilterServer(null)); Assert.Throws(() => mq.RegisterFilterServer("")); } [Fact] [DisplayName("RequestCode包含过滤服务器和统计相关码")] public void RequestCode_ContainsFilterAndStatsCodes() { Assert.Equal(301, (Int32)RequestCode.REGISTER_FILTER_SERVER); Assert.Equal(208, (Int32)RequestCode.GET_CONSUME_STATS); Assert.Equal(202, (Int32)RequestCode.GET_TOPIC_STATS_INFO); } } ================================================ FILE: XUnitTestRocketMQ/ConsumerStatesModelTests.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using NewLife.RocketMQ.Protocol.ConsumerStates; using Xunit; namespace XUnitTestRocketMQ; /// 消费者状态模型测试 public class ConsumerStatesModelTests { #region ConsumerStatesModel [Fact] [DisplayName("ConsumerStatesModel_默认值")] public void ConsumerStatesModel_Defaults() { var model = new ConsumerStatesModel(); Assert.Equal(0, model.ConsumeTps); Assert.Null(model.OffsetTable); } [Fact] [DisplayName("ConsumerStatesModel_设置属性")] public void ConsumerStatesModel_SetProperties() { var mqModel = new MessageQueueModel { BrokerName = "broker-a", QueueId = 0 }; var model = new ConsumerStatesModel { ConsumeTps = 1500.5, OffsetTable = new Dictionary { [mqModel] = new OffsetWrapperModel { BrokerOffset = 100, ConsumerOffset = 90 } } }; Assert.Equal(1500.5, model.ConsumeTps); Assert.NotNull(model.OffsetTable); Assert.Single(model.OffsetTable); Assert.Equal(100, model.OffsetTable[mqModel].BrokerOffset); } #endregion #region MessageQueueModel [Fact] [DisplayName("MessageQueueModel_默认值")] public void MessageQueueModel_Defaults() { var model = new MessageQueueModel(); Assert.Null(model.BrokerName); Assert.Equal(0, model.QueueId); Assert.Null(model.Topic); } [Fact] [DisplayName("MessageQueueModel_设置属性")] public void MessageQueueModel_SetProperties() { var model = new MessageQueueModel { BrokerName = "broker-a", QueueId = 3, Topic = "test_topic", }; Assert.Equal("broker-a", model.BrokerName); Assert.Equal(3, model.QueueId); Assert.Equal("test_topic", model.Topic); } #endregion #region OffsetWrapperModel [Fact] [DisplayName("OffsetWrapperModel_默认值")] public void OffsetWrapperModel_Defaults() { var model = new OffsetWrapperModel(); Assert.Equal(0, model.BrokerOffset); Assert.Equal(0, model.ConsumerOffset); Assert.Equal(0, model.LastTimestamp); Assert.Equal(0, model.PullOffset); } [Fact] [DisplayName("OffsetWrapperModel_设置所有属性")] public void OffsetWrapperModel_SetProperties() { var model = new OffsetWrapperModel { BrokerOffset = 1000, ConsumerOffset = 900, LastTimestamp = 1234567890L, PullOffset = 950, }; Assert.Equal(1000, model.BrokerOffset); Assert.Equal(900, model.ConsumerOffset); Assert.Equal(1234567890L, model.LastTimestamp); Assert.Equal(950, model.PullOffset); } [Fact] [DisplayName("OffsetWrapperModel_偏移差计算")] public void OffsetWrapperModel_OffsetDifference() { var model = new OffsetWrapperModel { BrokerOffset = 1000, ConsumerOffset = 800, }; var diff = model.BrokerOffset - model.ConsumerOffset; Assert.Equal(200, diff); } #endregion } ================================================ FILE: XUnitTestRocketMQ/ConsumerTests.cs ================================================ using NewLife; using NewLife.Log; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using System; using System.Linq; using System.Threading; using Xunit; namespace XUnitTestRocketMQ; public class ConsumerTests { private static Consumer _consumer; [Fact(Skip = "需要RocketMQ服务器支持")] public static void ConsumeTest() { var set = BasicTest.GetConfig(); var consumer = new Consumer { Topic = "nx_test", Group = "test", NameServerAddress = set.NameServer, FromLastOffset = true, BatchSize = 20, Log = XTrace.Log, }; consumer.OnConsume = OnConsume; consumer.Start(); _consumer = consumer; Thread.Sleep(3000); //foreach (var item in consumer.Clients) //{ // var rs = item.GetRuntimeInfo(); // Console.WriteLine("{0}\t{1}", item.Name, rs["brokerVersionDesc"]); //} } private static Boolean OnConsume(MessageQueue q, MessageExt[] ms) { Console.WriteLine("[{0}@{1}]收到消息[{2}]", q.BrokerName, q.QueueId, ms.Length); foreach (var item in ms.ToList()) { Console.WriteLine($"消息:主键【{item.Keys}】,产生时间【{item.BornTimestamp.ToDateTime()}】,内容【{item.Body.ToStr()}】"); } return true; } } ================================================ FILE: XUnitTestRocketMQ/HeaderTests.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// Header协议头测试 public class HeaderTests { #region 默认值 [Fact] [DisplayName("Header_默认Language为CPP")] public void Header_DefaultLanguage() { var header = new Header(); Assert.Equal("CPP", header.Language); } [Fact] [DisplayName("Header_默认SerializeType为JSON")] public void Header_DefaultSerializeType() { var header = new Header(); Assert.Equal("JSON", header.SerializeTypeCurrentRPC); } [Fact] [DisplayName("Header_默认Version为V4_8_0")] public void Header_DefaultVersion() { var header = new Header(); Assert.Equal(MQVersion.V4_8_0, header.Version); } [Fact] [DisplayName("Header_默认ExtFields为null")] public void Header_DefaultExtFieldsNull() { var header = new Header(); Assert.Null(header.ExtFields); } #endregion #region GetExtFields [Fact] [DisplayName("GetExtFields_首次调用创建空字典")] public void GetExtFields_CreatesNewDictionary() { var header = new Header(); var fields = header.GetExtFields(); Assert.NotNull(fields); Assert.Empty(fields); } [Fact] [DisplayName("GetExtFields_多次调用返回同一实例")] public void GetExtFields_ReturnsSameInstance() { var header = new Header(); var fields1 = header.GetExtFields(); var fields2 = header.GetExtFields(); Assert.Same(fields1, fields2); } [Fact] [DisplayName("GetExtFields_已有ExtFields时返回原字典")] public void GetExtFields_ExistingFields_ReturnsSame() { var existing = new Dictionary { ["key"] = "value" }; var header = new Header { ExtFields = existing }; var fields = header.GetExtFields(); Assert.Same(existing, fields); Assert.Equal("value", fields["key"]); } [Fact] [DisplayName("GetExtFields_大小写不敏感")] public void GetExtFields_CaseInsensitive() { var header = new Header(); var fields = header.GetExtFields(); fields["TestKey"] = "hello"; Assert.Equal("hello", fields["testkey"]); Assert.Equal("hello", fields["TESTKEY"]); } #endregion #region CreateException [Fact] [DisplayName("CreateException_基本异常创建")] public void CreateException_BasicCreation() { var header = new Header { Code = (Int32)ResponseCode.SYSTEM_ERROR, Remark = "Something went wrong" }; var ex = header.CreateException(); Assert.NotNull(ex); Assert.Equal(ResponseCode.SYSTEM_ERROR, ex.Code); Assert.Contains("Something went wrong", ex.Message); } [Fact] [DisplayName("CreateException_解析Exception后缀")] public void CreateException_ParsesExceptionSuffix() { var header = new Header { Code = (Int32)ResponseCode.TOPIC_NOT_EXIST, Remark = "org.apache.rocketmq.client.exception.MQClientException: No topic route info" }; var ex = header.CreateException(); Assert.Equal(ResponseCode.TOPIC_NOT_EXIST, ex.Code); Assert.Contains("No topic route info", ex.Message); } [Fact] [DisplayName("CreateException_解析逗号后文本")] public void CreateException_ParsesCommaDelimited() { var header = new Header { Code = (Int32)ResponseCode.SYSTEM_ERROR, Remark = "SomeException: Error message, extra info" }; var ex = header.CreateException(); Assert.Contains("Error message", ex.Message); Assert.DoesNotContain("extra info", ex.Message); } [Fact] [DisplayName("CreateException_空Remark不抛异常")] public void CreateException_EmptyRemark_NoThrow() { var header = new Header { Code = (Int32)ResponseCode.SUCCESS, Remark = null }; var ex = header.CreateException(); Assert.NotNull(ex); Assert.Equal(ResponseCode.SUCCESS, ex.Code); } [Fact] [DisplayName("CreateException_无Exception关键字保持原文")] public void CreateException_NoExceptionKeyword_KeepsOriginal() { var header = new Header { Code = (Int32)ResponseCode.NO_PERMISSION, Remark = "Access denied for this topic" }; var ex = header.CreateException(); Assert.Contains("Access denied for this topic", ex.Message); } #endregion } ================================================ FILE: XUnitTestRocketMQ/IPv6Tests.cs ================================================ using System; using System.ComponentModel; using System.IO; using System.Net; using NewLife.Data; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// IPv6消息解码测试 public class IPv6Tests { /// 写入大端序Int32 private static void WriteBigEndianInt32(MemoryStream ms, Int32 value) { ms.WriteByte((Byte)(value >> 24)); ms.WriteByte((Byte)(value >> 16)); ms.WriteByte((Byte)(value >> 8)); ms.WriteByte((Byte)value); } /// 写入大端序Int64 private static void WriteBigEndianInt64(MemoryStream ms, Int64 value) { WriteBigEndianInt32(ms, (Int32)(value >> 32)); WriteBigEndianInt32(ms, (Int32)value); } /// 写入大端序Int16 private static void WriteBigEndianInt16(MemoryStream ms, Int16 value) { ms.WriteByte((Byte)(value >> 8)); ms.WriteByte((Byte)value); } /// 构造消息的二进制数据 private static Byte[] BuildMessageBinary(Boolean ipv6) { var ms = new MemoryStream(); var ipBytes = ipv6 ? IPAddress.IPv6Loopback.GetAddressBytes() : new Byte[] { 127, 0, 0, 1 }; // SysFlag: 第2位(0x04)标识IPv6 var sysFlag = ipv6 ? 4 : 0; var body = "hello"u8.ToArray(); var topic = "test_topic"u8.ToArray(); var props = ""u8.ToArray(); // 计算StoreSize var ipSize = ipv6 ? 16 : 4; // StoreSize(4) + MagicCode(4) + BodyCRC(4) + QueueId(4) + Flag(4) + // QueueOffset(8) + CommitLogOffset(8) + SysFlag(4) + // BornTimestamp(8) + BornIP(ipSize) + BornPort(4) + // StoreTimestamp(8) + StoreIP(ipSize) + StorePort(4) + // ReconsumeTimes(4) + PreparedTransactionOffset(8) + // BodyLen(4) + body + TopicLen(1) + topic + PropsLen(2) + props var storeSize = 4 + 4 + 4 + 4 + 4 + 8 + 8 + 4 + 8 + ipSize + 4 + 8 + ipSize + 4 + 4 + 8 + 4 + body.Length + 1 + topic.Length + 2 + props.Length; WriteBigEndianInt32(ms, storeSize); // StoreSize WriteBigEndianInt32(ms, 0); // MagicCode WriteBigEndianInt32(ms, 0); // BodyCRC WriteBigEndianInt32(ms, 1); // QueueId WriteBigEndianInt32(ms, 0); // Flag WriteBigEndianInt64(ms, 100L); // QueueOffset WriteBigEndianInt64(ms, 200L); // CommitLogOffset WriteBigEndianInt32(ms, sysFlag); // SysFlag WriteBigEndianInt64(ms, 1000L); // BornTimestamp ms.Write(ipBytes, 0, ipBytes.Length);// BornHost IP WriteBigEndianInt32(ms, 9876); // BornHost Port WriteBigEndianInt64(ms, 2000L); // StoreTimestamp ms.Write(ipBytes, 0, ipBytes.Length);// StoreHost IP WriteBigEndianInt32(ms, 10911); // StoreHost Port WriteBigEndianInt32(ms, 0); // ReconsumeTimes WriteBigEndianInt64(ms, 0L); // PreparedTransactionOffset WriteBigEndianInt32(ms, body.Length); // BodyLength ms.Write(body, 0, body.Length); ms.WriteByte((Byte)topic.Length); // Topic length (1 byte) ms.Write(topic, 0, topic.Length); WriteBigEndianInt16(ms, (Int16)props.Length); // Properties length (2 bytes) if (props.Length > 0) ms.Write(props, 0, props.Length); return ms.ToArray(); } [Fact] [DisplayName("IPv4消息正确解码")] public void ReadMessage_IPv4() { var data = BuildMessageBinary(false); var pk = new ArrayPacket(data); var msgs = MessageExt.ReadAll(pk); Assert.Single(msgs); var msg = msgs[0]; Assert.Equal(1, msg.QueueId); Assert.Equal(100, msg.QueueOffset); Assert.Equal(200, msg.CommitLogOffset); Assert.Equal(0, msg.SysFlag & 4); // 不是IPv6 Assert.StartsWith("127.0.0.1:", msg.BornHost); Assert.StartsWith("127.0.0.1:", msg.StoreHost); Assert.Contains("9876", msg.BornHost); Assert.Contains("10911", msg.StoreHost); Assert.Equal("hello", msg.BodyString); Assert.Equal("test_topic", msg.Topic); // IPv4 MsgId应该是32个hex字符(16字节) Assert.Equal(32, msg.MsgId.Length); } [Fact] [DisplayName("IPv6消息正确解码")] public void ReadMessage_IPv6() { var data = BuildMessageBinary(true); var pk = new ArrayPacket(data); var msgs = MessageExt.ReadAll(pk); Assert.Single(msgs); var msg = msgs[0]; Assert.Equal(1, msg.QueueId); Assert.Equal(100, msg.QueueOffset); Assert.Equal(200, msg.CommitLogOffset); Assert.NotEqual(0, msg.SysFlag & 4); // 是IPv6 Assert.Contains("::1", msg.BornHost); Assert.Contains("::1", msg.StoreHost); Assert.Contains("9876", msg.BornHost); Assert.Contains("10911", msg.StoreHost); Assert.Equal("hello", msg.BodyString); Assert.Equal("test_topic", msg.Topic); // IPv6 MsgId应该是56个hex字符(28字节) Assert.Equal(56, msg.MsgId.Length); } [Fact] [DisplayName("SysFlag第2位判断IPv6")] public void SysFlag_IPv6_Bit() { // SysFlag=0 -> IPv4 Assert.Equal(0, 0 & 4); // SysFlag=4 -> IPv6 Assert.NotEqual(0, 4 & 4); // SysFlag=5(压缩+IPv6) -> IPv6 Assert.NotEqual(0, 5 & 4); // SysFlag=1(仅压缩) -> IPv4 Assert.Equal(0, 1 & 4); } } ================================================ FILE: XUnitTestRocketMQ/MQVersionTests.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; public class MQVersionTests { [Fact] public void Test1() { var ver = (MQVersion)275; Assert.Equal("V4_3_1", ver.ToString()); ver = (MQVersion)373; Assert.Equal("V4_8_0", ver.ToString()); } } ================================================ FILE: XUnitTestRocketMQ/MQVersionUpdateTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// MQVersion相关测试 public class MQVersionUpdateTests { [Fact] [DisplayName("MqBase默认版本为V4_9_7")] public void MqBase_DefaultVersion_V4_9_7() { using var producer = new Producer(); Assert.Equal(MQVersion.V4_9_7, producer.Version); } [Fact] [DisplayName("MQVersion枚举包含5.x版本")] public void MQVersion_Contains_5x() { Assert.True(Enum.IsDefined(typeof(MQVersion), MQVersion.V5_0_0)); Assert.True(Enum.IsDefined(typeof(MQVersion), MQVersion.V5_9_9)); Assert.True(Enum.IsDefined(typeof(MQVersion), MQVersion.HIGHER_VERSION)); } [Fact] [DisplayName("MQVersion_V4_9_7对应正确的枚举值")] public void MQVersion_V4_9_7_Value() { var ver = MQVersion.V4_9_7; Assert.Equal("V4_9_7", ver.ToString()); // 确保是有效的枚举值 Assert.True((Int32)ver > (Int32)MQVersion.V4_8_0); } [Fact] [DisplayName("MQVersion可自定义为5.x版本")] public void MqBase_Version_CanSet5x() { using var producer = new Producer { Version = MQVersion.V5_0_0 }; Assert.Equal(MQVersion.V5_0_0, producer.Version); } } ================================================ FILE: XUnitTestRocketMQ/ManagementTests.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using System.Threading.Tasks; using NewLife.Log; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// 管理功能测试 public class ManagementTests { [Fact(Skip = "需要RocketMQ服务器")] [DisplayName("DeleteTopic_删除主题")] public void DeleteTopic_Test() { var set = BasicTest.GetConfig(); using var mq = new Producer { NameServerAddress = set.NameServer, Log = XTrace.Log, }; mq.Start(); // 先创建再删除 mq.CreateTopic("nx_delete_test", 2); var count = mq.DeleteTopic("nx_delete_test"); Assert.True(count >= 0); } [Fact(Skip = "需要RocketMQ服务器")] [DisplayName("CreateSubscriptionGroup_创建消费组")] public void CreateSubscriptionGroup_Test() { var set = BasicTest.GetConfig(); using var mq = new Producer { NameServerAddress = set.NameServer, Log = XTrace.Log, }; mq.Start(); var count = mq.CreateSubscriptionGroup("nx_test_group", true, 16, 1); Assert.True(count >= 0); } [Fact(Skip = "需要RocketMQ服务器")] [DisplayName("DeleteSubscriptionGroup_删除消费组")] public void DeleteSubscriptionGroup_Test() { var set = BasicTest.GetConfig(); using var mq = new Producer { NameServerAddress = set.NameServer, Log = XTrace.Log, }; mq.Start(); mq.CreateSubscriptionGroup("nx_delete_group"); var count = mq.DeleteSubscriptionGroup("nx_delete_group"); Assert.True(count >= 0); } [Fact(Skip = "需要RocketMQ服务器")] [DisplayName("ViewMessage_按ID查看消息")] public void ViewMessage_Test() { var set = BasicTest.GetConfig(); using var mq = new Producer { Topic = "nx_test", NameServerAddress = set.NameServer, Log = XTrace.Log, }; mq.Start(); // 先发一条消息 var sr = mq.Publish("ViewMessage_Test", "TagA", null); Assert.NotNull(sr?.MsgId); // 尝试查看(可能因Broker索引延迟返回null) var msg = mq.ViewMessage(sr.MsgId); // 不做严格断言,仅验证不抛出异常 } [Fact(Skip = "需要RocketMQ服务器")] [DisplayName("GetClusterInfo_获取集群信息")] public void GetClusterInfo_Test() { var set = BasicTest.GetConfig(); using var mq = new Producer { NameServerAddress = set.NameServer, Log = XTrace.Log, }; mq.Start(); var info = mq.GetClusterInfo(); // 不做严格断言,仅验证不抛出异常 } [Fact(Skip = "需要RocketMQ服务器")] [DisplayName("GetConsumerConnectionList_获取消费者连接列表")] public async Task GetConsumerConnectionList_Test() { var set = BasicTest.GetConfig(); using var consumer = new Consumer { Topic = "nx_test", Group = "test", NameServerAddress = set.NameServer, Log = XTrace.Log, }; consumer.Start(); var list = await consumer.GetConsumerConnectionList("test"); // 不做严格断言,仅验证不抛出异常 } [Fact(Skip = "需要RocketMQ服务器")] [DisplayName("ResetConsumerOffset_重置消费偏移")] public async Task ResetConsumerOffset_Test() { var set = BasicTest.GetConfig(); using var consumer = new Consumer { Topic = "nx_test", Group = "test", NameServerAddress = set.NameServer, Log = XTrace.Log, }; consumer.Start(); // 重置到一小时前 var timestamp = DateTimeOffset.UtcNow.AddHours(-1).ToUnixTimeMilliseconds(); var result = await consumer.ResetConsumerOffset(timestamp); // 不做严格断言,仅验证不抛出异常 } } ================================================ FILE: XUnitTestRocketMQ/MessageExtendedTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// Message扩展属性测试(补充覆盖) public class MessageExtendedTests { #region Request-Reply属性 [Fact] [DisplayName("Message_ReplyToClient属性读写")] public void Message_ReplyToClient() { var msg = new Message(); Assert.Null(msg.ReplyToClient); msg.ReplyToClient = "client-001"; Assert.Equal("client-001", msg.ReplyToClient); } [Fact] [DisplayName("Message_CorrelationId属性读写")] public void Message_CorrelationId() { var msg = new Message(); Assert.Null(msg.CorrelationId); msg.CorrelationId = "CID-001"; Assert.Equal("CID-001", msg.CorrelationId); } [Fact] [DisplayName("Message_MessageType属性读写")] public void Message_MessageType() { var msg = new Message(); Assert.Null(msg.MessageType); msg.MessageType = "reply"; Assert.Equal("reply", msg.MessageType); } [Fact] [DisplayName("Message_RequestTimeout属性读写")] public void Message_RequestTimeout() { var msg = new Message(); Assert.Equal(0, msg.RequestTimeout); msg.RequestTimeout = 5000; Assert.Equal(5000, msg.RequestTimeout); } #endregion #region DelayTimeLevel [Fact] [DisplayName("Message_DelayTimeLevel属性读写")] public void Message_DelayTimeLevel() { var msg = new Message(); Assert.Equal(0, msg.DelayTimeLevel); msg.DelayTimeLevel = 3; Assert.Equal(3, msg.DelayTimeLevel); } #endregion #region WaitStoreMsgOK [Fact] [DisplayName("Message_WaitStoreMsgOK默认为true")] public void Message_WaitStoreMsgOK_DefaultTrue() { var msg = new Message(); Assert.True(msg.WaitStoreMsgOK); } [Fact] [DisplayName("Message_WaitStoreMsgOK可设置为false")] public void Message_WaitStoreMsgOK_SetFalse() { var msg = new Message { WaitStoreMsgOK = false }; Assert.False(msg.WaitStoreMsgOK); } #endregion #region TransactionId [Fact] [DisplayName("Message_TransactionId属性读写")] public void Message_TransactionId() { var msg = new Message(); Assert.Null(msg.TransactionId); msg.TransactionId = "TX-001"; Assert.Equal("TX-001", msg.TransactionId); } #endregion #region PutUserProperty / GetUserProperty [Fact] [DisplayName("PutUserProperty_存入自定义属性")] public void PutUserProperty_StoresProperty() { var msg = new Message(); msg.PutUserProperty("orderId", "12345"); Assert.Equal("12345", msg.GetUserProperty("orderId")); } [Fact] [DisplayName("PutUserProperty_Key为null时抛异常")] public void PutUserProperty_NullKey_ThrowsException() { var msg = new Message(); Assert.Throws(() => msg.PutUserProperty(null, "value")); } [Fact] [DisplayName("PutUserProperty_Value为null时抛异常")] public void PutUserProperty_NullValue_ThrowsException() { var msg = new Message(); Assert.Throws(() => msg.PutUserProperty("key", null)); } [Fact] [DisplayName("GetUserProperty_不存在的Key返回null")] public void GetUserProperty_NonExistentKey_ReturnsNull() { var msg = new Message(); Assert.Null(msg.GetUserProperty("nonexistent")); } #endregion #region SetBody [Fact] [DisplayName("SetBody_设置对象自动JSON序列化")] public void SetBody_Object_SerializesToJson() { var msg = new Message(); msg.SetBody(new { Name = "test", Value = 42 }); Assert.NotNull(msg.Body); Assert.Contains("test", msg.BodyString); Assert.Contains("42", msg.BodyString); } [Fact] [DisplayName("SetBody_Byte数组直接设置")] public void SetBody_ByteArray_SetDirectly() { var msg = new Message(); var data = new Byte[] { 1, 2, 3 }; msg.SetBody(data); Assert.Equal(data, msg.Body); } #endregion #region ToString [Fact] [DisplayName("Message_ToString有Body时返回BodyString")] public void Message_ToString_WithBody() { var msg = new Message(); msg.SetBody("Hello"); Assert.Equal("Hello", msg.ToString()); } [Fact] [DisplayName("Message_ToString无Body时返回类型名")] public void Message_ToString_WithoutBody() { var msg = new Message(); var str = msg.ToString(); Assert.NotNull(str); } #endregion #region GetProperties序列化 [Fact] [DisplayName("GetProperties_多个属性序列化为分隔字符串")] public void GetProperties_MultipleProperties() { var msg = new Message { Tags = "TagA", Keys = "Key1" }; var props = msg.GetProperties(); Assert.NotNull(props); Assert.Contains("TAGS", props); Assert.Contains("TagA", props); Assert.Contains("KEYS", props); Assert.Contains("Key1", props); } [Fact] [DisplayName("GetProperties_空属性返回空字符串")] public void GetProperties_EmptyProperties() { var msg = new Message(); var props = msg.GetProperties(); Assert.NotNull(props); Assert.Equal(String.Empty, props); } #endregion #region ParseProperties [Fact] [DisplayName("ParseProperties_解析标签和键")] public void ParseProperties_ParsesTagsAndKeys() { var msg = new Message(); msg.ParseProperties("TAGS\u0001TagA\u0002KEYS\u0001Key1\u0002"); Assert.Equal("TagA", msg.Tags); Assert.Equal("Key1", msg.Keys); } [Fact] [DisplayName("ParseProperties_解析延迟等级")] public void ParseProperties_ParsesDelayLevel() { var msg = new Message(); msg.ParseProperties("DELAY\u00013\u0002"); Assert.Equal(3, msg.DelayTimeLevel); } [Fact] [DisplayName("ParseProperties_空字符串返回原Properties")] public void ParseProperties_EmptyString_ReturnsOriginal() { var msg = new Message(); msg.PutUserProperty("existing", "value"); var result = msg.ParseProperties(""); Assert.Equal("value", msg.GetUserProperty("existing")); } #endregion } ================================================ FILE: XUnitTestRocketMQ/MessageId5xTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// 5.x MessageId格式测试 public class MessageId5xTests { [Fact] [DisplayName("CreateMessageId5x_生成有效的5x格式ID")] public void CreateMessageId5x_GeneratesValidId() { var mac = new Byte[] { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF }; var id = MessageExt.CreateMessageId5x(1, mac, 12345, 67890); Assert.NotNull(id); Assert.Equal(32, id.Length); Assert.StartsWith("01", id, StringComparison.OrdinalIgnoreCase); } [Fact] [DisplayName("TryParseMessageId5x_解析生成的ID")] public void TryParseMessageId5x_ParseCreatedId() { var mac = new Byte[] { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF }; var id = MessageExt.CreateMessageId5x(1, mac, 12345, 67890); var ok = MessageExt.TryParseMessageId5x(id, out var version, out var parsedMac, out var processId, out var counter); Assert.True(ok); Assert.Equal(1, version); Assert.Equal(mac, parsedMac); Assert.Equal(12345, processId); Assert.Equal(67890, counter); } [Fact] [DisplayName("TryParseMessageId5x_非5x格式返回false")] public void TryParseMessageId5x_Invalid_ReturnsFalse() { // 4.x格式(32个hex,但前缀不是01) var id = "AABBCCDD00001111000000000000FFFF"; var ok = MessageExt.TryParseMessageId5x(id, out _, out _, out _, out _); Assert.False(ok); } [Fact] [DisplayName("TryParseMessageId5x_Null输入返回false")] public void TryParseMessageId5x_Null_ReturnsFalse() { var ok = MessageExt.TryParseMessageId5x(null, out _, out _, out _, out _); Assert.False(ok); } [Fact] [DisplayName("TryParseMessageId5x_空字符串返回false")] public void TryParseMessageId5x_Empty_ReturnsFalse() { var ok = MessageExt.TryParseMessageId5x("", out _, out _, out _, out _); Assert.False(ok); } [Fact] [DisplayName("TryParseMessageId5x_长度不匹配返回false")] public void TryParseMessageId5x_WrongLength_ReturnsFalse() { var ok = MessageExt.TryParseMessageId5x("0101AABB", out _, out _, out _, out _); Assert.False(ok); } [Fact] [DisplayName("IsMessageId5x_有效5x格式返回true")] public void IsMessageId5x_Valid_ReturnsTrue() { var mac = new Byte[] { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66 }; var id = MessageExt.CreateMessageId5x(1, mac, 100, 200); Assert.True(MessageExt.IsMessageId5x(id)); } [Fact] [DisplayName("IsMessageId5x_4x格式返回false")] public void IsMessageId5x_4xFormat_ReturnsFalse() { // 典型的4.x格式MsgId(16字节=32hex,但前缀不是01) Assert.False(MessageExt.IsMessageId5x("C0A80001000030390000000000000001")); } [Fact] [DisplayName("IsMessageId5x_Null返回false")] public void IsMessageId5x_Null_ReturnsFalse() { Assert.False(MessageExt.IsMessageId5x(null)); } [Fact] [DisplayName("CreateMessageId5x_无MAC时使用随机字节")] public void CreateMessageId5x_NullMac_UsesRandom() { var id1 = MessageExt.CreateMessageId5x(1, null, 1, 1); var id2 = MessageExt.CreateMessageId5x(1, null, 1, 1); Assert.NotNull(id1); Assert.Equal(32, id1.Length); Assert.StartsWith("01", id1, StringComparison.OrdinalIgnoreCase); // 随机MAC,两次结果可能不同(概率极高) } [Fact] [DisplayName("CreateMessageId5x_不同计数器产生不同ID")] public void CreateMessageId5x_DifferentCounter_DifferentId() { var mac = new Byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 }; var id1 = MessageExt.CreateMessageId5x(1, mac, 100, 1); var id2 = MessageExt.CreateMessageId5x(1, mac, 100, 2); Assert.NotEqual(id1, id2); } } ================================================ FILE: XUnitTestRocketMQ/MessageQueueTests.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// MessageQueue消息队列标识测试 public class MessageQueueTests { #region Equals [Fact] [DisplayName("Equals_相同属性相等")] public void Equals_SameProperties_ReturnsTrue() { var q1 = new MessageQueue { Topic = "test", BrokerName = "broker-a", QueueId = 0 }; var q2 = new MessageQueue { Topic = "test", BrokerName = "broker-a", QueueId = 0 }; Assert.True(q1.Equals(q2)); Assert.True(q2.Equals(q1)); } [Fact] [DisplayName("Equals_不同Topic不相等")] public void Equals_DifferentTopic_ReturnsFalse() { var q1 = new MessageQueue { Topic = "test1", BrokerName = "broker-a", QueueId = 0 }; var q2 = new MessageQueue { Topic = "test2", BrokerName = "broker-a", QueueId = 0 }; Assert.False(q1.Equals(q2)); } [Fact] [DisplayName("Equals_不同BrokerName不相等")] public void Equals_DifferentBrokerName_ReturnsFalse() { var q1 = new MessageQueue { Topic = "test", BrokerName = "broker-a", QueueId = 0 }; var q2 = new MessageQueue { Topic = "test", BrokerName = "broker-b", QueueId = 0 }; Assert.False(q1.Equals(q2)); } [Fact] [DisplayName("Equals_不同QueueId不相等")] public void Equals_DifferentQueueId_ReturnsFalse() { var q1 = new MessageQueue { Topic = "test", BrokerName = "broker-a", QueueId = 0 }; var q2 = new MessageQueue { Topic = "test", BrokerName = "broker-a", QueueId = 1 }; Assert.False(q1.Equals(q2)); } [Fact] [DisplayName("Equals_非MessageQueue对象不相等")] public void Equals_NonMessageQueue_ReturnsFalse() { var q = new MessageQueue { Topic = "test", BrokerName = "broker-a", QueueId = 0 }; Assert.False(q.Equals("not a queue")); Assert.False(q.Equals(null)); Assert.False(q.Equals(42)); } [Fact] [DisplayName("Equals_自身比较相等")] public void Equals_Self_ReturnsTrue() { var q = new MessageQueue { Topic = "test", BrokerName = "broker-a", QueueId = 0 }; Assert.True(q.Equals(q)); } #endregion #region GetHashCode [Fact] [DisplayName("GetHashCode_相同属性相同哈希")] public void GetHashCode_SameProperties_SameHash() { var q1 = new MessageQueue { Topic = "test", BrokerName = "broker-a", QueueId = 0 }; var q2 = new MessageQueue { Topic = "test", BrokerName = "broker-a", QueueId = 0 }; Assert.Equal(q1.GetHashCode(), q2.GetHashCode()); } [Fact] [DisplayName("GetHashCode_同一实例多次调用相同哈希")] public void GetHashCode_SameInstance_MultipleCalls_SameHash() { var q = new MessageQueue { Topic = "test1", BrokerName = "broker-a", QueueId = 0 }; // 同一实例在一次执行过程中多次调用 GetHashCode,结果应保持一致 var h1 = q.GetHashCode(); var h2 = q.GetHashCode(); Assert.Equal(h1, h2); } [Fact] [DisplayName("GetHashCode_可用于字典键")] public void GetHashCode_WorksAsDictionaryKey() { var q1 = new MessageQueue { Topic = "test", BrokerName = "broker-a", QueueId = 0 }; var q2 = new MessageQueue { Topic = "test", BrokerName = "broker-a", QueueId = 0 }; var dict = new Dictionary(); dict[q1] = "value"; // q2 与 q1 属性相同且相等,应能作为键查到 // 注意:Dictionary 使用 GetHashCode + Equals 处理键的相等性 // 前提是 MessageQueue 正确重写了 Equals 和 GetHashCode Assert.True(dict.ContainsKey(q2)); Assert.Equal("value", dict[q2]); } #endregion #region ToString [Fact] [DisplayName("ToString_格式正确")] public void ToString_CorrectFormat() { var q = new MessageQueue { Topic = "test", BrokerName = "broker-a", QueueId = 3 }; var str = q.ToString(); Assert.Equal("broker-a[3]", str); } [Fact] [DisplayName("ToString_Null属性不抛异常")] public void ToString_NullBrokerName() { var q = new MessageQueue { QueueId = 0 }; var str = q.ToString(); Assert.Contains("[0]", str); } #endregion } ================================================ FILE: XUnitTestRocketMQ/MessageTests.cs ================================================ using System; using NewLife; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using NewLife.Serialization; using Xunit; namespace XUnitTestRocketMQ; public class MessageTests { [Fact] public void SetBody_WithString_SetsBodyCorrectly() { // Arrange var message = new Message(); var body = "Hello, World!"; // Act message.SetBody(body); // Assert Assert.Equal(body, message.BodyString); Assert.Equal(body.GetBytes(), message.Body); } [Fact] public void SetBody_WithByteArray_SetsBodyCorrectly() { // Arrange var message = new Message(); var body = new byte[] { 1, 2, 3, 4 }; // Act message.SetBody(body); // Assert Assert.Equal(body, message.Body); } [Fact] public void GetProperties_ReturnsCorrectProperties() { // Arrange var message = new Message { Tags = "Tag1", Keys = "Key1", DelayTimeLevel = 2, WaitStoreMsgOK = false }; // Act var properties = message.GetProperties(); // Assert Assert.Contains("TAGS\u0001Tag1\u0002", properties); Assert.Contains("KEYS\u0001Key1\u0002", properties); Assert.Contains("DELAY\u00012\u0002", properties); Assert.Contains("WAIT\u0001False\u0002", properties); var header = new SendMessageRequestHeader { ProducerGroup = "TestGroup", Topic = "TestTopic", SysFlag = 0, BornTimestamp = DateTime.UtcNow.ToLong(), Flag = message.Flag, Properties = message.GetProperties(), }; var ext = header.GetProperties(); //Assert.Equal(11, ext.Count); Assert.Equal("TAGS\u0001Tag1\u0002KEYS\u0001Key1\u0002DELAY\u00012\u0002WAIT\u0001False\u0002", ext["i"]); var broker = new BrokerClient([""]); var cmd = broker.CreateCommand(RequestCode.SEND_MESSAGE_V2, null, ext); var json = cmd.Header.ToJson(false, false, false); var js = new SystemJson(); var json2 = js.Write(cmd.Header, false, false, true); //Assert.Equal(json, json2); } [Fact] public void ParseProperties_SetsPropertiesCorrectly() { // Arrange var message = new Message(); var properties = "TAGS\u0001Tag1\u0002KEYS\u0001Key1\u0002DELAY\u00012\u0002WAIT\u0001False\u0002"; // Act var result = message.ParseProperties(properties); // Assert Assert.Equal("Tag1", message.Tags); Assert.Equal("Key1", message.Keys); Assert.Equal(2, message.DelayTimeLevel); Assert.False(message.WaitStoreMsgOK); } } ================================================ FILE: XUnitTestRocketMQ/MessageTraceTests.cs ================================================ using System; using System.Threading; using NewLife; using NewLife.Log; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ { public class MessageTraceTests { private const String Topic = "TopicDemo"; private const String Group = "TraceTestGroup"; private const String NameServerAddress = "127.0.0.1:9876"; [Fact(Skip = "需要RocketMQ服务器支持")] public void Producer_And_Consumer_With_Trace_Enabled_Should_Work() { // 使用 ManualResetEvent 来同步测试的完成 var mre = new ManualResetEvent(false); // 1. 创建并启动消费者 var consumer = new Consumer { Topic = Topic, Group = Group, NameServerAddress = NameServerAddress, EnableMessageTrace = true, // 启用消息轨迹 Log = XTrace.Log }; consumer.OnConsume = (q, msgs) => { foreach (var msg in msgs) { XTrace.WriteLine($"消费到消息: {msg.Body.ToStr()}"); } // 设置事件,表示消费成功 mre.Set(); return true; }; consumer.Start(); // 2. 创建并启动生产者 var producer = new Producer { Topic = Topic, Group = Group, // 生产者组可以和消费者组不同,这里为了简单使用同一个 NameServerAddress = NameServerAddress, EnableMessageTrace = true, // 启用消息轨迹 Log = XTrace.Log }; producer.Start(); // 3. 发送消息 var messageBody = "Hello, RocketMQ with Message Trace!"; var sendResult = producer.Publish(messageBody); Assert.NotNull(sendResult); Assert.Equal(SendStatus.SendOK, sendResult.Status); XTrace.WriteLine($"消息发送成功: MsgId={sendResult.MsgId}"); // 4. 等待消费者处理消息,设置一个超时时间以防测试挂起 bool consumed = mre.WaitOne(TimeSpan.FromSeconds(30)); // 5. 清理资源 producer.Stop(); consumer.Stop(); // 6. 断言 Assert.True(consumed, "消费者在超时时间内没有收到消息。"); } } } ================================================ FILE: XUnitTestRocketMQ/ModelTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ.Models; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// 模型和枚举测试 public class ModelTests { #region DelayTimeLevels [Fact] [DisplayName("DelayTimeLevels_包含18个等级")] public void DelayTimeLevels_Has18Levels() { var values = Enum.GetValues(typeof(DelayTimeLevels)); Assert.Equal(18, values.Length); } [Fact] [DisplayName("DelayTimeLevels_等级值从1开始")] public void DelayTimeLevels_StartsFrom1() { Assert.Equal(1, (Int32)DelayTimeLevels.S1); Assert.Equal(2, (Int32)DelayTimeLevels.S5); Assert.Equal(18, (Int32)DelayTimeLevels.Hour2); } [Fact] [DisplayName("DelayTimeLevels_关键等级值正确")] public void DelayTimeLevels_KeyValues() { Assert.Equal(5, (Int32)DelayTimeLevels.Min1); Assert.Equal(14, (Int32)DelayTimeLevels.Min10); Assert.Equal(16, (Int32)DelayTimeLevels.Min30); Assert.Equal(17, (Int32)DelayTimeLevels.Hour1); } #endregion #region MessageModels [Fact] [DisplayName("MessageModels_集群和广播模式")] public void MessageModels_Values() { Assert.Equal(0, (Int32)MessageModels.Clustering); Assert.Equal(1, (Int32)MessageModels.Broadcasting); } #endregion #region ConsumeTypes [Fact] [DisplayName("ConsumeTypes_包含Pull和Push")] public void ConsumeTypes_Values() { Assert.True(Enum.IsDefined(typeof(ConsumeTypes), "Pull")); Assert.True(Enum.IsDefined(typeof(ConsumeTypes), "Push")); } #endregion #region ConsumeEventArgs [Fact] [DisplayName("ConsumeEventArgs_属性可设置")] public void ConsumeEventArgs_PropertiesCanBeSet() { var mq = new MessageQueue { BrokerName = "b1", QueueId = 0 }; var msgs = new[] { new MessageExt { Topic = "t1" } }; var pr = new PullResult { Status = PullStatus.Found }; var args = new ConsumeEventArgs { Queue = mq, Messages = msgs, Result = pr, }; Assert.Same(mq, args.Queue); Assert.Single(args.Messages); Assert.Equal(PullStatus.Found, args.Result.Status); } [Fact] [DisplayName("ConsumeEventArgs_默认值为null")] public void ConsumeEventArgs_Defaults() { var args = new ConsumeEventArgs(); Assert.Null(args.Queue); Assert.Null(args.Messages); Assert.Null(args.Result); } #endregion #region ServiceState [Fact] [DisplayName("ServiceState_枚举包含基本状态")] public void ServiceState_Values() { Assert.True(Enum.IsDefined(typeof(ServiceState), "CreateJust")); Assert.True(Enum.IsDefined(typeof(ServiceState), "Running")); Assert.True(Enum.IsDefined(typeof(ServiceState), "ShutdownAlready")); } #endregion #region RequestCode [Fact] [DisplayName("RequestCode_核心指令码正确")] public void RequestCode_CoreValues() { Assert.Equal(10, (Int32)RequestCode.SEND_MESSAGE); Assert.Equal(11, (Int32)RequestCode.PULL_MESSAGE); Assert.Equal(12, (Int32)RequestCode.QUERY_MESSAGE); Assert.Equal(34, (Int32)RequestCode.HEART_BEAT); Assert.Equal(35, (Int32)RequestCode.UNREGISTER_CLIENT); Assert.Equal(100, (Int32)RequestCode.PUT_KV_CONFIG); Assert.Equal(104, (Int32)RequestCode.UNREGISTER_BROKER); Assert.Equal(105, (Int32)RequestCode.GET_ROUTEINTO_BY_TOPIC); } #endregion #region ResponseCode [Fact] [DisplayName("ResponseCode_核心响应码正确")] public void ResponseCode_CoreValues() { Assert.Equal(0, (Int32)ResponseCode.SUCCESS); Assert.Equal(1, (Int32)ResponseCode.SYSTEM_ERROR); Assert.Equal(16, (Int32)ResponseCode.NO_PERMISSION); Assert.Equal(17, (Int32)ResponseCode.TOPIC_NOT_EXIST); } #endregion #region LanguageCode [Fact] [DisplayName("LanguageCode_包含主要语言")] public void LanguageCode_MainValues() { Assert.True(Enum.IsDefined(typeof(LanguageCode), "JAVA")); Assert.True(Enum.IsDefined(typeof(LanguageCode), "CPP")); Assert.True(Enum.IsDefined(typeof(LanguageCode), "DOTNET")); Assert.True(Enum.IsDefined(typeof(LanguageCode), "GO")); } #endregion #region TransactionState [Fact] [DisplayName("TransactionState_枚举值正确")] public void TransactionState_Values() { Assert.Equal(4, (Int32)TransactionState.Prepared); Assert.Equal(8, (Int32)TransactionState.Commit); Assert.Equal(12, (Int32)TransactionState.Rollback); } #endregion } ================================================ FILE: XUnitTestRocketMQ/MqBasePropertyTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ; using NewLife.RocketMQ.Client; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// MqBase基类属性测试 public class MqBasePropertyTests { #region 默认值 [Fact] [DisplayName("Producer_默认Group为DEFAULT_PRODUCER")] public void Producer_DefaultGroup() { using var producer = new Producer(); Assert.Equal("DEFAULT_PRODUCER", producer.Group); } [Fact] [DisplayName("Producer_默认Topic为TBW102")] public void Producer_DefaultTopic() { using var producer = new Producer(); Assert.Equal("TBW102", producer.Topic); } [Fact] [DisplayName("Producer_默认DefaultTopicQueueNums为4")] public void Producer_DefaultTopicQueueNums() { using var producer = new Producer(); Assert.Equal(4, producer.DefaultTopicQueueNums); } [Fact] [DisplayName("Producer_默认InstanceName不为空")] public void Producer_DefaultInstanceName() { using var producer = new Producer(); // InstanceName 默认为进程 ID Assert.NotNull(producer.InstanceName); Assert.NotEmpty(producer.InstanceName); } [Fact] [DisplayName("Producer_默认PollNameServerInterval为30000")] public void Producer_DefaultPollInterval() { using var producer = new Producer(); Assert.Equal(30_000, producer.PollNameServerInterval); } [Fact] [DisplayName("Producer_默认HeartbeatBrokerInterval为30000")] public void Producer_DefaultHeartbeatInterval() { using var producer = new Producer(); Assert.Equal(30_000, producer.HeartbeatBrokerInterval); } [Fact] [DisplayName("Producer_默认SerializeType为JSON")] public void Producer_DefaultSerializeType() { using var producer = new Producer(); Assert.Equal(SerializeType.JSON, producer.SerializeType); } [Fact] [DisplayName("Producer_默认Version为V4_9_7")] public void Producer_DefaultVersion() { using var producer = new Producer(); Assert.Equal(MQVersion.V4_9_7, producer.Version); } [Fact] [DisplayName("Producer_默认不启用VIP通道")] public void Producer_DefaultVipChannelDisabled() { using var producer = new Producer(); Assert.False(producer.VipChannelEnabled); } [Fact] [DisplayName("Producer_默认不启用消息轨迹")] public void Producer_DefaultTraceDisabled() { using var producer = new Producer(); Assert.False(producer.EnableMessageTrace); } [Fact] [DisplayName("Producer_默认不使用外部代理")] public void Producer_DefaultExternalBrokerDisabled() { using var producer = new Producer(); Assert.False(producer.ExternalBroker); } #endregion #region ClientId [Fact] [DisplayName("ClientId_包含IP和InstanceName")] public void ClientId_ContainsIPAndInstance() { using var producer = new Producer(); var clientId = producer.ClientId; Assert.NotNull(clientId); Assert.Contains("@", clientId); } [Fact] [DisplayName("ClientId_含UnitName时追加")] public void ClientId_WithUnitName() { using var producer = new Producer { UnitName = "unit1" }; var clientId = producer.ClientId; Assert.Contains("@unit1", clientId); } #endregion #region 属性设置 [Fact] [DisplayName("Producer_可设置Name")] public void Producer_SetName() { using var producer = new Producer { Name = "TestProducer" }; Assert.Equal("TestProducer", producer.Name); } [Fact] [DisplayName("Producer_可设置NameServerAddress")] public void Producer_SetNameServerAddress() { using var producer = new Producer { NameServerAddress = "127.0.0.1:9876" }; Assert.Equal("127.0.0.1:9876", producer.NameServerAddress); } [Fact] [DisplayName("Producer_DefaultTopic静态属性为TBW102")] public void DefaultTopic_IsTBW102() { Assert.Equal("TBW102", MqBase.DefaultTopic); } [Fact] [DisplayName("Consumer_默认Group为DEFAULT_PRODUCER")] public void Consumer_DefaultGroup() { using var consumer = new Consumer(); Assert.Equal("DEFAULT_PRODUCER", consumer.Group); } [Fact] [DisplayName("Consumer_可设置属性")] public void Consumer_SetProperties() { using var consumer = new Consumer { Topic = "order_topic", Group = "CG_ORDER", NameServerAddress = "10.0.0.1:9876", }; Assert.Equal("order_topic", consumer.Topic); Assert.Equal("CG_ORDER", consumer.Group); Assert.Equal("10.0.0.1:9876", consumer.NameServerAddress); } #endregion #region Active状态 [Fact] [DisplayName("Producer_未启动时Active为false")] public void Producer_NotStarted_ActiveFalse() { using var producer = new Producer(); Assert.False(producer.Active); } [Fact] [DisplayName("Consumer_未启动时Active为false")] public void Consumer_NotStarted_ActiveFalse() { using var consumer = new Consumer(); Assert.False(consumer.Active); } #endregion } ================================================ FILE: XUnitTestRocketMQ/MqSettingTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ; using Xunit; namespace XUnitTestRocketMQ; /// MqSetting配置测试 public class MqSettingTests { [Fact] [DisplayName("MqSetting_属性可设置")] public void MqSetting_SetProperties() { var setting = new MqSetting { NameServer = "127.0.0.1:9876", Topic = "test_topic", Group = "test_group", Server = "http://ons.aliyun.com", AccessKey = "ak", SecretKey = "sk", }; Assert.Equal("127.0.0.1:9876", setting.NameServer); Assert.Equal("test_topic", setting.Topic); Assert.Equal("test_group", setting.Group); Assert.Equal("http://ons.aliyun.com", setting.Server); Assert.Equal("ak", setting.AccessKey); Assert.Equal("sk", setting.SecretKey); } } ================================================ FILE: XUnitTestRocketMQ/MultiTopicTests.cs ================================================ using System; using System.ComponentModel; using System.Linq; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// 多Topic订阅测试 public class MultiTopicTests { [Fact] [DisplayName("Consumer_Topics属性默认为null")] public void Consumer_Topics_DefaultNull() { using var consumer = new Consumer(); Assert.Null(consumer.Topics); } [Fact] [DisplayName("Consumer_可设置多个Topics")] public void Consumer_Topics_CanBeSet() { using var consumer = new Consumer { Topics = ["topic_a", "topic_b", "topic_c"] }; Assert.NotNull(consumer.Topics); Assert.Equal(3, consumer.Topics.Length); Assert.Equal("topic_a", consumer.Topics[0]); } [Fact] [DisplayName("Consumer_Topics为空时保持单Topic行为")] public void Consumer_Topics_EmptyFallsBackToSingleTopic() { using var consumer = new Consumer { Topic = "default_topic", Topics = null }; // Topics为null时,内部应使用单Topic Assert.Equal("default_topic", consumer.Topic); } [Fact] [DisplayName("MessageQueue_已包含Topic属性")] public void MessageQueue_HasTopicProperty() { var mq1 = new MessageQueue { Topic = "topic_a", BrokerName = "broker-a", QueueId = 0 }; var mq2 = new MessageQueue { Topic = "topic_b", BrokerName = "broker-a", QueueId = 0 }; // 不同Topic的队列不相等 Assert.NotEqual(mq1, mq2); Assert.NotEqual(mq1.GetHashCode(), mq2.GetHashCode()); } [Fact] [DisplayName("MessageQueue_相同Topic相等")] public void MessageQueue_SameTopic_Equal() { var mq1 = new MessageQueue { Topic = "topic_a", BrokerName = "broker-a", QueueId = 0 }; var mq2 = new MessageQueue { Topic = "topic_a", BrokerName = "broker-a", QueueId = 0 }; Assert.Equal(mq1, mq2); } [Fact(Skip = "需要RocketMQ服务器")] [DisplayName("Consumer_多Topic启动消费")] public void Consumer_MultiTopic_Start() { using var consumer = new Consumer { Topic = "topic_main", Topics = ["topic_a", "topic_b"], Group = "test_multi_group", NameServerAddress = "127.0.0.1:9876", }; // 验证Topics设置正确 Assert.Equal(2, consumer.Topics.Length); } } ================================================ FILE: XUnitTestRocketMQ/NameClientTests.cs ================================================ using System; using System.ComponentModel; using Moq; using NewLife; using NewLife.Data; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; public class NameClientTests { [Fact] [DisplayName("GetRouteInfo_解析标准JSON格式的路由信息")] public void GetRouteInfo() { var target = """ {"brokerDatas":[{"brokerAddrs":{"0":"10.2.3.117:10911"},"brokerName":"broker-a","cluster":"DefaultCluster","enableActingMaster":false}],"filterServerTable":{},"queueDatas":[{"brokerName":"broker-a","perm":6,"readQueueNums":8,"topicSysFlag":0,"writeQueueNums":8}]} """; var pb = new Producer(); var nc = new Mock("clientId", pb) { CallBase = true }; nc.Setup(e => e.Invoke(RequestCode.GET_ROUTEINTO_BY_TOPIC, null, It.IsAny(), false)) .Returns(new Command { Payload = (ArrayPacket)target.GetBytes() }); var client = nc.Object; var brokers = client.GetRouteInfo(null); Assert.Single(brokers); var broker = brokers[0]; Assert.Equal("broker-a", broker.Name); Assert.Equal(Permissions.Read | Permissions.Write, broker.Permission); Assert.Equal(8, broker.ReadQueueNums); Assert.Equal(8, broker.WriteQueueNums); Assert.Equal(0, broker.TopicSynFlag); Assert.Equal("DefaultCluster", broker.Cluster); Assert.Single(broker.Addresses); Assert.Equal("10.2.3.117:10911", broker.Addresses[0]); } [Fact] [DisplayName("GetRouteInfo_解析整数Key的brokerAddrs格式")] public void GetRouteInfo2() { var target = """ {"brokerDatas":[{"brokerAddrs":{0:"10.2.3.117:10911"},"brokerName":"broker-a","cluster":"DefaultCluster","enableActingMaster":false}],"filterServerTable":{},"queueDatas":[{"brokerName":"broker-a","perm":6,"readQueueNums":8,"topicSysFlag":0,"writeQueueNums":8}]} """; var pb = new Producer(); var nc = new Mock("clientId", pb) { CallBase = true }; nc.Setup(e => e.Invoke(RequestCode.GET_ROUTEINTO_BY_TOPIC, null, It.IsAny(), false)) .Returns(new Command { Payload = (ArrayPacket)target.GetBytes() }); var client = nc.Object; var brokers = client.GetRouteInfo(null); Assert.Single(brokers); var broker = brokers[0]; Assert.Equal("broker-a", broker.Name); Assert.Equal(Permissions.Read | Permissions.Write, broker.Permission); Assert.Equal(8, broker.ReadQueueNums); Assert.Equal(8, broker.WriteQueueNums); Assert.Equal(0, broker.TopicSynFlag); Assert.Equal("DefaultCluster", broker.Cluster); Assert.Single(broker.Addresses); Assert.Equal("10.2.3.117:10911", broker.Addresses[0]); } [Fact] [DisplayName("GetRouteInfo_非零topicSysFlag正确解析")] public void GetRouteInfo_NonZeroTopicSysFlag() { var target = """ {"brokerDatas":[{"brokerAddrs":{"0":"10.0.0.1:10911"},"brokerName":"broker-b","cluster":"TestCluster","enableActingMaster":false}],"filterServerTable":{},"queueDatas":[{"brokerName":"broker-b","perm":6,"readQueueNums":4,"topicSysFlag":3,"writeQueueNums":4}]} """; var pb = new Producer(); var nc = new Mock("clientId", pb) { CallBase = true }; nc.Setup(e => e.Invoke(RequestCode.GET_ROUTEINTO_BY_TOPIC, null, It.IsAny(), false)) .Returns(new Command { Payload = (ArrayPacket)target.GetBytes() }); var client = nc.Object; var brokers = client.GetRouteInfo("test_topic"); Assert.Single(brokers); Assert.Equal(3, brokers[0].TopicSynFlag); } [Fact] [DisplayName("GetRouteInfo_指定topic时缓存路由信息可通过GetTopicBrokers获取")] public void GetRouteInfo_WithTopic_CachesResult() { var target = """ {"brokerDatas":[{"brokerAddrs":{"0":"10.0.0.1:10911"},"brokerName":"broker-a","cluster":"DefaultCluster","enableActingMaster":false}],"filterServerTable":{},"queueDatas":[{"brokerName":"broker-a","perm":6,"readQueueNums":8,"topicSysFlag":0,"writeQueueNums":8}]} """; var pb = new Producer(); var nc = new Mock("clientId", pb) { CallBase = true }; nc.Setup(e => e.Invoke(RequestCode.GET_ROUTEINTO_BY_TOPIC, null, It.IsAny(), false)) .Returns(new Command { Payload = (ArrayPacket)target.GetBytes() }); var client = nc.Object; client.GetRouteInfo("my_topic"); // 缓存应可通过 GetTopicBrokers 取回 var cached = client.GetTopicBrokers("my_topic"); Assert.NotNull(cached); Assert.Single(cached); Assert.Equal("broker-a", cached[0].Name); } [Fact] [DisplayName("GetRouteInfo_null_topic不缓存且不抛出异常")] public void GetRouteInfo_NullTopic_DoesNotThrow() { var target = """ {"brokerDatas":[{"brokerAddrs":{"0":"10.0.0.1:10911"},"brokerName":"broker-a","cluster":"DefaultCluster","enableActingMaster":false}],"filterServerTable":{},"queueDatas":[{"brokerName":"broker-a","perm":6,"readQueueNums":8,"topicSysFlag":0,"writeQueueNums":8}]} """; var pb = new Producer(); var nc = new Mock("clientId", pb) { CallBase = true }; nc.Setup(e => e.Invoke(RequestCode.GET_ROUTEINTO_BY_TOPIC, null, It.IsAny(), false)) .Returns(new Command { Payload = (ArrayPacket)target.GetBytes() }); var client = nc.Object; // null topic 不应抛出 ArgumentNullException var brokers = client.GetRouteInfo(null); Assert.Single(brokers); Assert.Equal("broker-a", brokers[0].Name); } [Fact] [DisplayName("GetRouteInfo_Master地址排首位_Slave地址正确解析")] public void GetRouteInfo_MasterSlave_Addresses() { var target = """ {"brokerDatas":[{"brokerAddrs":{"0":"10.0.0.1:10911","1":"10.0.0.2:10911"},"brokerName":"broker-a","cluster":"DefaultCluster","enableActingMaster":false}],"filterServerTable":{},"queueDatas":[{"brokerName":"broker-a","perm":6,"readQueueNums":8,"topicSysFlag":0,"writeQueueNums":8}]} """; var pb = new Producer(); var nc = new Mock("clientId", pb) { CallBase = true }; nc.Setup(e => e.Invoke(RequestCode.GET_ROUTEINTO_BY_TOPIC, null, It.IsAny(), false)) .Returns(new Command { Payload = (ArrayPacket)target.GetBytes() }); var client = nc.Object; var brokers = client.GetRouteInfo(null); Assert.Single(brokers); var broker = brokers[0]; Assert.Equal("10.0.0.1:10911", broker.MasterAddress); Assert.Single(broker.SlaveAddresses); Assert.Equal("10.0.0.2:10911", broker.SlaveAddresses[0]); // Master 地址排在第一位 Assert.Equal("10.0.0.1:10911", broker.Addresses[0]); Assert.Equal(2, broker.Addresses.Length); Assert.True(broker.IsMaster); } } ================================================ FILE: XUnitTestRocketMQ/OrderConsumeTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ; using Xunit; namespace XUnitTestRocketMQ; /// 顺序消费锁定测试 public class OrderConsumeTests { [Fact] [DisplayName("OrderConsume_默认为false")] public void OrderConsume_DefaultFalse() { using var consumer = new Consumer(); Assert.False(consumer.OrderConsume); } [Fact] [DisplayName("OrderConsume_可启用")] public void OrderConsume_CanBeEnabled() { using var consumer = new Consumer { OrderConsume = true }; Assert.True(consumer.OrderConsume); } [Fact] [DisplayName("LockBatchMQAsync_空列表返回空")] public async void LockBatchMQAsync_EmptyList_ReturnsEmpty() { using var consumer = new Consumer(); var result = await consumer.LockBatchMQAsync([]); Assert.Empty(result); } [Fact] [DisplayName("LockBatchMQAsync_Null列表返回空")] public async void LockBatchMQAsync_NullList_ReturnsEmpty() { using var consumer = new Consumer(); var result = await consumer.LockBatchMQAsync(null); Assert.Empty(result); } [Fact] [DisplayName("UnlockBatchMQAsync_空列表不抛异常")] public async void UnlockBatchMQAsync_EmptyList_NoException() { using var consumer = new Consumer(); await consumer.UnlockBatchMQAsync([]); } [Fact] [DisplayName("UnlockBatchMQAsync_Null列表不抛异常")] public async void UnlockBatchMQAsync_NullList_NoException() { using var consumer = new Consumer(); await consumer.UnlockBatchMQAsync(null); } } ================================================ FILE: XUnitTestRocketMQ/PopConsumeTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// Pop消费模式测试 public class PopConsumeTests { [Fact] [DisplayName("PopMessageAsync_Null的BrokerName抛出异常")] public async void PopMessageAsync_NullBrokerName_ThrowsException() { using var consumer = new Consumer(); await Assert.ThrowsAsync(() => consumer.PopMessageAsync(null)); } [Fact] [DisplayName("PopMessageAsync_空BrokerName抛出异常")] public async void PopMessageAsync_EmptyBrokerName_ThrowsException() { using var consumer = new Consumer(); await Assert.ThrowsAsync(() => consumer.PopMessageAsync("")); } [Fact] [DisplayName("PopMessageAsync_可指定queueId参数")] public async void PopMessageAsync_WithQueueId_ThrowsWhenBrokerNameNull() { using var consumer = new Consumer(); // 验证带queueId的重载依然会在brokerName为null时抛出异常 await Assert.ThrowsAsync(() => consumer.PopMessageAsync(null, queueId: 0)); } [Fact] [DisplayName("AckMessageAsync_无Broker连接时返回false")] public async void AckMessageAsync_NoBroker_ReturnsFalse() { using var consumer = new Consumer(); // 未Start,无Broker连接 var result = await consumer.AckMessageAsync("nonexistent", "extra", 0); Assert.False(result); } [Fact] [DisplayName("AckMessageAsync_指定queueId_无Broker连接时返回false")] public async void AckMessageAsync_WithQueueId_NoBroker_ReturnsFalse() { using var consumer = new Consumer(); var result = await consumer.AckMessageAsync("nonexistent", "extra", 0, queueId: 2); Assert.False(result); } [Fact] [DisplayName("AckMessageAsync_传入MessageExt_Null消息抛出异常")] public async void AckMessageAsync_NullMsg_ThrowsException() { using var consumer = new Consumer(); await Assert.ThrowsAsync(() => consumer.AckMessageAsync("broker", (MessageExt)null)); } [Fact] [DisplayName("AckMessageAsync_传入MessageExt_缺少POP_CK属性抛出异常")] public async void AckMessageAsync_MsgWithoutPopCk_ThrowsArgumentException() { using var consumer = new Consumer(); var msg = new MessageExt { QueueId = 1, QueueOffset = 100 }; // 没有设置 PopCheckPoint(POP_CK) await Assert.ThrowsAsync(() => consumer.AckMessageAsync("broker", msg)); } [Fact] [DisplayName("AckMessageAsync_传入MessageExt_无Broker连接时返回false")] public async void AckMessageAsync_WithMsgExt_NoBroker_ReturnsFalse() { using var consumer = new Consumer(); var msg = new MessageExt { QueueId = 1, QueueOffset = 100 }; msg.PopCheckPoint = "100 1700000000000 60000 1 broker-a 1"; var result = await consumer.AckMessageAsync("nonexistent", msg); Assert.False(result); } [Fact] [DisplayName("ChangeInvisibleTimeAsync_无Broker连接时返回false")] public async void ChangeInvisibleTimeAsync_NoBroker_ReturnsFalse() { using var consumer = new Consumer(); var result = await consumer.ChangeInvisibleTimeAsync("nonexistent", "extra", 0, 30000); Assert.False(result); } [Fact] [DisplayName("ChangeInvisibleTimeAsync_指定queueId_无Broker连接时返回false")] public async void ChangeInvisibleTimeAsync_WithQueueId_NoBroker_ReturnsFalse() { using var consumer = new Consumer(); var result = await consumer.ChangeInvisibleTimeAsync("nonexistent", "extra", 0, 30000, queueId: 3); Assert.False(result); } [Fact] [DisplayName("ChangeInvisibleTimeAsync_传入MessageExt_Null消息抛出异常")] public async void ChangeInvisibleTimeAsync_NullMsg_ThrowsException() { using var consumer = new Consumer(); await Assert.ThrowsAsync(() => consumer.ChangeInvisibleTimeAsync("broker", (MessageExt)null, 30000)); } [Fact] [DisplayName("ChangeInvisibleTimeAsync_传入MessageExt_缺少POP_CK属性抛出异常")] public async void ChangeInvisibleTimeAsync_MsgWithoutPopCk_ThrowsArgumentException() { using var consumer = new Consumer(); var msg = new MessageExt { QueueId = 2, QueueOffset = 200 }; // 没有设置 PopCheckPoint(POP_CK) await Assert.ThrowsAsync(() => consumer.ChangeInvisibleTimeAsync("broker", msg, 30000)); } [Fact] [DisplayName("ChangeInvisibleTimeAsync_传入MessageExt_无Broker连接时返回false")] public async void ChangeInvisibleTimeAsync_WithMsgExt_NoBroker_ReturnsFalse() { using var consumer = new Consumer(); var msg = new MessageExt { QueueId = 2, QueueOffset = 200 }; msg.PopCheckPoint = "200 1700000000000 60000 1 broker-a 2"; var result = await consumer.ChangeInvisibleTimeAsync("nonexistent", msg, 30000); Assert.False(result); } [Fact] [DisplayName("MessageExt_PopCheckPoint属性读写正常")] public void MessageExt_PopCheckPoint_GetSet() { var msg = new MessageExt(); Assert.Null(msg.PopCheckPoint); msg.PopCheckPoint = "100 1700000000000 60000 1 broker-a 1"; Assert.Equal("100 1700000000000 60000 1 broker-a 1", msg.PopCheckPoint); Assert.Equal("100 1700000000000 60000 1 broker-a 1", msg.Properties["POP_CK"]); } [Fact] [DisplayName("RequestCode包含Pop消费相关码")] public void RequestCode_ContainsPopCodes() { Assert.Equal(200050, (Int32)RequestCode.POP_MESSAGE); Assert.Equal(200051, (Int32)RequestCode.ACK_MESSAGE); Assert.Equal(200052, (Int32)RequestCode.CHANGE_MESSAGE_INVISIBLETIME); Assert.Equal(200151, (Int32)RequestCode.BATCH_ACK_MESSAGE); } } ================================================ FILE: XUnitTestRocketMQ/ProducerTests.cs ================================================ using Moq; using NewLife; using NewLife.Data; using NewLife.Log; using NewLife.RocketMQ; using Xunit; namespace XUnitTestRocketMQ; public class ProducerTests { [Fact(Skip = "需要RocketMQ服务器支持")] public void CreateTopic() { var set = BasicTest.GetConfig(); var mq = new Producer { //Topic = "nx_test", NameServerAddress = set.NameServer, Log = XTrace.Log, }; mq.Start(); // 创建topic时,start前不能指定topic,让其使用默认TBW102 Assert.Equal("TBW102", mq.Topic); var rs = mq.CreateTopic("nx_test", 2); Assert.True(rs > 0); } [Fact(Skip = "需要RocketMQ服务器支持")] public static void ProduceTest() { var set = BasicTest.GetConfig(); using var mq = new Producer { Topic = "nx_test", NameServerAddress = set.NameServer, Log = XTrace.Log, }; mq.Start(); for (var i = 0; i < 10; i++) { var str = "学无先后达者为师" + i; //var str = Rand.NextString(1337); var sr = mq.Publish(str, "TagA", null); } } } ================================================ FILE: XUnitTestRocketMQ/ProducerTracerTests.cs ================================================ using System; using System.Threading; using NewLife; using NewLife.Log; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ { public class ProducerTracerTests { private const String Topic = "TopicDemo"; private const String Group = "TraceTestGroup"; private const String NameServerAddress = "127.0.0.1:9876"; [Fact(Skip = "需要RocketMQ服务器支持")] public void Producer_And_Consumer_With_Trace_Enabled_Should_Work() { XTrace.UseConsole(); // 使用 ManualResetEvent 来同步测试的完成 var mre = new ManualResetEvent(false); // 2. 创建并启动生产者 var producer = new Producer { Topic = Topic, Group = Group, // 生产者组可以和消费者组不同,这里为了简单使用同一个 NameServerAddress = NameServerAddress, EnableMessageTrace = true, // 启用消息轨迹 Log = XTrace.Log }; producer.Start(); // 3. 发送消息 var messageBody = "Hello, RocketMQ with Message Trace!"; var sendResult = producer.Publish(messageBody); Assert.NotNull(sendResult); Assert.Equal(SendStatus.SendOK, sendResult.Status); XTrace.WriteLine($"消息发送成功: MsgId={sendResult.MsgId}"); // 4. 等待消费者处理消息,设置一个超时时间以防测试挂起 bool consumed = mre.WaitOne(TimeSpan.FromSeconds(300)); // 5. 清理资源 producer.Stop(); // 6. 断言 Assert.True(consumed, "消费者在超时时间内没有收到消息。"); } } } ================================================ FILE: XUnitTestRocketMQ/ProtoTests.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using NewLife.Buffers; using NewLife.RocketMQ.Grpc; using Xunit; namespace XUnitTestRocketMQ; /// Protobuf编解码器测试 public class ProtoTests { #region SpanWriter/SpanReader 基础编解码 [Fact] [DisplayName("Varint_编码解码正确")] public void Varint_RoundTrip() { var buf = new Byte[128]; var writer = new SpanWriter(buf); writer.WriteRawVarint(0); writer.WriteRawVarint(1); writer.WriteRawVarint(127); writer.WriteRawVarint(128); writer.WriteRawVarint(300); writer.WriteRawVarint(UInt64.MaxValue); var reader = new SpanReader(writer.WrittenSpan.ToArray()); Assert.Equal(0UL, reader.ReadRawVarint()); Assert.Equal(1UL, reader.ReadRawVarint()); Assert.Equal(127UL, reader.ReadRawVarint()); Assert.Equal(128UL, reader.ReadRawVarint()); Assert.Equal(300UL, reader.ReadRawVarint()); Assert.Equal(UInt64.MaxValue, reader.ReadRawVarint()); Assert.True(reader.Available <= 0); } [Fact] [DisplayName("Fixed32_编码解码正确")] public void Fixed32_RoundTrip() { var buf = new Byte[64]; var writer = new SpanWriter(buf); writer.WriteRawFixed32(0); writer.WriteRawFixed32(12345); writer.WriteRawFixed32(UInt32.MaxValue); var reader = new SpanReader(writer.WrittenSpan.ToArray()); Assert.Equal(0U, reader.ReadFixed32()); Assert.Equal(12345U, reader.ReadFixed32()); Assert.Equal(UInt32.MaxValue, reader.ReadFixed32()); } [Fact] [DisplayName("Fixed64_编码解码正确")] public void Fixed64_RoundTrip() { var buf = new Byte[64]; var writer = new SpanWriter(buf); writer.WriteRawFixed64(0); writer.WriteRawFixed64(1234567890123456789); writer.WriteRawFixed64(UInt64.MaxValue); var reader = new SpanReader(writer.WrittenSpan.ToArray()); Assert.Equal(0UL, reader.ReadFixed64()); Assert.Equal(1234567890123456789UL, reader.ReadFixed64()); Assert.Equal(UInt64.MaxValue, reader.ReadFixed64()); } [Fact] [DisplayName("String字段_编码解码正确")] public void StringField_RoundTrip() { var buf = new Byte[256]; var writer = new SpanWriter(buf); writer.WriteString(1, "hello"); writer.WriteString(2, "世界"); writer.WriteString(3, ""); // 空字符串不写入 var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); // field 1: string "hello" var (fn1, wt1) = reader.ReadTag(); Assert.Equal(1, fn1); Assert.Equal(2, wt1); // length-delimited Assert.Equal("hello", reader.ReadProtoString()); // field 2: string "世界" var (fn2, wt2) = reader.ReadTag(); Assert.Equal(2, fn2); Assert.Equal(2, wt2); Assert.Equal("世界", reader.ReadProtoString()); // 没有 field 3(空字符串跳过) Assert.True(reader.Available <= 0); } [Fact] [DisplayName("Int32字段_编码解码正确")] public void Int32Field_RoundTrip() { var buf = new Byte[128]; var writer = new SpanWriter(buf); writer.WriteInt32(1, 42); writer.WriteInt32(2, -1); // 负数编码为10字节varint writer.WriteInt32(3, 0); // 0不写入 var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var (fn1, wt1) = reader.ReadTag(); Assert.Equal(1, fn1); Assert.Equal(0, wt1); // varint Assert.Equal(42, reader.ReadProtoInt32()); var (fn2, wt2) = reader.ReadTag(); Assert.Equal(2, fn2); Assert.Equal(-1, reader.ReadProtoInt32()); Assert.True(reader.Available <= 0); } [Fact] [DisplayName("SInt32_ZigZag编码解码正确")] public void SInt32_ZigZag_RoundTrip() { var buf = new Byte[128]; var writer = new SpanWriter(buf); writer.WriteSInt32(1, 0); // 不写入 writer.WriteSInt32(2, 1); writer.WriteSInt32(3, -1); writer.WriteSInt32(4, Int32.MinValue); writer.WriteSInt32(5, Int32.MaxValue); var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); // field 2: sint32 = 1 var (fn2, _) = reader.ReadTag(); Assert.Equal(2, fn2); Assert.Equal(1, reader.ReadSInt32()); // field 3: sint32 = -1 var (fn3, _) = reader.ReadTag(); Assert.Equal(3, fn3); Assert.Equal(-1, reader.ReadSInt32()); // field 4: sint32 = Int32.MinValue var (fn4, _) = reader.ReadTag(); Assert.Equal(4, fn4); Assert.Equal(Int32.MinValue, reader.ReadSInt32()); // field 5: sint32 = Int32.MaxValue var (fn5, _) = reader.ReadTag(); Assert.Equal(5, fn5); Assert.Equal(Int32.MaxValue, reader.ReadSInt32()); Assert.True(reader.Available <= 0); } [Fact] [DisplayName("Bool字段_编码解码正确")] public void BoolField_RoundTrip() { var buf = new Byte[64]; var writer = new SpanWriter(buf); writer.WriteBool(1, true); writer.WriteBool(2, false); // false不写入 var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var (fn1, _) = reader.ReadTag(); Assert.Equal(1, fn1); Assert.True(reader.ReadBool()); Assert.True(reader.Available <= 0); } [Fact] [DisplayName("Bytes字段_编码解码正确")] public void BytesField_RoundTrip() { var testData = new Byte[] { 0x01, 0x02, 0x03, 0xFF }; var buf = new Byte[64]; var writer = new SpanWriter(buf); writer.WriteBytes(1, testData); writer.WriteBytes(2, null); // null不写入 writer.WriteBytes(3, []); // 空不写入 var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var (fn1, wt1) = reader.ReadTag(); Assert.Equal(1, fn1); Assert.Equal(2, wt1); Assert.Equal(testData, reader.ReadProtoBytes()); Assert.True(reader.Available <= 0); } [Fact] [DisplayName("Map字段_编码解码正确")] public void MapField_RoundTrip() { var map = new Dictionary { ["key1"] = "value1", ["key2"] = "value2", }; var buf = new Byte[256]; var writer = new SpanWriter(buf); writer.WriteMap(1, map); var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var result = new Dictionary(); while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); Assert.Equal(1, fn); Assert.Equal(2, wt); var (k, v) = reader.ReadMapEntry(); result[k] = v; } Assert.Equal(2, result.Count); Assert.Equal("value1", result["key1"]); Assert.Equal("value2", result["key2"]); } [Fact] [DisplayName("SkipField_正确跳过未知字段")] public void SkipField_Works() { var buf = new Byte[256]; var writer = new SpanWriter(buf); writer.WriteInt32(1, 42); // varint writer.WriteString(2, "skip"); // length-delimited writer.WriteInt32(3, 99); // varint var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); // 读field 1 var (fn1, wt1) = reader.ReadTag(); Assert.Equal(1, fn1); Assert.Equal(42, reader.ReadProtoInt32()); // 跳过field 2 var (fn2, wt2) = reader.ReadTag(); Assert.Equal(2, fn2); reader.SkipField(wt2); // 读field 3 var (fn3, wt3) = reader.ReadTag(); Assert.Equal(3, fn3); Assert.Equal(99, reader.ReadProtoInt32()); Assert.True(reader.Available <= 0); } #endregion #region Timestamp/Duration [Fact] [DisplayName("Timestamp_编码解码正确")] public void Timestamp_RoundTrip() { var time = new DateTime(2024, 6, 15, 12, 30, 45, DateTimeKind.Utc); var buf = new Byte[128]; var writer = new SpanWriter(buf); writer.WriteTimestamp(1, time); var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var (fn, wt) = reader.ReadTag(); Assert.Equal(1, fn); Assert.Equal(2, wt); var result = reader.ReadTimestamp(); // 允许毫秒级误差(因为Timestamp精度是纳秒/100) Assert.Equal(time.Year, result.Year); Assert.Equal(time.Month, result.Month); Assert.Equal(time.Day, result.Day); Assert.Equal(time.Hour, result.Hour); Assert.Equal(time.Minute, result.Minute); Assert.Equal(time.Second, result.Second); } [Fact] [DisplayName("Duration_编码解码正确")] public void Duration_RoundTrip() { var duration = TimeSpan.FromSeconds(90); var buf = new Byte[128]; var writer = new SpanWriter(buf); writer.WriteDuration(1, duration); var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var (fn, _) = reader.ReadTag(); Assert.Equal(1, fn); var result = reader.ReadDuration(); Assert.Equal(90, (Int32)result.TotalSeconds); } #endregion #region 嵌套消息 [Fact] [DisplayName("嵌套消息_编码解码正确")] public void NestedMessage_RoundTrip() { var resource = new GrpcResource { ResourceNamespace = "test-ns", Name = "test-topic", }; var data = ProtoExtensions.Serialize(resource); var reader = new SpanReader(data); // 手工读取外层结构 var (fn1, _) = reader.ReadTag(); Assert.Equal(1, fn1); Assert.Equal("test-ns", reader.ReadProtoString()); var (fn2, _) = reader.ReadTag(); Assert.Equal(2, fn2); Assert.Equal("test-topic", reader.ReadProtoString()); } [Fact] [DisplayName("嵌套消息_WriteMessage编码解码正确")] public void NestedMessage_WriteMessage_RoundTrip() { var resource = new GrpcResource { ResourceNamespace = "test-ns", Name = "test-topic", }; var buf = new Byte[256]; var writer = new SpanWriter(buf); writer.WriteMessage(1, resource); var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var (fn, wt) = reader.ReadTag(); Assert.Equal(1, fn); Assert.Equal(2, wt); var result = reader.ReadProtoMessage(); Assert.Equal("test-ns", result.ResourceNamespace); Assert.Equal("test-topic", result.Name); } [Fact] [DisplayName("GrpcMessage_完整消息编码解码")] public void GrpcMessage_FullRoundTrip() { var msg = new GrpcMessage { Topic = new GrpcResource { ResourceNamespace = "ns", Name = "topic1" }, SystemProperties = new GrpcSystemProperties { Tag = "tagA", MessageId = "msg-001", MessageType = GrpcMessageType.NORMAL, BornTimestamp = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), BornHost = "localhost", QueueId = 3, QueueOffset = 12345, }, Body = new Byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F }, }; msg.UserProperties["user_key"] = "user_value"; var data = ProtoExtensions.Serialize(msg); var reader = new SpanReader(data); var result = new GrpcMessage(); result.Read(ref reader); Assert.Equal("ns", result.Topic.ResourceNamespace); Assert.Equal("topic1", result.Topic.Name); Assert.Equal("tagA", result.SystemProperties.Tag); Assert.Equal("msg-001", result.SystemProperties.MessageId); Assert.Equal(GrpcMessageType.NORMAL, result.SystemProperties.MessageType); Assert.Equal("localhost", result.SystemProperties.BornHost); Assert.Equal(3, result.SystemProperties.QueueId); Assert.Equal(12345, result.SystemProperties.QueueOffset); Assert.Equal(new Byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F }, result.Body); Assert.Equal("user_value", result.UserProperties["user_key"]); } #endregion #region GrpcClient帧编码 [Fact] [DisplayName("gRPC帧编码_正确")] public void GrpcFrame_Encode() { var data = new Byte[] { 0x01, 0x02, 0x03 }; var frame = GrpcClient.FrameEncode(data); Assert.Equal(8, frame.Length); Assert.Equal(0, frame[0]); // 不压缩 Assert.Equal(0, frame[1]); // 长度高位 Assert.Equal(0, frame[2]); Assert.Equal(0, frame[3]); Assert.Equal(3, frame[4]); // 长度=3 Assert.Equal(0x01, frame[5]); Assert.Equal(0x02, frame[6]); Assert.Equal(0x03, frame[7]); } [Fact] [DisplayName("gRPC帧解码_正确")] public void GrpcFrame_Decode() { var data = new Byte[] { 0x01, 0x02, 0x03 }; var frame = GrpcClient.FrameEncode(data); var decoded = GrpcClient.FrameDecode(frame); Assert.Equal(data, decoded); } [Fact] [DisplayName("gRPC帧_空数据编解码")] public void GrpcFrame_EmptyData() { var frame = GrpcClient.FrameEncode(null); Assert.Equal(5, frame.Length); Assert.Equal(0, frame[4]); // 长度0 var decoded = GrpcClient.FrameDecode(frame); Assert.Empty(decoded); } [Fact] [DisplayName("gRPC帧解码_数据不足返回空")] public void GrpcFrame_Decode_TooShort() { var decoded = GrpcClient.FrameDecode(new Byte[] { 0, 0, 0 }); Assert.Empty(decoded); } #endregion #region 服务消息 [Fact] [DisplayName("QueryRouteRequest_编码解码正确")] public void QueryRouteRequest_RoundTrip() { var request = new QueryRouteRequest { Topic = new GrpcResource { ResourceNamespace = "ns", Name = "test" }, Endpoints = new GrpcEndpoints { Scheme = AddressScheme.IPv4, Addresses = [new GrpcAddress { Host = "127.0.0.1", Port = 8081 }], }, }; var data = ProtoExtensions.Serialize(request); var reader = new SpanReader(data); var result = new QueryRouteRequest(); result.Read(ref reader); Assert.Equal("ns", result.Topic.ResourceNamespace); Assert.Equal("test", result.Topic.Name); Assert.Equal(AddressScheme.IPv4, result.Endpoints.Scheme); Assert.Single(result.Endpoints.Addresses); Assert.Equal("127.0.0.1", result.Endpoints.Addresses[0].Host); Assert.Equal(8081, result.Endpoints.Addresses[0].Port); } [Fact] [DisplayName("SendMessageRequest_编码解码正确")] public void SendMessageRequest_RoundTrip() { var request = new SendMessageRequest(); request.Messages.Add(new GrpcMessage { Topic = new GrpcResource { Name = "topic1" }, Body = new Byte[] { 1, 2, 3 }, SystemProperties = new GrpcSystemProperties { Tag = "tag1", MessageType = GrpcMessageType.NORMAL, }, }); var data = ProtoExtensions.Serialize(request); var reader = new SpanReader(data); var result = new SendMessageRequest(); result.Read(ref reader); Assert.Single(result.Messages); Assert.Equal("topic1", result.Messages[0].Topic.Name); Assert.Equal(new Byte[] { 1, 2, 3 }, result.Messages[0].Body); Assert.Equal("tag1", result.Messages[0].SystemProperties.Tag); } [Fact] [DisplayName("GrpcStatus_编码解码正确")] public void GrpcStatus_RoundTrip() { var status = new GrpcStatus { Code = GrpcCode.OK, Message = "Success", }; var data = ProtoExtensions.Serialize(status); var reader = new SpanReader(data); var result = new GrpcStatus(); result.Read(ref reader); Assert.Equal(GrpcCode.OK, result.Code); Assert.Equal("Success", result.Message); } [Fact] [DisplayName("GrpcMessageQueue_含AcceptMessageTypes编解码")] public void GrpcMessageQueue_WithTypes_RoundTrip() { var mq = new GrpcMessageQueue { Topic = new GrpcResource { Name = "topic1" }, Id = 2, Permission = GrpcPermission.READ_WRITE, Broker = new GrpcBroker { Name = "broker-a", Id = 0, Endpoints = new GrpcEndpoints { Scheme = AddressScheme.IPv4, Addresses = [new GrpcAddress { Host = "10.0.0.1", Port = 8081 }], }, }, AcceptMessageTypes = [GrpcMessageType.NORMAL, GrpcMessageType.DELAY], }; var data = ProtoExtensions.Serialize(mq); var reader = new SpanReader(data); var result = new GrpcMessageQueue(); result.Read(ref reader); Assert.Equal("topic1", result.Topic.Name); Assert.Equal(2, result.Id); Assert.Equal(GrpcPermission.READ_WRITE, result.Permission); Assert.Equal("broker-a", result.Broker.Name); Assert.Equal(2, result.AcceptMessageTypes.Count); Assert.Contains(GrpcMessageType.NORMAL, result.AcceptMessageTypes); Assert.Contains(GrpcMessageType.DELAY, result.AcceptMessageTypes); } #endregion } ================================================ FILE: XUnitTestRocketMQ/ProtocolDataTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// 协议数据结构测试 public class ProtocolDataTests { #region HeartbeatData [Fact] [DisplayName("HeartbeatData_默认属性为null")] public void HeartbeatData_Defaults() { var hb = new HeartbeatData(); Assert.Null(hb.ClientID); Assert.Null(hb.ConsumerDataSet); Assert.Null(hb.ProducerDataSet); } [Fact] [DisplayName("HeartbeatData_设置所有属性")] public void HeartbeatData_SetAllProperties() { var hb = new HeartbeatData { ClientID = "client-001", ConsumerDataSet = [new ConsumerData { GroupName = "CG1" }], ProducerDataSet = [new ProducerData { GroupName = "PG1" }], }; Assert.Equal("client-001", hb.ClientID); Assert.Single(hb.ConsumerDataSet); Assert.Equal("CG1", hb.ConsumerDataSet[0].GroupName); Assert.Single(hb.ProducerDataSet); Assert.Equal("PG1", hb.ProducerDataSet[0].GroupName); } #endregion #region ProducerData [Fact] [DisplayName("ProducerData_默认GroupName")] public void ProducerData_DefaultGroupName() { var pd = new ProducerData(); Assert.Equal("CLIENT_INNER_PRODUCER", pd.GroupName); } [Fact] [DisplayName("ProducerData_可自定义GroupName")] public void ProducerData_CustomGroupName() { var pd = new ProducerData { GroupName = "MY_PRODUCER" }; Assert.Equal("MY_PRODUCER", pd.GroupName); } #endregion #region ConsumerData [Fact] [DisplayName("ConsumerData_默认值")] public void ConsumerData_Defaults() { var cd = new ConsumerData(); Assert.Equal("CONSUME_FROM_LAST_OFFSET", cd.ConsumeFromWhere); Assert.Equal("CONSUME_ACTIVELY", cd.ConsumeType); Assert.Null(cd.GroupName); Assert.Equal("CLUSTERING", cd.MessageModel); Assert.Null(cd.SubscriptionDataSet); Assert.False(cd.UnitMode); } [Fact] [DisplayName("ConsumerData_设置所有属性")] public void ConsumerData_SetAllProperties() { var cd = new ConsumerData { ConsumeFromWhere = "CONSUME_FROM_FIRST_OFFSET", ConsumeType = "CONSUME_PASSIVELY", GroupName = "CG_TEST", MessageModel = "BROADCASTING", UnitMode = true, SubscriptionDataSet = [new SubscriptionData { Topic = "test" }], }; Assert.Equal("CONSUME_FROM_FIRST_OFFSET", cd.ConsumeFromWhere); Assert.Equal("CONSUME_PASSIVELY", cd.ConsumeType); Assert.Equal("CG_TEST", cd.GroupName); Assert.Equal("BROADCASTING", cd.MessageModel); Assert.True(cd.UnitMode); Assert.Single(cd.SubscriptionDataSet); } #endregion #region SubscriptionData [Fact] [DisplayName("SubscriptionData_默认值")] public void SubscriptionData_Defaults() { var sd = new SubscriptionData(); Assert.Null(sd.Topic); Assert.Equal("TAG", sd.ExpressionType); Assert.Equal("*", sd.SubString); Assert.Null(sd.TagsSet); Assert.Null(sd.CodeSet); Assert.False(sd.ClassFilterMode); Assert.Null(sd.FilterClassSource); Assert.True(sd.SubVersion > 0); } [Fact] [DisplayName("SubscriptionData_设置SQL92过滤")] public void SubscriptionData_SQL92Filter() { var sd = new SubscriptionData { Topic = "order_topic", ExpressionType = "SQL92", SubString = "price > 100", }; Assert.Equal("order_topic", sd.Topic); Assert.Equal("SQL92", sd.ExpressionType); Assert.Equal("price > 100", sd.SubString); } [Fact] [DisplayName("SubscriptionData_设置标签集合")] public void SubscriptionData_TagsSet() { var sd = new SubscriptionData { Topic = "test", TagsSet = ["TagA", "TagB", "TagC"], CodeSet = ["1", "2"], }; Assert.Equal(3, sd.TagsSet.Length); Assert.Equal("TagA", sd.TagsSet[0]); Assert.Equal(2, sd.CodeSet.Length); } #endregion #region QueryResult [Fact] [DisplayName("QueryResult_默认值")] public void QueryResult_Defaults() { var qr = new QueryResult(); Assert.Equal(0, qr.IndexLastUpdateTimestamp); Assert.Null(qr.MessageList); } [Fact] [DisplayName("QueryResult_设置属性")] public void QueryResult_SetProperties() { var qr = new QueryResult { IndexLastUpdateTimestamp = 12345, MessageList = [new MessageExt { Topic = "t1" }], }; Assert.Equal(12345, qr.IndexLastUpdateTimestamp); Assert.Single(qr.MessageList); Assert.Equal("t1", qr.MessageList[0].Topic); } #endregion } ================================================ FILE: XUnitTestRocketMQ/PullResultTests.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// PullResult拉取结果测试 public class PullResultTests { #region Read方法 [Fact] [DisplayName("Read_解析所有偏移字段")] public void Read_ParsesAllOffsetFields() { var dic = new Dictionary { ["MinOffset"] = "100", ["MaxOffset"] = "9999", ["NextBeginOffset"] = "500", }; var result = new PullResult(); result.Read(dic); Assert.Equal(100, result.MinOffset); Assert.Equal(9999, result.MaxOffset); Assert.Equal(500, result.NextBeginOffset); } [Fact] [DisplayName("Read_Null字典不抛异常")] public void Read_NullDictionary_NoException() { var result = new PullResult(); result.Read(null); Assert.Equal(0, result.MinOffset); Assert.Equal(0, result.MaxOffset); Assert.Equal(0, result.NextBeginOffset); } [Fact] [DisplayName("Read_空字典不影响属性")] public void Read_EmptyDictionary_NoEffect() { var result = new PullResult(); result.Read(new Dictionary()); Assert.Equal(0, result.MinOffset); } [Fact] [DisplayName("Read_部分字段解析")] public void Read_PartialFields() { var dic = new Dictionary { ["MaxOffset"] = "1000", }; var result = new PullResult(); result.Read(dic); Assert.Equal(0, result.MinOffset); Assert.Equal(1000, result.MaxOffset); Assert.Equal(0, result.NextBeginOffset); } [Fact] [DisplayName("Read_大小写不敏感")] public void Read_CaseInsensitive() { var dic = new Dictionary { ["minoffset"] = "50", ["MAXOFFSET"] = "200", }; var result = new PullResult(); result.Read(dic); Assert.Equal(50, result.MinOffset); Assert.Equal(200, result.MaxOffset); } #endregion #region ToString [Fact] [DisplayName("ToString_包含状态和偏移信息")] public void ToString_ContainsStatusAndOffsets() { var result = new PullResult { Status = PullStatus.Found, MinOffset = 10, MaxOffset = 100, Messages = [new MessageExt(), new MessageExt()] }; var str = result.ToString(); Assert.Contains("Found", str); Assert.Contains("10", str); Assert.Contains("100", str); Assert.Contains("2", str); } [Fact] [DisplayName("ToString_无消息时显示0")] public void ToString_NullMessages_ShowsZero() { var result = new PullResult { Status = PullStatus.NoNewMessage }; var str = result.ToString(); Assert.Contains("NoNewMessage", str); Assert.Contains("0", str); } #endregion #region 枚举 [Fact] [DisplayName("PullStatus_枚举值正确")] public void PullStatus_EnumValues() { Assert.Equal(0, (Int32)PullStatus.Found); Assert.Equal(1, (Int32)PullStatus.NoNewMessage); Assert.Equal(2, (Int32)PullStatus.NoMatchedMessage); Assert.Equal(3, (Int32)PullStatus.OffsetIllegal); Assert.Equal(4, (Int32)PullStatus.Unknown); } #endregion } ================================================ FILE: XUnitTestRocketMQ/QueryMessageTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ; using Xunit; namespace XUnitTestRocketMQ; /// 按Key查询消息测试 public class QueryMessageTests { [Fact] [DisplayName("QueryMessageByKey_Null的Key抛出异常")] public void QueryMessageByKey_NullKey_ThrowsException() { using var producer = new Producer(); Assert.Throws(() => producer.QueryMessageByKey("test", null)); } [Fact] [DisplayName("QueryMessageByKey_空Key抛出异常")] public void QueryMessageByKey_EmptyKey_ThrowsException() { using var producer = new Producer(); Assert.Throws(() => producer.QueryMessageByKey("test", "")); } [Fact(Skip = "需要RocketMQ服务器")] [DisplayName("QueryMessageByKey_按Key查询消息")] public void QueryMessageByKey_Integration() { var set = BasicTest.GetConfig(); using var producer = new Producer { Topic = "nx_test", NameServerAddress = set.NameServer, }; producer.Start(); var msgs = producer.QueryMessageByKey("nx_test", "test_key_123"); // 不做严格断言,仅验证不抛出异常 } } ================================================ FILE: XUnitTestRocketMQ/RequestHeaderTests.cs ================================================ using System; using System.ComponentModel; using System.Linq; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// 请求头GetProperties方法测试 public class RequestHeaderTests { #region SendMessageRequestHeader [Fact] [DisplayName("SendMessageRequestHeader_GetProperties返回所有属性")] public void SendMessageRequestHeader_GetProperties_ReturnsAll() { var header = new SendMessageRequestHeader { ProducerGroup = "PG_TEST", Topic = "test_topic", DefaultTopic = "TBW102", DefaultTopicQueueNums = 4, QueueId = 1, SysFlag = 0, BornTimestamp = 1000L, Flag = 0, Properties = "TAGS\u0001test", ReconsumeTimes = 0, UnitMode = false, ConsumeRetryTimes = 0, Batch = false, BrokerName = "broker-0", }; var dic = header.GetProperties(); Assert.NotNull(dic); Assert.True(dic.Count > 0); // XmlElement(ElementName="a") 映射 Assert.True(dic.ContainsKey("a"), "ProducerGroup应映射为a"); Assert.Equal("PG_TEST", dic["a"]); Assert.True(dic.ContainsKey("b"), "Topic应映射为b"); Assert.Equal("test_topic", dic["b"]); Assert.True(dic.ContainsKey("c"), "DefaultTopic应映射为c"); Assert.Equal("TBW102", dic["c"]); Assert.True(dic.ContainsKey("e"), "QueueId应映射为e"); Assert.True(dic.ContainsKey("i"), "Properties应映射为i"); } [Fact] [DisplayName("SendMessageRequestHeader_默认值GetProperties")] public void SendMessageRequestHeader_DefaultValues_GetProperties() { var header = new SendMessageRequestHeader(); var dic = header.GetProperties(); Assert.NotNull(dic); // 即使默认值,属性也应该被包含 Assert.True(dic.Count >= 10); } #endregion #region PullMessageRequestHeader [Fact] [DisplayName("PullMessageRequestHeader_GetProperties返回camelCase键")] public void PullMessageRequestHeader_GetProperties_CamelCaseKeys() { var header = new PullMessageRequestHeader { ConsumerGroup = "CG_TEST", Topic = "test_topic", Subscription = "*", QueueId = 2, QueueOffset = 100, MaxMsgNums = 32, SuspendTimeoutMillis = 20000, }; var dic = header.GetProperties(); Assert.NotNull(dic); Assert.True(dic.ContainsKey("consumerGroup"), "应转为camelCase"); Assert.True(dic.ContainsKey("topic")); Assert.True(dic.ContainsKey("subscription")); Assert.True(dic.ContainsKey("queueId")); Assert.True(dic.ContainsKey("queueOffset")); Assert.True(dic.ContainsKey("maxMsgNums")); Assert.True(dic.ContainsKey("suspendTimeoutMillis")); } [Fact] [DisplayName("PullMessageRequestHeader_默认ExpressionType为TAG")] public void PullMessageRequestHeader_DefaultExpressionType() { var header = new PullMessageRequestHeader(); Assert.Equal("TAG", header.ExpressionType); Assert.Equal("*", header.Subscription); Assert.Equal(20000, header.SuspendTimeoutMillis); } [Fact] [DisplayName("PullMessageRequestHeader_GetProperties值为字符串")] public void PullMessageRequestHeader_GetProperties_ValuesAreStrings() { var header = new PullMessageRequestHeader { QueueId = 5, MaxMsgNums = 32, }; var dic = header.GetProperties(); // PullMessageRequestHeader 的 GetProperties 会 + "" 转为字符串 Assert.IsType(dic["queueId"]); Assert.Equal("5", dic["queueId"]); } #endregion #region EndTransactionRequestHeader [Fact] [DisplayName("EndTransactionRequestHeader_GetProperties返回camelCase键")] public void EndTransactionRequestHeader_GetProperties_CamelCaseKeys() { var header = new EndTransactionRequestHeader { ProducerGroup = "PG_TX", TranStateTableOffset = 100, CommitLogOffset = 200, CommitOrRollback = 1, FromTransactionCheck = true, MsgId = "MSG001", TransactionId = "TX001", }; var dic = header.GetProperties(); Assert.NotNull(dic); Assert.True(dic.ContainsKey("producerGroup")); Assert.True(dic.ContainsKey("tranStateTableOffset")); Assert.True(dic.ContainsKey("commitLogOffset")); Assert.True(dic.ContainsKey("commitOrRollback")); Assert.True(dic.ContainsKey("fromTransactionCheck")); Assert.True(dic.ContainsKey("msgId")); Assert.True(dic.ContainsKey("transactionId")); Assert.Equal("PG_TX", dic["producerGroup"]); Assert.Equal("TX001", dic["transactionId"]); } [Fact] [DisplayName("EndTransactionRequestHeader_布尔值序列化")] public void EndTransactionRequestHeader_BooleanSerialization() { var header = new EndTransactionRequestHeader { FromTransactionCheck = true, }; var dic = header.GetProperties(); // 值是原始 Boolean 对象(非字符串),且为 true Assert.True(dic.ContainsKey("fromTransactionCheck")); var fromTransactionCheck = Assert.IsType(dic["fromTransactionCheck"]); Assert.True(fromTransactionCheck); } #endregion } ================================================ FILE: XUnitTestRocketMQ/RequestReplyTests.cs ================================================ using System; using System.Threading.Tasks; using NewLife; using NewLife.Log; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// Request-Reply 特性测试 public class RequestReplyTests { [Fact(Skip = "需要RocketMQ服务器支持")] public void RequestSyncTest() { var set = BasicTest.GetConfig(); // 创建生产者 using var producer = new Producer { Topic = "nx_request_test", NameServerAddress = set.NameServer, Log = XTrace.Log, }; producer.Start(); // 创建消费者 using var consumer = new Consumer { Topic = "nx_request_test", Group = "nx_request_group", NameServerAddress = set.NameServer, Log = XTrace.Log, FromLastOffset = true, }; // 消费者处理请求并返回回复 consumer.OnConsume = (q, ms) => { foreach (var item in ms) { XTrace.WriteLine("收到请求: {0}", item.BodyString); // 如果是请求消息,发送回复 if (!String.IsNullOrEmpty(item.CorrelationId)) { var replyBody = $"Reply to: {item.BodyString}"; consumer.SendReply(item, replyBody); } } return true; }; consumer.Start(); // 发送请求并等待响应 var requestBody = "Hello, this is a request!"; var response = producer.Request(requestBody, 5000); Assert.NotNull(response); Assert.Contains("Reply to:", response.BodyString); XTrace.WriteLine("收到响应: {0}", response.BodyString); } [Fact(Skip = "需要RocketMQ服务器支持")] public async Task RequestAsyncTest() { var set = BasicTest.GetConfig(); // 创建生产者 using var producer = new Producer { Topic = "nx_request_test_async", NameServerAddress = set.NameServer, Log = XTrace.Log, }; producer.Start(); // 创建消费者 using var consumer = new Consumer { Topic = "nx_request_test_async", Group = "nx_request_async_group", NameServerAddress = set.NameServer, Log = XTrace.Log, FromLastOffset = true, }; // 消费者异步处理请求并返回回复 consumer.OnConsumeAsync = async (q, ms, ct) => { foreach (var item in ms) { XTrace.WriteLine("收到请求: {0}", item.BodyString); // 如果是请求消息,发送回复 if (!String.IsNullOrEmpty(item.CorrelationId)) { var replyBody = $"Async Reply to: {item.BodyString}"; await consumer.SendReplyAsync(item, replyBody, ct).ConfigureAwait(false); } } return true; }; consumer.Start(); // 异步发送请求并等待响应 var requestBody = "Hello, this is an async request!"; var response = await producer.RequestAsync(requestBody, 5000).ConfigureAwait(false); Assert.NotNull(response); Assert.Contains("Async Reply to:", response.BodyString); XTrace.WriteLine("收到响应: {0}", response.BodyString); } [Fact(Skip = "需要RocketMQ服务器支持")] public async Task RequestTimeoutTest() { var set = BasicTest.GetConfig(); // 创建生产者 using var producer = new Producer { Topic = "nx_request_timeout_test", NameServerAddress = set.NameServer, Log = XTrace.Log, RequestTimeout = 1000, // 1秒超时 }; producer.Start(); // 不启动消费者,请求应该超时 // 发送请求,期望超时 var requestBody = "This should timeout"; await Assert.ThrowsAsync(async () => { await producer.RequestAsync(requestBody).ConfigureAwait(false); }); XTrace.WriteLine("请求超时测试通过"); } [Fact] public void MessagePropertiesTest() { // 测试消息属性 var message = new Message { Topic = "test_topic", ReplyToClient = "client_123", CorrelationId = "corr_456", MessageType = "REQUEST", RequestTimeout = 3000 }; message.SetBody("test body"); Assert.Equal("client_123", message.ReplyToClient); Assert.Equal("corr_456", message.CorrelationId); Assert.Equal("REQUEST", message.MessageType); Assert.Equal(3000, message.RequestTimeout); // 测试属性序列化 var props = message.GetProperties(); Assert.Contains("REPLY_TO_CLIENT", props); Assert.Contains("CORRELATION_ID", props); Assert.Contains("MSG_TYPE", props); Assert.Contains("REQUEST_TIMEOUT", props); } } ================================================ FILE: XUnitTestRocketMQ/ResponseExceptionTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// ResponseException响应异常测试 public class ResponseExceptionTests { [Fact] [DisplayName("构造函数_设置Code和Message")] public void Constructor_SetsCodeAndMessage() { var ex = new ResponseException(ResponseCode.SYSTEM_ERROR, "test error"); Assert.Equal(ResponseCode.SYSTEM_ERROR, ex.Code); Assert.Contains("SYSTEM_ERROR", ex.Message); Assert.Contains("test error", ex.Message); } [Fact] [DisplayName("构造函数_不同ResponseCode")] public void Constructor_DifferentCodes() { var ex1 = new ResponseException(ResponseCode.SUCCESS, "ok"); var ex2 = new ResponseException(ResponseCode.TOPIC_NOT_EXIST, "topic missing"); var ex3 = new ResponseException(ResponseCode.NO_PERMISSION, "denied"); Assert.Equal(ResponseCode.SUCCESS, ex1.Code); Assert.Equal(ResponseCode.TOPIC_NOT_EXIST, ex2.Code); Assert.Equal(ResponseCode.NO_PERMISSION, ex3.Code); } [Fact] [DisplayName("异常可以被捕获")] public void Exception_CanBeCaught() { try { throw new ResponseException(ResponseCode.SYSTEM_ERROR, "Test"); } catch (ResponseException ex) { Assert.Equal(ResponseCode.SYSTEM_ERROR, ex.Code); return; } Assert.Fail("异常未被捕获"); } [Fact] [DisplayName("异常继承自Exception")] public void Exception_InheritsFromException() { var ex = new ResponseException(ResponseCode.SUCCESS, "msg"); Assert.IsAssignableFrom(ex); } [Fact] [DisplayName("Null消息不抛异常")] public void Constructor_NullMessage_NoThrow() { var ex = new ResponseException(ResponseCode.SUCCESS, null); Assert.Equal(ResponseCode.SUCCESS, ex.Code); Assert.NotNull(ex.Message); } } ================================================ FILE: XUnitTestRocketMQ/RetryTests.cs ================================================ using System; using System.ComponentModel; using System.Threading.Tasks; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// 消费重试功能测试 public class RetryTests { [Fact] [DisplayName("MaxReconsumeTimes_默认值为16")] public void MaxReconsumeTimes_DefaultValue() { using var consumer = new Consumer(); Assert.Equal(16, consumer.MaxReconsumeTimes); } [Fact] [DisplayName("EnableRetry_默认启用")] public void EnableRetry_DefaultTrue() { using var consumer = new Consumer(); Assert.True(consumer.EnableRetry); } [Fact] [DisplayName("RetryDelayLevel_默认为0")] public void RetryDelayLevel_DefaultZero() { using var consumer = new Consumer(); Assert.Equal(0, consumer.RetryDelayLevel); } [Fact] [DisplayName("MaxReconsumeTimes_可自定义")] public void MaxReconsumeTimes_CanBeCustomized() { using var consumer = new Consumer { MaxReconsumeTimes = 5, }; Assert.Equal(5, consumer.MaxReconsumeTimes); } [Fact] [DisplayName("EnableRetry_可禁用")] public void EnableRetry_CanBeDisabled() { using var consumer = new Consumer { EnableRetry = false, }; Assert.False(consumer.EnableRetry); } [Fact] [DisplayName("SendMessageBack_Null消息抛出异常")] public async Task SendMessageBack_NullMessage_ThrowsException() { using var consumer = new Consumer(); await Assert.ThrowsAsync(() => consumer.SendMessageBackAsync(null)); } } ================================================ FILE: XUnitTestRocketMQ/SQL92FilterTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// SQL92过滤功能测试 public class SQL92FilterTests { [Fact] [DisplayName("ExpressionType_默认值为TAG")] public void ExpressionType_DefaultIsTAG() { using var consumer = new Consumer(); Assert.Equal("TAG", consumer.ExpressionType); } [Fact] [DisplayName("ExpressionType_可设置为SQL92")] public void ExpressionType_CanSetToSQL92() { using var consumer = new Consumer { ExpressionType = "SQL92", Subscription = "age > 18 AND region = 'hangzhou'", }; Assert.Equal("SQL92", consumer.ExpressionType); Assert.Contains("age > 18", consumer.Subscription); } [Fact] [DisplayName("PullMessageRequestHeader_ExpressionType默认TAG")] public void PullHeader_ExpressionType_Default() { var header = new PullMessageRequestHeader(); Assert.Equal("TAG", header.ExpressionType); } [Fact] [DisplayName("PullMessageRequestHeader_ExpressionType可设为SQL92")] public void PullHeader_ExpressionType_SQL92() { var header = new PullMessageRequestHeader { ExpressionType = "SQL92", Subscription = "price BETWEEN 10 AND 100", }; Assert.Equal("SQL92", header.ExpressionType); Assert.Equal("price BETWEEN 10 AND 100", header.Subscription); } } ================================================ FILE: XUnitTestRocketMQ/SendResultTests.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// SendResult发送结果测试 public class SendResultTests { #region Read方法 [Fact] [DisplayName("Read_解析所有字段")] public void Read_ParsesAllFields() { var dic = new Dictionary { ["MsgId"] = "0A0A0A0A00002A9F0000000000000001", ["OffsetMsgId"] = "0A0A0A0A00002A9F0000000000000002", ["QueueOffset"] = "12345", ["TransactionId"] = "TX001", ["RegionId"] = "DefaultRegion", }; var result = new SendResult(); result.Read(dic); Assert.Equal("0A0A0A0A00002A9F0000000000000001", result.MsgId); Assert.Equal("0A0A0A0A00002A9F0000000000000002", result.OffsetMsgId); Assert.Equal(12345, result.QueueOffset); Assert.Equal("TX001", result.TransactionId); Assert.Equal("DefaultRegion", result.RegionId); } [Fact] [DisplayName("Read_MSG_REGION设置RegionId")] public void Read_MsgRegion_SetsRegionId() { var dic = new Dictionary { ["MSG_REGION"] = "us-east-1", }; var result = new SendResult(); result.Read(dic); Assert.Equal("us-east-1", result.RegionId); } [Fact] [DisplayName("Read_Null字典不抛异常")] public void Read_NullDictionary_NoException() { var result = new SendResult(); result.Read(null); Assert.Null(result.MsgId); Assert.Equal(0, result.QueueOffset); } [Fact] [DisplayName("Read_空字典不影响属性")] public void Read_EmptyDictionary_NoEffect() { var result = new SendResult(); result.Read(new Dictionary()); Assert.Null(result.MsgId); Assert.Null(result.OffsetMsgId); Assert.Equal(0, result.QueueOffset); } [Fact] [DisplayName("Read_部分字段解析")] public void Read_PartialFields() { var dic = new Dictionary { ["MsgId"] = "ABC123", }; var result = new SendResult(); result.Read(dic); Assert.Equal("ABC123", result.MsgId); Assert.Null(result.OffsetMsgId); Assert.Null(result.TransactionId); } [Fact] [DisplayName("Read_大小写不敏感")] public void Read_CaseInsensitive() { var dic = new Dictionary { ["msgid"] = "LOWER_ID", ["QUEUEOFFSET"] = "999", }; var result = new SendResult(); result.Read(dic); Assert.Equal("LOWER_ID", result.MsgId); Assert.Equal(999, result.QueueOffset); } #endregion #region ToString [Fact] [DisplayName("ToString_包含所有关键信息")] public void ToString_ContainsAllInfo() { var result = new SendResult { Status = SendStatus.SendOK, MsgId = "MSG001", OffsetMsgId = "OFFSET001", QueueOffset = 42, Queue = new MessageQueue { BrokerName = "broker-a", QueueId = 3 } }; var str = result.ToString(); Assert.Contains("SendOK", str); Assert.Contains("MSG001", str); Assert.Contains("OFFSET001", str); Assert.Contains("42", str); } #endregion #region 属性 [Fact] [DisplayName("SendStatus_枚举值正确")] public void SendStatus_EnumValues() { Assert.Equal(0, (Int32)SendStatus.SendOK); Assert.Equal(1, (Int32)SendStatus.FlushDiskTimeout); Assert.Equal(2, (Int32)SendStatus.FlushSlaveTimeout); Assert.Equal(3, (Int32)SendStatus.SlaveNotAvailable); Assert.Equal(4, (Int32)SendStatus.SendError); } #endregion } ================================================ FILE: XUnitTestRocketMQ/SpanRefactorTests.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Text; using NewLife; using NewLife.Buffers; using NewLife.Data; using NewLife.RocketMQ; using NewLife.RocketMQ.Grpc; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// SpanReader/SpanWriter重构验证测试 /// /// 验证 Command、MessageExt、ProtoExtensions 使用 SpanReader/SpanWriter 重构后的正确性。 /// 包含:二进制协议往返测试、边界条件测试、跨框架兼容性测试。 /// [DisplayName("SpanReader/SpanWriter重构测试")] public class SpanRefactorTests { #region Command 二进制协议测试 [Fact] [DisplayName("Command_RocketMQ二进制格式_写入后读取一致")] public void Command_RocketMQ_WriteRead_RoundTrip() { // 构造一个Command,使用ROCKETMQ序列化写入后再读取 var cmd = new Command { Header = new Header { Code = 34, Flag = 0, Language = "DOTNET", Version = MQVersion.V4_8_0, Opaque = 100, SerializeTypeCurrentRPC = "ROCKETMQ", Remark = "Test", } }; cmd.Header.GetExtFields()["key1"] = "value1"; cmd.Header.GetExtFields()["key2"] = "value2"; // 写入到流 var ms = new MemoryStream(); cmd.Write(ms, null); ms.Position = 0; // 读取 var cmd2 = new Command(); var ok = cmd2.Read(ms); Assert.True(ok); Assert.Equal(cmd.Header.Code, cmd2.Header.Code); Assert.Equal(cmd.Header.Flag, cmd2.Header.Flag); Assert.Equal(cmd.Header.Language, cmd2.Header.Language); Assert.Equal(cmd.Header.Version, cmd2.Header.Version); Assert.Equal(cmd.Header.Opaque, cmd2.Header.Opaque); Assert.Equal(cmd.Header.Remark, cmd2.Header.Remark); var ext = cmd2.Header.GetExtFields(); Assert.Equal(2, ext.Count); Assert.Equal("value1", ext["key1"]); Assert.Equal("value2", ext["key2"]); } [Fact] [DisplayName("Command_RocketMQ二进制格式_无备注无扩展字段")] public void Command_RocketMQ_NoRemarkNoExt_RoundTrip() { var cmd = new Command { Header = new Header { Code = 0, Flag = 1, Language = "JAVA", Version = MQVersion.V5_2_0, Opaque = 0, SerializeTypeCurrentRPC = "ROCKETMQ", } }; var ms = new MemoryStream(); cmd.Write(ms, null); ms.Position = 0; var cmd2 = new Command(); var ok = cmd2.Read(ms); Assert.True(ok); Assert.Equal(0, cmd2.Header.Code); Assert.Equal(1, cmd2.Header.Flag); Assert.Null(cmd2.Header.Remark); var ext = cmd2.Header.GetExtFields(); Assert.Empty(ext); } [Fact] [DisplayName("Command_带Body的消息_写入读取一致")] public void Command_WithPayload_RoundTrip() { var body = Encoding.UTF8.GetBytes("{\"test\":\"hello\"}"); var cmd = new Command { Header = new Header { Code = 310, Flag = 0, Language = "DOTNET", Version = MQVersion.V4_8_0, Opaque = 1, SerializeTypeCurrentRPC = "ROCKETMQ", Remark = "SEND", }, Payload = new ArrayPacket(body), }; var ms = new MemoryStream(); cmd.Write(ms, null); ms.Position = 0; var cmd2 = new Command(); var ok = cmd2.Read(ms); Assert.True(ok); Assert.Equal(310, cmd2.Header.Code); var pk = cmd2.Payload; Assert.NotNull(pk); Assert.Equal("{\"test\":\"hello\"}", pk.ToStr()); } [Fact] [DisplayName("Command_中文备注_SpanWriter编码正确")] public void Command_ChineseRemark_RoundTrip() { var cmd = new Command { Header = new Header { Code = 100, Flag = 0, Language = "DOTNET", Version = MQVersion.V4_8_0, Opaque = 5, SerializeTypeCurrentRPC = "ROCKETMQ", Remark = "测试备注", } }; var ms = new MemoryStream(); cmd.Write(ms, null); ms.Position = 0; var cmd2 = new Command(); var ok = cmd2.Read(ms); Assert.True(ok); Assert.Equal("测试备注", cmd2.Header.Remark); } #endregion #region MessageExt SpanReader解码测试 [Fact] [DisplayName("MessageExt_5x消息ID_SpanWriter创建SpanReader解析往返")] public void MessageExt_5xId_CreateParse_RoundTrip() { var mac = new Byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 }; var pid = 54321; var counter = 999999; var id = MessageExt.CreateMessageId5x(2, mac, pid, counter); Assert.NotNull(id); Assert.Equal(32, id.Length); Assert.StartsWith("01", id); var ok = MessageExt.TryParseMessageId5x(id, out var ver, out var parsedMac, out var parsedPid, out var parsedCounter); Assert.True(ok); Assert.Equal(2, ver); Assert.Equal(mac, parsedMac); Assert.Equal(pid, parsedPid); Assert.Equal(counter, parsedCounter); } [Fact] [DisplayName("MessageExt_5xID_空MAC使用随机字节")] public void MessageExt_5xId_NullMac_UsesRandom() { var id1 = MessageExt.CreateMessageId5x(1, null, 100, 200); var id2 = MessageExt.CreateMessageId5x(1, null, 100, 200); // 两次生成应不同(随机MAC) Assert.NotNull(id1); Assert.NotNull(id2); Assert.Equal(32, id1.Length); Assert.Equal(32, id2.Length); // 前缀和版本相同 Assert.StartsWith("0101", id1); Assert.StartsWith("0101", id2); // MAC部分不同(极小概率相同) Assert.NotEqual(id1, id2); } [Fact] [DisplayName("MessageExt_IsMessageId5x_正确识别5x格式")] public void MessageExt_IsMessageId5x_CorrectDetection() { var mac = new Byte[] { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF }; var id5x = MessageExt.CreateMessageId5x(1, mac, 1000, 2000); Assert.True(MessageExt.IsMessageId5x(id5x)); // 4.x格式(前缀不是01) Assert.False(MessageExt.IsMessageId5x("AABBCCDD00001111000000000000FFFF")); // 长度不对 Assert.False(MessageExt.IsMessageId5x("0101AABB")); // null Assert.False(MessageExt.IsMessageId5x(null)); } #endregion #region ProtoExtensions SpanReader/SpanWriter重构测试 [Fact] [DisplayName("SpanWriter_Fixed32_使用扩展方法正确编解码")] public void SpanWriter_Fixed32_ExtensionMethod() { var buf = new Byte[128]; var writer = new SpanWriter(buf); writer.WriteFixed32(1, 0x12345678); writer.WriteFixed32(2, UInt32.MaxValue); writer.WriteFixed32(3, 1); var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var (fn1, wt1) = reader.ReadTag(); Assert.Equal(1, fn1); Assert.Equal(5, wt1); // wireType 5 = 32-bit Assert.Equal(0x12345678U, reader.ReadFixed32()); var (fn2, wt2) = reader.ReadTag(); Assert.Equal(2, fn2); Assert.Equal(UInt32.MaxValue, reader.ReadFixed32()); var (fn3, _) = reader.ReadTag(); Assert.Equal(3, fn3); Assert.Equal(1U, reader.ReadFixed32()); Assert.True(reader.Available <= 0); } [Fact] [DisplayName("SpanWriter_Fixed64_使用扩展方法正确编解码")] public void SpanWriter_Fixed64_ExtensionMethod() { var buf = new Byte[128]; var writer = new SpanWriter(buf); writer.WriteFixed64(1, 0x123456789ABCDEF0); var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var (fn1, wt1) = reader.ReadTag(); Assert.Equal(1, fn1); Assert.Equal(1, wt1); // wireType 1 = 64-bit Assert.Equal(0x123456789ABCDEF0UL, reader.ReadFixed64()); Assert.True(reader.Available <= 0); } [Fact] [DisplayName("SpanWriter_Float_编码解码正确")] public void SpanWriter_Float_RoundTrip() { var buf = new Byte[128]; var writer = new SpanWriter(buf); writer.WriteFloat(1, 3.14f); writer.WriteFloat(2, -1.5f); writer.WriteFloat(3, Single.MaxValue); writer.WriteFloat(4, Single.MinValue); var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var (fn1, _) = reader.ReadTag(); Assert.Equal(1, fn1); Assert.Equal(3.14f, reader.ReadFloat()); var (fn2, _) = reader.ReadTag(); Assert.Equal(2, fn2); Assert.Equal(-1.5f, reader.ReadFloat()); var (fn3, _) = reader.ReadTag(); Assert.Equal(3, fn3); Assert.Equal(Single.MaxValue, reader.ReadFloat()); var (fn4, _) = reader.ReadTag(); Assert.Equal(4, fn4); Assert.Equal(Single.MinValue, reader.ReadFloat()); Assert.True(reader.Available <= 0); } [Fact] [DisplayName("SpanWriter_Double_编码解码正确")] public void SpanWriter_Double_RoundTrip() { var buf = new Byte[128]; var writer = new SpanWriter(buf); writer.WriteDouble(1, 3.141592653589793); writer.WriteDouble(2, Double.MaxValue); writer.WriteDouble(3, Double.Epsilon); var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var (fn1, _) = reader.ReadTag(); Assert.Equal(1, fn1); Assert.Equal(3.141592653589793, reader.ReadProtoDouble()); var (fn2, _) = reader.ReadTag(); Assert.Equal(2, fn2); Assert.Equal(Double.MaxValue, reader.ReadProtoDouble()); var (fn3, _) = reader.ReadTag(); Assert.Equal(3, fn3); Assert.Equal(Double.Epsilon, reader.ReadProtoDouble()); Assert.True(reader.Available <= 0); } [Fact] [DisplayName("SpanWriter_Timestamp_编码解码正确")] public void SpanWriter_Timestamp_RoundTrip() { var buf = new Byte[128]; var writer = new SpanWriter(buf); var time = new DateTime(2025, 6, 15, 12, 30, 45, DateTimeKind.Utc); writer.WriteTimestamp(1, time); var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var (fn1, _) = reader.ReadTag(); Assert.Equal(1, fn1); var parsed = reader.ReadTimestamp(); Assert.Equal(time.Year, parsed.Year); Assert.Equal(time.Month, parsed.Month); Assert.Equal(time.Day, parsed.Day); Assert.Equal(time.Hour, parsed.Hour); Assert.Equal(time.Minute, parsed.Minute); Assert.Equal(time.Second, parsed.Second); } [Fact] [DisplayName("SpanWriter_Duration_编码解码正确")] public void SpanWriter_Duration_RoundTrip() { var buf = new Byte[128]; var writer = new SpanWriter(buf); var duration = TimeSpan.FromSeconds(3600) + TimeSpan.FromMilliseconds(500); writer.WriteDuration(1, duration); var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var (fn1, _) = reader.ReadTag(); Assert.Equal(1, fn1); var parsed = reader.ReadDuration(); Assert.Equal(3600, (Int32)parsed.TotalSeconds); Assert.Equal(500, parsed.Milliseconds); } [Fact] [DisplayName("SpanWriter_嵌套消息_子缓冲区编解码")] public void SpanWriter_NestedMessage_SubBuffer_RoundTrip() { var resource = new GrpcResource { Name = "test_topic", ResourceNamespace = "ns1", }; var buf = new Byte[256]; var writer = new SpanWriter(buf); writer.WriteMessage(1, resource); var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var (fn1, wt1) = reader.ReadTag(); Assert.Equal(1, fn1); Assert.Equal(2, wt1); // length-delimited var parsed = reader.ReadProtoMessage(); Assert.Equal("test_topic", parsed.Name); Assert.Equal("ns1", parsed.ResourceNamespace); } [Fact] [DisplayName("SpanWriter_Map字段_多个entry编解码")] public void SpanWriter_Map_MultiEntry_RoundTrip() { var map = new Dictionary { ["host"] = "10.0.0.1", ["port"] = "8080", ["env"] = "production", }; var buf = new Byte[512]; var writer = new SpanWriter(buf); writer.WriteMap(1, map); var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var result = new Dictionary(); while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; Assert.Equal(1, fn); Assert.Equal(2, wt); var (k, v) = reader.ReadMapEntry(); result[k] = v; } Assert.Equal(3, result.Count); Assert.Equal("10.0.0.1", result["host"]); Assert.Equal("8080", result["port"]); Assert.Equal("production", result["env"]); } [Fact] [DisplayName("SpanWriter_RepeatedString_多值编解码")] public void SpanWriter_RepeatedString_RoundTrip() { var values = new List { "alpha", "beta", "gamma" }; var buf = new Byte[256]; var writer = new SpanWriter(buf); writer.WriteRepeatedString(1, values); var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var result = new List(); while (reader.Available > 0) { var (fn, _) = reader.ReadTag(); if (fn == 0) break; Assert.Equal(1, fn); result.Add(reader.ReadProtoString()); } Assert.Equal(values, result); } #endregion #region 边界测试 [Fact] [DisplayName("SpanReader_读取超出边界_抛出异常")] public void SpanReader_ReadBeyondLimit_ThrowsException() { var data = new Byte[] { 0x01, 0x02 }; var reader = new SpanReader(data); // 读取2字节OK reader.ReadBytes(2); // 再读1字节应失败 var thrown = false; try { reader.ReadBytes(1); } catch { thrown = true; } Assert.True(thrown); } [Fact] [DisplayName("SpanReader_空数据_Available为0")] public void SpanReader_EmptyData_AvailableZero() { var reader = new SpanReader([]); Assert.True(reader.Available <= 0); } [Fact] [DisplayName("ProtoExtensions_Serialize_空消息返回空")] public void ProtoExtensions_Serialize_NullReturnsEmpty() { var data = ProtoExtensions.Serialize(null); Assert.Empty(data); } [Fact] [DisplayName("SpanWriter_Int32负数_10字节varint编码")] public void SpanWriter_NegativeInt32_10ByteVarint() { var buf = new Byte[64]; var writer = new SpanWriter(buf); writer.WriteInt32(1, -1); var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var (fn, _) = reader.ReadTag(); Assert.Equal(1, fn); Assert.Equal(-1, reader.ReadProtoInt32()); Assert.True(reader.Available <= 0); } [Fact] [DisplayName("SpanWriter_SInt64_ZigZag边界值")] public void SpanWriter_SInt64_ZigZag_BoundaryValues() { var buf = new Byte[128]; var writer = new SpanWriter(buf); writer.WriteSInt64(1, Int64.MinValue); writer.WriteSInt64(2, Int64.MaxValue); writer.WriteSInt64(3, -1); writer.WriteSInt64(4, 1); var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var (fn1, _) = reader.ReadTag(); Assert.Equal(1, fn1); Assert.Equal(Int64.MinValue, reader.ReadSInt64()); var (fn2, _) = reader.ReadTag(); Assert.Equal(2, fn2); Assert.Equal(Int64.MaxValue, reader.ReadSInt64()); var (fn3, _) = reader.ReadTag(); Assert.Equal(3, fn3); Assert.Equal(-1L, reader.ReadSInt64()); var (fn4, _) = reader.ReadTag(); Assert.Equal(4, fn4); Assert.Equal(1L, reader.ReadSInt64()); Assert.True(reader.Available <= 0); } [Fact] [DisplayName("SpanReader_SkipField_未知wireType抛异常")] public void SpanReader_SkipField_UnknownWireType_Throws() { var reader = new SpanReader(new Byte[] { 0x08, 0x01 }); // field 1, varint, value 1 reader.ReadTag(); var thrown = false; try { reader.SkipField(3); // wireType 3 is deprecated/unknown } catch (InvalidDataException) { thrown = true; } Assert.True(thrown); } #endregion #region 综合场景测试 [Fact] [DisplayName("SpanWriter_各种字段类型_完整编解码")] public void SpanWriter_CompleteMessage_RoundTrip() { var buf = new Byte[256]; var writer = new SpanWriter(buf); // 写入各种类型的字段 writer.WriteString(1, "TAG_test"); // tag writer.WriteString(2, "key_123"); // keys writer.WriteString(3, "msg_001"); // message_id writer.WriteString(4, "body_crc"); // body_digest.checksum writer.WriteEnum(5, (Int32)GrpcMessageType.NORMAL); writer.WriteTimestamp(6, new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc)); writer.WriteInt32(7, 3); // born_host.port writer.WriteInt32(8, 0); // queue_id var data = writer.WrittenSpan.ToArray(); Assert.True(data.Length > 0); // 确认能正确读取 var reader = new SpanReader(data); while (reader.Available > 0) { var (fn, wt) = reader.ReadTag(); if (fn == 0) break; reader.SkipField(wt); } } [Fact] [DisplayName("SpanWriter_PackedEnum_编解码")] public void SpanWriter_PackedEnum_RoundTrip() { var enums = new List { 1, 2, 4, 8, 16 }; var buf = new Byte[128]; var writer = new SpanWriter(buf); writer.WritePackedEnum(1, enums); var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); var (fn, wt) = reader.ReadTag(); Assert.Equal(1, fn); Assert.Equal(2, wt); // packed = length-delimited // 读取packed数据 var packedLen = (Int32)reader.ReadRawVarint(); var packedData = reader.ReadBytes(packedLen).ToArray(); var subReader = new SpanReader(packedData); var result = new List(); while (subReader.Available > 0) { result.Add((Int32)subReader.ReadRawVarint()); } Assert.Equal(enums, result); } [Fact] [DisplayName("SpanWriter_大数据量写入_Serialize正确处理")] public void SpanWriter_LargeData_SerializeHandles() { var resource = new GrpcResource { ResourceNamespace = new String('N', 500), Name = new String('T', 500), }; var data = ProtoExtensions.Serialize(resource); Assert.True(data.Length > 1000); var reader = new SpanReader(data); var result = new GrpcResource(); result.Read(ref reader); Assert.Equal(resource.ResourceNamespace, result.ResourceNamespace); Assert.Equal(resource.Name, result.Name); } [Fact] [DisplayName("SpanReader_SkipField_各线路类型正确跳过")] public void SpanReader_SkipField_AllWireTypes() { var buf = new Byte[256]; var writer = new SpanWriter(buf); writer.WriteInt32(1, 42); // varint (wireType 0) writer.WriteFixed64(2, 100); // 64-bit (wireType 1) writer.WriteString(3, "skip_me"); // length-delimited (wireType 2) writer.WriteFixed32(4, 200); // 32-bit (wireType 5) writer.WriteInt32(5, 999); // 这是目标字段 var data = writer.WrittenSpan.ToArray(); var reader = new SpanReader(data); // 跳过字段1-4 for (var i = 0; i < 4; i++) { var (_, wt) = reader.ReadTag(); reader.SkipField(wt); } // 读取字段5 var (fn5, _) = reader.ReadTag(); Assert.Equal(5, fn5); Assert.Equal(999, reader.ReadProtoInt32()); Assert.True(reader.Available <= 0); } [Fact] [DisplayName("Command_JSON格式_不受SpanWriter影响")] public void Command_JSON_NotAffected_ByRefactoring() { // JSON格式应继续使用JSON序列化,不受二进制重构影响 var cmd = new Command { Header = new Header { Code = 105, Flag = 0, Language = "JAVA", Version = MQVersion.V5_2_0, Opaque = 0, SerializeTypeCurrentRPC = "JSON", } }; cmd.Header.GetExtFields()["topic"] = "TestTopic"; var ms = new MemoryStream(); cmd.Write(ms, null); ms.Position = 0; var cmd2 = new Command(); var ok = cmd2.Read(ms); Assert.True(ok); Assert.Equal(105, cmd2.Header.Code); var ext = cmd2.Header.GetExtFields(); Assert.Equal("TestTopic", ext["topic"]); } #endregion } ================================================ FILE: XUnitTestRocketMQ/SupportApacheAclTest.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Threading; using NewLife; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ { public class SupportApacheAclTest { private const String NameServerAddress = "127.0.0.1:9876"; private const String TestTopic = "newlife_acl_test_topic"; /// /// 创建Topic时默认为系统 Topic => TBW102,可省略 /// private const String DefaultSysTopic = "TBW102"; private readonly AclOptions _aclOptions = new AclOptions() {AccessKey = "rocketmq2AcKey", SecretKey = "rocketmq2SeKey", OnsChannel = "LOCAL"}; [Fact(Skip = "需要配置ACL的RocketMQ服务器支持")] public void CreateTopicTest() { using var producer = CreateProducerInstance(DefaultSysTopic); producer.Start(); producer.CreateTopic(TestTopic, 2); producer.Dispose(); } [Fact(Skip = "需要配置ACL的RocketMQ服务器支持")] public void PublishMessageTest() { using var producer = CreateProducerInstance(TestTopic); producer.Start(); var pubResultList = new List(); for (var i = 0; i < 10; i++) { const String message = "大家好才是真的好!"; var pubResult = producer.Publish(message, "new_life_test_tag"); pubResultList.Add(pubResult.Status == SendStatus.SendOK); } Assert.True(pubResultList.All(_ => true)); producer.Dispose(); } [Fact(Skip = "需要配置ACL的RocketMQ服务器支持")] public void ConsumeMessageTest() { using var consumer = CreateConsumerInstance(TestTopic); consumer.OnConsume = OnConsume; consumer.Start(); Thread.Sleep(3000); static Boolean OnConsume(MessageQueue q, MessageExt[] ms) { Console.WriteLine("[{0}@{1}]收到消息[{2}]", q.BrokerName, q.QueueId, ms.Length); foreach (var item in ms.ToList()) { Console.WriteLine($"消息:主键【{item.Keys}】,产生时间【{item.BornTimestamp.ToDateTime()}】,内容【{item.Body.ToStr()}】"); } return true; } } private Producer CreateProducerInstance(String topic) { var producer = new Producer(); producer.NameServerAddress = NameServerAddress; producer.Topic = topic; producer.AclOptions = _aclOptions; return producer; } private Consumer CreateConsumerInstance(String topic) { var consumer = new Consumer(); consumer.NameServerAddress = NameServerAddress; consumer.Topic = topic; consumer.AclOptions = _aclOptions; consumer.Group = "new_life_test_group"; consumer.FromLastOffset = true; consumer.BatchSize = 5; return consumer; } } } ================================================ FILE: XUnitTestRocketMQ/TraceModelTests.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using NewLife.RocketMQ.MessageTrace; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// 消息轨迹数据模型测试 public class TraceModelTests { #region TraceContext [Fact] [DisplayName("TraceContext_默认值")] public void TraceContext_Defaults() { var ctx = new TraceContext(); Assert.Equal(default, ctx.TraceType); Assert.Null(ctx.RegionId); Assert.Null(ctx.GroupName); Assert.Equal(0, ctx.CostTime); Assert.False(ctx.Success); Assert.Null(ctx.RequestId); Assert.NotNull(ctx.TraceBeans); Assert.Empty(ctx.TraceBeans); } [Fact] [DisplayName("TraceContext_设置属性")] public void TraceContext_SetProperties() { var ctx = new TraceContext { TraceType = TraceType.Pub, GroupName = "PG_TEST", CostTime = 100, Success = true, RequestId = "REQ001", RegionId = "cn-hangzhou", }; Assert.Equal(TraceType.Pub, ctx.TraceType); Assert.Equal("PG_TEST", ctx.GroupName); Assert.Equal(100, ctx.CostTime); Assert.True(ctx.Success); Assert.Equal("REQ001", ctx.RequestId); Assert.Equal("cn-hangzhou", ctx.RegionId); } [Fact] [DisplayName("TraceContext_添加TraceBeans")] public void TraceContext_AddTraceBeans() { var ctx = new TraceContext(); ctx.TraceBeans.Add(new TraceBean { Topic = "t1", MsgId = "M1" }); ctx.TraceBeans.Add(new TraceBean { Topic = "t2", MsgId = "M2" }); Assert.Equal(2, ctx.TraceBeans.Count); Assert.Equal("t1", ctx.TraceBeans[0].Topic); Assert.Equal("M2", ctx.TraceBeans[1].MsgId); } #endregion #region TraceBean [Fact] [DisplayName("TraceBean_默认值")] public void TraceBean_Defaults() { var bean = new TraceBean(); Assert.Null(bean.Topic); Assert.Null(bean.MsgId); Assert.Null(bean.OffsetMsgId); Assert.Null(bean.Tags); Assert.Null(bean.Keys); Assert.Null(bean.StoreHost); Assert.Equal(0, bean.BodyLength); Assert.Null(bean.ClientHost); Assert.Null(bean.MsgType); Assert.Equal(0, bean.StoreTime); } [Fact] [DisplayName("TraceBean_设置所有属性")] public void TraceBean_SetAllProperties() { var bean = new TraceBean { Topic = "test_topic", MsgId = "MSG001", OffsetMsgId = "OFFSET001", Tags = "TagA", Keys = "Key1", StoreHost = "127.0.0.1:10911", BodyLength = 256, ClientHost = "192.168.1.1", MsgType = "Normal", StoreTime = 1000L, }; Assert.Equal("test_topic", bean.Topic); Assert.Equal("MSG001", bean.MsgId); Assert.Equal("OFFSET001", bean.OffsetMsgId); Assert.Equal("TagA", bean.Tags); Assert.Equal("Key1", bean.Keys); Assert.Equal("127.0.0.1:10911", bean.StoreHost); Assert.Equal(256, bean.BodyLength); Assert.Equal("192.168.1.1", bean.ClientHost); Assert.Equal("Normal", bean.MsgType); Assert.Equal(1000L, bean.StoreTime); } #endregion #region TraceType枚举 [Fact] [DisplayName("TraceType_枚举值正确")] public void TraceType_EnumValues() { Assert.Equal(0, (Int32)TraceType.Pub); Assert.Equal(1, (Int32)TraceType.SubBefore); Assert.Equal(2, (Int32)TraceType.SubAfter); } #endregion #region SendMessageContext [Fact] [DisplayName("SendMessageContext_字段可设置")] public void SendMessageContext_FieldsCanBeSet() { var msg = new Message { Topic = "test" }; var mq = new MessageQueue { BrokerName = "broker-a", QueueId = 0 }; var result = new SendResult { Status = SendStatus.SendOK, MsgId = "M1" }; var ctx = new SendMessageContext { ProducerGroup = "PG_TEST", Message = msg, Mq = mq, BrokerAddr = "127.0.0.1:10911", SendResult = result, MsgType = "Normal", BornHost = DateTime.Now, }; Assert.Equal("PG_TEST", ctx.ProducerGroup); Assert.Same(msg, ctx.Message); Assert.Same(mq, ctx.Mq); Assert.Equal("127.0.0.1:10911", ctx.BrokerAddr); Assert.Same(result, ctx.SendResult); Assert.Equal("Normal", ctx.MsgType); } [Fact] [DisplayName("SendMessageContext_TraceContext可设置")] public void SendMessageContext_TraceContextCanBeSet() { var traceCtx = new TraceContext { TraceType = TraceType.Pub }; var ctx = new SendMessageContext { TraceContext = traceCtx }; Assert.Same(traceCtx, ctx.TraceContext); } #endregion #region ConsumeMessageContext [Fact] [DisplayName("ConsumeMessageContext_字段可设置")] public void ConsumeMessageContext_FieldsCanBeSet() { var msgList = new List { new() { Topic = "t1" } }; var mq = new MessageQueue { BrokerName = "b1", QueueId = 0 }; var ctx = new ConsumeMessageContext { ConsumerGroup = "CG_TEST", MsgList = msgList, Mq = mq, Success = true, MsgType = "Normal", }; Assert.Equal("CG_TEST", ctx.ConsumerGroup); Assert.Single(ctx.MsgList); Assert.Same(mq, ctx.Mq); Assert.True(ctx.Success); Assert.Equal("Normal", ctx.MsgType); } #endregion } ================================================ FILE: XUnitTestRocketMQ/TransactionCheckTests.cs ================================================ using System; using System.ComponentModel; using System.Threading.Tasks; using NewLife.RocketMQ; using NewLife.RocketMQ.Protocol; using Xunit; namespace XUnitTestRocketMQ; /// 事务回查功能测试 public class TransactionCheckTests { [Fact] [DisplayName("OnCheckTransaction_默认为null")] public void OnCheckTransaction_DefaultNull() { using var producer = new Producer(); Assert.Null(producer.OnCheckTransaction); Assert.Null(producer.OnCheckTransactionAsync); } [Fact] [DisplayName("OnCheckTransaction_可设置回调委托")] public void OnCheckTransaction_CanSetCallback() { var callbackInvoked = false; using var producer = new Producer { OnCheckTransaction = (msg, transactionId) => { callbackInvoked = true; return TransactionState.Commit; } }; Assert.NotNull(producer.OnCheckTransaction); var state = producer.OnCheckTransaction(new MessageExt(), "test-txid"); Assert.True(callbackInvoked); Assert.Equal(TransactionState.Commit, state); } [Fact] [DisplayName("OnCheckTransactionAsync_可设置异步回调委托")] public async Task OnCheckTransactionAsync_CanSetCallback() { var callbackInvoked = false; using var producer = new Producer { OnCheckTransactionAsync = async (msg, transactionId, ct) => { await Task.CompletedTask; callbackInvoked = true; return TransactionState.Rollback; } }; Assert.NotNull(producer.OnCheckTransactionAsync); var state = await producer.OnCheckTransactionAsync(new MessageExt(), "test-txid", default); Assert.True(callbackInvoked); Assert.Equal(TransactionState.Rollback, state); } } ================================================ FILE: XUnitTestRocketMQ/VipChannelTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ; using NewLife.RocketMQ.Client; using Xunit; namespace XUnitTestRocketMQ; /// VIP通道测试 public class VipChannelTests { [Fact] [DisplayName("VipChannelEnabled_默认为false")] public void VipChannelEnabled_DefaultFalse() { using var producer = new Producer(); Assert.False(producer.VipChannelEnabled); } [Fact] [DisplayName("VipChannelEnabled_可设置为true")] public void VipChannelEnabled_CanSetTrue() { using var producer = new Producer(); producer.VipChannelEnabled = true; Assert.True(producer.VipChannelEnabled); } [Fact] [DisplayName("VipChannelEnabled_Consumer也支持")] public void VipChannelEnabled_ConsumerSupport() { using var consumer = new Consumer(); consumer.VipChannelEnabled = true; Assert.True(consumer.VipChannelEnabled); } [Fact] [DisplayName("BrokerClient_VIP模式端口偏移为端口减2")] public void BrokerClient_VipPortOffset() { // 验证 BrokerClient 构造时如果启用VIP,端口会减2 // 通过 MqBase 的属性间接验证 using var producer = new Producer { VipChannelEnabled = true, }; // VipChannelEnabled 设置为 true 时,BrokerClient 创建时会使用 port - 2 Assert.True(producer.VipChannelEnabled); } [Fact] [DisplayName("BrokerClient_非VIP模式端口不变")] public void BrokerClient_NonVipPortUnchanged() { using var producer = new Producer { VipChannelEnabled = false, }; Assert.False(producer.VipChannelEnabled); } } ================================================ FILE: XUnitTestRocketMQ/WeightRoundRobinTests.cs ================================================ using System; using System.ComponentModel; using NewLife.RocketMQ.Common; using Xunit; namespace XUnitTestRocketMQ; /// 带权重负载均衡算法测试 public class WeightRoundRobinTests { #region Set方法 [Fact] [DisplayName("Set_Null参数抛出异常")] public void Set_NullWeights_ThrowsArgumentNullException() { var lb = new WeightRoundRobin(); Assert.Throws(() => lb.Set(null)); } [Fact] [DisplayName("Set_设置权重后Ready为true")] public void Set_ValidWeights_SetsReadyTrue() { var lb = new WeightRoundRobin(); Assert.False(lb.Ready); lb.Set([1, 2, 3]); Assert.True(lb.Ready); Assert.Equal(3, lb.Weights.Length); } [Fact] [DisplayName("Set_相同权重不重复设置")] public void Set_SameWeights_NoReset() { var lb1 = new WeightRoundRobin(); var lb2 = new WeightRoundRobin(); // 初次设置相同权重 lb1.Set([1, 2, 3]); lb2.Set([1, 2, 3]); // 先各选一次改变状态 lb1.Get(); lb2.Get(); // 对 lb1 再次设置相同权重,不应重置状态 lb1.Set([1, 2, 3]); // Ready 仍应为 true Assert.True(lb1.Ready); // 后续多次 Get 的返回序列应与未再次 Set 的 lb2 完全一致 for (var i = 0; i < 10; i++) { var expected = lb2.Get(); var actual = lb1.Get(); Assert.Equal(expected, actual); } } [Fact] [DisplayName("Set_不同权重重新初始化")] public void Set_DifferentWeights_Reinitializes() { var lb = new WeightRoundRobin(); lb.Set([1, 2]); lb.Set([3, 4, 5]); Assert.Equal(3, lb.Weights.Length); Assert.Equal(3, lb.Weights[0]); Assert.Equal(4, lb.Weights[1]); Assert.Equal(5, lb.Weights[2]); } #endregion #region Get方法 [Fact] [DisplayName("Get_未初始化时返回0")] public void Get_NotInitialized_ReturnsZero() { var lb = new WeightRoundRobin(); var idx = lb.Get(out var times); Assert.Equal(0, idx); Assert.Equal(1, times); } [Fact] [DisplayName("Get_等权重均匀分配")] public void Get_EqualWeights_EvenDistribution() { var lb = new WeightRoundRobin(); lb.Set([1, 1, 1]); var counts = new Int32[3]; for (var i = 0; i < 30; i++) { var idx = lb.Get(); counts[idx]++; } // 等权重应该近似均匀分配 Assert.Equal(10, counts[0]); Assert.Equal(10, counts[1]); Assert.Equal(10, counts[2]); } [Fact] [DisplayName("Get_不等权重按比例分配")] public void Get_UnequalWeights_ProportionalDistribution() { var lb = new WeightRoundRobin(); lb.Set([3, 1]); var counts = new Int32[2]; for (var i = 0; i < 40; i++) { var idx = lb.Get(); counts[idx]++; } // 权重3:1,40次中应大约30次和10次 Assert.True(counts[0] > counts[1], $"权重高的选中次数({counts[0]})应多于权重低的({counts[1]})"); } [Fact] [DisplayName("Get_输出正确的次数")] public void Get_ReturnsTimes_Correctly() { var lb = new WeightRoundRobin(); lb.Set([1, 1]); lb.Get(out var times1); Assert.Equal(1, times1); lb.Get(out _); lb.Get(out var times3); // 第一个索引被选中第2次 Assert.Equal(2, times3); } [Fact] [DisplayName("Get_单个权重总是返回0")] public void Get_SingleWeight_AlwaysReturnsZero() { var lb = new WeightRoundRobin(); lb.Set([5]); for (var i = 0; i < 10; i++) { var idx = lb.Get(); Assert.Equal(0, idx); } } [Fact] [DisplayName("Get无参版本与有参版本一致")] public void Get_NoOutParam_SameAsWithOutParam() { var lb1 = new WeightRoundRobin(); var lb2 = new WeightRoundRobin(); lb1.Set([2, 3, 1]); lb2.Set([2, 3, 1]); for (var i = 0; i < 20; i++) { var idx1 = lb1.Get(); var idx2 = lb2.Get(out _); Assert.Equal(idx1, idx2); } } #endregion } ================================================ FILE: XUnitTestRocketMQ/XUnitTestRocketMQ.csproj ================================================ net10.0 RocketMQ单元测试 NewLife.RocketMQ 单元测试项目 ..\Bin\UnitTest false all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive