1. 蓝牙服务发现协议(SDP)深度解析
作为一名蓝牙协议栈开发工程师,我经常需要与SDP(Service Discovery Protocol)打交道。今天我想系统梳理一下这个看似简单却暗藏玄机的协议,分享一些实际开发中的经验教训。
SDP本质上是一种"服务黄页",它允许蓝牙设备在建立连接前,先了解对方支持哪些服务以及这些服务的具体特性。举个例子,当你的手机连接蓝牙耳机时,就是通过SDP先确认耳机支持A2DP(音频传输)和AVRCP(远程控制)服务,然后才建立相应的协议连接。
2. SDP核心架构解析
2.1 基础通信模型
SDP采用经典的C/S架构,但有趣的是,蓝牙设备通常同时具备Client和Server能力。在实际交互中:
- 主动发起查询的一方是Client
- 响应查询请求的一方是Server
- 一个设备可以同时承担两种角色
这种设计带来了很大灵活性。比如在手机连接耳机的场景中:
- 手机作为Client查询耳机的服务(Server角色)
- 耳机也可以反过来查询手机的服务(Client角色)
2.2 服务记录(Service Record)结构
每个服务在SDP Server中都表现为一条服务记录,它包含两个核心部分:
-
服务句柄(ServiceHandle):32位唯一标识符
- 范围:0x00010000-0xFFFFFFFF
- 注意:前65535个ID(0x00000001-0x0000FFFF)是保留值
-
服务属性表:描述服务特征的键值对集合
- 属性ID:16位无符号整数
- 属性值:类型和格式由属性ID决定
plaintext复制服务记录示例:
ServiceHandle: 0x00010001
Attributes:
0x0001 (ServiceClassIDList): [0x110A] # Audio Sink服务
0x0004 (ProtocolDescriptorList): [L2CAP, AVDTP]
0x0009 (BluetoothProfileDescriptorList): [A2DP 1.3]
实际开发中发现,某些蓝牙芯片对ServiceHandle的分配有特殊要求。比如某厂商芯片要求必须以0x00010000为起始,每次递增0x10000,否则会导致服务注册失败。
3. 关键数据类型详解
3.1 UUID处理机制
UUID是服务识别的核心,蓝牙采用128位基UUID:
00000000-0000-1000-8000-00805F9B34FB
为节省空间,实际使用中会进行转换:
-
16位UUID:如0x110A代表Audio Sink服务
- 转换公式:128位UUID = 16位值 × 2^96 + 基UUID
- 即:0x110A → 0000110A-0000-1000-8000-00805F9B34FB
-
32位UUID:如自定义服务
- 转换公式:128位UUID = 32位值 × 2^96 + 基UUID
常见UUID示例:
- 0x0001 - SDP服务本身
- 0x0003 - RFCOMM协议
- 0x000F - BNEP协议
- 0x1101 - Serial Port服务
3.2 数据元素(Data Element)编码
SDP使用统一的数据编码格式,每个数据元素包含:
头部(1字节):
- 高5位:类型描述符
- 低3位:大小索引
常见类型值:
- 0x01 (1): Nil类型
- 0x02 (2): 无符号整数
- 0x03 (3): 有符号整数
- 0x04 (4): UUID
- 0x05 (5): 字符串
- 0x06 (6): 布尔值
- 0x07 (7): 数据元素序列
大小索引示例:
- 000: 1字节数据
- 001: 2字节数据
- 010: 4字节数据
- 011: 8字节数据
- 100: 16字节数据
- 101: 后续1字节指示长度
- 110: 后续2字节指示长度
- 111: 后续4字节指示长度
调试技巧:当解析SDP数据包时,我习惯先用第一个字节判断数据类型和长度,这样可以快速定位解析错误。比如遇到0x25(00100101):
- 类型=4(UUID)
- 大小=5(8字节长度)
表示接下来是一个8字节的UUID数据。
4. SDP协议交互全解析
4.1 PDU基础结构
所有SDP数据包都遵循相同头部格式:
plaintext复制PDU Header:
PDU ID (1字节): 请求/响应类型
TransactionID (2字节): 事务标识
ParameterLength (2字节): 参数部分长度
常见PDU类型:
- 0x01: SDP_ErrorResponse
- 0x02: SDP_ServiceSearchRequest
- 0x03: SDP_ServiceSearchResponse
- 0x04: SDP_ServiceAttributeRequest
- 0x05: SDP_ServiceAttributeResponse
- 0x06: SDP_ServiceSearchAttributeRequest
- 0x07: SDP_ServiceSearchAttributeResponse
4.2 服务查询事务
4.2.1 服务搜索流程
-
请求包(SDP_SERVICE_SEARCH_REQ):
- ServiceSearchPattern:要查询的UUID列表
- MaximumServiceRecordCount:期望返回的最大记录数
- ContinuationState:续传状态(首次为0)
-
响应包(SDP_SERVICE_SEARCH_RSP):
- TotalServiceRecordCount:匹配的服务总数
- CurrentServiceRecordCount:本次返回的数量
- ServiceRecordHandleList:服务句柄列表
- ContinuationState:非0表示还有数据
典型错误码:
- 0x0001: 无效语法
- 0x0002: 无效PUD大小
- 0x0003: 无效ContinuationState
- 0x0004: 内存不足
实际案例:某次调试发现Android手机无法发现我们的自定义服务,最终定位是MaximumServiceRecordCount设置过小(只设为5),而设备实际支持8个服务,导致部分服务无法被发现。建议至少设置为16。
4.3 属性查询事务
4.3.1 属性查询流程
-
请求包(SDP_SERVICE_ATTR_REQ):
- ServiceRecordHandle:目标服务句柄
- MaximumAttributeByteCount:最大返回字节数
- AttributeIDList:要查询的属性ID范围
- ContinuationState:续传状态
-
响应包(SDP_SERVICE_ATTR_RSP):
- AttributeListByteCount:返回的属性数据长度
- AttributeList:属性数据(Data Element序列)
- ContinuationState:续传状态
关键属性ID:
- 0x0000: 保留值
- 0x0001: ServiceClassIDList
- 0x0004: ProtocolDescriptorList
- 0x0005: BrowseGroupList
- 0x0006: LanguageBaseAttributeIDList
- 0x0009: BluetoothProfileDescriptorList
- 0x0200: 厂商自定义属性起始ID
4.4 组合查询事务
SDP_SERVICE_SEARCH_ATTR_REQ结合了搜索和属性查询,适合批量获取服务信息:
-
请求包:
- ServiceSearchPattern:服务搜索模式
- MaximumAttributeByteCount:最大返回字节数
- AttributeIDList:属性ID列表
- ContinuationState:续传状态
-
响应包:
- AttributeListsByteCount:返回数据总长度
- AttributeLists:属性数据列表
- ContinuationState:续传状态
性能对比:在BLE设备上测试发现,使用组合查询比分开查询快约40%,但会多消耗15%的内存。建议在内存充足的设备上优先使用组合查询。
5. 实战案例分析
5.1 典型查询流程解析
以查询A2DP服务为例:
- 首次查询:
plaintext复制SDP_SERVICE_SEARCH_REQ:
TransactionID: 0x1234
ServiceSearchPattern: [0x110A] (Audio Sink)
MaxCount: 10
Continuation: 0x00
- 服务响应:
plaintext复制SDP_SERVICE_SEARCH_RSP:
TransactionID: 0x1234
TotalCount: 1
CurrentCount: 1
Handles: [0x00010001]
Continuation: 0x00
- 属性查询:
plaintext复制SDP_SERVICE_ATTR_REQ:
TransactionID: 0x1235
Handle: 0x00010001
MaxBytes: 512
AttrIDs: [0x0000-0xFFFF] # 查询所有属性
Continuation: 0x00
- 属性响应:
plaintext复制SDP_SERVICE_ATTR_RSP:
TransactionID: 0x1235
AttrBytes: 120
Attributes: [
0x0001: [0x110A],
0x0004: [L2CAP, AVDTP],
0x0009: [A2DP 1.3],
...
]
Continuation: 0x00
5.2 平台实现差异
-
iOS风格:
- 先SDP_SERVICE_SEARCH_REQ查询服务句柄
- 再针对每个服务SDP_SERVICE_ATTR_REQ查询属性
- 优点:节省带宽
- 缺点:交互次数多
-
Android风格:
- 直接使用SDP_SERVICE_SEARCH_ATTR_REQ
- 一次性获取所有服务和属性
- 优点:交互次数少
- 缺点:数据量大
兼容性建议:在我们的SDK实现中,会先尝试Android风格查询,如果失败再回退到iOS风格,这样能兼顾效率和兼容性。
6. 开发经验与避坑指南
6.1 常见问题排查
-
服务注册失败:
- 检查ServiceHandle是否唯一
- 确认没有使用保留的Handle范围
- 验证属性格式是否符合规范
-
查询无响应:
- 确认对方设备是否支持SDP
- 检查L2CAP通道是否建立成功(PSM=0x0001)
- 验证TransactionID是否每次请求都不同
-
数据解析错误:
- 检查Data Element的头部解析
- 确认字节序(大端模式)
- 验证属性值的类型是否匹配
6.2 性能优化技巧
-
合理设置MaximumAttributeByteCount:
- 太小会导致多次查询
- 太大会增加单次响应时间
- 建议值:512-1024字节
-
缓存策略:
- 对频繁查询的服务缓存结果
- 设置合理的缓存过期时间
- 监听服务变更通知(如蓝牙4.1+的ServiceChanged特性)
-
连接参数优化:
- 适当增大L2CAP MTU
- 调整查询超时时间(建议2-5秒)
- 并行查询多个服务(需管理好TransactionID)
6.3 调试工具推荐
-
协议分析工具:
- Wireshark(需蓝牙嗅探适配器)
- Ellisys Bluetooth Analyzer
- Frontline ComProbe
-
开发调试工具:
- Android: Bluetooth HCI snoop log
- iOS: PacketLogger(需MFi授权)
- Linux: hcidump
-
实用命令行工具:
bash复制# Linux查看SDP服务 sdptool browse <BD_ADDR> # 查看本地服务 sdptool records <BD_ADDR> # 注册服务示例 sdptool add --channel=1 A2DP
7. 进阶话题
7.1 蓝牙各版本差异
-
蓝牙2.1+EDR:
- 基础SDP功能
- 服务缓存有限
-
蓝牙4.0(LE):
- 引入GATT替代SDP
- 但双模设备仍需SDP
-
蓝牙4.1+:
- ServiceChanged特性
- 更好的服务更新通知机制
7.2 与GATT的对比
| 特性 | SDP(BR/EDR) | GATT(LE) |
|---|---|---|
| 发现机制 | 主动查询 | 广播+主动读取 |
| 数据组织 | 服务记录 | 特征值(Characteristics) |
| 交互模式 | 请求-响应 | 读/写/通知 |
| 典型延迟 | 较高(100-500ms) | 较低(20-100ms) |
| 适用场景 | 传统蓝牙设备 | 低功耗设备 |
7.3 安全考量
-
信息泄露风险:
- SDP会暴露设备支持的服务
- 建议对敏感服务设置访问权限
-
拒绝服务攻击:
- 恶意的大量SDP查询会耗尽资源
- 应实现请求频率限制
-
安全增强建议:
- 对关键服务启用身份验证
- 实现服务访问控制列表(ACL)
- 定期审计服务注册情况
在实现自定义蓝牙服务时,我通常会先通过SDP暴露基础信息服务,待认证通过后再通过安全通道提供完整功能。这种分层设计既能保证可发现性,又能确保安全性。
