公司动态

L2CAP 完整数据包结构——分段重组、流控、信道隔离实战拆解

📅 2026/8/25 5:55:38
L2CAP 完整数据包结构——分段重组、流控、信道隔离实战拆解
L2CAP 是经典蓝牙协议栈的承重墙——RFCOMM、SDP、AVDTP、BNEP 全压在它上面。理解 L2CAP 的数据包结构和 SAR分段重组机制是排查数据丢包“大包发不出”多协议互相阻塞等问题的前提。这篇按字段拆解 L2CAP 数据包并讲清 ERTM 流控和信道隔离的实战要点。TL;DRL2CAP 用 CIDChannel Identifier隔离上层协议每个上层协议独占一个信道。Basic 模式无流控无重传最简单但不可靠ERTM 模式有 ARQ 和流控是可靠传输的基础。SAR分段重组解决 L2CAP SDU最大 64KB和基带 ACL 包最大 1083B的大小不匹配。ERTM 的 TxSeq/ReqSeq 实现确认和重传TxWindow 控制在途帧数。信道隔离的关键是每条信道独立的 CID、独立的 MTU、独立的流控状态——不要混用。目录L2CAP 在协议栈中的位置Basic 模式 PDU 结构ERTM 模式 PDU 结构分段与重组SAR流控机制信道隔离抓包实战1. L2CAP 在协议栈中的位置1.1 协议栈层级应用层 (Profiles) │ RFCOMM / SDP / AVDTP / BNEP ... │ ▼ L2CAP ← 本层 │ ▼ HCI → BasebandL2CAP 的核心职责协议复用上层多协议通过 Channel 并发分段与重组SAR适配基带分组大小QoS为不同 Channel 协商流量参数信道管理建立、配置、关闭逻辑信道1.2 SDU 和 PDU 的区别SDUService Data Unit上层交给 L2CAP 的数据大小可达 64KBPDUProtocol Data UnitL2CAP 发给基带的数据受基带包大小限制L2CAP 的工作就是把大 SDU 切成小 PDU加上 L2CAP 头发给基带。接收端逆向重组。1.3 CIDChannel Identifier每个 L2CAP 信道用 16 位 CID 标识CID用途0x0001L2CAP 信令信道0x0002连接接收信道0x0003连接发起信道0x0004AMP 管理协议0x0005LE L2CAP 信令0x0007BR/EDR 安全管理SM0x0040动态分配应用使用固定 CID 用于协议栈自身通信动态 CID0x0040分配给上层协议。2. Basic 模式 PDU 结构2.1 PDU 格式Basic 模式是最简单的 L2CAP PDU┌──────────────┬──────────────┬────────────────┐ │ Length │ CID │ Payload │ │ (16 bit) │ (16 bit) │ (0~65535 B) │ └──────────────┴──────────────┴────────────────┘ 2 字节 2 字节 变长LengthPayload 的字节数不含头CID目标信道 ID接收端据此分发到上层Payload实际数据2.2 抓包示例一个 SDP 查询请求的 L2CAP PDU04 00 01 00 35 00 ... │ │ │ │ └── SDP payload │ │ └── CID 0x0001 (L2CAP 信令) └── Length 4等等这里 CID0x0001 是信令信道。实际 SDP 数据走动态 CID如 0x004008 00 40 00 35 00 01 00 ... │ │ │ │ └── SDP payload │ │ └── CID 0x0040 (动态分配给 SDP) └── Length 82.3 Basic 模式的局限Basic 模式没有流控和重传不保证可靠基带丢包 L2CAP 不补不保证顺序基带可能乱序L2CAP 不整理无流控发快了接收端可能溢出适合小数据、可容忍丢失的场景如 SDP 查询。大数据或要可靠的场景用 ERTM。3. ERTM 模式 PDU 结构3.1 为什么需要 ERTMERTMEnhanced Retransmission Mode解决 Basic 的不可靠问题ARQ 重传丢包自动重传流控滑动窗口控制发送速率顺序保证接收端按 TxSeq 排序RFCOMMSPP 串口通常用 ERTM 保证可靠传输。3.2 ERTM PDU 格式┌──────────┬──────────┬──────────────┬─────────┬────────┐ │ Length │ CID │ Control (16) │ Info │ FCS(16)│ │ (16) │ (16) │ │ Field │ │ └──────────┴──────────┴──────────────┴─────────┴────────┘比 Basic 多了 Control 和 FCSControl16 bit包含 TxSeq、ReqSeq、SAR 标志FCS16 bit帧校验序列CRC 校验3.3 Control 字段详解ERTM 的 Control 字段I 帧信息帧Bit: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ │SAR│ TxSeq │ F │ R │ ReqSeq │ Type │ └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘Typebit 0-1帧类型。00I帧数据01S帧监督10Start11EndReqSeqbit 2-8期望收到的下一帧序号ACK 机制Rbit 9重传标志Fbit 10Final 标志Poll/FinalTxSeqbit 11-14本帧序号SARbit 15-16分段标志3.4 S 帧监督帧S 帧用于控制不带数据Control (S 帧): Type 01 Supervisory Type: 00 RR (Receiver Ready) - 接收就绪ACK 01 REJ (Reject) - 拒绝要求重传 10 RNR (Receiver Not Ready) - 接收未就绪 11 SREJ (Selective Reject) - 选择性重传3.5 TxSeq 和 ReqSeq 的协同发送方发送 TxSeq1, 2, 3 接收方收到后回 ReqSeq4 (期望下一帧是 4表示 1/2/3 都收到了) 如果发送方没收到 ReqSeq超时后重传。这是典型的 ARQ 协议——和 TCP 的 ACK 类似。4. 分段与重组SAR4.1 为什么要 SAR基带 ACL 分组最大载荷DH5/EDR3仅 1083 字节而 L2CAP 上层 SDU 可达 64KB。必须分段。上层 SDU (64KB) ↓ L2CAP 切成多个 PDU (每个 ≤ MTU) ↓ 基带 ACL 包 (每个 ≤ 1083B)4.2 Basic 模式的 SARBasic 模式的 SAR 隐式——L2CAP 直接切包不标记起始/结束。接收端按 CID 重组// 发送端voidl2cap_send_basic(uint16_tcid,uint8_t*data,size_tlen){size_toffset0;while(offsetlen){size_tchunkmin(MTU,len-offset);send_basic_pdu(cid,dataoffset,chunk);offsetchunk;}}Basic 模式的问题如果多个 SDU 交错到达接收端无法区分边界——所以 Basic 模式实际不支持交错必须一个 SDU 发完才能发下一个。4.3 ERTM 模式的 SARERTM 在 Control 字段标记 SARSAR 值含义00未分段完整 SDU01起始分段10结束分段11中间分段// ERTM 发送分段voidl2cap_send_ertm(uint16_tcid,uint8_t*data,size_tlen){size_toffset0;uint8_ttxseq0;while(offsetlen){size_tchunkmin(MTU,len-offset);uint8_tsar;if(lenMTU){sarSAR_UNSEGMENTED;}elseif(offset0){sarSAR_START;}elseif(offsetchunklen){sarSAR_END;}else{sarSAR_CONTINUATION;}send_ertm_pdu(cid,txseq,sar,dataoffset,chunk);offsetchunk;}}4.4 接收端重组typedefstruct{uint8_t*buffer;size_toffset;bool receiving;}reassembly_ctx_t;voidon_ertm_pdu(uint16_tcid,uint8_ttxseq,uint8_tsar,uint8_t*data,size_tlen){reassembly_ctx_t*ctxget_ctx(cid);switch(sar){caseSAR_UNSEGMENTED:// 完整 SDU直接交付deliver_to_upper(cid,data,len);break;caseSAR_START:// 起始分段ctx-offset0;ctx-receivingtrue;memcpy(ctx-bufferctx-offset,data,len);ctx-offsetlen;break;caseSAR_CONTINUATION:// 中间分段if(!ctx-receiving){LOG_E(Unexpected continuation);return;}memcpy(ctx-bufferctx-offset,data,len);ctx-offsetlen;break;caseSAR_END:// 结束分段if(!ctx-receiving){LOG_E(Unexpected end);return;}memcpy(ctx-bufferctx-offset,data,len);ctx-offsetlen;ctx-receivingfalse;// 完整 SDU交付deliver_to_upper(cid,ctx-buffer,ctx-offset);break;}}4.5 SAR 的常见问题问题1分段丢失如果中间分段丢失整个 SDU 无法重组。ERTM 通过 ARQ 重传丢失的分段。问题2缓冲不足接收端要预分配 64KB 缓冲存重组中的 SDU。低端设备内存紧张时可能不够。问题3交错问题Basic 模式不支持交错——一个 SDU 没发完不能发另一个。ERTM 通过 SAR 标记支持交错但实现复杂。5. 流控机制5.1 滑动窗口ERTM 用滑动窗口控制发送速率TxWindow 5最多 5 帧在途 发送方[1][2][3][4][5] ← 已发送等 ACK ↑ 窗口前沿 收到 ACK3 后1/2 已确认 发送方[3][4][5][6][7] ← 可以继续发 6/7TxWindow 大小影响吞吐和内存太小吞吐低等 ACK 才能发下一个太大内存占用高要缓存所有未确认帧典型值RFCOMM 用 2-5AVDTP 用 1-2。5.2 重传机制发送方发帧后启动重传定时器超时未收到 ACK 则重传typedefstruct{uint8_tframe[MAX_PDU_SIZE];size_tlen;uint8_ttxseq;uint32_tretransmit_count;uint64_tlast_send_time;}tx_frame_t;voidon_retransmit_timer(){for(inti0;iwindow_size;i){tx_frame_t*ftx_window[i];if(f-txseqexpected_ack){// 已确认跳过continue;}if(now()-f-last_send_timeRETRANSMIT_TIMEOUT){// 超时重传resend_frame(f);f-last_send_timenow();f-retransmit_count;if(f-retransmit_countMAX_RETRANSMITS){// 重传次数过多断开信道l2cap_disconnect(cid);}}}}5.3 流控状态接收端用 RNR 帧告知发送端我满了接收端缓冲快满时 → 发送 RNR (Receiver Not Ready) → 发送方暂停发送 接收端缓冲恢复 → 发送 RR (Receiver Ready) → 发送方恢复发送这避免了接收端缓冲溢出导致丢包。5.4 Flush TimeoutFlush Timeout 是 Basic/Streaming 模式的参数——基带包发出去后如果在这个时间内没成功交付就丢弃不重传Flush Timeout 100ms: 基带包发出去100ms 内没 ACK 就丢弃 适用于实时流A2DP宁丢不重传避免延迟累积A2DP 通常设 Flush Timeout 32ms 或 64ms——超过就丢保证实时性。6. 信道隔离6.1 独立 CID每条 L2CAP 信道有独立的 CID接收端按 CID 分发voidon_l2cap_pdu(uint16_tcid,uint8_t*data,size_tlen){switch(cid){caseCID_SDP:sdp_handle(data,len);break;caseCID_RFCOMM:rfcomm_handle(data,len);break;caseCID_AVDTP:avdtp_handle(data,len);break;default:LOG_W(Unknown CID: 0x%04x,cid);}}6.2 独立 MTU每条信道协商独立的 MTUtypedefstruct{uint16_tcid;uint16_tlocal_mtu;uint16_tremote_mtu;uint16_teffective_mtu;// min(local, remote)}l2cap_channel_t;// 配置时协商voidon_config_request(uint16_tcid,uint16_tremote_mtu){l2cap_channel_t*chfind_channel(cid);ch-remote_mturemote_mtu;ch-effective_mtumin(ch-local_mtu,remote_mtu);// 响应send_config_response(cid,ch-local_mtu);}SDP 的 MTU 可能是 672默认AVDTP 的 MTU 可能是 1083适配 DH5。互不影响。6.3 独立流控状态ERTM 信道的流控状态TxSeq、ReqSeq、TxWindow是每条信道独立的typedefstruct{uint16_tcid;uint8_ttxseq;uint8_treqseq;uint8_ttx_window_size;tx_frame_ttx_window[MAX_WINDOW];// ... 其他流控状态}ertm_channel_t;ertm_channel_tchannels[MAX_CHANNELS];// 每条信道独立RFCOMM 信道的流控不影响 AVDTP 信道——这是隔离的核心。6.4 隔离失效的后果如果信道隔离失效如共用缓冲、共用序号会出现问题根因现象AVDTP 卡顿影响 RFCOMM共用队列串口数据延迟SDP 查询阻塞 A2DP共用线程音频卡顿一条信道断开影响其他共用状态全部断连正确实现必须保证每条信道的资源独立。6.5 信道建立流程ResponderInitiatorResponderInitiator信道建立挂起状态OPEN 状态可传输数据Connection Request (PSM, SCID)Connection Response (DCID, 结果待定)Configuration Request (DCID, MTU, QoS)Configuration Request (SCID, MTU)Configuration Response (接受)Configuration Response (接受)建立流程的关键PSM标识上层协议如 0x0003SDPSCID/DCID源 CID 和目标 CID两端各自分配MTU/QoS参数协商7. 抓包实战7.1 Wireshark 解析Wireshark 能解析 L2CAP 包Frame 123: 1024 bytes Bluetooth HCI ACL Packet L2CAP Packet Length: 1020 CID: 0x0040 (AVDTP) Payload: AVDTP ...看 CID 就知道是哪个上层协议的数据。7.2 常见 CID 速查CID协议出现场景0x0001L2CAP 信令建立/配置/断开信道0x0003SDP 早期现已用动态 CID0x0005RFCOMM 早期现已用动态 CID0x0040动态分配SDP/RFCOMM/AVDTP/AVCTP实际抓包中看到的多是 0x0040因为 SDP/RFCOMM 等都用动态 CID。7.3 排查数据丢包现象上层应用发现数据丢失。排查步骤看 L2CAP 层是否有重传ERTM 模式如果有重传但最终失败是链路质量问题如果没有重传Basic 模式是设计问题——应该用 ERTM看 SAR 标记分段是否正确检查 START/CONTINUATION/END 是否完整缺失分段会导致整个 SDU 丢弃看流控状态是否窗口满TxWindow 满了会暂停发送可能是接收端处理慢看 FCS 校验是否有 CRC 错误FCS 错误的帧会被丢弃7.4 排查大包发不出现象上层尝试发 64KB 数据但 L2CAP 发不出。排查查 MTUhcitool lsc看协商的 MTU如果 MTU 太小如 672大包要分很多段查 TxWindowERTM 窗口是否太小TxWindow1 时等 ACK 才能发下一个吞吐低查 Flush Timeout是否设置过短Flush Timeout10ms 会导致大包没发完就被丢弃7.5 排查多协议互相阻塞现象SDP 查询时 A2DP 卡顿。排查查信道隔离是否每条信道独立队列共用队列会导致一个协议阻塞另一个查线程模型是否 L2CAP 处理在单线程单线程时 SDP 处理慢会阻塞 A2DP查优先级是否实时流A2DP有更高优先级A2DP 应该优先于 SDP 调度