openharmony 软总线机制详解-Softbus Discovery模块(一)
openharmony 软总线机制详解-Softbus Discovery模块(二)
本文在前两篇对设备发现流程的宏观分析基础上,将探讨其核心组件——Discovery Manager的内部实现机制。Discovery Manager作为设备发现服务的中央调度单元,负责所有发布(Publish)与发现(Subscribe)请求的生命周期管理、数据组织、回调分发以及与系统底层服务的协同。
从应用调用到核心管理的完整链路
当应用程序需要发布服务或发现设备时,整个调用链路涉及多个层次的协同工作。下图展示了完整的调用流程:
这个流程从应用层的简单接口调用开始,经过SDK适配、IPC通信、参数验证、对象创建、分组管理、协议分发,最终到底层网络协议的执行。每个层次都有明确的职责分工,共同构成了完整的设备发现服务链路。
发现管理器的初始化与生命周期
DiscMgrInit函数负责发现管理器的完整初始化过程:
int32_t DiscMgrInit(void)
{
DISC_CHECK_AND_RETURN_RET_LOGW(g_isInited == false, SOFTBUS_OK, DISC_INIT, "already inited");
g_discMgrMediumCb.OnDeviceFound = DiscOnDeviceFound;
g_discCoapInterface = DiscCoapInit(&g_discMgrMediumCb);
g_discBleInterface = DiscBleInit(&g_discMgrMediumCb);
DISC_CHECK_AND_RETURN_RET_LOGE(g_discBleInterface != NULL || g_discCoapInterface != NULL,
SOFTBUS_DISCOVER_MANAGER_INIT_FAIL, DISC_INIT, "ble and coap both init failed");
g_publishInfoList = CreateSoftBusList();
DISC_CHECK_AND_RETURN_RET_LOGE(g_publishInfoList != NULL, SOFTBUS_DISCOVER_MANAGER_INIT_FAIL, DISC_INIT,
"init publish info list failed");
g_discoveryInfoList = CreateSoftBusList();
DISC_CHECK_AND_RETURN_RET_LOGE(g_discoveryInfoList != NULL, SOFTBUS_DISCOVER_MANAGER_INIT_FAIL, DISC_INIT,
"init discovery info list failed");
for (int32_t i = 0; i < CAPABILITY_MAX_BITNUM; i++) {
ListInit(&g_capabilityList[i]);
}
g_isInited = true;
return SOFTBUS_OK;
}
函数首先设置g_discMgrMediumCb回调结构体,将OnDeviceFound字段指向DiscOnDeviceFound函数,建立了从底层协议栈到发现管理器的事件通知通道。随后,函数分别调用DiscCoapInit和DiscBleInit来初始化CoAP和BLE两种传输介质的接口实现,这两个函数返回的DiscoveryFuncInterface指针被保存到g_discCoapInterface和g_discBleInterface全局变量中。
在协议接口初始化完成后,函数创建g_publishInfoList和g_discoveryInfoList两个SoftBusList对象,分别用于管理发布服务和发现服务的DiscItem对象。这两个链表构成了发现管理器按包名分组管理的基础数据结构。最后,函数初始化g_capabilityList数组中的每个链表头节点,为基于能力的索引机制做好准备。
当应用进程异常退出时,DiscMgrOnPackageDied函数会被调用来清理相关资源:
void DiscMgrOnPackageDied(const char *pkgName)
{
DISC_LOGI(DISC_CONTROL, "pkg is dead. pkgName=%{public}s", pkgName);
RemoveDiscInfoForPublish(pkgName);
RemoveDiscInfoForDiscovery(pkgName);
}
函数接收包名作为参数,分别调用RemoveDiscInfoForPublish和RemoveDiscInfoForDiscovery来清理该包下的所有发布和发现服务。清理过程包括从DiscItem的InfoList中移除DiscInfo对象,从g_capabilityList索引中移除相关节点,释放内存资源,以及通知底层协议栈停止相关的网络操作。
DiscInfo对象的创建与初始化过程
CreateDiscInfoForPublish函数在接收到发布请求后,首先分配DiscInfo结构体的内存空间,然后开始填充这个核心数据结构的各个字段:
static DiscInfo *CreateDiscInfoForPublish(const PublishInfo *info)
{
DiscInfo *infoNode = (DiscInfo *)SoftBusCalloc(sizeof(DiscInfo));
DISC_CHECK_AND_RETURN_RET_LOGE(infoNode != NULL, NULL, DISC_CONTROL, "calloc info node failed");
ListInit(&(infoNode->node));
ListInit(&(infoNode->capNode));
infoNode->id = info->publishId;
infoNode->mode = info->mode;
infoNode->medium = info->medium;
PublishOption *option = &(infoNode->option.publishOption);
option->freq = info->freq;
option->ranging = info->ranging;
int32_t bitmap = TransferStringCapToBitmap(info->capability);
if (bitmap < 0) {
SoftBusFree(infoNode);
return NULL;
}
BitmapSet(option->capabilityBitmap, (uint32_t)bitmap);
return infoNode;
}
函数将PublishInfo中的发布ID、发现模式、传输介质和交换频率等参数复制到DiscInfo对象中,同时初始化链表节点和统计信息字段。在处理能力信息时,函数调用TransferStringCapToBitmap将应用层传入的能力字符串转换为内部使用的位图表示。
TransferStringCapToBitmap函数通过遍历预定义的g_capabilityMap映射表来实现字符串到位图的转换:
static int32_t TransferStringCapToBitmap(const char *capability)
{
DISC_CHECK_AND_RETURN_RET_LOGW(capability != NULL, SOFTBUS_DISCOVER_MANAGER_CAPABILITY_INVALID,
DISC_CONTROL, "capability is null");
for (uint32_t i = 0; i < sizeof(g_capabilityMap) / sizeof(g_capabilityMap[0]); i++) {
if (strcmp(capability, g_capabilityMap[i].capability) == 0) {
DISC_LOGD(DISC_CONTROL, "capability=%{public}s", capability);
return g_capabilityMap[i].bitmap;
}
}
return SOFTBUS_DISCOVER_MANAGER_CAPABILITY_INVALID;
}
这个映射表定义了系统支持的所有能力类型,包括"osdCapability"、“castPlus”、"dvKit"等能力标识符及其对应的位图值。转换完成后,函数使用BitmapSet操作将对应的位设置到PublishOption的capabilityBitmap字段中,这个位图后续将用于能力匹配和索引操作。
能力机制的设计原理与实现
能力机制是OpenHarmony SoftBus发现服务的核心概念,它定义了设备所具备的功能特性和服务类型。在发现管理器中,能力机制可通过位图表示、字符串映射、索引分组
能力定义与映射机制
系统通过g_capabilityMap全局映射表定义了所有支持的设备能力类型:
static const CapabilityMap g_capabilityMap[] = {
{CASTPLUS_CAPABILITY_BITMAP, (char *)"castPlus"},
{DVKIT_CAPABILITY_BITMAP, (char *)"dvKit"},
{DDMP_CAPABILITY_BITMAP, (char *)"ddmpCapability"},
{OSD_CAPABILITY_BITMAP, (char *)"osdCapability"},
{SHARE_CAPABILITY_BITMAP, (char *)"share"},
{APPROACH_CAPABILITY_BITMAP, (char *)"approach"},
};
每个能力类型都包含两个关键信息:位图值和字符串标识符。位图值是一个整数,其二进制表示中只有一位被设置为1,用于在capabilityBitmap中标识该能力。字符串标识符则是应用层使用的可读性标识,如"castPlus"表示投屏能力,"osdCapability"表示OSD显示能力。
这种双重表示机制的设计考虑了不同层面的需求。应用层开发者使用直观的字符串来描述设备能力,而系统内部使用位图进行高效的比较和索引操作。TransferStringCapToBitmap函数承担了两种表示之间的转换职责。
位图操作的底层实现
发现管理器使用位图来表示设备的多种能力,通过位运算实现快速的能力检查和设置:
static void BitmapSet(uint32_t *bitMap, uint32_t pos)
{
*bitMap |= 1U << pos;
}
static bool IsBitmapSet(const uint32_t *bitMap, uint32_t pos)
{
return ((1U << pos) & (*bitMap)) ? true : false;
}
BitmapSet函数通过左移操作创建一个只有指定位置为1的掩码,然后与原位图进行或运算,将对应位设置为1。IsBitmapSet函数则通过与运算检查指定位置是否为1。这种位运算的方式使得能力的设置和检查都能在常数时间内完成。
下图展示了能力机制的核心组件和转换流程:
位图表示还支持设备的多能力特性。一个设备可以同时具备多种能力,如既支持投屏又支持文件分享,这些能力在位图中表现为多个位被同时设置为1。在设备发现事件处理时,DiscOnDeviceFound函数会遍历设备能力位图的每一位,为每种能力分别触发相应的订阅者回调。
能力索引的分组策略
g_capabilityList数组实现了基于能力的高效索引机制,每个数组元素对应一种特定的设备能力:
#define CAPABILITY_MAX_BITNUM 16
static ListNode g_capabilityList[CAPABILITY_MAX_BITNUM];
这个数组的索引直接对应能力位图中的位位置,使得系统能够通过简单的数组访问快速定位到特定能力的订阅者链表。当AddDiscInfoToCapabilityList函数将订阅服务加入索引时,它会遍历订阅服务的能力位图,找到被设置的位,然后将DiscInfo对象的capNode节点插入到对应索引的链表中。
其优势在于其O(1)的查找复杂度。当设备发现事件到达时,系统不需要遍历所有订阅者来查找匹配的服务,而是直接通过能力位图索引到相关的订阅者链表。对于具有多种能力的设备,系统会为每种能力分别进行索引查找,确保所有相关的订阅者都能收到通知。
能力匹配的精确性保证
能力匹配机制确保了发现事件的精确分发,避免了不相关服务的误触发。在CreateDiscInfoForSubscribe函数中,系统将订阅服务的能力字符串转换为位图表示,并严格按照单一能力原则进行处理:
static DiscInfo *CreateDiscInfoForSubscribe(const SubscribeInfo *info)
{
// ... 其他初始化代码 ...
int32_t bitmap = TransferStringCapToBitmap(info->capability);
if (bitmap < 0) {
SoftBusFree(infoNode);
return NULL;
}
BitmapSet(option->capabilityBitmap, (uint32_t)bitmap);
return infoNode;
}
每个订阅服务只能订阅一种特定的能力类型,这种限制简化了索引管理和匹配逻辑。当设备具备多种能力时,系统会为每种能力分别查找订阅者,确保匹配的精确性。如果某个应用需要订阅多种能力,它需要创建多个订阅服务,每个服务对应一种特定的能力类型。
能力匹配的另一个重要特性是其对发布和订阅服务的区别处理。**只有订阅类型的服务才会被加入能力索引,发布服务不参与索引机制。
能力数据的传输与验证
除了能力类型标识外,发现服务还支持携带具体的能力数据。这些数据通过capabilityData字段传输,为设备间的精确匹配提供了更丰富的信息:
typedef struct {
int32_t freq;
bool isSameAccount;
bool isWakeRemote;
uint32_t capabilityBitmap[CAPABILITY_NUM];
uint8_t *capabilityData;
uint32_t dataLen;
} SubscribeOption;
capabilityData字段可以包含JSON格式的结构化数据,描述设备的具体能力参数。例如,对于投屏能力,数据可能包含支持的分辨率、编码格式等信息;对于文件分享能力,数据可能包含支持的文件类型、传输协议等参数。
能力在设备发现中的匹配流程
当底层协议栈发现新设备时,设备的能力信息被封装在DeviceInfo结构体中传递给发现管理器:
typedef struct {
char devId[DISC_MAX_DEVICE_ID_LEN];
char accountHash[MAX_ACCOUNT_HASH_LEN];
DeviceType devType;
char devName[DISC_MAX_DEVICE_NAME_LEN];
bool isOnline;
unsigned int addrNum;
ConnectionAddr addr[CONNECTION_ADDR_MAX];
unsigned int capabilityBitmapNum;
unsigned int capabilityBitmap[DISC_MAX_CAPABILITY_NUM];
char custData[DISC_MAX_CUST_DATA_LEN];
int32_t range;
} DeviceInfo;
DeviceInfo中的capabilityBitmap字段包含了设备声明的所有能力类型。DiscOnDeviceFound函数通过遍历这个位图来确定设备具备的能力,然后为每种能力查找相应的订阅者。
能力匹配过程还考虑了设备能力的动态性。设备可能在运行过程中动态地启用或禁用某些能力,这些变化会通过新的设备发现事件反映到发现管理器中。订阅者通过接收这些更新事件,能够及时了解网络中设备能力的变化情况。
能力机制的扩展性设计
发现管理器的能力机制具有良好的扩展性,支持新能力类型的动态添加。当系统需要支持新的设备能力时,开发者只需要在g_capabilityMap映射表中添加相应的条目,定义新的能力位图值和字符串标识符。
CAPABILITY_MAX_BITNUM常量定义了系统支持的最大能力数量,当前设置为16,意味着系统最多可以支持16种不同的设备能力。这个限制是基于uint32_t位图的位数考虑,如果需要支持更多能力类型,可以扩展为uint64_t或使用多个uint32_t组成的位图数组。
能力机制的扩展还体现在其对不同传输介质的适配上。CoAP和BLE协议可能对能力信息有不同的编码和传输要求,发现管理器通过统一的DiscoveryFuncInterface接口将这些差异屏蔽,使得上层的能力管理逻辑不受底层协议变化的影响。
当DiscInfo对象创建完成后,系统需要将其纳入发现管理器的管理体系中。这个过程涉及到发现管理器的双重分组管理机制,即按包名分组和按能力分组的并行管理结构。InnerPublishService函数首先通过GetDiscItem函数在g_publishInfoList中查找对应包名的DiscItem对象,如果不存在则调用CreateDiscItem函数创建新的包管理对象。
包名分组管理机制的实现
GetDiscItem函数通过遍历g_publishInfoList链表来查找指定包名的DiscItem对象:
static DiscItem *GetDiscItem(SoftBusList *serviceList, const char *packageName)
{
DiscItem *itemNode = NULL;
LIST_FOR_EACH_ENTRY(itemNode, &serviceList->list, DiscItem, node) {
if (strcmp(itemNode->packageName, packageName) == 0) {
return itemNode;
}
}
return NULL;
}
这个查找过程采用字符串比较的方式,逐一检查链表中每个DiscItem对象的packageName字段是否与目标包名匹配。
当需要创建新的DiscItem对象时,CreateDiscItem函数分配内存空间并初始化各个字段:
typedef struct {
ListNode node;
char packageName[PKG_NAME_SIZE_MAX];
InnerCallback callback;
uint32_t infoNum;
ListNode InfoList;
} DiscItem;
函数将包名复制到packageName字段中,根据服务类型设置相应的回调函数,并初始化InfoList链表用于管理该包下的所有发现信息。创建完成后,新的DiscItem对象被插入到对应的服务列表中,同时更新列表的计数器。
DiscItem对象创建完成后,系统将新创建的DiscInfo对象加入到DiscItem的InfoList链表中。这个过程通过设置DiscInfo对象的item指针建立了两者之间的关联关系,同时更新DiscItem的infoNum计数器。这种设计使得系统能够快速定位某个包下的所有发现服务,为后续的资源管理和生命周期控制提供了基础。
能力分组索引机制的建立
在完成包名分组管理后,系统需要建立基于能力的索引机制来优化设备发现事件的分发效率。对于订阅类型的服务,AddDiscInfoToCapabilityList函数负责将DiscInfo对象加入到相应的能力索引链表中:
static void AddDiscInfoToCapabilityList(DiscInfo *info, const ServiceType type)
{
if (type != SUBSCRIBE_SERVICE && type != SUBSCRIBE_INNER_SERVICE) {
DISC_LOGI(DISC_CONTROL, "publish no need to add");
return;
}
for (uint32_t tmp = 0; tmp < CAPABILITY_MAX_BITNUM; tmp++) {
if (IsBitmapSet(&(info->option.subscribeOption.capabilityBitmap[0]), tmp) == true) {
if (type == SUBSCRIBE_SERVICE) {
ListTailInsert(&(g_capabilityList[tmp]), &(info->capNode));
} else {
ListNodeInsert(&(g_capabilityList[tmp]), &(info->capNode));
}
break;
}
}
}
这个函数首先检查服务类型,只有SUBSCRIBE_SERVICE和SUBSCRIBE_INNER_SERVICE类型的服务才会被加入能力索引,因为只有订阅服务需要接收设备发现事件的通知。
函数通过遍历DiscInfo对象的capabilityBitmap位图来确定该服务订阅的能力类型。当发现某个位被设置时,函数将DiscInfo对象的capNode节点插入到g_capabilityList数组对应索引的链表中。这里存在一个重要的优先级机制:内部服务通过ListNodeInsert插入到链表头部,而外部应用服务通过ListTailInsert插入到链表尾部,这样确保了内部服务在设备发现事件处理时具有更高的优先级。
g_capabilityList数组是发现管理器实现高效能力匹配的核心数据结构:
static ListNode g_capabilityList[CAPABILITY_MAX_BITNUM];
这个数组包含CAPABILITY_MAX_BITNUM个链表头节点,每个索引位置对应一种特定的设备能力。通过这种设计,当设备发现事件到达时,系统能够根据设备的能力位图直接定位到相关的订阅者链表,避免了对所有订阅者的全量遍历。
下图展示了双重分组管理的数据结构关系:
多介质管理与协议分发
当DiscInfo对象完成分组管理后,系统需要将发布请求分发到具体的传输介质实现。CallInterfaceByMedium函数根据DiscInfo对象中指定的medium字段来选择相应的传输协议:
static int32_t CallInterfaceByMedium(const DiscInfo *info, const InterfaceFuncType type)
{
switch (info->medium) {
case COAP:
return CallSpecificInterfaceFunc(&(info->option), g_discCoapInterface, info->mode, type);
case BLE:
return CallSpecificInterfaceFunc(&(info->option), g_discBleInterface, info->mode, type);
case AUTO: {
int coapRes = CallSpecificInterfaceFunc(&(info->option), g_discCoapInterface, info->mode, type);
int bleRes = CallSpecificInterfaceFunc(&(info->option), g_discBleInterface, info->mode, type);
DISC_CHECK_AND_RETURN_RET_LOGE(coapRes == SOFTBUS_OK || bleRes == SOFTBUS_OK,
SOFTBUS_DISCOVER_MANAGER_INNERFUNCTION_FAIL, DISC_CONTROL, "all medium failed");
return SOFTBUS_OK;
}
default:
return SOFTBUS_DISCOVER_MANAGER_INNERFUNCTION_FAIL;
}
}
发现管理器支持CoAP、BLE和AUTO三种介质类型,其中AUTO类型表示同时使用CoAP和BLE两种协议进行发布。对于CoAP介质,函数调用g_discCoapInterface指向的接口实现;对于BLE介质,函数调用g_discBleInterface指向的接口实现。这两个全局指针在发现管理器初始化时通过DiscCoapInit和DiscBleInit函数进行设置,分别指向CoAP和BLE协议栈提供的DiscoveryFuncInterface接口实现:
typedef struct {
int32_t (*Publish)(const PublishOption *option);
int32_t (*StartScan)(const PublishOption *option);
int32_t (*Unpublish)(const PublishOption *option);
int32_t (*StopScan)(const PublishOption *option);
int32_t (*StartAdvertise)(const SubscribeOption *option);
int32_t (*Subscribe)(const SubscribeOption *option);
int32_t (*Unsubscribe)(const SubscribeOption *option);
int32_t (*StopAdvertise)(const SubscribeOption *option);
void (*LinkStatusChanged)(LinkStatus status);
void (*UpdateLocalDeviceInfo)(InfoTypeChanged type);
} DiscoveryFuncInterface;
CallSpecificInterfaceFunc函数进一步根据发现模式来选择具体的操作类型。对于发布服务,主动模式调用Publish接口,被动模式调用StartScan接口;对于订阅服务,主动模式调用StartAdvertise接口,被动模式调用Subscribe接口。这种设计体现了发现服务在不同模式下的行为差异:主动模式下设备主动广播或扫描,被动模式下设备等待其他设备的发现请求。
当AUTO介质类型被选择时,系统会同时调用CoAP和BLE两种协议的相应接口。函数分别获取两种协议的执行结果,只要其中一种协议执行成功,整个操作就被认为是成功的。这种设计提高了发现服务的可靠性和覆盖范围,确保在某种协议不可用的情况下,服务仍然能够通过其他协议正常工作。
设备发现事件的处理流程
当底层协议栈检测到新设备时,会通过回调机制触发DiscOnDeviceFound函数。这个函数是整个设备发现事件处理流程的入口点:
static void DiscOnDeviceFound(const DeviceInfo *device, const InnerDeviceInfoAddtions *additions)
{
DISC_CHECK_AND_RETURN_LOGW(device != NULL, DISC_CONTROL, "device is null");
DISC_CHECK_AND_RETURN_LOGW(additions != NULL, DISC_CONTROL, "additions is null");
DISC_LOGD(DISC_CONTROL,
"capabilityBitmap=%{public}d, medium=%{public}d", device->capabilityBitmap[0], additions->medium);
for (uint32_t tmp = 0; tmp < CAPABILITY_MAX_BITNUM; tmp++) {
if (IsBitmapSet((uint32_t *)device->capabilityBitmap, tmp) == false) {
continue;
}
if (SoftBusMutexLock(&(g_discoveryInfoList->lock)) != SOFTBUS_OK) {
DISC_LOGE(DISC_CONTROL, "lock failed");
return;
}
DiscInfo *infoNode = NULL;
LIST_FOR_EACH_ENTRY(infoNode, &(g_capabilityList[tmp]), DiscInfo, capNode) {
DISC_LOGD(DISC_CONTROL, "find callback id=%{public}d", infoNode->id);
infoNode->statistics.discTimes++;
InnerDeviceFound(infoNode, device, additions);
}
(void)SoftBusMutexUnlock(&(g_discoveryInfoList->lock));
}
}
函数接收DeviceInfo结构体和InnerDeviceInfoAddtions结构体作为参数,分别包含了发现设备的基本信息和附加信息。DiscOnDeviceFound函数首先解析设备的能力位图,通过遍历CAPABILITY_MAX_BITNUM个位来确定设备具备哪些能力。对于设备具备的每种能力,函数都会在g_capabilityList数组中找到对应的订阅者链表。在访问这些链表之前,函数通过SoftBusMutexLock获取g_discoveryInfoList的互斥锁,确保在多线程环境下的数据一致性。
在找到相关的订阅者链表后,函数使用LIST_FOR_EACH_ENTRY宏遍历链表中的每个DiscInfo对象。对于每个订阅者,函数首先更新其statistics.discTimes统计信息,记录该订阅者接收到设备发现事件的次数,然后调用InnerDeviceFound函数进行具体的事件处理。
下图展示了设备发现事件的处理流程:
内外部模块的差异化处理机制
InnerDeviceFound函数负责根据订阅者的类型来选择不同的通知机制:
static void InnerDeviceFound(DiscInfo *infoNode, const DeviceInfo *device, const InnerDeviceInfoAddtions *additions)
{
if (IsInnerModule(infoNode) == false) {
(void)infoNode->item->callback.serverCb.OnServerDeviceFound(infoNode->item->packageName, device, additions);
return;
}
DISC_LOGD(DISC_CONTROL, "call from inner module.");
if (infoNode->item->callback.innerCb.OnDeviceFound != NULL) {
DfxRecordDeviceFound(infoNode, device, additions);
infoNode->item->callback.innerCb.OnDeviceFound(device, additions);
}
}
函数首先调用IsInnerModule来判断当前的订阅者是否属于系统内部模块:
static bool IsInnerModule(const DiscInfo *infoNode)
{
for (uint32_t i = 0; i < MODULE_MAX; i++) {
DISC_LOGD(DISC_CONTROL, "packageName=%{public}s", infoNode->item->packageName);
if (strcmp(infoNode->item->packageName, g_discModuleMap[i]) == 0) {
DISC_LOGD(DISC_CONTROL, "true");
return true;
}
}
DISC_LOGD(DISC_CONTROL, "false");
return false;
}
这个判断基于DiscInfo对象关联的DiscItem中的packageName字段,通过与预定义的g_discModuleMap数组进行比较来确定模块类型。
对于外部应用模块,函数调用DiscItem中callback联合体的serverCb.OnServerDeviceFound回调函数。这个回调函数会将设备发现事件通过IPC机制传递给应用进程,其中包含了包名信息用于权限验证和设备信息的完整传输。
对于内部系统模块,函数直接调用callback联合体的innerCb.OnDeviceFound回调函数。由于内部模块与发现管理器运行在同一进程中,这种直接函数调用的方式具有更高的执行效率。在调用回调函数之前,系统还会调用DfxRecordDeviceFound函数记录相关的统计信息,包括首次发现时间、发现次数等数据,这些信息用于系统性能分析和故障诊断。