1.2.2 字节管理类
本节设计一个跨平台Buffer类,用来管理二进制数据。
简单定义
最简单的Buffer类,通常结构如下:
其中,pBuf指向二进制数据的开始地址,nSize为二进制数据的字节数,如图1-6所示。在开源Web服务器Nginx中,字符串就是这样定义的[11]。
然而上述定义存在很多问题,比如,如何控制内存分配与释放,如何让多个线程安全地共享,如何在函数与线程间高效地传递,减少复制次数。
Buffer作为传输通信中最常用的对象,我们需要为其实现更多的易用特性。
·极小栈消耗。
·内存自动回收。
·跨线程安全传递。
·写时复制(Copy-on-Write)。
·十六进制表示。
·Base64转换。
内存布局
本书提供的Buffer实现,采用如图1-7所示的内存布局。对比图1-6的简单定义,新的内存布局有如下特点。
(1)栈区只使用了一个指向堆区的指针,这使得占用栈空间的代价极小。
(2)栈指针指向堆区Buffer的起始地址,这使得可以随时访问Buffer的内容。
(3)在堆区Buffer前面,还有两个成员:nRefCount用来维护Buffer的引用计数;nSize用来表示Buffer的大小。
图1-6 简单Buffer类的内存布局
图1-7 改进后Buffer类的内存布局
为了方便读取Buffer之前的数据,我们定义了如下结构:
引用计数
引用计数可以有效地记录对象自身被线程持有的情况。
(1)当Buffer初始化时,其引用计数为1。
(2)当需要读取访问或复制构造Buffer时,无须复制Buffer的内容。我们仅需新增一个指向堆区的栈指针,并且让Buffer的引用计数加1。这里的栈指针,可能来自同一个线程的栈,也可能来自不同线程的栈。
(3)当不再需要访问Buffer时,将其引用计数减1。
(4)当引用计数减到0时,对象不再被任何线程持有,可安全地销毁[12]堆区Buffer。
(5)当需要修改Buffer时,根据引用计数的情况,决定是否需要写时复制。
由于涉及多个线程之间的共享操作,引用计数必须用原子变量来实现。
在Common文件夹下,有一个DAtomic.h文件,提供了对C++11<atomic>的类型封装。
写时复制
当引用计数为1时,我们可以正常地随意读写Buffer内的所有数据。
但当引用计数大于1时,如要修改Buffer的内容,我们就需要对其数据进行复制。这样各个线程仍可以像自己独占Buffer一样,访问各自的Buffer数据,使读写互不影响。
使用memcpy函数将自身的Buffer内容复制到新的Buffer中,由于该函数在不同平台下原型不同,我们使用DXP类对其进行了封装。
新分配的Buffer,其引用计数为1,原有的Buffer引用计数会减1。
更多Buffer的函数,可参看示例工程的DBuffer类。