3. L2CAP层:逻辑链路控制与适配协议
好,咱们今天聊聊L2CAP。说实话,这是蓝牙协议栈里我最喜欢的一层。为什么?因为它干的是脏活累活——把底层那些乱七八糟的数据包,整理得井井有条,让上层应用能舒舒服服地收发数据。
你想想看,底层是啥?是ACL链路,是基带层的那些包。它们只管发,不管你的数据是长是短,是连续还是离散。L2CAP就是中间那个翻译官,把上层的大块数据切成小块,或者把小块拼成大块。嗯,这个角色很关键。
3.1 L2CAP的组帧
组帧,说白了就是给数据加个“信封”。L2CAP的帧结构其实不复杂,我直接给你看:
+----------------+----------------+----------------+
| 长度 (2字节) | 通道ID (2字节) | 净荷 (可变长) |
+----------------+----------------+----------------+
就这么简单?对,就这么简单。但别小看它。
- 长度字段:表示净荷有多少字节。注意,它不包括长度字段和通道ID本身。这个设计很巧妙,接收方拿到长度就知道要收多少数据。
- 通道ID:这是L2CAP的灵魂。每个通道ID对应一个逻辑连接。比如0x0001是信令通道,0x0002是面向连接通道的起始值。我在项目中遇到过有人把通道ID搞混了,结果数据发到了错误的接收方,排查了好久才发现。
- 净荷:就是上层传下来的数据,比如SDP查询、RFCOMM的帧,或者A2DP的音频数据。
重要:L2CAP帧的最大长度受MTU限制。后面我们会详细讲MTU协商,这里先记住:组帧时不能超过MTU,否则就要分段。
3.2 分段与重组
为什么要分段?因为底层ACL链路有最大包长限制。比如经典蓝牙的ACL包最大是1021字节(3-DH5包),但上层应用可能一次要发64KB的数据。这时候L2CAP就得把大包切成小段。
分段的过程是这样的:
- 上层应用调用L2CAP发送接口,传下来一个大的SDU(服务数据单元)。
- L2CAP检查SDU长度是否超过MTU。如果超过,就把它切成多个PDU(协议数据单元)。
- 每个PDU加上L2CAP头,然后交给底层发送。
重组就是反过来的过程。接收方收到多个PDU,根据L2CAP头里的信息,把它们拼回原来的SDU。
个人经验:我曾经在一个车载项目中,遇到音频数据断断续续的问题。排查到最后,发现是L2CAP分段时,某个PDU丢失了,导致整个SDU重组失败。后来我加了个超时重传机制,问题就解决了。嗯,这里要注意,分段后的PDU是独立传输的,任何一个丢了,整个SDU都得重传。
3.3 MTU协商
MTU,最大传输单元。这个值决定了L2CAP一次能传多少数据。MTU协商是L2CAP层最重要的握手过程之一。
为什么需要协商?因为两端设备的能力不同。比如手机可能支持1024字节的MTU,但车载音响只支持672字节。如果不协商,手机发一个1024字节的包,音响就收不了。
协商过程是这样的:
- 发起方发送一个
CONNECTION_REQ请求,里面带上自己期望的MTU值。 - 接收方回复
CONNECTION_RSP,带上自己能接受的MTU值。 - 双方取较小的那个值作为最终的MTU。
我建议你在实现时,把MTU协商放在连接建立的第一步。别等到传数据了才发现MTU不匹配,那就尴尬了。
| MTU值 | 典型场景 | 说明 |
|---|---|---|
| 48字节 | 早期蓝牙设备 | 基本不可用,只适合传控制信令 |
| 672字节 | 经典蓝牙默认值 | 大多数设备都支持,安全可靠 |
| 1024字节 | A2DP音频流 | 适合传音频数据,效率高 |
| 65535字节 | BLE扩展 | BLE 4.2之后支持,但实际受底层限制 |
避坑指南:我曾经在一个项目中,把MTU设得太大,结果底层ACL链路频繁丢包。后来发现,MTU不是越大越好。你想想看,MTU越大,一个包丢了重传的成本就越高。车载环境干扰多,我建议MTU设在672-1024之间,既保证效率,又保证可靠性。
3.4 面向连接与无连接通道
L2CAP支持两种通道类型:面向连接(CO)和无连接(CL)。
3.4.1 面向连接通道
面向连接,就是先建立连接,再传数据。这个过程有点像打电话——先拨号,接通了再说话。
建立连接需要三步:
- 发送
CONNECTION_REQ请求。 - 接收方回复
CONNECTION_RSP,带上结果码。 - 如果成功,双方就可以通过这个通道ID收发数据了。
面向连接通道的特点:
- 可靠:有重传机制,数据不会丢。
- 有序:数据按发送顺序到达。
- 适合大块数据:比如A2DP音频流、RFCOMM数据。
3.4.2 无连接通道
无连接通道,就是不需要建立连接,直接发数据。这有点像广播——你喊一嗓子,谁爱听谁听。
无连接通道的特点:
- 快速:不需要握手,直接发。
- 不可靠:没有重传,丢了就丢了。
- 适合广播数据:比如蓝牙信标、设备发现。
我的建议:在车载音响中,音频数据一定要用面向连接通道。为什么?因为音频流对连续性和顺序性要求很高。你总不希望听歌时,突然少了一段吧?但像设备发现、服务广播这类数据,用无连接通道就够了,省时省力。
3.5 实战中的注意事项
好了,理论讲完了,我分享几个实战中的坑:
- MTU协商失败:有些设备不按规范来,MTU协商时返回的值不合理。我建议你在代码里加个校验,如果MTU小于48字节,直接断开连接。
- 分段重组超时:如果某个分段迟迟不到,整个SDU就卡住了。我习惯设一个超时定时器,比如500ms,超时了就丢弃这个SDU,通知上层重传。
- 通道ID冲突:多任务环境下,通道ID可能被重复分配。记得用互斥锁保护通道ID的分配过程。
嗯,L2CAP层就讲到这里。下一章我们聊RFCOMM,那个更贴近应用层,也更有意思。