当前位置: 首页 > news >正文

【Bluedroid】蓝牙 SDP(服务发现协议)模块代码解析与流程梳理

本文深入剖析Bluedroid蓝牙协议栈中 SDP(服务发现协议)服务记录的全生命周期管理流程,涵盖初始化、记录创建、服务搜索、记录删除等核心环节。通过解析代码逻辑与数据结构,揭示各模块间的协作机制,包括线程安全设计、回调机制及协议栈交互细节,为蓝牙服务开发与调试提供系统性参考。

一、概述

蓝牙SDP协议用于设备间服务发现,其实现分为四层:

  1. 接口层(btif):提供btif_sdp_get_interface接口,封装initsearchcreate_sdp_record等操作。

  2. 业务逻辑层(BTA):通过BTA_SdpSearchBTA_SdpCreateRecordByUser派发任务到主线程。

  3. 协议栈层(Stack):实现SDP数据库管理(SDP_CreateRecordSDP_AddAttribute)。

  4. 资源管理层:通过槽位(sdp_slots)管理记录内存,支持动态分配与释放。

关键设计:

  • 线程安全:使用std::recursive_mutex保护槽位操作。

  • 内存优化:预计算记录大小实现紧凑存储。

  • 异步模型:通过do_in_main_thread解耦调用与执行。

二、源码解读

2.1 SDP 模块初始化与架构设计

①接口封装:通过btif_sdp_get_interface暴露统一接口结构体sdp_if,包含初始化(init)、反初始化(deinit)、搜索(search)、记录创建(create_sdp_record)和删除(remove_sdp_record)等核心功能,实现模块解耦。

②初始化流程

  • init函数注册回调bt_sdp_callbacks,调用sdp_server_init初始化槽位(sdp_slots),并通过btif_enable_service启用 SDP 服务。

  • sdp_server_init初始化固定大小的槽位数组(MAX_SDP_SLOTS=128),标记所有槽位为空闲(SDP_RECORD_FREE)。

③线程安全:关键操作(如槽位分配、释放)通过std::recursive_mutex加锁,避免多线程竞争。

④deinitinit的反向操作,释放槽位内存并禁用服务。

btif_sdp_get_interface

packages/modules/Bluetooth/system/btif/src/btif_sdp.cc
// 使用初始化列表对 sdp_if 进行初始化
static const btsdp_interface_t sdp_if = {sizeof(btsdp_interface_t), init, deinit, search, create_sdp_record,remove_sdp_record};// 返回定义的 SDP 接口结构体实例的指针。其他模块可以通过调用这个函数来获取 SDP 接口的访问权限
const btsdp_interface_t* btif_sdp_get_interface(void) {log::verbose("");return &sdp_if;
}

定义了一个 SDP 接口结构体实例,封装 SDP 接口的实现,并提供一个统一的接口供其他模块使用。通过 btif_sdp_get_interface 函数,其他模块可以获取到 SDP 接口的指针,从而调用接口中定义的各种功能函数,如初始化、搜索服务、创建和移除 SDP 记录等。

init

packages/modules/Bluetooth/system/btif/src/btif_sdp.cc
static btsdp_callbacks_t* bt_sdp_callbacks = NULL;static bt_status_t init(btsdp_callbacks_t* callbacks) {log::verbose("Sdp Search Init");bt_sdp_callbacks = callbacks;sdp_server_init(); // 对 SDP 服务器进行初始化btif_enable_service(BTA_SDP_SERVICE_ID); // 启用 SDP 服务return BT_STATUS_SUCCESS;
}

对蓝牙 SDP(Service Discovery Protocol,服务发现协议)进行初始化操作。SDP 协议用于在蓝牙设备之间发现可用的服务及其相关信息。

sdp_server_init

packages/modules/Bluetooth/system/btif/src/btif_sdp_server.cc
bt_status_t sdp_server_init() {log::verbose("Sdp Server Init");init_sdp_slots();return BT_STATUS_SUCCESS;
}

对蓝牙 SDP服务器进行初始化操作。调用 init_sdp_slots 函数执行具体的初始化工作,最后返回表示操作成功的状态码。

packages/modules/Bluetooth/system/btif/src/btif_sdp_server.cc
typedef union {bluetooth_sdp_hdr_overlay hdr;bluetooth_sdp_mas_record mas;bluetooth_sdp_mns_record mns;bluetooth_sdp_pse_record pse;bluetooth_sdp_pce_record pce;bluetooth_sdp_ops_record ops;bluetooth_sdp_sap_record sap;bluetooth_sdp_dip_record dip;bluetooth_sdp_mps_record mps;
} bluetooth_sdp_record;typedef struct {sdp_state_t state;int sdp_handle;bluetooth_sdp_record* record_data;
} sdp_slot_t;#define MAX_SDP_SLOTS 128
static sdp_slot_t sdp_slots[MAX_SDP_SLOTS];static void init_sdp_slots() {int i;memset(sdp_slots, 0, sizeof(sdp_slot_t) * MAX_SDP_SLOTS);/* if SDP_RECORD_FREE is zero - no need to set the value */if (SDP_RECORD_FREE != 0) {for (i = 0; i < MAX_SDP_SLOTS; i++) {sdp_slots[i].state = SDP_RECORD_FREE;}}
}

对 SDP 槽位进行初始化,确保所有槽位在使用前都处于空闲状态。

btif_enable_service(BTA_SDP_SERVICE_ID)

packages/modules/Bluetooth/system/btif/src/btif_core.cc
typedef uint32_t tBTA_SERVICE_MASK;static tBTA_SERVICE_MASK btif_enabled_services = 0;/********************************************************************************* Function         btif_enable_service** Description      Enables the service 'service_ID' to the service_mask.*                  Upon BT enable, BTIF core shall invoke the BTA APIs to*                  enable the profiles*******************************************************************************/
void btif_enable_service(tBTA_SERVICE_ID service_id) {btif_enabled_services |= (1 << service_id); // 设置服务掩码,表示该服务已启用log::verbose("current services:0x{:x}", btif_enabled_services);if (btif_is_enabled()) {btif_dm_enable_service(service_id, true); // 向蓝牙协议栈发送指令,实际启用该服务}
}

启用 SDP 服务。

deinit

packages/modules/Bluetooth/system/btif/src/btif_sdp.cc
static bt_status_t deinit() {log::verbose("Sdp Search Deinit");bt_sdp_callbacks = NULL;sdp_server_cleanup();btif_disable_service(BTA_SDP_SERVICE_ID);return BT_STATUS_SUCCESS;
}

清理蓝牙 SDP相关资源并禁用 SDP 服务。包括清空回调函数指针、清理 SDP 服务器和禁用 SDP 服务等操作。

sdp_server_cleanup

packages/modules/Bluetooth/system/btif/src/btif_sdp_server.cc
void sdp_server_cleanup() {log::verbose("Sdp Server Cleanup");std::unique_lock<std::recursive_mutex> lock(sdp_lock);int i;for (i = 0; i < MAX_SDP_SLOTS; i++) {/*remove_sdp_record(i); we cannot send messages to the other threads, since* they might*                       have been shut down already. Just do local cleanup.*/free_sdp_slot(i);}
}

对蓝牙 SDP 服务器进行清理,确保服务器在关闭时释放所有占用的资源。通过加锁操作保证了清理过程的线程安全,避免了多线程环境下的资源竞争问题。同时,由于考虑到其他线程可能已经关闭,只进行本地清理操作。

free_sdp_slot

packages/modules/Bluetooth/system/btif/src/btif_sdp_server.cc
static int free_sdp_slot(int id) {int handle = -1;bluetooth_sdp_record* record = NULL;if (id < 0 || id >= MAX_SDP_SLOTS) {log::error("failed - id {} is invalid", id);return handle;}{std::unique_lock<std::recursive_mutex> lock(sdp_lock);handle = sdp_slots[id].sdp_handle;sdp_slots[id].sdp_handle = 0;if (sdp_slots[id].state != SDP_RECORD_FREE) {/* safe a copy of the pointer, and free after unlock() */record = sdp_slots[id].record_data;}sdp_slots[id].state = SDP_RECORD_FREE;}if (record != NULL) {osi_free(record);} else {// Record have already been freedhandle = -1;}return handle;
}

通过对传入的 ID 进行有效性检查、加锁确保线程安全、清理槽位信息和释放记录数据,实现了对指定 SDP 槽位的释放操作。在释放过程中,遵循了先保存数据再释放内存的原则,避免了在持有锁的情况下进行内存操作,提高了程序的并发性能和健壮性。

2.2 服务搜索流程

①异步搜索发起:search函数调用BTA_SdpSearch,通过do_in_main_thread在主线程启动搜索:

  • 检查当前 SDP 操作状态(sdp_active),避免并发搜索。

  • 初始化发现数据库(SDP_InitDiscoveryDb),设置 UUID 和属性过滤器。

  • 发起 L2CAP 连接(sdp_conn_originate),通过SDP_ServiceSearchAttributeRequest2执行搜索请求。

②连接与安全策略

  • L2CA_ConnectReq2设置安全级别(BTM_SetSecurityLevel),确保连接符合协议规范(如加密需伴随认证)。

  • sdp_conn_originate分配连接控制块(CCB),处理链路状态(已连接 / 断开中),确保连接请求可靠传递。

③结果回调:搜索完成后通过bta_sdp_search_cback触发BTA_SDP_SEARCH_COMP_EVTsdp_dm_cback调用btif_transfer_context深拷贝结果数据,避免跨模块指针引用问题。

search

/packages/modules/Bluetooth/system/btif/src/btif_sdp.cc
static bt_status_t search(RawAddress* bd_addr, const Uuid& uuid) {BTA_SdpSearch(*bd_addr, uuid);return BT_STATUS_SUCCESS;
}

BTA_SdpSearch

packages/modules/Bluetooth/system/bta/sdp/bta_sdp_api.cc
/********************************************************************************* Function         BTA_SdpSearch** Description      This function performs service discovery for a specific*                  service on given peer device. When the operation is*                  completed the tBTA_SDP_DM_CBACK callback function will be*                  called with a BTA_SDP_SEARCH_COMPLETE_EVT.** Returns          BTA_SDP_SUCCESS, if the request is being processed.*                  BTA_SDP_FAILURE, otherwise.*******************************************************************************/
tBTA_SDP_STATUS BTA_SdpSearch(const RawAddress& bd_addr,const bluetooth::Uuid& uuid) {do_in_main_thread(FROM_HERE, base::BindOnce(bta_sdp_search, bd_addr, uuid));return BTA_SDP_SUCCESS;
}

在指定的蓝牙对等设备上执行特定服务的发现操作。启动一个异步的蓝牙 SDP 服务发现操作。将服务发现任务封装在一个一次性的任务中,并在主线程中异步执行。函数立即返回 BTA_SDP_SUCCESS,表示请求正在处理,而实际的操作结果会通过回调函数通知调用者。提高了程序的响应性,避免阻塞主线程。

当服务发现操作完成后,会调用 tBTA_SDP_DM_CBACK 回调函数,并传递 BTA_SDP_SEARCH_COMPLETE_EVT 事件。该函数返回一个 tBTA_SDP_STATUS 类型的值,表示请求是否正在处理。

bta_sdp_search

packages/modules/Bluetooth/system/bta/sdp/bta_sdp_act.cc
/********************************************************************************* Function     bta_sdp_search** Description  Discovers all sdp records for an uuid on remote device** Returns      void*******************************************************************************/
void bta_sdp_search(const RawAddress bd_addr, const bluetooth::Uuid uuid) {tBTA_SDP_STATUS status = BTA_SDP_FAILURE;log::verbose("in, sdp_active:{}", bta_sdp_cb.sdp_active);// 1. 检查 SDP 是否正在进行if (bta_sdp_cb.sdp_active) { // 表示 SDP 操作正在进行/* SDP is still in progress */status = BTA_SDP_BUSY;if (bta_sdp_cb.p_dm_cback) {tBTA_SDP_SEARCH_COMP result;memset(&result, 0, sizeof(result));result.uuid = uuid;result.remote_addr = bd_addr;result.status = status;tBTA_SDP bta_sdp;bta_sdp.sdp_search_comp = result;bta_sdp_cb.p_dm_cback(BTA_SDP_SEARCH_COMP_EVT, &bta_sdp, NULL); // 通知调用者SDP操作忙碌}return;}// 2.  启动 SDP 操作bta_sdp_cb.sdp_active = true; // 表示 SDP 操作开始bta_sdp_cb.remote_addr = bd_addr;/* initialize the search for the uuid */log::verbose("init discovery with UUID: {}", uuid.ToString());get_legacy_stack_sdp_api()->service.SDP_InitDiscoveryDb(p_bta_sdp_cfg->p_sdp_db, p_bta_sdp_cfg->sdp_db_size, 1, &uuid, 0, NULL); // 初始化 SDP 发现数据库// 3. 启动 SDP 服务搜索请求Uuid* bta_sdp_search_uuid = (Uuid*)osi_malloc(sizeof(Uuid));*bta_sdp_search_uuid = uuid;// 发起 SDP 服务搜索属性请求if (!get_legacy_stack_sdp_api()->service.SDP_ServiceSearchAttributeRequest2(bd_addr, p_bta_sdp_cfg->p_sdp_db, bta_sdp_search_cback,(void*)bta_sdp_search_uuid)) {bta_sdp_cb.sdp_active = false; // 表示 SDP 操作结束/* failed to start SDP. report the failure right away */if (bta_sdp_cb.p_dm_cback) {tBTA_SDP_SEARCH_COMP result;memset(&result, 0, sizeof(result));result.uuid = uuid;result.remote_addr = bd_addr;result.status = status;tBTA_SDP bta_sdp;bta_sdp.sdp_search_comp = result;bta_sdp_cb.p_dm_cback(BTA_SDP_SEARCH_COMP_EVT, &bta_sdp, NULL); // 通知调用者SDP操作失败bluetooth::shim::CountCounterMetrics(android::bluetooth::CodePathCounterKeyEnum::SDP_FAILURE, 1);}}/*else report the result when the cback is called*/
}

在远程蓝牙设备上发现指定 UUID 对应的所有 SDP记录。通过检查 SDP 操作状态,避免了同时进行多个 SDP 操作。在 SDP 操作空闲时,会初始化 SDP 发现数据库并发起服务搜索请求。若请求失败,会立即通知回调函数;若请求成功,结果将在回调函数 bta_sdp_search_cback 被调用时处理。

SDP_InitDiscoveryDb
packages/modules/Bluetooth/system/stack/sdp/sdp_api.cc
/********************************************************************************* Function         SDP_InitDiscoveryDb** Description      This function is called to initialize a discovery database.** Parameters:      p_db        - (input) address of an area of memory where the*                                        discovery database is managed.*                  len         - (input) size (in bytes) of the memory*                                 NOTE: This must be larger than*                                       sizeof(tSDP_DISCOVERY_DB)*                  num_uuid    - (input) number of UUID filters applied*                  p_uuid_list - (input) list of UUID filters*                  num_attr    - (input) number of attribute filters applied*                  p_attr_list - (input) list of attribute filters*** Returns          bool*                          true if successful*                          false if one or more parameters are bad*******************************************************************************/
bool SDP_InitDiscoveryDb(tSDP_DISCOVERY_DB* p_db, uint32_t len,uint16_t num_uuid, const Uuid* p_uuid_list,uint16_t num_attr, const uint16_t* p_attr_list) {// 1. 参数验证uint16_t xx;/* verify the parameters */if (p_db == NULL || (sizeof(tSDP_DISCOVERY_DB) > len) ||num_attr > SDP_MAX_ATTR_FILTERS || num_uuid > SDP_MAX_UUID_FILTERS) {log::error("SDP_InitDiscoveryDb Illegal param: p_db {}, len {}, num_uuid {}, ""num_attr {}",fmt::ptr(p_db), len, num_uuid, num_attr);return (false);}// 2. 数据库内存初始化memset(p_db, 0, (size_t)len);p_db->mem_size = len - sizeof(tSDP_DISCOVERY_DB); // 计算数据库可用于存储记录的有效内存大小p_db->mem_free = p_db->mem_size; // 初始化可用内存大小为总有效内存大小p_db->p_first_rec = NULL;p_db->p_free_mem = (uint8_t*)(p_db + 1); // 将指向可用内存起始位置的指针设置为p_db结构体之后的地址// 3. UUID 过滤器设置// 将 p_uuid_list中的 UUID 过滤器依次复制到 p_db->uuid_filters 数组中for (xx = 0; xx < num_uuid; xx++) p_db->uuid_filters[xx] = *p_uuid_list++;p_db->num_uuid_filters = num_uuid; // 记录实际使用的 UUID 过滤器数量// 4. 属性过滤器设置及排序for (xx = 0; xx < num_attr; xx++) p_db->attr_filters[xx] = *p_attr_list++;/* sort attributes */sdpu_sort_attr_list(num_attr, p_db); // 对属性过滤器列表进行排序,以便后续处理p_db->num_attr_filters = num_attr; //记录实际使用的属性过滤器数量return (true);
}

初始化一个用于蓝牙服务发现协议(SDP)的发现数据库。该数据库用于存储和管理在服务发现过程中检索到的信息。通过对传入参数的严格验证和对发现数据库的初始化设置,确保了数据库在使用前处于正确的状态。设置了数据库的内存信息、UUID 过滤器和属性过滤器,并对属性过滤器进行排序,为后续的服务发现操作提供了基础。

SDP_ServiceSearchAttributeRequest2
packages/modules/Bluetooth/system/stack/sdp/sdp_api.cc
/********************************************************************************* Function         SDP_ServiceSearchAttributeRequest2** Description      This function queries an SDP server for information.**                  The difference between this API function and the function*                  SDP_ServiceSearchRequest is that this one does a*                  combined ServiceSearchAttributeRequest SDP function.*                  (This is for Unplug Testing)** Returns          true if discovery started, false if failed.*******************************************************************************/
bool SDP_ServiceSearchAttributeRequest2(const RawAddress& p_bd_addr,tSDP_DISCOVERY_DB* p_db,tSDP_DISC_CMPL_CB2* p_cb2,const void* user_data) {//1. 发起连接tCONN_CB* p_ccb; // 表示连接控制块,用于管理与 SDP 服务器的连接/* Specific BD address */p_ccb = sdp_conn_originate(p_bd_addr); // 尝试发起与指定设备的 SDP 连接if (!p_ccb) return (false);//2. 配置连接控制块p_ccb->disc_state = SDP_DISC_WAIT_CONN; // 表示当前正在等待与 SDP 服务器建立连接p_ccb->p_db = p_db; // 将传入的发现数据库指针赋值给连接控制块,以便后续将服务发现结果存储到该数据库中p_ccb->p_cb2 = p_cb2; // 将传入的回调函数指针赋值给连接控制块,以便在服务发现完成时调用该回调函数p_ccb->is_attr_search = true; // 表示这是一个属性搜索请求p_ccb->user_data = user_data; // 将用户自定义的数据指针赋值给连接控制块,以便在回调函数被调用时传递给它return (true);
}

通过发起与目标蓝牙设备的 SDP 连接,并配置连接控制块的相关信息,为服务发现过程做好准备。若连接发起成功,则返回 true 表示发现过程已启动;若连接发起失败,则返回 false。为后续的服务发现操作奠定了基础。

SDP_ServiceSearchRequest 函数的区别在于,此函数执行的是一个组合的 ServiceSearchAttributeRequest SDP 功能,为了进行拔插测试(Unplug Testing)而设计。函数返回一个布尔值,若成功启动发现过程则返回 true,若失败则返回 false

sdp_conn_originate
packages/modules/Bluetooth/system/stack/sdp/sdp_main.cc
/********************************************************************************* Function         sdp_conn_originate** Description      This function is called from the API to originate a*                  connection.** Returns          void*******************************************************************************/
tCONN_CB* sdp_conn_originate(const RawAddress& p_bd_addr) {// 1. 变量声明tCONN_CB* p_ccb; // 表示连接控制块,存储与连接相关的信息uint16_t cid; // 存储 L2CAP 连接的连接 ID// 2. 分配连接控制块/* Allocate a new CCB. Return if none available. */p_ccb = sdpu_allocate_ccb(); // 尝试分配一个新的连接控制块if (p_ccb == NULL) {log::warn("no spare CCB for peer {}", ADDRESS_TO_LOGGABLE_CSTR(p_bd_addr));return (NULL);}log::verbose("SDP - Originate started for peer {}",ADDRESS_TO_LOGGABLE_CSTR(p_bd_addr));// 3. 检查是否已有活动的 SDP 连接/* Look for any active sdp connection on the remote device */cid = sdpu_get_active_ccb_cid(p_bd_addr);// 4. 设置连接标志和保存设备地址/* We are the originator of this connection */p_ccb->con_flags |= SDP_FLAGS_IS_ORIG; // 表示当前设备是该连接的发起者/* Save the BD Address */p_ccb->device_address = p_bd_addr;// 5. 根据情况发起 L2CAP 连接请求/* Transition to the next appropriate state, waiting for connection confirm */if (!bluetooth::common::init_flags::sdp_serialization_is_enabled() ||cid == 0) {p_ccb->con_state = SDP_STATE_CONN_SETUP; // 表示正在进行连接建立cid = L2CA_ConnectReq2(BT_PSM_SDP, p_bd_addr, BTM_SEC_NONE); // 发起 L2CAP 连接请求} else { // 已有活动的 SDP 连接p_ccb->con_state = SDP_STATE_CONN_PEND; // 表示连接处于等待状态log::warn("SDP already active for peer {}. cid={:#0x}",ADDRESS_TO_LOGGABLE_CSTR(p_bd_addr), cid);}// 6. 检查 L2CAP 连接请求是否成功/* Check if L2CAP started the connection process */if (cid == 0) { // 表示 L2CAP 连接请求失败log::warn("SDP - Originate failed for peer {}",ADDRESS_TO_LOGGABLE_CSTR(p_bd_addr));sdpu_release_ccb(*p_ccb); // 释放之前分配的连接控制块return (NULL);}// 7. 返回连接控制块指针p_ccb->connection_id = cid; // 将连接 ID 保存到连接控制块中return (p_ccb);
}

发起一个到指定蓝牙设备的 SDP连接。从 API 层被调用,会尝试分配连接控制块(CCB),检查是否已有活动的 SDP 连接,然后根据情况发起 L2CAP(Logical Link Control and Adaptation Protocol,逻辑链路控制和适配协议)连接请求。如果连接发起成功,返回指向连接控制块的指针;若失败,则返回 NULL

L2CA_ConnectReq2

packages/modules/Bluetooth/system/stack/l2cap/l2c_api.cc
uint16_t L2CA_ConnectReq2(uint16_t psm, const RawAddress& p_bd_addr,uint16_t sec_level) {get_btm_client_interface().security.BTM_SetSecurityLevel(true, "", 0, sec_level, psm, 0, 0);return L2CA_ConnectReq(psm, p_bd_addr);
}

发起一个 L2CAP连接请求。在发起连接请求之前,先设置目标设备的安全级别,然后调用 L2CA_ConnectReq 函数来实际发起连接。

BTM_SetSecurityLevel
packages/modules/Bluetooth/system/stack/btm/btm_sec.cc
/********************************************************************************* Function         BTM_SetSecurityLevel** Description      Register service security level with Security Manager** Parameters:      is_originator - true if originating the connection*                  p_name      - Name of the service relevant only if*                                authorization will show this name to user.*                                Ignored if BT_MAX_SERVICE_NAME_LEN is 0.*                  service_id  - service ID for the service passed to*                                authorization callback*                  sec_level   - bit mask of the security features*                  psm         - L2CAP PSM*                  mx_proto_id - protocol ID of multiplexing proto below*                  mx_chan_id  - channel ID of multiplexing proto below** Returns          true if registered OK, else false*******************************************************************************/
bool BTM_SetSecurityLevel(bool is_originator, const char* p_name,uint8_t service_id, uint16_t sec_level, uint16_t psm,uint32_t mx_proto_id, uint32_t mx_chan_id) {return btm_sec_cb.AddService(is_originator, p_name, service_id, sec_level,psm, mx_proto_id, mx_chan_id);
}

向蓝牙安全管理器(Security Manager)注册服务的安全级别。通过调用 btm_sec_cb 对象的 AddService 方法,将服务的相关信息(包括发起连接标志、服务名称、服务 ID、安全级别、L2CAP PSM 等)传递给安全管理器,以便在建立连接时应用相应的安全策略。

tBTM_SEC_CB::AddService

packages/modules/Bluetooth/system/stack/btm/btm_sec_cb.cc
#define BTM_NO_AVAIL_SEC_SERVICES ((uint16_t)0xffff)
bool tBTM_SEC_CB::AddService(bool is_originator, const char* p_name,uint8_t service_id, uint16_t sec_level,uint16_t psm, uint32_t mx_proto_id,uint32_t mx_chan_id) {tBTM_SEC_SERV_REC* p_srec;uint16_t index;uint16_t first_unused_record = BTM_NO_AVAIL_SEC_SERVICES;bool record_allocated = false;log::verbose("sec_level:0x{:x}", sec_level);/* See if the record can be reused (same service name, psm, mx_proto_id,service_id, and mx_chan_id), or obtain the next unused record */p_srec = &sec_serv_rec[0];// 1. 记录复用与分配(核心资源管理)for (index = 0; index < BTM_SEC_MAX_SERVICE_RECORDS; index++, p_srec++) {/* Check if there is already a record for this service */if (p_srec->security_flags & BTM_SEC_IN_USE) {// // 检查是否存在相同服务的已有记录(PSM、协议 ID、服务名等匹配)if (p_srec->psm == psm && p_srec->mx_proto_id == mx_proto_id &&service_id == p_srec->service_id && p_name &&/* 匹配发起方或接收方服务名 */(!strncmp(p_name, (char*)p_srec->orig_service_name,/* strlcpy replaces end char with termination char*/BT_MAX_SERVICE_NAME_LEN - 1) ||!strncmp(p_name, (char*)p_srec->term_service_name,/* strlcpy replaces end char with termination char*/BT_MAX_SERVICE_NAME_LEN - 1))) {record_allocated = true;break;}}/* Mark the first available service record */else if (!record_allocated) {// 找到第一个未使用的记录,清零并标记*p_srec = {};record_allocated = true;first_unused_record = index;}}if (!record_allocated) {log::warn("Out of Service Records ({})", BTM_SEC_MAX_SERVICE_RECORDS);return (record_allocated);  // 无可用记录时失败}/* Process the request if service record is valid *//* If a duplicate service wasn't found, use the first available */if (index >= BTM_SEC_MAX_SERVICE_RECORDS) {index = first_unused_record;p_srec = &sec_serv_rec[index];}p_srec->psm = psm;p_srec->service_id = service_id;p_srec->mx_proto_id = mx_proto_id;// 2. 安全级别配置(发起方 vs 接收方)if (is_originator) { // 场景一:连接发起方// 存储发起方通道 ID 和服务名p_srec->orig_mx_chan_id = mx_chan_id;strlcpy((char*)p_srec->orig_service_name, p_name,BT_MAX_SERVICE_NAME_LEN + 1);// 清除接收方相关的旧标志(确保仅保留发起方策略)/* clear out the old setting, just in case it exists */{p_srec->security_flags &=~(BTM_SEC_OUT_ENCRYPT | BTM_SEC_OUT_AUTHENTICATE | BTM_SEC_OUT_MITM);}// 安全模式适配:SP/SC 模式下,认证必须伴随 MITM 保护/* Parameter validation.  Originator should not set requirements for* incoming connections */sec_level &= ~(BTM_SEC_IN_ENCRYPT | BTM_SEC_IN_AUTHENTICATE |BTM_SEC_IN_MITM | BTM_SEC_IN_MIN_16_DIGIT_PIN);if (security_mode == BTM_SEC_MODE_SP || security_mode == BTM_SEC_MODE_SC) {if (sec_level & BTM_SEC_OUT_AUTHENTICATE) sec_level |= BTM_SEC_OUT_MITM;}// 协议强制规则:加密必须开启认证/* Make sure the authenticate bit is set, when encrypt bit is set */if (sec_level & BTM_SEC_OUT_ENCRYPT) sec_level |= BTM_SEC_OUT_AUTHENTICATE;/* outgoing connections usually set the security level right before* the connection is initiated.* set it to be the outgoing service */p_out_serv = p_srec; // 更新全局发起方服务指针} else { // 场景二:连接接收方// 存储接收方通道 ID 和服务名p_srec->term_mx_chan_id = mx_chan_id;strlcpy((char*)p_srec->term_service_name, p_name,BT_MAX_SERVICE_NAME_LEN + 1);// 清除发起方相关的旧标志(确保仅保留接收方策略)/* clear out the old setting, just in case it exists */{p_srec->security_flags &=~(BTM_SEC_IN_ENCRYPT | BTM_SEC_IN_AUTHENTICATE | BTM_SEC_IN_MITM |BTM_SEC_IN_MIN_16_DIGIT_PIN);}// 过滤发起方参数(接收方不应设置发起方规则)/* Parameter validation.  Acceptor should not set requirements for outgoing* connections */sec_level &=~(BTM_SEC_OUT_ENCRYPT | BTM_SEC_OUT_AUTHENTICATE | BTM_SEC_OUT_MITM);// 安全模式适配:逻辑与发起方对称if (security_mode == BTM_SEC_MODE_SP || security_mode == BTM_SEC_MODE_SC) {if (sec_level & BTM_SEC_IN_AUTHENTICATE) sec_level |= BTM_SEC_IN_MITM;}// 协议强制规则:逻辑与发起方对称/* Make sure the authenticate bit is set, when encrypt bit is set */if (sec_level & BTM_SEC_IN_ENCRYPT) sec_level |= BTM_SEC_IN_AUTHENTICATE;}// 3. 最终标志设置与p_srec->security_flags |= (uint16_t)(sec_level | BTM_SEC_IN_USE);log::debug("[{}]: id:{}, is_orig:{} psm:0x{:04x} proto_id:{} chan_id:{}  : ""sec:0x{:x} service_name:[{}] (up to {} chars saved)",index, service_id, logbool(is_originator).c_str(), psm, mx_proto_id,mx_chan_id, p_srec->security_flags, p_name, BT_MAX_SERVICE_NAME_LEN);return (record_allocated);
}

AddService 函数是蓝牙安全管理模块(BTM)的核心方法,用于注册服务的安全策略。其主要职责包括:

  • 管理服务安全记录:维护一个固定大小的安全服务记录数组(sec_serv_rec),支持复用或创建新记录。

  • 设置安全级别:根据是否为连接发起方(is_originator),配置不同的安全标志(加密、认证、MITM 保护等)。

  • 参数校验与策略适配:确保安全级别参数符合蓝牙协议规范(如加密必须伴随认证),并根据安全模式(SP/SC)调整策略。

L2CA_ConnectReq

packages/modules/Bluetooth/system/stack/l2cap/l2c_api.cc
/********************************************************************************* Function         L2CA_ConnectReq** Description      Higher layers call this function to create an L2CAP*                  connection.*                  Note that the connection is not established at this time,*                  but connection establishment gets started. The callback*                  will be invoked when connection establishes or fails.** Returns          the CID of the connection, or 0 if it failed to start*******************************************************************************/
uint16_t L2CA_ConnectReq(uint16_t psm, const RawAddress& p_bd_addr) {log::verbose("BDA {} PSM: 0x{:04x}", ADDRESS_TO_LOGGABLE_STR(p_bd_addr), psm);// 1. 初始化与参数校验/* Fail if we have not established communications with the controller */if (!BTM_IsDeviceUp()) {log::warn("BTU not ready");return 0; // 蓝牙未启动,连接失败}/* Fail if the PSM is not registered */tL2C_RCB* p_rcb = l2cu_find_rcb_by_psm(psm);if (p_rcb == nullptr) {log::warn("no RCB, PSM={}", loghex(psm));return 0; // 目标 PSM 未注册,服务不可用}// 2. 链路控制块(LCB)管理/* First, see if we already have a link to the remote *//* assume all ERTM l2cap connection is going over BR/EDR for now */tL2C_LCB* p_lcb = l2cu_find_lcb_by_bd_addr(p_bd_addr, BT_TRANSPORT_BR_EDR);if (p_lcb == nullptr) {// 无链路连接,创建新的 LCB 并启动链路建立流程/* No link. Get an LCB and start link establishment */p_lcb = l2cu_allocate_lcb(p_bd_addr, false, BT_TRANSPORT_BR_EDR);/* currently use BR/EDR for ERTM mode l2cap connection */if (p_lcb == nullptr) {log::warn("connection not started for PSM={}, p_lcb={}", loghex(psm),fmt::ptr(p_lcb));return 0; // 分配 LCB 失败}l2cu_create_conn_br_edr(p_lcb); // 发起 BR/EDR 链路连接}// 3. 通道控制块(CCB)分配与配置/* Allocate a channel control block */tL2C_CCB* p_ccb = l2cu_allocate_ccb(p_lcb, 0);if (p_ccb == nullptr) {log::warn("no CCB, PSM={}", loghex(psm));return 0;}/* Save registration info */p_ccb->p_rcb = p_rcb; // 关联目标服务的 RCBp_ccb->connection_initiator = L2CAP_INITIATOR_LOCAL; // 标记为本地发起连接// 4. 根据链路状态触发连接流程/* If link is up, start the L2CAP connection */if (p_lcb->link_state == LST_CONNECTED) {// 链路已连接,直接触发 L2CAP 连接请求l2c_csm_execute(p_ccb, L2CEVT_L2CA_CONNECT_REQ, nullptr);} else if (p_lcb->link_state == LST_DISCONNECTING) {/* If link is disconnecting, save link info to retry after disconnect* Possible Race condition when a reconnect occurs* on the channel during a disconnect of link. This* ccb will be automatically retried after link disconnect* arrives*/log::verbose("L2CAP API - link disconnecting: RETRY LATER");// 链路正在断开,缓存 CCB 以便断开后重试/* Save ccb so it can be started after disconnect is finished */p_lcb->p_pending_ccb = p_ccb;}log::verbose("L2CAP - L2CA_conn_req(psm: 0x{:04x}) returned CID: 0x{:04x}",psm, p_ccb->local_cid);/* Return the local CID as our handle *///无论链路是否立即建立,只要请求成功发起,即返回分配的本地 CID(非零值表示成功,0 表示失败)。后续通过 CID 管理通道状态,连接结果通过回调通知上层return p_ccb->local_cid;
}

发起 L2CAP 连接请求。其主要流程包括:

  • 校验蓝牙控制器状态:确保蓝牙已启动且可用。

  • 查找协议服务复用器(PSM)对应的注册控制块(RCB):验证目标服务是否已注册。

  • 管理链路控制块(LCB):处理与目标设备的链路连接(若未建立,则创建并启动链路建立流程)。

  • 分配通道控制块(CCB):管理 L2CAP 通道的生命周期。

  • 触发连接状态机:根据链路状态(已连接 / 断开中)执行相应操作,或缓存待处理的连接请求。

  • 链路已连接:若链路状态为 LST_CONNECTED,调用 l2c_csm_execute 触发状态机处理 L2CEVT_L2CA_CONNECT_REQ 事件,发送 L2CAP Connect Request PDU

结果回调:bta_sdp_search_cback(BTA_SDP_SEARCH_COMP_EVT

packages/modules/Bluetooth/system/bta/sdp/bta_sdp_act.cc
/** Callback from btm after search is completed */
static void bta_sdp_search_cback(UNUSED_ATTR const RawAddress& bd_addr,tSDP_RESULT result, const void* user_data) {// 1. 局部变量初始化tBTA_SDP_STATUS status = BTA_SDP_FAILURE;int count = 0;log::verbose("res: 0x{:x}", result);bta_sdp_cb.sdp_active = false;// 2. 检查回调函数指针if (bta_sdp_cb.p_dm_cback == NULL) return;// 3. 提取 UUIDUuid& uuid = *(reinterpret_cast<Uuid*>(const_cast<void*>(user_data)));// 4. 初始化事件数据结构tBTA_SDP_SEARCH_COMP evt_data;memset(&evt_data, 0, sizeof(evt_data));evt_data.remote_addr = bta_sdp_cb.remote_addr;evt_data.uuid = uuid;// 5. 处理搜索成功或数据库满的情况if (result == SDP_SUCCESS || result == SDP_DB_FULL) {tSDP_DISC_REC* p_rec = NULL;do {p_rec = get_legacy_stack_sdp_api()->db.SDP_FindServiceUUIDInDb(p_bta_sdp_cfg->p_sdp_db, uuid, p_rec);/* generate the matching record data pointer */if (!p_rec) {log::verbose("UUID not found");continue;}// 根据不同的 UUID 生成相应的 SDP 记录status = BTA_SDP_SUCCESS;if (uuid == UUID_MAP_MAS) {log::verbose("found MAP (MAS) uuid");bta_create_mas_sdp_record(&evt_data.records[count], p_rec);} else if (uuid == UUID_MAP_MNS) {log::verbose("found MAP (MNS) uuid");bta_create_mns_sdp_record(&evt_data.records[count], p_rec);} else if (uuid == UUID_PBAP_PSE) {log::verbose("found PBAP (PSE) uuid");bta_create_pse_sdp_record(&evt_data.records[count], p_rec);} else if (uuid == UUID_OBEX_OBJECT_PUSH) {log::verbose("found Object Push Server (OPS) uuid");bta_create_ops_sdp_record(&evt_data.records[count], p_rec);} else if (uuid == UUID_SAP) {log::verbose("found SAP uuid");bta_create_sap_sdp_record(&evt_data.records[count], p_rec);} else if (uuid == UUID_PBAP_PCE) { // 处理 PBAP PCE UUID 特殊情况log::verbose("found PBAP (PCE) uuid");if (p_rec != NULL) {uint16_t peer_pce_version = 0;get_legacy_stack_sdp_api()->record.SDP_FindProfileVersionInRec(p_rec, UUID_SERVCLASS_PHONE_ACCESS, &peer_pce_version);if (peer_pce_version != 0) {btif_storage_set_pce_profile_version(p_rec->remote_bd_addr,peer_pce_version);}} else {log::verbose("PCE Record is null");}} else if (uuid == UUID_DIP) {log::verbose("found DIP uuid");bta_create_dip_sdp_record(&evt_data.records[count], p_rec);} else {/* we do not have specific structure for this */log::verbose("profile not identified. using raw data");bta_create_raw_sdp_record(&evt_data.records[count], p_rec);p_rec = NULL;  // Terminate loop/* For raw, we only extract the first entry, and then return theentire raw data chunk.TODO: Find a way to split the raw data into record chunks, anditerate to extract generic data for each chunk - e.g. rfcommchannel and service name. */}count++;} while (p_rec != NULL && count < BTA_SDP_MAX_RECORDS);evt_data.record_count = count;}// 6. 完成事件数据结构设置evt_data.status = status;tBTA_SDP bta_sdp;bta_sdp.sdp_search_comp = evt_data;// 调用回调函数 bta_sdp_cb.p_dm_cback 通知搜索完成bta_sdp_cb.p_dm_cback(BTA_SDP_SEARCH_COMP_EVT, &bta_sdp, (void*)&uuid);bluetooth::shim::CountCounterMetrics(android::bluetooth::CodePathCounterKeyEnum::SDP_SUCCESS, 1);osi_free(const_cast<void*>(user_data));  // We no longer need the user data to track the search
}

bta_sdp_search_cback 是一个回调函数,在 SDP搜索完成后由 btm 调用。主要任务是处理搜索结果,根据搜索结果生成相应的 SDP 记录,并将处理后的结果通过回调函数反馈给调用者,同时记录搜索成功的指标数据。

sdp_dm_cback(BTA_SDP_SEARCH_COMP_EVT)
packages/modules/Bluetooth/system/btif/src/btif_sdp.cc
static void sdp_dm_cback(tBTA_SDP_EVT event, tBTA_SDP* p_data,void* user_data) {switch (event) {case BTA_SDP_SEARCH_COMP_EVT: {int size = sizeof(tBTA_SDP);// 1. 计算所需内存大小size += get_sdp_records_size(p_data->sdp_search_comp.records,p_data->sdp_search_comp.record_count);// 2. 深度复制记录内容/* need to deep copy the record content */btif_transfer_context(btif_sdp_search_comp_evt, event, (char*)p_data,size, sdp_search_comp_copy_cb);break;}case BTA_SDP_CREATE_RECORD_USER_EVT: {on_create_record_event(PTR_TO_INT(user_data));break;}case BTA_SDP_REMOVE_RECORD_USER_EVT: {on_remove_record_event(PTR_TO_INT(user_data));break;}default:break;}
}

sdp_dm_cback 是一个回调函数,用于处理蓝牙 SDP相关的事件。它根据不同的事件类型执行不同的操作,这里主要分析 BTA_SDP_SEARCH_COMP_EVT 事件。当接收到 BTA_SDP_SEARCH_COMP_EVT 事件时,首先计算存储 SDP 搜索结果所需的总内存大小,然后调用 btif_transfer_context 函数进行上下文转移和数据的深度复制。目的是为了在不同的线程或者上下文环境中安全地处理 SDP 搜索结果,避免数据在传递过程中被意外修改或者丢失。

btif_sdp_search_comp_evt
/packages/modules/Bluetooth/system/btif/src/btif_sdp.cc
static void btif_sdp_search_comp_evt(uint16_t event, char* p_param) {tBTA_SDP_SEARCH_COMP* evt_data = (tBTA_SDP_SEARCH_COMP*)p_param; // 提取事件数据log::verbose("event = {}", event);if (event != BTA_SDP_SEARCH_COMP_EVT) return;HAL_CBACK(bt_sdp_callbacks, sdp_search_cb, (bt_status_t)evt_data->status,evt_data->remote_addr, evt_data->uuid, evt_data->record_count,evt_data->records);
}

处理蓝牙 SDP搜索完成事件。当接收到 SDP 搜索完成的通知时,该函数会对事件进行检查,并将搜索结果通过回调函数传递给上层模块。以便上层模块根据搜索结果进行相应的处理,如显示搜索到的服务信息、连接到特定的服务等。

2.3 SDP 记录创建流程 

①上层接口调用:create_sdp_record触发以下步骤:

  • 调用alloc_sdp_slot分配空闲槽位,计算记录内存需求(含动态数据)并深拷贝数据。

  • 通过BTA_SdpCreateRecordByUser将创建请求派发到主线程,触发bta_sdp_create_record回调。

②协议栈处理

  • 主线程通过sdp_dm_cback分发BTA_SDP_CREATE_RECORD_USER_EVT事件,调用on_create_record_event

  • 根据记录类型(如SDP_TYPE_PBAP_PSE)调用具体创建函数(如add_pbaps_sdp),最终通过SDP_CreateRecord在底层数据库创建记录,并调用SDP_AddAttribute添加属性。

③内存管理

  • copy_sdp_records实现动态数据深拷贝,确保结构体指针指向独立内存。

  • SDP_AddAttributeToRecord管理属性存储,按 ID 排序并校验内存空间。

create_sdp_record


bt_status_t create_sdp_record(bluetooth_sdp_record* record,int* record_handle) {int handle;// 1. 分配 SDP 槽位handle = alloc_sdp_slot(record);log::verbose("handle = 0x{:08x}", handle);if (handle < 0) return BT_STATUS_FAIL;// 2. 通知协议栈创建记录BTA_SdpCreateRecordByUser(INT_TO_PTR(handle));*record_handle = handle;return BT_STATUS_SUCCESS;
}

在蓝牙 SDP服务器中创建一个新的服务记录。通过以下步骤完成记录创建:

  • 分配 SDP 槽位:从预定义的槽位数组中获取一个空闲槽位,用于存储服务记录数据。

  • 通知底层协议栈:通过 BTA_SdpCreateRecordByUser 函数告知蓝牙协议栈创建服务记录,并关联槽位句柄。

  • 返回句柄:将新创建记录的句柄返回给调用者,以便后续操作(如更新、删除记录)。

alloc_sdp_slot

/packages/modules/Bluetooth/system/btif/src/btif_sdp_server.cc
/* Reserve a slot in sdp_slots, copy data and set a reference to the copy.* The record_data will contain both the record and any data pointed to by* the record.* Currently this covers:*   service_name string,*   user1_ptr and*   user2_ptr. */
static int alloc_sdp_slot(bluetooth_sdp_record* in_record) {// 1. 计算记录内存大小int record_size = get_sdp_records_size(in_record, 1);// 2. 预分配内存并复制数据/* We are optimists here, and preallocate the record.* This is to reduce the time we hold the sdp_lock. */bluetooth_sdp_record* record = (bluetooth_sdp_record*)osi_malloc(record_size);copy_sdp_records(in_record, record, 1);// 3. 加锁分配槽位{std::unique_lock<std::recursive_mutex> lock(sdp_lock);  // 自动加锁(作用域内有效)for (int i = 0; i < MAX_SDP_SLOTS; i++) {if (sdp_slots[i].state == SDP_RECORD_FREE) {sdp_slots[i].state = SDP_RECORD_ALLOCED;sdp_slots[i].record_data = record; // 绑定记录数据到槽位return i; // 返回槽位索引作为句柄}}} // 作用域结束,锁自动释放// 4. 分配失败处理log::error("failed - no more free slots!");/* Rearly the optimist is too optimistic, and cleanup is needed...*/osi_free(record);return -1;
}

在 SDP 服务器的槽位数组(sdp_slots)中分配一个空闲槽位,用于存储蓝牙服务记录。其主要流程包括:

  • 计算记录内存需求:根据输入的服务记录数据,计算所需的内存大小(包括记录本身和指向的数据)。

  • 预分配内存:提前分配内存并复制记录数据,减少锁的持有时间,提升并发性能。

  • 槽位分配:通过加锁遍历槽位数组,找到空闲槽位并绑定记录数据。

  • 错误处理:若分配失败,释放预分配的内存并返回错误。

get_sdp_records_size

packages/modules/Bluetooth/system/btif/src/btif_sdp_server.cc
int get_sdp_records_size(bluetooth_sdp_record* in_record, int count) {bluetooth_sdp_record* record = in_record; // 指向 SDP 记录数组的指针,用于获取每条记录的动态数据长度信息// 1. 初始化总大小int records_size = 0;// 2. 遍历每条记录计算内存int i;for (i = 0; i < count; i++) {record = &in_record[i];records_size += sizeof(bluetooth_sdp_record); // 结构体自身大小// 3. 服务名称字符串内存records_size += record->hdr.service_name_length; // 字符串内容长度if (record->hdr.service_name_length > 0) {records_size++; /* + '\0' termination of string */}// 4. 用户自定义数据内存records_size += record->hdr.user1_ptr_len; // user1_ptr 指向的数据长度records_size += record->hdr.user2_ptr_len; // user2_ptr 指向的数据长度}return records_size;
}

计算指定数量的 SDP 记录及其关联动态数据的总内存需求,用于提前分配内存以完成深拷贝。具体计算内容包括:

  • 结构体自身大小:每个 bluetooth_sdp_record 结构体的固定内存占用。

  • 动态数据大小:结构体中动态分配的字符串(服务名称)和用户自定义数据(user1_ptruser2_ptr)的实际数据长度。

  • 额外开销:服务名称字符串的终止符 \0 占用的额外字节。

copy_sdp_records

packages/modules/Bluetooth/system/btif/src/btif_sdp.cc
/* Deep copy all content of in_records into out_records.* out_records must point to a chunk of memory large enough to contain all* the data. Use getSdpRecordsSize() to calculate the needed size. */
void copy_sdp_records(bluetooth_sdp_record* in_records,bluetooth_sdp_record* out_records, int count) {int i;bluetooth_sdp_record* in_record;bluetooth_sdp_record* out_record;// 1. 内存布局初始化// 从目标数组末尾开始向前分配空间,确保记录结构体和动态数据在内存中连续存储char* free_ptr =(char*)(&out_records[count]); /* set pointer to after the last entry */// 2. 逐记录复制for (i = 0; i < count; i++) {in_record = &in_records[i];out_record = &out_records[i];*out_record = *in_record; // 浅拷贝结构体成员// 3. 服务名称深拷贝if (in_record->hdr.service_name == NULL ||in_record->hdr.service_name_length == 0) {out_record->hdr.service_name = NULL;out_record->hdr.service_name_length = 0;} else {// 修正目标指针指向新内存out_record->hdr.service_name = free_ptr;  // Update service_name pointer// Copy stringmemcpy(free_ptr, in_record->hdr.service_name,in_record->hdr.service_name_length);free_ptr += in_record->hdr.service_name_length; // 指针后移*(free_ptr) = '\0';  // Set '\0' termination of stringfree_ptr++; // 跳过终止符位置}// 4. 用户自定义数据深拷贝// 处理 user1_ptrif (in_record->hdr.user1_ptr != NULL) {out_record->hdr.user1_ptr = (uint8_t*)free_ptr;  // Update pointer // 修正目标指针memcpy(free_ptr, in_record->hdr.user1_ptr,in_record->hdr.user1_ptr_len);  // Copy contentfree_ptr += in_record->hdr.user1_ptr_len;}// 处理 user2_ptr(逻辑与 user1_ptr 一致)if (in_record->hdr.user2_ptr != NULL) {out_record->hdr.user2_ptr = (uint8_t*)free_ptr;  // Update pointermemcpy(free_ptr, in_record->hdr.user2_ptr,in_record->hdr.user2_ptr_len);  // Copy contentfree_ptr += in_record->hdr.user2_ptr_len;}}return;
}

将源 SDP 记录数据深拷贝到目标内存区域,确保目标数据独立于源数据,避免指针引用导致的内存问题。具体功能包括:

  1. 结构体成员浅拷贝:复制 bluetooth_sdp_record 结构体的基本成员(如服务 UUID、属性列表指针等)。

  2. 动态数据深拷贝:对结构体中指向动态分配内存的指针(如服务名称字符串、user1_ptruser2_ptr)进行内存复制,创建独立副本。

  3. 连续内存管理:将所有复制后的数据存储在目标内存的连续区域,通过指针偏移计算确保内存布局正确。

BTA_SdpCreateRecordByUser

packages/modules/Bluetooth/system/bta/sdp/bta_sdp_api.cc
/********************************************************************************* Function         BTA_SdpCreateRecordByUser** Description      This function is used to request a callback to create a SDP*                  record. The registered callback will be called with event*                  BTA_SDP_CREATE_RECORD_USER_EVT.** Returns          BTA_SDP_SUCCESS, if the request is being processed.*                  BTA_SDP_FAILURE, otherwise.*******************************************************************************/
tBTA_SDP_STATUS BTA_SdpCreateRecordByUser(void* user_data) {do_in_main_thread(FROM_HERE,base::BindOnce(bta_sdp_create_record, user_data));return BTA_SDP_SUCCESS;
}

请求创建一个 SDP(服务发现协议)记录。通过向主线程发送一个任务,让主线程调用 bta_sdp_create_record 函数来完成 SDP 记录的创建操作。该函数会触发之前注册的回调函数,回调事件为 BTA_SDP_CREATE_RECORD_USER_EVT

bta_sdp_record
packages/modules/Bluetooth/system/bta/sdp/bta_sdp_act.cc
/********************************************************************************* Function     bta_sdp_record** Description  Discovers all sdp records for an uuid on remote device** Returns      void*******************************************************************************/
void bta_sdp_create_record(void* user_data) {if (bta_sdp_cb.p_dm_cback)bta_sdp_cb.p_dm_cback(BTA_SDP_CREATE_RECORD_USER_EVT, NULL, user_data);
}

蓝牙协议栈中 SDP模块的核心回调函数,用于触发用户自定义的 SDP 记录创建流程。主要作用是将创建记录的请求传递给上层模块(如设备管理模块),通过预先注册的回调接口完成记录的实际创建或配置。

sdp_dm_cback(BTA_SDP_CREATE_RECORD_USER_EVT)
/packages/modules/Bluetooth/system/btif/src/btif_sdp.cc
static void sdp_dm_cback(tBTA_SDP_EVT event, tBTA_SDP* p_data,void* user_data) {switch (event) {case BTA_SDP_SEARCH_COMP_EVT: {int size = sizeof(tBTA_SDP);size += get_sdp_records_size(p_data->sdp_search_comp.records,p_data->sdp_search_comp.record_count);/* need to deep copy the record content */btif_transfer_context(btif_sdp_search_comp_evt, event, (char*)p_data,size, sdp_search_comp_copy_cb);break;}case BTA_SDP_CREATE_RECORD_USER_EVT: {on_create_record_event(PTR_TO_INT(user_data)); // 转换句柄并触发上层回调break;}case BTA_SDP_REMOVE_RECORD_USER_EVT: {on_remove_record_event(PTR_TO_INT(user_data));break;}default:break;}
}

sdp_dm_cback 是蓝牙 SDP 模块与设备管理模块(DM)之间的回调函数,用于处理 SDP 相关事件。通过匹配不同的事件类型(如服务搜索完成、创建记录请求等),调用相应的处理函数,实现跨模块的事件驱动逻辑。其核心职责包括:

  • 事件分发:根据事件类型(tBTA_SDP_EVT)执行不同的处理分支。

  • 数据处理:对服务搜索完成事件(BTA_SDP_SEARCH_COMP_EVT)进行内存深拷贝,确保数据跨模块安全传递。

  • 用户回调触发:将创建 / 删除记录的事件传递给上层业务逻辑(如 on_create_record_event)。

on_create_record_event
packages/modules/Bluetooth/system/btif/src/btif_sdp_server.cc
/******************************************************************************* CALLBACK FUNCTIONS* Called in BTA context to create/remove SDP records.******************************************************************************/
typedef enum {SDP_TYPE_RAW,         // Used to carry raw SDP search data for unknown UUIDsSDP_TYPE_MAP_MAS,     // Message Access Profile - ServerSDP_TYPE_MAP_MNS,     // Message Access Profile - Client (Notification Server)SDP_TYPE_PBAP_PSE,    // Phone Book Profile - ServerSDP_TYPE_PBAP_PCE,    // Phone Book Profile - ClientSDP_TYPE_OPP_SERVER,  // Object Push ProfileSDP_TYPE_SAP_SERVER,  // SIM Access ProfileSDP_TYPE_DIP,         // Device Identification ProfileSDP_TYPE_MPS          // Multi-Profile Specification
} bluetooth_sdp_types;void on_create_record_event(int id) {/** 1) Fetch the record pointer, and change its state?* 2) switch on the type to create the correct record* 3) Update state on completion* 4) What to do at fail?* */// 1. 函数参数和初始化log::verbose("Sdp Server");const sdp_slot_t* sdp_slot = start_create_sdp(id); // 获取指定 ID 对应的 SDP 槽位信息tBTA_SERVICE_ID service_id = -1;bluetooth_sdp_record* record; // 用于指向 SDP 记录数据// 2. 检查记录是否有效/* In the case we are shutting down, sdp_slot is NULL */if (sdp_slot != nullptr && (record = sdp_slot->record_data) != nullptr) {int handle = -1;// 3. 根据记录类型创建 SDP 记录switch (record->hdr.type) {case SDP_TYPE_MAP_MAS:handle = add_maps_sdp(&record->mas);service_id = BTA_MAP_SERVICE_ID;break;case SDP_TYPE_MAP_MNS:handle = add_mapc_sdp(&record->mns);service_id = BTA_MN_SERVICE_ID;break;case SDP_TYPE_PBAP_PSE:handle = add_pbaps_sdp(&record->pse);service_id = BTA_PBAP_SERVICE_ID;break;case SDP_TYPE_OPP_SERVER:handle = add_opps_sdp(&record->ops);break;case SDP_TYPE_SAP_SERVER:handle = add_saps_sdp(&record->sap);break;case SDP_TYPE_PBAP_PCE:handle = add_pbapc_sdp(&record->pce);service_id = BTA_PCE_SERVICE_ID;break;case SDP_TYPE_MPS:handle = add_mps_sdp(&record->mps);break;case SDP_TYPE_RAW:if (record->hdr.rfcomm_channel_number > 0) {handle = add_rfc_sdp_rec(record->hdr.service_name, record->hdr.uuid,record->hdr.rfcomm_channel_number);}break;default:log::verbose("Record type {} is not supported", record->hdr.type);break;}// 4. 处理记录创建成功的情况if (handle != -1) {set_sdp_handle(id, handle); // 将创建成功的记录句柄与记录 ID 关联起来if (service_id > 0) {/*** {@link btif_enable_service} calls {@link btif_dm_enable_service}, which calls {@link* btif_in_execute_service_request}.*     - {@link btif_enable_service} btif_enable_servicesets the mask {@link btif_enabled_services}.*     - {@link btif_dm_enable_service} invokes the java callback to return uuids based*       on the enabled services mask.*     - {@link btif_in_execute_service_request} gates the java callback in {@link*       btif_dm_enable_service}.*/btif_enable_service(service_id); // 启用该服务}}}
}

on_create_record_event 函数是一个回调函数,在 BTA(蓝牙应用层)上下文环境中被调用,用于创建 SDP(服务发现协议)记录。它根据传入的记录 ID,获取对应的 SDP 记录信息,依据记录的类型调用不同的函数来创建具体的 SDP 记录,并在创建成功后更新相关状态和启用对应的服务。

这里以SDP_TYPE_RAW记录类型为例进行分析。SDP_TYPE_RAW用于携带未知通用唯一识别码(UUID)的原始 SDP 搜索数据。在蓝牙服务发现过程中,可能会遇到一些未识别的 UUID,此时可以使用 SDP_TYPE_RAW 类型来存储和处理这些原始数据,以便后续进一步分析或调试。

add_rfc_sdp_rec

/packages/modules/Bluetooth/system/btif/src/btif_sock_sdp.cc
// Adds an SDP record to the SDP database using the given |name|, |uuid|, and
// |channel|. Note that if the |uuid| is empty, the |uuid| will be set based
// upon the |channel| passed in.
int add_rfc_sdp_rec(const char* name, Uuid uuid, const int channel) {if (uuid.IsEmpty()) {switch (channel) {case RESERVED_SCN_PBS:  // PBAP Reserved portuuid = UUID_PBAP_PSE;break;case RESERVED_SCN_OPS:uuid = UUID_OBEX_OBJECT_PUSH;break;default:uuid = UUID_SPP;break;}}return add_rfc_sdp_by_uuid(name, uuid, channel);
}

向 SDP数据库中添加一条 SDP 记录。接收服务名称(name)、服务的 UUID(uuid)以及 RFCOMM 通道号(channel作为参数。若传入的 UUID 为空,根据通道号为其设置合适的 UUID,最后调用 add_rfc_sdp_by_uuid 函数完成 SDP 记录的添加操作。

add_rfc_sdp_by_uuid

packages/modules/Bluetooth/system/btif/src/btif_sock_sdp.cc
// Adds an RFCOMM SDP record for a service with the given |name|, |uuid|, and
// |channel|. This function attempts to identify the type of the service based
// upon its |uuid|, and will override the |channel| with a reserved channel
// number if the |uuid| matches one of the preregistered bluez SDP records.
static int add_rfc_sdp_by_uuid(const char* name, const Uuid& uuid,const int channel) {log::verbose("uuid: {}, service_name: {}, channel: {}", uuid.ToString(), name,channel);/** Bluetooth Socket API relies on having preregistered bluez sdp records for* HSAG, HFAG, OPP & PBAP that are mapped to rc chan 10, 11,12 & 19. Today* HSAG and HFAG is routed to BRCM AG and are not using BT socket API so for* now we will need to support OPP and PBAP to enable 3rd party developer apps* running on BRCM Android.** To do this we will check the UUID for the requested service and mimic the* SDP records of bluez upon reception.  See functions add_opush() and* add_pbap() in sdptool.c for actual records.*/// 1. 确定最终通道号int final_channel = get_reserved_rfc_channel(uuid);if (final_channel == -1) { // 表示没有为该 uuid找到保留的通道号final_channel = channel;}// 2. 根据uuid调用不同的添加记录函数int handle = 0;if (uuid == UUID_OBEX_OBJECT_PUSH) {handle = add_ops_sdp(name, final_channel);} else if (uuid == UUID_PBAP_PSE) {// PBAP Server is always channel 19handle = add_pbap_sdp(name, final_channel);} else if (uuid == UUID_SPP) {handle = add_spp_sdp(name, final_channel);} else if (uuid == UUID_MAP_MAS) {// Record created by new SDP create record interfacehandle = 0xff;} else {handle = add_sdp_by_uuid(name, uuid, final_channel);}return handle;
}

为具有指定名称(name)、通用唯一识别码(uuid)和通道号(channel)的服务添加一个 RFCOMM(Radio Frequency Communication,射频通信)的 SDP记录。根据 uuid 识别服务类型,若 uuid 匹配预注册的 BlueZ SDP 记录,会用保留的通道号覆盖传入的 channel,最后根据不同的 uuid 调用相应的函数来添加 SDP 记录,并返回记录的句柄。

这里主要分析调用 add_sdp_by_uuid 函数添加通用的 SDP 记录的情况。

add_sdp_by_uuid
packages/modules/Bluetooth/system/btif/src/btif_sock_sdp.cc
// Registers a service with the given |name|, |uuid|, and |channel| in the SDP
// database as a generic L2CAP RFCOMM protocol, storing its |uuid| as a service
// class sequence.
static int add_sdp_by_uuid(const char* name, const Uuid& uuid,const uint16_t channel) {log::verbose("uuid: {}, scn: {}, service_name: {}", uuid.ToString(), channel,name);// 1. 记录创建uint32_t handle = get_legacy_stack_sdp_api()->handle.SDP_CreateRecord();if (handle == 0) {log::error("failed to create sdp record, scn: {}, service_name: {}",channel, name);return 0;}// 2. 数据准备// Convert the |uuid| into a big-endian representation and add it as a// sequence.uint8_t type = UUID_DESC_TYPE;uint8_t type_len = UUID_MAX_LENGTH;uint8_t type_buf[48];// Store the address of type buf in a pointer on the stack, so we can pass// a double pointer to SDP_AddSequenceuint8_t* type_buf_ptr = type_buf;uint8_t* tmp = type_buf;// 3. 创建基本 SDP 记录// Create the base SDP record.const char* stage = "create_base_record";if (!create_base_record(handle, name, channel, false /* with_obex */))goto error;// Do the conversion to big-endian -- tmp is only used to iterate through the// UUID array in the macro and serves no other purpose as the conversion// macros are not hygenic.// 4. UUID 转换为大端格式{ ARRAY_TO_BE_STREAM(tmp, uuid.To128BitBE().data(), UUID_MAX_LENGTH); }// 5. 添加服务类序列stage = "service_class_sequence";if (!get_legacy_stack_sdp_api()->handle.SDP_AddSequence(handle, (uint16_t)ATTR_ID_SERVICE_CLASS_ID_LIST, 1, &type, &type_len,&type_buf_ptr))goto error;// 6. 输出成功日志并写入 EIRlog::verbose("service registered successfully, service_name: {}, handle: 0x{:08x}",name, handle);{// Write the custom 128-bit UUID to EIRtBTA_CUSTOM_UUID curr = {uuid, handle};bta_sys_add_cust_uuid(curr); // 将自定义的 128 位 UUID 写入 EIR}return handle;error:get_legacy_stack_sdp_api()->handle.SDP_DeleteRecord(handle);log::error("failed to register service stage: {}, service_name: {}", stage,name);return 0;
}

在 SDP数据库中注册一个服务。该服务使用通用的 L2CAP RFCOMM 协议,会将传入的 uuid 作为服务类序列存储。会创建 SDP 记录,添加基本信息和服务类序列,并在成功时将自定义的 128 位 UUID 写入 EIR(Extended Inquiry Response),最后返回服务记录的句柄;若过程中出现错误,则删除已创建的记录并返回 0。

SDP_CreateRecord
/********************************************************************************* Function         SDP_CreateRecord** Description      This function is called to create a record in the database.*                  This would be through the SDP database maintenance API. The*                  record is created empty, teh application should then call*                  "add_attribute" to add the record's attributes.** Returns          Record handle if OK, else 0.*******************************************************************************/
uint32_t SDP_CreateRecord(void) {uint32_t handle;uint8_t buf[4];tSDP_DB* p_db = &sdp_cb.server_db;// 1. 记录池可用性检查/* First, check if there is a free record */if (p_db->num_records < SDP_MAX_RECORDS) {// 2. 记录结构体初始化memset(&p_db->record[p_db->num_records], 0, sizeof(tSDP_RECORD));// 3. 生成记录句柄/* We will use a handle of the first unreserved handle plus last record** number + 1 */if (p_db->num_records)handle = p_db->record[p_db->num_records - 1].record_handle + 1;elsehandle = 0x10000;p_db->record[p_db->num_records].record_handle = handle;p_db->num_records++;log::verbose("SDP_CreateRecord ok, num_records:{}", p_db->num_records);// 4. 自动添加句柄属性/* Add the first attribute (the handle) automatically */UINT32_TO_BE_FIELD(buf, handle);SDP_AddAttribute(handle, ATTR_ID_SERVICE_RECORD_HDL, UINT_DESC_TYPE, 4,buf);return (p_db->record[p_db->num_records - 1].record_handle);} elselog::error("SDP_CreateRecord fail, exceed maximum records:{}",SDP_MAX_RECORDS);return (0);
}

用于创建一个空的 SDP 记录。其主要流程包括:

  1. 记录池管理:检查是否有可用记录槽位(不超过 SDP_MAX_RECORDS)。

  2. 句柄生成:为新记录分配唯一的句柄(Handle),基于前一条记录句柄递增或从初始值 0x10000 开始。

  3. 初始化记录:清空记录结构体,自动添加ATTR_ID_SERVICE_RECORD_HDL属性(存储句柄值)。

  4. 返回句柄:成功时返回新记录句柄,失败时返回 0(如记录池已满)。

SDP_AddAttribute

packages/modules/Bluetooth/system/stack/sdp/sdp_db.cc
/********************************************************************************* Function         SDP_AddAttribute** Description      This function is called to add an attribute to a record.*                  This would be through the SDP database maintenance API.*                  If the attribute already exists in the record, it is*                  replaced with the new value.** NOTE             Attribute values must be passed as a Big Endian stream.** Returns          true if added OK, else false*******************************************************************************/
bool SDP_AddAttribute(uint32_t handle, uint16_t attr_id, uint8_t attr_type,uint32_t attr_len, uint8_t* p_val) {uint16_t zz;tSDP_RECORD* p_rec = &sdp_cb.server_db.record[0]; // 指向 SDP 记录数组首元素// sdp_cb.server_db 是全局的 SDP 服务器数据库,存储所有已创建的 SDP 记录// 1. 空指针校验if (p_val == nullptr) {log::warn("Trying to add attribute with p_val == nullptr, skipped");return (false);}// 2. 日志格式化输出// TODO(305066880): invoke would_log when implemented to check// if LOG_VERBOSE is displayed.if (true) {if ((attr_type == UINT_DESC_TYPE) ||(attr_type == TWO_COMP_INT_DESC_TYPE) ||(attr_type == UUID_DESC_TYPE) ||(attr_type == DATA_ELE_SEQ_DESC_TYPE) ||(attr_type == DATA_ELE_ALT_DESC_TYPE)) {#define MAX_ARR_LEN 200// one extra byte for storing terminating zero byte// 将二进制数据转换为十六进制字符串(如 UUID 的大端字节流)char num_array[2 * MAX_ARR_LEN + 1] = {0};uint32_t len = (attr_len > MAX_ARR_LEN) ? MAX_ARR_LEN : attr_len;#undef MAX_ARR_LENfor (uint32_t i = 0; i < len; i++) {snprintf(&num_array[i * 2], sizeof(num_array) - i * 2, "%02X",(uint8_t)(p_val[i]));}log::verbose("SDP_AddAttribute: handle:{:X}, id:{:04X}, type:{}, len:{}, ""p_val:{}, *p_val:{}",handle, attr_id, attr_type, attr_len, fmt::ptr(p_val), num_array);} else if (attr_type == BOOLEAN_DESC_TYPE) {log::verbose("SDP_AddAttribute: handle:{:X}, id:{:04X}, type:{}, len:{}, ""p_val:{}, *p_val:{}",handle, attr_id, attr_type, attr_len, fmt::ptr(p_val), *p_val);} else if ((attr_type == TEXT_STR_DESC_TYPE) ||(attr_type == URL_DESC_TYPE)) {if (p_val[attr_len - 1] == '\0') {log::verbose("SDP_AddAttribute: handle:{:X}, id:{:04X}, type:{}, len:{}, ""p_val:{}, *p_val:{}",handle, attr_id, attr_type, attr_len, fmt::ptr(p_val),(char*)p_val);} else {log::verbose("SDP_AddAttribute: handle:{:X}, id:{:04X}, type:{}, len:{}, ""p_val:{}",handle, attr_id, attr_type, attr_len, fmt::ptr(p_val));}} else {log::verbose("SDP_AddAttribute: handle:{:X}, id:{:04X}, type:{}, len:{}, p_val:{}",handle, attr_id, attr_type, attr_len, fmt::ptr(p_val));}}// 3. 数据库查找目标记录/* Find the record in the database */for (zz = 0; zz < sdp_cb.server_db.num_records; zz++, p_rec++) {if (p_rec->record_handle == handle) { // 匹配记录句柄// 校验记录剩余空间// error out early, no need to look upif (p_rec->free_pad_ptr >= SDP_MAX_PAD_LEN) {log::error("the free pad for SDP record with handle {} is full, skip adding ""the attribute",handle);return (false);}// 调用底层函数添加属性return SDP_AddAttributeToRecord(p_rec, attr_id, attr_type, attr_len,p_val);}}return (false);
}

向指定 SDP 记录中添加或更新属性。其核心职责包括:

  1. 属性唯一性管理:若属性已存在,覆盖原有值;若不存在,新增属性并按 ID 排序存储。

  2. 数据格式校验:确保属性值以大端格式(Big Endian)传递,符合 SDP 协议规范,并对不同类型属性(如 UUID、文本、布尔值等)进行差异化日志处理。

  3. 数据库遍历与操作:通过记录句柄查找目标记录,调用底层函数 SDP_AddAttributeToRecord 完成属性的实际存储。

  4. 内存安全控制:检查记录剩余空间(free_pad_ptr),避免缓冲区溢出,对文本类型支持截断处理。

SDP_AddAttributeToRecord

/home/yanlongli/code/android/packages/modules/Bluetooth/system/stack/sdp/sdp_db.cc
********************************************************************************* Function         SDP_AddAttributeToRecord** Description      This function is called to add an attribute to a record.*                  This would be through the SDP database maintenance API.*                  If the attribute already exists in the record, it is*                  replaced with the new value.** NOTE             Attribute values must be passed as a Big Endian stream.** Returns          true if added OK, else false*******************************************************************************/
bool SDP_AddAttributeToRecord(tSDP_RECORD* p_rec, uint16_t attr_id,uint8_t attr_type, uint32_t attr_len,uint8_t* p_val) {uint16_t xx, yy;tSDP_ATTRIBUTE* p_attr = &p_rec->attribute[0];// 1. 查找属性是否存在/* Found the record. Now, see if the attribute already exists */for (xx = 0; xx < p_rec->num_attributes; xx++, p_attr++) {/* The attribute exists. replace it */if (p_attr->id == attr_id) {SDP_DeleteAttributeFromRecord(p_rec, attr_id); // 先删除旧属性break;}if (p_attr->id > attr_id) break; // 属性数组按 ID 升序排列,提前终止查找}// 2. 校验属性数量限制if (p_rec->num_attributes >= SDP_MAX_REC_ATTR) return (false);// 3. 确定属性插入位置/* If not found, see if we can allocate a new entry */if (xx == p_rec->num_attributes) // 未找到属性,追加到数组末尾p_attr = &p_rec->attribute[p_rec->num_attributes];else {   // 插入到有序数组的指定位置,后续属性后移/* Since the attributes are kept in sorted order, insert ours here */for (yy = p_rec->num_attributes; yy > xx; yy--)p_rec->attribute[yy] = p_rec->attribute[yy - 1];}p_attr->id = attr_id;p_attr->type = attr_type;p_attr->len = attr_len;if (p_rec->free_pad_ptr + attr_len >= SDP_MAX_PAD_LEN) {if (p_rec->free_pad_ptr >= SDP_MAX_PAD_LEN) {log::error("SDP_AddAttributeToRecord failed: free pad {} equals or exceeds max ""padding length {}",p_rec->free_pad_ptr, SDP_MAX_PAD_LEN);return (false);}// 4. 内存空间校验与分配/* do truncate only for text string type descriptor */if (attr_type == TEXT_STR_DESC_TYPE) {log::warn("SDP_AddAttributeToRecord: attr_len:{} too long. truncate to ({})",attr_len, SDP_MAX_PAD_LEN - p_rec->free_pad_ptr);attr_len = SDP_MAX_PAD_LEN - p_rec->free_pad_ptr;p_val[SDP_MAX_PAD_LEN - p_rec->free_pad_ptr - 1] = '\0';} elseattr_len = 0;}if (attr_len > 0) {p_attr->len = attr_len;memcpy(&p_rec->attr_pad[p_rec->free_pad_ptr], p_val, (size_t)attr_len); // 写入属性值p_attr->value_ptr = &p_rec->attr_pad[p_rec->free_pad_ptr]; // 记录值指针p_rec->free_pad_ptr += attr_len; // 更新剩余空间指针} else if (attr_len == 0 && p_attr->len != 0) {/* if truncate to 0 length, simply don't add */log::error("SDP_AddAttributeToRecord fail, length exceed maximum: ID {}: ""attr_len:{}",attr_id, attr_len);p_attr->id = p_attr->type = p_attr->len = 0;return (false);}// 5. 更新属性计数并返回p_rec->num_attributes++;return (true);
}

向指定 SDP 记录中添加或更新属性。其核心逻辑包括:

  • 属性唯一性检查:若属性已存在,先删除旧属性再插入新值;若不存在,按属性 ID 排序插入合适位置。

  • 内存分配与校验:确保属性值大小不超过记录的可用空间(SDP_MAX_PAD_LEN),对文本类型支持截断处理。

  • 数据持久化:将属性值存储到记录的连续内存块(attr_pad)中,并更新属性指针和剩余空间。

SDP_AddSequence
packages/modules/Bluetooth/system/stack/sdp/sdp_db.cc
/********************************************************************************* Function         SDP_AddSequence** Description      This function is called to add a sequence to a record.*                  This would be through the SDP database maintenance API.*                  If the sequence already exists in the record, it is replaced*                  with the new sequence.** NOTE             Element values must be passed as a Big Endian stream.** Returns          true if added OK, else false*******************************************************************************/
bool SDP_AddSequence(uint32_t handle, uint16_t attr_id, uint16_t num_elem,uint8_t type[], uint8_t len[], uint8_t* p_val[]) {uint16_t xx;uint8_t* p;uint8_t* p_head;bool result;// 1. 内存分配与初始化uint8_t* p_buff =(uint8_t*)osi_malloc(sizeof(uint8_t) * SDP_MAX_ATTR_LEN * 2);p = p_buff;// 2. 构建序列/* First, build the sequence */for (xx = 0; xx < num_elem; xx++) {p_head = p;switch (len[xx]) {case 1:UINT8_TO_BE_STREAM(p, (type[xx] << 3) | SIZE_ONE_BYTE);break;case 2:UINT8_TO_BE_STREAM(p, (type[xx] << 3) | SIZE_TWO_BYTES);break;case 4:UINT8_TO_BE_STREAM(p, (type[xx] << 3) | SIZE_FOUR_BYTES);break;case 8:UINT8_TO_BE_STREAM(p, (type[xx] << 3) | SIZE_EIGHT_BYTES);break;case 16:UINT8_TO_BE_STREAM(p, (type[xx] << 3) | SIZE_SIXTEEN_BYTES);break;default:UINT8_TO_BE_STREAM(p, (type[xx] << 3) | SIZE_IN_NEXT_BYTE);UINT8_TO_BE_STREAM(p, len[xx]);break;}ARRAY_TO_BE_STREAM(p, p_val[xx], len[xx]);if (p - p_buff > SDP_MAX_ATTR_LEN) {/* go back to before we add this element */p = p_head;if (p_head == p_buff) {/* the first element exceed the max length */log::error("SDP_AddSequence - too long(attribute is not added)!!");osi_free(p_buff);return false;} elselog::error("SDP_AddSequence - too long, add {} elements of {}", xx,num_elem);break;}}// 3. 添加序列到记录result = SDP_AddAttribute(handle, attr_id, DATA_ELE_SEQ_DESC_TYPE,(uint32_t)(p - p_buff), p_buff);osi_free(p_buff);return result;
}

向 SDP记录中添加一个序列。该函数属于 SDP 数据库维护 API 的一部分,若指定的序列已存在于记录中,会用新的序列替换它。函数要求元素值以大端字节序(Big Endian)的流形式传入,最终返回操作是否成功的布尔值。

SDP_AddAttribute前面已分析,不在赘述。

2.4 SDP 记录删除流程

①上层触发删除:remove_sdp_record执行以下操作:

  • 校验记录 ID 有效性,通过互斥锁获取记录类型,禁用关联的蓝牙服务(如BTA_PBAP_SERVICE_ID)。

  • 调用free_sdp_slot释放槽位,通过BTA_SdpRemoveRecordByUser触发主线程删除。

②协议栈删除逻辑

  • 主线程通过bta_sdp_remove_record回调触发BTA_SDP_REMOVE_RECORD_USER_EVTon_remove_record_event调用SDP_DeleteRecord

  • SDP_DeleteRecord根据句柄(handle)执行单记录或全量删除:

    • 单记录删除:遍历数组找到目标记录,前移后续记录以保持连续,更新主 DI 句柄(若必要)。

    • 全量删除:直接重置记录数与主 DI 句柄。

③数据一致性:删除过程中调整属性指针(value_ptr)以适应数组重组,但存在潜在逻辑错误(实际无需调整,因指针为记录内相对偏移)。

remove_sdp_record

packages/modules/Bluetooth/system/btif/src/btif_sdp_server.cc
bt_status_t remove_sdp_record(int record_id) {int handle;// 1. 参数有效性校验if (record_id >= MAX_SDP_SLOTS) {return BT_STATUS_PARM_INVALID;}bluetooth_sdp_record* record;bluetooth_sdp_types sdp_type = SDP_TYPE_RAW;// 2. 线程安全的记录获取{std::unique_lock<std::recursive_mutex> lock(sdp_lock);record = sdp_slots[record_id].record_data;if (record != NULL) {sdp_type = record->hdr.type; // 获取记录类型}}// 3. 服务类型匹配与服务禁用tBTA_SERVICE_ID service_id = -1;switch (sdp_type) {case SDP_TYPE_MAP_MAS:service_id = BTA_MAP_SERVICE_ID;break;case SDP_TYPE_MAP_MNS:service_id = BTA_MN_SERVICE_ID;break;case SDP_TYPE_PBAP_PSE:service_id = BTA_PBAP_SERVICE_ID;break;case SDP_TYPE_PBAP_PCE:service_id = BTA_PCE_SERVICE_ID;break;default:/* other enumeration values were not enabled in {@link on_create_record_event} */break;}if (service_id > 0) {// {@link btif_disable_service} sets the mask {@link btif_enabled_services}.btif_disable_service(service_id); // 禁用对应的蓝牙服务}// 4. 释放 SDP 槽位/* Get the Record handle, and free the slot */handle = free_sdp_slot(record_id); // 释放槽位并获取记录句柄log::verbose("Sdp Server id={} to handle=0x{:08x}", record_id, handle);/* Pass the actual record handle */if (handle > 0) {BTA_SdpRemoveRecordByUser(INT_TO_PTR(handle)); // 通过 BTA 接口删除记录return BT_STATUS_SUCCESS;}log::verbose("Sdp Server - record already removed - or never created");return BT_STATUS_FAIL;
}

从蓝牙 SDP数据库中删除指定 ID 的服务记录。其核心流程包括:

  1. 参数校验:检查记录 ID 是否在有效范围内。

  2. 线程安全访问:通过互斥锁确保多线程环境下对 SDP 槽位的安全操作。

  3. 服务类型识别:根据记录类型获取对应的服务 ID,以便禁用相关服务。

  4. 资源释放:释放 SDP 槽位,并通过 BTA 接口触发记录删除。

  5. 状态同步:更新服务启用状态,确保协议栈与上层逻辑一致。

BTA_SdpRemoveRecordByUser

packages/modules/Bluetooth/system/bta/sdp/bta_sdp_api.cc
/********************************************************************************* Function         BTA_SdpRemoveRecordByUser** Description      This function is used to request a callback to remove a SDP*                  record. The registered callback will be called with event*                  BTA_SDP_REMOVE_RECORD_USER_EVT.** Returns          BTA_SDP_SUCCESS, if the request is being processed.*                  BTA_SDP_FAILURE, otherwise.*******************************************************************************/
tBTA_SDP_STATUS BTA_SdpRemoveRecordByUser(void* user_data) {do_in_main_thread(FROM_HERE,base::BindOnce(bta_sdp_remove_record, user_data));return BTA_SDP_SUCCESS;
}

蓝牙协议栈(BTA)中用于请求删除 SDP 记录的接口。其核心作用是将删除记录的请求提交到主线程处理,并通过回调机制通知上层逻辑操作结果。具体流程包括:

  1. 异步处理封装:通过 do_in_main_thread 将删除操作派发到主线程执行,避免在当前线程阻塞。

  2. 回调触发:当删除操作完成后,上层注册的回调函数(如 sdp_dm_cback)会收到 BTA_SDP_REMOVE_RECORD_USER_EVT 事件,从而更新状态或释放资源。

bta_sdp_remove_record

packages/modules/Bluetooth/system/bta/sdp/bta_sdp_act.cc
/********************************************************************************* Function     bta_sdp_create_record** Description  Discovers all sdp records for an uuid on remote device** Returns      void*******************************************************************************/
void bta_sdp_remove_record(void* user_data) {if (bta_sdp_cb.p_dm_cback)bta_sdp_cb.p_dm_cback(BTA_SDP_REMOVE_RECORD_USER_EVT, NULL, user_data);
}

触发回调函数,以通知上层应用程序有一个 SDP(服务发现协议)记录删除事件发生。当 BTA_SdpRemoveRecordByUser 函数将删除 SDP 记录的请求派发到主线程后,主线程会调用此函数来处理该请求,最终通过回调机制通知上层应用。

on_remove_record_event

packages/modules/Bluetooth/system/btif/src/btif_sdp_server.cc
void on_remove_record_event(int handle) {log::verbose("Sdp Server");// User data carries the actual SDP handle, not the ID.if (handle != -1 && handle != 0) {bool result;result = get_legacy_stack_sdp_api()->handle.SDP_DeleteRecord(handle);if (!result) {log::error("Unable to remove handle 0x{:08x}", handle);}}
}

SDP模块的回调处理函数,用于响应 SDP 记录删除事件。其核心职责是:

  • 校验记录句柄的有效性。

  • 调用底层 API 删除指定句柄的 SDP 记录。

  • 记录操作结果日志,便于调试和故障排查。

SDP_DeleteRecord

/********************************************************************************* Function         SDP_DeleteRecord** Description      This function is called to add a record (or all records)*                  from the database. This would be through the SDP database*                  maintenance API.**                  If a record handle of 0 is passed, all records are deleted.** Returns          true if succeeded, else false*******************************************************************************/
bool SDP_DeleteRecord(uint32_t handle) {uint16_t xx, yy, zz;tSDP_RECORD* p_rec = &sdp_cb.server_db.record[0];// 1. 全量删除逻辑if (handle == 0 || sdp_cb.server_db.num_records == 0) {/* Delete all records in the database */sdp_cb.server_db.num_records = 0;/* require new DI record to be created in SDP_SetLocalDiRecord */sdp_cb.server_db.di_primary_handle = 0;return (true);}// 2. 单记录删除逻辑else {/* Find the record in the database */for (xx = 0; xx < sdp_cb.server_db.num_records; xx++, p_rec++) {if (p_rec->record_handle == handle) { // 找到目标记录// 重组记录数组:将后续记录前移/* Found it. Shift everything up one */for (yy = xx; yy < sdp_cb.server_db.num_records - 1; yy++, p_rec++) {*p_rec = *(p_rec + 1); // 复制后续记录/* Adjust the attribute value pointer for each attribute */for (zz = 0; zz < p_rec->num_attributes; zz++)p_rec->attribute[zz].value_ptr -= sizeof(tSDP_RECORD);}sdp_cb.server_db.num_records--;log::verbose("SDP_DeleteRecord ok, num_records:{}",sdp_cb.server_db.num_records);/* if we're deleting the primary DI record, clear the *//* value in the control block */if (sdp_cb.server_db.di_primary_handle == handle) {// 更新主 DI 句柄(若删除的是主记录)sdp_cb.server_db.di_primary_handle = 0;}return (true);}}}return (false);
}

删除单个或所有 SDP 记录。其核心逻辑包括:

  1. 全量删除处理:当传入句柄为 0 时,清空整个数据库。

  2. 单记录删除处理:根据句柄查找并删除指定记录,重组记录数组以保持连续性。

  3. 元数据更新:更新数据库记录数量、主设备信息(DI)句柄等状态。

三、时序图

3.1 SDP 服务初始化流程时序图

3.2 SDP 服务搜索流程时序图

3.3 创建SDP记录流程时序图

3.4 删除SDP记录流程时序图

四、总结

本文围绕蓝牙 SDP 服务记录的生命周期,详细解析了从初始化、创建、搜索到删除的完整流程。核心设计包括:

  • 线程安全:通过主线程任务派发与互斥锁确保操作原子性。

  • 解耦与抽象:通过接口结构体(sdp_if)和回调机制分离底层实现与上层逻辑。

  • 协议兼容性:支持旧版协议栈接口(get_legacy_stack_sdp_api),并通过类型枚举(bluetooth_sdp_types)适配多种服务类型。

  • 内存管理:深拷贝与动态内存分配确保数据独立,避免悬垂指针。

相关文章:

【Bluedroid】蓝牙 SDP(服务发现协议)模块代码解析与流程梳理

本文深入剖析Bluedroid蓝牙协议栈中 SDP&#xff08;服务发现协议&#xff09;服务记录的全生命周期管理流程&#xff0c;涵盖初始化、记录创建、服务搜索、记录删除等核心环节。通过解析代码逻辑与数据结构&#xff0c;揭示各模块间的协作机制&#xff0c;包括线程安全设计、回…...

obj = null; 赋值null之前没有其他引用指向obj对象,那么,当obj=null时,会被垃圾回收机制立即回收吗?

不会立即回收。 具体原因是&#xff1a; 赋值 obj null; 后&#xff0c;对象变成“不可达”&#xff0c;符合垃圾回收条件&#xff0c;但垃圾回收器并不会立刻回收它。垃圾回收是CLR自动控制的非确定性过程&#xff0c;什么时候执行回收取决于系统内存压力、GC策略、分代情况…...

Android 数据持久化之 文件存储

在 Android 开发中&#xff0c;存储文件是一个常见的需求。 本文中介绍 openFileOutput 和 File 两种不同的方式来操作文件。 一、File 方式 根据文件的存储位置和访问权限&#xff0c;可以将文件存储分为内部存储&#xff08;Internal Storage&#xff09;和外部存储&#x…...

差分OPA verilogaA 模型

做电路设计&#xff0c;需要提前用理想模型如VerilogA模型做验证。这里分享一个由ahdlib库里单端opamp改造而来的差分opamp。参考何乐年的《模拟集成电路设计与仿真》10.4节423页&#xff1b; 描述的小信号模型如上。 VerilogA 用到了SRI/C&#xff0c;GBWgm/C,gaingm*r1等概念…...

oracle goldengate非并行进程转换为并行进程

oracle goldengate非并行进程转换为并行进程 在上一期的文章中写道了直接创建并行进程的方式对大事务进行分解&#xff0c;这对于新建立同步进程的时候提前规划是很有帮助的&#xff0c;但是如果对已经进行了同步的进程重新建立需要耗时比较长&#xff0c;Oracle提供了非并行进…...

58.[前端开发-前端工程化]Day05-webpack-Git安装-配置-Git命令

Git版本控制工具详解 1 邂逅版本控制工具 认识版本控制&#xff08;版本控制&#xff09; 版本控制的功能 版本控制的历史 2 集中式和分布式区别 集中式版本控制 分布式版本控制 3 Git的环境安装搭建 Git的安装 Bash – CMD – GUI 区别 Git的配置分类 Git的配置选项 Git的…...

CF每日5题

每日刷题两小时颐养天年 1855A 800 思维 将不高兴的同学计数cnt 不高兴的同学之间两两交换&#xff0c;一定不会在 p i i p_ii pi​i的位置上&#xff0c;贡献是cnt/2 如果cnt%2>0&#xff0c;那就多交换一次 void solve() {int n;cin>>n;int cnt0;forr(i,1,n){in…...

Redis实现分布式获取全局唯一自增ID的案例。

【1】简易自增版本(从 1 开始 1,2,3&#xff0c;...) 项目结构 下面是一个基于 RedisTemplate 实现的分布式全局唯一自增 ID 生成器的案例。适用于 Java Spring Boot 环境&#xff0c;利用 Redis 的原子操作 INCR 指令。 ✅ 原理说明 Redis 提供的 INCR 命令是原子性的&…...

创建型模式:工厂方法(Factory Method)模式

一、简介 工厂方法(Factory Method)模式是一种创建型设计模式,它定义了一个创建对象的接口,但让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。在 C# 中,工厂方法模式提供了一种更灵活的对象创建方式,将对象的创建和使用分离,提高了代码的可维护性和…...

大型语言模型在网络安全领域的应用综述

大型语言模型在网络安全领域的应用综述 简介1. 引言1.1 背景与意义1.2 LLMs 的基本概念1.3 LLMs 在网络安全中的优势1.4 报告目标 2. 文献综述方法2.1 研究问题2.2 文献检索策略2.3 文献筛选标准 3. LLMs 在网络安全领域的应用3.1 软件和系统安全 (Software and System Securit…...

TDEngine 与 Grafana

目录 实践目录 Grafana 参考文档 实践目录 10.60.100.194&#xff1a;/home/dualven/tdengine Grafana systemctl status grafana-server http://10.60.100.194:3000/ 这个端口与mydoor的new server服务冲突 &#xff08;同时只开一个&#xff09; 参考文档 运行监…...

iPhone手机连接WiFi异常解决方法

iPhone手机连接WiFi异常解决方法 一、问题现象二、iPhone连不上可能的原因三、基础排查与快速修复第一步:重启大法第二步:忽略网络,重新认证第三步:关闭“私有无线局域网地址”第四步:修改DNS服务器第五步:还原网络设置四、路由器端排查及设置关闭MAC地址过滤或添加到白名…...

微服务不注册到nacos的方法

引言:在开发中,有时候多个开发一起开发,可能会同时注册到dev环境中,这样可能会影响dev环境,那么在idea添加2个参数即可解决 spring.cloud.nacos.discovery.register-enabled falsespring.cloud.nacos.discovery.enabled false...

Spring Boot + Vue 实现在线视频教育平台

一、项目技术选型 前端技术&#xff1a; HTML CSS JavaScript Vue.js 前端框架 后端技术&#xff1a; Spring Boot 轻量级后端框架 MyBatis 持久层框架 数据库&#xff1a; MySQL 5.x / 8.0 开发环境&#xff1a; IDE&#xff1a;Eclipse / IntelliJ IDEA JDK&…...

【嵌入式开发-SPI】

嵌入式开发-SPI ■ SPI简介■ SPI &#xff08;Standard SPI&#xff09;■ DSPI &#xff08;Dual SPI&#xff09;■ QSPI是 Queued SPI的简写 ■ SPI简介 SPI协议其实是包括&#xff1a;Standard SPI、Dual SPI和Queued SPI三种协议接口&#xff0c;分别对应3-wire, 4-wire…...

【链表扫盲】FROM GPT

链表是一种线性数据结构&#xff0c;由节点&#xff08;Node&#xff09;组成&#xff0c;每个节点包含两个部分&#xff1a; 数据域&#xff08;data&#xff09;&#xff1a; 存储节点值。指针域&#xff08;next&#xff09;&#xff1a; 存储指向下一个节点的引用。 链表…...

如何在macOS上通过SSHFS挂载远程文件系统

在macOS系统中&#xff0c;想要便捷地访问远程计算机上的目录&#xff1f;借助SSH文件系统&#xff08;SSHFS&#xff09;就能轻松实现。SSHFS是一款文件系统客户端&#xff0c;它基于SSH文件传输协议&#xff08;SFTP&#xff09;建立安全连接&#xff0c;进而实现对远程文件的…...

Android studio profiler使用

主要讲内存泄露排查 1、把怀疑内存泄露的页面都跑一边&#xff0c;然后回到初始页面 2、打开profile的home&#xff0c;找到Analysis Memory Usage&#xff0c;点击右下角start profiler task&#xff0c;开始分析内存&#xff0c;等待分析完成&#xff0c;分析过程中页面是卡…...

排序算法-选择排序

选择排序是一种简单直观的排序算法&#xff0c;其核心思想是每次从未排序的部分中选出最小&#xff08;或最大&#xff09;的元素&#xff0c;放到已排序部分的末尾。 选择排序步骤 初始化&#xff1a;将序列分为已排序部分&#xff08;初始为空&#xff09;和未排序部分&…...

云计算的基础概论

一、云计算基础概念 1. 云计算定义 • 英文&#xff1a;Cloud Computing • 定义&#xff1a;通过互联网&#xff08;Internet&#xff09;按需提供可扩展的计算资源&#xff08;如服务器、存储、数据库、网络、软件等&#xff09;&#xff0c;用户无需管理底层基础设施。 …...

仿LISP运算 - 华为OD机试真题(A卷、JavaScript题解)

华为OD机试题库《C》限时优惠 9.9 华为OD机试题库《Python》限时优惠 9.9 华为OD机试题库《JavaScript》限时优惠 9.9 针对刷题难&#xff0c;效率慢&#xff0c;我们提供一对一算法辅导&#xff0c; 针对个人情况定制化的提高计划&#xff08;全称1V1效率更高&#xff09;。 看…...

数据透视表控件DHTMLX Pivot v2.1发布,新增HTML 模板、增强样式等多个功能

DHTMLX Pivot数据透视表能快速地对数据进行计数、总计、平均和执行许多其他操作。近日&#xff0c;DHTMLX Pivot发布了2.1版本&#xff0c;该版本扩展了开发人员通过新增的 CSS 样式选项、HTML 模板以及数字和日期的自定义格式修改表格外观的能力。此外&#xff0c;该版本还增加…...

简易的考试系统设计(Web实验)

简易的考试系统设计&#xff08;Web实验&#xff09; 1.实验内容与设计思想&#xff08;一&#xff09;实验需求&#xff08;二&#xff09;设计思路 2.代码展示3.实验小结 1.实验内容与设计思想 &#xff08;一&#xff09;实验需求 1.编写两个页面程序&#xff0c;一个HTML…...

C++之set和map的运用

目录 序列式容器和关联式容器 熟识set 在STL中的底层结构&#xff1a; set的构造和迭代器 set的增删查 multiset和set的差异 练习题&#xff1a; 熟识map map类的介绍 pair类型介绍 map的构造 map的增删查 map的数据修改 测试样例&#xff1a; multimap和map的差…...

基于智能家居项目 RGB彩灯(P9813)

一、P9813 是什么&#xff1f; P9813 是一颗专门用来控制 RGB LED灯珠 的芯片&#xff0c;也就是说&#xff0c;它能控制红色、绿色、蓝色三种灯光的亮度&#xff0c;从而调出各种颜色。它最常见的用途就是在各种“会变色”的灯带中。 它的通信方式非常简单&#xff0c;只需要…...

EMQX 作为 MQTT Broker,支持 ​MQTT over TCP​ 和 ​MQTT over WebSocket​ 两种协议

1. EMQX 支持的协议与端口​ 协议类型默认端口用途说明​MQTT over TCP​1883标准的 MQTT 协议&#xff0c;基于 TCP 传输&#xff08;用于后端服务、物联网设备等&#xff09;。​MQTT over TLS​8883加密的 MQTT over TCP&#xff08;TLS/SSL 加密&#xff0c;安全性更高&am…...

软件测试学习笔记

第1章 绪论 软件测试 本质上说&#xff0c;就是寻找软件的缺陷、错误&#xff0c;对其质量度量的方法与过程。软件测试的一切活动都围绕着两个目标&#xff08;验证是否符合需求&#xff0c;识别差异&#xff09;而行进。它是测试思维、策略方针、设计实施的基本出发点。 学…...

Vue3 + Node.js 实现客服实时聊天系统(WebSocket + Socket.IO 详解)

Node.js 实现客服实时聊天系统&#xff08;WebSocket Socket.IO 详解&#xff09; 一、为什么选择 WebSocket&#xff1f; 想象一下淘宝客服的聊天窗口&#xff1a;你发消息&#xff0c;客服立刻就能看到并回复。这种即时通讯效果是如何实现的呢&#xff1f;我们使用 Vue3 作…...

python 上海新闻爬虫

1. 起因&#xff0c; 目的: 继续做新闻爬虫。我之前写过。此文先记录2个新闻来源。后面打算进行过滤&#xff0c;比如只选出某一个类型新闻。 2. 先看效果 过滤出某种类型的新闻&#xff0c;然后生成 html 页面&#xff0c;而且&#xff0c;自动打开这个页面。 比如科技犯罪…...

【Axure高保真原型】中继器表格批量上传数据

今天和大家分享中继器表格批量上传数据的原型模板&#xff0c;效果包括&#xff1a; 点击上传按钮&#xff0c;可以真实的打开本地文件夹选择文件&#xff1b; 选择的文件如果不是表格格式&#xff08;xls、xlsx、xlt、csv&#xff09;&#xff0c;就会显示提示弹窗&#xff1…...

复刻低成本机械臂 SO-ARM100 单关节控制(附代码)

视频讲解&#xff1a; 复刻低成本机械臂 SO-ARM100 单关节控制&#xff08;附代码&#xff09; 代码仓库&#xff1a;GitHub - LitchiCheng/SO-ARM100: Some Test code on SO-ARM100 昨天用bambot的web的方式调试了整个机械臂&#xff0c;对于后面的仿真的sim2real来说&#x…...

视频编解码学习7之视频编码简介

视频编码技术发展历程与主流编码标准详解 视频编码技术是现代数字媒体领域的核心技术之一&#xff0c;它通过高效的压缩算法大幅减少了视频数据的体积&#xff0c;使得视频的存储、传输和播放变得更加高效和经济。从早期的H.261标准到最新的AV1和H.266/VVC&#xff0c;视频编码…...

【NextPilot日志移植】整体功能概要

整体日志系统的实现功能 该日志系统主要实现了飞行日志的记录功能&#xff0c;支持多种日志记录模式&#xff0c;可将日志存储到文件或通过 MAVLink 协议传输&#xff0c;同时具备日志加密、空间管理、事件记录等功能。具体如下&#xff1a; 日志记录模式&#xff1a;支持按武…...

Windows系统下使用Kafka和Zookeeper,Python运行kafka(二)

1.配置 Zookeeper 进入解压后的 Zookeeper 目录&#xff08;例如 F:\zookeeper\conf&#xff09;&#xff0c;复制 zoo_sample.cfg 文件并命名为 zoo.cfg&#xff08;如果 zoo.cfg 已经存在&#xff0c;则直接编辑该文件&#xff09;。 打开 zoo.cfg 文件&#xff0c;配置相关…...

《构建社交应用用户激励引擎:React Native与Flutter实战解析》

React Native凭借其与JavaScript和React的紧密联系&#xff0c;为开发者提供了一个熟悉且灵活的开发环境。在构建用户等级体系时&#xff0c;它能够充分利用现有的前端开发知识和工具。通过将用户在社交应用中的各种行为进行量化&#xff0c;比如发布动态的数量、点赞评论的次数…...

Oracle OCP认证考试考点详解083系列13

题记&#xff1a; 本系列主要讲解Oracle OCP认证考试考点&#xff08;题目&#xff09;&#xff0c;适用于19C/21C,跟着学OCP考试必过。 61. 第61题&#xff1a; 题目 解析及答案&#xff1a; 关于基于RPM的Oracle数据库安装&#xff0c;以下哪两项是正确的&#xff1f; A) …...

【AI】DeepWiki 页面转换成 Markdown 保存 - Chrome 扩展

GitHub: https://github.com/zxmfke/deepwiki-md-chrome-extension 背景 个人比较喜欢整理项目架构&#xff0c;更多都是保存成 markdown 的格式保存&#xff0c;然后发博客。deepwiki 刚好把 github 仓库代码的架构输出出来了&#xff0c;不过没有办法下载成 markdown 格式&…...

HTTP 状态码是服务器对客户端请求的响应标识,用于表示请求的处理结果

以下是完整的 HTTP 状态码分类和常见状态码详解&#xff1a; 一、状态码分类&#xff08;5大类&#xff09; 分类范围描述常见场景1xx100-199信息性响应请求已被接收&#xff0c;继续处理2xx200-299成功响应请求成功处理3xx300-399重定向响应需要进一步操作4xx400-499客户端错…...

【AI论文】FlexiAct:在异构场景中实现灵活的动作控制

摘要&#xff1a;动作定制涉及生成视频&#xff0c;其中主体执行由输入控制信号指示的动作。 当前的方法使用姿势引导或全局运动定制&#xff0c;但受到空间结构&#xff08;如布局、骨架和视点一致性&#xff09;严格约束的限制&#xff0c;降低了在不同主题和场景下的适应性。…...

ubuntu24.04安装anaconda

1. ubuntu安装ananconda 进入官网&#xff1a;添加链接描述 直接点击Download下载&#xff0c;它会自动匹配合适的版本 打开保存下载文件&#xff0c;点击右键&#xff0c;选择在终端打开&#xff0c;输入 bash Anaconda3-2024.10-1-Linux-x86_64.sh不断点击Enter&#xff0c…...

六、Hadoop初始化与启动

成功部署一个Hadoop集群并不仅仅是安装好软件那么简单。在它真正能够为我们处理海量数据之前&#xff0c;还需要一系列精心的初始化和启动步骤。这些步骤确保了各个组件能够正确协同工作。完成启动后&#xff0c;Hadoop还提供了便捷的 Web 用户界面 (Web UI)&#xff0c;帮助我…...

边缘网关(边缘计算)

边缘网关是边缘计算架构中的关键组件&#xff0c;充当连接终端设备&#xff08;如传感器、IoT设备&#xff09;与云端或核心网络的桥梁。它在数据源头附近进行实时处理、分析和过滤&#xff0c;显著提升效率并降低延迟。 核心功能 协议转换 ○ 支持多种通信协议&#xff08;如…...

学成在线之课程管理

一&#xff1a;业务概述 我负责的课程管理这一块&#xff0c;可以发布课程&#xff0c;可以对课程列表进行一个管理&#xff0c;发布课程这分为三步&#xff1a;首先是需要进行填写课程相关的信息&#xff0c;再设计这个课程的大纲&#xff0c;最后是选择发布这门课程&#xff…...

python里面的class,类,方法,函数,def

一、ds 好的!我将用专业术语结合通俗解释来梳理这些概念,并用结构化方式呈现它们的关系: 1. 核心概念解析 类 (Class) 定义:类是面向对象编程(OOP)中的核心概念,是创建对象的模板(蓝图)。它封装了一组属性(数据)和方法(行为),用于描述具有相同特征和功能的对象…...

复盘20250508

根据行业趋势、政策支持、公司基本面及技术壁垒&#xff0c;我推荐以下两支最可能持续上涨的个股&#xff0c;并深度分析原因&#xff1a; 1. 奥普光电&#xff08;机器视觉光刻机芯片&#xff09; 核心逻辑&#xff1a; 光刻机国产化核心标的&#xff1a;控股股东长春光机所…...

数据结构 - 10( B- 树 B+ 树 B* 树 4000 字详解 )

一&#xff1a;B- 树 1.1 B- 树的引入 在使用二叉搜索树对数据进行排序时&#xff0c;存在一个缺陷&#xff1a;随着数据量的增大&#xff0c;二叉搜索树的高度也会随之增加。虽然在数据量较小时&#xff0c;这种情况并不明显&#xff0c;但当数据量变得庞大时&#xff0c;树…...

算法与数据结构 - 常用图算法总结

在图论中&#xff0c;图算法非常重要&#xff0c;广泛应用于计算机科学、网络分析、社交网络、地理信息系统等领域。下面是一些常用的图算法&#xff0c;按不同功能和应用场景分类&#xff1a; 1. 图的遍历 图遍历算法用于遍历图中的节点和边。主要有两种常见的图遍历方法&am…...

涨薪技术|0到1学会性能测试第53课-Tomcat配置

前面的推文我们掌握了Tomcat服务器的3种监控技术知识。今天给大家分享Tomcat调优技术。后续文章都会系统分享干货,带大家从0到1学会性能测试。 01Tomcat配置 当Tomcat服务器安装好并开始运行后,需要对服务器进行一些基本配置,通常关于Tomcat服务器的配置包括两部分: 第一:…...

亚马逊推出新型仓储机器人 Vulcan:具备“触觉”但不会取代人类工人

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…...

后退n帧协议

滑动窗口机制&#xff08;Sliding Window&#xff09; 发送方有一个发送窗口&#xff0c;最多可以连续发送 N 个未确认的帧&#xff08;N 就是窗口大小&#xff09;。 接收方通常只有一个接收窗口&#xff0c;只接收按序到达的帧&#xff0c;不接受乱序帧。 累计确认机制&…...