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%
后,就会重新申请一段固定大小的堆空间来进行扩容