在 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 服务无法被发现
排查步骤:
-
确认服务已注册:
sdptool browse local -
检查服务类是否正确设置
-
确认设备的 Discoverable 状态
-
查看 syslog 错误:
journalctl -t bluetoothd
问题 2:客户端搜索超时
排查步骤:
-
检查 L2CAP 连接是否建立
-
确认远端设备支持 SDP
-
检查 MTU 大小:
btmgmt info -
使用 btmon 查看流量
问题 3:自定义服务注册失败
排查步骤:
-
检查 Unix Socket 是否存在:
ls -la /var/run/sdp -
确认 bluetoothd 正在运行
-
验证服务记录格式
-
查看详细错误日志
十一、核心函数索引
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 模块完整实现了蓝牙规范中的服务发现协议,其架构设计具有以下特点:
分层清晰:库层(lib/sdp.c)、服务端(sdpd-*)、客户端(sdp-client.c)职责明确
双 Socket 设计:L2CAP Socket 处理远端请求,Unix Socket 处理本地注册
动态扩展:支持动态服务注册/注销,无需重启守护进程
访问控制:通过 ACL 机制控制服务的可访问性
会话缓存:客户端实现会话缓存,减少重复连接开销
延续状态:完整支持大数据量的分块传输机制
理解 SDP 模块的实现,对于蓝牙设备的服务暴露、客户端应用的服务发现以及嵌入式蓝牙开发都具有重要的实践价值。与 BLE GATT 相比,SDP 在经典蓝牙场景下仍是不可替代的服务发现协议。