第4章 L2CAP协议:逻辑链路控制与适配协议

L2CAP,全称是逻辑链路控制与适配协议。说实话,这个名字挺拗口的。但它的作用很简单——给上层应用提供更舒服的数据通道。

我刚开始接触蓝牙协议栈时,总觉得L2CAP就是个“数据搬运工”。后来踩了几个坑才明白,它其实是整个蓝牙通信的“交通调度中心”。没有它,HCI层传上来的那些原始数据包,上层根本没法用。

4.1 L2CAP在协议栈中的位置

先看看L2CAP站在哪。它位于HCI层之上,RFCOMM、ATT、BNEP这些协议之下。说白了,它是承上启下的中间层。

我个人习惯把蓝牙协议栈想象成一栋楼:

  • 最底层是射频和基带(地基)
  • HCI层是电梯(传输原始数据)
  • L2CAP是楼层走廊(分配房间、管理通道)
  • 再往上就是各个房间里的住户(各种Profile)

L2CAP干的事,就是给每个“住户”分配独立的通道,保证他们不会互相干扰。

4.2 面向连接通道 vs 无连接通道

L2CAP支持两种通道模式。这个知识点,面试时经常被问到。

4.2.1 面向连接通道(Connection-Oriented Channel)

这种通道,我习惯叫它“专线”。建立连接后,双方独占这条通道。数据按顺序到达,可靠性高。

适用于:A2DP音频流、文件传输、RFCOMM通信。

我在项目中遇到过一个问题:车载电话的音频经常断断续续。排查到最后,发现是L2CAP的面向连接通道被其他服务占用了带宽。嗯,这里要注意——一条面向连接通道在同一时间只能被一个服务使用

关键特性:

  • 需要显式建立连接(Connect Request/Response)
  • 数据按序交付
  • 支持流控和重传
  • 通道ID(CID)在0x0040到0xFFFF之间分配

4.2.2 无连接通道(Connectionless Channel)

无连接通道,说白了就是“广播”。不需要建立连接,直接往一个固定的CID(0x0002)上发数据。谁想收谁就监听。

适用于:广播数据、设备发现、简单状态通知。

我曾经在调试车载蓝牙的广播功能时,发现无连接通道的数据包经常丢失。后来查了规范才知道——无连接通道不保证可靠传输,也不支持重传。所以,关键数据千万别走这条路。

对比项 面向连接通道 无连接通道
建立连接 需要 不需要
可靠性 高(支持重传) 低(无重传)
数据顺序 保证 不保证
典型应用 A2DP、RFCOMM 广播、状态通知
CID范围 0x0040 - 0xFFFF 固定0x0002

4.3 MTU协商机制

MTU,最大传输单元。这个参数,我建议每个做蓝牙开发的工程师都要搞明白。因为它直接影响通信效率。

你想想看,如果发送方一次能发1000字节,但接收方只能收500字节,那数据就会丢。所以,通信前双方得商量好:你最大能收多少?

4.3.1 协商过程

MTU协商发生在L2CAP连接建立的阶段。流程是这样的:

  1. 发起方发送连接请求,里面带上自己的MTU值
  2. 接收方回复连接响应,也带上自己的MTU值
  3. 双方取较小值作为实际使用的MTU

举个例子:

  • 手机说:我能收1000字节
  • 车机说:我只能收500字节
  • 最终双方用500字节通信

我的经验:在Android Automotive上,默认的L2CAP MTU通常是672字节。但很多车机芯片只支持到512字节。如果你发现蓝牙连接后数据传输特别慢,先查查MTU是不是被限制了。

4.3.2 代码示例:MTU协商

在Android中,L2CAP的MTU协商是通过BluetoothSocket的配置来实现的。下面是一个典型的设置:

// 创建L2CAP面向连接通道
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);
BluetoothSocket socket = device.createL2capSocket(
    PSM,                    // 协议服务复用器
    L2capSocketOption.MTU,  // 设置MTU选项
    1024                    // 请求的MTU值
);

// 连接后获取实际协商的MTU
int actualMtu = socket.getMaxTransmitPacketSize();
Log.d("L2CAP", "协商后的MTU: " + actualMtu);

注意:getMaxTransmitPacketSize()返回的是实际协商后的值,不是你请求的那个值。我曾经犯过这个错——以为设了1024就能发1024,结果发大包时一直失败。

避坑指南:我曾经在调试一款国产车机时,发现MTU协商总是失败。查了三天,最后发现是车机的蓝牙芯片固件有bug——它不支持MTU协商请求中的扩展特性标志。解决方案是:在发起连接时,把扩展特性标志置为0。

4.4 L2CAP的帧格式

L2CAP的帧结构其实不复杂。我一般把它分成三部分:

  • 长度字段(2字节):表示后面数据的长度
  • 通道ID(2字节):标识这条数据属于哪个通道
  • 载荷(可变长度):真正的数据内容

对于面向连接通道,帧格式更简单:

| 长度 (2字节) | CID (2字节) | 载荷 (0-65535字节) |

无连接通道的帧会多一个协议/服务复用器字段(PSM),用来标识上层协议类型。

4.5 实际项目中的注意事项

最后,分享几个我在车载项目中积累的经验:

  1. MTU不是越大越好。有些工程师喜欢把MTU设到65535,觉得这样效率高。但在车载环境下,射频干扰多,大包更容易出错。我一般建议控制在512-1024字节之间。
  2. 注意PSM的分配。PSM是奇数,范围从0x0001到0xFFFF。0x0001是SDP,0x0003是RFCOMM。自定义服务要用0x1001以上的值。
  3. 超时处理要谨慎。L2CAP的响应超时默认是40秒。在车载场景下,这个时间太长了。我习惯把它缩短到10秒,用户体验会好很多。

总结一下:L2CAP是蓝牙协议栈的“交通调度中心”。面向连接通道用于可靠通信,无连接通道用于广播。MTU协商决定了通信效率,一定要根据实际场景合理设置。

下一章,我们会讲RFCOMM协议。到时候你会看到,RFCOMM其实就是跑在L2CAP面向连接通道上的“串口模拟器”。嗯,内容更有意思。