目录 [隐藏] 把终端会话当成聊天对话情绪价值:萌宠头像还有些顺手的细节试试看 这两年写代码的方式变了。以前是一个终端、一个项目、一条命令跑到底。现在桌面上常年开着三四个 AI 编程 Agent —— Claude Code 在改一个模块,Codex 在另一个仓库跑重构,Kiro CLI 在补测试,旁边还挂着 SSH、构建、日志。然后传统终端就有点撑不住了。Tab 和窗口多到记不清哪个是哪个;哪个 Agent 跑完了在等我、哪个还在 thinking、哪个报错停住了,全靠一个个点过去看;AI 会话、普通 Shell、日志窗口混在一排 Tab 里,切换成本高得离谱;手一抖关掉终端,一堆上下文就没了。但是微信我们可以同时和十几个人聊天:谁有新消息就亮个红点,点进去就是完整上下文。为什么终端会话不能这样管?于是有了 ChatTerm。把终端会话当成聊天对话ChatTerm 是一个终端工作区 —— 在真实终端之上,加了一层 IM 风格的会话管理。一句话:把终端会话,当成聊天软件里的对话来管。左边一列 IM 风格的会话列表,每个会话有头像、有最新输出预览、有未读红点;它能认出哪个会话是 Claude Code、哪个是 Codex、哪个是 Kiro、哪个只是普通 Shell,自动配上对应的头像和状态;它能看出一个 Agent 此刻是在 thinking 还是 idle —— 侧边栏一眼就分清谁在干活、谁在等你回话;Agent 回了什么,侧边栏直接给你回复预览,不用切进去看。预览这块,我没用"截屏 OCR"那种脆办法。Claude Code、Codex、Kiro 都支持在 Stop、PostToolUse 这类事件上挂 hook —— ChatTerm 就在这些 hook 上挂一个小脚本,通过命名管道(FIFO)把事件喂回应用。干净、实时;而且 ChatTerm 没开着的时候,消息直接丢弃,绝不会卡住你的 Agent。情绪价值:萌宠头像初次之外，还是提供了情绪价值 —— 萌宠头像。一键就能把头像方块换成会眨眼、会摇摆、会喊「!」的小动物,表情和气泡还跟着会话状态走:thinking、asking、error、done、idle、有未读…… 各有各的反应。盯一整天 AI 写代码,侧边栏有只小动物陪你一起 thinking。还有些顺手的细节会话持久化 —— 重启后会话列表自动恢复,Agent 会话还能 --resume 接上次的上下文;⌘K 搜索 —— 按名字、工作目录,甚至按输出内容,快速跳到某个会话;主题 —— 内置几套配色,macOS 还能直接导入系统 Terminal 的 .terminal 主题。技术上,前端是 React 19 + TypeScript + xterm.js,后端 Rust + Tauri 2 + portable-pty —— 一个轻量的原生应用,不是又一个 Electron 壳。试试看macOS、Linux、Windows 都有包。最快的方式是一行装好 App,再一行配 Agent hooks:curl -fsSL https://raw.githubusercontent.com/chatterm/chatterm/main/scripts/install-remote.sh | bash curl -fsSL https://raw.githubusercontent.com/chatterm/chatterm/main/scripts/setup-hooks.sh | bash源码、Releases、各平台详细安装说明都在 GitHub —— github.com/chatterm/chatterm。如果你也同时盯着好几个 AI Agent 干活,欢迎试试,也欢迎来提 issue。
目录 [隐藏] TCP 丢包检测：为什么重要？SACK：Selective Acknowledgment（选择性确认）FACK：Forward Acknowledgment（前向确认）RACK：Recent Acknowledgment（基于时间的确认）TLP：Tail Loss Probe（尾部丢包探测）RACK-TLP：现代 TCP 的黄金组合对比总结 在传输控制协议（TCP）数十年的演进中，丢包检测与恢复机制始终是影响传输性能的核心环节。从最初依赖序列号判断丢包的 Reno 模式，到引入选择性确认的 SACK，再到基于时间判定的 RACK，乃至进一步强化的 RACK-TLP，TCP 的恢复能力经历了逐步现代化的升级。本文将带你从理论层面，深入理解 TCP 的四种关键丢包检测机制：FACK、SACK、RACK 与 RACK-TLP，并对比它们的工作原理、适用场景与演进关系。TCP 丢包检测：为什么重要？丢包是 TCP 面临的常见网络问题。能否及时发现丢包并快速恢复，直接决定了 TCP 在以下场景中的表现：弱网或移动网络（丢包率高、时延抖动大）；数据中心高吞吐场景（对恢复速度敏感）；中间设备存在乱序、ACK 压缩行为。传统 TCP 采用 RTO（Retransmission Timeout） 机制来兜底触发重传。但初始 RTO 延迟高达 1 秒，误判多，恢复慢，成为瓶颈。因此，TCP 社区不断探索更快、更精准的丢包检测算法。SACK：Selective Acknowledgment（选择性确认）原理：接收方在 ACK 中显式告诉发送方：“我已经收到哪些数据段”，而不是仅告诉“收到到哪了”。例如：如果收到了第 1、2、5 段，SACK 可以告诉发送方：我收到了 [1-2] 和 [5]，中间 [3-4] 丢失。优点：精确指出哪些段被接收；适用于多段丢包场景；避免不必要的重复重传。局限：虽然能知道“哪里没收到”，但仍依赖序列号判断丢包；容易受到网络乱序、ACK 压缩的干扰，误判丢包或延迟重传。FACK：Forward Acknowledgment（前向确认）原理：FACK 在 SACK 的基础上进一步演化。它不仅看 SACK 报告了哪些段，还估算：“接收方到底收到了多少字节？”通过前向确认值（Forward ACK），FACK 推测数据流的最前进位置，从而判断哪些段在这个位置之前却没被确认，更可能丢失。优点：提升丢包恢复的粒度与速度；可以更准确判断哪些段未收到。局限：依然依赖序列号推断；在中间设备导致乱序或 ACK 抖动时仍可能误判。RACK：Recent Acknowledgment（基于时间的确认）定义于 RFC 8985原理：RACK 完全抛弃了“序列号先后”来判断丢包，转而使用“发送时间”作为核心依据。核心思想：如果一个数据段比其他段更早发送，但对方先确认了后发的数据，那么早发未确认的很可能丢了。举个例子：第 10、11、12 段几乎同时发送；ACK 收到 11 和 12，唯独没收到 10；RACK 判断：第 10 段被丢了，不等 RTO，直接重传。优点：更快发现丢包；不受乱序、ACK 压缩等影响；大幅提升弱网或数据中心 TCP 的表现。前提：TCP 栈需准确记录每个段的发送时间；依赖 SACK 辅助判断哪些段被确认。TLP：Tail Loss Probe（尾部丢包探测）原理：TLP 设计是为了解决“尾部最后几个数据段被丢，但连接进入沉默”的问题。在传统 TCP 中，这种场景只能等待 RTO 超时，可能导致 1 秒以上延迟！TLP 的做法是：检测连接是否在“尾部”安静阶段；主动发一个探测包（probe）来“敲门”；如果 ACK 回来，一切正常；如果没有，尽快触发重传。优点：避免长时间挂起；恢复速度远快于 RTO。RACK-TLP：现代 TCP 的黄金组合RACK + TLP = 快速、鲁棒的 TCP 丢包检测与恢复体系RACK 提供 时间驱动的准确丢包检测，TLP 提供 尾部沉默保护，两者组合应对大多数网络复杂场景。这种组合是目前 Google、Facebook、AWS 等大规模生产网络中的默认选择，也正逐步成为现代 TCP 栈的主流机制。对比总结特性SACKFACKRACKRACK-TLP丢包判断依据序号 + SACK序号 + 前向确认发送时间发送时间 + 探测包抗乱序能力一般一般强强响应速度中等较快快快速且稳健实现复杂度中较高高高是否现代推荐（已基础）（中间形态）（现代默认）（最佳实践）TCP 的丢包检测机制已经从最早的基于序号推测，演进到现在以“发送时间 + 探测包”为核心的智能模型：FACK 是对 SACK 的增强，更好地衡量未确认数据；RACK 是范式转移，开创“时间驱动的丢包检测”；TLP 补齐了边界条件，保护连接不被挂起；RACK-TLP 组合，代表了现代 TCP 的最佳丢包恢复路径。
目录 [隐藏] 一 特性剖析1. TLS 1.3 VS 1.22. 安全增强2.1 PSK KE VS PSK ECHDE2.2 密钥派生3. 2-RTT VS 1-RTT4. 0-RTT Early Data5. 会话恢复二 测试分析1. 编译使用2. 测试汇总3. VS Code 调试4. Wireshark 抓包分析4.1 TLS 1.2 完整握手4.2 TLS 1.3 0-RTT Early Data4.3 会话恢复 这里结合 RFC8446，MbedTLS 源码， Wireshark 抓包对 TLS 1.3 几个重要特性做深入剖析。一 特性剖析1. TLS 1.3 VS 1.2https://datatracker.ietf.org/doc/html/rfc8446#section-1.2 。TLS 1.3 对比 TLS 1.2 的主要差异。项目条目TLS 1.2TLS 1.3加密与算法对称加密算法支持多种，包括已废弃的 CBC、RC4仅支持 AEAD 算法（如 AES-GCM、ChaCha20-Poly1305） 密钥派生算法自定义复杂结构 KDF使用统一的 HKDF（HMAC-based Extract-and-Expand） 密钥交换机制支持 RSA、DH、ECDHE，包括静态密钥移除静态 RSA/DH，统一使用 ECDHE，具备前向安全 签名算法支持RSA、DSA、ECDSARSA、DSA、ECDSA安全改进前向安全（PFS）可选，取决于是否使用 (EC)DHE所有公钥交换均具备前向安全 报文完整性保护加密和 MAC 分离（MAC-then-encrypt）使用 AEAD（集成加密+认证） 证书扩展等隐私保护扩展字段如 SNI 以明文发送扩展通过 EncryptedExtensions 加密传输握手机制握手消息加密ServerHello 后部分消息仍明文ServerHello 后全部握手消息加密 握手流程结构含 ServerHelloDone、ChangeCipherSpec精简状态机，移除冗余消息 0-RTT 数据支持不支持支持 0-RTT，允许部分早期数据发送（有安全限制）会话恢复会话恢复机制Session ID / Session ticket 两种方式统一为 PSK-based 会话恢复，使用 SessionTicket + Binder PSK 模式较少使用明确支持 PSK-only / PSK + ECDHE兼容性机制版本协商通过 ClientHello.version，存在兼容问题使用 extension 提供版本列表，增强兼容性 压缩与 DSA支持 TLS-level 压缩、支持 DSA移除压缩与 DSA，避免压缩攻击重点介绍全新特性前。介绍下 TLS 1.3 的完整握手流程。https://datatracker.ietf.org/doc/html/rfc8446#section-2如上完整握手流程，包含密钥交换（Key Exchange）和双向认证（Authentication，可选）流程。对于密钥交换仅支持 ECDHE。对于认证支持证书认证和 PSK 认证，当然也可以不做认证。如上 KE 和 Auth 流程组合，TLS 1.3 共支持如下5种模式。握手模式描述完整握手ECDHE使用 ECDHE 密钥交换，无认证。 PSK ECHDE使用预设密钥 PSK 快速认证 + ECDHE 密钥交换 证书 ECHDE使用证书进行身份验证 + ECDHE 密钥交换会话恢复PSK ECDHE基于 Session Ticket 快速恢复会话，流程与 PSK-ECDHE 类似，区别在于 PSK 来源不同：完整 PSK-ECDHE 使用预共享凭据；会话恢复则使用前次握手后协商出的临时 PSK。 PSK Only基于 Session Ticket 快速恢复会话，但是不做 ECHDE。需要说明的是，尽管不做 ECHDE，但是还有一个简单 KE，同 TLS 1.2 PSK KE 原理相似。在后面章节会详细介绍。2. 安全增强2.1 PSK KE VS PSK ECHDETLS 1.2 PSK KE 对比 TLS 1.3 PSK ECDHE 一个安全特性就是前向安全。 前向安全确保了长期密钥泄露不会影响历史会话的安全性。以下是 TLS 1.2 PSK KE 与 TLS 1.3 PSK ECDHE 的握手流程对比。在密钥交换过程中，最大的区别在于是否引入了临时椭圆曲线 Diffie-Hellman 的密钥交换。注意：如上为简化公式。完整的密钥派生会在后面详细介绍。在 TLS 1.2 PSK KE 模式下，会话密钥完全依赖预共享密钥（PSK），如果 PSK 泄露，攻击者可以离线解密之前或当前的通信内容。提示：ClientRandom 和 ServerRandom 为明文交换，所以 PSK 泄漏，MasterSecret 就会泄漏。在 TLS 1.3 PSK ECDHE 模式下，引入了临时的 ECDHE ClientShare 和 ServerShare 公钥交换，即使 PSK 被泄露，也无法恢复会话密钥，因此提供了前向安全性。提示：尽管 ClientShare、ServerShare、ClientRandom、ServerRandom 都是明文交换，但是 ECDH 共享密钥计算需要私钥参与。所以 MasterSecret 不会直接泄漏。2.2 密钥派生https://datatracker.ietf.org/doc/html/rfc8446#section-7.1如下流程为 TLS 1.3 完整的密钥派生流程。HKDF-Extract 为熵压缩器，可以看作是一个安全的“种子生成器” 。需要输入 salt 和 IKM（Input Key Material）。如下框图为从上方获取 Salt 参数，从左侧获取 IKM 参数，输出结果向下，其输出变量名写在右边。Derive-Secret 为密钥派生器，可以理解为如上“种子”长出多种不同用途的“树”。需要输入：Secret 和 Label 和 Messages。Derive-Secret(Secret, Label, Messages) = HKDF-Expand-Label(Secret, Label, Transcript-Hash(Messages), Hash.length)如下框图为从上方获取 Secret 参数，Label 和 Messages 已经包含在 Derive-Secret() 函数参数中。其中 ClientHello...ServerHello 表示两条消息的 hash 值。ClientHello...server Finished 表示从 ClientHello 到 Finished 所有消息的 hash 值。0 表示一个长度为 Hash.length 字节的全 0 字节串。首先需要具体说明每一个密钥的使用场景。阶段密钥描述Early Secret 早期密钥binder_key用于验证 PSK 的合法性（在 ClientHello 中计算 binder HMAC） client_early_traffic_secret用于 加密 0-RTT 数据（仅客户端发送） early_exporter_master_secret用于早期阶段的数据导出（如 token 等）Handshake Secret 握手阶段密钥client_handshake_traffic_secret加密客户端握手阶段消息，如 Certificate、Finished server_handshake_traffic_secret加密服务端握手阶段消息，如 Certificate、FinishedMaster Secret 主密钥server_application_traffic_secret_0加密服务端应用层数据 exporter_master_secret用于导出额外密钥材料（给上层协议或 token 用） resumption_master_secret会话恢复时用于生成 Session Ticket 中的 PSK如上密钥派生流程，主要输入信息为 PSK 和 ECDHE。上面我们已经讨论了 TLS 1.3 的5种不同模式。接下来我们看看不同模式的 PSK 和 ECDHE 信息。握手模式描述PSK完整握手ECDHE使用 ECDHE 密钥交换，无认证。无 PSK，有 ECHDE PSK ECHDE使用预设密钥 PSK 快速认证 + ECDHE 密钥交换预设 PSK，有 ECHDE 证书 ECHDE使用证书进行身份验证 + ECDHE 密钥交换无 PSK，有 ECDHE会话恢复PSK ECDHE基于 Session Ticket 快速恢复会话，流程与 PSK-ECDHE 类似，区别在于 PSK 来源不同：常规 PSK-ECDHE 使用预共享凭据；会话恢复则使用前次握手后协商出的临时 PSK。有 PSK，PSK = HKDF-Expand-Label(resumption_master_secret, "resumption", ticket_nonce, Hash.length)PSK 由 resumption_master_secret（上次会话派生出） 和 ticket_nonce（上次会话临时临时生成，通过 NewSessionTicket 发送给客户端）。有 ECDHE PSK Only基于 Session Ticket 快速恢复会话，但是不做 ECHDE。需要说明的是，尽管不做 ECHDE，但是还有一个简单 KE，同时 TLS 1.2 PSK KE 原理一致。在后面章节会详细介绍。有 PSK，PSK 同上。无 ECDHE。3. 2-RTT VS 1-RTTTLS 1.3 的一个重要特性是优化了握手阶段的消息交互效率，将原本的 2-RTT 优化为 1-RTT，主要通过以下两个机制实现：在 TLS 1.2 中，ClientKeyExchange 和 ServerKeyExchange 是独立消息；而在 TLS 1.3 中，这些密钥交换参数被整合进 ClientHello 和 ServerHello 的 key_share 和 pre_shared_key 扩展中，避免了额外的往返消息。对于身份验证相关的 Finished 消息，TLS 1.2 是由客户端先发送，TLS 1.3 则调整为服务端先发送，这使得 Finished 可以合并在服务端的响应路径中发出。另外对于 RTT 计算你可能还会有如下疑问。TLS 1.3 最后还需要发送 Finished 消息，应该算 1.5 RTT？对于客户端 Application Data 可以和 Finished 可以一起发送。对于 ServerHello 和 CertificateRequest 以及 Certificate 是单独发送为什么 RTT 算在一起？在 TLS 1.3 中，服务端从 ServerHello 开始，接下来可以批量发出：EncryptedExtensions → Certificate → CertificateVerify → Finished，中间不需要等待客户端的响应，因此这整个过程都算作第 1 个 RTT 内的服务端响应部分。如果 TCP RWND = 1 MSS，并且不支持 Trickle ，RTT 如何计算？在特定网络条件下（RWND = 1 MSS，不支持 Trickle），TLS 1.3 中服务端确实无法一次性发完所有握手消息，需要多个 TCP ACK 往返来推动 TLS 消息发出。在网络层面上 RTT 被拉长了：虽然 TLS 协议逻辑上是一轮 RTT完成握手，但实际 由于 TCP 的传输能力受限，ServerHello 后的证书链等可能被拆分成多个 TCP 分段发送。这个时候，从“时延体验”上看类似于 2-RTT，但协议本身仍然是 1-RTT 的握手逻辑。4. 0-RTT Early Datahttps://datatracker.ietf.org/doc/html/rfc8446#section-2.3首先说明应用场景。0-RTT Early Data 只能用以有 PSK 的握手流程。基于 Resumption PSK 会话恢复流程基于 External PSK 的完整握手流程不能在首次基于证书的完整握手流程中使用。提示：如图所示绿色握手消息表示 Early Data 特定流程。如上图所示ClientHello客户端发起握手，并尝试发送 Early Data，包含以下关键扩展：字段描述+ early_data告诉服务端：“我打算发送 0-RTT 数据”。仅作为信号，无内容。+ pre_shared_key包含 PSK identity（用于识别会话）和 binder（对 ClientHello 做 MAC 认证）。用于认证服务器并恢复密钥。(Application Data*)如果客户端在 ClientHello 中附带了 early_data 扩展，它可以立即附加加密的 Early Application Data（比如一个 HTTP 请求或 MQTT CONNECT）。该数据使用 client_early_traffic_secret 派生出的密钥进行加密。ServerHello服务端响应握手，并选择一个 PSK：字段描述+ pre_shared_key告诉客户端：“我接受了你提供的哪个 PSK identity”。这一决定决定了后续密钥派生路径。{EncryptedExtensions}这是 TLS 1.3 新增的加密消息，用于传输额外扩展字段：字段描述+ early_data*如果服务端接受了 0-RTT 数据，则必须发送该扩展，告知客户端 early data 被接受；否则不发送，表示拒绝 early data。(EndOfEarlyData)如果客户端确实发送了 Early Data，那么它在收到服务端 EncryptedExtensions 后，必须发送 EndOfEarlyData 消息。它是握手中的一个标记，告知服务端：早期应用数据（0-RTT）已经发送完毕。它也意味着后续将切换到 Handshake Secret 加密上下文。[Application Data*]这是服务端在握手完成后发送的正常 Application Data：使用 server_application_traffic_secret 加密；它可能包含服务端响应，例如 HTTP 响应或 MQTT CONNACK。总结：消息加密上下文作用说明ClientHello明文携带 PSK、声明 early_data 意图(Application Data*)使用 early secret 加密早期发送的业务数据ServerHello明文响应 ClientHello，指定使用哪个 PSK{EncryptedExtensions}Handshake Secret服务端确认是否接受 early_dataEndOfEarlyDataHandshake Secret客户端结束 early data 发送[Application Data*]Application Secret正式数据传输阶段开始5. 会话恢复https://datatracker.ietf.org/doc/html/rfc8446#section-2.2https://datatracker.ietf.org/doc/html/rfc5077会话恢复是基于完整 1-RTT 握手协议进一步优化的机制，旨在减少连接建立时的延迟开销。TLS 1.2 支持两种会话恢复机制：Session ID（服务端有状态）和 Session Ticket（服务端无状态）。TLS 1.3 延续了 Session Ticket 的会话恢复机制，但不再直接恢复 Master Secret，而是将 Resumption Secret 用作 PSK，重新进行密钥派生。该过程支持可选的 ECDHE 交换（PSK+ECDHE），进一步增强前向安全性。看看3种完整握手流程的在2种会话恢复下的收益。完整握手握手模式会话恢复恢复模式收益完整握手ECDHE会话恢复PSK ECDHE无收益 PSK Only省略 ECDHE 流程 PSK ECHDE会话恢复PSK ECDHE无收益，还增加 PSK 解密流程 PSK Only省略 ECDHE 流程 证书 ECHDE会话恢复PSK ECDHE省略证书如流程 PSK Only省略证书认证和 ECDHE 流程提示：如图所示绿色握手消息表示 PSK 会话恢复特定流程。https://datatracker.ietf.org/doc/html/rfc5077#section-4TLS 1.3 会话恢复的关键路径是：完成完整握手后，服务端通过 NewSessionTicket 向客户端下发一个加密的 Ticket。该 Ticket 由服务端的 Ticket Protection Key 加密，服务端不保存 Ticket 内容，因此这种机制被称为“无状态的会话恢复”。在后续会话恢复阶段，客户端将该 Ticket 原样通过 ClientHello 的 pre_shared_key 扩展回传，服务端解密后取出其中的 PSK，重新执行密钥派生流程，从而恢复会话。详细看看 Ticket 数据结构。https://datatracker.ietf.org/doc/html/rfc8446#section-4.6.1struct { uint32 ticket_lifetime; // 客户端可使用该 Ticket 的有效时间（单位：秒） uint32 ticket_age_add; // 随机扰动值，防止重放攻击，服务端生成 opaque ticket_nonce<0..255>; // 用于导出 PSK 的 nonce 值（参与 HKDF） opaque ticket<1..2^16-1>; // 实际 Ticket 内容（密文） Extension extensions<0..2^16-2>; // 可选扩展，例如 early_data 支持等 } NewSessionTicket;https://datatracker.ietf.org/doc/html/rfc5077#section-4struct { opaque key_name[16]; // 标识用于加密该 ticket 的密钥名称 opaque iv[16]; // AEAD 加密使用的初始化向量 opaque encrypted_state<0..2^16-1>; // 加密的 session state（服务端状态） opaque mac[32]; // AEAD 加密产生的认证标签（MAC） } ticket;提示：而对于 PSK 加密的 encrypted_state 数据结构在 RFC 8446 和 RFC 5077 均没有详细定义。需要结合 https://github.com/Mbed-TLS/mbedtls/blob/v3.6.3/library/ssl_tls.c#L3818 代码。struct { uint32_t ticket_age_add; // 与 NewSessionTicket 中一致，用于计算实际 age uint8_t ticket_flags; // 标志位，如是否支持 0-RTT 等 uint8_t resumption_key_len; // PSK 长度（通常是 32） uint8_t resumption_key[32]; // 客户端用于恢复 early_secret 的 PSK uint32_t ticket_lifetime; // Ticket 生命周期，服务端控制 uint16_t ticket_len; // ticket ID 长度 uint8_t ticket[ticket_len]; // 唯一标识该 Session Ticket 的 ID // 以下为可选字段（根据编译选项启用） uint64_t ticket_creation_time; // Ticket 创建时间（用于服务端过期检查） uint32_t max_early_data_size; // 客户端支持的最大 Early Data 大小 uint16_t record_size_limit; // 最大记录大小限制 uint16_t hostname_len; // 主机名长度（客户端 SNI） uint8_t hostname[hostname_len]; // 主机名内容（如有） uint16_t alpn_len; // ALPN 协议长度 uint8_t alpn[alpn_len]; // ALPN 协议内容 } encrypted_state;TLS 1.3 的会话恢复不同于 TLS 1.2 的直接恢复 Master Secret。其核心增强在使用 resumption_key 作为 PSK，重新执行一轮完整的密钥派生，从而获得前向安全。分别看看客户端和服务端如何持有 PSK 。客户端在接收到 NewSessionTicket 后，根据 ticket_nonce 和本地的 resumption_master_secret 派生出会话恢复用的 PSK：对于 Server 端，并不存储 Ticket，而是等待客户端在 ClientHello 的 pre_shared_key 直接将加密 ticket 回传给服务端，使用 Ticket Protection Key 解密后直接取出 encrypted_state 中的 resumption_key 作为 PSK。对于 Ticket Protection Key 的管理，不再 RFC 讨论中，Mbed TLS 做了双密钥的轮换管理。https://github.com/Mbed-TLS/mbedtls/blob/22098d41c6620ce07cf8a0134d37302355e1e5ef/library/ssl_ticket.c#L151-L209/* * Rotate active session ticket encryption key */ int mbedtls_ssl_ticket_rotate(mbedtls_ssl_ticket_context *ctx, const unsigned char *name, size_t nlength, const unsigned char *k, size_t klength, uint32_t lifetime)二 测试分析1. 编译使用这里使用 Mbed TLS v3.6.3 官方仓库 programs/ssl/ssl_client2.c 和 programs/ssl/ssl_server2.c 完成测试。直接参考工程 README.md 编译 ssl_client2 和 ssl2_server2。例如 Make 直接在根目录运行 make 即可。git clone https://github.com/Mbed-TLS/mbedtls.git git submodule update --init cd mbedtls make对于不同测试可以参考 Mbed TLS 的测试用例，比如 0-RTT （Early Data）。测试脚本会告诉你使能哪些宏，使用哪些参数，以及输出日志。tests/opt-testcases/tls13-misc.sh#L235-L266requires_config_enabled MBEDTLS_SSL_PROTO_TLS1_3 requires_config_enabled MBEDTLS_SSL_CLI_C requires_config_enabled MBEDTLS_SSL_SRV_C requires_config_enabled MBEDTLS_SSL_SESSION_TICKETS requires_config_enabled MBEDTLS_HAVE_TIME requires_config_enabled MBEDTLS_SSL_EARLY_DATA requires_config_enabled MBEDTLS_DEBUG_C requires_config_enabled MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_EPHEMERAL_ENABLED requires_any_configs_enabled MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_PSK_ENABLED \ MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_PSK_EPHEMERAL_ENABLED run_test "TLS 1.3 m->m: resumption with early data" \ "$P_SRV debug_level=4 early_data=1 crt_file=../framework/data_files/server5.crt key_file=../framework/data_files/server5.key" \ "$P_CLI debug_level=3 early_data=1 new_session_tickets=1 reco_mode=1 reconnect=1" \ 0 \ -c "Protocol is TLSv1.3" \ -c "Saving session for reuse... ok" \ -c "Reconnecting with saved session" \ -c "HTTP/1.0 200 OK" \ -c "received max_early_data_size" \ -c "NewSessionTicket: early_data(42) extension received." \ -c "ClientHello: early_data(42) extension exists." \ -c "EncryptedExtensions: early_data(42) extension received." \ -c "bytes of early data written" \ -C "0 bytes of early data written" \ -s "Protocol is TLSv1.3" \ -s "key exchange mode: psk" \ -s "Select PSK ciphersuite" \ -s "Sent max_early_data_size" \ -s "NewSessionTicket: early_data(42) extension exists." \ -s "ClientHello: early_data(42) extension exists." \ -s "EncryptedExtensions: early_data(42) extension exists." \ -s "early data bytes read"如上宏配置可以通过 scripts/config.py 检查和设置当前配置。scripts/config.py --file include/mbedtls/mbedtls_config.h get MBEDTLS_SSL_EARLY_DATA ;echo $? #1表示未使能，0表示使能 scripts/config.py --file include/mbedtls/mbedtls_config.h set MBEDTLS_SSL_EARLY_DATA scripts/config.py --file include/mbedtls/mbedtls_config.h get MBEDTLS_SSL_EARLY_DATA ;echo $? #1表示未使能，0表示使能对于当前需要测试的会话恢复。其他宏都默认使能（当前你可以通过 get 检查），你只需要使能如上 MBEDTLS_SSL_EARLY_DATA。scripts/config.py --file include/mbedtls/mbedtls_config.h set MBEDTLS_SSL_EARLY_DATA make ls programs/ssl/ssl_*2TLS 1.2 PSK KE 本地连接测试。服务端programs/ssl/ssl_server2 debug_level=3 nbio=0 min_version=tls12 max_version=tls13 auth_mode="required" tickets=0 server_addr=127.0.0.1 force_ciphersuite=TLS-PSK-WITH-AES-128-GCM-SHA256 psk=6162636465666768696a6b6c6d6e6f70 psk_identity="PSK Identity"客户端programs/ssl/ssl_client2 nss_keylog=1 nss_keylog_file=tls_client_nss_key.log debug_level=3 nbio=0 server_name="localhost" tickets=0 server_addr=127.0.0.1 force_version=tls12 psk=6162636465666768696a6b6c6d6e6f70 psk_identity="PSK Identity"TLS 1.3 PSK ECDHE 本地连接测试。服务端programs/ssl/ssl_server2 debug_level=3 nbio=0 min_version=tls12 max_version=tls13 auth_mode="required" force_ciphersuite=TLS1-3-AES-128-GCM-SHA256 tickets=1 server_addr=127.0.0.1 psk=6162636465666768696a6b6c6d6e6f70 psk_identity="PSK Identity"客户端programs/ssl/ssl_client2 nss_keylog=1 nss_keylog_file=tls_client_nss_key.log debug_level=3 nbio=0 server_name="localhost" tickets=0 new_session_tickets=0 reco_mode=0 server_addr=127.0.0.1 server_addr=127.0.0.1 force_version=tls13 psk=6162636465666768696a6b6c6d6e6f70 psk_identity="PSK Identity"TLS 1.3 双向证书 ECDHE 本地连接测试。服务端programs/ssl/ssl_server2 debug_level=3 nbio=0 min_version=tls12 max_version=tls13 auth_mode="required" crt_file=framework/data_files/server5.crt key_file=framework/data_files/server5.key客户端programs/ssl/ssl_client2 nss_keylog=1 nss_keylog_file=tls_client_nss_key.log debug_level=3 nbio=0 server_name="localhost" tickets=0 new_session_tickets=0 reco_mode=0 server_addr=127.0.0.1 server_addr=127.0.0.1 force_version=tls13 auth_mode="required" crt_file=framework/data_files/server5.crt key_file=framework/data_files/server5.key key_opaque_algs=ecdsa-sign,noneTLS 1.3 开启 Session Tickets，进行会话恢复。服务端programs/ssl/ssl_server2 debug_level=3 nbio=0 min_version=tls12 max_version=tls13 auth_mode="required" force_ciphersuite=TLS1-3-AES-128-GCM-SHA256 tickets=1 server_addr=127.0.0.1 psk=6162636465666768696a6b6c6d6e6f70 psk_identity="PSK Identity"客户端programs/ssl/ssl_client2 nss_keylog=1 nss_keylog_file=tls_client_nss_key.log debug_level=3 nbio=0 server_name="localhost" tickets=1 new_session_tickets=1 reco_mode=0 server_addr=127.0.0.1 server_addr=127.0.0.1 force_version=tls13 psk=6162636465666768696a6b6c6d6e6f70 psk_identity="PSK Identity" reconnect=1 TLS 1.3 0-RTT Early Data 测试。服务端programs/ssl/ssl_server2 debug_level=3 nbio=0 min_version=tls12 max_version=tls13 auth_mode="required" force_ciphersuite=TLS1-3-AES-128-GCM-SHA256 tickets=1 server_addr=127.0.0.1 psk=6162636465666768696a6b6c6d6e6f70 psk_identity="PSK Identity" early_data=1 max_early_data_size=200客户端programs/ssl/ssl_client2 nss_keylog=1 nss_keylog_file=tls_client_nss_key.log debug_level=3 nbio=0 server_name="localhost" tickets=1 new_session_tickets=1 reco_mode=0 server_addr=127.0.0.1 server_addr=127.0.0.1 force_version=tls13 psk=6162636465666768696a6b6c6d6e6f70 psk_identity="PSK Identity" reconnect=1 early_data=1 2. 测试汇总如下测试日志汇总通过此脚本自动生成 run_tls_test.sh。测试用例抓包文件客户端日志服务端日志Key LogTLS 1.2 PSK KEtls12_psk_capture.pcapngtls12_psk_client.logtls12_psk_server.logtls12_psk_nss_key.logTLS 1.3 PSK ECDHEtls13_psk_ecdhe_capture.pcapngtls13_psk_ecdhe_client.logtls13_psk_ecdhe_server.log TLS 1.3 证书 ECDHEtls13_cert_capture.pcapngtls13_cert_client.logtls13_cert_server.log TLS 1.3 会话恢复tls13_session_resumption_capture.pcapngtls13_session_resumption_client.logtls13_session_resumption_server.log TLS 1.3 0-RTT Early Datatls13_0rtt_capture.pcapngtls13_0rtt_client.logtls13_0rtt_server.log 3. VS Code 调试为了方便 VS Code 单步调试走断点阅读代码，你需要编译配置优化和调试等级。scripts/config.py --file include/mbedtls/mbedtls_config.h set MBEDTLS_SSL_EARLY_DATA make make CFLAGS='--coverage -g3 -O0' LDFLAGS='--coverage'参考 Using C++ on Linux in VS Code 或者 Using Clang in Visual Studio Code 进行 VSCode 调试。这里把如上所有测试命令配置 launch.json。直接保存在 根目录 .vscode 即可以调试。4. Wireshark 抓包分析因为 Wireshark 暂时还不支持 TLS 1.3 SSLKEYLOGFILE https://wiki.wireshark.org/TLS 解密。所以 TLS 1.3 信息有限。但是对于一些关键扩展和握手消息还是值得分析。4.1 TLS 1.2 完整握手因为 TLS 1.3 握手消息都有加密，这里解释 TLS 1.2 PSK KE 抓包分析。使用 Wireshark 打开 tls12_psk_capture.pcapng 抓包文件。Wireshark -> Preferences... -> (Pre)-Master-Secret log filename -> tls12_psk_nss_key.logWireshark -> Statistics -> Flow Graph4.2 TLS 1.3 0-RTT Early Datatls13_0rtt_capture.pcapngEarly Data 指示Early Data 数据4.3 会话恢复tls13_session_resumption_capture.pcapng完整握手初次会话，预设 PSK。会话恢复，同时包含预设 PSK 和会话恢复的 PSK，前面一个为 Ticket。
目录 [隐藏] 1 错误模型2 MAC 验证3 SpectrumWiFiPhy3.1 饱和性能3.2 包误码率性能3.3 干扰性能4 Bianchi 验证5 多用户传输验证 本文翻译自，https://www.nsnam.org/docs/release/3.43/models/html/wifi-testing.html#spectrumwifiphy 。目前，大多数关于测试和验证的文档都存在于出版物中，以下列出了一些参考文献。1 错误模型802.11b 错误模型的验证结果可在以下 技术报告 中查看。需要注意以下两点说明： 上述参考文献中的图 1-4 对应的是 ns-3 NIST 比特错误率（BER）模型。在该论文附录的程序（80211b.c）中，有两个用于生成数据的常量。第一个是分组大小，设置为 1024 字节。第二个是“噪声”，其值设为 7 dB；这一值是通过经验选取的，以使曲线与 CMU 测试床的报告数据最契合。虽然值为 1.55 dB 更符合 CMU 论文报告的 -99 dBm 噪声底限，但噪声系数 7 dB 最适合与 CMU 实验数据拟合。这一默认值 7 dB 是 ns3::YansWifiPhy 模型中的 RxNoiseFigure。更改噪声系数会使曲线向左或向右移动，但不会改变其斜率。可以通过运行 examples/wireless/wifi-clear-channel-cmu.cc 示例程序重现这些曲线。当启用 GNU 科学库（GSL）时，会生成以下图表：802.11b 的清晰信道（AWGN）错误模型。802.11b 的清晰信道（AWGN）错误模型802.11a/g OFDM 错误模型的验证结果可在以下 技术报告 中查看。 可以通过运行 examples/wireless/wifi-ofdm-validation.cc 示例程序重现这些曲线，以下图表展示了 802.11a/g（OFDM）Wi-Fi 的帧错误率（NIST 模型）。802.11a/g（OFDM）Wi-Fi 的帧错误率（NIST 模型）对于 802.11n/ac/ax，可以通过运行 wifi-ofdm-ht-validation.cc、wifi-ofdm-vht-validation.cc 和 wifi-ofdm-he-validation.cc 示例程序生成类似曲线。以下分别展示了 802.11n（HT OFDM）Wi-Fi 的帧错误率（NIST 模型）、802.11ac（VHT OFDM）Wi-Fi 的帧错误率（NIST 模型） 和 802.11ax（HE OFDM）Wi-Fi 的帧错误率（NIST 模型）。但这些曲线目前尚无验证。802.11b 的清晰信道（AWGN）错误模型802.11n（HT OFDM）Wi-Fi 的帧错误率（NIST 模型）802.11ax（HE OFDM）Wi-Fi 的帧错误率（NIST 模型）2 MAC 验证802.11 DCF MAC 层的验证已在 [baldo2010] 中完成。802.11 PCF 的操作通过运行 wifi-pcf 示例（启用 PCAP 文件生成），并使用 Wireshark 观察帧交换得以验证。3 SpectrumWiFiPhySpectrumWifiPhy 实现已通过饱和和分组错误率程序（下文描述）进行验证，其结果与传统的 YansWifiPhy 一致。测试中通过切换这两种物理层实现进行比较。一个基本单元测试通过手动构造的分组注入接收的 PHY 对象，控制每个分组到达的时间和接收功率，并检查接收结果。然而，大多数对 SpectrumWifiPhy 的测试是使用下文描述的示例程序，以及一个独立的 LTE/Wi-Fi 共存研究中完成的（未在此文档中记录）。3.1 饱和性能程序 examples/wireless/wifi-spectrum-saturation-example.cc 允许用户选择 SpectrumWifiPhy 或 YansWifiPhy 进行饱和测试。用户可以通过参数 '--wifiType=ns3::YansWifiPhy' 或 '--wifiType=ns3::SpectrumWifiPhy' 切换物理层类型。输出结果没有差异，这符合预期，因为该测试主要验证的是 DCF，而不是物理层。默认情况下，程序将使用 SpectrumWifiPhy，并在 10 秒内发送饱和 UDP 数据，启用 802.11n 功能。输出显示主流 802.11n 速率（短保护间隔和长保护间隔）的结果：wifiType: ns3::SpectrumWifiPhy distance: 1m index MCS width Rate (Mb/s) Tput (Mb/s) Received 0 0 20 6.5 5.81381 4937 1 1 20 13 11.8266 10043 2 2 20 19.5 17.7935 15110 3 3 20 26 23.7958 20207 4 4 20 39 35.7331 30344 5 5 20 52 47.6174 40436 6 6 20 58.5 53.6102 45525 7 7 20 65 59.5501 50569 ... 63 15 40 300 254.902 216459上述输出显示了前 8 种模式（共 32 种）及最后一种模式的结果。前 8 种模式对应未启用短保护间隔和信道绑定的情况。后续 24 种模式分别为启用短保护间隔（第 9 至 16 种模式），然后启用信道绑定，再依次禁用和启用短保护间隔（第 17 至 32 种模式）。第 33 至 64 种模式在两条空间流（MIMO 抽象）下重复上述配置。运行以下命令可使用 YansWifiPhy：./ns3 run "wifi-spectrum-saturation-example --wifiType=ns3::YansWifiPhy"输出结果：wifiType: ns3::YansWifiPhy distance: 1m index MCS width Rate (Mb/s) Tput (Mb/s) Received 0 0 20 6.5 5.81381 4937 1 1 20 13 11.8266 10043 2 2 20 19.5 17.7935 15110 3 3 20 26 23.7958 20207 ...这符合预期，因为在这种情况下，YansWifiPhy 和 SpectrumWifiPhy 使用相同的误码率模型。3.2 包误码率性能程序 examples/wireless/wifi-spectrum-per-example.cc 允许用户选择 SpectrumWifiPhy 或 YansWifiPhy，并选择节点间的距离，同时记录接收统计数据和接收 SNR（通过 WifiPhy::MonitorSnifferRx 跟踪源观察）。程序使用 Friis 传播损耗模型。将发送功率从默认的 40 mW（16 dBm）降低到 1 dBm，以降低基线 SNR；用户还可以通过更改节点间的距离进一步调整 SNR。默认情况下，程序会在 50 米距离下，针对 10 秒的仿真时间，对从 0 到 31 的速率索引逐步测试，生成如下输出：wifiType: ns3::SpectrumWifiPhy distance: 50m; time: 10; TxPower: 1 dBm (1.3 mW) index MCS Rate (Mb/s) Tput (Mb/s) Received Signal (dBm) Noise (dBm) SNR (dB) 0 0 6.50 5.77 7414 -79.71 -93.97 14.25 1 1 13.00 11.58 14892 -79.71 -93.97 14.25 2 2 19.50 17.39 22358 -79.71 -93.97 14.25 3 3 26.00 22.96 29521 -79.71 -93.97 14.25 4 4 39.00 0.00 0 N/A N/A N/A 5 5 52.00 0.00 0 N/A N/A N/A 6 6 58.50 0.00 0 N/A N/A N/A 7 7 65.00 0.00 0 N/A N/A N/A如上饱和性能测试中的情况，使用 YansWifiPhy 运行此程序将生成相同的输出。3.3 干扰性能程序 examples/wireless/wifi-spectrum-per-interference.cc 基于上述包误码率示例，但从未许可 LTE 干扰测试中引入了 WaveformGenerator，允许用户通过命令行（使用 --waveformPower 参数）注入非 Wi-Fi 信号。与包误码率示例程序相比，此程序的发送功率恢复为默认的 40 mW（16 dBm）。默认情况下，干扰发生器关闭，程序行为类似于其他包误码率示例；通过添加少量功率（例如 --waveformPower=0.001），可以观察到 SNR 降低和帧丢失。没有干扰时的部分输出如下：./ns3 run "wifi-spectrum-per-interference" wifiType: ns3::SpectrumWifiPhy distance: 50m; time: 10; TxPower: 16 dBm (40 mW) index MCS Rate (Mb/s) Tput (Mb/s) Received Signal (dBm)Noi+Inf(dBm) SNR (dB) 0 0 6.50 5.77 7414 -64.69 -93.97 29.27 1 1 13.00 11.58 14892 -64.69 -93.97 29.27 2 2 19.50 17.39 22358 -64.69 -93.97 29.27 3 3 26.00 23.23 29875 -64.69 -93.97 29.27 4 4 39.00 34.90 44877 -64.69 -93.97 29.27 5 5 52.00 46.51 59813 -64.69 -93.97 29.27 6 6 58.50 52.39 67374 -64.69 -93.97 29.27 7 7 65.00 58.18 74819 -64.69 -93.97 29.27 ...添加少量波形功率后，较高阶调制可能因较低 SNR 出现帧丢失：./ns3 run "wifi-spectrum-per-interference --waveformPower=0.001" wifiType: ns3::SpectrumWifiPhy distance: 50m; sent: 1000 TxPower: 16 dBm (40 mW) index MCS Rate (Mb/s) Tput (Mb/s) Received Signal (dBm)Noi+Inf(dBm) SNR (dB) 0 0 6.50 5.77 7414 -64.69 -80.08 15.38 1 1 13.00 11.58 14892 -64.69 -80.08 15.38 2 2 19.50 17.39 22358 -64.69 -80.08 15.38 3 3 26.00 23.23 29873 -64.69 -80.08 15.38 4 4 39.00 0.41 531 -64.69 -80.08 15.38 5 5 52.00 0.00 0 N/A N/A N/A 6 6 58.50 0.00 0 N/A N/A N/A 7 7 65.00 0.00 0 N/A N/A N/A ...选择 ns3::YansWifiPhy 时，不会启用波形发生器，因为只有 YansWifiPhy 类型的发送器可以连接到 YansWifiChannel。4 Bianchi 验证程序 src/wifi/examples/wifi-bianchi.cc 允许用户将 ns-3 模拟结果与 [bianchi2000] 和 [bianchi2005] 提出的 Bianchi 模型进行比较。MATLAB 脚本用于生成 Bianchi 模型的代码及其输出，可在 src/wifi/examples/reference 文件夹中找到。用户可运行 MATLAB 脚本 generate_bianchi.m 以重新生成 Bianchi 结果。默认情况下，程序 src/wifi/examples/wifi-bianchi.cc 模拟一个 802.11a 自组网络场景，PHY 速率设置为 54 Mbit/s，并从 5 个站点循环到 50 个站点（每次增加 5 个站点）。程序生成一个 plt 文件，用户可以使用 Gnuplot 快速生成 eps 文件并可视化图形。5 多用户传输验证OFDMA 支持的实现已通过理论模型 [magrin2021mu] 进行验证。在非饱和条件下的延迟、饱和条件下的吞吐量以及 UL OFDMA 的传输范围，已在 [avallone2021wcm] 中提供初步评估。
目录 [隐藏] 1. 模型概览1.1. 高层 MAC 模型1.2. 低层 MAC 模型1.3. 物理层模型2. 范围与限制3. 设计细节3.1. WifiPhy 和相关模型3.1.1. PhyEntity3.1.2. WifiPpdu3.1.3. YansWifiPhy 和 WifiPhyStateHelper3.1.4. InterferenceHelper3.1.5. ErrorRateModel3.1.6. TableBasedErrorRateModel3.1.7. Legacy ErrorRateModels3.1.8. SpectrumWifiPhy3.1.9. MU-MIMO PHY 支持3.2. MAC 模型3.2.1. 基础架构模式中的关联3.2.2. 漫游3.2.3. 信道访问3.2.4. Frame Exchange Managers3.2.5. MAC 队列3.2.6. 多用户传输3.2.7. 多用户调度器3.2.8. 轮询多用户调度器3.2.9. 增强的多链路单无线电操作（EMLSR）3.2.10. 确认管理器3.2.11. WifiDefaultAckManager3.2.12. 保护管理器3.2.13. WifiDefaultProtectionManager3.2.14. 速率控制算法3.2.15. ConstantRateWifiManager3.2.16. IdealWifiManager3.2.17. ThompsonSamplingWifiManager3.2.18. MinstrelWifiManager3.2.19. MinstrelHtWifiManager3.2.20. 802.11ax OBSS PD 空间复用3.2.21. OBSS PD 算法3.2.22. 恒定 OBSS PD 算法3.2.23. 修改 Wi-Fi 模型 最近太想进步了，开始研究 NS-3 Wi-Fi 仿真模型。本文翻译自，https://www.nsnam.org/docs/models/html/wifi-design.html 。ns-3 节点可以包含多个 NetDevice 对象，就像一台实际的计算机可以包含多个独立的网络接口卡（如以太网、Wi-Fi、蓝牙等）。本章节描述了 ns-3 的 WifiNetDevice 及其相关模型。通过将 WifiNetDevice 对象添加到 ns-3 节点中，可以创建基于 802.11 的基础设施网络和自组织（Ad Hoc）网络模型。1. 模型概览WifiNetDevice 模拟了基于 IEEE 802.11 标准的无线网络接口控制器 [ieee80211]。我们将在后续章节中详细说明，但简单来说，ns-3 提供了以下 802.11 相关模型：基本的 802.11 分布式协调功能（DCF），支持 基础设施模式 和 自组织模式802.11a、802.11b、802.11g、802.11n（支持 2.4 GHz 和 5 GHz 频段）、802.11ac、802.11ax（支持 2.4 GHz、5 GHz 和 6 GHz 频段）以及 802.11be 的物理层MSDU 聚合 和 MPDU 聚合 的 802.11n 扩展，并支持两者组合（两级聚合）802.11ax 的 下行链路 OFDMA（DL OFDMA） 和 上行链路 OFDMA（UL OFDMA）（包括多用户增强分布式协调功能（MU EDCA）参数集的支持）802.11be 的 多链路 发现与设置基于 QoS 的增强分布式协调功能（EDCA）以及 802.11e 的队列扩展支持使用不同的传播损耗模型和传播延迟模型，详细信息请参见 Propagation 章节经验证的分组错误模型和帧检测模型，与链路仿真和其他参考一致多种速率控制算法，包括 Aarf、Arf、Cara、Onoe、Rraa、ConstantRate、Minstrel 和 Minstrel-HT802.11s（网状网络），另见其他章节802.11p 和 WAVE（车联网），另见其他章节ns-3 提供的 802.11 模型尝试准确实现 802.11 规范的 MAC 层功能，同时为 802.11a/b/e/g/n/ac/ax/be 物理层提供基于分组的抽象。在 ns-3 中，节点可以在不同的信道上拥有多个 WifiNetDevice，且 WifiNetDevice 能与其他设备类型共存。通过使用 SpectrumWifiPhy 框架，还可以创建涉及跨信道干扰或单个信道上多种无线技术共存的场景。WifiNetDevice 及其模型的源代码位于目录 src/wifi。实现是模块化的，主要包括三个子层模型：物理层模型（PHY layer models）：模拟具体修正案的物理层操作及通用物理层功能。低层 MAC 模型（lower MAC models）：模拟诸如介质访问（DCF 和 EDCA）、帧保护（RTS/CTS）以及确认（ACK/BlockAck）等功能。在 ns-3 中，低层 MAC 包括一个 帧交换管理器（Frame Exchange Manager） 层级结构、一个 信道访问管理器（Channel Access Manager）、Txop 对象、BlockAckManager 对象和一个 MAC 中间实体（MAC middle entity）。高层 MAC 模型（upper MAC models）：实现 Wi-Fi 中非时间关键的过程，如 MAC 层信标生成、探测和关联状态机，以及一组 速率控制算法。在文献中，这一子层有时称为 高层 MAC（upper MAC），相较于时间关键的硬件实现，它更偏向软件实现。接下来，我们将概述每一层的设计，具体结构如图 WifiNetDevice 架构 所示。对于 802.11be 的多链路设备（MLDs），WifiPhy、FrameExchangeManager 和 ChannelAccessManager 的实例数量与链路数量相同。WifiNetDevice 架构1.1. 高层 MAC 模型目前，高层 MAC 的父类 ns3::WifiMac 有三种变体：接入点（AP）（ns3::ApWifiMac）、非 AP 站点（STA）（ns3::StaWifiMac）以及独立基本服务集（IBSS）中的 STA（通常称为自组织网络）（ns3::AdhocWifiMac）。其中最简单的是 ns3::AdhocWifiMac，它实现了一个不执行信标生成、探测或关联的 Wi-Fi MAC。ns3::StaWifiMac 类实现了一个主动探测和关联状态机，能够在错过过多信标时自动重新关联。而 ns3::ApWifiMac 则实现了一个生成周期性信标并接受所有关联请求的 AP。这三种高层 MAC 模型共享一个共同的父类 ns3::WifiMac。该类提供了一些 MAC 配置选项，其中包括一个 QosSupported 属性，用于配置 802.11e/WMM 风格的 QoS 支持。此外，还可以在低层 MAC 中使用多种 速率控制算法。这些算法均实现为 ns3::WifiRemoteStationManager 的子类。完整的可用速率控制算法列表在单独的章节中提供。1.2. 低层 MAC 模型低层 MAC 分为三个主要组件：ns3::FrameExchangeManager：这是一个类层次结构，负责实现支持的 IEEE 802.11 修正案中引入的帧交换序列。它还处理帧聚合、帧重传、保护和确认。ns3::ChannelAccessManager：实现分布式协调功能（DCF）和增强分布式协调功能（EDCAF）。ns3::Txop 和 ns3::QosTxop：负责分组队列管理。ns3::Txop 对象用于不支持 QoS 的高层 MAC，以及标准规定应通过 DCF 访问介质的帧（例如管理帧）的传输。而 ns3::QosTxop 则用于支持 QoS 的高层 MAC。1.3. 物理层模型物理层模型主要负责对分组的接收进行建模以及跟踪能量消耗。分组接收通常包含以下三个主要组件：每个接收的分组都会通过概率方法评估其接收是否成功或失败。成功概率取决于分组的调制方式、信噪比（以及干扰比），以及物理层的状态（例如在传输或休眠期间无法进行接收）。一个对象用于记录所有接收到的信号，以便在做出接收决策时计算每个分组的正确干扰功率。一个或多个与调制方式和标准相对应的错误模型用于查找接收成功的概率。ns-3 提供了两种物理层模型供用户选择，其基本接口定义在 ns3::WifiPhy 类中。YansWifiPhy 类实现了一个简单的物理层模型，该模型在题为 Yet Another Network Simulator 的论文中有所描述。“Yans” 这一缩写源自该论文标题。SpectrumWifiPhy 类是一个基于 ns-3 用于其他无线模型的 Spectrum 框架的更高级实现。Spectrum 框架允许对信号进行精细的频率分解，并支持多种技术在单一信道上共存的场景。2. 范围与限制IEEE 802.11 标准 [ieee80211] 是一个庞大的规范，ns-3 并未涵盖其所有方面；仅记录 ns-3 的合规性本身就可能构成一份很长的文档。本节尝试总结 ns-3 对标准的符合性以及与实际行为的对比。物理层和信道模型以每个分组为基础进行操作。当使用默认的 YansWifiPhy 模型时，未包含频率选择性传播或干扰效应。目前也不支持定向天线。在加性白高斯噪声（AWGN）场景或宽带干扰场景下，性能由对接收信号的信噪比应用分析模型（基于调制方式和信道宽度等因素）决定，噪声包括热噪声以及其他 Wi-Fi 分组的干扰。当使用 SpectrumWifiPhy 时，其他无线技术的干扰才会被建模。以下是物理层和信道模型的具体限制：不支持 802.11n/ac/ax/be 波束成形不支持 802.11n RIFS未实现 802.11 PCF/HCF/HCCA不支持信道切换公告缺少认证和加密功能未建模处理延迟不支持使用 HT/VHT/HE/EHT 格式传输 RTS/CTS 和 ACK能量消耗模型未考虑 MIMO物理层支持 802.11ax 前导符打孔，但 MAC 层目前尚未利用该功能仅支持最小化的 MU-MIMO（假设理想的物理层，没有 MAC 层实现）在 MAC 层，大多数 802.11a/b/e/g/n/ac/ax/be 的主要功能已被实现，但模型中仍有一些零星的限制。对 802.11n、ac、ax 和 be 的支持仍在演进中。一些未被标准强制要求的实现选择如下：对于 802.11b，假定 BSSBasicRateSet 为 1-2 Mbit/s。对于 802.11a/g，假定 BSSBasicRateSet 为 6-12-24 Mbit/s。假定 OperationalRateSet 包含所有强制速率（参见 问题 183）。Wi-Fi 管理器总是为管理帧选择最低的基本速率。如果 STA（AP 或非 AP）支持 VHT，则一旦第一个分组排队，就会始终设置 Block Ack 协议，无论该分组是否将在 A-MPDU 中传输。一旦创建了 A-MSDU，它就不会再被修改，即使是在首次传输之前。这意味着该 A-MSDU 无法与其他 MSDU 结合使用 A-MSDU 聚合。3. 设计细节本节其余部分将深入描述一些 Wi-Fi 模型的设计细节。若用户希望直接跳转到 Wi-Fi 模块的使用章节（用户文档），可以跳过此部分。我们按照自下而上的分层方式组织了这些详细描述，首先介绍信道和物理层模型，然后是 MAC 层模型。我们首先关注物理层框架的选择。ns-3 支持一个仅 Wi-Fi 的物理层模型 YansWifiPhy，该模型不提供信号的频率级别分解。对于仅涉及 Wi-Fi 信号且不涉及与频率相关的传播损失或衰落模型的仿真，默认的 YansWifiPhy 框架是合适的选择。而对于包含同一信道上混合技术或频率相关效应的仿真，SpectrumWifiPhy 更为适合。这两个框架的配置方式非常相似。SpectrumWifiPhy 框架使用 Spectrum 模块 信道框架。YansWifiChannel 是 ns-3 Wi-Fi 模块中唯一的具体信道模型类。ns3::YansWifiChannel 的实现使用了 ns-3 Propagation 模块中提供的传播损失和传播延迟模型。特别是，可以将多个传播模型添加到信道对象（如果添加了多个损失模型，则可以链式组合），还可以添加传播延迟模型。从 ns3::YansWifiPhy 对象发送到信道的分组会以特定的信号强度发送，之后信号强度会因传播损失模型而衰减，同时引入传输（序列化）延迟以及由于任何信道传播延迟模型（通常由设备之间的光速传播延迟引起）的传播延迟，然后被复制到所有其他 ns3::YansWifiPhy 对象。仅允许将 ns3::YansWifiPhy 对象附加到 ns3::YansWifiChannel；因此，不允许模拟其他技术（如 LTE）的对象。此外，来自不同信道的分组不会相互影响；例如，如果信道被逻辑配置为信道 5 和 6，尽管它们的信道编号可能重叠，但分组不会引起相邻信道干扰。3.1. WifiPhy 和相关模型ns3::WifiPhy 是一个抽象基类，表示 802.11 物理层功能。传递给该对象的分组（通过 Send() 方法）会被发送到信道对象，在接收时，接收 PHY 对象根据信号强度和干扰决定分组是否接收成功。此类还提供了多个回调，用于通知物理层事件，公开了一个状态机的概念，可供监控 MAC 层的流程（如载波侦听），并处理休眠/唤醒/关闭模型以及能量消耗。ns3::WifiPhy 挂接到 WifiNetDevice 中的 ns3::FrameExchangeManager 对象。目前 WifiPhy 有两个实现：ns3::YansWifiPhy 和 ns3::SpectrumWifiPhy。它们与以下五个对象协同工作：PhyEntity：包含物理层处理的修订特定部分。WifiPpdu：对修订特定的物理协议数据单元（PPDU）进行建模。WifiPhyStateHelper：维护物理层状态机。InterferenceHelper：跟踪信道上观察到的所有分组。ErrorModel：根据信噪比（SNR）计算错误的概率。3.1.1. PhyEntity3.1.1.1. 背景简介考虑到相关文件的大小和复杂性，对 ns3::WifiPhy 和 ns3::WifiMode（以及其他文件）进行了一些重构。此外，添加和维护新的 PHY 修订已成为一项复杂的任务（尤其是那些实现于其他模块内部的任务，如 DMG）。所采用的解决方案是引入 PhyEntity 类，这些类包含 PHY 处理的“修订”特定部分（即 HT/VHT/HE/EHT 等）。在标准中，每个 PHY 层描述章节的开头都有“PHY 实体”的概念，例如 IEEE 802.11-2016 第 21.1.1 节：:: 第 21 条规定了一个非常高吞吐量（VHT）正交频分复用（OFDM）系统的 PHY 实体。注意，在 wave 模块中已经存在一个同名概念（例如 WaveNetDevice::AddPhy），用于指代每个 11p 信道上的 WifiPhy，但该术语仅在类中使用，并且没有任何文件使用该名称，因此使用该名称来表示 802.11 修订不会引起歧义。3.1.1.2. 架构抽象基类 ns3::PhyEntity 提供了一组唯一的 API，可供每个 PHY 实体使用，对应于 IEEE 802.11 标准的不同修订。目前已实现的 PHY 实体包括：ns3::DsssPhy：DSSS 和 HR/DSSS（11b）的 PHY 实体ns3::OfdmPhy：OFDM（11a 和 11p）的 PHY 实体ns3::ErpOfdmPhy：ERP-OFDM（11g）的 PHY 实体ns3::HtPhy：HT（11n）的 PHY 实体ns3::VhtPhy：VHT（11ac）的 PHY 实体ns3::HePhy：HE（11ax）的 PHY 实体ns3::EhtPhy：EHT（11be）的 PHY 实体它们的继承图见图 PhyEntity 继承结构，并紧密遵循标准的逻辑，例如 IEEE 802.11-2016 第 21.1.1 节：:: VHT PHY 基于 第 19 条中定义的 HT PHY，而 HT PHY 又基于 第 17 条中定义的 OFDM PHY。PhyEntity 继承结构这种架构可以以修订特定的方式处理以下操作：WifiMode 的处理和数据/PHY 速率计算，PPDU 字段大小和持续时间计算，以及发送和接收路径。3.1.2. WifiPpdu与 PhyEntity 类似，ns3::WifiPpdu 基类被具体化为以下修订特定的 PPDU：ns3::DsssPpdu：DSSS 和 HR/DSSS（11b）的 PPDUns3::OfdmPpdu：OFDM（11a 和 11p）的 PPDUns3::ErpOfdmPpdu：ERP-OFDM（11g）的 PPDUns3::HtPpdu：HT（11n）的 PPDUns3::VhtPpdu：VHT（11ac）的 PPDUns3::HePpdu：HE（11ax）的 PPDUns3::EhtPpdu：EHT（11be）的 PPDU它们的继承图如图 WifiPpdu hierarchy 所示，并紧密遵循标准的逻辑。例如，IEEE 802.11-2016 第 21.3.8.1 节指出：:: 为了与非 VHT STA 兼容，定义了特定的非 VHT 字段，这些字段可由符合 Clause 17（OFDM）或 Clause 19（HT）的非 VHT STA 接收。WifiPpdu 继承结构3.1.3. YansWifiPhy 和 WifiPhyStateHelper类 ns3::YansWifiPhy 负责接收从 MAC（ns3::FrameExchangeManager 对象）传递来的分组，将其发送到所连接的 ns3::YansWifiChannel，并从信道接收分组。如果接收成功，则将分组传递回 MAC。信号接收的能量根据发送功率计算，并基于发射机的发射增益、接收机的接收增益和适用的路径损失传播模型进行调整。类 ns3::WifiPhyStateHelper 管理 PHY 层的状态机，并允许其他对象作为 监听器 挂钩以监控 PHY 状态。监听器的主要用途是让 MAC 层知道 PHY 是否忙碌（用于传输和避免冲突）。PHY 层可以处于以下状态之一：TX：PHY 当前正在为其关联的 MAC 发送信号。RX：PHY 已同步信号，正在等待接收最后一位以将其转发到 MAC。CCA_BUSY：PHY 正在为主信道发出 PHY-CCA.indication(BUSY) 指示。IDLE：PHY 未处于 TX、RX 或 CCA_BUSY 状态。SWITCHING：PHY 正在切换信道。SLEEP：PHY 处于节能模式，无法发送或接收帧。OFF：PHY 已关闭，无法发送或接收帧。接收分组的过程如下：对于 YansWifiPhy，大部分逻辑在 WifiPhy 基类中实现。YansWifiChannel 调用 WifiPhy::StartReceivePreamble()，后者调用适当 PHY 实体的 PhyEntity::StartReceivePreamble() 开始接收分组，但首先会检查分组的信号功率是否高于 WifiPhy::RxSensitivity 中存储的阈值。任何低于该阈值的分组将被丢弃，默认值为 -101 dBm（20 MHz 信号在室温下的热噪声底）。此属性的目的包括：弱信号对仿真结果无影响，但会消耗内存和处理资源，因此应丢弃；可将阈值调高以模拟基本载波侦听的限制，用于涉及空间复用的实验。在 StartReceivePreamble() 中，分组被立即添加到干扰助手中以进行信噪跟踪，随后根据 PHY 的状态决定接收步骤。如果 PHY 处于发送状态，分组将被丢弃；如果 PHY 为空闲或正在接收，且使用了 FrameCaptureModel（且分组在捕获窗口内），则调用 PhyEntity::StartPreambleDetectionPeriod()。PhyEntity::StartPreambleDetectionPeriod() 通常会调度一个事件 PhyEntity::EndPreambleDetectionPeriod()，用于检查前导码是否被检测到。自 ns-3.30 版本起，从空闲状态的状态机转换会被抑制直到前导码检测事件完成。PhyEntity::EndPreambleDetectionPeriod() 方法使用前导码检测模型检查信号是否足够强以被接收。若检测到前导码，则调度事件 PhyEntity::EndReceiveField() 到前导码结束时，并将 PHY 置于 CCA_BUSY 状态。目前 ns-3 中仅提供一个简单的阈值前导码检测模型 ThresholdPreambleDetectionModel。如果没有前导码检测模型，则假定前导码已被检测到。自 ns-3.30 起，默认添加 ThresholdPreambleDetectionModel，其阈值为 -82 dBm 的 RSSI 和 4 dB 的 SNR，只有两者均超过阈值时，前导码才会成功检测到。在 PhyEntity::EndReceiveField() 中，检查当前前导码和头部字段是否接收正确。若接收成功，则调用 PhyEntity::StartReceiveField() 处理下一个字段，否则中止接收。该流程在处理单个 MPDU 时运行。对于 MPDU 聚合（A-MPDU），StartReceivePayload 为每个 MPDU 调度一个接收事件 ScheduleEndOfMpdus，成功接收的 MPDU 被逐一传递到 FrameExchangeManager，并在 A-MPDU 完成后通知成功接收的 MPDU 数量。3.1.4. InterferenceHelperInterferenceHelper 是一个用于跟踪所有接收分组并计算其错误概率的对象，同时还评估信道上的能量是否以及何时超过给定阈值。错误概率的基本计算过程如图 SNIR function over time 所示。在 ns-3 模型中，分组以比特（而非符号）表示。InterferenceHelper 将分组分解为一个或多个具有不同信噪比（SNIR）的“片段”。每个片段会使用错误模型独立计算指定比特数的错误概率。InterferenceHelper 根据这些片段及其持续时间，生成一个聚合的“错误概率”值，并将其返回给 WifiPhy 以决定是否接收分组。SNIR 随时间的变化通过 SNIR 函数，我们可以推导出所使用调制和编码方案的比特错误率（BER）和分组错误率（PER）。如果使用 MIMO 且接收天线数量多于空间流数量，则计算的 SNIR 会获得如下增益（假设未使用 STBC）：对于加性高斯白噪声（AWGN），发送天线数量的影响可以忽略。增益值如下：antennas NSS gain 2 x 1 1 0 dB 1 x 2 1 3 dB 2 x 2 1 3 dB 3 x 3 1 4.8 dB 3 x 3 2 1.8 dB 3 x 3 3 0 dB 4 x 4 1 6 dB 4 x 4 2 3 dB 4 x 4 3 1.2 dB 4 x 4 4 0 dB 3.1.5. ErrorRateModelns-3 根据输入帧的信噪比（SNR）以及可能重叠的干扰帧（即信噪加干扰比，SINR）决定分组是成功还是错误。分组错误率（PER）与 SINR 的关系由 ns3::ErrorRateModel 定义，目前有多个模型可用。PER 是帧的调制和编码方案（MCS）、其 SINR 和配置的具体 ErrorRateModel 的函数。ns-3 的默认 ErrorRateModel 随版本更新不断改进。目前（截至 ns-3.33 版本）针对现代 OFDM 标准（如 802.11n/ac/ax）的默认模型是 ns3::TableBasedErrorRateModel。对于 802.11a/g，默认使用 ns3::YansErrorRateModel；对于 802.11b，默认使用 ns3::DsssErrorRateModel。在 ns-3.33 版本之前，现代标准的默认模型是 ns3::NistErrorRateModel。不同错误模型的详细说明可参考外部文献。当前的 OFDM 模型基于 [patidar2017] 中的工作，使用 MATLAB WLAN 工具箱进行链路仿真，并与 IEEE TGn 结果进行验证 [erceg2004]。有关其他错误模型的更多文献，请参见 [pei80211ofdm]、[pei80211b]、[lacage2006yans] 等。当前的 ns-3 错误率模型仅适用于加性高斯白噪声（AWGN）信道，未建模任何潜在的频率选择性衰落效应。错误模型总结如下：ns3::TableBasedErrorRateModel：用于 OFDM 模式，同时复用 ns3::DsssErrorRateModel 处理 802.11b 模式。这是 802.11n/ac/ax 的默认模型。ns3::YansErrorRateModel：用于 OFDM 模式，同时复用 ns3::DsssErrorRateModel 处理 802.11b 模式。这是 802.11a/g 的默认模型。ns3::DsssErrorRateModel：包含 802.11b 模式的模型。1 Mbps 和 2 Mbps 基于经典调制分析。5.5 Mbps 和 11 Mbps 使用 GSL 时基于 [pursley2009] 提供的 CCK 调制模型；否则，使用基于 MATLAB 的备选模型。ns3::NistErrorRateModel：用于 OFDM 模式，同时复用 ns3::DsssErrorRateModel 处理 802.11b 模式。用户可以在 OFDM 模式下选择 NIST、YANS 或基于表格的模型，而 DSSS 模式下始终使用 DsssErrorRateModel。3.1.6. TableBasedErrorRateModelns3::TableBasedErrorRateModel 是针对 802.11n/ac/ax 新增的默认模型，而 ns3::YansErrorRateModel 是 802.11a/g 的默认模型。与基于误差界的分析模型不同，TableBasedErrorRateModel 包含 AWGN 信道的端到端链路仿真表格（PER vs SNR）。由于为所有可能的分组大小和输入 SNR 生成查找表在计算上不可行，该模型采用 IEEE P802.11 TGax [porat2016] 提出的建议，使用参考长度 32 字节（小于 400 字节）和 1458 字节（大于或等于 400 字节）进行外推估计。LDPC 编码仅需单一参考长度。MATLAB WLAN 工具箱用于生成每种调制和编码方案的表格。对 SNR 的每个值，模拟运行直到总计 40000 个分组被模拟。结果与 TGax 曲线非常接近，如图 Comparison of table-based OFDM Error Model with TGax results.。基于表格的 OFDM 错误模型与 TGax 结果对比3.1.7. Legacy ErrorRateModels原始错误率模型 ns3::YansErrorRateModel 基于分析结果。对于 802.11b 调制，1 Mbps 模式基于 DBPSK；2 Mbps 模式基于 DQPSK。详细信息参见 [lacage2006yans]。ns3::NistErrorRateModel 后来加入。DSSS 调制的 1 Mbps 和 2 Mbps 模式与 YansErrorRateModel 一致，而 5.5 Mbps 和 11 Mbps 模式基于 [pursley2009] 的研究。OFDM 调制模式的结果基于 NIST [miller2003]。当 GSL 可用时，802.11b 的 5.5 Mbps 和 11 Mbps 模式使用 [pursley2009]；否则，使用 MATLAB 模拟的备选模型。基于分析的错误模型在高 MCS 时逐渐偏离链路仿真结果，如图 YANS and NIST error model comparison with TGn results 所示。这推动了基于链路仿真的新错误模型的开发。YANS 和 NIST 错误模型与 TGn 结果的比较3.1.8. SpectrumWifiPhy本节描述了 SpectrumWifiPhy 类的实现，代码位于 src/wifi/model/spectrum-wifi-phy.{cc,h}。此外，该实现还使用了以下同目录中的类：wifi-spectrum-phy-interface.{cc,h}wifi-spectrum-signal-parameters.{cc,h}以及 spectrum 模块中的类：wifi-spectrum-value-helper.{cc,h}SpectrumWifiPhy 类重用了最初为 YansWifiPhy 构建的干扰管理器和错误率模型，但在第一步中允许将外部（非 Wi-Fi）信号视为附加噪声。为使 Spectrum 框架适配 Wi-Fi，进行了两项主要改动。首先，物理层必须发送与 Spectrum 信道框架兼容的信号，特别是允许不同技术信号共存的 MultiModelSpectrumChannel（对于纯 Wi-Fi 仿真中 5 GHz 和 6 GHz 频段，也可以使用 SingleModelSpectrumChannel，但不适用于 2.4 GHz 频段。如果在使用 SingleModelSpectrumChannel 时遇到错误，请切换到 MultiModelSpectrumChannel）。其次，InterferenceHelper 必须扩展以支持插入非 Wi-Fi 信号并将其接收功率添加到噪声中，就像意外 Wi-Fi 信号（可能来自不同的 SSID 或隐藏节点）添加到噪声中一样。与 YansWifiPhy 不同，在 SpectrumWifiPhy 中，超过 CcaEdThreshold 的外部信号会触发 CCA_BUSY 状态（见 802.11-2012 标准中 16.4.8.5 节定义的 CCA 模式 1）。因此，WifiPhy::CcaEdThreshold 属性在该模型中可能比在 YansWifiPhy 模型中更为重要。为了支持 Spectrum 信道，YansWifiPhy 的发送和接收方法被适配以使用 Spectrum 信道 API。这需要开发一些 SpectrumModel 相关的类。例如，WifiSpectrumValueHelper 用于使用 Spectrum 框架创建 Wi-Fi 信号，并将其能量分布到各个频段。频谱被细分为子频段（宽度等于 OFDM 子载波，取决于技术）。分配给特定信道的功率分布在子频段上，近似于标准中定义的 OFDM 发射频谱掩码。WifiBandwidthFilter 类用于在传输过程的早期阶段丢弃信号，通过忽略发射带宽（包括保护带）与当前操作信道不重叠的任何 Wi-Fi PPDU。这样可以绕过信号传播/损失计算，从而减少计算负担并提高仿真性能。要启用 WifiBandwidthFilter，用户可以使用以下代码进行对象聚合：Ptr<WifiBandwidthFilter> wifiFilter = CreateObject<WifiBandwidthFilter> (); Ptr<MultiModelSpectrumChannel> spectrumChannel = CreateObject<MultiModelSpectrumChannel> (); spectrumChannel->AddSpectrumTransmitFilter(wifiFilter);为了提供更方便的用户配置体验，现有的 YansWifi 帮助类（位于 src/wifi/helper）被复制并调整，用于提供等效的 SpectrumWifi 帮助类。最后，为了避免 C++ 多重继承问题，插入了一个名为 WifiSpectrumPhyInterface 的小型转发类，作为 SpectrumWifiPhy 和 Spectrum 通道之间的桥梁。WifiSpectrumPhyInterface 调用了不同的 SpectrumWifiPhy::StartRx() 方法以启动接收过程。此方法会根据 WifiPhy::RxSensitivity 属性检查信号强度，并丢弃弱信号。同时，它还会验证信号是否为 Wi-Fi 信号；非 Wi-Fi 信号会添加到 InterferenceHelper 中，在这里它们可能会触发 CCA_BUSY，但不会在接收链中进一步处理。有效的 Wi-Fi 信号会触发调用 WifiPhy::StartReceivePreamble，随后按照之前描述的流程继续处理。此外，为了支持更灵活的通道切换，SpectrumWifiPhy 可以管理多个 WifiSpectrumPhyInterface 实例（多 RF 接口概念）。每个实例处理频谱的特定频率范围，通过 MHz 定义起始和结束频率，且这些范围不能重叠。只有一个实例对应于 SpectrumWifiPhy 的活动 RF 接口，其他实例被称为非活动 RF 接口，可能会与频谱通道断开连接。多 RF 接口概念如果将 SpectrumWifiPhy::TrackSignalsFromInactiveInterfaces 属性设置为 true（默认值），非活动 RF 接口会连接到各自的频谱通道。当非活动 RF 接口接收到属于其覆盖频率范围配置部分的信号时，SpectrumWifiPhy 也会接收这些信号。非活动接口监控的频谱部分由中心频率和信道宽度指定，并无缝设置为使用该频率范围的 Spectrum PHY 的活动信道的等效配置。SpectrumWifiPhy 将这些来自非活动接口的接收信号转发给 InterferenceHelper，而不进一步处理它们。这样做的好处是，当信号在新信道上开始传输但切换尚未完成时，能够生成更准确的 PHY-CCA.indication，这些信号否则会被忽略。这在图 通道切换期间信号跟踪的示例 中进行了说明，其中红色部分仅在 SpectrumWifiPhy::TrackSignalsFromInactiveInterfaces 设置为 true 时生成。通道切换期间信号跟踪的示例3.1.9. MU-MIMO PHY 支持在 ns-3.40 版本中，为 PHY 层引入了 MU-MIMO 的基础支持。当前模型可用于下行链路和上行链路的 MU-MIMO 传输。MU-MIMO 和 OFDMA 的多用户传输依赖于 MAC 层中 TXVECTOR 的填充方式（尚未实现）。由于不支持混合 OFDMA 和 MU-MIMO 配置，TXVECTOR 会在所有用户分配相同 RU 时标识为 MU-MIMO，否则它表示为 OFDMA 传输。在 PHY 层，OFDMA 和 MU-MIMO 的传输方式相似，主要区别在于 MU-MIMO 在上行链路方向上允许多个发射器同时共享相同的频谱。当前的 PHY 抽象模型假设完美条件，其中干扰管理器能够检测属于同一 MU-MIMO 传输的信号，并确保它们不会相互干扰。模型仍然支持与其他信号（包括其他 MU-MIMO 传输）的干扰。3.2. MAC 模型3.2.1. 基础架构模式中的关联在基础架构模式中，关联是一种高层 MAC 功能，由关联管理器（WifiAssocManager 基类和默认子类 WifiDefaultAssocManager）实现。站点 MAC、关联管理器基类和子类之间的交互如图 扫描过程 所示。扫描过程STA 的 Wi-Fi MAC 请求关联管理器启动具有指定参数的扫描过程，包括扫描类型（主动或被动）、目标 SSID、扫描的信道列表等。在扫描结束时，STA Wi-Fi MAC 期望收到最佳 AP 的通知以进行关联。在扫描期间接收到的每个 Beacon 或 Probe Response 帧都会被转发到关联管理器，该管理器会维护一个与扫描参数匹配的候选 AP 列表。默认关联管理器会按照接收到的 Beacon/Probe Response 帧的 SNR 降序排列这些 AP。当通知开始扫描过程时，默认关联管理器会调度一个方法调用，该方法处理在调用时已接收到的帧中包含的信息。当 AP 和 STA 都具有多个链接（即它们是 802.11be MLDs）时，默认关联管理器尝试尽可能多地建立链接。这涉及在 STA 的一些链接上切换工作信道以匹配与 AP MLD 关联的 AP 所运行的信道。如果 AP 因某种原因拒绝关联，STA 将尝试与下一个最佳 AP 关联，直到候选列表耗尽，这时 STA 将进入 “REFUSED” 状态。在这种情况下，模拟用户需要通过某种方式强制重新尝试关联，例如更改配置（即 STA 不会在拒绝后持续尝试关联）。一旦关联完成，如果模拟用户更改了配置，STA 将尝试重新关联到现有的 AP。如果丢失的 Beacon 数量超过阈值，STA 将通知设备其他部分链路已断开（关联丢失），并重新启动扫描过程。需要注意的是，当关联请求失败且未明确被拒绝时（即 AP 未响应关联请求），也可能发生这种情况。在非 AP MLD 的情况下，要丢失关联，必须在任何链接的最大 Beacon 丢失次数乘以两次连续 Beacon 帧间隔的时间间隔内未接收到 Beacon。3.2.2. 漫游当前不支持第 2 层漫游（即 STA 将其关联从一个 AP 切换到另一个 AP）。因此，IEEE 802.11 标准中描述的最小/最大信道驻留时间实现也被省略，因为它仅在信道漫游的上下文中有意义。3.2.3. 信道访问802.11 分布式协调功能（DCF）用于计算何时允许访问传输介质。虽然实现 DCF 使用周期性定时器（每个时隙到期触发）会相对简单，但我们选择了 ji2004sslswn(https://www.nsnam.org/docs/models/html/wifi-references.html#ji2004sslswn) 中描述的方法，该方法仅在需要时惰性计算退避定时器持续时间，据称性能明显优于更简单的周期性定时器解决方案。DCF 基本访问的描述详见 ieee80211-2016(https://www.nsnam.org/docs/models/html/wifi-references.html#ieee80211-2016) 的第 10.3.4.2 节：“当 STA 确定传输队列中有帧时，若介质空闲且持续空闲一个 DIFS 时间（或一个 EIFS 时间，从上一忙碌事件结束算起，以较大者为准），并且退避定时器为零，则可以传输 MPDU。否则，应遵循 10.3.4.3 节描述的随机退避过程。”因此，当以下所有条件都满足时，STA 可以不调用退避过程：在帧排队等待传输时，介质处于空闲状态；从帧排队开始到以下事件的较新者为止，介质持续空闲：一个 DIFS 时间；从上一个介质忙碌事件（与接收的错误帧相关联）结束开始的一个 EIFS 时间；退避定时器为零。EDCA 的退避过程与 DCF 的稍有不同，其描述详见 ieee80211-2016(https://www.nsnam.org/docs/models/html/wifi-references.html#ieee80211-2016) 的第 10.22.2.2 节。在信道访问方面，ns-3 默认配置下，QoS 和非 QoS AP 的 Beacon 在 PIFS 时间（SIFS 加上一个时隙时间）后访问信道，且无需退避（CWmin 和 CWmax 都设置为零），这使得 Beacon 比其他访问类别具有更高的优先级。3.2.4. Frame Exchange Managers随着 IEEE 802.11 标准的不断演进，添加的功能越来越多，因此让单个组件处理所有允许的帧交换序列变得愈发困难。为保持代码的清晰和可扩展性，同时避免代码重复，ns-3 引入了一系列 FrameExchangeManager 类。这些类分别处理不同修正案引入的帧交换序列。FrameExchangeManager 类的继承关系如图 FrameExchangeManager hierarchy 所示。FrameExchangeManager 层次结构每个 FrameExchangeManager 类支持的功能如下：FrameExchangeManager：基类，处理非 QoS 站点的基本序列，包括 MPDU 和普通确认帧、RTS/CTS 和 CTS-to-self、NAV 设置和重置、MPDU 分片等。QosFrameExchangeManager：添加了 TXOP 支持，包括多种保护设置、通过 CF-End 进行 TXOP 截断、TXOP 恢复、在响应 TXOP 持有者发送的 RTS 时忽略 NAV。HtFrameExchangeManager：支持压缩型 Block Ack、A-MSDU 和 A-MPDU 聚合以及隐式 Block Ack 请求策略。VhtFrameExchangeManager：支持 S-MPDUs。HeFrameExchangeManager：支持通过 DL OFDMA 和 UL OFDMA 传输和接收多用户帧。FrameExchangeManager 类可以通过属性控制其处理的帧交换序列。例如，FrameExchangeManager 基类具有 ProtectedIfResponded 属性，用于启用/禁用 RTS/CTS 保护，适用于已响应需要确认的帧的站点，即使该帧未受到 RTS/CTS 的保护。3.2.5. MAC 队列在 QoS 站点，每个 EDCA 功能，以及在非 QoS 站点，DCF，都有自己的 MAC 队列（WifiMacQueue 类的一个实例），用于存储来自上层的待传输数据包。在 QoS 站点，每个接收到的数据包根据套接字优先级分配一个用户优先级（例如，可参考 wifi-multi-tos 或 wifi-mac-ofdma 示例），该优先级决定处理数据包的接入类别（Access Category）。默认情况下，Wi-Fi MAC 队列支持流控制，因此如果对应的 MAC 队列已满，上层不会向下转发数据包。Wi-Fi MAC 队列不支持动态队列限制（如字节队列限制）；因此，只有当某接入类别的 WifiMacQueue 完全满时，才会对流量控制层产生反压（即，当队列深度达到 MaxSize 属性值时，默认值为 500 个数据包）。TCP 小队列（TSQ）[corbet2012] 是 Linux 的一项功能，用于从 Wi-Fi 设备向套接字层提供反馈，以控制 Wi-Fi 层队列中的数据量。ns-3 的 TCP 尚未实现 TSQ，WifiNetDevice 也未提供此类具体反馈（尽管使用现有的追踪源可能足以支持）。无论如何，实验测试表明，TSQ 会干扰 Wi-Fi 上行传输中的聚合 [grazia2022]。数据包在 Wi-Fi MAC 队列中保留，直到被确认或丢弃。数据包可能因以下原因被丢弃，例如，其生命周期到期（即在队列中停留时间过长）或达到最大重试次数。数据包的最大生命周期可以通过 WifiMacQueue 的 MaxDelay 属性配置。可以使用多种追踪源来跟踪数据包传输的结果（请参阅对应的 Doxygen 文档）：WifiMac 追踪源：AckedMpdu、NAckedMpdu、DroppedMpdu、MpduResponseTimeout、PsduResponseTimeout、PsduMapResponseTimeout。WifiMacQueue 追踪源：Expired。内部，Wi-Fi MAC 队列由多个子队列组成，每个子队列存储特定类型的帧（例如，数据帧或管理帧），并具有特定的接收方地址和 TID。对于单用户传输，下一站由 Wi-Fi MAC 队列调度器（由 WifiMac 实例持有）确定。Wi-Fi MAC 队列调度器由一个基类（WifiMacQueueScheduler）及其定义特定调度策略的子类实现。默认调度器（FcfsWifiQueueScheduler）为管理帧分配更高优先级，并以先进先出（FCFS）方式处理数据帧。对于多用户传输（见下文），调度由多用户调度器执行，该调度器可以选择是否咨询 Wi-Fi MAC 队列调度器，以确定通过多用户下行链路（DL）或上行链路（UL）传输需要服务的站点。3.2.6. 多用户传输自 IEEE 802.11ax 修正案引入以来，多用户（MU）传输成为可能，通过 OFDMA 和/或 MU-MIMO 实现下行链路（DL）和上行链路（UL）传输。目前，ns-3 仅支持通过 OFDMA 进行的多用户传输。以下为 DL OFDMA 的三种确认序列。第一种确认序列由多个 BlockAckRequest/BlockAck 帧组成，这些帧作为单用户帧发送，如图 单用户格式的 DL MU 帧确认 所示。单用户格式的 DL MU 帧确认第二种确认序列使用 MU-BAR 触发帧发送（作为单用户帧），以请求在 TB PPDUs 中发送的 BlockAck 响应，如图 通过单用户帧发送的 MU-BAR 触发帧的 DL MU 帧确认 所示。通过单用户帧发送的 MU-BAR 触发帧的 DL MU 帧确认第三种确认序列将 MU-BAR 触发帧聚合到 DL MU PPDU 中的每个 PSDU，BlockAck 响应在 TB PPDUs 中发送，如图 通过聚合的 MU-BAR 触发帧的 DL MU 帧确认 所示。通过聚合的 MU-BAR 触发帧的 DL MU 帧确认对于 UL OFDMA，支持 BSRP 触发帧和基本触发帧，如图 使用 UL OFDMA 的帧交换序列 所示。AP 发送 BSRP 触发帧，以请求站点发送包含缓冲状态报告（Buffer Status Reports）的 QoS Null 帧。AP 发送基本触发帧，以请求站点在 TB PPDUs 中发送数据帧，这些帧由 AP 通过 Multi-STA BlockAck 帧确认。需要注意的是，为了使两次帧交换序列以 SIFS 间隔分隔（如图所示），必须满足以下条件：传输接入类别具有非零 TXOP 限制；TXOP 中有足够剩余时间进行基本触发帧启动的帧交换序列；多用户调度器选择在 BSRP 触发帧之后发送基本触发帧。使用 UL OFDMA 的帧交换序列3.2.7. 多用户调度器一个新的组件，称为 MultiUserScheduler，负责确定聚合 AP 在获得 TXOP 时执行的帧交换序列（DL OFDMA、UL OFDMA 或 BSRP 触发帧），以及执行所选帧交换序列所需的信息（例如，在 DL OFDMA 的情况下需要发送的 PSDU 集合）。TXOP 是在请求信道访问后（通常由 DCF/EDCA 执行）获得的。如果设备有帧需要传输，MultiUserScheduler 可以协调 UL MU 传输，即使没有 DL 流量。访问请求间隔可以通过 AccessReqInterval 属性设置为非零值。此访问请求间隔是 MultiUserScheduler 两次连续请求信道访问的间隔。这些请求独立于 AP 队列中是否有帧存在。还可以设置 MultiUserScheduler 请求信道访问的接入类别（通过 AccessReqAc 属性），并选择访问请求间隔是否从 MultiUserScheduler 上次请求信道访问或上次由 DCF/EDCA 获得信道访问的时间开始计算（通过 DelayAccessReqUponAccess 属性）。MultiUserScheduler 是一个抽象基类。目前，唯一可用的子类是 RrMultiUserScheduler。默认情况下，没有多用户调度器聚合到 AP（因此未启用 OFDMA）。3.2.8. 轮询多用户调度器轮询多用户调度器动态分配每个站点的优先级，以确保在选择站点进行 DL 多用户传输时的空中时间公平性。NStations 属性允许设置可成为 DL 多用户帧接收方的最大站点数。因此，每当 HE AP 访问信道以传输 DL 多用户帧时，调度器确定 AP 需要发送帧的站点数（以该属性指定的值为上限），并尝试为尽可能多的站点分配相等大小的 RUs，而不留有未使用的相同大小的 RUs。例如，如果信道带宽为 40 MHz，且确定的站点数为 5，则前 4 个站点（按优先级排序）将各分配一个 106-tone RU（如果分配了 52-tone RU，将会有三个 52-tone RUs 未使用）。如果可以分配中央 26-tone RU（由 UseCentral26TonesRus 属性确定），尚未分配 RU 的站点可能会被分配这些 RU。在上述示例中，第五个站点将分配一个可用的中央 26-tone RU。当启用 UL OFDMA（通过 EnableUlOfdma 属性）时，每个 DL OFDMA 帧交换之后都会有一个 UL OFDMA 帧交换，涉及相同的站点集和与之前 DL 多用户帧相同的 RU 分配。可以选择（取决于 EnableBsrp 属性的值）在发送基本触发帧之前发送 BSRP 触发帧，以便 AP 收集站点的缓冲状态信息。3.2.9. 增强的多链路单无线电操作（EMLSR）IEEE 802.11be 修正案引入了 EMLSR 操作模式，允许非 AP MLD 在一组设置的链路（称为 EMLSR 链路）上交替进行帧交换（参见 IEEE 802.11be D4.1 第 35.3.17 节）。ns-3 支持以下描述的 EMLSR 操作。3.2.9.1. 支持 EMLSR 操作模式的非 AP MLD 架构支持 EMLSR 操作模式的非 AP MLD 架构基于以下假设：只有一个 PHY 实例（称为 主 PHY）具有完整的 TX/RX 能力，而其他 PHY 实例（简称为 辅助 PHY 或 aux PHYs）的 TX/RX 能力有限。因此，只有主 PHY 能够与 AP MLD 进行帧交换。由于帧交换可以发生在任何 EMLSR 链路上，主 PHY 所操作的链路会在模拟过程中动态切换，具体如以下所述。3.2.9.2. 启用/禁用 EMLSR 模式在支持 EMLSR 操作模式的非 AP MLD 上，可以启用 EMLSR 模式的链路（至少两条），并与支持 EMLSR 操作模式的 AP MLD 执行多链路设置。MLD 的 EHT 配置中的 EmlsrActivated 属性决定了该 MLD 是否支持 EMLSR 操作模式。如果非 AP MLD 的 EmlsrActivated 属性被设置为 true，则 WifiMacHelper 将通过 SetEmlsrManager 方法配置的类型和属性值安装一个 EMLSR 管理器。可以通过 EMLSR 管理器基类的 EmlsrLinkSet 属性启用或禁用非 AP MLD 链路上的 EMLSR 模式（默认情况下，多链路设置后禁用 EMLSR 模式）。设置 EmlsrLinkSet 属性会触发 EML 操作模式通知帧的发送，用于向 AP 通知新的 EMLSR 链路集（如果多链路设置已完成）。否则，EMLSR 链路集将被存储，并在多链路设置完成后立即发送 EML 操作模式通知帧。因此，用户可以通过在初始化时设置 EmlsrLinkSet 属性，选择在多链路设置完成后立即启用某些链路上的 EMLSR 模式；或者可以在初始化时将 EmlsrLinkSet 属性留空，并在运行时设置它以在特定的模拟时间启用某些链路上的 EMLSR 模式（多链路设置完成后）。非 AP MLD 用于发送 EML 操作模式通知帧的链路由 EMLSR 管理器子类选择。默认的 EMLSR 管理器子类 DefaultEmlsrManager 选择主 PHY 操作的链路。当非 AP MLD 收到 EML 操作模式通知帧的确认后，会启动一个计时器，其持续时间为 AP MLD 通知的切换超时时间。当计时器过期，或者非 AP MLD 收到来自 AP MLD 的 EML 操作模式通知帧时，EMLSR 模式将在请求的链路集上启用（如果该集合不为空）或禁用（否则）。请求启用 EMLSR 模式的链路集必须包括主 PHY 操作的链路；在其他启用 EMLSR 模式的链路上操作的 PHY 实例被认为是辅助 PHY。主 PHY 的 PHY 实例通过 EMLSR 管理器基类的 MainPhyId 属性进行配置。该类还允许定义辅助 PHY 的 TX/RX 能力：AuxPhyMaxModClass 属性表示辅助 PHY 支持的最大调制类别。AuxPhyChannelWidth 属性表示辅助 PHY 支持的最大信道宽度（MHz）。该属性的值可能会根据支持的最大调制类别自动限制。AuxPhyTxCapable 属性表示辅助 PHY 是否能够发送帧。3.2.9.3. 下行链路 TXOPEMLSR 操作：下行链路 TXOP当支持 EMLSR 操作模式的 AP MLD 需要在与以 EMLSR 模式运行的非 AP MLD 的链路上启动帧交换时，它会发送一个 MU-RTS 触发帧，作为该交换的初始控制帧（ICF），请求非 AP MLD（以及可能的其他设备）响应（见图 EMLSR 操作：下行链路 TXOP）。MU-RTS 触发帧以非 HT 重复 PPDU 形式传输，传输速率为 6 Mbps、12 Mbps 或 24 Mbps。当初始控制帧的传输开始时，AP MLD 会阻止 EMLSR 链路上受 MU-RTS 触发帧影响的客户端的其他链路上的传输，以避免在这些链路上启动新的帧交换。当 EMLSR 模式启用时，可以通过 EMLSR 管理器基类的 EmlsrPaddingDelay 和 EmlsrTransitionDelay 属性分别设置填充延迟和切换延迟。3.2.9.4. 上行链路 TXOPEMLSR 操作：上行链路 TXOPEMLSR 客户端可以在任何 EMLSR 链路上启动 UL TXOP。当在辅助 PHY 操作的链路上获得信道访问时，辅助 PHY 会发送 RTS 帧，然后主 PHY 接管 TXOP（见图 EMLSR 操作：上行链路 TXOP）。通过 EmlsrManager 类的 MediumSyncDuration、MsdMaxNTxops 和 MsdOfdmEdThreshold 属性可以分别配置同步延迟计时器的持续时间、最大 TXOP 尝试次数和主 20 MHz 信道的 OFDM ED 阈值。3.2.10. 确认管理器自 IEEE 802.11e 修正案引入以来，可用的确认策略编码在 QoS 数据帧的 QoS 控制字段中的确认策略子字段中。WifiAckManager 是为多种确认管理器提供接口的抽象基类，目前默认确认管理器是 WifiDefaultAckManager。3.2.11. WifiDefaultAckManagerWifiDefaultAckManager 根据以下属性值确定使用的确认策略：UseExplicitBar：决定当需要接收方响应且当前传输包括多个帧（A-MPDU）时是否使用 显式 Block Ack 请求 策略。BaThreshold：确定 Block Ack 协议的发起方需要请求接收方响应的条件。DlMuAckSequenceType：选择 DL MU 帧的确认序列（单用户格式确认、通过 MU-BAR 触发帧确认或通过聚合到数据帧的 MU-BAR 触发帧确认）。3.2.12. 保护管理器保护管理器负责在发送帧时决定是否使用保护机制。WifiProtectionManager 是一个抽象基类，为多种保护管理器提供接口。目前默认的保护管理器是 WifiDefaultProtectionManager。3.2.13. WifiDefaultProtectionManagerWifiDefaultProtectionManager 根据远程站点管理器提供的信息选择合适的保护机制。3.2.14. 速率控制算法ns-3 提供多种速率控制算法，有些基于真实设备中的实现，另一些来自学术文献。以下是 MAC 低层可用的速率控制算法：真实设备中的算法：ArfWifiManagerOnoeWifiManagerConstantRateWifiManagerMinstrelWifiManagerMinstrelHtWifiManager文献中的算法：IdealWifiManager（WifiHelper 的默认算法）AarfWifiManager [lacage2004aarfamrr]AmrrWifiManager [lacage2004aarfamrr]CaraWifiManager [kim2006cara]RraaWifiManager [wong2006rraa]AarfcdWifiManager [maguolo2008aarfcd]ParfWifiManager [akella2007parf]AparfWifiManager [chevillat2005aparf]ThompsonSamplingWifiManager [krotov2020rate]3.2.15. ConstantRateWifiManager固定速率控制算法始终为每个数据包使用相同的传输模式。用户可以为所有单播包设置所需的 DataMode，以及为所有请求控制包（例如 RTS）设置 ControlMode。如果希望为非单播包指定不同的数据模式，用户需设置 WifiRemoteStationManager 的 NonUnicastMode 属性。否则，将默认使用最低速率模式。可用属性：DataMode（默认值：WifiMode::OfdmRate6Mbps）：指定所有单播包的模式。ControlMode（默认值：WifiMode::OfdmRate6Mbps）：指定所有请求控制包的模式。3.2.16. IdealWifiManager理想速率控制算法根据先前发送数据包的信噪比（SNR）选择最佳模式。当节点 A 向节点 B 发送单播包，且 B 成功接收后，B 会记录接收包的 SNR 并通过 ACK 将其发送回 A。这样，A 可以通过一个额外的机制（因而被称为“理想”）获取发送给 B 的数据包的 SNR，从而根据一组 SNR 阈值选择传输模式。可用属性：BerThreshold（默认值：1e-6）：用于计算每种模式的 SNR 阈值的最大比特错误率（BER）。3.2.17. ThompsonSamplingWifiManagerThompson 采样（TS）是多臂老虎机问题的经典解决方案。ThompsonSamplingWifiManager 实现了一种基于 TS 的速率控制算法，其目标是提供一个简单的统计算法，且参数较少。3.2.18. MinstrelWifiManagerMinstrel 是一种源自 madwifi 项目的速率控制算法，当前是 Linux 内核的默认速率控制算法。3.2.19. MinstrelHtWifiManagerMinstrel 的扩展，支持 802.11n/ac/ax。3.2.20. 802.11ax OBSS PD 空间复用802.11ax 模式支持 OBSS PD 空间复用功能。OBSS PD 表示重叠基本服务集前导检测（Overlapping Basic Service Set Preamble-Detection），在特定条件下允许 STA 忽略其他 BSS 的 PPDU。3.2.21. OBSS PD 算法ObssPdAlgorithm 是 OBSS PD 算法的基类，实现了通用功能，包括设置回调并在算法请求时执行 PHY 重置。3.2.22. 恒定 OBSS PD 算法恒定 OBSS PD 算法由 ConstantObssPdAlgorithm 类实现。3.2.23. 修改 Wi-Fi 模型修改默认的 Wi-Fi 模型是研究中常见的任务。本节概述了如何修改默认的 Wi-Fi 模型，常见任务包括但不限于：创建或修改 Wi-Fi 帧/头部，修改 wifi-mac-header.*。修改 MAC 低层，例如处理新控制帧（RTS/CTS/ACK/Block ACK）、更改双向/四向事务，通常修改 frame-exchange-manager.* 或其子类。修改 MAC 高层，例如处理新管理帧（Beacon/Probe），修改 wifi-mac.*、sta-wifi-mac.*、ap-wifi-mac.* 或 adhoc-wifi-mac.*。Wi-Fi 队列管理：修改 txop.* 和 qos-txop.*。通道接入管理：修改 channel-access-manager.* 文件，这些文件负责授予对 Txop 和 QosTxop 的访问。修改分片和 RTS 阈值，这些由 Wi-Fi 远程站点管理器处理。修改或创建新速率控制算法，需创建 Wi-Fi 远程站点管理器的子类或修改现有的算法。
目录 [隐藏] 1. 问题分析2. 问题结论3. 测试用例3.1. 创建 libtest.c3.2. 创建 main.c3.3. 编译并测试3.4. 预期输出 1. 问题分析日常被 GCC 折磨，GCC 链接报错，提示未定义符号。ld: Undefined symbols: _ssl_print_string, referenced from: _tls13_get_quic_secret in packet-tls.c.o _tls13_get_quic_secret in packet-tls.c.o排查编译流程确认该符号其最终被链接进入 third_party/wireshark/build/run/libwireshark.dylib。而我最终又链接了 libwireshark.dylib。usr/bin/cc -lwireshark packet-tls.c.o -o tls_dissector.so并且确认该符号并非 static。extern void ssl_print_string(const gchar* name, const StringInfo* data); void ssl_print_string(const gchar* name, const StringInfo* data) { ssl_print_data(name, data->data, data->data_len); }直接查找二进制文件也能够确认其符号已经被链接进入 dylib。 grep "packet-tls-utils.c.o" -r . --include=*.dylib Binary file ./third_party/wireshark/build/run/libwireshark.17.0.5.dylib matches通过 nm 确认符号属性发现其为 t。$ nm third_party/wireshark/build/run/libwireshark.17.0.5.dylib|grep _ssl_print_string 0000000000ad2108 t _ssl_print_string符号类型描述U未定义符号 (undefined)A绝对符号 (absolute)T全局可见的文本段符号 (global text section symbol)t本地文本段符号 (local text section symbol)D全局可见的数据段符号 (global data section symbol)d本地数据段符号 (local data section symbol)B全局可见的 BSS 段符号 (global bss section symbol)b本地 BSS 段符号 (local bss section symbol)C公共符号 (common symbol)c本地公共符号 (local common symbol)-调试符号表条目 (for debugger symbol table entries)S全局可见的其他段中的符号 (global symbol in a section other than those above)s本地其他段中的符号 (local symbol in a section other than those above)I全局可见的间接符号 (global indirect symbol)i本地间接符号 (local indirect symbol)u动态共享库中的小写 u 表示对同一库中另一个模块的私有外部未定义引用 (an undefined reference to a private external in another module in the same library)再次查看对应的 .o 确认其为 T。$ nm third_party/wireshark/./build/epan/dissectors/CMakeFiles/dissectors.dir/packet-tls-utils.c.o |grep _ssl_print_string 0000000000001c00 T _ssl_print_string2. 问题结论那么该文本符号从 T 变成了 t 只能够是在链接阶段了。最后确认了 -fvisibility=hidden。这个选项会将所有符号的默认可见性设置为隐藏，除非显式地声明为 default。这意味着，尽管在编译 .o 文件时符号是全局可见的，但在链接生成 .dylib 文件时符号变成了局部可见。那为什么要这么做呢？https://gitlab.com/wireshark/wireshark/-/blob/4aa814ac25a18ea48d4002351ed45d4b245b0c08/CMakeLists.txt#L1070-1104# # Try to have the compiler default to hiding symbols, so that only # symbols explicitly exported with WS_DLL_PUBLIC will be visible # outside (shared) libraries; that way, more UN*X builds will catch # failures to export symbols, rather than having that fail only on # Windows. # # We don't need that with MSVC, as that's the default. # if( NOT CMAKE_C_COMPILER_ID MATCHES "MSVC") # # Try the GCC-and-compatible -fvisibility-hidden first. # check_c_compiler_flag(-fvisibility=hidden FVHIDDEN) if(FVHIDDEN) set(CMAKE_C_FLAGS "-fvisibility=hidden ${CMAKE_C_FLAGS}") 主要目的是设置编译器默认隐藏符号，确保只有显式导出的符号才会在共享库（shared library）中可见。这可以帮助在更多的 Unix 系统上捕捉到符号导出失败的问题，而不仅仅是在 Windows 上。https://gitlab.com/wireshark/wireshark/-/blob/4aa814ac25a18ea48d4002351ed45d4b245b0c08/include/ws_symbol_export.h#L131-134 /* * Symbols exported from libraries. */ #define WS_DLL_PUBLIC_DEF __attribute__ ((visibility ("default")))3. 测试用例下面是一个简单的示例代码，用于测试 -fvisibility=hidden 和 __attribute__((visibility("default"))) 的效果。我们将创建两个文件：一个是库文件 libtest.c，另一个是测试程序 main.c。在库文件中，我们将定义两个函数，其中一个函数使用 __attribute__((visibility("default"))) 明确导出，而另一个函数不使用任何可见性属性。3.1. 创建 libtest.c// libtest.c #include <stdio.h> // 这个函数将被默认隐藏，因为我们使用 -fvisibility=hidden 编译 void hidden_function() { printf("This is a hidden function.\n"); } // 这个函数将被显式导出，即使我们使用 -fvisibility=hidden 编译 __attribute__((visibility("default"))) void visible_function() { printf("This is a visible function.\n"); }3.2. 创建 main.c// main.c #include <stdio.h> // 声明从库中导入的函数 void hidden_function(); void visible_function(); int main() { // 调用这两个函数 hidden_function(); visible_function(); return 0; }3.3. 编译并测试首先编译库文件 libtest.c，使用 -fvisibility=hidden 来隐藏默认符号：gcc -fvisibility=hidden -c libtest.c -o libtest.o然后创建静态库 libtest.a：ar rcs libtest.a libtest.o接着编译主程序 main.c 并链接静态库：gcc main.c libtest.a -o main最后运行测试程序：./main3.4. 预期输出如果一切正常，你应该看到以下输出：This is a hidden function. This is a visible function.在编译和链接过程中，可以使用 nm 命令来查看库中的符号，验证符号的可见性：nm libtest.o你应该看到类似以下的输出：0000000000000000 t hidden_function 000000000000001a T visible_function其中，hidden_function 是本地可见的（小写 t），而 visible_function 是全局可见的（大写 T）。
目录 [隐藏] 1. 名称2. 语法3. 选项4. 描述4.1. QDISCS4.2. CLASSES4.3. FILTERS4.4. QEVENTS4.5. 无类 QDISCS4.6. 配置无类 QDISCS4.7. 类别 QDISCS4.8. 操作原理4.9. 命名4.10. 参数5. TC 命令5.1. 监控5.2. 选项5.3. 格式5.4. 示例6. 历史7. 参见8. 作者 TC1. 名称tc - 显示/操作流量控制（traffic control）设置2. 语法tc [ OPTIONS ] qdisc [ add | change | replace | link | delete ] dev DEV [ parent qdisc-id | root ] [ handle qdisc-id ] [ ingress_block BLOCK_INDEX ] [ egress_block BLOCK_INDEX ] qdisc [ qdisc specific parameters ] tc [ OPTIONS ] class [ add | change | replace | delete ] dev DEV parent qdisc-id [ classid class-id ] qdisc [ qdisc specific parameters ] tc [ OPTIONS ] filter [ add | change | replace | delete | get ] dev DEV [ parent qdisc-id | root ] [ handle filter-id ] protocol protocol prio priority filtertype [ filtertype specific parameters ] flowid flow-id tc [ OPTIONS ] filter [ add | change | replace | delete | get ] block BLOCK_INDEX [ handle filter-id ] protocol protocol prio priority filtertype [filtertype specific parameters ] flowid flow-id tc [ OPTIONS ] chain [ add | delete | get ] dev DEV [ parent qdisc-id | root ] filtertype [ filtertype specific parameters ] tc [ OPTIONS ] chain [ add | delete | get ] block BLOCK_INDEX filtertype [ filtertype specific parameters ] tc [ OPTIONS ] [ FORMAT ] qdisc { show | list } [ dev DEV ] [ root | ingress | handle QHANDLE | parent CLASSID ] [ invisible ] tc [ OPTIONS ] [ FORMAT ] class show dev DEV tc [ OPTIONS ] filter show dev DEV tc [ OPTIONS ] filter show block BLOCK_INDEX tc [ OPTIONS ] chain show dev DEV tc [ OPTIONS ] chain show block BLOCK_INDEX tc [ OPTIONS ] monitor [ file FILENAME ]3. 选项OPTIONS := { [ -force ] -b[atch] [ filename ] | [ -n[etns] name ] | [ -N[umeric] ] | [ -nm | -nam[es] ] | [ { -cf | -c[onf] } [ filename ] ] [-t[imestamp] ] | [ -t[short] | [ -o[neline] ] } css Copy code FORMAT := { -s[tatistics] | -d[etails] | -r[aw] | -i[ec] | -g[raph] | -j[json] | -p[retty] | -col[or] }4. 描述tc 用于配置 Linux 内核中的流量控制。流量控制包括以下几个方面：整形（SHAPING）当流量被整形时，其传输速率会受到控制。整形不仅仅是降低可用带宽，它还用于平滑流量的突发，以改善网络行为。整形发生在出口端（egress）。调度（SCHEDULING）通过调度数据包的传输，可以改善需要交互的流量的表现，同时保证大流量传输的带宽。重新排序也称为优先级设置，仅在出口端发生。检测（POLICING）整形处理的是传输中的流量，而检测处理的是到达的流量。因此，检测发生在入口端（ingress）。丢弃（DROPPING）超出设定带宽的流量也可能会被立即丢弃，这在入口端和出口端都会发生。流量处理由三种对象控制：qdiscs、classes 和 filters。4.1. QDISCSqdisc 是 “队列规则 (queueing disciplin)” 的简称，对于理解流量控制至关重要。每当内核需要将数据包发送到接口时，它会被排入配置好的 qdisc 中。紧接着，内核会尽可能多地从 qdisc 中取出数据包，交给网络适配器驱动。简单的 QDISC 是 “pfifo”，它不进行任何处理，仅仅是一个纯粹的先进先出（FIFO）队列。它在网络接口暂时无法处理流量时，会存储这些流量。4.2. CLASSES一些 qdiscs 可以包含 classes，这些 classes 进一步包含 qdiscs——流量可能会被排入这些内部的 qdiscs 中。当内核尝试从这样的 classful qdisc 中取出数据包时，数据包可能来自任何 class。qdisc 可能例如通过尝试从某些 classes 中先取出数据包来优先处理某些类型的流量。4.3. FILTERS过滤器用于 classful qdisc 来确定数据包将被排入哪个 class。当流量到达一个具有子类的 class 时，它需要被分类。可以使用多种方法来实现分类，其中之一就是过滤器。所有附加到 class 的过滤器都会被调用，直到其中一个返回判决。如果没有做出判决，可能会有其他标准可用。这在不同的 qdisc 中有所不同。需要注意的是，过滤器存在于 qdiscs 中——它们不是事件的控制者。可用的过滤器包括：basic: 基于 ematch 表达式过滤数据包。详见 tc-ematch(8)。bpf: 使用 (e)BPF 过滤数据包，详见 tc-bpf(8)。cgroup: 基于其进程的控制组过滤数据包。详见 tc-cgroup(8)。flow, flower: 基于流量的分类器，按流量（通过可选择的键识别）过滤数据包。详见 tc-flow(8) 和 tc-flower(8)。fw: 基于 fwmark 过滤。将 fwmark 值直接映射到流量类。详见 tc-fw(8)。route: 基于路由表过滤数据包。详见 tc-route(8)。rsvp: 匹配资源预留协议（RSVP）数据包。tcindex: 基于流量控制索引过滤数据包。详见 tc-tcindex(8)。u32: 在任意数据包数据上进行通用过滤，辅助以抽象常见操作的语法。详见 tc-u32(8)。matchall: 匹配每个数据包的流量控制过滤器。详见 tc-matchall(8)。4.4. QEVENTSQdiscs 可以在 qdisc 中发生某些感兴趣的事件时调用用户配置的操作。每个 qevent 可以是未使用的，或者附加一个 block。然后将过滤器附加到该 block，使用 “tc block BLOCK_IDX” 语法。当与附件点相关的 qevent 发生时，将执行该 block。例如，数据包可以被丢弃或延迟等，具体取决于 qdisc 和相关的 qevent。示例：tc qdisc add dev eth0 root handle 1: red limit 500K avpkt 1K \ qevent early_drop block 10 tc filter add block 10 matchall action mirred egress mirror dev eth14.5. 无类 QDISCS无类 qdiscs 包括：choke: CHOKe（选择并保留响应流，选择并丢弃非响应流）是一种无类 qdisc，旨在识别并惩罚那些垄断队列的流量。CHOKe 是 RED 的一种变体，配置方式与 RED 类似。codel: CoDel（发音为 "coddle"）是一种自适应的 “无旋钮” 活跃队列管理算法（AQM），旨在解决 RED 及其变体的一些缺陷。它通过动态调整队列的丢弃概率来减少队列延迟和波动。[p|b]fifo: 最简单的 qdisc，纯粹的先进先出（FIFO）行为。按数据包或字节限制队列长度。例如，pfifo 和 bfifo 分别表示基于数据包和字节的 FIFO 队列。fq: Fair Queue Scheduler 实现 TCP 节流，并扩展到每个 qdisc 的数百万个并发流。它通过将流量公平地分配给所有流，避免了流量集中在少数流上，确保了多流环境中的公平性。fq_codel: Fair Queuing Controlled Delay 是一种将公平排队与 CoDel AQM 方案结合的队列规则。FQ_Codel 使用随机模型将传入的数据包分类到不同的流中，为每个流提供公平的带宽份额。每个流的排队由 CoDel 规则管理，避免了流内的重新排序，因为 CoDel 内部使用 FIFO 队列。fq_pie: FQ-PIE（带有比例积分控制器增强的流量排队）将 Flow Queuing 与 PIE AQM 方案结合。FQ-PIE 使用 Jenkins 哈希函数将传入的数据包分类到不同的流中，为所有流提供公平的带宽份额。每个流由 PIE 算法管理，旨在减少延迟和队列积压。gred: Generalized Random Early Detection 将多个 RED 队列组合以实现多个丢弃优先级。这对于实现保证转发（如 RFC 2597）是必需的。通过使用多个队列，可以根据流量类型进行更精细的控制。hhf: Heavy-Hitter Filter 区分小流和重流。其目标是捕捉重流，并将其移动到优先级较低的队列中，从而避免批量流量对关键流量的延迟影响。ingress: 这是一个特殊的 qdisc，专门用于入口流量控制。它允许在数据包到达接口之前进行流量整形和处理。mqprio: Multiqueue Priority Qdisc 是一种简单的队列规则，它允许使用优先级和可配置的优先级到流量类别映射，将流量流分配到硬件队列范围。这里的流量类别是指一组连续的 qdisc 类别，1:1 映射到一组硬件暴露的队列。multiq: Multiqueue 是一种针对具有多个 Tx 队列的设备优化的 qdisc。它被添加用于硬件希望避免头部阻塞的情况。它将循环遍历各个带宽，并在出队数据包之前验证与带宽相关联的硬件队列是否未停止。netem: Network Emulator 是 Linux 流量控制设施的增强功能，它允许向来自选定网络接口的数据包添加延迟、丢包、重复和其他特性。pfifo_fast: 适用于 “Advanced Router” 启用内核的标准 qdisc。包括一个三带队列，它尊重服务类型标志以及可能分配给数据包的优先级。pie: Proportional Integral controller-Enhanced (PIE) 是一种控制理论的活跃队列管理方案。它基于比例积分控制器，但旨在控制延迟。red: Random Early Detection 通过在接近配置的带宽分配时随机丢弃数据包来模拟物理拥塞。非常适合带宽非常大的应用。rr: Round-Robin qdisc 支持多队列网络设备。自 Linux 2.6.27 内核版本起被移除。sfb: Stochastic Fair Blue 是一种基于数据包丢失和链路利用历史的无类 qdisc，旨在管理拥塞，同时尝试防止非响应流（即不对拥塞标记或丢包做出反应的流）影响响应流的性能。与 RED 不同，BLUE 尝试自动确定理想的标记概率，而不需要配置标记概率。sfq: Stochastic Fairness Queueing 重新排序排队的流量，以便每个 “会话” 轮流发送数据包。tbf: Token Bucket Filter 适合将流量减速到精确配置的速率。对大带宽具有良好的扩展性。4.6. 配置无类 QDISCS在没有类 qdiscs 的情况下，无类 qdiscs 只能附加在设备的根节点上。完整的语法为：tc qdisc add dev DEV root QDISC QDISC-PARAMETERS要删除，请执行：tc qdisc del dev DEV root在没有配置 qdisc 的情况下，pfifo_fast qdisc 是自动默认的。4.7. 类别 QDISCS类别 qdiscs 包括：ATM将流映射到基础异步传输模式设备的虚拟电路中。CBQ基于类别的排队实现了丰富的链路共享层次结构。它包含了形状元素和优先级功能。形状通过基于平均数据包大小和基础链路带宽的链路空闲时间计算进行。对于某些接口，带宽可能不明确。DRR缺额轮询调度器是对随机公平排队（SFQ）的更灵活的替代方案。与 SFQ 不同，它没有内置队列——你需要添加类别，然后设置过滤器以相应地分类数据包。这对于使用具有不同设置的 RED qdisc 来处理特定流量可能很有用。没有默认类别——如果数据包无法分类，则会被丢弃。DSMARK根据 TOS 字段对数据包进行分类，根据分类更改数据包的 TOS 字段。ETSETS qdisc 是一个合并了 PRIO 和 DRR qdiscs 功能的调度程序。ETS 使配置一组严格和带宽共享的带宽变得简单，以实现 802.1Qaz 中描述的传输选择。HFSC分层公平服务曲线为叶类别提供精确的带宽和延迟分配，并公平分配多余的带宽。与 HTB 不同，它通过丢弃数据包来实现低延迟，从而使交互式会话受益。HTB分层令牌桶实现了具有丰富链路共享层次结构的类别，强调符合现有实践。HTB 促进了对类别的带宽保证，同时允许指定类别间共享的上限。它包含基于 TBF 的形状元素，并可以对类别进行优先级排序。PRIOPRIO qdisc 是一个非形状容器，具有可配置数量的类别，这些类别按照顺序出队。这允许轻松地对流量进行优先级排序，其中较低的类别只有在较高类别没有数据包可用时才会发送。为了方便配置，默认情况下会遵守服务类型位。QFQ快速公平排队是一个 O(1) 调度器，提供接近最优的保证，它是第一个以常数成本实现这一目标的调度器，并且在处理组和数据包长度方面也保持常数。QFQ 算法没有循环，并使用非常简单的指令和数据结构，适合硬件实现。4.8. 操作原理类别形成一个树形结构，每个类别有一个单一的父节点。一个类别可以有多个子节点。某些 qdiscs 允许在运行时添加类别（如 CBQ、HTB），而其他 qdiscs（如 PRIO）则以静态数量的子节点创建。允许动态添加类别的 qdiscs 可以有零个或多个子类别，用于排队流量。此外，每个类别包含一个叶子 qdisc，默认具有 pfifo 行为，虽然可以用其他 qdisc 替代。这个 qdisc 可能再次包含类别，但每个类别只能有一个叶子 qdisc。当数据包进入类别 qdisc 时，它可以被分类到其中的一个类别。分类的三个标准如下，但并非所有 qdiscs 都会使用这三个标准：tc 过滤器如果 tc 过滤器附加到类别上，它们首先被查阅以获取相关指令。过滤器可以匹配数据包头的所有字段，以及由 iptables 应用的防火墙标记。服务类型（TOS）一些 qdiscs 内置了根据 TOS 字段分类数据包的规则。skb->priority用户空间程序可以使用 SO_PRIORITY 选项在 'skb->priority' 字段中编码类别 ID。树中的每个节点可以有自己的过滤器，但较高级别的过滤器也可以直接指向较低级别的类别。如果分类失败，数据包会被排队到该类别附加的叶子 qdisc 上。具体细节请参考 qdisc 的手册页。4.9. 命名所有 qdiscs、类别和过滤器都有 ID，这些 ID 可以指定，也可以自动分配。ID 由一个主编号和一个次编号组成，以冒号分隔 - 主编号:次编号。主编号和次编号都是十六进制数字，并且限制在 16 位内。有两个特殊值：root 由全 1 的主编号和次编号表示，未指定则为全 0。QDISCS一个可能拥有子节点的 qdisc 被分配一个主编号，称为 'handle'，而次编号命名空间则留给类别。handle 表达为 '10:'。通常，预计会有子节点的 qdiscs 会明确分配一个 handle。CLASSES居于 qdisc 下的类别共享它们的 qdisc 主编号，但每个类别都有一个称为 'classid' 的单独次编号，该编号与其父类别无关，仅与其父 qdisc 相关。类别的命名遵循与 qdiscs 相同的习惯。FILTERS过滤器有一个三部分 ID，只有在使用哈希过滤器层次结构时才需要。4.10. 参数以下参数在 TC 中被广泛使用。有关其他参数，请参见个别 qdiscs 的手册页。速率（RATES）带宽或速率。这些参数接受浮点数，可能后跟单位（支持 SI 和 IEC 单位），或者是浮点数后跟 '%' 字符以指定为设备速度的百分比（例如 5%，99.5%）。警告：将速率指定为百分比意味着是当前速度的一个分数；如果速度发生变化，值不会重新计算。bit 或裸数每秒位数kbit每秒千比特mbit每秒兆比特gbit每秒千兆比特tbit每秒太比特bps每秒字节数kbps每秒千字节mbps每秒兆字节gbps每秒千兆字节tbps每秒太字节指定 IEC 单位时，替换 SI 前缀（k-、m-、g-、t-）为 IEC 前缀（ki-、mi-、gi-、ti-）。TC 内部将速率存储为 32 位无符号整数，单位为 bps，因此可以指定最大速率为 4294967295 bps。时间（TIMES）时间长度。可以指定为浮点数后跟可选单位：s、sec 或 secs整秒ms、msec 或 msecs毫秒us、usec、usecs 或裸数微秒TC 定义了自己的时间单位（等于微秒），并将时间值存储为 32 位无符号整数，因此可以指定最大时间值为 4294967295 微秒。大小（SIZES）数据量。可以指定为浮点数后跟可选单位：b 或裸数字节kbit千比特kb 或 k千字节mbit兆比特mb 或 m兆字节gbit千兆比特gb 或 g千兆字节TC 内部将大小存储为 32 位无符号整数，单位为字节，因此可以指定最大大小为 4294967295 字节。值（VALUES）其他没有单位的值。这些参数默认按十进制解释，但可以通过添加 '0' 或 '0x' 前缀分别指示 TC 按八进制或十六进制解释它们。5. TC 命令以下命令适用于 qdiscs、类别和过滤器：add向节点添加 qdisc、类别或过滤器。对于所有实体，必须传递父节点，方法是通过其 ID 或直接附加到设备的根节点。当创建 qdisc 或过滤器时，可以使用 handle 参数进行命名。类别使用 classid 参数进行命名。delete通过指定其 handle（也可以是 'root'）删除 qdisc。所有子类别及其叶子 qdiscs 将被自动删除，以及任何附加到它们的过滤器。change某些实体可以“就地”修改。与 'add' 共享语法，唯一的区别是 handle 不能更改，父节点也不能更改。换句话说，change 不能移动节点。replace在现有节点 ID 上执行几乎原子的删除/添加操作。如果节点尚不存在，则会创建它。get显示给定接口 DEV、qdisc-id、优先级、协议和过滤器 ID 的单个过滤器。show显示附加到给定接口的所有过滤器。必须传递有效的父 ID。link仅适用于 qdiscs，执行替换，其中节点必须已经存在。5.1. 监控tc 工具可以监控由内核生成的事件，例如添加/删除 qdiscs、过滤器或操作，或修改现有的。以下命令适用于监控：file如果给出 file 选项，tc 不会监听内核事件，而是打开给定的文件并转储其内容。文件必须是二进制格式并包含 netlink 消息。5.2. 选项-b, -b filename, -batch, -batch filename从提供的文件或标准输入中读取命令并执行。第一次失败将导致 tc 终止。-force在批处理模式下遇到错误时不要终止 tc。如果在执行命令时发生错误，应用程序的返回码将为非零。-o, -oneline将每条记录输出到单行，用 \ 字符替换换行符。这在你想用 wc(1) 统计记录数或用 grep(1) 搜索输出时很方便。-n, -net, -netns 将 tc 切换到指定的网络命名空间 NETNS。实际上，这只是简化了执行以下操作：ip netns exec NETNS tc [ OPTIONS ] OBJECT { COMMAND | help }为：tc -n[etns] NETNS [ OPTIONS ] OBJECT { COMMAND | help }-N, -Numeric直接打印协议、作用域、dsfield 等的数字，而不是将其转换为可读名称。-cf, -conf 指定配置文件的路径。此选项与其他选项（如 -nm）一起使用。-t, -timestamp当 tc 监控运行时，在事件消息之前打印时间戳，格式为：Timestamp: <Day> <Month> <hh:mm:ss> <YYYY> <usecs> usec-ts, -tshort当 tc 监控运行时，在事件消息之前打印简短时间戳，格式为：[<YYYY>-<MM>- T<hh:mm:ss>.<ms>]5.3. 格式show 命令有额外的格式化选项：-s, -stats, -statistics输出更多有关数据包使用的统计信息。-d, -details输出关于速率和单元大小的更详细信息。-r, -raw输出句柄的原始十六进制值。-p, -pretty对于 u32 过滤器，解码偏移量和掩码值为基于 TCP/IP 的等效过滤器命令。在 JSON 输出中，添加空格以提高可读性。-iec以 IEC 单位打印速率（例如 1K = 1024）。-g, -graph以 ASCII 图形显示类别。如果指定了 -s 选项，则在每个类别下打印通用统计信息。类别只能通过 dev 选项过滤。-ccolor配置彩色输出。如果省略参数或选择 always，则无论标准输出状态如何，彩色输出都会启用。如果选择 auto，则在启用彩色输出之前会检查标准输出是否为终端。如果选择 never，则禁用彩色输出。如果指定多次，最后一个参数将优先。如果同时给出了 -json，则此标志会被忽略。-j, -json以 JSON 格式显示结果。-nm, -name从 /etc/iproute2/tc_cls 文件或由 -cf 选项指定的文件中解析类别名称。此文件仅为类别 ID 到类别名称的映射：# 这里是注释 1:40 voip # 这里是另一个注释 1:50 web 1:60 ftp 1:2 home如果指定了 -nm 而未指定 -cf，且 /etc/iproute2/tc_cls 文件不存在，tc 不会失败，这使得可以传递 -nm 选项以创建 tc 别名。-br, -brief仅打印识别过滤器和操作（句柄、cookie 等）以及统计信息所需的基本数据。此选项目前仅支持 tc filter show 和 tc actions ls 命令。5.4. 示例tc -g class show dev eth0以 ASCII 图形显示 eth0 接口上的类别。tc -g -s class show dev eth0以 ASCII 图形显示类别，并在每个类别下方显示统计信息。6. 历史tc 由 Alexey N. Kuznetsov 编写，并在 Linux 2.2 中添加。7. 参见tc-basic(8)`, `tc-bfifo(8)`, `tc-bpf(8)`, `tc-cake(8)`, `tc-cbq(8)`, `tc-cgroup(8)`, `tc-choke(8)`, `tc-codel(8)`, `tc-drr(8)`, `tc-ematch(8)`, `tc-ets(8)`, `tc-flow(8)`, `tc-flower(8)`, `tc-fq(8)`, `tc-fq_codel(8)`, `tc-fq_pie(8)`, `tc-fw(8)`, `tc-hfsc(7)`, `tc-hfsc(8)`, `tc-htb(8)`, `tc-mqprio(8)`, `tc-pfifo(8)`, `tc-pfifo_fast(8)`, `tc-pie(8)`, `tc-red(8)`, `tc-route(8)`, `tc-sfb(8)`, `tc-sfq(8)`, `tc-stab(8)`, `tc-tbf(8)`, `tc-tcindex(8)`, `tc-u32(8)用户文档请访问 http://lartc.org/，但请将 bug 报告和补丁发送到：netdev@vger.kernel.org8. 作者手册页由 bert hubert（ahu@ds9a.nl）维护。iproute2 2001年12月16日 TC(8)
目录 [隐藏] 配对方法选择使用 GAPBondMgr不同配对方法的 GAPBondMgr 示例禁用配对启用配对LE Secure ConnectionsLESC 限制和建议Just Works 配对Passcode EntryNumeric ComparisonOut of Band 配对启用绑定的 GAPBondMgr 示例GAPBondMgr 和 SNV GAP Bond Manager 和 LE 安全连接GAP Bond Manager (GAPBondMgr) 是一个可配置模块，用于从应用程序中承担大部分与安全管理器 (SM) 协议相关的配对和绑定安全机制。GAPBondMgr 在协议栈任务的上下文中执行。术语描述配对 (Pairing)生成和交换密钥的过程。不要与建立两个设备之间的 BLE 连接混淆。加密 (Encryption)配对后或重新连接时加密数据。关联 (Association)基于设备的 I/O 能力选择的配对方法。支持 Just Works、数字比较、密码输入和带外 (OOB) 配对。认证 (Authentication)使用支持 MITM 保护的关联方法进行的配对过程。绑定 (Bonding)将配对过程中生成的密钥存储在非易失性内存中，以用于下次加密。授权 (Authorization)除了认证之外的附加应用级别验证。OOB (Out of Band)配对密钥不是通过 RF 交换，而是通过串口或 NFC 等其他方式交换。这也提供了 MITM 保护。MITM (Man in the Middle)中间人攻击保护。在配对过程中提供认证，防止恶意攻击者在密钥交换期间冒充对等设备。Just Works无认证的配对关联方法，密钥交换不受 MITM 保护。配对过程：通过 配对方法选择 中描述的方法交换密钥。加密过程：使用步骤 1 中的密钥加密链路。绑定过程：将密钥存储在安全闪存 (Simple Non Volatile memory, SNV) 中。重新连接：使用存储在 SNV 中的密钥加密链路。配对方法选择蓝牙核心规范版本 4.2 添加了支持 LE Secure Connections 的功能，以增强 BLE 配对过程的强度。详细描述请参见 Bluetooth Core Specification Version 5.1 的 Security Architecture 部分 ([Vol 1], Part A, Section 5.1)。以前版本 (4.1 和 4.0) 使用的方法定义为 LE Legacy Pairing。主要区别在于 Secure Connection 使用椭圆曲线 Diffie-Hellman (ECDH) 密码术，而 LE Legacy Pairing 则不使用。以下是核心规范中定义的四种配对方法，每种配对方法在 不同配对方法的 GAPBondMgr 示例 中有详细描述。Just Works (LE Secure Connections 或 LE Legacy)密码输入 (Passkey Entry, LE Secure Connections 或 LE Legacy)数字比较 (Numeric Comparison, LE Secure Connections)带外 (Out Of Band, LE Secure Connections 或 LE Legacy)配对方法的选择以及配对是否成功取决于配对过程中双方设备的以下参数：带外 (OOB) 设置 / 未设置中间人攻击保护 (MITM) 设置 / 未设置输入/输出 (IO) 能力支持 / 不支持 LE 安全连接GAPBondMgr 参数与以下参数相对应，详情请参见 BLE Stack API 参考 (GAPBondMgr 部分)。GAPBOND_OOB_ENABLED: 带外 (OOB) 设置 / 未设置GAPBOND_MITM_PROTECTION: 中间人攻击保护 (MITM) 设置 / 未设置GAPBOND_IO_CAPABILITIES: 输入/输出 (IO) 能力GAPBOND_SECURE_CONNECTION: 支持 / 不支持 LE 安全连接超出 Bluetooth Core Specification Version 5.1 定义的内容，本参数还会影响配对是否成功，详情参见 BLE Stack API 参考 (GAPBondMgr 部分)。以下是从 Bluetooth Core Specification Version 5.1 的 Selecting Key Generation Method 部分 ([Vol 3], Part H, Section 2.3.5.1) 中的表格。使用这些表格确定任何参数组合下选择的配对模式。如果两台设备都支持 LE 安全连接，请使用 Figure 83 确定下一步。图 83. 当两台设备都支持 LE 安全连接时的 GAPBondMgr 参数。如果至少有一台设备 不 支持 LE 安全连接，请使用 Figure 84 确定下一步。图 84. 当一台或两台设备不支持 LE Secure Connections 时的 GAPBondMgr 参数。如果基于上述表格，IO 能力被用于确定关联模型，请使用以下图表。图 85. 具有 IO 能力的 GAPBondMgr 参数。使用 GAPBondMgr本节描述了应用程序必须执行的配置、启动和使用 GAPBondMgr 的步骤。GAPBondMgr 定义在 gapbondmgr.c 和 gapbondmgr.h 中。完整的 API 包括命令、可配置参数、事件和回调，详情请参见 BLE Stack API Reference (GAPBondMgr 部分)。使用 GAPBondMgr 模块的一般步骤如下：在协议栈项目的 build_config.opt 中定义以下内容，以包含 GAPBondMgr 功能：-DGAP_BOND_MGR如果使用 LE Secure Connections，PDU 大小必须 >= 69。可以通过在应用项目中定义以下预处理符号来设置：MAX_PDU_SIZE=69。另外，LE Secure Connections 可用的最小堆大小为 3690。详见 Dynamic Memory Allocation。通过初始化所需的参数来配置 GAPBondMgr。GAPBondMgr 配置参数不是持久性的，设备重置后必须重新配置。详见 BLE Stack API Reference (GAPBondMgr 部分)。有关各种配对/绑定模式的示例，请参见 GAPBondMgr Examples for Different Pairing Methods。使用 GAPBondMgr 注册应用回调，以便应用程序可以与 GAPBondMgr 通信并接收事件通知。// 在启动设备后向 Bond Manager 注册 GAPBondMgr_Register(&bondmanager_callbacks);这里的 bondmanager_callbacks 定义为包含 GAPBondMgr 回调的结构体。一个密码回调函数是必须的。// Bond Manager 回调 static gapBondCBs_t bondMgrCBs = { SimpleCentral_passcodeCb, // 密码回调 SimpleCentral_pairStateCb // 配对/绑定状态回调 };一旦 GAPBondMgr 配置完成，从应用程序的角度来看，它主要是自主运行的。当建立连接时，GAPBondMgr 根据初始化期间设置的配置参数管理配对和绑定。它还通过定义的回调与应用程序进行通信。可以随时异步设置一些参数并调用函数，详见 BLE Stack API Reference (GAPBondMgr 部分)。大部分 GAPBondMgr 和应用程序之间的通信此时通过在步骤 4 中注册的回调进行。图 86 是 GAPBondMgr 通知应用程序配对完成的流程图示例。其他各种事件的流程也类似，后续部分将详细说明。图 86. GAPBondMgr 回调示例。不同配对方法的 GAPBondMgr 示例本节提供了可以实现的安全类型的消息图示。这些模式假设存在合适的 I/O 能力，并且是否支持 LE Secure Connections 允许配对方法。详见 配对方法选择 了解这些参数如何影响配对。这些示例仅考虑配对方面。绑定可以添加到每种配对类型中，并在下一节中展示。注意此处的代码片段不是完整的功能示例，仅用于说明目的。禁用配对将配对模式设置为 GAPBOND_PAIRING_MODE_NO_PAIRING 时，BLE 栈会自动拒绝任何配对尝试。按如下方式配置 GAPBondMgr 以禁用配对：// 禁用配对 uint8_t pairMode = GAPBOND_PAIRING_MODE_NO_PAIRING; GAPBondMgr_SetParameter(GAPBOND_PAIRING_MODE, sizeof(uint8_t), &pairMode);启用配对在形成连接后开始或允许配对过程，GAPBondMgr 可以配置为自动请求配对或等待对等设备的配对请求。实际行为取决于设备的 GAP 角色（Central 或 Peripheral）和 GAPBondMgr 配对模式 (GAPBOND_PAIRING_MODE) 的设置。在 Peripheral 角色设备上启动配对过程时，GAPBOND_PAIRING_MODE_INITIATE 会在 GAPBondMgr 知道连接已形成后不久发送 Slave Security Request。对于 Central 角色设备，GAPBOND_PAIRING_MODE_INITIATE 会发送配对请求或请求 Link Layer 加密链路（如果设备之前已配对/绑定）：// 启动配对请求 uint8_t pairMode = GAPBOND_PAIRING_MODE_INITIATE; GAPBondMgr_SetParameter(GAPBOND_PAIRING_MODE, sizeof(uint8_t), &pairMode);Peripheral 可以配置为在配对模式设置为 GAPBOND_PAIRING_MODE_WAIT_FOR_REQ 时等待 Central 的 配对请求。选择此配对模式时，GAPBondMgr 会根据其他配置参数自动响应 配对响应。// 等待配对请求 uint8_t pairMode = GAPBOND_PAIRING_MODE_WAIT_FOR_REQ; GAPBondMgr_SetParameter(GAPBOND_PAIRING_MODE, sizeof(uint8_t), &pairMode);提示:当与智能手机 Central 设备配对时，建议使用 GAPBOND_PAIRING_MODE_WAIT_FOR_REQ，因为当 Peripheral 发送 Slave Security Request 时可能会出现未定义行为。当访问 GATT 安全特性时，iOS 和 Android 都会在 Peripheral 响应 身份验证不足 错误响应时启动配对。LE Secure ConnectionsLE Secure Connections 在 BLE5-Stack 中默认启用。如果不想使用 LE Secure Connections，请在 GAPBondMgr 初始化期间将 GAPBOND_SECURE_CONNECTION 变量设置为 GAPBOND_SECURE_CONNECTION_NONE。uint8_t gapbondSecure = GAPBOND_SECURE_CONNECTION_NONE; GAPBondMgr_SetParameter(GAPBOND_SECURE_CONNECTION, sizeof(uint8_t), &gapbondSecure);在使用 LE Secure Connections 时解读空中嗅探器日志时，需使用 Bluetooth Core Specification Version 5.1 Vol 3 Part H 第 2.3.5.6.1 节中定义的特定“调试”密钥。在 BLE5-Stack 中，可以通过 SysConfig 启用该密钥，选中 BLE，Bond Manager 下的 'ECC Debug Keys'。当启动设备或非启动设备使用此特定调试密钥时，支持 LE Secure Connections 的空中嗅探器设备可以确定 LTK，从而监控/解密整个连接中的加密通信。LESC 限制和建议LE Secure Connections 使用 ECDH 公私钥对作为配对过程的一部分。详见 配对方法选择 和 Bluetooth Core Specification Version 5.1 的 LE Secure Connections 配对第二阶段。简而言之，在 LESC 配对的第一阶段，每个设备会生成自己的 ECDH 公私钥对。在第二阶段，每个设备将根据交换的公钥计算 Diffie-Hellman (DH) 密钥。ECC 公私钥和 DH 密钥生成由 CC13x2 或 CC26x2 上的私钥加速器 (PKA) 的 ECDH TI-RTOS 驱动实现。这些操作是 阻塞 的，需要约 136.5ms 执行。在 LESC 配对过程中，链接层 (LL) 会两次访问 ECDH 驱动程序，一次生成公私钥对，一次生成 DH 密钥。这意味着堆栈 OSAL 任务将在配对过程中两次阻塞约 136.5ms。然而，用户应用程序任务在此期间仍然可以运行。因此，TI 建议在使用 LESC 时遵循以下建议：监督超时时间 >= 140ms为减少所需的阻塞时间，用户应用程序可以在配对过程之前生成公私钥对，或定义何时回收密钥，使用以下 GapBondMgr 参数。这些选项是互斥的，因为应用程序生成密钥会绕过回收参数。要真正最小化 ECC 对 BLE 连接的影响，请在建立任何连接之前执行 ECC。GAPBOND_ECC_KEYSGAPBOND_ECCKEY_REGEN_POLICYJust Works 配对Just Works 配对允许加密而无需中间人 (MITM) 认证，因此易受 MITM 攻击。Just Works 配对可以是 LE Legacy 或 LE Secure Connections 配对。对于 Just Works 配对，GAPBondMgr 不需要应用程序的额外输入。如下配置 GAPBondMgr 进行 Just Works 配对：uint8_t mitm = FALSE; GAPBondMgr_SetParameter(GAPBOND_MITM_PROTECTION, sizeof(uint8_t), &mitm);图 87 描述了 GAPBondMgr 和应用程序在 Just Works 配对中的交互。应用程序在发送配对请求后会收到 GAPBOND_PAIRING_STATE_STARTED 事件，在配对过程完成后会收到 GAPBOND_PAIRING_STATE_COMPLETE 事件。此时，链路已加密。图 87. Just Works 配对。Passcode Entry密码输入是一种认证配对方式，可以防止 MITM 攻击。它可以与 LE Legacy 配对或 Secure Connections 配对一起使用。在这种配对方法中，一个设备显示 6 位数密码，另一个设备输入密码。详见 配对方法选择，IO 能力决定哪个设备执行哪个角色。启动 GAPBondMgr 时注册的密码回调用于输入或显示密码。以下是启动密码输入配对的示例，其中密码被显示。定义密码回调// Bond Manager 回调 static gapBondCBs_t bondMgrCBs = { SimpleCentral_passcodeCb, // 密码回调 SimpleCentral_pairStateCb // 配对/绑定状态回调 }; static void SimpleCentral_passcodeCb(uint8_t *deviceAddr, uint16_t connHandle, uint8_t uiInputs, uint8_t uiOutputs, uint32_t numComparison) { uint8_t *pData; // 为密码事件分配空间。 if ((pData = ICall_malloc(sizeof(uint8_t)))) { *pData = uiOutputs; // 排队事件。 SimpleCentral_enqueueMsg(SC_EVT_PASSCODE_NEEDED, 0, pData); } }配置 GAPBondMgruint8_t pairMode = GAPBOND_PAIRING_MODE_INITIATE; uint8_t mitm = TRUE; GAPBondMgr_SetParameter(GAPBOND_PAIRING_MODE, sizeof(uint8_t), &pairMode); GAPBondMgr_SetParameter(GAPBOND_MITM_PROTECTION, sizeof(uint8_t), &mitm);处理密码回调并发送响应到栈。//! BLE 默认密码 #define B_APP_DEFAULT_PASSCODE 123456 static void SimpleCentral_processPasscode(uint16_t connHandle, uint8_t uiOutputs) { // 向用户显示密码 if (uiOutputs != 0) { Display_printf(dispHandle, SC_ROW_CUR_CONN, 0, "Passcode: %d", B_APP_DEFAULT_PASSCODE); } // 发送密码响应 GAPBondMgr_PasscodeRsp(connHandle, SUCCESS, B_APP_DEFAULT_PASSCODE); }根据从 GAPBondMgr 返回的 uiInputs 和 uiOutputs，密码必须显示或输入。然后将密码发送到 GAPBondMgr 以继续配对。在此情况下，密码静态设置为 123456。在实际产品中，密码可能是随机生成的，设备必须提供一种方式让用户输入密码，然后将其发送到 GAPBondMgr。GAPBondMgr 和应用程序之间的交互如图 88 所示。居中示例Numeric Comparison数字比较是一种认证配对方式，可以防止中间人 (MITM) 攻击。它仅作为 LE Secure Connections 配对方式；不适用于 LE Legacy。在数字比较配对中，两个设备都会显示一个 6 位数的代码。每个设备然后通过按钮按压或其他 Yes-No 输入确认代码是否匹配。启动 GAPBondMgr 时注册的密码回调用于显示 6 位数的代码。以下是启动数字比较配对的示例，其中显示了密码。IO 能力必须设置为显示/Yes-No 才能选择数字比较（即两边都是显示/Yes-No）。定义密码回调以显示代码。// Bond Manager 回调 static gapBondCBs_t bondMgrCBs = { SimpleCentral_passcodeCb, // 密码回调 SimpleCentral_pairStateCb // 配对/绑定状态回调 }; static void SimpleCentral_passcodeCb(uint8_t *deviceAddr, uint16_t connHandle, uint8_t uiInputs, uint8_t uiOutputs, uint32_t numComparison) { gapPasskeyNeededEvent_t *pData; // 为密码事件分配空间。 if ((pData = ICall_malloc(sizeof(gapPasskeyNeededEvent_t)))) { memcpy(pData->deviceAddr, deviceAddr, B_ADDR_LEN); pData->connectionHandle = connHandle; pData->numComparison = numComparison; // 排队事件。 SimpleCentral_enqueueMsg(SC_EVT_PASSCODE_NEEDED, 0, pData); } }配置 GAPBondMgruint8_t pairMode = GAPBOND_PAIRING_MODE_WAIT_FOR_REQ; uint8_t scMode = GAPBOND_SECURE_CONNECTION_ONLY; uint8_t mitm = TRUE; uint8_t ioCap = GAPBOND_IO_CAP_DISPLAY_YES_NO; GAPBondMgr_SetParameter(GAPBOND_IO_CAPABILITIES, sizeof(uint8_t), &ioCap); GAPBondMgr_SetParameter(GAPBOND_PAIRING_MODE, sizeof(uint8_t), &pairMode); GAPBondMgr_SetParameter(GAPBOND_MITM_PROTECTION, sizeof(uint8_t), &mitm); GAPBondMgr_SetParameter(GAPBOND_SECURE_CONNECTION, sizeof(uint8_t), &scMode);处理密码回调并显示代码。static void SimpleCentral_processPasscode(uint16_t connHandle, uint8_t uiOutputs) { // 向用户显示密码 if (uiOutputs != 0) { Display_printf(dispHandle, SC_ROW_CUR_CONN, 0, "Passcode: %d", B_APP_DEFAULT_PASSCODE); } // 发送密码响应 GAPBondMgr_PasscodeRsp(connHandle, SUCCESS, B_APP_DEFAULT_PASSCODE); }接受用户输入的 Yes-No 并将响应发送到 GAPBondMgr。if (keys & KEY_RIGHT) { // 发送响应以表明代码匹配 GAPBondMgr_PasscodeRsp(connHandle, SUCCESS, TRUE); return; }在这种情况下，GAPBondMgr_PasscodeRsp 的第三个参数通常接受一个密码，被重载为向堆栈发送 TRUE 以表明代码匹配并继续配对。数字比较过程如图 89 所示。图 89. 数字比较。Out of Band 配对带外 (OOB) 是一种功能，允许两个设备通过不在设备频段内的通信通道（例如 NFC）发送认证信息。目的是让没有输入或输出能力的两个设备创建认证配对。通过发送三个认证参数来实现：设备地址、随机数和确认值。OOB 认证的主要目的是防止 MITM 攻击。方法是由一个设备生成密钥（可以是发起方或响应方）。然后 OOB 数据通过 BLE 之外的其他通信手段传输给另一台设备。OOB 数据在运行时生成。这可以使用椭圆曲线密码术 (ECC) 或不使用。在此示例中，我们将使用 ECC 生成密钥。要使用 Central 应用（如 Simple Central）生成 OOB 数据，请执行以下步骤：创建 gapBondOOBData_t 变量，一个用于本地 OOB 数据，一个用于远程设备的数据：gapBondOOBData_t localOobData; gapBondOOBData_t remoteOobData;定义配对状态回调// Bond Manager 回调 static gapBondCBs_t bondMgrCBs = { SimpleCentral_passcodeCb, // 密码回调 SimpleCentral_pairStateCb // 配对/绑定状态回调 }; static void SimpleCentral_pairStateCb(uint16_t connHandle, uint8_t state, uint8_t status) { scPairStateData_t *pData; // 为事件数据分配空间。 if ((pData = ICall_malloc(sizeof(scPairStateData_t)))) { pData->connHandle = connHandle; pData->status = status; // 排队事件。 if (SimpleCentral_enqueueMsg(SC_EVT_PAIR_STATE, state, (uint8_t*) pData) != SUCCESS) { ICall_free(pData); } } }如果要使用 ECC 生成密钥，请调用 GAPBondMgr_GenerateEccKeys。（如果不调用此 API，则密钥将在没有 ECC 的情况下生成。）在处理配对状态回调时添加对 GAPBondMgr_SCGetLocalOOBParameters 的调用。static void SimpleCentral_processPairState(uint8_t state, scPairStateData_t* pPairData) { uint8_t status = pPairData->status; uint8_t pairMode = 0; if (state == GAPBOND_GENERATE_ECC_DONE) { if (status == SUCCESS) { GAPBondMgr_SCGetLocalOOBParameters(&localOobData); uint8_t i; for (i = 0; i < KEYLEN; i++) { Display_printf(dispHandle, SC_ROW_CUR_CONN, 0, "OOB data confirm[%d]: %d", i, localOobData.confirm[i]); } for (i = 0; i < KEYLEN; i++) { Display_printf(dispHandle, SC_ROW_CUR_CONN, 0, "OOB data rand[%d]: %d", i, localOobData.rand[i]); } } break; } }或者将此添加到现有的配对状态回调中。else if (state == GAPBOND_GENERATE_ECC_DONE) { if (status == SUCCESS) { GAPBondMgr_SCGetLocalOOBParameters(&localOobData); uint8_t i; for (i = 0; i < KEYLEN; i++) { Display_printf(dispHandle, SC_ROW_CUR_CONN+i, 0, "OOB data confirm[%d]: %d", i, localOobData.confirm[i]); } for (i = 0; i < KEYLEN; i++) { Display_printf(dispHandle, SC_ROW_CUR_CONN+16+i, 0, "OOB data rand[%d]: %d", i, localOobData.rand[i]); } } } 接收到 OOB 数据的设备（而不是生成它的设备）应将 GAPBOND_OOB_ENABLED 参数设置为 true。如果两台设备都生成并共享了它们的 OOB 数据，则都应设置此标志。在本例中，我们只在外设设备上设置此标志。uint8_t oobEnabled = TRUE; GAPBondMgr_SetParameter(GAPBOND_OOB_ENABLED, sizeof(uint8_t), &oobEnabled);执行 OOB 通信，即将 localOobData 中的数据传输给响应设备。为了简单起见，在本例中，Central 设备将显示数据，因此可以在外设设备上硬编码这些数据。Figure 90. Central 设备显示 OOB 数据Listing 79. SimplePeripheral_processGapMessage::GAP_DEVICE_INIT_DONE_EVENT OOB 数据应编码在 peripheral 设备.uint8_t oobEnabled = TRUE; GAPBondMgr_SetParameter(GAPBOND_OOB_ENABLED, sizeof(uint8_t), &oobEnabled); remoteOobData.confirm[0] = 204; remoteOobData.confirm[1] = 29; remoteOobData.confirm[2] = 240; remoteOobData.confirm[3] = 154; remoteOobData.confirm[4] = 141; remoteOobData.confirm[5] = 40; remoteOobData.confirm[6] = 132; remoteOobData.confirm[7] = 41; remoteOobData.confirm[8] = 156; remoteOobData.confirm[9] = 106; remoteOobData.confirm[10] = 91; remoteOobData.confirm[11] = 230; remoteOobData.confirm[12] = 11; remoteOobData.confirm[13] = 88; remoteOobData.confirm[14] = 117; remoteOobData.confirm[15] = 104; remoteOobData.rand[0] = 56; remoteOobData.rand[1] = 242; remoteOobData.rand[2] = 38; remoteOobData.rand[3] = 152; remoteOobData.rand[4] = 189; remoteOobData.rand[5] = 78; remoteOobData.rand[6] = 164; remoteOobData.rand[7] = 36; remoteOobData.rand[8] = 153; remoteOobData.rand[9] = 82; remoteOobData.rand[10] = 167; remoteOobData.rand[11] = 190; remoteOobData.rand[12] = 140; remoteOobData.rand[13] = 235; remoteOobData.rand[14] = 207; remoteOobData.rand[15] = 93; GAPBondMgr_SCSetRemoteOOBParameters(&remoteOobData, 1);OOB 配对处理流程 Figure 91..Figure 91. OOB 配对流程启用绑定的 GAPBondMgr 示例绑定可以为任何类型的配对启用或禁用，通过 GAPBOND_BONDING_ENABLED 参数设置。绑定发生在配对过程完成之后。要启用绑定，请按如下方式配置 GAPBondMgr：uint8_t bonding = TRUE; GAPBondMgr_SetParameter(GAPBOND_BONDING_ENABLED, sizeof(uint8_t), &bonding);启用绑定后，GAPBondMgr 会将配对过程中传输的长期密钥 (LTK) 存储到 SNV 中。有关更多信息，请参见 GAPBondMgr and SNV。完成绑定后，应用程序会收到 GAPBOND_PAIRING_STATE_COMPLETE 事件。当初次连接、配对和绑定时，应用程序配对状态回调只会传递 GAPBOND_PAIRING_STATE_BOND_SAVED。对于与已绑定设备的未来连接，安全密钥从闪存中加载，从而跳过配对过程。在这种情况下，应用程序配对状态回调只会传递 GAPBOND_PAIRING_STATE_BONDED。如图 92 所示。图 92. 启用绑定的 GAPBondMgr 示例。GAPBondMgr 和 SNV本节描述 GAPBondMgr 如何使用 SNV 闪存区域存储绑定信息。有关 SNV 的更多信息，请参见 Flash。可以存储的绑定数量由 GAP_BONDINGS_MAX 定义，该值在 gapbondmgr.h 中默认设置为 10，应用程序可以根据需要修改。如果启用“最近最少使用”方案（least recently used, LRU），则 GAPBondMgr 在没有更多可用绑定时的功能会有所不同。有关 GAPBOND_LRU_BOND_REPLACEMENT 参数的更多信息，请参见 BLE Stack API Reference (GAPBondMgr 部分)。如果此参数设置为 false，则无法添加更多绑定，除非手动删除一个绑定。如果参数设置为 true，则删除最近最少使用的绑定以为新绑定腾出空间。一个绑定条目包含以下组件：绑定记录： 包括对等设备的地址、地址类型、隐私重连地址和状态标志。总共 8 个字节，定义如下：// NV 数据结构，用于存储连接设备的地址信息 typedef struct { // // 对等设备地址 // 如果此绑定存在身份信息，则这是一个身份地址 // uint8_t addr[B_ADDR_LEN]; // // 对等设备地址类型 // GAP_Peer_Addr_Types_t addrType; // // 绑定状态标志 // GAP_BONDED_STATE_FLAGS uint8_t stateFlags; } gapBondRec_t;客户端特征配置 (CCC)： 每个条目中存储的 CCC 数量由 GAP_CHAR_CFG_MAX 定义，默认设置为 4。每个 CCC 由 4 个字节组成，定义如下：// NV 数据结构，用于存储连接设备的特征配置 typedef struct { uint16 attrHandle; // 特征句柄 uint8 value; // 该设备的特征值 } gapBondCharCfg_t;本地长期密钥 (LTK) 信息： 存储本地设备的加密信息。总共 28 个字节，结构如下：typedef struct { uint8 LTK[KEYLEN]; // 长期密钥 (LTK) uint16 div; // LTK eDiv uint8 rand[B_RANDOM_NUM_SIZE]; // LTK 随机数 uint8 keySize; // LTK 密钥大小 4. **连接设备长期密钥信息：** 存储连接设备的加密信息。也是一个 gapBondLTK_t 结构，总共 28 个字节。 5. **连接设备身份解析密钥 (IRK)：** 存储配对期间生成的 IRK。是一个 16 字节数组。 6. **连接设备签名解析密钥 (SRK)：** 存储配对期间生成的 SRK。是一个 16 字节数组。 7. **连接设备签名计数器：** 存储配对期间生成的签名计数器。是一个 4 字节的字。 ### 增加绑定条目数量 可以存储的绑定数量由 `GAP_BONDINGS_MAX` 定义，在 gapbondmgr.h 中默认设置为 10。然而，由于 SNV 的结构，如果需要 `GAP_BONDINGS_MAX` 值超过 13，则需要进行一些额外的更改以支持存储更多的绑定： 1. 修改 `GAP_BONDINGS_MAX` 为所需的最大绑定条目数。在需要删除旧绑定之前，此值不应超过 32。 2. 在文件 bcomdef.h 中修改 Bonding NV Items、GATT Configuration NV Items 和 Customer NV Items 的起始和结束范围。这通过修改 BLE_NVID_GAP_BOND_END、BLE_NVID_GATT_CFG_START、BLE_NVID_GATT_CFG_END、BLE_NVID_CUST_START 和 BLE_NVID_CUST_END 来实现。修改必须遵循以下规则： - **对于 GAP:** (BLE_NVID_GAP_BOND_END - BLE_NVID_GAP_BOND_START) >= GAP_BONDINGS_MAX * 6 - **对于 GATT:** (BLE_NVID_GATT_CFG_END - BLE_NVID_GATT_CFG_START) >= GAP_BONDINGS_MAX。 - 各范围之间不能有重叠。 - 所有索引都是 1 字节的值，因此不应超过 0xFF 或 255。
目录 [隐藏] -m64-fpack-struct\_attrribute_结论 聊聊 GCC 中的对齐最近看到一个问题，在 64 位操作系统中。实际测试环境 Ubuntu 22.04，GCC 11。struct test{ long a int b; char c }问 sizeof(stuct test) =？当然我知道这道题想考察的 int/long 定义以及对齐问题，之前编码也不会太在意这类似问题，直接打印出来就行，但是作为考题这里有诸多不严谨地方。我们好好探索下。不管三七二十一，直接编译打印。$ gcc test.c $ ./a.out 16显然地，这里 int 为4个字节，char 被4字节对齐。看几个 GCC 选项-m64https://gcc.gnu.org/onlinedocs/gcc-9.5.0/gcc/x86-Options.html#x86-Options-m32 -m64 -mx32 -m16 -miamcu Generate code for a 16-bit, 32-bit or 64-bit environment. The -m32 option sets int, long, and pointer types to 32 bits, and generates code that runs on any i386 system. The -m64 option sets int to 32 bits and long and pointer types to 64 bits, and generates code for the x86-64 architecture. For Darwin only the -m64 option also turns off the -fno-pic and -mdynamic-no-pic options. The -mx32 option sets int, long, and pointer types to 32 bits, and generates code for the x86-64 architecture. The -m16 option is the same as -m32, except for that it outputs the .code16gcc assembly directive at the beginning of the assembly output so that the binary can run in 16-bit mode. The -miamcu option generates code which conforms to Intel MCU psABI. It requires the -m32 option to be turned on.看起来这里默认最大设置都是4字节对齐。$ gcc test.c -m16 #编译失败 $ gcc test.c -m32 $ ./a.out 12 $ gcc test.c -m64 $ ./a.out 16-fpack-structhttps://gcc.gnu.org/onlinedocs/gcc-9.5.0/gcc/Code-Gen-Options.html#Code-Gen-Options-fpack-struct[=n] Without a value specified, pack all structure members together without holes. When a value is specified (which must be a small power of two), pack structure members according to this value, representing the maximum alignment (that is, objects with default alignment requirements larger than this are output potentially unaligned at the next fitting location.显然地，这里的编译选项能够设置 long 类型和对齐。先看默认值。$ gcc test.c -fpack-struct=1 $ ./a.out 13 $ gcc test.c -fpack-struct=2 $ ./a.out 14 $ gcc test.c -fpack-struct=4 $ ./a.out 16 $ gcc test.c -fpack-struct=8 $ ./a.out 16\_attrribute_这里能够设置 -m32 将 long 和指针类型设置为 4字节，但是对齐方式还是 4字节。https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.htmlstruct test_a { char a; //1 int b; //4 long c; //8 } __attribute__ ((packed)) ; //13 //__attribute__ ((aligned(1))) ; //16 //__attribute__ ((aligned(2))) ; //16 //__attribute__ ((aligned(4))) ; //16 //__attribute__ ((aligned(8))) ; //16 //__attribute__ ((aligned(16))) ; //16packed 为字节对齐，aligned 不管设置多少都是4自己对齐。结论如下结论。选项描述-m32 -m64 -mx32 -m16 -miamcu设置 long 和指针数据类型长度，-m32为4个字节，-m64 为8个字节。-fpack-struct设置对齐，支持 1、2、4字节对齐。\_attrribute_设置对齐，支持 1、4字节对齐。
目录 [隐藏] 5.9.1 路由数据库5.9.1.1 路由器 ID 集5.9.1.2 链路集5.9.2 路由集5.9.3 Leader 数据库5.9.3.1 ID 分配集5.9.3.2 Leader 数据库丢失5.9.4 链路裕量和链路指标5.9.5 路由成本和下一跳5.9.6 循环和循环检测5.9.7 发送 Advertisement5.9.8 处理 Route64 TLVs5.9.9 路由器 ID 管理5.9.10 路由器 ID 分配5.9.10.1 ADDR_SOL.req – 地址请求5.9.10.2 ADDR_SOL.rsp – 地址请求响应5.9.10.3 ADDR_REL.ntf – 地址释放通知5.9.11 在链路超时之前发送链路请求5.10.1 Thread 网络内的单播数据包转发5.10.1.1 完整 Thread 设备转发5.10.1.2 简化 Thread 设备转发5.10.1.3 使用 Mesh Address Headers 转发数据包5.10.2 在 Thread 网络之外的单播数据包转发5.10.3 Thread 网络内的任播数据包转发5.10.3.1 Leader ALOC5.10.3.2 DHCPv6 Agent ALOC5.10.3.3 Service ALOC5.10.3.4 Commissioner ALOC5.10.3.5 Neighbor Discovery Agent ALOC5.10.3.6 Primary Backbone Router ALOC5.10.4 链路层目的地地址的选择 5.9 路由协议本文为 Thread 路由协议相关规范，翻译自Thread Specification->5.9 Routing Protocol，作为理论储备。这里未做任何解释，接下来会结合其他系列文档从抓包、代码实现结合协议规范去理解该路由协议。本节描述了 Thread 网络中使用的路由协议。Thread 路由协议是一个简单的距离向量路由协议。协议的主要目标是最大化地可以在单个消息中传输的路由信息量。因此，该协议限制了包含最多 MAX_ROUTERS 或更少路由器的网络。注意：可以使用超过 MAX_ROUTERS 的路由地址，以便在重新使用路由器地址之前等待超时。路由器定期向 Thread 网络分区中的所有其他路由器广播其路由成本以及与邻居之间的单跳链路质量。到达特定目的地的路由成本是所有邻居对该目的地广播的成本中的最小值加上到该邻居的成本。 Advertisement 发送的速率由 Trickle 算法的一个实例确定RFC 6206。提示：Trickle 算法是一种用于网络协议中的算法，旨在有效地维护和更新网络中的信息。它通常用于自组织网络（例如传感器网络）中，以确保网络中的路由信息、邻居信息或其他状态信息得到及时更新，同时尽量减少网络中的控制消息的数量。Trickle 算法的核心思想是在保持低开销的同时，仍然能够快速地传播重要的变更。该算法通过引入慢速的消息传播机制和快速的消息传播机制来实现这一目标。具体来说，Trickle 算法的工作方式如下：慢速传播： 定期（例如每分钟）发送一条慢速消息，其中包含节点当前的信息。这有助于保持网络中的信息更新。快速传播： 在检测到信息变更时，立即发送一条快速消息。这有助于迅速传播关键信息。定时器机制： 每个节点都维护一个定时器，定期触发慢速消息的发送。当接收到慢速或快速消息时，可以重置定时器。Trickle 算法通过这种方式实现了一种自适应机制，可以根据网络的实际变化来动态调整消息的传播频率。这有助于在网络拓扑结构变化较小的情况下，降低消息传播的开销，提高网络的效率。 Trickle 算法在许多自组织网络协议中都得到了应用，例如RPL（适用于低功耗和有限能量的网络中的IPv6路由协议）。5.9.1 路由数据库路由器维护一个路由数据库，记录使用 Thread 距离向量路由的每个单独接口的信息。这些数据库包含以下各小节中描述的数据集。5.9.1.1 路由器 ID 集接口的路由器 ID 集包含两个值：ID_set 是当前有效路由器 ID 的集合。ID_sequence_number 是分配给当前 ID_set 的序列号。5.9.1.2 链路集接口的链路集记录了与该接口上的其他路由器是或曾是邻居的信息。路由器为与每个邻居的链接维护一个元组，如下所示：(L_router_id, L_link_margin, L_incoming_quality, L_outgoing_quality, L_age)其中：L_router_id 是分配给该邻居的路由器 ID。L_link_margin 是从该邻居接收到的消息的测得链路余量。L_incoming_quality 是根据 L_link_margin 计算得到的传入链路质量度量。L_outgoing_quality 是由该邻居报告的从本路由器发送消息的传出链路质量度量。L_age 是自上次从该邻居接收到 MLE Advertisement 以来经过的时间。5.9.2 路由集接口的路由集记录了关于其他路由器的路由成本和下一跳信息，这些路由器是从该路由器出发可达（具有有限路由成本）的或最近可达的。路由器为每个可达路由器维护一个元组，如下所示：(R_destination, R_next_hop, R_route_cost)其中：R_destination 是该路由的目的地分配的路由器 ID。R_next_hop 是沿路由的下一跳分配的路由器 ID。R_route_cost 是由 R_next_hop 报告给 R_destination 的路由成本。5.9.3 Leader 数据库作为 Leader 的路由器维护一个额外的数据库，用于跟踪路由器 ID 的分配情况。与路由数据库类似，Leader 为设备作为 Leader 的每个接口维护一个单独的 Leader 数据库。5.9.3.1 ID 分配集接口的分配集记录了每个路由器 ID 的状态。Leader 为每个路由器 ID 维护一个元组，如下所示：(ID_id, ID_owner, ID_reuse_time)其中：ID_id 是一个路由器 ID。ID_owner 是当前分配给该路由器 ID 的 IEEE 802.15.4 扩展地址，或者如果未分配，则是最近分配给它的路由器的 IEEE 802.15.4 扩展地址。ID_reuse_time 是可以重新分配此路由器 ID 的时间，如果它当前未分配。Leader 记录一个路由器 ID 是否当前分配在路由器 ID 集中。5.9.3.2 Leader 数据库丢失在重置事件中， Leader 可能会丢失其 Leader 数据库、路由集和路由数据库的信息。如果 Leader 在重置前确定其 Thread 分区仍然处于活动状态，就必须按以下描述重新构建 Leader 数据库、路由集和路由数据库的丢失状态，并继续作为该 Thread 分区的 Leader 运行。详细信息请参阅第4章第4.7.1.3节《重置后的路由器同步》以确定这一点。路由数据库（第5.9.1节，路由数据库）和路由集（第5.9.2节，路由集）通过整合收到的相邻路由器的 MLE Link Accept、MLE Link Accept 和 Request 以及 MLE Advertisement 消息的信息进行重建。具体而言，ID_sequence_number 是通过选择具有最新 ID Sequence 值的入站 MLE Route64 TLV 并使用其中的 ID Sequence 值来重建的。然后，Router ID Set 是根据在所选的 MLE Route64 TLV 中指示的活动 Router IDs 进行重建。详细的 MLE Route64 TLV 处理细节请参阅第5.9.8节《处理 Route64 TLVs》。在 Leader 数据库 ID 分配集中，对于收到的 Route64 TLVs 中指示的每个活动 Router ID，创建一个元组，并将 ID_id 设置为 Router ID。对于无法确定 MAC 扩展地址的 Router IDs，元组的 ID_owner 字段设置为“invalid”（或“unknown”）。对于所有相邻路由器，可以通过接收的 MLE 消息的 link-local IPv6 源地址确定 MAC 扩展地址。ID_owner 字段将设置为相应的 MAC 扩展地址。ID_reuse_time 字段不设置，因为当前未激活的 Router IDs 不会创建元组。5.9.4 链路裕量和链路指标L_link_margin 值是来自相邻路由器的接收相对信号强度的运行平均值。相对信号强度记录为高于本地噪声底线的 dB，称为链路裕量。平均链路裕量根据表5-5转换为单向链路质量。表5-5 平均链路裕量转换为单向链路质量链路裕量(Link Margin)链路质量(Link Quality)链路成本(Link Cost)>20 dB31>10 dB22>2 dB14≤ 2 dB0无穷大单向链路质量通过 MLE Advertisements 中的 Route64 TLVs 与相邻设备共享。链路成本由链路的两个单向链路质量的最小值确定。例如，一个链路在一个方向上的链路质量为2，在另一个方向上的链路质量为1，因此最小链路质量为1，因此链路成本为4。只有四种可能的链路质量值可以最小化向相邻设备通信链路质量的开销。链路裕量可能直接由测量传入信号强度和噪声底线确定。或者，它可以从已确定与链路裕量存在相关性的一个或多个其他链路质量测量中派生。测量和相关性取决于硬件平台。无论链路裕量如何确定，都必须使用平均化来平滑短期波动。Thread 设备必须针对每个相邻设备维护链路裕量的指数加权移动平均值，每当从相邻设备接收到新的 MAC 帧时更新（采样），新样本值的加权为 1/8 或 1/16。这种权重选择是特定于实现的。例如，如果相邻设备的平均链路裕量为 A，并且从该相邻设备收到的链路裕量为 X，则新的平均值为 (A 7/8) + (X 1/8)。确定链路质量时还必须应用迟滞，以便接近两种质量之间边界的链路裕量不会频繁更改 L_incoming_quality。迟滞值为 2 dB，用于链路质量 1-2 和链路质量 2-3 之间的阈值。迟滞值为 1 dB，用于链路质量 0-1 之间的阈值。例如，平均链路裕量必须至少增加到 12 dB，才能将现有质量 1 的链路更改为质量 2 的链路，并且至少增加到 3 dB，才能将现有质量 0 的链路更改为质量 1 的链路。如果路由器连续接收到特定路由器邻居的 802.15.4 状态 NO_ACK 的 FAILED_ROUTER_TRANSMISSIONS 次单播 MCPS.DATA-Requests，则必须将与该路由器邻居的链路质量设置为“0”。如第5.9.7节《发送 Advertisement 》所述，路由器随后将重置其 Trickle 计时器。请注意，由于失败的 clear channel assessments 导致的传输故障不算作未收到确认。5.9.5 路由成本和下一跳对于路由器 ID（R），路由成本是直接发送到 R 的成本和最小多跳路由成本中的最小值。更具体地说，如果在 Link Set 中存在 L_router_id = R 的条目，则 Cost_direct 是与 L_incoming_quality 和 L_outgoing_quality 的最小值关联的链路成本。如果没有这样的条目，则 Cost_direct 是无穷大。如果在 Route Set 中存在 R_destination = R 的条目，并且 R_route_cost 加上到 R_next_hop 的链路成本小于或等于 MAX_ROUTE_COST，则 Cost_multihop 就是这个总和。如果没有这样的条目，或者总和大于 MAX_ROUTE_COST，则 Cost_multihop 就是无穷大。到达 R 的路由成本是 Cost_direct 和 Cost_multihop 的最小值。如果 Cost_direct 和 Cost_multihop 都是无穷大，则 R 是不可达的。如果 Cost_direct 是有限的且小于或等于 Cost_multihop，则路由的下一跳是 R 本身。否则，下一跳是 R_next_hop。5.9.6 循环和循环检测到某些目的地的路由图中存在循环会导致到达该目的地的成本高于 MAX_ROUTE_COST。没有“分割视野”循环避免。如果路由器从邻居处收到一个数据包，而该邻居是数据包目的地的下一跳，则必须从 Route Set 中删除目的地的条目。这样可以检测并移除两跳循环。5.9.7 发送 Advertisement路由器按照本节描述发送 MLE Advertisement 。REED 按照第5.16.6节《子节点》的描述定期发送 MLE Advertisement 。所有 MLE Advertisement 必须发送到链路本地所有节点组播地址（FF02::1）。发送 MLE Advertisement 的速率由使用 Trickle 算法 RFC 6206 确定，使用 ADVERTISEMENT_I_MIN 和 ADVERTISEMENT_I_MAX 作为参数 I_MIN 和 I_MAX。Trickle 参数 k 设置为无穷大。 Advertisement 没有一致性检查。当报告的到一个或多个目的地的路由成本（在 Route64 TLV 中）变为或从无穷大（表示为值“0”）时，Trickle 计时器将被重置。如果删除具有非零成本的路由器 ID，则还应将其重置。在发送 Advertisement 时，路由器在 Route64 TLV 中包括 L_incoming_quality、L_outgoing_quality 和其 ID_set 中每个路由器 ID 的路由成本。如果到一个路由器 ID 的路由成本小于 MAX_ROUTE_COST，则 Route 值是该成本。否则，Route 值为无穷大（表示为值“0”）。5.9.8 处理 Route64 TLVs处理传入的 Route64 TLV 的第一步是将其路由器 ID 序列号与 ID_sequence_number 进行比较。如果 Route64 TLV 的序列号较高，则必须用 Route64 TLV 的序列号和 ID 集合替换 ID_sequence_number 和 ID_set 的值。若 Route64 TLV 的序列号比 ID_sequence_number 相同或更低，则 ID_sequence_number 和 ID_set 必须保持不变。如果接收设备具有路由器 ID，并且 MLE Advertisement 包括该 ID 的入链路质量，则发件者的 L_outgoing_quality 必须设置为该入链路质量。最后，对于除发件者 ID 或接收者 ID 外出现在 ID_set 和 Route64 TLV 的已分配路由器 ID 掩码中的每个路由器 ID（R），设 Cost_reported 为 Route64 TLV 中 R 的路由成本。随后：如果 Cost_reported = 0 且 R_next_hop == TLV 发件者 ID，则从路由器集合中删除 R。否则，如果 Cost_reported > 0 并且以下条件满足之一：路由器集合中没有路由器 ID R 的条目R_next_hop == TLV 发件者 ID发件者链接成本 + Cost_reported < R_next_hop 链接成本 + R_route_cost 则通过将 R_next_hop 设置为发件者 ID 并将 R_route_cost 设置为 Cost_reported，添加或更新 R 条目。REED 必须处理 Route64 TLVs，以获取 Thread 网络上的路由器数量，用于确定是否成为活跃路由器（参见第 5.9.9 节，路由器 ID 管理）。它还必须从父节点的 Route64 TLV 中获取到 Leader 的成本。如果此成本为无穷大，则子设备设置一个计时器，时间为 PARENT_ROUTE_TO_LEADER_TIMEOUT 秒。如果在计时器到期时未从父节点收到新的 Route64 TLV，则它尝试寻找并连接到具有有限 Leader 成本的新父节点。5.9.9 路由器 ID 管理Leader 负责管理路由器 ID。与所有路由器一样，Leader 维护一个显示已分配路由器 ID 的 ID_set。与其他路由器不同，Leader 还维护一个 ID_Assignment_Set，其中包含具有已分配 ID 的每个设备的 IEEE 802.15.4 扩展地址，对于当前未分配的 ID，则包含可以重新分配 ID 的时间。每当 Leader 分配或取消分配路由器 ID 时，它会更改其 ID_set，递增其 ID_sequence_number，并更新 ID_Assignment_Set 中路由器 ID 的条目。为了回收未使用的 ID，当 ID_sequence_number 在 ID_SEQUENCE_PERIOD 秒内保持不变时，Leader 还会递增 ID_sequence_number。如果一个路由器在 NETWORK_ID_TIMEOUT 秒内未从邻居处收到新的 ID_sequence_number，则它必须视为与 Leader 断开连接，并停止使用其当前的路由器 ID。Leader 可以随时取消分配路由器 ID。如果路由器 ID 的路由成本已达到 INFINITE_COST_TIMEOUT，则 Leader 应该取消分配路由器 ID。当 Leader 取消分配路由器 ID 时，它会从其 ID_set 中删除该 ID，递增 ID_sequence_number，并将路由器 ID 的 Reuse_Time 设置为当前时间加上 ID_REUSE_DELAY。Leader 不应在至少 ID_REUSE_DELAY 秒内重新分配路由器 ID。如果一个路由器根据新的网络数据确定其路由器 ID 已经取消分配，则它必须假定已与 Leader 断开连接，并按第 5.16.1 节“失去连接”的描述进行处理。如果 Thread 网络中分区的活跃路由器数量少于 ROUTER_UPGRADE_THRESHOLD（邻近节点 Route64 TLV 报告的），REED 必须等待从检测到条件开始到 ROUTER_SELECTION_JITTER 秒之间的随机时间，然后如果条件仍然满足，则必须尝试通过向 Leader 请求路由器 ID 来成为活跃路由器。这有助于防止许多子设备同时请求路由器 ID。如果 Thread 网络分区中的活跃路由器数量至少为 ROUTER_UPGRADE_THRESHOLD，低于 MAX_ROUTERS，并且最多有一个具有外部连接性的边界路由器在路由器角色中发挥作用，那么具有外部连接性的边界路由器应该尝试成为活跃路由器，方法是使用上述包括随机性的过程。这种方法通过边界路由器更好地连接到边网主机。如果网络中活跃路由器的数量超过 ROUTER_DOWNGRADE_THRESHOLD，则符合以下所有条件的活跃路由器必须尝试释放其路由器 ID 并成为 REED。让 M 为满足以下条件的路由器集合的邻居路由器：在 M 集合中至少有 MIN_DOWNGRADE_NEIGHBORS 个邻居。至少有一个邻居与 M 集合中所有路由器具有至少与 D 本身的质量相同的更好质量的链接。拥有的子设备数量少于三倍的多余路由器数量。换句话说，如果有 N 个活跃路由器且路由器有 C 个子设备，则 C < 3 *（N - ROUTER_DOWNGRADE_THRESHOLD）。不是 Leader。不是特定 Provisioning Domain（参见第 5.13.7 节“配置域”）的唯一边界路由器。如果多个边界路由器符合降级条件，则它们可以使用某种外部机制来选择是否进行降级。如果成为子设备的决定是由路由器数量增加触发的，设备必须首先在检测到增加后延迟随机时间，时间为 ROUTER_SELECTION_JITTER 秒，然后重新检查总路由器数量和上述标准。这有助于防止许多路由器同时成为子设备。5.9.10 路由器 ID 分配想要成为路由器的 REED 必须发送地址请求消息以请求 Leader 分配一个路由器 ID。Leader 的路由器 ID 是从共享的 Thread 网络数据中获取的。5.9.10.1 ADDR_SOL.req – 地址请求地址请求消息定义如下：CoAP 请求 URIcoap://[]:MM/a/as交易模式Req+Rsp_Piggybacked 或 Req+Rsp_Separate（请求部分）CoAP 载荷MAC 扩展地址 TLV[RLOC16 TLV]状态 TLVMAC 扩展地址 TLV 包括以便 Leader 可以跟踪 Thread 设备和 RLOC16 之间的映射关系。状态 TLV 包含表 5-10 的状态值之一，并指示发送请求的原因。如果原因是活动路由器数量少于 ROUTER_UPGRADE_THRESHOLD，则包含 TOO_FEW_ROUTERS。如果原因是发送者收到 MLE Child ID 请求消息（参见第 4.7.1 节“附着到父节点”），则包含 HAVE_CHILD_ID_REQUEST。如果一个拥有子设备的路由器移动到不同分区（参见第 5.16.4 节“重置 Thread 网络分区数据”），则包含 PARENT_PARTITION_CHANGE。如果发送者是希望通过成为路由器来提供与边网主机更好连接性的边界路由器，则包含 BORDER_ROUTER_REQUEST。如果先前具有状态 BORDER_ROUTER_REQUEST 的请求不成功，发送者可以使用不同的状态值 HAVE_CHILD_ID_REQUEST 重复请求（在应用抖动后）。但前提是在发送请求时，边界路由器确定当前具有外部连接性的边界路由器最多只有一个是活动路由器。RLOC16 TLV 仅在发送者先前是路由器，可能在另一个分区内，并希望保留其原始路由器 ID 时包含。Leader 不得为多个由 MAC 扩展地址标识的设备分配 RLOC16。如果状态是 HAVE_CHILD_ID_REQUEST 或 PARENT_PARTITION_CHANGE，则 Leader 为 REED 分配路由器 ID（如果有可用的）。如果状态是 BORDER_ROUTER_REQUEST，则 Leader 分配路由器 ID（如果有可用的），并且以下至少满足一个条件：当前具有外部连接性的边界路由器中最多有一个是活动路由器（如 Thread 网络数据所示）。路由器数量低于 ROUTER_UPGRADE_THRESHOLD。 注意：外部连接性的确定在第 5.9.9 节“路由器 ID 管理”中定义。如果状态是 TOO_FEW_ROUTERS，则 Leader 为 REED 分配路由器 ID（如果活动路由器数量少于 ROUTER_UPGRADE_THRESHOLD）。如果状态是未分配的编号或在请求中没有意义的状态编号，则 Leader 发送包含状态 UNRECOGNIZED_STATUS 的地址请求响应。如果 REED 请求了特定的路由器 ID 并且该路由器 ID 可用，则选择该路由器 ID。如果无法为分配使用或者由于请求原因无法分配路由器 ID，则 Leader 发送包含状态 'No Address Available' 的地址请求响应。如果分配了路由器 ID，则 Leader 发送包含状态 'Success' 的响应。5.9.10.2 ADDR_SOL.rsp – 地址请求响应地址请求响应消息格式如下：CoAP 响应代码2.04 Changed交易模式Req+Rsp_Piggybacked 或 Req+Rsp_Separate（响应部分）CoAP 载荷状态 TLV[RLOC16 TLV][路由器掩码 TLV]状态 TLV 包含表 5-10 中标记为有效的状态值，标记为 ADDR_SOL.rsp。RLOC16 TLV 包含分配的 RLOC16。路由器掩码 TLV 包含反映新 ID 分配的更新当前 ID 序列和路由器 ID 掩码。仅当状态为 'Success' 时才包含 RLOC16 TLV 和路由器掩码 TLV。如果一个路由器希望返回成为子设备，它必须首先作为子设备重新连接到网络，然后必须向 Leader 发送一个包含不再使用的路由器 ID 的地址释放通知消息。在释放之前重新连接避免了与 Leader 的争用，因为 Leader 需要同时标记路由器 ID 为未分配并将其用于发送响应。5.9.10.3 ADDR_REL.ntf – 地址释放通知地址释放通知消息是可靠通知，格式如下：CoAP 请求URI coap://[]:MM/a/ar交易模式Ntf/Qry/Ans_CONCoAP载荷MAC 扩展地址 TLVRLOC16 TLV如果一个路由器希望返回成为子设备，它必须首先作为子设备重新连接到网络，然后必须向 Leader 发送一个 ADDR_REL.ntf，其中包含它不再使用的 RLOC16 路由器地址。5.9.11 在链路超时之前发送链路请求路由器必须监视 Link Set 中每个路由器链路的 L_age 值。如果对于给定邻居，此值增加到超过 3 * ADVERTISEMENT_I_MAX，则至少有三个连续的 MLE Advertisement 消息被丢失。在这种情况下，路由器必须向邻居路由器的链路本地地址发送 MLE 链路请求单播消息。该 MLE 链路请求消息必须延迟最多 MLE_MAX_REACTION_TIME，以毫秒为分辨率。该 MLE 链路请求消息必须包含一个 TLV 请求 TLV，用于 Route64 TLV。如果从邻居路由器收到包含 Route64 TLV 的相应 Link Accept 消息，则 Link Set 中该链路的 L_age 值应设置为 '0'。5.10 单播数据包转发Thread 规范支持以下类型的单播和组播数据包转发：同属于 Thread 网络的源设备和目的设备之间的单播数据包转发。源设备或目的设备位于 Thread 网络之外的单播数据包转发。当目的设备位于 Thread 网络之外且被用任播 IPv6 地址寻址时，也包括任播数据包转发。同属于 Thread 网络的源设备和目的设备之间使用任播定位器（ALOC）目的地址的任播数据包转发。5.10.1 Thread 网络内的单播数据包转发在同一时间内，最多会有 MAX_ROUTERS 个路由器共享其路由信息，正如 5.9 节中描述的路由协议。在 Thread 网络内，只有路由器可以执行多跳数据包转发。5.10.1.1 完整 Thread 设备转发在 Thread 网络内部，只使用 RLOC 进行路由。Thread 内部的多跳数据包转发使用 6LoWPAN Mesh Header 和 RLOC16 在链路层进行。FTD 需要解析 EID 到 RLOC 地址映射并维护 EID 到 RLOC 映射缓存。FTD 还直接响应 EID 到 RLOC 映射查询。FTD 执行 IPv6 数据包转发的步骤如下：获取 IPv6 目的地的 RLOC。如果 IPv6 目的地址是 RLOC，则设备使用 IPv6 目的地址直接进行路由。如果 IPv6 目的地址是 ALOC，则设备使用第 5.10.3 节“Thread 网络内部的任播数据包转发”中定义的该类型 ALOC 的查找方法来确定适当的目的地的 RLOC，然后转发消息。如果 IPv6 目的地址在网内但不是 RLOC，则设备应检查其邻居表和 EID 到 RLOC 映射缓存，看看是否已知 RLOC。如果 RLOC 尚未知晓，则设备发起 EID 到 RLOC 查询，使用地址查询以获取 IPv6 目的地地址的 RLOC。如果 IPv6 目的地址在网外，则使用第 5.10.2 节“Thread 网络外的单播数据包转发”的规则确定适当的出口边界路由器的 RLOC。确定 RLOC 是否属于相邻设备。如果 RLOC 属于相邻设备，则 6LoWPAN 层使用 RLOC16 编码 6LoWPAN 帧，而无需使用 6LoWPAN Mesh Header，并设置 IEEE 802.15.4 目的地址为 RLOC16。如果 RLOC 不属于相邻设备，则 6LoWPAN 层使用 6LoWPAN Mesh Header 编码 6LoWPAN 帧，将 V 和 F 位设置为“1”，将 Originator Address 字段使用本地 RLOC16，将 Final Address 字段使用目的地的 RLOC16。6LoWPAN Mesh Header 中的 hopsLft 字段必须设置为大于到达目的地的路由成本。如果设备不是活跃路由器，则 hopsLft 字段必须增加一；如果目标 RLOC16 不是活跃路由器的 RLOC16，则 hopsLft 字段也必须增加一。请注意，将 hopsLft 字段设置为 14 或更小的值会减小 6LoWPAN Mesh Header 的大小。接着，设备使用路由集确定下一跳的 RLOC，并设置 IEEE 802.15.4 目的地址为下一跳的 RLOC16。接收到数据包的下一跳设备会查找路由集/邻居表中的下一跳，减少 6LoWPAN Mesh Header 中的跳数计数，然后将数据包发送给下一跳，作为 IEEE 802.15.4 目的地址。5.10.1.2 简化 Thread 设备转发MTD 作为仅操作主机设备，将所有 IPv6 数据包转发到其父级，而无需使用 6LoWPAN Mesh Header。因此，父级执行 IPv6 转发（而不是 6LoWPAN 网格转发）来处理来自 MTD 子级的数据包。 具体来说，MTD 不解析 EID 到 RLOC，并依赖其 FTD 父级来执行查询并在 6LoWPAN Mesh Header 中包括 RLOC16。对于具有网外目的地的数据包，MTD 不选择边界路由器，并依赖其 FTD 父级选择适当的边界路由器。MTD 也不选择特定的 DHCPv6 代理，而是使用 DHCPv6 代理 ALOC，并依赖其 FTD 父级将数据包传递给适当的 DHCPv6 代理。MTD 不选择特定的服务器，而是使用适当服务类型 ID 的服务 ALOC，并依赖其 FTD 父级将数据包传递给适当的服务器。MTD 也可以以类似的方式使用其他 ALOC 类型。5.10.1.3 使用 Mesh Address Headers 转发数据包RFC 4944 的第 11 节描述了如何使用“链路层路由表”中的信息转发带有 Mesh Addressing headers 的数据包。在 Thread 网络中，该表将 Mesh Addressing 头中的目标 RLOC16 映射到到达目的地的下一跳的 RLOC16。在活跃路由器上，此表格如下填充：如果有到路由器 R 的路由，则链路层路由表具有 R 的 RLOC16 条目和对应于 R 的潜在子级的 RLOC16。这些条目的下一跳是到达 R 的路由的下一跳。对于 Mode 位 D=1 的每个此路由器的子级，有一个将子级的 RLOC16 映射到子级本身的条目。此类条目的下一跳是子级。对于 Mode 位 D=0 的每个此路由器的子级，有一个将子级的 RLOC16 映射到路由器本身的条目。此类条目没有下一跳。路由器移除 Mesh Header，并根据第 5.10.1.1 节“完整 Thread 设备转发”的规定将数据包传递给子级。 请注意：以上规则意味着父级将指示其 Mode 位 D=0 子级的 mesh 目的地视为自己的 RLOC16。 一旦带有 6LoWPAN Mesh Header 的数据包到达 Mesh Header 指示的路由器，该路由器会移除 Mesh Header 并根据第 5.10.1.1 节“完整 Thread 设备转发”的规定将数据包传递给自身或子级。 对于 REEDs 和 FEDs，所有 RLOC16 的下一跳是父级。MTD 不接收或处理 mesh header，并且没有链路层路由表。5.10.2 在 Thread 网络之外的单播数据包转发Thread 边界路由器必须在 Thread 接口和其他接口之间实现 IP 层的数据包转发，以确保与外部网络或互联网的通信。Thread 边界路由器向 Leader 提供关于它们提供的有效前缀集合的默认路由以及对于给定有效前缀提供的任何其他特定路由的信息。Leader 聚合边界路由器信息，并在整个 Thread 网络分区中传播此信息。Thread 网络数据使用 RLOC16 识别边界路由器。从 Thread 网络发出的消息在链路层使用 6LoWPAN Mesh Header 从发起节点根据以下规则转发到适当的边界路由器，然后由边界路由器将数据包转发到其他接口。有关此转发的详细信息请参阅第 9.3.2 节，在第 9 章边界路由器中内部和外部接口之间的数据包转发。如果 IPv6 目标地址不在网内，则路由器使用 IPv6 源地址确定提供与 IPv6 源地址的前缀相同的 Provisioning Domain 的边界路由器集合。在该集合中，路由器按以下方式找到最佳的出口边界路由器：查找与 IPv6 目标地址具有最长前缀匹配的 External Route Set 前缀。如果有多个 External Route 前缀匹配，选择具有更高优先级（R_preference）的前缀。如果没有 External Route 前缀匹配，则选择提供默认路由的边界路由器。如果有多个边界路由器提供默认路由，则选择具有更高优先级（P_preference）的边界路由器。如果有两个或更多优先级相同的边界路由器，则选择具有最低网格路径成本的边界路由器。最后，如果没有边界路由器提供默认路由，则路由此数据包失败。路由器应按照第 5.8 节 ICMPv6 中指定的方式向数据包的 IPv6 源发送 ICMPv6 Destination Unreachable 消息，其中 ICMPv6 Code 为 0。Thread 网络的传入消息基于 GUA 或补充 ULA 地址，边界路由器使用其内部地址表或地址查询来识别最终目的地的 RLOC。提供用于 SLAAC 或 DHCPv6 地址配置的前缀的边界路由器可以为网内或网外使用该前缀发送地址查询（有关更多信息，请参阅第 9 章边界路由器中的第 9.2.3 节全局地址和第 9.2.4 节补充唯一本地地址）。完成此步骤后，边界路由器根据第 5.10.1 节中描述的算法将数据包转发到网格目的地。5.10.3 Thread 网络内的任播数据包转发使用 ALOC 目标的 IPv6 数据包的路由由 FTDs 完成，如第 5.10.1.1 节“完整 Thread 设备转发”所定义，使用本节为每种 ALOC 类型定义的特定查询方法。在以下子部分中，“传递到此 ALOC 失败” 表示路由器必须返回数据包的 IPv6 源，使用 ICMPv6 Destination Unreachable 消息，并将 ICMPv6 Code 设置为“0”（无法到达目的地）。ICMPv6 消息负载必须至少包含原始消息的完整 IPv6 头。5.10.3.1 Leader ALOCLeader ALOC 解析为 Leader Data 中 Leader Router ID 字段中的 RLOC。5.10.3.2 DHCPv6 Agent ALOC如果 IPv6 目标地址是 DHCPv6 代理 ALOC，则路由器使用 ALOC 中编码的上下文 ID 来确定相关前缀。对于此前缀，它考虑提供该服务的 DHCPv6 代理集合（即，提供该前缀且 P_dhcp = true 的边界路由器）。在该集合中，路由器按以下方式找到“最近”的 DHCPv6 代理：如果集合中有多个 DHCPv6 代理，则选择具有最低网格路径成本的代理。如果集合中没有 DHCPv6 代理，则传递到此 ALOC 失败。5.10.3.3 Service ALOC如果 IPv6 目标地址是 Service ALOC，则路由器使用服务类型 ID 查找 Thread 网络数据中提供该服务类型的服务器集合。然后选择具有最低网格路径成本的服务器作为目的地 RLOC。如果没有具有该服务类型 ID 的服务器，则传递到此 ALOC 失败。5.10.3.4 Commissioner ALOC如果 IPv6 目标地址是 Commissioner ALOC，则路由器将此 ALOC 中编码的值 N 与当前 Commissioner 会话 ID 进行比较（从 Commissioner Dataset 获取）。如果相等，则将 ALOC 解析为 Commissioner Dataset 中的 Border Agent Locator RLOC16 字段。在其他情况下，传递到此 ALOC 失败。5.10.3.5 Neighbor Discovery Agent ALOC如果 IPv6 目标地址是 Neighbor Discovery Agent ALOC，则路由器使用 ALOC 中编码的上下文 ID 来确定相关前缀。对于此前缀，它考虑提供该服务的 Neighbor Discovery Agent 集合（即，提供该前缀且 P_nd = true 的边界路由器）。在该集合中，路由器按以下方式找到“最近”的 ND 代理：如果集合中有多个 ND 代理，则选择具有最低网格路径成本的代理。如果集合中没有 ND 代理，则传递到此 ALOC 失败。5.10.3.6 Primary Backbone Router ALOC如果 IPv6 目标地址是 Primary Backbone Router ALOC（PBBR ALOC），则路由器首先选择使用以下算法的 Primary BBR 的 Server TLV。将 S 初始化为 BBR Dataset 中所有 Server TLV 的集合。然后：如果 S 为空，则选择无，并完成此算法。如果 S 的大小为一个，则选择该集合成员并完成此算法。选择具有最高优先级的 S 中成员，规则如下：规则 1：Leader 的 Server TLV 优先级高于非 Leader 的 Server TLV。规则 2：前述规则相同时，具有更高 BBR Sequence Number 的 Server TLV 优先级高。规则 3：前述规则相同时，具有更高 RLOC16 值的 Server TLV 优先级高。 最后，如果选择了 Server TLV，则使用该 Server TLV 中的 RLOC16 作为目的地。否则，传递到此 ALOC 失败。5.10.4 链路层目的地地址的选择发送到 rx-off-when-idle 子设备的单播数据包的链路层目的地可以是该子设备的 RLOC16，而不管 IP 目标如何，并且消息必须使用适当的链路层单播传输发送。如果 IP 目标是链路本地单播地址，则链路层目的地是相应的 MAC 扩展地址，由 IP 目标的 64 位 IID 和通用/本地位取反而得。请注意，Thread 设备不根据其 16 位 MAC 地址配置链路本地地址。在所有其他情况下，数据包的下一跳是一个 RLOC。如果数据包有网格头，则 RLOC 是通向网格头目的地的下一跳。否则，它是由上述第 5.10.1.1 至 5.10.1.3 节中定义的单播/任播转发规则确定的。