GATT 协议与服务发现
GATT 协议与服务发现蓝牙面试核心考点考点定位面试权重★★★★☆高频考点面试官常问“请解释 GATT 协议在 BLE 中的作用”“服务发现的过程是怎样的客户端和服务端如何交互”“Characteristic 和 Service 的区别是什么Descriptor 有什么用”“手写一个服务发现的伪代码流程”这道题几乎出现在每一场蓝牙开发面试中因为它直接考察你对 BLE 应用层通信机制的理解深度。面试官想听到的不仅仅是定义而是你对“谁发起、谁响应、数据怎么走”的完整认知。一、核心概念一句话说清楚GATT通用属性协议是 BLE 应用层的通信框架它定义了数据如何组织Service/Characteristic/Descriptor以及如何交互Client-Server 模型。服务发现是 GATT 客户端通常是手机主动查询 GATT 服务端通常是蓝牙外设上所有可用服务的过程。二、原理深度展开2.1 GATT 的数据组织层级想象一个蓝牙温度计GATT Server温度计 └── Service: 温度服务 (UUID: 0x1809) ├── Characteristic: 温度值 (UUID: 0x2A1C) │ ├── Properties: Read, Notify │ ├── Value: 36.5°C │ └── Descriptor: 客户端特征配置 (CCCD) │ └── 控制是否启用 Notify └── Characteristic: 温度单位 (UUID: 0x2A1D) ├── Properties: Read, Write └── Value: Celsius关键点Service逻辑功能单元如电池服务、设备信息服务Characteristic数据的最小操作单元如电池电量百分比Descriptor描述 Characteristic 的元数据如是否支持通知、用户描述2.2 服务发现的工作流程这是面试中最容易出细节的地方。记住服务发现永远是客户端主动发起。客户端手机 服务端外设 | | |--- 1. 发送发现主服务请求 -----| |-- 2. 返回主服务列表 ---------| |--- 3. 发现包含服务 ----------| |-- 4. 返回包含服务列表 -------| |--- 5. 发现特征值 ------------| |-- 6. 返回特征值列表 ---------| |--- 7. 发现描述符 ------------| |-- 8. 返回描述符列表 ---------| | | | 完成所有服务发现 |面试常考细节发现顺序是固定的先发现所有主服务 → 再发现每个主服务下的包含服务 → 再发现每个服务的特征值 → 最后发现特征值的描述符UUID 的两种形式16位 UUID蓝牙 SIG 标准服务和 128位 UUID自定义服务CCCD客户端特征配置描述符是唯一一个必须被客户端主动写入的描述符用来开启/关闭通知或指示2.3 数据交互模式操作发起方方向典型场景Read客户端客户端←服务端读取静态数据如设备名称Write客户端客户端→服务端发送指令如开关灯Notify服务端客户端←服务端实时数据推送如心率Indicate服务端客户端←服务端需要确认的推送如关键告警面试陷阱Notify 和 Indicate 的区别是什么Notify服务端发送后不等待确认速度快但可能丢包Indicate服务端发送后等待客户端确认可靠但速度慢两者都需要先通过 CCCD 开启三、面试常考细节容易忽略但高频3.1 服务发现的性能问题面试官追问“如果外设有 20 个服务每个服务有 10 个特征值发现过程需要多少次交互”答案发现主服务1 次请求 1 次响应发现包含服务按需通常 1 次请求 1 次响应发现特征值每个服务 1 次请求 1 次响应 20 次发现描述符每个特征值 1 次请求 1 次响应 200 次总计约 222 次交互不包含分包优化策略面试加分项使用gatt_discover_all_services一次性发现所有服务缓存服务发现结果避免重复发现只发现需要的服务按 UUID 过滤3.2 MTU 对服务发现的影响核心机制MTU最大传输单元决定了单次能传输的数据量。默认 MTU 是 23 字节其中 3 字节是协议头实际可用 20 字节。影响如果特征值 UUID 列表超过 20 字节需要分包传输每个包只能携带约 4 个 16位 UUID 或 1 个 128位 UUIDMTU 协商通常在服务发现前完成面试话术“在服务发现前我会先发起 MTU 协商请求将 MTU 提升到 512 甚至更高这样可以减少分包次数提升发现效率。”四、手撕代码服务发现伪代码# 蓝牙服务发现伪代码Android 风格 class GattClient: def discover_services(self, gatt): # 1. 发现所有主服务 gatt.discover_services() # 2. 等待 on_services_discovered 回调 def on_services_discovered(gatt, status): if status ! GATT_SUCCESS: return for service in gatt.get_services(): # 3. 发现每个服务的特征值 gatt.discover_characteristics(service) # 4. 等待 on_characteristics_discovered 回调 def on_characteristics_discovered(gatt, service, status): for char in service.get_characteristics(): # 5. 发现每个特征值的描述符 gatt.discover_descriptors(char) # 6. 等待 on_descriptors_discovered 回调 def on_descriptors_discovered(gatt, char, status): for desc in char.get_descriptors(): if desc.uuid CCCD_UUID: # 记录需要开启通知的特征值 self.notify_chars.append(char)面试官会问“这段代码有什么问题”答案回调嵌套太深没有处理并发和错误恢复。实际开发中应该使用状态机管理发现流程实现超时重试机制处理部分发现失败的情况五、对比和区分高频对比题5.1 GATT vs GAP维度GATTGAP职责数据组织和传输连接管理和广播层级应用层链路层之上角色Client/ServerCentral/Peripheral主要操作读写、通知、指示广播、扫描、连接典型场景传输温度数据发现设备、建立连接面试话术“GAP 负责‘找到设备并连上’GATT 负责‘连上后怎么传数据’。两者是 BLE 协议栈的两个支柱缺一不可。”5.2 Service vs Characteristic vs Descriptor概念类比作用必须存在Service文件夹逻辑分组是Characteristic文件实际数据是Descriptor文件属性描述数据特性否六、举一反三进阶追问追问 1“如果服务发现过程中断如何处理”答案记录已发现的服务/特征值列表从断点处继续发现按 UUID 范围实现超时机制3 秒无响应则重试重试 3 次失败后通知用户追问 2“如何实现一个自定义服务”答案生成 128位 UUID如0000xxxx-1212-efde-1523-785feabcd123定义 Service 和 Characteristic 的权限实现 Characteristic 的读写回调注册到 GATT 服务器追问 3“Service Changed 特征值是什么”答案当服务端动态添加/删除服务时通过这个特征值通知客户端客户端收到后需要重新执行服务发现这是 BLE 4.0 引入的机制用于支持动态服务七、面试话术总结当面试官问“请描述 GATT 服务发现过程”时参考答案“GATT 服务发现是客户端主动发起的查询过程。首先客户端发送发现主服务请求服务端返回所有主服务的 UUID 和句柄。然后客户端逐个查询每个主服务下的包含服务和特征值。对于每个特征值客户端还会查询其描述符特别是 CCCD 描述符用于控制通知和指示的开关。整个过程遵循‘先服务、再特征、后描述’的固定顺序。实际开发中我会注意 MTU 协商对分包的影响并实现缓存机制避免重复发现。如果服务发现失败我会实现重试逻辑最多重试 3 次每次间隔 500ms。”加分项主动提到“我会在发现完成后根据业务需求选择性开启某些特征值的通知而不是全部开启以节省功耗。”总结GATT 和服务发现是蓝牙面试的必考点。掌握数据组织层级、交互流程、性能优化和异常处理你就能在面试中脱颖而出。记住面试官要的不是背书而是你真正理解“数据怎么从外设到手机”的完整链路。