核心目标:掌握 DNP3 独有的伪传输层(Transport Function)设计------它如何在"无连接"链路上实现大块数据的可靠分段传输,以及它与链路层、应用层的衔接关系。
前置知识:Part 2(链路层 FT3 帧,用户数据上限 250 字节);了解 IEC 104 的 I 帧 N(S)/N® 机制便于对比。
3.1 为什么需要伪传输层
3.1.1 问题:链路层装不下应用层数据
链路层单帧用户数据最多 250 字节,而应用层的一个数据块可能远大于此:
- 一次完整性轮询响应:数百个遥测点,可达数千字节
- 一次事件轮询响应:几十个带时标的事件
- 文件传输的数据块:按块大小协商,常为 2048 字节
如果没有分段机制,应用层的大报文根本无法通过链路层。
3.1.2 DNP3 的答案:协议栈中间插入"伪传输层"
┌─────────────────────────────────┐
│ 应用层(Part 4-5) │ ← 大报文(几百~几千字节)
├─────────────────────────────────┤
│ 伪传输层(本篇) │ ← 切成 ≤246 字节的段,加 4B 传输头
├─────────────────────────────────┤
│ 数据链路层(Part 2) │ ← 每段装入一个 ≤250B 的 FT3 帧
├─────────────────────────────────┤
│ 物理层(串口 / TCP / UDP) │
└─────────────────────────────────┘
3.1.3 为什么"伪"
称它"伪传输层",是因为它不是完整的传输协议(没有 TCP 那样的连接状态、拥塞控制),只做三件事:
- 分段:把应用报文切成链路层装得下的块
- 编号:给每个块编顺序号(SEQ)
- 重组:接收方按序号拼回完整报文
与 IEC 104 的本质差异 :IEC 104 假设底层是可靠的 TCP 字节流,用 APCI 头部的 N(S)/N® 序号做滑动窗口确认;DNP3 的伪传输层不依赖底层协议,串口、电台、TCP、UDP 都能跑------这是 80 年代设计留下的"传输无关"基因。
3.2 传输头(Transport Header)
3.2.1 传输头结构
每个链路帧的用户数据以 4 字节传输头 开头:
┌───────┬───────┬───────┬─────────┬──────────────────┐
│ bit7 │ bit6 │ bit5 │ bit4 │ bit3 ─ bit0 │
│ FIN │ FIR │ CON │ 保留=0 │ SEQ(模 16 序号) │
└───────┴───────┴───────┴─────────┴──────────────────┘
| 位 | 名称 | 含义 |
|---|---|---|
| bit7 | FIN | Final,=1 表示这是报文的最后一段 |
| bit6 | FIR | First,=1 表示这是报文的第一段 |
| bit5 | CON | Confirm,=1 表示请求接收方确认 |
| bit4 | --- | 保留,必须为 0 |
| bit3-0 | SEQ | 段序号 0-15,模 16 递增回绕 |
3.2.2 标志位的组合语义
单帧报文(应用数据 ≤246 字节):
FIR=1, FIN=1, SEQ=0 → 传输头 0xC0
多帧报文(应用数据 >246 字节):
第 1 段:FIR=1, FIN=0, SEQ=n → 0xC0 | n
中间段:FIR=0, FIN=0, SEQ=n+1, n+2... → 0x00 | n
最后 1 段:FIR=0, FIN=1, SEQ=m → 0x80 | m
注意:单帧报文 SEQ 规范上应置 0;多帧报文 SEQ 从 0 开始逐段递增(FIR 段 SEQ 通常为 0),模 16 回绕。接收方以 SEQ 连续性作为重组依据。
3.2.3 每帧应用数据上限推导
链路层用户数据上限 = 250 字节
减:传输头 = 4 字节
────────────────────────────
每帧应用层数据上限 = 246 字节
N 字节应用报文 → ceil(N / 246) 个链路帧
例:500 字节应用数据 → 246 + 246 + 8 = 3 帧
3.3 分段与重组状态机
3.3.1 发送端分段流程
应用层报文(500 字节示例)
│
▼ 按 246 字节切块
┌──────────┬──────────┬──────────┐
│ 段1 246B │ 段2 246B │ 段3 8B │
└──────────┴──────────┴──────────┘
│ │ │
▼ ▼ ▼
TH=0xC0 TH=0x10 TH=0x82
FIR=1 FIR=0 FIR=0
FIN=0 FIN=0 FIN=1
SEQ=0 SEQ=1 SEQ=2
3.3.2 接收端重组状态机
IDLE(空闲)
│ 收到 FIR=1, FIN=1(单帧)
▼
直接交付应用层
IDLE(空闲)
│ 收到 FIR=1, FIN=0(多帧开始)
▼
RECEIVING(重组中,记录 SEQ=0)
│ 收到 SEQ 连续递增的下一段
▼
RECEIVING
│ 收到 FIN=1 段(SEQ 连续)
▼
交付完整报文 → IDLE
异常路径:
RECEIVING 中收到 FIR=1(新报文开始) → 丢弃旧缓冲,重新开始
RECEIVING 中 SEQ 不连续 / 超时未收齐 → 丢弃缓冲 → IDLE
3.3.3 分段重组时序图
主站 Master 从站 Outstation 主站 Master 从站 Outstation #mermaid-svg-XhREKiWitwPGAPOV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XhREKiWitwPGAPOV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XhREKiWitwPGAPOV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XhREKiWitwPGAPOV .error-icon{fill:#552222;}#mermaid-svg-XhREKiWitwPGAPOV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XhREKiWitwPGAPOV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XhREKiWitwPGAPOV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XhREKiWitwPGAPOV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XhREKiWitwPGAPOV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XhREKiWitwPGAPOV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XhREKiWitwPGAPOV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XhREKiWitwPGAPOV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XhREKiWitwPGAPOV .marker.cross{stroke:#333333;}#mermaid-svg-XhREKiWitwPGAPOV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XhREKiWitwPGAPOV p{margin:0;}#mermaid-svg-XhREKiWitwPGAPOV .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-XhREKiWitwPGAPOV text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-XhREKiWitwPGAPOV .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-XhREKiWitwPGAPOV .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-XhREKiWitwPGAPOV .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-XhREKiWitwPGAPOV .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-XhREKiWitwPGAPOV #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-XhREKiWitwPGAPOV .sequenceNumber{fill:white;}#mermaid-svg-XhREKiWitwPGAPOV #sequencenumber{fill:#333;}#mermaid-svg-XhREKiWitwPGAPOV #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-XhREKiWitwPGAPOV .messageText{fill:#333;stroke:none;}#mermaid-svg-XhREKiWitwPGAPOV .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-XhREKiWitwPGAPOV .labelText,#mermaid-svg-XhREKiWitwPGAPOV .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-XhREKiWitwPGAPOV .loopText,#mermaid-svg-XhREKiWitwPGAPOV .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-XhREKiWitwPGAPOV .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-XhREKiWitwPGAPOV .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-XhREKiWitwPGAPOV .noteText,#mermaid-svg-XhREKiWitwPGAPOV .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-XhREKiWitwPGAPOV .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-XhREKiWitwPGAPOV .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-XhREKiWitwPGAPOV .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-XhREKiWitwPGAPOV .actorPopupMenu{position:absolute;}#mermaid-svg-XhREKiWitwPGAPOV .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-XhREKiWitwPGAPOV .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-XhREKiWitwPGAPOV .actor-man circle,#mermaid-svg-XhREKiWitwPGAPOV line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-XhREKiWitwPGAPOV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 应用层产生 500B 响应 收齐 3 段,重组为 500B 报文 链路帧1: TH=0xC0(SEQ=0) + 246B链路帧2: TH=0x10(SEQ=1) + 246B链路帧3: TH=0x82(SEQ=2, FIN=1) + 8B应用层确认(若 CON 置位)
3.3.4 主站 → 从站方向的分段
主站→从站的大报文(如批量写配置、文件下发)同样分段,但每个链路帧走 CONFIRMED USER DATA(FC=4)并逐帧 ACK------可靠性由链路层保证,主站方向通常不使用传输层 CON 位。
主站分段发送(链路层逐帧确认):
M → O: CONFIRMED USER DATA 帧1 (TH=0xC0) O → M: ACK
M → O: CONFIRMED USER DATA 帧2 (TH=0x10) O → M: ACK
M → O: CONFIRMED USER DATA 帧3 (TH=0x82) O → M: ACK
3.4 传输层确认机制
3.4.1 CON 位与确认
传输头 CON 位在从站 → 主站方向有实际意义:从站请求主站对该报文做应用层确认。
从站 → 主站 的报文(尤其 Unsolicited 上报):
TH 的 CON=1 → 主站收齐并重组后,回一个应用层 CONFIRM(功能码 0)
TH 的 CON=0 → 主站无需确认(如轮询响应)
主站确认的作用:
- 让从站知道"上送的 Unsolicited 事件已安全到达"
- 从站未收到确认 → 超时后重发 Unsolicited
3.4.2 两个方向的可靠性分工
| 方向 | 可靠性机制 | 说明 |
|---|---|---|
| 主站 → 从站 | 链路层 CONFIRMED USER DATA + ACK | 每个链路帧单独确认,FCB 去重 |
| 从站 → 主站 | 传输层 CON + 应用层 CONFIRM | 整个报文确认一次(Unsolicited 场景) |
这种"方向不对称"的设计与 DNP3 的主从模型一致:主站可信、可控,从站需要确认来确保事件不丢。
3.4.3 超时与重发
从站发送 CON=1 的多帧 Unsolicited 后:
- 等待主站 CONFIRM,超时(工程上常配 1-5s)
- 未收到 → 重发整个报文(SEQ 重新从 0 开始)
- 重发 N 次后放弃,事件留在缓冲中等下次轮询
3.5 伪传输层 vs IEC 104 滑动窗口对比
| 维度 | DNP3 伪传输层 | IEC 104 APCI(I 帧) |
|---|---|---|
| 所处位置 | 链路层与应用层之间,独立一层 | 应用层 APDU 头部 |
| 序号 | SEQ,模 16(0-15) | N(S)/N®,模 128(0-127) |
| 序号方向 | 仅段序号,无"期待接收"反向序号 | N(S) 发送 + N® 接收双向序号 |
| 确认方式 | 主站侧靠链路层 ACK;从站侧靠应用层 CONFIRM | S 帧统一确认 |
| 窗口 | 无窗口概念,逐段收发 | k/w 参数限制未确认帧数 |
| 重发 | 整报文重发(从站侧) | 按 N® 缺口重发未确认帧 |
| 底层依赖 | 无(串口/电台/TCP/UDP 通用) | 必须 TCP |
| 设计哲学 | 为不可靠低速链路自建分段 | 在可靠 TCP 之上做应用确认 |
工程启示:正因为伪传输层的"传输无关"特性,DNP3 可以无缝跑在串口、无线电台、TCP(端口 20000)、UDP 上,而协议栈上层完全无感。IEC 104 则被 TCP 绑定。两者各有取舍------DNP3 的代价是 SEQ 空间小(模 16)、需要自行处理重组超时。
小结与导航
本期完成了协议栈的第二层:
- 伪传输层的定位 ------ 解决链路层 250 字节限制,让大报文跨"无连接"链路可靠传输
- 传输头结构 ------ FIN/FIR/CON + SEQ(模 16),单帧 0xC0、多帧按 246 字节/段切分
- 分段重组状态机 ------ 按 FIR/FIN/SEQ 连续性组装,异常时丢弃缓冲
- 确认机制 ------ 主站→从站靠链路层逐帧 ACK;从站→主站靠应用层 CONFIRM(CON 位)
- 与 IEC 104 对比 ------ 模 16 段序号 vs 模 128 滑动窗口,传输无关 vs TCP 绑定
下期预告
Part 4:应用层框架 将进入协议栈最上层:
- 应用层报文结构(应用控制字 + 功能码 + 对象头 + 数据体)
- 功能码全集(读/写/操作/冻结/文件/安全...)
- IIN 内部指示字 16 位全解
- 请求-响应报文逐字段剖析
参考标准
DNP Users Group Specification Volume 3: Transport Function
推荐工具Wireshark(过滤
dnp3)------ 观察分段报文的 FIN/FIR/SEQ 标注opendnp3 ------ 参考实现中的
TransportRx/TransportTx状态机代码