SDAP(Service Discovery Application Profile)是蓝牙协议栈中负责设备服务发现的核心协议。当两个蓝牙设备建立连接后,如何知道对方提供了哪些服务(如串口服务、音频服务、文件传输服务)?SDAP 就是答案。它基于 SDP(Service Discovery Protocol)实现服务的查询、注册和匹配。本文基于蓝牙核心规范与 BlueZ 5.x 源码,深入浅出地讲解 SDAP 协议原理与源码落地,彻底理解蓝牙服务发现的完整机制。
目录
[一、SDAP 协议核心框架](#一、SDAP 协议核心框架)
[二、BlueZ SDP 服务器初始化](#二、BlueZ SDP 服务器初始化)
[五、BlueZ SDP 客户端实现](#五、BlueZ SDP 客户端实现)
[八、经典蓝牙与 BLE 的服务发现差异](#八、经典蓝牙与 BLE 的服务发现差异)
一、SDAP 协议核心框架
1.1 SDAP 与 SDP 的关系
SDAP 是应用层 Profile,SDP 是底层协议,两者关系如下:

SDAP 的核心职责:
-
服务查询:向远端设备查询可用服务
-
属性检索:获取服务的详细属性信息(协议描述、端口号、服务名称等)
-
服务注册:将本地服务注册到 SDP 服务器供其他设备发现
-
服务匹配:根据 UUID 匹配符合条件的服务
1.2 SDP PDU 类型
SDP 定义了六种核心 PDU 类型:
|-----------------------------------|--------|-----------------|-------------|
| PDU 类型 | 码值 | 方向 | 作用 |
| Error Response | 0x01 | Server → Client | 错误响应 |
| Service Search Request | 0x02 | Client → Server | 服务搜索请求 |
| Service Search Response | 0x03 | Server → Client | 服务搜索响应 |
| Service Attribute Request | 0x04 | Client → Server | 属性请求 |
| Service Attribute Response | 0x05 | Server → Client | 属性响应 |
| Service Search Attribute Request | 0x06 | Client → Server | 搜索+属性请求(组合) |
| Service Search Attribute Response | 0x07 | Server → Client | 搜索+属性响应 |
BlueZ 源码 定义:
#define SDP_ERROR_RSP 0x01
#define SDP_SVC_SEARCH_REQ 0x02
#define SDP_SVC_SEARCH_RSP 0x03
#define SDP_SVC_ATTR_REQ 0x04
#define SDP_SVC_ATTR_RSP 0x05
#define SDP_SVC_SEARCH_ATTR_REQ 0x06
#define SDP_SVC_SEARCH_ATTR_RSP 0x07
1.3 PDU 帧结构
所有 SDP PDU 共享相同的头部结构:
0 8 16 32
+--------+--------+--------+
| pdu_id | tid | plen |
| (8bit) | (16bit)| (16bit)|
+--------+--------+--------+
-
pdu_id:PDU 类型标识
-
tid:事务 ID(Transaction ID),用于匹配请求和响应
-
plen:参数长度(Parameter Length)
源码 结构体:
typedef struct {
uint8_t pdu_id;
uint16_t tid;
uint16_t plen;
} __attribute__ ((packed)) sdp_pdu_hdr_t;
1.4 服务记录与属性
每个服务在 SDP 服务器中以**服务记录(Service Record)**的形式存在:
typedef struct {
uint32_t handle; /* 服务记录句柄 */
sdp_list_t *pattern; /* 搜索模式:UUID 序列 */
sdp_list_t *attrlist; /* 属性列表 */
uuid_t svclass; /* 主服务类别 UUID */
} sdp_record_t;
常用属性 ID:
|-----------|------------------------|--------------|
| 属性 ID | 属性名称 | 作用 |
| 0x0000 | ServiceRecordHandle | 服务记录句柄 |
| 0x0001 | ServiceClassIDList | 服务类别 UUID 列表 |
| 0x0003 | ServiceID | 服务唯一 ID |
| 0x0004 | ProtocolDescriptorList | 协议描述列表 |
| 0x0100 | ServiceName | 服务名称(UTF-8) |
| 0x0101 | ServiceDescription | 服务描述 |
| 0x0102 | ProviderName | 服务提供者名称 |
1.5 协议描述列表(ProtocolDescriptorList)
这是服务记录中最重要的属性之一,定义了如何访问该服务:
ProtocolDescriptorList = Sequence {
Protocol { UUID, Protocol-Specific-Params },
Protocol { UUID, Protocol-Specific-Params },
...
}
示例:SPP(串口服务)的协议描述
Sequence {
Protocol { L2CAP_UUID(0x0100) },
Protocol { RFCOMM_UUID(0x0003), Channel(1) }
}
这表示:通过 L2CAP 连接,再使用 RFCOMM 协议,通道号为 1。
二、BlueZ SDP 服务器初始化
2.1 服务器启动流程
BlueZ 的 SDP 服务器在 bluetoothd 启动时初始化:
static int init_server(uint16_t mtu, int central, int compat)
{
struct l2cap_options opts;
struct sockaddr_l2 l2addr;
/* 1. 注册公共浏览组根 */
register_public_browse_group();
/* 2. 注册 SDP 服务器自身的服务记录 */
register_server_service();
/* 3. 创建 L2CAP socket */
l2cap_sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP);
if (l2cap_sock < 0) {
error("opening L2CAP socket: %s", strerror(errno));
return -1;
}
/* 4. 绑定到 SDP PSM (0x0001) */
memset(&l2addr, 0, sizeof(l2addr));
l2addr.l2_family = AF_BLUETOOTH;
bacpy(&l2addr.l2_bdaddr, BDADDR_ANY);
l2addr.l2_psm = htobs(SDP_PSM); /* SDP_PSM = 0x0001 */
if (bind(l2cap_sock, (struct sockaddr *) &l2addr, sizeof(l2addr)) < 0) {
error("binding L2CAP socket: %s", strerror(errno));
return -1;
}
/* 5. 设置 MTU */
opts.omtu = mtu;
opts.imtu = mtu;
setsockopt(l2cap_sock, SOL_L2CAP, L2CAP_OPTIONS, &opts, sizeof(opts));
/* 6. 监听连接 */
if (listen(l2cap_sock, 5) < 0) {
error("listen: %s", strerror(errno));
return -1;
}
return 0;
}
2.2 请求处理流程
static gboolean io_session_event(GIOChannel *chan, GIOCondition cond, gpointer data)
{
sdp_pdu_hdr_t hdr;
uint8_t *buf;
int sk, len, size;
sk = g_io_channel_unix_get_fd(chan);
/* 1. 先读取 PDU 头部 */
len = recv(sk, &hdr, sizeof(sdp_pdu_hdr_t), MSG_PEEK);
if (len < 0 || (unsigned int) len < sizeof(sdp_pdu_hdr_t))
goto cleanup;
/* 2. 根据 plen 计算完整 PDU 大小 */
size = sizeof(sdp_pdu_hdr_t) + ntohs(hdr.plen);
buf = malloc(size);
/* 3. 读取完整 PDU */
len = recv(sk, buf, size, 0);
if (len <= 0) {
free(buf);
goto cleanup;
}
/* 4. 处理请求 */
handle_request(sk, buf, len);
return TRUE;
cleanup:
sdp_svcdb_collect_all(sk);
sdp_cstate_cleanup(sk);
return FALSE;
}
2.3 请求分发
static void process_request(sdp_req_t *req)
{
sdp_pdu_hdr_t *reqhdr = (sdp_pdu_hdr_t *)req->buf;
sdp_pdu_hdr_t *rsphdr;
int status = SDP_INVALID_SYNTAX;
switch (reqhdr->pdu_id) {
case SDP_SVC_SEARCH_REQ:
SDPDBG("Got a svc srch req");
status = service_search_req(req, &rsp);
rsphdr->pdu_id = SDP_SVC_SEARCH_RSP;
break;
case SDP_SVC_ATTR_REQ:
SDPDBG("Got a svc attr req");
status = service_attr_req(req, &rsp);
rsphdr->pdu_id = SDP_SVC_ATTR_RSP;
break;
case SDP_SVC_SEARCH_ATTR_REQ:
SDPDBG("Got a svc srch attr req");
status = service_search_attr_req(req, &rsp);
rsphdr->pdu_id = SDP_SVC_SEARCH_ATTR_RSP;
break;
/* 本地注册/更新/删除请求 */
case SDP_SVC_REGISTER_REQ:
if (req->local) {
status = service_register_req(req, &rsp);
rsphdr->pdu_id = SDP_SVC_REGISTER_RSP;
}
break;
/* ... 其他请求类型 */
}
/* 发送响应 */
rsphdr->tid = reqhdr->tid;
rsphdr->plen = htons(rsp.data_size);
send(req->sock, rsp.data, rsp.data_size, 0);
}
三、服务搜索流程详解
3.1 服务搜索请求/响应
Service Search Request PDU:

Service Search Response PDU:

3.2 服务搜索核心实现
static int service_search_req(sdp_req_t *req, sdp_buf_t *buf)
{
sdp_list_t *pattern = NULL;
uint16_t expected, actual;
sdp_cont_state_t *cstate = NULL;
uint8_t *pdata = req->buf + sizeof(sdp_pdu_hdr_t);
/* 1. 提取搜索模式(UUID 序列) */
scanned = extract_des(pdata, data_left, &pattern, &dtd, SDP_TYPE_UUID);
if (scanned == -1) {
status = SDP_INVALID_SYNTAX;
goto done;
}
/* 2. 获取期望返回的记录数 */
expected = get_be16(pdata);
/* 3. 检查续传状态 */
sdp_cstate_get(req, pdata, data_left, &cstate, &cinfo);
/* 4. 遍历服务数据库,匹配 UUID */
sdp_list_t *list = sdp_get_record_list();
for (; list && rsp_count < expected; list = list->next) {
sdp_record_t *rec = list->data;
/* UUID 匹配检查 */
if (sdp_match_uuid(pattern, rec->pattern) > 0 &&
sdp_check_access(rec->handle, &req->device)) {
rsp_count++;
put_be32(rec->handle, pdata); /* 写入服务记录句柄 */
}
}
/* 5. 如果结果超出 MTU,创建续传状态 */
if (rsp_count > actual) {
cStateId = sdp_cstate_alloc_buf(req, buf);
buf->data_size -= handleSize; /* 只返回部分结果 */
} else {
sdp_set_cstate_pdu(buf, NULL); /* 无续传状态 */
}
done:
return status;
}
3.3 UUID 匹配算法
static int sdp_match_uuid(sdp_list_t *search, sdp_list_t *pattern)
{
/* search: 客户端搜索的 UUID 列表
* pattern: 服务记录中的 UUID 列表(已排序)
*/
int patlen = sdp_list_len(pattern);
/* 如果服务记录的 UUID 数量少于搜索的 UUID 数量,直接不匹配 */
if (patlen < sdp_list_len(search))
return -1;
/* 检查搜索模式中的每个 UUID 是否都在目标模式中存在 */
for (; search; search = search->next) {
uuid_t *uuid128 = sdp_uuid_to_uuid128((uuid_t *)search->data);
sdp_list_t *list = sdp_list_find(pattern, uuid128, sdp_uuid128_cmp);
bt_free(uuid128);
if (!list)
return 0; /* 不匹配 */
}
return 1; /* 完全匹配 */
}
四、属性提取流程
4.1 属性请求/响应
Service Attribute Request PDU:

Service Attribute Response PDU:

4.2 属性提取核心实现
static int extract_attrs(sdp_record_t *rec, sdp_list_t *seq, sdp_buf_t *buf)
{
if (!rec)
return SDP_INVALID_RECORD_HANDLE;
/* 生成完整的服务记录 PDU */
sdp_gen_record_pdu(rec, &pdu);
/* 遍历请求的属性 ID 列表 */
for (; seq; seq = seq->next) {
struct attrid *aid = seq->data;
if (aid->dtd == SDP_UINT16) {
/* 单个属性 ID */
uint16_t attr = aid->uint16;
sdp_data_t *a = sdp_data_get(rec, attr);
if (a)
sdp_append_to_pdu(buf, a);
} else if (aid->dtd == SDP_UINT32) {
/* 属性范围(高 16 位是起始 ID,低 16 位是结束 ID) */
uint32_t range = aid->uint32;
uint16_t low = (0xffff0000 & range) >> 16;
uint16_t high = 0x0000ffff & range;
/* 如果请求全部属性(0x0000-0xffff),直接复制完整 PDU */
if (low == 0x0000 && high == 0xffff && pdu.data_size <= buf->buf_size) {
memcpy(buf->data, pdu.data, pdu.data_size);
buf->data_size = pdu.data_size;
break;
}
/* 否则,逐个提取属性 */
for (attr = low; attr < high; attr++) {
sdp_data_t *data = sdp_data_get(rec, attr);
if (data)
sdp_append_to_pdu(buf, data);
}
}
}
free(pdu.data);
return 0;
}
五、BlueZ SDP 客户端实现
5.1 服务搜索入口
BlueZ 提供了两个核心搜索函数:
/* 搜索服务(结果服务类别 ID 不必匹配 UUID) */
int bt_search(const bdaddr_t *src, const bdaddr_t *dst,
uuid_t *uuid, bt_callback_t cb, void *user_data,
bt_destroy_t destroy, uint16_t flags);
/* 搜索服务(结果服务类别 ID 必须匹配 UUID) */
int bt_search_service(const bdaddr_t *src, const bdaddr_t *dst,
uuid_t *uuid, bt_callback_t cb, void *user_data,
bt_destroy_t destroy, uint16_t flags);
5.2 搜索上下文管理
struct search_context {
bdaddr_t src; /* 源蓝牙地址 */
bdaddr_t dst; /* 目标蓝牙地址 */
sdp_session_t *session; /* SDP 会话 */
bt_callback_t cb; /* 回调函数 */
bt_destroy_t destroy; /* 资源释放函数 */
gpointer user_data; /* 用户数据 */
uuid_t uuid; /* 搜索的 UUID */
guint io_id; /* IO 监听 ID */
gboolean filter_svc_class; /* 是否过滤服务类别 */
};
5.3 搜索完整流程
static int create_search_context(struct search_context **ctxt,
const bdaddr_t *src, const bdaddr_t *dst,
uuid_t *uuid, uint16_t flags)
{
sdp_session_t *s;
/* 1. 尝试从缓存获取 SDP 会话 */
s = get_cached_sdp_session(src, dst);
if (!s)
s = sdp_connect(src, dst, SDP_NON_BLOCKING | flags);
if (!s)
return -errno;
/* 2. 创建搜索上下文 */
*ctxt = g_try_malloc0(sizeof(struct search_context));
bacpy(&(*ctxt)->src, src);
bacpy(&(*ctxt)->dst, dst);
(*ctxt)->session = s;
(*ctxt)->uuid = *uuid;
/* 3. 设置连接监听 */
sk = sdp_get_socket(s);
chan = g_io_channel_unix_new(sk);
(*ctxt)->io_id = g_io_add_watch(chan,
G_IO_OUT | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
connect_watch, *ctxt);
return 0;
}
5.4 连接完成后的处理
static gboolean connect_watch(GIOChannel *chan, GIOCondition cond,
gpointer user_data)
{
struct search_context *ctxt = user_data;
sdp_list_t *search, *attrids;
uint32_t range = 0x0000ffff; /* 请求所有属性 */
/* 1. 设置响应回调 */
sdp_set_notify(ctxt->session, search_completed_cb, ctxt);
/* 2. 构建搜索参数 */
search = sdp_list_append(NULL, &ctxt->uuid);
attrids = sdp_list_append(NULL, &range);
/* 3. 发送服务搜索+属性请求 */
sdp_service_search_attr_async(ctxt->session,
search, SDP_ATTR_REQ_RANGE, attrids);
/* 4. 设置数据读取监听 */
ctxt->io_id = g_io_add_watch(chan,
G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
search_process_cb, ctxt);
return FALSE;
}
5.5 响应解析
static void search_completed_cb(uint8_t type, uint16_t status,
uint8_t *rsp, size_t size, void *user_data)
{
struct search_context *ctxt = user_data;
sdp_list_t *recs = NULL;
int scanned, seqlen = 0, bytesleft = size;
/* 1. 检查响应类型和状态 */
if (status || type != SDP_SVC_SEARCH_ATTR_RSP) {
err = -EPROTO;
goto done;
}
/* 2. 提取服务记录序列 */
scanned = sdp_extract_seqtype(rsp, bytesleft, &dataType, &seqlen);
if (!scanned || !seqlen)
goto done;
/* 3. 逐个提取服务记录 */
do {
sdp_record_t *rec;
int recsize;
rec = sdp_extract_pdu(rsp, bytesleft, &recsize);
if (!rec)
break;
/* 4. 如果需要,过滤服务类别 */
if (ctxt->filter_svc_class &&
sdp_uuid_cmp(&ctxt->uuid, &rec->svclass) != 0) {
sdp_record_free(rec);
continue;
}
recs = sdp_list_append(recs, rec);
} while (scanned < (ssize_t) size && bytesleft > 0);
done:
/* 5. 缓存 SDP 会话 */
cache_sdp_session(&ctxt->src, &ctxt->dst, ctxt->session);
/* 6. 调用用户回调 */
if (ctxt->cb)
ctxt->cb(recs, err, ctxt->user_data);
}
六、服务缓存机制
6.1 缓存结构
#define CACHE_TIMEOUT 2 /* 缓存有效期(秒) */
struct cached_sdp_session {
bdaddr_t src; /* 源地址 */
bdaddr_t dst; /* 目标地址 */
sdp_session_t *session; /* SDP 会话 */
unsigned int timer; /* 超时定时器 */
guint io_id; /* IO 监听 ID */
};
static GSList *cached_sdp_sessions = NULL;
6.2 缓存管理
static int sap_server_register(struct sap_server *server)
{
sdp_record_t *rec = sdp_record_alloc();
uuid_t root_uuid, sap_uuid, rfcomm_uuid;
sdp_list_t *svclass_list, *proto_list;
/* 1. 设置服务类别 */
sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP);
sdp_uuid16_create(&sap_uuid, SAP_SVCLASS_ID);
svclass_list = sdp_list_append(NULL, &root_uuid);
svclass_list = sdp_list_append(svclass_list, &sap_uuid);
sdp_set_service_classes(rec, svclass_list);
/* 2. 设置服务 ID */
sdp_uuid16_create(&sap_uuid, SAP_SVCLASS_ID);
sdp_set_service_id(rec, sap_uuid);
/* 3. 设置协议描述列表 */
proto_list = sdp_list_append(NULL, &l2cap);
proto_list = sdp_list_append(proto_list, &rfcomm);
sdp_set_proto_desc_list(rec, proto_list);
/* 4. 设置 RFCOMM 通道 */
sdp_data_t *channel = sdp_data_alloc(SDP_UINT8, &server->channel);
sdp_attr_add(rec, SDP_ATTR_RFCOMM_CHANNEL, channel);
/* 5. 设置服务名称 */
sdp_set_info_attr(rec, "SIM Access Server", NULL, NULL);
/* 6. 注册到 SDP 服务器 */
server->record_handle = sdp_record_register(rec, 0);
sdp_record_free(rec);
return 0;
}
缓存作用:避免短时间内重复建立 SDP 连接,减少协议开销。
七、服务注册流程
7.1 本地服务注册
BlueZ 支持通过 Unix socket 进行本地服务注册:
int device_discover_services(struct btd_device *device)
{
int err;
if (device->bredr)
err = device_browse_sdp(device, NULL); /* 经典蓝牙:SDP 浏览 */
else
err = device_browse_gatt(device, NULL); /* BLE:GATT 发现 */
if (err == 0 && device->discov_timer) {
timeout_remove(device->discov_timer);
device->discov_timer = 0;
}
return err;
}
7.2 Profile 服务注册示例(SAP)
static int sap_server_register(struct sap_server *server)
{
sdp_record_t *rec = sdp_record_alloc();
uuid_t root_uuid, sap_uuid, rfcomm_uuid;
sdp_list_t *svclass_list, *proto_list;
/* 1. 设置服务类别 */
sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP);
sdp_uuid16_create(&sap_uuid, SAP_SVCLASS_ID);
svclass_list = sdp_list_append(NULL, &root_uuid);
svclass_list = sdp_list_append(svclass_list, &sap_uuid);
sdp_set_service_classes(rec, svclass_list);
/* 2. 设置服务 ID */
sdp_uuid16_create(&sap_uuid, SAP_SVCLASS_ID);
sdp_set_service_id(rec, sap_uuid);
/* 3. 设置协议描述列表 */
proto_list = sdp_list_append(NULL, &l2cap);
proto_list = sdp_list_append(proto_list, &rfcomm);
sdp_set_proto_desc_list(rec, proto_list);
/* 4. 设置 RFCOMM 通道 */
sdp_data_t *channel = sdp_data_alloc(SDP_UINT8, &server->channel);
sdp_attr_add(rec, SDP_ATTR_RFCOMM_CHANNEL, channel);
/* 5. 设置服务名称 */
sdp_set_info_attr(rec, "SIM Access Server", NULL, NULL);
/* 6. 注册到 SDP 服务器 */
server->record_handle = sdp_record_register(rec, 0);
sdp_record_free(rec);
return 0;
}
八、经典蓝牙与 BLE 的服务发现差异
8.1 核心差异对比
|------------|----------------------------|---------------------------|
| 维度 | 经典蓝牙 (BR/EDR) | BLE |
| 发现协议 | SDP (SDAP Profile) | GATT Discovery |
| 传输层 | L2CAP (PSM=0x0001) | L2CAP CoC / ATT |
| 服务标识 | UUID + ServiceRecordHandle | UUID + Attribute Handle |
| 数据结构 | Service Record | GATT Attribute |
| 搜索方式 | Service Search Request | Discover Primary Services |
| 属性获取 | Service Attribute Request | Read by Type Request |
| 默认 MTU | 672 | 23 |
8.2 BlueZ 中的差异化处理
int device_discover_services(struct btd_device *device)
{
int err;
if (device->bredr)
err = device_browse_sdp(device, NULL); /* 经典蓝牙:SDP 浏览 */
else
err = device_browse_gatt(device, NULL); /* BLE:GATT 发现 */
if (err == 0 && device->discov_timer) {
timeout_remove(device->discov_timer);
device->discov_timer = 0;
}
return err;
}
8.3 BLE GATT 发现流程
BLE 使用 GATT 协议进行服务发现,流程如下:
1. Discover Primary Services (ATT_OP_READ_BY_TYPE_REQ, UUID=0x2800)
→ 返回所有主服务的 UUID 和起始 Handle
2. Discover Characteristics (ATT_OP_READ_BY_TYPE_REQ, UUID=0x2803)
→ 返回每个服务的特征列表
3. Discover Descriptors (ATT_OP_FIND_INFO_REQ)
→ 返回每个特征的描述符
4. Read Characteristic Value (ATT_OP_READ_REQ)
→ 读取特征值
九、开发要点与常见问题
9.1 开发要点
1. SDP 搜索基本流程
#include <bluetooth/sdp.h>
#include <bluetooth/sdp_lib.h>
/* 搜索指定 UUID 的服务 */
uuid_t uuid;
sdp_uuid16_create(&uuid, SERIAL_PORT_SVCLASS_ID);
bt_search_service(src_bdaddr, dst_bdaddr, &uuid,
search_callback, user_data, NULL, 0);
2. 服务记录解析
void search_callback(sdp_list_t *recs, int err, void *user_data)
{
sdp_list_t *r;
for (r = recs; r; r = r->next) {
sdp_record_t *rec = r->data;
char name[256];
/* 获取服务名称 */
sdp_get_string_attr(rec, SDP_ATTR_SVCNAME_PRIMARY, name, sizeof(name));
/* 获取协议描述 */
sdp_data_t *proto = sdp_data_get(rec, SDP_ATTR_PROTO_DESC_LIST);
/* 获取 RFCOMM 通道号 */
uint8_t channel = 0;
sdp_data_t *channel_data = sdp_data_get(rec, SDP_ATTR_RFCOMM_CHANNEL);
if (channel_data)
channel = channel_data->val.uint8;
}
}
3. 本地服务注册
/* 通过 sdptool 命令行注册 */
sdptool add --channel=1 SP
/* 或通过代码注册 */
sdp_record_t *rec = sdp_record_alloc();
/* ... 设置服务属性 ... */
sdp_record_register(rec, 0);
sdp_record_free(rec);
9.2 常见问题
问题 1:服务搜索超时
原因:远端设备未响应或 L2CAP 连接失败。
解决方案:
/* 设置非阻塞模式 */
sdp_session_t *session = sdp_connect(src, dst, SDP_NON_BLOCKING);
/* 设置超时回调 */
timeout_add_seconds(5, search_timeout, ctxt, NULL);
问题 2:搜索结果为空
原因:UUID 不匹配或远端设备未注册该服务。
解决方案:
/* 使用 bt_search 而非 bt_search_service */
/* bt_search:结果服务类别不必匹配 UUID */
/* bt_search_service:结果服务类别必须匹配 UUID */
问题 3:属性提取不完整
原因:响应超出 MTU,需要续传。
解决方案:BlueZ 已自动处理续传状态,回调中会收到完整的服务记录列表。
问题 4:BLE 设备无法使用 SDP
原因:BLE 不支持 SDP 协议。
解决方案:使用 GATT 发现接口:
/* BLE 使用 GATT 发现 */
device_browse_gatt(device, callback);
十、总结
SDAP 作为蓝牙服务发现的核心协议,通过 SDP 实现了设备间服务的查询、注册和匹配。BlueZ 的实现体现了以下设计思想:
-
分层架构:SDAP(应用层)→ SDP(协议层)→ L2CAP(传输层)
-
异步设计:通过回调机制处理搜索结果,不阻塞主线程
-
缓存优化:短时间内重复搜索复用 SDP 会话
-
续传支持:响应超出 MTU 时自动分割,通过续传状态恢复
-
差异化适配:经典蓝牙使用 SDP,BLE 使用 GATT
开发建议:
-
使用
bt_search_service()进行精确的服务搜索 -
使用
bt_search()进行模糊搜索(返回所有包含指定 UUID 的服务) -
通过
sdp_get_string_attr()和sdp_get_int_attr()提取服务属性 -
BLE 设备使用 GATT 接口而非 SDP
附录:核心函数索引
|--------------------|--------------------------|------------|
| 文件 | 函数 | 功能 |
| src/sdpd-server.c | init_server | SDP 服务器初始化 |
| src/sdpd-server.c | handle_request | 请求处理入口 |
| src/sdpd-request.c | service_search_req | 服务搜索请求处理 |
| src/sdpd-request.c | service_attr_req | 属性请求处理 |
| src/sdpd-request.c | sdp_match_uuid | UUID 匹配算法 |
| src/sdp-client.c | bt_search | 服务搜索(模糊匹配) |
| src/sdp-client.c | bt_search_service | 服务搜索(精确匹配) |
| src/sdp-client.c | cache_sdp_session | 缓存 SDP 会话 |
| src/device.c | device_discover_services | 设备服务发现入口 |
| lib/sdp_lib.h | sdp_data_get | 获取服务属性 |
| lib/sdp_lib.h | sdp_record_register | 注册服务记录 |