简介:分布式软总线组件面向OpenHarmony底层开发与系统移植人员,聚焦近场设备间统一通信管理这一核心难题。现实中WiFi、蓝牙等通信方式差异大、链路融合共享与冲突难以处理,该组件提供不区分链路的设备发现连接、组网与传输能力,并统一管理设备拓扑,为数据传输提供已组网设备信息,支持消息、字节、流、文件等多种传输形态。资源包共2000个文件,以702个h头文件、531个c与433个cpp源文件为主体,辅以137个gn、24个gni构建脚本及91个xml配置、70个init初始化文件,另有少量json、py、yaml等,整体约3.7MB,覆盖发现、组网、传输各模块实现。已有383人学习。读者可借此深入理解软总线连接管理、BLE与BR发现、P2P处理、DFile文件传输及网络账本等关键机制,适合作为源码研读与二次开发的参考底本。
1. 分布式软总线组件拆包:从发现到传输,这套源码能跑通什么
多设备通信最让人头疼的不是写不出代码,而是同一套业务逻辑在 WiFi 上能跑,换到蓝牙就翻车。分布式软总线要解决的就是这件事:把 WiFi、蓝牙这些差异极大的物理链路抽象成统一的发现、组网、传输接口,上层业务不用关心底层走的是哪条路。这次拆的这套组件源码,覆盖了软总线最核心的三块能力——设备发现(disc_ble.c、p2p_v1_processor.c)、组网管理(lnn_net_builder.c、lnn_distributed_net_ledger.c)、数据传输(softbus_conn_ble_manager.c、softbus_conn_br_manager.c、fillp_conn.c、nstackx_dfile_session.c、nstackx_file_manager.c、client_trans_proxy_file_manager.c)。适合谁看?如果你在做 OpenHarmony 底层组件适配、多设备协同功能开发,或者单纯想搞清楚近场通信的发现-组网-传输全链路是怎么串起来的,这套源码值得逐文件过一遍。它不是 demo 级别的玩具,而是把 BLE 发现、BR 连接管理、DFile 文件传输、FillP 可靠传输这些模块都拆开实现的工程代码。
2. 发现层拆解:BLE 广播与 P2P 协商怎么把设备捞出来
发现是软总线的入口。没有发现,后面组网和传输都无从谈起。这套源码里发现层主要分两条线:一条是 BLE 广播发现(disc_ble.c),另一条是 P2P 协商发现(p2p_v1_processor.c)。两条线最终都会把发现的设备信息写入 lnn_distributed_net_ledger.c 维护的设备账本里,供组网层使用。
2.1 disc_ble.c 的广播与扫描逻辑
BLE 发现的核心是广播包和扫描响应。设备周期性地发送广播包,包里携带设备 ID、能力位、服务 UUID 等关键信息。扫描方收到广播后解析出设备信息,判断是否为目标设备类型,然后决定是否发起连接。
// disc_ble.c 中广播数据组装的核心逻辑(简化示意) static int ble_start_adv(void) { BleAdvData adv_data = {0}; // 设置广播标志位,标识设备可被发现 adv_data.flags = BLE_ADV_FLAG_GENERAL_DISCOVERABLE; // 填入设备唯一标识,用于对端识别 adv_data.device_id = GetLocalDeviceId(); // 填入能力位图,告诉对端本设备支持哪些传输方式 adv_data.capability = CAP_BLE | CAP_WIFI | CAP_BR; // 设置服务 UUID,扫描方据此过滤 adv_data.service_uuid = SOFTBUS_SERVICE_UUID; return BleHalStartAdv(&adv_data); }这段代码的关键参数有三个:flags决定设备是否可被发现,capability位图让对端知道本设备支持哪些链路,service_uuid是扫描过滤的依据。实际调试时最常见的坑是广播包超过 31 字节导致截断,设备 ID 和服务 UUID 只能二选一精简。我一般会把设备 ID 做哈希压缩到 6 字节以内,把更多空间留给能力位和服务信息。
扫描侧的逻辑在ble_start_scan里,需要设置扫描窗口和扫描间隔。窗口越大发现越快但功耗越高,窗口越小则相反。常见做法是快发现阶段用 30ms 窗口 + 30ms 间隔,发现目标后切到低速扫描维持连接。
2.2 p2p_v1_processor.c 的协商流程
P2P 发现走的是另一条路,依赖 WiFi 的 P2P 能力做设备探测和协商。p2p_v1_processor.c 里实现了一套状态机,从 IDLE 到 PROBE 到 NEGOTIATE 再到 CONNECTED,每个状态处理不同的消息类型。
// p2p_v1_processor.c 状态机处理入口(简化示意) static void p2p_process_message(P2pMessage *msg) { switch (g_p2p_state) { case P2P_STATE_IDLE: // 收到探测请求,回复探测响应 if (msg->type == P2P_MSG_PROBE_REQ) { p2p_send_probe_rsp(msg->src_addr); g_p2p_state = P2P_STATE_PROBE; } break; case P2P_STATE_PROBE: // 收到协商请求,进入协商流程 if (msg->type == P2P_MSG_NEGO_REQ) { p2p_handle_nego(msg); g_p2p_state = P2P_STATE_NEGOTIATE; } break; case P2P_STATE_NEGOTIATE: // 协商完成,建立连接 if (msg->type == P2P_MSG_NEGO_CNF) { p2p_establish_link(msg); g_p2p_state = P2P_STATE_CONNECTED; } break; default: break; } }状态机的每个状态都有超时保护,超时后会回退到 IDLE 重新开始。这里容易翻车的地方是协商消息的序列号处理——如果对端重发协商请求而本地没有去重,会重复进入协商流程导致状态错乱。血泪经验是:所有协商消息必须带序列号,收到重复序列号直接丢弃并回复上一次的协商结果。
2.3 设备账本 lnn_distributed_net_ledger.c 的数据结构
发现到的设备信息最终写入设备账本。这个文件维护了一张设备信息表,每条记录包含设备 ID、设备类型、能力位、连接状态、最近活跃时间等字段。
// lnn_distributed_net_ledger.c 设备信息节点定义(简化示意) typedef struct { char device_id[DEVICE_ID_MAX_LEN]; // 设备唯一标识 uint32_t capability; // 能力位图 uint32_t conn_state; // 当前连接状态 uint64_t last_active_time; // 最近活跃时间戳 ListNode *link_list; // 该设备上的可用链路列表 } DeviceLedgerNode; // 插入或更新设备信息 int32_t UpdateDeviceInfo(const char *device_id, const DeviceInfo *info) { DeviceLedgerNode *node = FindLedgerNode(device_id); if (node == NULL) { node = CreateLedgerNode(device_id); if (node == NULL) { return SOFTBUS_ERR; } AddToLedgerList(node); } // 更新能力位和活跃时间 node->capability = info->capability; node->last_active_time = GetCurrentTimeMs(); return SOFTBUS_OK; }账本的核心作用是给组网层提供“当前有哪些设备可用、每台设备有哪些链路可用”的查询能力。参数上需要特别注意last_active_time的更新时机——每次收到设备的心跳或数据包都要刷新,否则设备会被误判为离线。常见做法是设置一个 30 秒的过期阈值,后台定时任务扫描超时节点并清理。
3. 组网层实战:lnn_net_builder.c 怎么把设备串成一张网
发现只是把设备找出来,组网才是决定设备之间怎么连、走哪条链路、拓扑怎么维护。lnn_net_builder.c 是组网层的核心,负责根据设备能力和链路质量选择最优路径,维护网络拓扑,处理链路变化。
3.1 组网决策的输入与输出
组网决策的输入来自设备账本和链路质量探测。每台设备的能力位告诉组网层它支持哪些链路(BLE、BR、WiFi),链路质量探测则给出每条链路的实时评分(信号强度、丢包率、带宽估计)。组网层的输出是一张拓扑图,描述每台设备应该通过哪条链路与哪些设备直连。
// lnn_net_builder.c 链路选择核心逻辑(简化示意) static LinkType SelectBestLink(const DeviceLedgerNode *node) { LinkType best_link = LINK_TYPE_NONE; uint32_t best_score = 0; ListNode *iter = node->link_list; while (iter != NULL) { LinkInfo *link = (LinkInfo *)iter->data; // 综合评分:信号强度权重 0.4,带宽权重 0.4,稳定性权重 0.2 uint32_t score = link->rssi * 4 + link->bandwidth * 4 + link->stability * 2; if (score > best_score) { best_score = score; best_link = link->type; } iter = iter->next; } return best_link; }评分权重是可以调的。如果业务对带宽敏感(比如文件传输),把带宽权重调高;如果对延迟敏感(比如消息推送),把信号强度和稳定性权重调高。我一般会在初始化时根据业务场景预设一组权重,运行过程中根据实际传输质量动态微调。
3.2 拓扑维护与链路切换
拓扑维护的核心是处理链路变化。当一条链路断开时,组网层需要快速判断是否有备用链路可以切换,如果没有则通知上层业务。lnn_net_builder.c 里用了一个事件驱动模型,链路状态变化会触发OnLinkStateChanged回调,回调里执行拓扑重算。
// 链路状态变化处理(简化示意) static void OnLinkStateChanged(const char *device_id, LinkType type, LinkState state) { if (state == LINK_STATE_DOWN) { // 链路断开,从账本中移除该链路 RemoveLinkFromLedger(device_id, type); // 尝试选择备用链路 LinkType backup = SelectBestLink(FindLedgerNode(device_id)); if (backup != LINK_TYPE_NONE) { // 有备用链路,发起切换 SwitchToLink(device_id, backup); } else { // 无备用链路,通知业务层设备不可达 NotifyDeviceUnreachable(device_id); } } }链路切换最容易踩的坑是切换过程中的数据丢失。如果切换时还有未发送完的数据包,直接丢弃会导致业务层超时重传,体验很差。常见做法是在切换前先把待发送数据缓存起来,切换完成后重新发送。缓存大小需要根据业务数据量设置,太小会丢包,太大会占内存。
3.3 组网与发现的联动
组网层不是独立工作的,它需要发现层持续提供设备信息。发现层每发现一台新设备就写入账本,组网层定时扫描账本决定是否发起组网。这个联动关系里有一个关键参数:组网触发阈值。如果发现一台设备就立刻组网,会导致频繁组网开销大;如果等太久,业务层会感知到设备可用性延迟。
我一般会把阈值设为“发现后 500ms 内如果设备仍在线且能力匹配,则触发组网”。这个时间窗口足够过滤掉偶发的广播包,又不会让业务层等太久。lnn_net_builder.c 里通过定时器实现这个逻辑,定时器周期设为 200ms,每次扫描账本中新增的设备节点。
4. 传输层拆解:从 BLE 连接管理到 DFile 文件传输
传输层是软总线最复杂的部分,因为要同时支持消息、字节、流、文件四种数据类型,还要适配 BLE、BR、WiFi 三种链路。这套源码里传输层分了好几个模块:softbus_conn_ble_manager.c 和 softbus_conn_br_manager.c 负责链路连接管理,fillp_conn.c 负责可靠传输,nstackx_dfile_session.c 和 nstackx_file_manager.c 负责文件传输,client_trans_proxy_file_manager.c 负责代理文件管理。
4.1 softbus_conn_ble_manager.c 的连接管理
BLE 连接管理的核心是维护连接状态机和数据收发通道。BLE 的传输特点是 MTU 小(默认 23 字节,协商后可到 512 字节)、速率低,所以连接管理里需要做数据分片和重组。
// softbus_conn_ble_manager.c 数据发送分片逻辑(简化示意) int32_t BleSendData(const char *device_id, const uint8_t *data, uint32_t len) { uint32_t mtu = GetBleMtu(device_id); uint32_t offset = 0; while (offset < len) { uint32_t chunk_size = (len - offset) > mtu ? mtu : (len - offset); // 每个分片带序号,接收端据此重组 BlePacket packet = { .seq = offset / mtu, .total_len = len, .chunk_len = chunk_size, .payload = data + offset }; int32_t ret = BleSendPacket(device_id, &packet); if (ret != SOFTBUS_OK) { return ret; } offset += chunk_size; } return SOFTBUS_OK; }分片的关键参数是 MTU。BLE 4.0 默认 MTU 是 23 字节,实际可用载荷只有 20 字节;BLE 4.2 以后支持 MTU 协商,最大可以到 512 字节。如果协商失败,代码会回退到默认 MTU。这里容易翻车的地方是分片序号溢出——如果数据量很大,序号用 uint16 会回绕,接收端需要处理回绕逻辑。常见做法是用 uint32 做序号,或者用偏移量代替序号。
4.2 fillp_conn.c 的可靠传输
FillP 是软总线自研的可靠传输协议,跑在 BLE 和 BR 链路之上,提供类似 TCP 的可靠有序传输。fillp_conn.c 里实现了滑动窗口、确认应答、超时重传、拥塞控制等机制。
// fillp_conn.c 滑动窗口发送逻辑(简化示意) static int32_t FillpSendWithWindow(FillpConn *conn, const uint8_t *data, uint32_t len) { while (conn->send_window > 0 && conn->send_offset < len) { uint32_t chunk = (len - conn->send_offset) > conn->mss ? conn->mss : (len - conn->send_offset); // 发送数据段 FillpSendSegment(conn, data + conn->send_offset, chunk); conn->send_offset += chunk; conn->send_window--; // 启动重传定时器 StartRetransTimer(conn, conn->send_offset); } return SOFTBUS_OK; }FillP 的关键参数有三个:MSS(最大段大小)、窗口大小、重传超时。MSS 根据链路 MTU 计算,BLE 链路上一般设为 200 字节左右;窗口大小决定并发能力,BLE 上一般设为 4-8 个 MSS;重传超时根据链路 RTT 动态调整,初始值设为 500ms,每次重传翻倍。血泪经验是:BLE 链路的 RTT 波动很大,固定超时值会导致大量不必要的重传,必须做动态 RTT 估计。
4.3 nstackx_dfile_session.c 与文件传输
DFile 是软总线的文件传输模块,nstackx_dfile_session.c 管理传输会话,nstackx_file_manager.c 管理文件分片和校验。文件传输的核心是把大文件切成小块,通过 FillP 可靠传输到对端,对端重组后校验完整性。
// nstackx_dfile_session.c 文件传输会话初始化(简化示意) int32_t DFileSessionCreate(const char *device_id, const char *file_path) { DFileSession *session = calloc(1, sizeof(DFileSession)); // 打开文件,获取文件大小 session->file_fd = open(file_path, O_RDONLY); session->file_size = GetFileSize(session->file_fd); // 计算分片数量,每片默认 64KB session->total_blocks = (session->file_size + DFILE_BLOCK_SIZE - 1) / DFILE_BLOCK_SIZE; // 初始化传输位图,标记哪些块已确认 session->ack_bitmap = calloc(session->total_blocks, sizeof(uint8_t)); // 建立 FillP 连接 session->conn = FillpConnCreate(device_id); return SOFTBUS_OK; }文件传输的参数里,分片大小是最关键的。分片太小会导致确认包过多,传输效率低;分片太大会导致单次传输时间长,出错重传代价高。常见做法是 BLE 链路用 4KB 分片,WiFi 链路用 64KB 分片。nstackx_file_manager.c 里还实现了断点续传——传输中断后重新连接时,根据 ack_bitmap 跳过已确认的块。
4.4 client_trans_proxy_file_manager.c 的代理管理
代理文件管理模块负责在文件传输过程中做权限校验、路径映射、传输进度回调。它的作用是让上层业务不用直接操作文件描述符,而是通过代理接口发起传输请求。
// client_trans_proxy_file_manager.c 代理传输接口(简化示意) int32_t ProxySendFile(const char *device_id, const char *local_path, const char *remote_path) { // 校验本地文件是否存在且可读 if (access(local_path, R_OK) != 0) { return SOFTBUS_ERR; } // 创建传输会话 DFileSession *session = DFileSessionCreate(device_id, local_path); if (session == NULL) { return SOFTBUS_ERR; } // 设置远端路径映射 DFileSessionSetRemotePath(session, remote_path); // 注册进度回调 DFileSessionSetProgressCallback(session, OnTransferProgress); // 启动传输 return DFileSessionStart(session); }代理层的核心价值是解耦。业务层只需要调用ProxySendFile传入本地路径和远端路径,不需要关心底层用的是 BLE 还是 WiFi,也不需要关心分片和重传。进度回调让业务层可以展示传输进度,常见做法是每传输 5% 回调一次,避免回调过于频繁影响性能。
5. 避坑与排查:这套源码跑起来最容易翻车的五个地方
5.1 发现不到设备,广播包被系统过滤
现象:BLE 扫描一直扫不到目标设备,但用通用蓝牙调试工具能看到广播包。
原因:OpenHarmony 的 BLE 扫描有白名单过滤机制,如果目标设备的广播包里的服务 UUID 不在白名单里,扫描结果会被系统过滤掉。
解决:检查 disc_ble.c 里注册的服务 UUID 是否与扫描侧的白名单一致。常见做法是在初始化时调用BleSetScanFilter把软总线的服务 UUID 加入白名单。另外注意广播包长度不能超过 31 字节,超长会导致广播失败。
5.2 组网成功但传输失败,链路选择与实际能力不匹配
现象:设备账本显示组网成功,但发起文件传输时报“链路不可用”。
原因:组网层选择的链路类型与传输层实际支持的链路类型不一致。比如组网层选了 BLE 链路,但传输层初始化时只注册了 WiFi 传输通道。
解决:检查 lnn_net_builder.c 里的链路选择逻辑和传输层的链路注册逻辑是否对齐。常见做法是在组网决策前先查询传输层的可用链路列表,只从可用链路里选。另外注意链路能力位要在发现阶段就协商好,不能等到传输时才发现对端不支持。
5.3 文件传输中途卡死,FillP 窗口耗尽
现象:文件传输到一半进度条不动了,日志显示 FillP 发送窗口为 0。
原因:接收端的确认包丢失或延迟,导致发送端窗口无法释放。BLE 链路丢包率较高时容易出现这个问题。
解决:检查 fillp_conn.c 里的重传定时器是否正常工作。常见做法是设置一个窗口探测定时器,当窗口为 0 超过一定时间(比如 2 秒)时主动发送探测包,触发接收端回复确认。另外可以适当增大窗口初始值,给丢包留出缓冲空间。
5.4 设备账本内存泄漏,长时间运行后 OOM
现象:设备运行几个小时后内存持续增长,最终 OOM 被杀。
原因:lnn_distributed_net_ledger.c 里设备下线后没有释放对应的账本节点,或者链路列表没有清理。
解决:检查设备下线处理逻辑,确保调用RemoveLedgerNode释放节点内存。常见做法是加一个后台定时任务,每 60 秒扫描一次账本,清理超过 5 分钟未活跃的设备节点。另外注意链路列表里的 LinkInfo 结构也要逐个释放,不能只释放头节点。
5.5 BLE 连接频繁断开重连,连接参数不合理
现象:BLE 连接建立后几秒内就断开,然后自动重连,循环往复。
原因:BLE 连接参数(连接间隔、从机延迟、监督超时)设置不合理。连接间隔太短会导致功耗高,太长会导致响应慢;监督超时太短会导致误判断开。
解决:检查 softbus_conn_ble_manager.c 里的连接参数配置。常见做法是连接间隔设为 30ms,从机延迟设为 0,监督超时设为 2 秒。如果业务对功耗敏感,可以把连接间隔调到 100ms 以上,但要注意传输延迟会增加。
6. 进阶技巧:用日志和抓包把软总线黑匣子打开
软总线的问题排查最难的地方在于链路层是黑匣子——你看到的是业务层报错,但不知道底层到底发生了什么。我一般会从三个层面入手:日志分级、抓包分析、性能计数。
日志分级方面,这套源码里每个模块都有自己的日志宏。建议在调试时把发现层和组网层的日志级别调到 DEBUG,传输层调到 INFO。传输层日志量太大,DEBUG 级别会拖慢性能。可以在softbus_conn_ble_manager.c里加一个条件编译开关,只在特定设备 ID 上打开 DEBUG 日志。
// 条件日志示例:只对特定设备打开详细日志 #define TARGET_DEVICE_ID "target_device_001" #define BLE_LOG_DEBUG(device_id, fmt, ...) \ do { \ if (strcmp(device_id, TARGET_DEVICE_ID) == 0) { \ SOFTBUS_LOG_DEBUG(fmt, ##__VA_ARGS__); \ } \ } while (0)抓包分析方面,BLE 链路可以用 HCI 日志抓包,WiFi 链路可以用空中抓包。重点看三个东西:广播包内容是否完整、连接建立时的参数协商是否成功、数据传输时的确认包是否及时。FillP 的确认包在抓包工具里可能显示为自定义协议,需要对照 fillp_conn.c 里的包格式手动解析。
性能计数方面,建议在关键路径上加计数器:发现到的设备数、组网成功次数、传输字节数、重传次数、窗口耗尽次数。这些计数器可以导出到系统日志,运行一段时间后分析趋势。比如重传次数持续增长说明链路质量在恶化,窗口耗尽次数多说明接收端处理能力不足。
| 排查层面 | 工具 | 关键指标 | 常见问题定位 |
|---|---|---|---|
| 日志分级 | 系统日志 | DEBUG/INFO 级别日志量 | 模块初始化失败、状态机异常 |
| 抓包分析 | HCI 日志/空中抓包 | 广播包完整性、确认包延迟 | 发现失败、传输卡死 |
| 性能计数 | 自定义计数器 | 重传率、窗口耗尽次数 | 链路质量恶化、接收端瓶颈 |
从那以后我每次接手软总线相关的问题,都强制走一遍“日志分级→抓包→计数器”三步排查流程,先定位是发现层、组网层还是传输层的问题,再深入具体模块。这套源码的模块划分很清晰,只要定位到层,剩下的就是看代码和加日志的事。希望帮到你。
本文还有配套的精品资源,点击获取