UDT(四):接收缓冲区管理

1. 简介

  • 此文章尚未涉及到接收缓冲区中数据的重组/重传/可靠性等相关内容,这部分内容会在后续的文章中详细介绍
  • 这里先简单介绍一下接收缓冲区的数据是如何存储的,以及接收缓冲区的容量是如何调整的
  • 分析接收缓冲区的具体实现时,要带有如下几个问题
    • 接收缓冲区中的数据是如何划分的?数据管理的基本单元是什么?
    • 接收缓冲区的容量是固定的吗?如果接收缓冲区的容量不固定,调整策略又是什么?
    • 接收缓冲区的数据有几种状态?接收缓冲区中的数据何时会被丢弃?

2. 源码分析

  • 相关文件:queue.cpp/queue.h buffer.cpp/buffer.h

2.1 数据基本单元

  • 如前文所述,发送缓冲区中的数据是以数据块的形式进行管理;无论是读写数据还是丢弃数据,都是按数据块进行的
  • 接收缓冲区则不然,接收缓冲区的数据按一个一个的数据单元进行管理,每个数据单元都有四种状态
    • 0 - 空闲
    • 1 - 已占用
    • 2 - 当前数据单元中的数据已读取,但是还未释放所占用的内存,用于处理数据包乱序的情况
    • 3 - 丢弃
    struct CUnit
    {
        // 一个UDT数据包,参考前文中的UDT包结构
        CPacket m_Packet;
        // 数据单元状态:0-空闲;1-已占用;2-已读取未释放;3-丢弃
        int m_iFlag;
    };
    

2.2 数据单元队列

  • 使用一个顶层循环队列CUnitQueue来管理所有数据单元队列
  • 顶层队列中的每个节点存储的都是一个数据单元队列的入口地址

2.2.1 队列入口

  • 数据单元队列入口
    • m_pUnit指向一个数据单元
    • m_pBuffer指向数据单元真正使用的堆空间地址
    • m_iSize用于统计队列中共有多少个数据单元
    // CQEntry == Class Queue Entry
    struct CQEntry
    {
        // 队列元素指针
        CUnit* m_pUnit;
        // 指针域,指向堆空间
        char* m_pBuffer;
        // 队列中共有多少个数据单元
        int m_iSize;
    
        // 指向下一个队列
        CQEntry* m_pNext;
    }
    

2.2.2 顶层队列CUnitQueue初始化

  • 文件queue.cpp/queue.h
  • 用于初始的函数int CUnitQueue::init(int size, int mss, int version);
    • 为所有的数据单元申请堆空间
    • 初始化所有数据单元为空闲状态,并初始化所有数据单元的负载指向真正的堆空间
    • CUnitQueue使用一个循环队列来管理管理所有的数据单元队列CUnit*,初始化时CUnitQueue中只有一个数据单元队列,共有m_iSize个数据单元
    • 每个数据单元中负载数据的最大大小为mss
    int CUnitQueue::init(int size, int mss, int version)
    {
        CQEntry* tempq = NULL;
        CUnit* tempu = NULL;
        char* tempb = NULL;
    
        // CUnitQueue入口
        tempq = new CQEntry;
        // CUnitQueue中的数据单元
        tempu = new CUnit [size];
        // CUnitQueue真正存储数据的堆空间,每个数据单元中负载数据的最大大小为mss
        tempb = new char [size * mss];
    
        // 初始化数据单元队列中的所有数据单元为空闲状态
        for (int i = 0; i < size; ++ i)
        {
            // 初始化所有数据单元为空闲状态
            tempu[i].m_iFlag = 0;
            // 初始化所有数据单元占用的堆空间地址,基地址 + 偏移量
            tempu[i].m_Packet.m_pcData = tempb + i * mss;
        }
        
        // 初始化CUnitQueue队列入口
        tempq->m_pUnit = tempu;          // 队列入口指针
        tempq->m_pBuffer = tempb;        // 真正存储数据的堆空间
        tempq->m_iSize = size;           // 队列中共有多少个节点
    
        // 队列入口 = 当前队列 = 最后一个队列
        m_pQEntry = m_pCurrQueue = m_pLastQueue = tempq;
        // CUnitQueue循环队列
        m_pQEntry->m_pNext = m_pQEntry;
    
        // 第一个可用的数据单元
        m_pAvailUnit = m_pCurrQueue->m_pUnit;
        
        // 队列中的数据单元总数
        m_iSize = size;
        // 队列中每个数据单元负载的最大长度
        m_iMSS = mss;
        // IPv4/IPv6
        m_iIPversion = version;
        
        return 0;
    }
    

2.2.3 顶层队列CUnitQueue扩容

  • CUnitQueue可动态扩容,当已占用的数据单元个数超过队列空间的90%后,就需要进行扩容
  • 所谓扩容,就是创建一个新的数据单元队列,为其分配m_size * m_iMSS字节的堆空间,然后将这个新的数据单元队列纳入CUnitQueue的管理中
    int CUnitQueue::increase()
    {
        // 统计有多少数据单元已被占用
        int real_count = 0;
        // CUnitQueue队列入口
        CQEntry* p = m_pQEntry;
        // 遍历所有队列,统计有多少数据单元出于非空闲状态
        while (p != NULL)
        {
            // 队列中的数据单元
            CUnit* u = p->m_pUnit;
                // 遍历队列中的数据单元
                for (CUnit* end = u + p->m_iSize; u != end; ++ u)
                    // 统计处于有多少数据单元出于非空闲状态
                    if (u->m_iFlag != 0)
                        ++ real_count;
        
            // 所有队列都遍历完成
            if (p == m_pLastQueue)
                p = NULL;
            // 当前队列统计完成,切换到下一个队列
            else
                p = p->m_pNext;
        }
        // 有多少节点处于非空闲状态
        m_iCount = real_count;
        
        // 如果队列中处于非空闲状态的数据单元比例小于90%,则认为不需要增加队列容量
        if (double(m_iCount) / m_iSize < 0.9)
            return -1;
    
        CQEntry* tempq = NULL;
        CUnit* tempu = NULL;
        char* tempb = NULL;
    
        // 每个数据单元队列的容量,所有的队列容量都相同,都有m_iSize个数据单元
        int size = m_pQEntry->m_iSize;
    
        // 创建创建一个新的数据单元队列
        tempq = new CQEntry;
        tempu = new CUnit [size];
        tempb = new char [size * m_iMSS];
    
        for (int i = 0; i < size; ++ i)
        {
            // 初始化所有数据单元为空间状态
            tempu[i].m_iFlag = 0;
            // 初始化所有数据单元的堆空间
            tempu[i].m_Packet.m_pcData = tempb + i * m_iMSS;
        }
        tempq->m_pUnit = tempu;
        tempq->m_pBuffer = tempb;
        tempq->m_iSize = size;
    
        // 将新建的数据单元队列纳入到CUnitQueue中
        m_pLastQueue->m_pNext = tempq;
        m_pLastQueue = tempq;
        m_pLastQueue->m_pNext = m_pQEntry;
    
        // CUnitQueue容量
        m_iSize += size;
    
        return 0;
    }
    

2.2.4 获取一个可用的数据单元

  • 获取一个可用的数据单元,用来存储接收到的数据包
    • 遍历所有的数据单元队列,直到找到一个空间的数据单元
    • 若遍历完成仍未找到一个空闲的数据单元,则进行扩容
    CUnit* CUnitQueue::getNextAvailUnit()
    {
        // m_iCount > 0.9 * m_iSize; 即堆空间的使用率超过了90%,需要扩容
        if (m_iCount * 10 > m_iSize * 9)
            increase();
    
        // 堆空间已满
        if (m_iCount >= m_iSize)
            return NULL;
    
    // 队列入口,当前正在使用的队列
    CQEntry* entrance = m_pCurrQueue;
    
        do
        {
            // 从m_pAvailUnit开始,遍历队列,找到一个空闲的数据单元
            for (CUnit* sentinel = m_pCurrQueue->m_pUnit + m_pCurrQueue->m_iSize - 1; m_pAvailUnit != sentinel; ++ m_pAvailUnit)
            if (m_pAvailUnit->m_iFlag == 0)
                return m_pAvailUnit;
        
            // 检查当前队列的第一个数据单元是否可用
            if (m_pCurrQueue->m_pUnit->m_iFlag == 0)
            {
                m_pAvailUnit = m_pCurrQueue->m_pUnit;
                return m_pAvailUnit;
            }
    
            // 当前队列中没有可用的数据单元,遍历下一个队列
            m_pCurrQueue = m_pCurrQueue->m_pNext;
            m_pAvailUnit = m_pCurrQueue->m_pUnit;
        } while (m_pCurrQueue != entrance);
    
        // 没有找到可用的数据单元,需要扩容
        increase();
    
        return NULL;
    }
    

3. 总结

  • 接收缓冲区中的数据按数据单元CUnit为基本单位进行管理
  • 接收缓冲区中的数据单元有几种状态:空闲/已占用/数据已被读取待释放内存/丢弃数据 四种状态
  • 接收缓冲区可动态扩容,当接收缓冲区的占用率达到90%后,就会重新申请一段固定大小的堆空间来进行扩容
posted @ 2024-10-29 10:48  zhijun  阅读(4)  评论(0编辑  收藏  举报