深入fecal实现 (4) 数据窗口 (AppDataWindow)

47 阅读7分钟

在上一章 解码器 (Decoder) 中,我们认识了那位能够从蛛丝马迹中还原真相的数据侦探。我们了解到,解码器需要收集所有收到的数据包(无论是原始的还是恢复的)作为“线索”来破案。但你是否想过,解码器是如何管理这些线索的?它又是如何快速判断哪些线索已经到手,哪些还缺失呢?

这就引出了我们本章的主角:数据窗口 (AppDataWindow)。它虽然不像编码器和解码器那样身处一线,但它却是整个 fecal 工厂中不可或缺的“中央档案室”和“高效管家”。没有它,编码和解码过程将陷入一片混乱。

为什么需要一个“中央档案室”?

想象一下,编码器和解码器是两位忙碌的工匠。他们面前堆满了成百上千个数据包。

  • 编码器需要知道:“我总共要处理多少个原始包?每个包应该多大?”
  • 解码器的需求则更复杂:“我收到了哪些原始包?它们的索引分别是多少?哪些包丢失了?我又收到了哪些恢复包?”

如果让工匠们自己去翻找和记录这些信息,效率会极其低下。他们需要一个专门的助手来处理这些繁杂但至关重要的管理工作。AppDataWindow 就是这个助手。它的核心任务就是:为编码器和解码器提供一个统一、高效的数据管理中心,记录所有与数据包相关的元信息。

AppDataWindow:基础蓝图

AppDataWindow 是一个基础结构体,定义在 FecalCommon.h 文件中。它就像档案室的基础设计蓝图,规定了所有数据管理工作都必须记录的最基本信息。

// 文件: FecalCommon.h
struct AppDataWindow
{
    // 应用参数
    unsigned InputCount = 0;   // 输入符号(原始包)的数量
    uint64_t TotalBytes = 0;   // 所有输入字节的总大小
    unsigned FinalBytes = 0;   // 最后一个符号的大小
    unsigned SymbolBytes = 0;  // 其他所有符号的大小
};

这段代码非常直白。它记录了:

  • InputCount:原始数据包的总数。这是最关键的信息之一。
  • TotalBytes:所有原始数据包加起来的总大小。
  • SymbolBytesFinalBytes:由于数据总大小不一定能被包数整除,所以大部分包的大小是 SymbolBytes,最后一个包的大小可能是 FinalBytes

无论是编码器还是解码器,都需要这些基本信息来正确地分配内存和处理数据。

DecoderAppDataWindow:解码器的专属高级档案员

虽然编码器只需要基础的档案管理,但解码器的工作要复杂得多,它需要一位更专业的“高级档案员”。因此,fecal 为解码器设计了一个专门的子类:DecoderAppDataWindow

DecoderAppDataWindow 继承了 AppDataWindow 的所有基础功能,并在此之上添加了许多强大的追踪能力。

graph TD
    A["AppDataWindow (基础档案蓝图)"] --> B{"DecoderAppDataWindow (解码器专属档案室)"};

    subgraph "基础信息"
        A_InputCount["总包数 (InputCount)"]
        A_TotalBytes["总大小 (TotalBytes)"]
    end
    
    subgraph "解码器专属追踪"
        B_Original["原始包存放区 (OriginalData)"]
        B_Recovery["恢复包存放区 (RecoveryData)"]
        B_Status["接收状态追踪 (Subwindows)"]
    end

    A_InputCount --> A
    A_TotalBytes --> A

    B --> B_Original
    B --> B_Recovery
    B --> B_Status
    
    style B fill:#87ceeb,stroke:#333,stroke-width:2px

让我们看看 DecoderAppDataWindowFecalDecoder.h 中的定义,它增加了哪些“专属装备”:

// 文件: FecalDecoder.h (简化版)
struct DecoderAppDataWindow : AppDataWindow
{
    // 已收到的原始数据
    std::vector<OriginalInfo> OriginalData;

    // 已收到的恢复数据
    std::vector<RecoveryInfo> RecoveryData;

    // 追踪哪些条目已填充
    std::vector<Subwindow> Subwindows;

    // 已收到的独立原始包数量
    unsigned OriginalGotCount = 0;

    // ... 其他 ...
};

新增的核心部分是:

  1. OriginalData:一个向量(可以看作是动态数组),用来存放所有已收到的原始包的指针和信息。
  2. RecoveryData:另一个向量,用来存放所有已收到的恢复包的指针和信息。
  3. Subwindows:这是最巧妙的设计!它是一个用来高效追踪哪些原始包已收到、哪些已丢失的系统。我们稍后会深入讲解。
  4. OriginalGotCount:一个计数器,记录当前收到了多少个不同的原始包。

“管家”是如何工作的?

当解码器收到一个数据包时,它会把它交给 DecoderAppDataWindow 这个“管家”进行归档。让我们通过一个时序图,看看当解码器收到一个原始数据包时,内部发生了什么。

sequenceDiagram
    participant User as 用户
    participant Decoder as 解码器
    participant Window as DecoderAppDataWindow

    User->>Decoder: 调用 fecal_decoder_add_original(symbol)
    Decoder->>Window: 调用 AddOriginal(symbol.Index, symbol.Data)
    Window->>Window: 检查该包是否已存在?(避免重复)
    Window->>Window: 记录数据指针到 OriginalData 列表
    Window->>Window: 调用 MarkGotElement(symbol.Index) 更新接收状态
    Window->>Decoder: 返回 true (添加成功)
    Decoder->>User: 返回 Fecal_Success

归档入库 (AddOriginal)

AddOriginal 方法负责将一个新收到的原始包登记入册。

// 文件: FecalDecoder.cpp (简化版)
bool DecoderAppDataWindow::AddOriginal(unsigned column, uint8_t* data)
{
    // 如果我们已经有这个包了,就直接返回
    if (OriginalData[column].Data)
        return false;

    // 记录这个包
    OriginalData[column].Data = data;
    MarkGotElement(column); // 关键一步:标记为已收到
    ++OriginalGotCount;

    return true;
}

这个过程就像图书管理员把一本新书上架:

  1. 检查书架上这个位置是不是已经有书了。
  2. 把书(data 指针)放到对应的位置(OriginalData[column])。
  3. 在借阅卡片上打个勾(MarkGotElement),表示这本书在馆。
  4. 总在馆书籍数量加一(++OriginalGotCount)。

神奇的接收状态追踪系统

DecoderAppDataWindow 最核心的价值在于它能以极高的效率回答“哪些包丢了?”这个问题。这是通过 SubwindowsMarkGotElement 方法实现的。

这里的 Subwindow 内部包含一个叫做 CustomBitSet 的结构,你可以把它想象成一张巨大的打孔卡。总共有 InputCount 个位置,每个位置代表一个原始包。初始时,所有位置都是空的(值为 0)。

MarkGotElement被调用时,它就在这张卡片上对应的位置打一个孔(将值设为 1)。

// 文件: FecalDecoder.cpp (简化版)
void DecoderAppDataWindow::MarkGotElement(unsigned element)
{
    // 1. 计算 element 属于哪个子窗口 (subwindow)
    Subwindow& subwindow = Subwindows[element / kSubwindowSize];
    
    // 2. 在该子窗口的“打孔卡”上,将对应的位设为 1
    subwindow.Got.Set(element % kSubwindowSize);
    
    // 3. 该子窗口的已收到计数加一
    subwindow.GotCount++;
}

由于计算机操作位(0 和 1)的速度飞快,这个标记过程几乎是瞬时的。

快速查找失物 (FindNextLostElement)

当解码器准备构建恢复矩阵 (Recovery Matrix)时,它需要知道所有丢失包的索引。这时,FindNextLostElement 方法就派上用场了。它会快速扫描那张“打孔卡”,找到下一个值为 0 的位置。

// 文件: FecalDecoder.cpp (简化版)
unsigned DecoderAppDataWindow::FindNextLostElement(unsigned elementStart)
{
    // 从 elementStart 开始,在各个 subwindow 中寻找
    while (subwindowIndex < subwindowEnd)
    {
        // 如果这个子窗口可能还有丢失的包
        if (Subwindows[subwindowIndex].GotCount < kSubwindowSize)
        {
            // 在“打孔卡”中寻找第一个为 0 的位
            bitIndex = Subwindows[subwindowIndex].Got.FindFirstClear(bitIndex);

            // 如果找到了,计算并返回它的全局索引
            if (bitIndex < kSubwindowSize)
                return subwindowIndex * kSubwindowSize + bitIndex;
        }
        // ... 检查下一个子窗口 ...
    }
    return InputCount; // 没找到,说明都收到了
}

这个函数同样非常高效。它不是一个一个地检查,而是可以整块(WordT,通常是64位)地检查位图,极大地加快了查找速度。这使得解码器在需要时,能够瞬间列出所有“失踪人员”的名单。

总结

在本章中,我们深入了解了 fecal 库的“中央档案室”——数据窗口 (AppDataWindow)。我们学到了:

  • 它的使命:为编码器和解码器提供一个集中管理所有数据包信息的地方,避免混乱。
  • AppDataWindow 是一个基础蓝图,定义了所有数据管理都需要的核心信息,如总包数和总大小。
  • DecoderAppDataWindow 是解码器的专属“高级管家”,它不仅记录基础信息,还通过列表和高效的位图(BitSet)来实时追踪哪些原始包已收到、哪些已丢失,以及收到了哪些恢复包。
  • 高效的追踪机制:通过 MarkGotElementFindNextLostElement 等方法,DecoderAppDataWindow 能够以极快的速度更新和查询数据包的状态,为解码器的下一步工作(建立恢复矩阵)提供了坚实的基础。

现在,我们已经完全明白了,当数据包丢失时,解码器是如何通过它的得力助手 DecoderAppDataWindow 准确地知道“哪些包丢了”和“我手头有哪些恢复包”的。

知道了这些信息之后,解码器就可以开始真正的“破案”工作了——建立一个数学方程组来解出丢失的数据。这个方程组的具体形式,就是我们下一章将要探讨的主题:恢复矩阵 (Recovery Matrix)。