From ffb005fec688fe0654cfcb5cd6d69347ef29efcb Mon Sep 17 00:00:00 2001 From: SoulStar Date: Mon, 11 May 2026 17:46:16 +0800 Subject: [PATCH] =?UTF-8?q?feat=20-=20=E6=B7=BB=E5=8A=A0WPFNodeDemo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WpfNodeTest/.github/copilot-instructions.md | 228 +++++++ .../instructions/benchmark.instructions.md | 118 ++++ .../instructions/development.instructions.md | 324 ++++++++++ .../.github/instructions/net.instructions.md | 573 ++++++++++++++++++ WpfNodeTest/WpfNodeTest.slnx | 3 + WpfNodeTest/WpfNodeTest/App.xaml | 9 + WpfNodeTest/WpfNodeTest/App.xaml.cs | 53 ++ WpfNodeTest/WpfNodeTest/AssemblyInfo.cs | 10 + WpfNodeTest/WpfNodeTest/Config/NodeTypes.json | 34 ++ WpfNodeTest/WpfNodeTest/MainWindow.xaml | 84 +++ WpfNodeTest/WpfNodeTest/MainWindow.xaml.cs | 62 ++ .../WpfNodeTest/Models/AGVDockingNode.cs | 92 +++ .../WpfNodeTest/Models/NodeGraphData.cs | 37 ++ .../WpfNodeTest/Models/NodeTypeConfig.cs | 20 + .../Services/NodeGraphSerializer.cs | 121 ++++ .../Services/NodeTypeConfigService.cs | 79 +++ .../Services/PathPlanningService.cs | 110 ++++ .../ViewModels/MainWindowViewModel.cs | 106 ++++ .../Views/NodeTemplateControl.xaml | 11 + .../Views/NodeTemplateControl.xaml.cs | 57 ++ WpfNodeTest/WpfNodeTest/WpfNodeTest.csproj | 24 + WpfNodeTest/example_graph.json | 66 ++ 22 files changed, 2221 insertions(+) create mode 100644 WpfNodeTest/.github/copilot-instructions.md create mode 100644 WpfNodeTest/.github/instructions/benchmark.instructions.md create mode 100644 WpfNodeTest/.github/instructions/development.instructions.md create mode 100644 WpfNodeTest/.github/instructions/net.instructions.md create mode 100644 WpfNodeTest/WpfNodeTest.slnx create mode 100644 WpfNodeTest/WpfNodeTest/App.xaml create mode 100644 WpfNodeTest/WpfNodeTest/App.xaml.cs create mode 100644 WpfNodeTest/WpfNodeTest/AssemblyInfo.cs create mode 100644 WpfNodeTest/WpfNodeTest/Config/NodeTypes.json create mode 100644 WpfNodeTest/WpfNodeTest/MainWindow.xaml create mode 100644 WpfNodeTest/WpfNodeTest/MainWindow.xaml.cs create mode 100644 WpfNodeTest/WpfNodeTest/Models/AGVDockingNode.cs create mode 100644 WpfNodeTest/WpfNodeTest/Models/NodeGraphData.cs create mode 100644 WpfNodeTest/WpfNodeTest/Models/NodeTypeConfig.cs create mode 100644 WpfNodeTest/WpfNodeTest/Services/NodeGraphSerializer.cs create mode 100644 WpfNodeTest/WpfNodeTest/Services/NodeTypeConfigService.cs create mode 100644 WpfNodeTest/WpfNodeTest/Services/PathPlanningService.cs create mode 100644 WpfNodeTest/WpfNodeTest/ViewModels/MainWindowViewModel.cs create mode 100644 WpfNodeTest/WpfNodeTest/Views/NodeTemplateControl.xaml create mode 100644 WpfNodeTest/WpfNodeTest/Views/NodeTemplateControl.xaml.cs create mode 100644 WpfNodeTest/WpfNodeTest/WpfNodeTest.csproj create mode 100644 WpfNodeTest/example_graph.json diff --git a/WpfNodeTest/.github/copilot-instructions.md b/WpfNodeTest/.github/copilot-instructions.md new file mode 100644 index 0000000..c5fd588 --- /dev/null +++ b/WpfNodeTest/.github/copilot-instructions.md @@ -0,0 +1,228 @@ +# 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` | + +--- + +## 2. 核心原则 + +检索优先、风格一致、兼容友好、**主动优化**。 +发现明显缺陷(资源泄漏、空引用、逻辑错误)时主动修复;优化请求时深入分析,不做表面工作。 +改动较小直接做并说明;改动较大(涉及公共 API 或大范围重构)先列方案询问确认。 + +--- + +## 3. 兼容性约束(极重要) + +NewLife 核心库支持 `.NET 4.5` 至最新版本(`net45` → `net10`)。 + +- **语言版本**:`latest`,最大化使用最新 C# 语法糖(switch 表达式、集合表达式 `[]`、`?.`/`??`、模式匹配、目标类型 `new`、record 等) +- **禁止高版本专属 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` +- 新增 API 需评估各框架兼容性,必要时提供条件编译降级实现 + +--- + +## 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 条件运算符**:优先使用 `?.`/`??` 简化空值检查 + +```csharp +// ✅ 单行 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(); + +// ✅ 循环必须保留花括号(即使单语句) +foreach (var item in list) +{ + Process(item); +} + +// ✅ 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 模式:不用异常作常规分支 +- 类型转换:优先使用 `ToInt()`/`ToBoolean()` 等扩展方法 +- 对外异常不暴露内部实现/路径 + +--- + +## 5. NewLife 内置工具 + +优先使用项目内置工具而非标准库,**禁止重复造轮子**: + +- 字符串构建:`Pool.StringBuilder`(替代 `new StringBuilder()`) +- 时间戳:`Runtime.TickCount64` +- 类型转换:`ToInt()`、`ToBoolean()`、`ToDouble()`、`ToDateTime()` 等扩展方法 +- 追踪埋点:`Tracer?.NewSpan()` + +--- + +## 6. 防御性注释(禁止删除) + +代码中带有说明文字的被注释代码属于**防御性注释**,记录历史踩坑经验。**禁止删除,禁止"恢复"执行**。可补充更详细说明。 + +```csharp +// 曾经尝试过同步等待,但会导致线程池饥饿和死锁 +// var result = task.Result; + +// 不要使用 SendAsync 的无超时重载,否则会造成连接泄漏 +// await client.SendAsync(data); +``` + +--- + +## 7. 工作流 + +触发检查(第 1 节触发信号表匹配,命中则读取专用指令) → 检索(**优先复用**现有实现) → 评估(公共 API/兼容性/性能) → 方案 → 实施 → 验证 → 说明 + +- **触发检查**:开始工作前必须完成,遗漏专用指令将导致输出不符合要求 +- **实施**:完成主任务;顺带修复明显缺陷;顺带简化重复代码;保留原注释与结构 +- **验证**:代码变更必须编译通过;找到相关测试则运行;仅文档变更可跳过 + +### 主动优化原则 + +用户要求**分析/优化代码**时: + +| 行动 | 说明 | +|------|------| +| **架构梳理** | 重构不清晰的结构,让代码更易懂 | +| **缺陷修复** | 资源泄漏、空引用、并发问题、逻辑错误 → 直接修复 | +| **代码简化** | 提取重复代码、合并冗余判断、应用现代语法 | +| **性能优化** | 缓存重复计算、池化高频对象、避免无用分配 | +| **注释完善** | 补充缺失的 XML 注释和关键逻辑说明 | + +--- + +## 8. 测试 + +- 框架 xUnit;类名 `{ClassName}Tests`;方法加 `[DisplayName("中文描述意图")]` +- 网络端口用 `0`/随机,IO 用临时目录 +- 先搜索 `{ClassName}` 引用定位测试文件,再找 `{ClassName}Tests.cs`;**未找到需说明**,不自动创建测试项目 + +--- + +## 9. 文档与发布 + +### Markdown 文档 + +UTF-8 无 BOM;存放 `Doc/` 目录;文件名优先中文。**已有文件必须先读取再增量修改,禁止覆盖。** + +### NuGet 版本 + +| 类型 | 格式 | 示例 | +|------|------|------| +| 正式版 | `{主}.{子}.{年}.{月日}` | `11.9.2025.0701` | +| 测试版 | `{主}.{子}.{年}.{月日}-beta{时分}` | `11.9.2025.0701-beta0906` | + +--- + +## 10. 重要禁止项 + +以下是 AI 容易犯但在本项目影响严重的错误: + +- 将 `String`/`Int32` 改为 `string`/`int`(本项目反 C# 惯例,**必须用正式名**) +- 删除防御性注释(带说明的注释代码) +- 删除循环体的花括号 +- 将 `` 拆成多行 +- 擅自删除 `public`/`protected` 成员 +- 擅自新增外部 NuGet 依赖(需说明理由) +- 仅删除空白行/注释制造"格式优化"提交 +- 虚构不存在的 API/文件/类型 +- 伪造测试结果/性能数据 +- 在热点路径添加未缓存反射/复杂 Linq +- 输出敏感凭据/内部地址 +- 发现问题却视而不见 +- 用户要求优化时仅做注释/测试等表面工作 +- **跳过第 1 节触发检查**(命中关键词却未加载专用指令文件,是最严重的遗漏错误) + +--- + +## 11. 变更说明模板 + +```markdown +## 概述 +做了什么 / 为什么 + +## 影响 +- 公共 API:是/否 +- 性能影响:无/有(说明) + +## 兼容性 +降级策略 / 条件编译点 + +## 风险与后续 +潜在回归 / 是否补测试 +``` + +--- + +(完) diff --git a/WpfNodeTest/.github/instructions/benchmark.instructions.md b/WpfNodeTest/.github/instructions/benchmark.instructions.md new file mode 100644 index 0000000..a900493 --- /dev/null +++ b/WpfNodeTest/.github/instructions/benchmark.instructions.md @@ -0,0 +1,118 @@ +--- +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. **性能概览**(一句话用途 + 核心结论,3~5 条要点,每条一句话) +2. **测试环境**(CPU / OS / Runtime,代码块 3~4 行即可) +3. **测试结果**(BDN 原始表格,保留 Mean / Error / StdDev / Allocated) +4. **结果分析**(见下方约束) +5. **瓶颈与优化建议**(见第 8 节,仅列有数据支撑的瓶颈) + +### 7.1 结果分析约束 + +结果分析是对 BDN 数据的**提炼**,不是重新排列。遵循以下规则: + +- **禁止重复制表**:不要把 BDN 数据换个单位(如 ops/s)再列一张完整表格;如需标注业务指标,在正文中用"XXX 操作约 N M ops/s"一笔带过 +- **对比用文字而非新表**:横向/纵向对比直接写结论("ArrayPacket 构造比 OwnerPacket 快 ~670x,Slice 零分配"),不为每个对比维度单独建表 +- **仅在对比维度 ≥3 且差异显著时**才建一张对比表,且最多一张 +- **总量控制**:结果分析文字不超过 BDN 原始表格总行数的 1/3 + +### 7.2 篇幅控制 + +- 分析 + 瓶颈部分的文字行数 ≤ 数据表格行数(含表头) +- 无需为每个数字都单独解读;读者能从原表看出的趋势不必复述 + +## 8. 瓶颈与优化建议规范 + +### 8.1 撰写原则 + +- **数据驱动**:所有结论必须有 BDN 实测数据支撑,**禁止无数据臆测**,没有 profiler 数据时不编造 ns 级拆解 +- **量化表达**:用"快 X 倍"、"省 Y%"、"降 Z B/op",避免"显著""明显"等模糊词 +- **可操作**:指明具体修改位置和方案,不泛泛建议 +- **宁少勿凑**:只列真正有影响的瓶颈(通常 1~3 个),不为凑数列 P3 级微小问题 + +### 8.2 瓶颈表格(唯一模板,合并展示) + +用一张表汇总,不再拆分多张表: + +```markdown +| 优先级 | 瓶颈 | 现象(实测数据) | 优化方向 | 预期收益 | +|--------|------|----------------|---------|---------| +| P0 | {名称} | {BDN 数据描述} | {方案} | {速度/内存预估} | +| P1 | {名称} | {BDN 数据描述} | {方案} | {速度/内存预估} | +``` + +- **P0**:影响核心吞吐或 >30% 性能损失,必须优化 +- **P1**:影响扩展性或有明显内存压力,建议优化 +- **P2**:次要瓶颈,可选优化(仅在确有数据支撑时列出) +- 同级按影响程度降序;无明显瓶颈时写"未发现显著瓶颈"即可 + +### 8.3 补充说明(可选) + +表格之后可对 P0/P1 瓶颈各写 2~3 句补充根因和方案细节,**不要求也不鼓励**对每个瓶颈展开长篇分析。 diff --git a/WpfNodeTest/.github/instructions/development.instructions.md b/WpfNodeTest/.github/instructions/development.instructions.md new file mode 100644 index 0000000..44729b4 --- /dev/null +++ b/WpfNodeTest/.github/instructions/development.instructions.md @@ -0,0 +1,324 @@ +--- +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 个任务)。 + +--- + +(完) diff --git a/WpfNodeTest/.github/instructions/net.instructions.md b/WpfNodeTest/.github/instructions/net.instructions.md new file mode 100644 index 0000000..ba7509b --- /dev/null +++ b/WpfNodeTest/.github/instructions/net.instructions.md @@ -0,0 +1,573 @@ +--- +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); + } +} +``` + +--- + +(完) diff --git a/WpfNodeTest/WpfNodeTest.slnx b/WpfNodeTest/WpfNodeTest.slnx new file mode 100644 index 0000000..798b5a3 --- /dev/null +++ b/WpfNodeTest/WpfNodeTest.slnx @@ -0,0 +1,3 @@ + + + diff --git a/WpfNodeTest/WpfNodeTest/App.xaml b/WpfNodeTest/WpfNodeTest/App.xaml new file mode 100644 index 0000000..9e09a2c --- /dev/null +++ b/WpfNodeTest/WpfNodeTest/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/WpfNodeTest/WpfNodeTest/App.xaml.cs b/WpfNodeTest/WpfNodeTest/App.xaml.cs new file mode 100644 index 0000000..e45952f --- /dev/null +++ b/WpfNodeTest/WpfNodeTest/App.xaml.cs @@ -0,0 +1,53 @@ +using NewLife.Log; +using NodeNetwork; +using System.Configuration; +using System.Data; +using System.Windows; + +namespace WpfNodeTest +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + public App() + { + XTrace.UseConsole(); + } + + //protected override void OnStartup(StartupEventArgs e) + //{ + // base.OnStartup(e); + // NNViewRegistrar.RegisterSplat(); + //} + + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + NNViewRegistrar.RegisterSplat(); // 注册NodeNetwork视图 + XTrace.WriteLine("1.OnStartup被触发"); + } + + protected override void OnActivated(EventArgs e) + { + base.OnActivated(e); + XTrace.WriteLine("2.OnActivated被触发"); + } + + protected override void OnDeactivated(EventArgs e) + { + base.OnDeactivated(e); + XTrace.WriteLine("3.OnDeactivated被触发"); + } + + protected override void OnExit(ExitEventArgs e) + { + base.OnExit(e); + XTrace.WriteLine("4.OnExit被触发"); + } + + + } + +} diff --git a/WpfNodeTest/WpfNodeTest/AssemblyInfo.cs b/WpfNodeTest/WpfNodeTest/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/WpfNodeTest/WpfNodeTest/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/WpfNodeTest/WpfNodeTest/Config/NodeTypes.json b/WpfNodeTest/WpfNodeTest/Config/NodeTypes.json new file mode 100644 index 0000000..60b8d0e --- /dev/null +++ b/WpfNodeTest/WpfNodeTest/Config/NodeTypes.json @@ -0,0 +1,34 @@ +{ + "NodeTypes": [ + { + "TypeName": "提升机", + "DefaultInputCount": 1, + "DefaultOutputCount": 1, + "Description": "垂直运输设备" + }, + { + "TypeName": "电梯", + "DefaultInputCount": 1, + "DefaultOutputCount": 1, + "Description": "楼层间运输" + }, + { + "TypeName": "工位", + "DefaultInputCount": 1, + "DefaultOutputCount": 0, + "Description": "作业工位" + }, + { + "TypeName": "物流接驳位", + "DefaultInputCount": 2, + "DefaultOutputCount": 2, + "Description": "物流中转点" + }, + { + "TypeName": "库位", + "DefaultInputCount": 1, + "DefaultOutputCount": 0, + "Description": "存储位置" + } + ] +} diff --git a/WpfNodeTest/WpfNodeTest/MainWindow.xaml b/WpfNodeTest/WpfNodeTest/MainWindow.xaml new file mode 100644 index 0000000..10f69bf --- /dev/null +++ b/WpfNodeTest/WpfNodeTest/MainWindow.xaml @@ -0,0 +1,84 @@ + + + + + + + + + + + +