【BlueZ 】sdp 模块:服务发现协议的用户态实现基础

在 Bluetooth 经典蓝牙(BR/EDR)体系中,SDP Service Discovery Protocol 服务发现协议 扮演着"服务目录"的核心角色。任何蓝牙设备若想对外提供可发现的服务(如串口、文件传输、音频播放等),都必须通过 SDP 在服务数据库中注册相应的服务记录;而想要使用远端设备服务的客户端,也必须通过 SDP 查询远端设备的服务信息。


目录

[一、SDP 模块整体架构](#一、SDP 模块整体架构)

[二、SDP 数据结构深度解析](#二、SDP 数据结构深度解析)

[三、SDP 服务端实现](#三、SDP 服务端实现)

四、服务数据库管理

五、服务注册实现

六、请求处理实现

七、客户端实现

[八、PDU 编解码实现](#八、PDU 编解码实现)

九、错误处理与异常机制

十、开发调试要点

十一、核心函数索引

十二、总结


BlueZ 5.x 的 SDP 模块完整实现了蓝牙规范中的 SDP 协议栈,包括服务数据库管理、服务注册/注销、搜索请求处理、属性查询、延续状态管理等核心功能。该模块的源码实现位于 src/ 目录下的多个文件中,采用了服务端/客户端分离的架构设计。

本文基于 BlueZ 5.x 完整源码,深度剖析 SDP 模块的用户态实现,覆盖以下核心源码文件:

  • lib/sdp.h --- SDP 协议数据结构与 API 定义

  • lib/sdp.c --- SDP 库层实现(PDU 编解码、UUID 转换)

  • src/sdpd.h --- SDP 守护进程内部接口

  • src/sdpd-server.c --- SDP 服务端实现(Socket 监听、连接管理)

  • src/sdpd-database.c --- 服务数据库管理(增删改查)

  • src/sdpd-request.c --- 请求处理(ServiceSearch、ServiceAttribute 等)

  • src/sdpd-service.c --- 内置服务注册实现

  • src/sdp-client.c --- SDP 客户端实现(远端服务搜索)


一、 SDP 模块整体架构

1.1 模块分层设计

BlueZ SDP 模块采用清晰的分层架构:

1.2 SDP 协议基本概念

服务记录(Service Record)

SDP 的核心是服务记录 ,每个服务记录包含一个 32 位的服务记录句柄 (Service Record Handle)和一系列属性(Attributes)。

cpp 复制代码
// lib/sdp.h:491
typedef struct {
    uint32_t handle;           // 服务记录句柄
    sdp_list_t *pattern;        // 搜索模式(UUID 列表)
    sdp_list_t *attrlist;      // 属性列表
    uuid_t svclass;            // 主要服务类(用于 EIR)
} sdp_record_t;

属性(Attribute)

每个属性由 16 位的属性 ID 和对应的值组成。属性值可以是多种数据类型:

cpp 复制代码
// lib/sdp.h:503
struct sdp_data_struct {
    uint8_t dtd;           // 数据类型描述符
    uint16_t attrId;       // 属性 ID
    union {
        int8_t     int8;
        int16_t    int16;
        int32_t    int32;
        int64_t    int64;
        uint128_t  int128;
        uint8_t    uint8;
        uint16_t   uint16;
        uint32_t   uint32;
        uint64_t   uint64;
        uint128_t  uint128;
        uuid_t     uuid;
        char       *str;
        sdp_data_t *dataseq;
    } val;
    sdp_data_t *next;     // 链表下一节点
    int unitSize;
};

PDU 数据包

SDP 协议定义了多种 PDU(Protocol Data Unit)类型:

cpp 复制代码
// lib/sdp.h:389-395
#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  // 服务搜索属性响应

// 扩展操作码(用于服务注册/更新/移除)
#define SDP_SVC_REGISTER_REQ       0x75
#define SDP_SVC_REGISTER_RSP       0x76
#define SDP_SVC_UPDATE_REQ         0x77
#define SDP_SVC_UPDATE_RSP         0x78
#define SDP_SVC_REMOVE_REQ         0x79
#define SDP_SVC_REMOVE_RSP         0x80

1.3 与 BLE GATT 的对比

|-------------|----------------------------------------|---------------------------|
| 特性 | 经典蓝牙 SDP | BLE GATT |
| 协议层次 | L2CAP (PSM=0x0001) | ATT (L2CAP) |
| 服务模型 | 服务记录 + 属性 | 服务 + 特征 + 描述符 |
| 数据格式 | 类型-长度-值 (TLV) | 类型-长度-值 (TLV) |
| UUID 大小 | 16/32/128 位 | 16/128 位 |
| 查询方式 | ServiceSearch / ServiceSearchAttribute | Primary Service Discovery |
| 响应大小 | 最多 65535 字节 | 最多 MTU-1 字节 |
| 延续机制 | 支持(Continuation State) | 支持(Continue) |
| 安全性 | 基于 L2CAP 安全 | 基于 ATT 安全 |

二、SDP 数据结构深度解析

2.1 uuid_t:UUID 类型

UUID 是 SDP 的基础元素,支持 16 位、32 位和 128 位三种格式:

cpp 复制代码
// lib/sdp.h:431-438
typedef struct {
    uint8_t type;
    union {
        uint16_t  uuid16;
        uint32_t  uuid32;
        uint128_t uuid128;
    } value;
} uuid_t;

// UUID 类型常量
#define SDP_UUID16    0x19  // 16 位 UUID
#define SDP_UUID32    0x1A  // 32 位 UUID
#define SDP_UUID128   0x1C  // 128 位 UUID

UUID 创建函数

cpp 复制代码
// lib/sdp.c 中实现
void sdp_uuid16_create(uuid_t *uuid, uint16_t val);
void sdp_uuid32_create(uuid_t *uuid, uint32_t val);
void sdp_uuid128_create(uuid_t *uuid, uint128_t val);
int sdp_uuid128_to_uuid(uuid_t *uuid);  // 压缩 128 位 UUID

2.2 sdp_list_t:通用链表

SDP 库使用 sdp_list_t 作为基础数据结构:

cpp 复制代码
// lib/sdp.h:447-451
typedef struct _sdp_list sdp_list_t;
struct _sdp_list {
    sdp_list_t *next;
    void *data;
};

链表操作 API

cpp 复制代码
// lib/sdp.c 中实现
sdp_list_t *sdp_list_append(sdp_list_t *list, void *data);
sdp_list_t *sdp_list_prepend(sdp_list_t *list, void *data);
sdp_list_t *sdp_list_insert_sorted(sdp_list_t *list, void *data, 
                                    int (*cmp)(const void *, const void *));
void sdp_list_free(sdp_list_t *list, sdp_free_func_t func);
int sdp_list_len(sdp_list_t *list);

2.3 sdp_data_t:数据元素

每个属性值都由 sdp_data_t 表示,支持多种数据类型:

cpp 复制代码
// lib/sdp.h:503-523
struct sdp_data_struct {
    uint8_t dtd;              // 数据类型描述符
    uint16_t attrId;          // 属性 ID(可选)
    union {
        uint16_t  uint16;     // 16 位无符号整数
        uint32_t  uint32;     // 32 位无符号整数
        uint128_t uint128;    // 128 位值
        uuid_t    uuid;       // UUID
        char      *str;       // 字符串
        sdp_data_t *dataseq;  // 数据序列(嵌套)
        // ... 其他类型
    } val;
    sdp_data_t *next;
};

数据类型常量

cpp 复制代码
// lib/sdp.h:353-384
#define SDP_DATA_NIL           0x00  // 空
#define SDP_UINT8              0x08  // 8 位无符号整数
#define SDP_UINT16             0x09  // 16 位无符号整数
#define SDP_UINT32             0x0A  // 32 位无符号整数
#define SDP_UUID16             0x19  // 16 位 UUID
#define SDP_UUID128            0x1C  // 128 位 UUID
#define SDP_TEXT_STR8          0x25  // 8 位长度文本字符串
#define SDP_SEQ8               0x35  // 8 位长度数据序列
#define SDP_ALT8               0x3D  // 8 位长度备选序列
#define SDP_URL_STR8           0x45  // 8 位长度 URL 字符串

2.4 sdp_pdu_hdr_t:PDU 头

每个 SDP 报文都有统一的 PDU 头结构:

cpp 复制代码
// lib/sdp.h:420-424
typedef struct {
    uint8_t  pdu_id;      // PDU 类型标识
    uint16_t tid;         // 事务 ID(Transaction ID)
    uint16_t plen;        // 负载长度(Payload Length)
} __attribute__((packed)) sdp_pdu_hdr_t;

三、SDP 服务端实现

3.1 服务初始化

init_server()

sdpd-server.c中实现服务器初始化:

cpp 复制代码
// sdpd-server.c
static int init_server(uint16_t mtu, int central, int compat)
{
    // 1. 注册内置服务
    register_public_browse_group();   // 公共浏览组
    register_server_service();        // SDP 服务器自身

    // 2. 创建 L2CAP Socket(用于远端蓝牙设备连接)
    l2cap_sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP);
    
    // 绑定到 SDP 端口 (PSM=0x0001)
    addr.l2_psm = htobs(SDP_PSM);
    bind(l2cap_sock, ...);
    listen(l2cap_sock, 5);

    // 3. 创建 Unix Domain Socket(用于本地应用注册服务)
    unix_sock = socket(PF_UNIX, SOCK_STREAM, 0);
    strcpy(unaddr.sun_path, SDP_UNIX_PATH);  // /var/run/sdp
    bind(unix_sock, ...);
    listen(unix_sock, 5);

    chmod(SDP_UNIX_PATH, 0660);

    return 0;
}

双 Socket 设计

  • L2CAP Socket:监听 PSM=0x0001,接收远端蓝牙设备的 SDP 请求

  • Unix Domain Socket:监听 /var/run/sdp,接收本地应用的服务注册/注销请求

3.2 连接事件处理

io_accept_event():接受新连接

cpp 复制代码
// sdpd-server.c:181
static gboolean io_accept_event(GIOChannel *chan, GIOCondition cond, gpointer data)
{
    int nsk;
    
    // 接受新连接
    if (data == &l2cap_sock) {
        struct sockaddr_l2 addr;
        nsk = accept(l2cap_sock, (struct sockaddr *) &addr, &len);
    } else if (data == &unix_sock) {
        struct sockaddr_un addr;
        nsk = accept(unix_sock, (struct sockaddr *) &addr, &len);
    }

    // 为新连接设置 I/O 监听
    g_io_add_watch(io, G_IO_IN | G_IO_HUP | G_IO_ERR, io_session_event, ...);
}

io_session_event():会话事件处理

cpp 复制代码
// sdpd-server.c:138
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);

    // 读取 PDU 头(不消耗缓冲区)
    len = recv(sk, &hdr, sizeof(sdp_pdu_hdr_t), MSG_PEEK);
    
    // 计算完整 PDU 大小
    size = sizeof(sdp_pdu_hdr_t) + ntohs(hdr.plen);
    buf = malloc(size);

    // 读取完整 PDU
    len = recv(sk, buf, size, 0);

    // 分发到请求处理函数
    handle_request(sk, buf, len);
    
    // 连接关闭时清理
    if (cond & (G_IO_HUP | G_IO_ERR)) {
        sdp_svcdb_collect_all(sk);
        sdp_cstate_cleanup(sk);
        return FALSE;
    }
}

3.3 请求处理分发

handle_request()

handle_request()是所有请求的统一入口:

cpp 复制代码
// sdpd-request.c:1118
void handle_request(int sk, uint8_t *data, int len)
{
    sdp_req_t req;
    sdp_buf_t rsp;
    uint16_t tid;
    uint8_t *pdu;
    uint8_t opcode;

    // 1. 解析请求头
    pdu = data;
    opcode = *pdu++;
    tid = get_be16(pdu);
    pdu += 2;

    // 2. 检查是否为内部请求(来自 Unix Socket)
    if (opcode >= 0x75 && opcode <= 0x80) {
        handle_internal_request(sk, mtu, data, len);
        return;
    }

    // 3. 构建请求结构体
    req.sock = sk;
    req.opcode = opcode;
    req.len = len - sizeof(sdp_pdu_hdr_t);
    req.buf = pdu;

    // 4. 初始化响应缓冲区
    rsp.data_size = 0;
    rsp.buf_size = ...;
    rsp.data = malloc(rsp.buf_size);

    // 5. 根据操作码分发处理
    switch (opcode) {
    case SDP_SVC_SEARCH_REQ:        // 0x02
        status = service_search_req(&req, &rsp);
        break;
    case SDP_SVC_ATTR_REQ:          // 0x04
        status = service_attr_req(&req, &rsp);
        break;
    case SDP_SVC_SEARCH_ATTR_REQ:   // 0x06
        status = service_search_attr_req(&req, &rsp);
        break;
    }

    // 6. 发送响应
    send_response(sk, tid, opcode, &rsp, status);
}

PDU 操作码分类

  • 标准 SDP 请求(0x02-0x07):来自远端蓝牙设备

  • 扩展请求(0x75-0x80):来自本地应用的服务管理请求

四、服务数据库管理

4.1 数据库结构

SDP 服务数据库由两个链表组成(sdpd-database.c:

cpp 复制代码
// sdpd-database.c:28-34
static sdp_list_t *service_db;   // 服务记录数据库
static sdp_list_t *access_db;    // 访问控制数据库

typedef struct {
    uint32_t handle;      // 服务记录句柄
    bdaddr_t device;      // 允许访问的设备地址
} sdp_access_t;           // 访问控制条目

设计说明

  • service_db:存储所有已注册的服务记录,按句柄排序

  • access_db:存储访问控制列表,限定哪些设备可以访问特定服务

4.2 服务添加

sdp_record_add()

sdp_record_add() 将服务记录添加到数据库:

cpp 复制代码
// sdpd-database.c:162
void sdp_record_add(const bdaddr_t *device, sdp_record_t *rec)
{
    sdp_access_t *dev;

    // 按句柄排序插入服务记录
    service_db = sdp_list_insert_sorted(service_db, rec, record_sort);

    // 添加访问控制条目
    dev = malloc(sizeof(*dev));
    bacpy(&dev->device, device);
    dev->handle = rec->handle;
    access_db = sdp_list_insert_sorted(access_db, dev, access_sort);
}

record_sort():排序比较函数

cpp 复制代码
// sdpd-database.c:41
int record_sort(const void *r1, const void *r2)
{
    const sdp_record_t *rec1 = r1;
    const sdp_record_t *rec2 = r2;
    return rec1->handle - rec2->handle;  // 按句柄升序排列
}

4.3 服务查找与移除

sdp_record_find()

通过句柄查找服务记录:

cpp 复制代码
// sdpd-database.c:181
sdp_list_t *record_locate(uint32_t handle)
{
    if (service_db) {
        sdp_list_t *p;
        sdp_record_t r;
        
        r.handle = handle;
        p = sdp_list_find(service_db, &r, record_sort);
        return p;
    }
    return NULL;
}

sdp_record_remove()

通过句柄移除服务记录:

cpp 复制代码
// sdpd-database.c
int sdp_record_remove(uint32_t handle)
{
    sdp_list_t *p, *prev = NULL;

    for (p = service_db; p; prev = p, p = p->next) {
        sdp_record_t *rec = p->data;
        if (rec->handle == handle) {
            // 从链表中移除
            if (prev)
                prev->next = p->next;
            else
                service_db = p->next;
            
            // 释放资源
            sdp_record_free(rec);
            free(p);
            
            // 同时清理访问控制
            access_remove(handle);
            
            return 0;
        }
    }
    return -1;  // 未找到
}

4.4 句柄分配

sdp_next_handle()

分配新的服务记录句柄:

cpp 复制代码
// sdpd-database.c
uint32_t sdp_next_handle(void)
{
    static uint32_t next_handle = 0x10000;  // 初始值
    return next_handle++;
}

句柄分配策略

  • 内置服务:使用固定句柄(如 SDP_SERVER_RECORD_HANDLE + 1

  • 动态注册服务:从 0x10000 开始递增分配

4.5 访问控制

sdp_check_access()

检查远端设备是否有权访问特定服务:

cpp 复制代码
// sdpd-database.c
int sdp_check_access(uint32_t handle, bdaddr_t *device)
{
    sdp_list_t *p;
    sdp_access_t a;

    a.handle = handle;
    p = sdp_list_find(access_db, &a, access_sort);
    
    if (p) {
        sdp_access_t *entry = p->data;
        // 检查设备地址是否匹配(或为 BDADDR_ANY)
        if (bacmp(&entry->device, BDADDR_ANY) == 0 ||
            bacmp(&entry->device, device) == 0)
            return 0;  // 允许访问
    }
    
    return -1;  // 拒绝访问
}

五、服务注册实现

5.1 内置服务注册

register_server_service()

register_server_service() 注册 SDP 服务器自身的服务记录:

cpp 复制代码
// sdpd-service.c:344
void register_server_service(void)
{
    sdp_list_t *classIDList;
    uuid_t classID;
    sdp_record_t *record;

    // 1. 分配新记录
    record = sdp_record_alloc();
    record->handle = SDP_SERVER_RECORD_HANDLE;

    // 2. 添加属性
    sdp_attr_add_new(record, SDP_ATTR_RECORD_HANDLE, SDP_UINT32, 
                    &record->handle);

    // 3. 添加服务类(SDP Server Service Class)
    sdp_uuid16_create(&classID, SDP_SERVER_SVCLASS_ID);
    classIDList = sdp_list_append(0, &classID);
    sdp_set_service_classes(record, classIDList);

    // 4. 添加协议描述符(L2CAP 协议)
    add_l2cap_proto_desc(record, ...);

    // 5. 添加语言基属性列表
    add_lang_attrs(record);

    // 6. 设置主要服务名
    sdp_attr_add_new(record, SDP_ATTR_SVCNAME_PRIMARY, 
                    SDP_TEXT_STR8, "Bluetooth");

    // 7. 注册到数据库
    sdp_record_add(BDADDR_ANY, record);
}

register_public_browse_group()

注册公共浏览组:

cpp 复制代码
// sdpd-service.c:315
void register_public_browse_group(void)
{
    sdp_list_t *browselist;
    uuid_t bgscid, pbgid;
    sdp_record_t *browse = sdp_record_alloc();

    browse->handle = SDP_SERVER_RECORD_HANDLE + 1;

    // 添加服务类(Browse Group Descriptor)
    sdp_uuid16_create(&bgscid, BROWSE_GRP_DESC_SVCLASS_ID);
    browselist = sdp_list_append(0, &bgscid);
    sdp_set_service_classes(browse, browselist);

    // 添加组 ID(Public Browse Group)
    sdp_uuid16_create(&pbgid, PUBLIC_BROWSE_GROUP);
    sdp_attr_add_new(browse, SDP_ATTR_GROUP_ID,
                    SDP_UUID16, &pbgid.value.uuid16);

    // 注册
    sdp_record_add(BDADDR_ANY, browse);
}

5.2 动态服务注册

service_register_req()

处理来自 Unix Socket 的服务注册请求:

cpp 复制代码
// sdpd-request.c
int service_register_req(sdp_req_t *req, sdp_buf_t *rsp)
{
    sdp_record_t *rec;
    uint32_t handle;

    // 1. 解析请求中的服务记录
    rec = sdp_extract_pdu(req->buf, req->len, ...);
    if (!rec)
        return SDP_INVALID_SYNTAX;

    // 2. 分配新句柄
    handle = sdp_next_handle();
    rec->handle = handle;

    // 3. 添加句柄属性
    sdp_attr_add_new(rec, SDP_ATTR_RECORD_HANDLE, 
                    SDP_UINT32, &handle);

    // 4. 注册到数据库
    sdp_record_add(BDADDR_ANY, rec);

    // 5. 设置可收集标记(Unix Socket 关闭时自动移除)
    sdp_svcdb_set_collectable(rec, req->sock);

    // 6. 更新数据库时间戳
    update_db_timestamp();

    // 7. 构造响应(返回新句柄)
    sdp_set_record_handle_pdu(rsp, handle);
    
    return 0;
}

sdp_svcdb_set_collectable()

设置服务的可收集标记(Unix Socket 关闭时自动移除):

cpp 复制代码
// sdpd-database.c:145
void sdp_svcdb_set_collectable(sdp_record_t *record, int sock)
{
    sdp_indexed_t *item = malloc(sizeof(sdp_indexed_t));
    item->sock = sock;
    item->record = record;
    socket_index = sdp_list_insert_sorted(socket_index, item, compare_indices);
}

sdp_svcdb_collect_all()

当 Unix Socket 关闭时,自动移除通过该 Socket 注册的所有服务:

cpp 复制代码
// sdpd-database.c:94
void sdp_svcdb_collect_all(int sock)
{
    sdp_list_t *p, *q;

    for (p = socket_index, q = 0; p; ) {
        sdp_indexed_t *item = p->data;
        if (item->sock == sock) {
            sdp_list_t *next = p->next;
            // 从数据库中移除服务
            sdp_record_remove(item->record->handle);
            sdp_record_free(item->record);
            free(item);
            // 从索引链表中移除
            if (q)
                q->next = next;
            else
                socket_index = next;
            free(p);
            p = next;
        } else {
            q = p;
            p = p->next;
        }
    }
}

六、请求处理实现

6.1 ServiceSearch Request

service_search_req()

service_search_req()处理服务搜索请求:

cpp 复制代码
// sdpd-request.c:390
static int service_search_req(sdp_req_t *req, sdp_buf_t *buf)
{
    sdp_list_t *pattern;       // 请求中的 UUID 搜索模式
    sdp_list_t *matches;       // 匹配的服务句柄列表
    uint16_t max_count;        // 最大返回数量
    uint16_t rsp_count;

    // 1. 解析请求 PDU
    extract_des(req->buf, req->len, &pattern, SDP_TYPE_UUID, SDP_SEQ8);
    
    // 2. 获取最大返回数量
    max_count = get_be16(req->buf + ...);

    // 3. 遍历所有服务记录,匹配搜索模式
    matches = NULL;
    for (rec = service_db; rec; rec = rec->next) {
        sdp_record_t *record = rec->data;
        
        // 检查访问权限
        if (sdp_check_access(record->handle, &req->bdaddr) < 0)
            continue;

        // 检查是否匹配搜索模式
        if (sdp_record_matches_pattern(record, pattern)) {
            matches = sdp_list_append(matches, &record->handle);
        }
        
        // 检查是否达到最大数量
        if (sdp_list_len(matches) >= max_count)
            break;
    }

    // 4. 构造响应 PDU
    rsp_count = sdp_list_len(matches);
    put_be16(rsp_count, pTotalRecordCount);
    put_be16(rsp_count, pCurrentRecordCount);
    
    // 添加匹配的句柄
    for (m = matches; m; m = m->next) {
        uint32_t handle = *((uint32_t *)m->data);
        put_be32(handle, pdata);
        pdata += 4;
    }
    
    // 5. 处理延续状态
    if (还有更多数据) {
        sdp_set_cstate_pdu(buf, &newState);
    } else {
        sdp_set_cstate_pdu(buf, NULL);  // 无延续
    }

    // 6. 释放资源
    sdp_list_free(pattern, free);
    sdp_list_free(matches, NULL);
}

匹配逻辑

  • 搜索模式为空:匹配所有服务

  • 搜索模式不为空:服务的 pattern 列表必须包含所有请求的 UUID

6.2 ServiceAttribute Request

service_attr_req()

service_attr_req()处理属性查询请求:

cpp 复制代码
// sdpd-request.c:700
static int service_attr_req(sdp_req_t *req, sdp_buf_t *buf)
{
    uint32_t handle;
    sdp_list_t *attrids;
    sdp_record_t *record;

    // 1. 解析请求 PDU
    handle = get_be32(req->buf);       // 服务记录句柄
    extract_des(req->buf + 4, ..., &attrids, SDP_TYPE_ATTRID, SDP_SEQ8);

    // 2. 查找服务记录
    record = sdp_record_find(handle);
    if (!record)
        return SDP_INVALID_RECORD_HANDLE;

    // 3. 检查访问权限
    if (sdp_check_access(handle, &req->bdaddr) < 0)
        return SDP_INVALID_RECORD_HANDLE;

    // 4. 提取请求的属性
    if (attrids == NULL) {
        // 请求所有属性
        sdp_gen_record_pdu(record, buf);
    } else {
        // 请求指定属性
        extract_attrs(record, attrids, buf);
    }

    // 5. 处理延续状态
    ...
}

6.3 ServiceSearchAttribute Request

service_search_attr_req()

service_search_attr_req()是 SDP 最常用的操作,结合搜索和属性查询:

cpp 复制代码
// sdpd-request.c:837
static int service_search_attr_req(sdp_req_t *req, sdp_buf_t *buf)
{
    sdp_list_t *pattern;
    sdp_list_t *attrids;
    uint16_t max_count;
    sdp_list_t *matched_records;

    // 1. 解析搜索模式
    extract_des(req->buf, ..., &pattern, SDP_TYPE_UUID, SDP_SEQ8);
    
    // 2. 获取最大返回数量
    max_count = get_be16(req->buf + ...);

    // 3. 解析属性 ID 列表
    extract_des(req->buf + ..., ..., &attrids, SDP_TYPE_ATTRID, SDP_SEQ8);

    // 4. 搜索匹配的服务记录
    matched_records = search_services(pattern, max_count, &req->bdaddr);

    // 5. 为每个匹配的记录生成属性 PDU
    for (rec = matched_records; rec; rec = rec->next) {
        sdp_record_t *record = rec->data;
        
        if (attrids == NULL) {
            // 生成完整记录 PDU
            sdp_gen_record_pdu(record, &pdu);
        } else {
            // 生成指定属性 PDU
            extract_attrs(record, attrids, &pdu);
        }
        
        // 追加到响应缓冲区
        append_pdu(buf, &pdu);
    }

    // 6. 处理延续状态
    ...
}

6.4 延续状态管理

sdp_cont_state_t

SDP 支持延续状态(Continuation State),用于大数据量的分块传输:

cpp 复制代码
// sdpd-request.c:33-39
typedef struct {
    uint32_t timestamp;               // 时间戳(唯一标识)
    union {
        uint16_t maxBytesSent;        // ServiceAttribute 使用
        uint16_t lastIndexSent;       // ServiceSearch 使用
    } cStateValue;
} sdp_cont_state_t;

延续状态处理流程

cpp 复制代码
// sdpd-request.c 中

// 1. 客户端发送带有 continuationState 的请求
// 2. 服务器查找对应的延续状态
cinfo = sdp_get_cont_info(req, cstate);

// 3. 如果找到,从缓存中继续发送数据
if (cinfo) {
    // 检查是否已发送完毕
    if (cstate->cStateValue.maxBytesSent >= cinfo->buf.data_size) {
        status = SDP_INVALID_CSTATE;  // 无效状态
    }
    
    // 从上次位置继续发送
    memcpy(pdata, pCacheBuffer + i * 4, 4);
}

// 4. 如果还有更多数据,设置新的延续状态
if (还有更多数据) {
    newState.timestamp = sdp_get_time();
    newState.cStateValue.lastIndexSent = i;
    sdp_set_cstate_pdu(buf, &newState);
} else {
    sdp_set_cstate_pdu(buf, NULL);  // 无延续
    sdp_cont_info_free(cinfo);
}

七、客户端实现

7.1 会话缓存

cached_sdp_session

sdp-client.c:31实现了 SDP 会话缓存机制:

cpp 复制代码
// sdp-client.c:31-37
struct cached_sdp_session {
    bdaddr_t src;              // 本地设备地址
    bdaddr_t dst;              // 远端设备地址
    sdp_session_t *session;    // SDP 会话
    unsigned int timer;        // 超时定时器
    guint io_id;               // I/O 监听 ID
};

#define CACHE_TIMEOUT 2  // 缓存超时时间(秒)

缓存操作

cpp 复制代码
// sdp-client.c:58-82
static sdp_session_t *get_cached_sdp_session(const bdaddr_t *src, const bdaddr_t *dst)
{
    // 查找匹配的缓存会话
    for (l = cached_sdp_sessions; l; l = l->next) {
        struct cached_sdp_session *c = l->data;
        if (bacmp(&c->src, src) || bacmp(&c->dst, dst))
            continue;

        // 命中缓存,取消超时定时器
        timeout_remove(c->timer);
        g_source_remove(c->io_id);

        // 返回缓存的会话
        session = c->session;
        cached_sdp_sessions = g_slist_remove(cached_sdp_sessions, c);
        g_free(c);
        return session;
    }
    return NULL;
}

7.2 异步服务搜索

search_context

搜索操作的上下文结构:

cpp 复制代码
// sdp-client.c:125-135
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;               // I/O 监听 ID
    gboolean filter_svc_class; // 是否过滤服务类
};

search_completed_cb()

搜索完成回调:

cpp 复制代码
// sdp-client.c:149
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. 解析响应 PDU
    scanned = sdp_extract_seqtype(rsp, bytesleft, &dataType, &seqlen);
    if (!scanned || !seqlen)
        goto done;

    // 3. 提取每个服务记录
    do {
        sdp_record_t *rec;
        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 < size && bytesleft > 0);

    // 5. 缓存会话(供后续使用)
    cache_sdp_session(&ctxt->src, &ctxt->dst, ctxt->session);

    // 6. 调用回调函数返回结果
    if (ctxt->cb)
        ctxt->cb(recs, err, ctxt->user_data);

    // 7. 清理资源
    if (recs)
        sdp_list_free(recs, (sdp_free_func_t) sdp_record_free);
    search_context_cleanup(ctxt);
}

八、PDU 编解码实现

8.1 PDU 编码

sdp_gen_record_pdu()

将服务记录序列化为 PDU:

cpp 复制代码
// lib/sdp.c:903
int sdp_gen_record_pdu(const sdp_record_t *rec, sdp_buf_t *buf)
{
    sdp_data_t *d;

    // 1. 序列化 record handle
    d = sdp_data_alloc(SDP_UINT32, &rec->handle);
    sdp_append_to_pdu(buf, d);
    sdp_data_free(d);

    // 2. 序列化所有属性
    for (d = rec->attrlist; d; d = d->next) {
        if (d->attrId == SDP_ATTR_RECORD_HANDLE)
            continue;  // 跳过,已单独处理
        sdp_append_to_pdu(buf, d);
    }

    return buf->data_size;
}

sdp_append_to_pdu()

将数据元素追加到 PDU 缓冲区:

cpp 复制代码
// lib/sdp.c
int sdp_append_to_pdu(sdp_buf_t *buf, sdp_data_t *d)
{
    // 1. 检查缓冲区空间
    if (buf->data_size + d->unitSize > buf->buf_size)
        return -1;

    // 2. 写入属性 ID(如果有)
    if (d->attrId) {
        put_be16(d->attrId, buf->data + buf->data_size);
        buf->data_size += 2;
    }

    // 3. 写入数据类型描述符
    buf->data[buf->data_size++] = d->dtd;

    // 4. 根据数据类型写入值
    switch (d->dtd) {
    case SDP_UINT8:
    case SDP_INT8:
        buf->data[buf->data_size++] = d->val.uint8;
        break;
    case SDP_UINT16:
    case SDP_INT16:
        put_be16(d->val.uint16, buf->data + buf->data_size);
        buf->data_size += 2;
        break;
    case SDP_UINT32:
    case SDP_INT32:
        put_be32(d->val.uint32, buf->data + buf->data_size);
        buf->data_size += 4;
        break;
    case SDP_UUID16:
        put_be16(d->val.uuid.value.uuid16, buf->data + buf->data_size);
        buf->data_size += 2;
        break;
    case SDP_UUID128:
        memcpy(buf->data + buf->data_size, d->val.uuid.value.uuid128, 16);
        buf->data_size += 16;
        break;
    case SDP_TEXT_STR8:
    case SDP_URL_STR8:
        len = strlen(d->val.str);
        buf->data[buf->data_size++] = len;
        memcpy(buf->data + buf->data_size, d->val.str, len);
        buf->data_size += len;
        break;
    case SDP_SEQ8:
    case SDP_SEQ16:
        // 处理嵌套的数据序列
        sdp_append_seq_to_pdu(buf, d->val.dataseq);
        break;
    }
}

8.2 PDU 解码

sdp_extract_pdu()

从 PDU 缓冲区解析服务记录:

cpp 复制代码
// lib/sdp.c
sdp_record_t *sdp_extract_pdu(const uint8_t *data, int size, int *scanned)
{
    sdp_record_t *rec;
    sdp_data_t *d;
    int pos = 0;

    rec = sdp_record_alloc();

    // 1. 解析 record handle
    d = sdp_data_extract(data, size, &pos);
    if (d && d->dtd == SDP_UINT32) {
        rec->handle = d->val.uint32;
        sdp_data_free(d);
    }

    // 2. 解析所有属性
    while (pos < size) {
        d = sdp_data_extract(data + pos, size - pos, &pos);
        if (!d)
            break;
        sdp_attr_add(rec, d->attrId, d);
    }

    *scanned = pos;
    return rec;
}

九、错误处理与异常机制

9.1 SDP 错误码

cpp 复制代码
// lib/sdp.h:411-415
#define SDP_INVALID_VERSION        0x0001  // 无效版本
#define SDP_INVALID_RECORD_HANDLE  0x0002  // 无效记录句柄
#define SDP_INVALID_SYNTAX         0x0003  // 无效语法
#define SDP_INVALID_PDU_SIZE        0x0004  // 无效 PDU 大小
#define SDP_INVALID_CSTATE         0x0005  // 无效延续状态

9.2 常见错误场景

1. 无效记录句柄

当请求中的服务记录句柄不存在时:

cpp 复制代码
// sdpd-request.c
record = sdp_record_find(handle);
if (!record)
    return SDP_INVALID_RECORD_HANDLE;

2. 访问权限拒绝

当远端设备没有访问特定服务的权限时:

cpp 复制代码
// sdpd-request.c
if (sdp_check_access(handle, &req->bdaddr) < 0)
    return SDP_INVALID_RECORD_HANDLE;

3. 延续状态失效

当延续状态的时间戳无效或数据已被清理时:

cpp 复制代码
// sdpd-request.c
cinfo = sdp_get_cont_info(req, cstate);
if (!cinfo) {
    status = SDP_INVALID_CSTATE;
    goto done;
}

9.3 资源清理

sdp_cstate_cleanup()

清理指定 Socket 的所有延续状态:

cpp 复制代码
// sdpd-request.c
void sdp_cstate_cleanup(int sock)
{
    sdp_list_t *p, *q;

    for (p = cstates, q = 0; p; ) {
        sdp_cont_info_t *cinfo = p->data;
        if (cinfo->sock == sock) {
            sdp_list_t *next = p->next;
            free(cinfo->buf.data);
            free(cinfo);
            if (q)
                q->next = next;
            else
                cstates = next;
            free(p);
            p = next;
        } else {
            q = p;
            p = p->next;
        }
    }
}

十、开发调试要点

10.1 启用 SDP 调试日志

cpp 复制代码
# 编译时启用调试
./configure --enable-debug

# 启动 bluetoothd 并启用调试
bluetoothd -d

10.2 使用 sdptool 工具

BlueZ 提供了 sdptool 命令行工具用于测试 SDP:

cpp 复制代码
# 查看本地服务
sdptool browse local

# 浏览远端设备的服务
sdptool browse 00:11:22:33:44:55

# 搜索特定服务类
sdptool search SPP 00:11:22:33:44:55

# 添加自定义服务(测试用)
sdptool add sp

# 移除服务
sdptool del 0x10000

10.3 使用 btmon 监控 SDP 流量

cpp 复制代码
# 启动 btmon 捕获蓝牙流量
sudo btmon

# 查看 SDP 相关的 L2CAP 流量
sudo btmon | grep -A5 "SDP\|0x0001"

# 保存到文件分析
sudo btmon -w sdp_traffic.btsnoop

10.4 常见问题排查

问题 1:SDP 服务无法被发现

排查步骤

  1. 确认服务已注册:sdptool browse local

  2. 检查服务类是否正确设置

  3. 确认设备的 Discoverable 状态

  4. 查看 syslog 错误:journalctl -t bluetoothd

问题 2:客户端搜索超时

排查步骤

  1. 检查 L2CAP 连接是否建立

  2. 确认远端设备支持 SDP

  3. 检查 MTU 大小:btmgmt info

  4. 使用 btmon 查看流量

问题 3:自定义服务注册失败

排查步骤

  1. 检查 Unix Socket 是否存在:ls -la /var/run/sdp

  2. 确认 bluetoothd 正在运行

  3. 验证服务记录格式

  4. 查看详细错误日志

十一、核心函数索引

11.1 服务端函数

|--------------------------------|---------------------|---------------------------|
| 函数名 | 源文件 | 功能描述 |
| init_server | sdpd-server.c:44 | 初始化 SDP 服务器 |
| handle_request | sdpd-request.c:1118 | 请求分发入口 |
| service_search_req | sdpd-request.c:390 | 处理 ServiceSearch |
| service_attr_req | sdpd-request.c:700 | 处理 ServiceAttribute |
| service_search_attr_req | sdpd-request.c:837 | 处理 ServiceSearchAttribute |
| service_register_req | sdpd-request.c | 处理服务注册 |
| register_server_service | sdpd-service.c:344 | 注册 SDP 服务器服务 |
| register_public_browse_group | sdpd-service.c:315 | 注册公共浏览组 |

11.2 数据库函数

|-------------------------|---------------------|-----------|
| 函数名 | 源文件 | 功能描述 |
| sdp_record_add | sdpd-database.c:162 | 添加服务记录 |
| sdp_record_remove | sdpd-database.c | 移除服务记录 |
| sdp_record_find | sdpd-database.c | 查找服务记录 |
| sdp_next_handle | sdpd-database.c | 分配新句柄 |
| sdp_check_access | sdpd-database.c | 检查访问权限 |
| sdp_svcdb_reset | sdpd-database.c:75 | 重置数据库 |
| sdp_svcdb_collect_all | sdpd-database.c:94 | 收集关闭连接的服务 |

11.3 库层函数

|----------------------|----------------|--------------|
| 函数名 | 源文件 | 功能描述 |
| sdp_record_alloc | lib/sdp.c | 分配服务记录 |
| sdp_record_free | lib/sdp.c | 释放服务记录 |
| sdp_attr_add | lib/sdp.c:573 | 添加属性到记录 |
| sdp_attr_add_new | lib/sdp.c:2242 | 添加新属性 |
| sdp_data_get | lib/sdp.c | 获取属性值 |
| sdp_gen_record_pdu | lib/sdp.c:903 | 生成记录 PDU |
| sdp_extract_pdu | lib/sdp.c | 从 PDU 解析记录 |
| sdp_uuid16_create | lib/sdp.c | 创建 16 位 UUID |

11.4 客户端函数

|-----------------------|------------------|-----------|
| 函数名 | 源文件 | 功能描述 |
| sdp_connect | lib/sdp.c | 建立 SDP 会话 |
| sdp_disconnect | lib/sdp.c | 断开会话 |
| sdp_search | lib/sdp.c | 执行服务搜索 |
| cache_sdp_session | sdp-client.c:95 | 缓存会话 |
| search_completed_cb | sdp-client.c:149 | 搜索完成回调 |

十二、总结

BlueZ SDP 模块完整实现了蓝牙规范中的服务发现协议,其架构设计具有以下特点:

  1. 分层清晰:库层(lib/sdp.c)、服务端(sdpd-*)、客户端(sdp-client.c)职责明确

  2. 双 Socket 设计:L2CAP Socket 处理远端请求,Unix Socket 处理本地注册

  3. 动态扩展:支持动态服务注册/注销,无需重启守护进程

  4. 访问控制:通过 ACL 机制控制服务的可访问性

  5. 会话缓存:客户端实现会话缓存,减少重复连接开销

  6. 延续状态:完整支持大数据量的分块传输机制

理解 SDP 模块的实现,对于蓝牙设备的服务暴露、客户端应用的服务发现以及嵌入式蓝牙开发都具有重要的实践价值。与 BLE GATT 相比,SDP 在经典蓝牙场景下仍是不可替代的服务发现协议。


相关推荐
小白学大数据1 小时前
Python 项目实战:用 Flask 构建生产级 MySQL 增删改查 REST API
开发语言·人工智能·python·mysql·flask
YangYang9YangYan1 小时前
资金分析校招 JD 拆解,现金流建模与数据分析能力怎么准备
大数据·人工智能·数据分析
名不经传的养虾人1 小时前
从0到1:企业级AI项目迭代日记 Vol.110|协作有了预算,延迟有了归因,知识有了保真
大数据·人工智能·机器人·ai编程·企业ai
孟郎郎1 小时前
Windows 环境使用 OpenCode 配置使用大模型
人工智能·windows·ai·大模型·开源软件·opencode
逐米时代1 小时前
实验检测智能管理:LIMS如何让检验任务排程和资质校验自动化
人工智能·机器学习
赴生-1 小时前
Liunx 操作系统 进程控制(上)
linux
TK泰妞1 小时前
2026TikTok带货视频怎么做?
人工智能·prompt·音视频
蓝速科技1 小时前
会议室门牌显示模板选型与品牌视觉适配落地指南丨蓝速科技
大数据·运维·数据库·人工智能·科技