From a789aec2a3844fb17354e7866f998d1efec6771c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 11 May 2026 16:09:04 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20ADR-0002=20=E4=BA=8B=E5=8A=A1=E5=B1=82?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=20+=20README=20=E5=85=A8=E9=87=8F=E9=87=8D?= =?UTF-8?q?=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0002(事务层设计) - 四 FSM 独立而非通用:状态名重叠但 RFC 语义不同 - 时间轮:ScheduledExecutorService + 虚拟线程派发 - 事务键 (branch, sent-by, method) + RFC 3261 magic cookie 强制 - 同步代码风格 FSM(CAS 状态迁移,无事件循环) - MessageSender 单方法 SPI 解耦 transport - 列出何时重评估 + 关键 RFC §17 行为映射 README 重写 - 完整模块结构(含 sip-gb28181 第 10 模块) - 关键决策链接到 ADR-0001 / ADR-0002 - 5 段快速上手代码:解析编码 / UDP 传输 / REGISTER / OPTIONS / GB28181 - 路线图:phase 0-5、7 标记完成,8-10 待办 - 工程化:169 测试当前规模、CI 配置、文件级约定 docs/adr/README.md:ADR 索引新增 0002 条目。 Co-authored-by: li xuanqun <793005378@qq.com> --- README.md | 177 ++++++++++++++++------ docs/adr/0002-transaction-layer-design.md | 162 ++++++++++++++++++++ docs/adr/README.md | 1 + 3 files changed, 291 insertions(+), 49 deletions(-) create mode 100644 docs/adr/0002-transaction-layer-design.md diff --git a/README.md b/README.md index a11b930..7121b87 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,161 @@ # SIP Stack -> 一个现代化、模块化、基于 JDK 21 虚拟线程的 Java SIP 协议栈。 +> 现代化、模块化、基于 **JDK 21 虚拟线程** 的 Java SIP 协议栈。 > **纯库实现**:协议核心零运行时依赖,框架适配可选。 -> **第一阶段目标**:GB28181(B 场景)。**最终目标**:通用 SIP 协议栈(A 场景)。 +> **覆盖范围**:RFC 3261 核心 + RFC 7616 Digest + GB/T 28181-2016 国标应用层。 -⚠ 项目处于**重启初期**,当前仓库只有骨架。详见 [#1 路线图](#路线图)。 +[![JDK](https://img.shields.io/badge/JDK-21%2B-blue)]() +[![License](https://img.shields.io/badge/License-MIT-yellow)]() --- ## 设计原则 -1. **协议核心不感知框架与网络库**。`sip-message`、`sip-codec` 模块零运行时依赖。 -2. **分层强制隔离**。通过 Maven 多模块 + JPMS `module-info.java` 在编译期阻断脏依赖。 +1. **协议核心不感知框架与网络库**。`sip-message`、`sip-codec` 零运行时依赖。 +2. **分层强制隔离**。Maven 多模块 + JPMS `module-info.java` 在编译期阻断脏依赖。 3. **拥抱 JDK 21**。虚拟线程让事务/对话 FSM 用同步代码即可表达,无需回调/事件循环。 -4. **RFC 4475 驱动开发**。第一行测试代码就是 torture tests,逼出真正合规的解析器。 -5. **可插拔传输**。默认 `sip-transport-nio`,未来可加 `sip-transport-netty` 等不破坏 SPI。 +4. **RFC 4475 驱动开发**。Torture-test fixtures 是合规闸门,每个新解析特性都先有 fixture。 +5. **可插拔传输**。默认 `sip-transport-nio`(NIO + 虚拟线程),未来可加 `sip-transport-netty` 等不破坏 SPI。 ## 模块结构 ``` -sip-stack-parent POM 父模块 -├── sip-message 不可变消息模型(sealed + record) [零依赖] -├── sip-codec wire-format 解析/编码(手写,RFC 3261 §25) [零依赖+sip-message] -├── sip-transport-api 传输 SPI 纯接口 [+sip-message] -├── sip-transport-nio JDK 21 NIO + 虚拟线程默认实现 [+slf4j] -├── sip-transaction 事务层 FSM + 时间轮 [+slf4j] -├── sip-dialog 对话层 [+slf4j] -├── sip-ua 高级 UA API [+slf4j] -└── sip-compliance-tests RFC 4475 / RFC 5118 合规测试 [test-only] +sip-stack-parent +├── sip-message 不可变消息模型(sealed + record) [零依赖] +├── sip-codec wire-format 解析/编码 + typed headers [+sip-message] +├── sip-transport-api 传输 SPI 纯接口 [+sip-message] +├── sip-transport-nio JDK 21 NIO + 虚拟线程默认实现(UDP + TCP) [+codec, +slf4j] +├── sip-transaction RFC 3261 §17 四个事务 FSM + 时间轮 [+transport-api] +├── sip-dialog RFC 3261 §12 对话层(Route Set / Target Refresh)[+transaction] +├── sip-ua 高级 UA API(REGISTER / OPTIONS / Digest 鉴权) [+dialog] +├── sip-gb28181 GB/T 28181-2016 应用层(MANSCDP XML) [+ua] +└── sip-compliance-tests RFC 4475 / RFC 5118 合规测试 [test-only] ``` ## 关键决策 -| 维度 | 选择 | 说明 | -| ----------- | ----------------------------------- | ---- | -| 项目定位 | B → A | 先做 GB28181 信令网关,最终做通用栈 | -| JDK | **21 LTS** | 虚拟线程、Pattern Matching、Records、Sealed | -| 网络方案 | **JDK NIO + 虚拟线程**(不引入 Netty) | UDP 用 `DatagramChannel`,TCP/TLS 用 `Socket` + 虚拟线程
详见 [ADR-0001](docs/adr/0001-transport-layer-selection.md) | -| 协议核心依赖 | 仅 SLF4J(且仅在需要日志的模块) | `sip-message` / `sip-codec` 连 SLF4J 都不引入 | -| 框架适配 | 暂不做 | 先打磨纯库;Spring Boot starter 是后续可选模块 | -| 解析器 | 手写递归下降 | 不使用 ANTLR / 正则 / parser combinator | -| 消息模型 | 不可变 record + sealed | 模式匹配即穷尽;后续可加更多 `permits` | +所有重要架构决策记录在 [`docs/adr/`](docs/adr/) 目录,遵循 ADR 格式。 -> 所有重要架构决策记录在 [`docs/adr/`](docs/adr/) 目录下,遵循 ADR (Architecture Decision Record) 格式。 +- [ADR-0001](docs/adr/0001-transport-layer-selection.md) — 传输层:**JDK 21 虚拟线程 + NIO**(不引入 Netty) +- [ADR-0002](docs/adr/0002-transaction-layer-design.md) — 事务层:四 FSM + 时间轮 + 虚拟线程 -## 构建 +| 维度 | 选择 | +| --- | --- | +| 项目定位 | B(GB28181)→ A(通用 SIP 栈) | +| JDK | 21 LTS | +| 网络方案 | JDK NIO + 虚拟线程 | +| 解析器 | 手写递归下降 | +| 消息模型 | 不可变 `record` + `sealed interface` | +| 模块隔离 | Maven 多模块 + JPMS `module-info.java` | +| 鉴权 | RFC 7616(SHA-256 / SHA-512-256)+ MD5 兼容 | +| 框架适配 | 暂不做(纯库优先) | -```bash +## 快速上手 + +### 1. 解析 / 编码 + +```java +SipMessage msg = SipParser.parse(bytes); +byte[] wire = SipEncoder.encode(msg); +``` + +### 2. 创建 UDP 传输 + +```java +NioUdpTransport transport = new NioUdpTransport( + new InetSocketAddress("0.0.0.0", 5060)); +transport.start().get(); +transport.listener(in -> { + SipMessage m = in.message(); + // 处理 m +}); +``` + +### 3. 注册 + Digest 鉴权 + +```java +TimerScheduler timers = TimerScheduler.create(); +RegistrationClient client = new RegistrationClient(transport::send, timers); + +SipResponse response = client.register( + SipUri.parse("sip:registrar.example.com"), + SipUri.parse("sip:alice@example.com"), + SipUri.parse("sip:alice@pc.example.com"), + 3600, + new DigestCredentials("alice", "secret"), + registrarEndpoint +).get(); +``` + +### 4. 发起 OPTIONS 探测 + +```java +OptionsProbe probe = new OptionsProbe(transport::send, timers); +SipResponse rsp = probe.probe(targetUri, localAor, peerEndpoint).get(); +``` + +### 5. GB28181 Keepalive + +```java +String xml = GbMessages.keepaliveXml( + GbMessages.nextSerialNumber(), + "34020000001320000001", + "OK"); +SipRequest message = GbMessages.manscdpMessage( + platformUri, deviceUri, platformUri, + fromTag, callId, cseq, + xml, "127.0.0.1:5060"); +transport.send(message, platformEndpoint); +``` + +## 路线图 + +- [x] **Phase 0** — 骨架 + ADR-0001 +- [x] **Phase 1** — typed headers(Via / NameAddr / CSeq / Contact / Route / 简单整数头 / Call-ID) +- [x] **Phase 2** — 解析器 + 编码器(含 SipUriParser)+ RFC 4475 闸门 +- [x] **Phase 3** — `sip-transport-nio`(UDP + TCP) +- [x] **Phase 4** — 事务层四个 FSM + 时间轮(ADR-0002) +- [x] **Phase 5** — Dialog + REGISTER + OPTIONS + Digest 鉴权(RFC 7616) +- [x] **Phase 7** — GB28181 应用层(MANSCDP XML) +- [ ] **Phase 8** — TLS / WebSocket 传输 +- [ ] **Phase 9** — 互操作测试(Kamailio / Asterisk + SIPp) +- [ ] **Phase 10** — Spring Boot starter(可选) + +## 测试 + +``` mvn -B verify ``` 要求:**JDK 21+**、Maven 3.8+。 -## 路线图 +当前规模:**10 modules、169 个测试**(含 34 个合规动态测试)。 + +### 合规闸门 -- [x] **Phase 0**:决策定型、骨架搭建(当前 PR) -- [ ] **Phase 1**:`sip-message` 补齐 typed headers(Via/From/To/CSeq/Contact/Route) -- [ ] **Phase 2**:`sip-codec` 手写解析器,**RFC 4475 fixture 全部通过** -- [ ] **Phase 3**:`sip-codec` 编码器,roundtrip 测试 -- [ ] **Phase 4**:`sip-transport-nio` UDP 完整实现 + 集成测试 -- [ ] **Phase 5**:`sip-transaction` 四个 FSM + 时间轮 -- [ ] **Phase 6**:`sip-dialog` + REGISTER + Digest 鉴权 -- [ ] **Phase 7**:GB28181 适配(catalog、ptz、报警等扩展消息体) -- [ ] **Phase 8**:`sip-transport-nio` TCP/TLS -- [ ] **Phase 9**:互操作测试(Kamailio / Asterisk via Testcontainers + SIPp) +- **RFC 4475** torture test fixtures 驻留在 `sip-compliance-tests/src/test/resources/torture/rfc4475/` +- 每个新 fixture 通过 `tools/write_fixture.py` 从 `.fixture` 源转码到 byte-exact `.raw`,杜绝 CRLF 被无声破坏 +- 合规测试在 parser、parse→encode roundtrip、typed-header 解析三个层面分别验证 ## 参考规范 -- RFC 3261 SIP 核心 -- RFC 3263 服务器定位(DNS NAPTR/SRV) -- RFC 3264 Offer/Answer 模型 -- RFC 3581 rport -- RFC 4566 / 8866 SDP -- RFC 4475 / 5118 Torture Tests -- RFC 5626 NAT outbound -- RFC 6665 SUBSCRIBE/NOTIFY -- RFC 7616 Digest Auth -- GB/T 28181-2016 安防监控国标 +- **RFC 3261** SIP 核心 +- **RFC 3263** DNS 定位 +- **RFC 3264** Offer/Answer +- **RFC 3581** rport (NAT) +- **RFC 4475 / 5118** Torture Tests +- **RFC 5626** NAT outbound +- **RFC 5923** TCP 连接复用 +- **RFC 7616** Digest 鉴权(SHA-256) +- **GB/T 28181-2016** 安防监控国标 + +## 工程化 + +- `.gitattributes` 强制 `*.raw` binary +- `maven-enforcer-plugin` 强制 JDK 21、Maven 3.8+ +- `-Werror`、`-Xlint:all`、`-parameters` +- GitHub Actions CI:`mvn -B verify` on JDK 21 +- 所有架构决策有对应 ADR 文件 ## 许可证 diff --git a/docs/adr/0002-transaction-layer-design.md b/docs/adr/0002-transaction-layer-design.md new file mode 100644 index 0000000..7c5e105 --- /dev/null +++ b/docs/adr/0002-transaction-layer-design.md @@ -0,0 +1,162 @@ +# ADR-0002:事务层设计 — 四 FSM + 时间轮 + 虚拟线程 + +- **Status**:Accepted +- **Date**:2026-05-11 +- **Deciders**:项目维护者 +- **Tags**:transaction, fsm, timing, virtual-threads + +--- + +## 1. Context + +RFC 3261 §17 定义了 SIP 协议栈的核心机制 — 事务层。每个事务是一个有限状态机(FSM),跟踪一个请求-响应对的完整生命周期。RFC 同时定义了 11 个定时器(A 到 K),用于驱动重传、超时和"等待残留消息"。 + +设计选择决定了: +- 协议核心代码的**可读性**(同步 vs 异步表达) +- **正确性**(状态迁移和定时器交互必须严格符合 RFC) +- **可测试性**(无需真实 transport / 真实时间就能跑完所有迁移) +- **可观测性**(FSM 状态、定时器、计数器对运维至关重要) + +--- + +## 2. Decision + +### 2.1 四个独立 FSM 而非通用状态机 + +**实装**: +- `NonInviteClientTransaction` (§17.1.2) +- `NonInviteServerTransaction` (§17.2.2) +- `InviteClientTransaction` (§17.1.1) +- `InviteServerTransaction` (§17.2.1) + +**理由**: +- 这四个 FSM 在 RFC 中是**独立定义**的,状态名虽有重叠但语义和迁移不同 +- 强行"统一通用 FSM"会导致大量条件分支和 if-else,反而失去清晰度 +- 共享代码(如定时器调度、终止流程)通过 helper 提取,**不是 base class** + +### 2.2 时间轮:JDK `ScheduledExecutorService` + 虚拟线程派发 + +**实装**: +```java +public interface TimerScheduler extends AutoCloseable { + Handle schedule(Runnable task, long delayMs); + interface Handle { void cancel(); boolean isCancelled(); } +} +``` +默认实现 = 单线程 `ScheduledExecutorService`(入队)+ 虚拟线程池(回调派发)。 + +**理由**: +- 即便繁忙服务器,活跃事务数也在几万量级,`ScheduledThreadPoolExecutor` 完全够用 +- 把回调切到虚拟线程,**永远不阻塞 timer wheel** +- 接口很窄,将来真的撞上 PPS 瓶颈,可以替换为 `HashedWheelTimer`(Netty 风格),**调用方一行不改** + +### 2.3 事务键:`(branch, sent-by, method)` + +**实装**: +```java +public record TransactionKey(String branch, String sentBy, SipMethod method) { } +``` + +强制要求 `branch` 以 RFC 3261 magic cookie `z9hG4bK` 开头。 + +**特殊处理**:ACK 匹配规则(§17.1.3 末段) +- 2xx 的 ACK 是**新事务**,不应匹配现有 +- 非 2xx 的 ACK 必须命中原 INVITE 服务端事务 +- 通过 `TransactionKey.of(request, SipMethod.INVITE)` 显式让调用方表达意图 + +**理由**: +- 不支持 RFC 2543 兼容性 —— 现代 SIP 设备都用 RFC 3261 cookie +- 三元组比纯 branch 更鲁棒(理论上 sent-by 可以是同一 branch 的"伪装防御") + +### 2.4 同步代码风格的 FSM(拥抱虚拟线程) + +**实装**:FSM 的状态迁移用 `AtomicReference` + CAS 表达,**没有事件循环、没有回调队列**。 + +定时器触发回调跑在虚拟线程上,可以直接调用 `state.compareAndSet(...)` 而不必担心阻塞。 + +**理由**: +- SIP FSM 的状态迁移本质上是"事件→新状态"的映射,CAS 比 actor / state-machine 框架更简单 +- 调用 listener 是直接函数调用 —— 调用方可以在 listener 里同步处理 `SipResponse`,包括用 `CompletableFuture.get()` 等待下游 +- 这是 ADR-0001(虚拟线程)的直接收益 + +### 2.5 TU 通过 listener 接口而非订阅 + +**实装**: +- `ClientTransactionListener.onProvisional / onFinal / onTimeout / onTransportError` +- `ServerTransactionListener.onRequest / onRetransmit / onTerminate` + +**理由**: +- 一个事务只有一个 TU 关心 — 不需要发布订阅 +- 单 listener 接口让回调签名是**编译期类型安全**的,比 EventBus / pub-sub 灵活但易错 +- 失败时 listener 抛异常被 FSM 静默捕获 + 日志,**不影响事务本身** + +### 2.6 与 transport 解耦:`MessageSender` SPI + +**实装**: +```java +@FunctionalInterface +public interface MessageSender { + CompletableFuture send(SipMessage message, Endpoint peer); +} +``` + +事务层依赖 `MessageSender`(单方法 SPI),**不依赖 `Transport`**。 + +**理由**: +- 单元测试用 `RecordingSender` 把所有发送捕获到 list,不需要真实 socket +- 18 个事务测试 1.5 秒内跑完,**无任何 I/O** +- 集成测试时把真实 `NioUdpTransport::send` 作为 lambda 传入 + +--- + +## 3. Consequences + +### 正面 +- FSM 实现紧贴 RFC §17 状态图,代码可对照 review +- 单元测试简单(无需 mock framework,只需 RecordingSender) +- 定时器内存开销小(每个事务约 3-5 个活跃定时器,单条目 < 200 bytes) +- 与 transport 解耦使得未来切 Netty / 虚拟线程切回平台线程都不影响事务层 + +### 负面 +- 4 个 FSM 类有 30-40% 共享逻辑(终止、CAS 状态迁移),用 helper 提取后还有少量重复 +- `ScheduledExecutorService` 在万级并发下定时器精度约 ±10ms(够用,但对 IMS 级运营商场景偏低) +- 没有 transaction snapshot / replay 机制,崩溃后事务状态丢失(**生产 SIP 应用同样有这个语义**,不是 bug) + +### 中性 +- 不再借鉴 JAIN-SIP 的"通用 transaction state"风格,新人需要熟悉四个独立 FSM 各自的状态名 + +--- + +## 4. Key RFC 3261 §17 Behavior Captures + +| 行为 | 实装位置 | +|------|---------| +| INVITE 客户端收 2xx → 立即 Terminated(TU 负责 ACK) | `InviteClientTransaction.onResponse` | +| INVITE 客户端收非 2xx → 自动构造发送 ACK(§17.1.1.3) | `InviteClientTransaction.sendAckForNonSuccess` | +| INVITE 服务端收 ACK → Completed → Confirmed | `InviteServerTransaction.onRequest` | +| Non-INVITE 服务端在 Completed 收 retransmit → 重发上一响应 | `NonInviteServerTransaction.onRequest` | +| Timer A / E / G 指数退避,封顶 T2 | `Timing.timerA/E/G(attempt)` | +| 可靠传输上 Timer D/I/J/K = 0 | `Timing.TIMER_*_MS_RELIABLE` | + +--- + +## 5. When to Reconsider + +1. 撞上**单进程 100K+ 并发事务**且 ScheduledExecutorService 时间精度成为瓶颈 → 切换 HashedWheelTimer 实现 `TimerScheduler` +2. 引入 **PRACK(RFC 3262)** / **UPDATE(RFC 3311)** —— 这两个会复用现有 FSM,但 PRACK 的"可靠 provisional"逻辑可能需要在 INVITE Server FSM 上扩展状态 +3. 引入**事务状态持久化**(崩溃恢复 / 集群迁移)—— 需要把 `AtomicReference` 改造为事件溯源 + +--- + +## 6. References + +- RFC 3261 §17 — Transactions +- RFC 3261 §A — Timer values +- RFC 3262 — PRACK (future) +- ADR-0001 — Transport layer selection (虚拟线程基础) + +## 7. 变更记录 + +| 日期 | 变更 | 作者 | +| --- | --- | --- | +| 2026-05-11 | 创建并记为 Accepted | 项目维护者 | diff --git a/docs/adr/README.md b/docs/adr/README.md index 86fa3f7..0c04e43 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -26,3 +26,4 @@ | 编号 | 标题 | 状态 | | ---- | ---- | ---- | | [ADR-0001](0001-transport-layer-selection.md) | 传输层选型:JDK 21 虚拟线程 + NIO vs Netty | Accepted | +| [ADR-0002](0002-transaction-layer-design.md) | 事务层设计:四 FSM + 时间轮 + 虚拟线程 | Accepted |