【BlueZ 】蓝牙 SDAP 协议入门:BlueZ 中设备服务发现的基础逻辑

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 的实现体现了以下设计思想:

  1. 分层架构:SDAP(应用层)→ SDP(协议层)→ L2CAP(传输层)

  2. 异步设计:通过回调机制处理搜索结果,不阻塞主线程

  3. 缓存优化:短时间内重复搜索复用 SDP 会话

  4. 续传支持:响应超出 MTU 时自动分割,通过续传状态恢复

  5. 差异化适配:经典蓝牙使用 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 | 注册服务记录 |


相关推荐
老衲の少女心1 小时前
【AI项目】用 LangGraph 构建智能批改与解题系统:条件路由、SSE流式输出Agent工作流实践
人工智能
RobinDevNotes1 小时前
K8s+Megatron-LM+vLLM打通超大模型训练全链路
人工智能·kubernetes
资源大佬星 课it1 小时前
人工智能机器学习系统班,人工智能深度学习系统班
人工智能
连线Insight1 小时前
从谷歌到百度,AI正在打开搜索增长空间
人工智能
DevNo1 小时前
旅行素材太多不会剪?三款手机AI工具实测体验
人工智能
TMT星球1 小时前
伽利略机器人亮相WRC 2026,突破形态局限发布“陆行具身系统”Galileo X
大数据·人工智能·机器人
是大乔家的1 小时前
AI漫剧用什么软件制作?知漫剧批量生成连载实战分享
人工智能
mounter6251 小时前
GCCRS 突破与 Rust 入核:Linux 工具链的重大变革
linux·开发语言·rust·linux kernel·kernel
IT小白杨2 小时前
当MCP遇上指纹浏览器:AI自然语言驱动隔离环境的实践思路
人工智能·经验分享·selenium·安全·业界资讯·指纹浏览器