软总线-传输模块-多通道并行协商

本篇不讲那些设计模式,架构什么的,感觉几十万行代码,看的人都晕了,停了一周,感觉之前理解方式错了,我刚开始想着熟悉事件流程,出了问题可以判断出在那个环节,但是对于这种大型项目来说,根本看不过来,看了前面忘了后面。我和豆包聊了一会,再结合之前和同事聊天,决定换个方式,从模块的设计策略,处理了什么难题,获得了什么,舍弃了什么,异常处理什么问题,这些角度来分析。这是我接下来的策略,本部分不会上传到Laval,Laval发布的其实都有裁剪过。

多通道并行协商听着很高大上,其实还是挺好理解的,首先要了解一些前置,传输模块是负责设备之间通信传输的模块,在开始通信之前,我记得之前内部培训,设备会先注册到云端,有什么发现发布流程,那么就是在这个交互过程中会在本地有一个networkid记录,本地会拿着这个networkid去查询对端信息,创建一个sessionid(这只是业务流程,还没真正开始,因为还有appinfo等很多结构体还没装配),会话对应着一个channel,这里只能对应一个channel,这个是在最上层的结构体写死的,一个会话只能负责一个channel

session会话与channel通道区别

会话算是应用层,代表业务链路的上下文,存着业务所需的像包名、权限、业务标识这些的

channel通道是真实承载数据流底层载体

  • 选择 1:1 核心原因:
    • 分布式业务场景天然是一单业务对应一条数据流;
    • 极大简化状态机、会话取消、资源回收、QoS、保序逻辑(参考你 TransOpenChannel 代码);
    • 规避多通道带来的报文乱序、资源竞争、生命周期管理复杂度;
  • 如果业务需要多路数据流,框架推荐新建多个 Session,而不是单 Session 创建多条 Channel。

channel通道与Lane车道区别

通道是类似于管理器,我要传这个数据流,通过什么方式传输,而lane是具体的物理链路的代名词,比如是采取WiFi还是蓝牙链路。

一个通道channel可以对应多个lane,比如当WiFi的质量不太好的时候可以切换到蓝牙传输,或者像大数据量传输的时候,可以采取多个lane并发传输,这样效率更高,这方面的算法还没有分析,可以先做一个埋点。

channel通道与Lane车道创建时机

之前在逻辑理解上,是一个session后面接一个channel,一个channel后面接着一个或者多个lane,但是创建时机上,会先申请lane,再将LaneConnInfo转换成ConnectOption,用来创建channel通道。

TransOpenChannel 被调用
        │
        ▼
  ① 申请 Lane(找路/修路)
     - TransGetLaneInfo / TransAsyncGetLaneInfo
     - 输出:laneHandle + LaneConnInfo(路的信息)
        │
        ▼
  ② Lane 申请成功了,路有了
     - 状态置为 LAN_COMPLETE
        │
        ▼
  ③ 打开 Channel(在这条路上开车道)
     - TransOpenChannelProc ← 你之前问的函数
     - 输入:LaneConnInfo 转成的 ConnectOption
     - 输出:channelId
        │
        ▼
  ④ 把 channel 和 lane 绑定起来
     - TransLaneMgrAddLane

Lane复用机制

复用机制核心数据结构:lnn_lane_link.h

typedef struct {
    bool isServerSide;       // 是服务端还是客户端
    uint32_t laneScore;      // 车道评分(质量分)
    uint32_t laneFload;      // 车道负载
    uint32_t clientRef;      // 【关键】客户端引用计数
    LaneLinkInfo link;       // 链路具体信息
    ListNode node;
    uint64_t laneId;         // 全局唯一lane ID
} LaneResource;

其中clientRef就是lane的引用计数。

申请Lane流程

lnn_lane_link.c

申请 Lane
    │
    ▼
  查资源池(g_laneResource)
    │
    ├─ 有可用的同类型lane?
    │    └─ 是 → clientRef++ → 直接返回 → 复用成功
    │
    └─ 没有 → 新建链路 → 加入资源池 → clientRef = 1 → 新建成功

Lane选择策略

Lane选择不一定是选择复用 lnn_lane_select.c

策略触发条件行为
reuseBestEffort手表设备等低功耗场景优先复用已有 BR 链路,不新建
默认链路普通请求按默认优先级选链路
QoS 驱动指定了带宽 / 延迟要求按 QoS 选最合适的链路
RTT 优先要求低延迟选 RTT 最低的链路
SelectExpectLanesByQos:
if (request->qosRequire.reuseBestEffort) {
    ret = DecideReuseLane(networkId, request, &laneLinkList);
} else if (request->qosRequire.minBW == 0 && ...) {
    ret = DecideDefaultLink(...);
} else if (request->qosRequire.rttLevel == LANE_RTT_LEVEL_LOW) {
    ret = DecideCustomLink(networkId, CUSTOM_QOS_RTT, ...);
} else {
    ret = DecideAvailableLane(networkId, request, &laneLinkList);
}

多通道并行协商

之前讲到这个项目设计的是一个channel可以使用多个lane,一个lane也可以被多个channel同时使用,这里面就容易产生资源竞争的问题,这个项目并不是采用简单的多路复用机制来解决问题,而是采用一套分层机制来实现

核心机制:同一物理链路只有一份资源记录,谁用谁加引用

1. 去重基础:laneId 是确定性生成的

GenerateLaneId 把两端 udid 排序后拼接 + linkType,做哈希得到 64 位 laneId:

  • 同一对设备 + 同一种链路类型 → laneId 永远相同
  • 所以不管多少个 channel 请求,最终都落到同一个 LaneResource 上,这是"一个 lane 被多个 channel 共享"的根基
uint64_t GenerateLaneId(const char *localUdid, const char *remoteUdid, LaneLinkType linkType)
{
    if (localUdid == NULL || remoteUdid == NULL) {
        LNN_LOGE(LNN_LANE, "udid is NULL");
        return SOFTBUS_INVALID_PARAM;
    }
    const char *bigUdid = NULL;
    const char *smallUdid = NULL;
    if (strcmp(localUdid, remoteUdid) >= 0) {
        bigUdid = localUdid;
        smallUdid = remoteUdid;
    } else {
        bigUdid = remoteUdid;
        smallUdid = localUdid;
    }
    uint8_t laneIdParamBytes[LANE_ID_BUF_LEN];
    (void)memset_s(laneIdParamBytes, sizeof(laneIdParamBytes), 0, sizeof(laneIdParamBytes));
    uint64_t laneId = INVALID_LANE_ID;
    uint16_t type = (uint16_t)linkType;
    // sharded copy, LANE_ID_BUF_LEN = UDID_BUF_LEN + UDID_BUF_LEN + TYPE_BUF_LEN
    if (memcpy_s(laneIdParamBytes, UDID_BUF_LEN, bigUdid, strlen(bigUdid)) == EOK &&
        memcpy_s(laneIdParamBytes + UDID_BUF_LEN, UDID_BUF_LEN, smallUdid, strlen(smallUdid)) == EOK &&
        memcpy_s(laneIdParamBytes + UDID_BUF_LEN + UDID_BUF_LEN, TYPE_BUF_LEN, &type, sizeof(type)) == EOK) {
        uint8_t laneIdHash[LANE_ID_HASH_LEN] = {0};
        if (SoftBusGenerateStrHash(laneIdParamBytes, LANE_ID_BUF_LEN, laneIdHash) != SOFTBUS_OK) {
            LNN_LOGE(LNN_LANE, "generate laneId hash fail");
            return INVALID_LANE_ID;
        }
        uint32_t len = sizeof(laneId) <= LANE_ID_HASH_LEN ? sizeof(laneId) : LANE_ID_HASH_LEN;
        if (memcpy_s(&laneId, sizeof(laneId), laneIdHash, len) != EOK) {
            LNN_LOGE(LNN_LANE, "memcpy laneId hash fail");
            return INVALID_LANE_ID;
        }
        char *anonyLocalUdid = NULL;
        char *anonyRemoteUdid = NULL;
        Anonymize(localUdid, &anonyLocalUdid);
        Anonymize(remoteUdid, &anonyRemoteUdid);
        LNN_LOGI(LNN_LANE, "generate laneId=%{public}" PRIu64 " with localUdid=%{public}s,"
            "remoteUdid=%{public}s, linkType=%{public}d",
            laneId, AnonymizeWrapper(anonyLocalUdid), AnonymizeWrapper(anonyRemoteUdid), linkType);
        AnonymizeFree(anonyLocalUdid);
        AnonymizeFree(anonyRemoteUdid);
        return laneId;
    }
    LNN_LOGE(LNN_LANE, "memcpy laneId param bytes fail");
    return INVALID_LANE_ID;
}
输入: localUdid, remoteUdid, linkType
         ↓
[1] 字符串排序 确保一致性
    - 用 strcmp 比较两个 UDID
    - 较大的放前面 (bigUdid)
    - 较小的放后面 (smallUdid)
         ↓
[2] 构造字节数组
    laneIdParamBytes = bigUdid + smallUdid + linkType
    (共 LANE_ID_BUF_LEN 字节)
         ↓
[3] 生成 Hash
    调用 SoftBusGenerateStrHash 对字节数组做 Hash
         ↓
[4] 返回 Hash 值作为 Lane ID (uint64_t)

2. 资源池 + 引用计数(核心)

全局资源池 g_laneResource(一个链表),每次建链成功后调用 AddLaneResourceToPool

释放时 DelLaneResourceByLaneIdIsNeedDelResource--clientRef只有 clientRef == 0 且不是 server 端时才真正删除资源(此时才去拆 P2P 连接)。

int32_t AddLaneResourceToPool(const LaneLinkInfo *linkInfo, uint64_t laneId, bool isServerSide)
{
    if (linkInfo == NULL || laneId == INVALID_LANE_ID) {
        LNN_LOGE(LNN_LANE, "linkInfo is nullptr or invalid laneId");
        return SOFTBUS_INVALID_PARAM;
    }
    if (LaneLock() != SOFTBUS_OK) {
        LNN_LOGE(LNN_LANE, "lane lock fail");
        return SOFTBUS_LOCK_ERR;
    }
    int32_t addResult = SOFTBUS_LANE_RESOURCE_EXCEPT;
    LaneResource* resourceItem = GetValidLaneResource(linkInfo);
    if (resourceItem != NULL) {
        addResult = UpdateExistLaneResource(resourceItem, isServerSide);
        LaneUnlock();
        return addResult;
    }
    LaneUnlock();
    addResult = CreateNewLaneResource(linkInfo, laneId, isServerSide);
    if (addResult != SOFTBUS_OK) {
        LNN_LOGE(LNN_LANE, "create laneResource fail, result=%{public}d", addResult);
        return addResult;
    }
    if (!isServerSide) {
        AddNetworkResourceInner(linkInfo, laneId);
    }
    return SOFTBUS_OK;
}
查找资源 GetValidLaneResource
         ↓
    ┌─────┴─────┐
    │  存在?     │
    └─────┬─────┘
       Yes │     No
      ┌────┴────┐
      ↓         ↓
[存在]更新资源  [不存在]
UpdateExist    先 LaneUnlock()
  Lane         再 CreateNew
  Resource     NewLaneResource
      ↓         ↓
  LaneUnlock   return
      ↓
   return

3. 互斥锁:每个共享数据区一把锁

位置保护对象
g_laneMutexlnn_lane.c:75laneReqId 位图、listener 链表
g_laneResource.locklnn_lane_link.c:66资源池增删改查
g_p2pLinkMutexlnn_lane_link_p2p.c:161进行中的 P2P 建链/拆链请求
g_linkConflictList.locklnn_lane_link_conflict.c:40冲突记录表

注意所有查找都是锁内 memcpy 副本、锁外使用(如 FindLaneResourceByLaneId:665-690),避免锁释放后悬垂指针。

4. P2P/WiFiDirect 连接的并发合并

LnnConnectP2p 是先尝试复用、不复用才建链:(/foundation/communication/dsoftbus/core/bus_center/lnn/lane_hub/lane_manager/src/lnn_lane_link_p2p.c)

  1. TryWifiDirectReuse:查资源池发现 (peerUdid, linkType) 已有 lane 且 wifi direct 判定无需重新协商信道 → 直接挂载到已有连接上,不发起新连接
  2. 失败才走 SelectGuideChannel 协商建链,且建链请求本身也记录在 g_p2pLinkList 里做进行中去重
 if (TryWifiDirectReuse(request, laneReqId, callback) == SOFTBUS_OK) {
        return SOFTBUS_OK;
    }
    return SelectGuideChannel(request, laneReqId, callback);

5. 物理层冲突的"记录 + 避让"

手机场景真正稀缺的是射频资源,所以还有一层冲突管理(lnn_lane_link_conflict.c):

  • GetConflictTypeWithErrcode:155 把建链失败错误码归类为:角色冲突(GO/GC)、链路数上限LNN_LANE_P2P_MAX_NUM = 4)、三 VAP 冲突、软 AP 芯片冲突
  • AddLinkConflictInfo 记录冲突,带 30 秒时效CONFLICT_INFO_TIMELINESS),超时自动删除
  • 后续分配时查 FindLinkConflictInfoByDevId 避让刚冲突过的设备;释放 HML 时用 CheckLinkConflictByReleaseLink 判断是否有排队请求等着这条链路

6. 双端对称注册,防止"一端拆一端用"

P2P/HML 是双端协商的,链路两端的设备都会各自把这条链路注册进自己的资源池。LaneResource.isServerSide 标记这条链路是谁发起的:server 端即使本地 clientRef 归零也不会拆物理链路IsNeedDelResource:458-474),只有等对端释放才清理 —— 避免了双端时序不一致导致的竞争。(/foundation/communication/dsoftbus/core/bus_center/lnn/lane_hub/lane_manager/src/lnn_lane_link.c)

static bool IsNeedDelResource(uint64_t laneId, bool isServerSide, LaneResource *item)
{
    if (item->laneId != laneId) {
        return false;
    }
    uint32_t ref = 0;
    bool isServer = false;
    if (isServerSide) {
        ref = item->clientRef;
        if (item->clientRef == 0) {
            ListDelete(&item->node);
            SoftBusFree(item);
            if (g_laneResource.cnt != 0) {
                g_laneResource.cnt--;
            }
        } else {
            item->isServerSide = false;
        }
    } else {
        isServer = item->isServerSide;
        ref = item->clientRef;
        if (item->clientRef != 0) {
            ref = --item->clientRef;
        }
        if (!isServer && ref == 0) {
            DeleteNetworkResourceByLaneId(laneId);
            ListDelete(&item->node);
            SoftBusFree(item);
            if (g_laneResource.cnt != 0) {
                g_laneResource.cnt--;
            }
        }
    }
    LNN_LOGI(LNN_LANE, "del laneId=%{public}" PRIu64 " resource, isServer=%{public}d, clientRef=%{public}u",
        laneId, isServer, ref);
    return true;
}

结语

这里还只是channel和lane层面的多路控制处理策略,我估计在传输层的发送那边可能会有多路复用、时序发送等处理策略避免资源竞争,看后面验证。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值