网络编程中有这样一种场景:需要应用程序代码一边从TCP/IP协议栈接收数据(reading data from socket),一边解析接收的数据。具体场景例如:用户点击Youtube或优酷网站上的视频内容,这时用户PC上的播放软件就是一边接收数据一边对数据进行解码并播放的。这样的场景的存在如下约束:
1. 必须边接收数据,边对数据进行解析,不能等待到数据全部接收完整后才解析(用户等待的时间与体验成反比)。
2. 数据为流式数据(如TCP承载),需对接收到的数据进行定界分析,将数据转化为可被应用程序解析的结构化数据。
3. 数据的解析需要兼顾性能和内存空间的利用效率(如果减少内存拷贝,分配适当大小的缓存空间)。
本文将设计一个适合上述场景的环形缓冲组件,提供方便的数据缓存与读取接口,让编码专注于数据解析的逻辑,而不是将过多的精力消耗在缓冲区本身的处理上。本文讨论POSIX的一种优化的环形缓冲实现方式,并提出了进一步优化:
1. 高效的数据写入与读取接口,如应用程序可能对某段数据不感兴趣,则可将其直接忽略掉。
2. 封装了常见的整形数据读取接口,解析程序可以直接读数1~4字节的整形数据。
#ifndef _CIRCULAR_BUFFER_H #define _CIRCULAR_BUFFER_H typedef struct CircularBuffer { void *ptr; /* 必须为整数倍内存页面大小*/ unsigned long count; unsigned long read_offset; unsigned long write_offset; } CircularBuffer; /* 创建环形缓冲区 */ CircularBuffer *cbCreate(unsigned long order); /* 销毁环形缓冲区 */ void cbFree(CircularBuffer *cb); /* 重置缓冲区,使之可用于新的业务数据缓存 */ void cbClear(CircularBuffer *cb); int cbIsEmpty(CircularBuffer *cb); unsigned long cbUsedSpaceSize(CircularBuffer *cb); unsigned long cbFreeSpaceSize(CircularBuffer *cb); /* 向环形缓冲写入len 字节数据 */ unsigned long cbPushBuffer(CircularBuffer *cb, void *buffer, unsigned long len); /* 从环形缓冲读取len字节存放到buffer中, buffer可以为NULL,忽略len字节的数据*/ void *cbReadBuffer(CircularBuffer *cb, void *buffer, unsigned long len); /* 从环形缓冲区读取1个字节 */ unsigned char cbReadUINT8(CircularBuffer *cb); /* 从环形缓冲区读取1个短整形数 */ unsigned short cbReadUINT16(CircularBuffer *cb); short cbReadSINT16(CircularBuffer *cb); unsigned int cbReadUINT24(CircularBuffer *cb); int cbReadSINT24(CircularBuffer *cb); unsigned int cbReadUINT32(CircularBuffer *cb); int cbReadSINT32(CircularBuffer *cb); #endifcbCreate接口创建并初始化一个环形缓冲区,实现如下:
CircularBuffer *cbCreate(unsigned long order) { int fd = 0, status = 0; void *address = NULL; char path[] = "/dev/shm/circular_buffer_XXXXXX"; CircularBuffer *cb = (CircularBuffer *)malloc(sizeof(CircularBuffer)); if (NULL == cb) { return NULL; } order = (order <= 12 ? 12 : order); cb->count = 1UL << order; cb->read_offset = 0; cb->write_offset = 0; /* 分配2倍指定的缓冲空间 */ cb->ptr = mmap(NULL, cb->count << 1, PROT_NONE, MAP_ANONYMOUS |MAP_PRIVATE, -1, 0); if (MAP_FAILED == cb->ptr) { abort(); | } /* 根据path模块创建一个唯一的临时文件 */ fd = mkstemp(path); if (0 > fd) { abort(); } /* 删除文件访问的目录入口,进程仍可使用该文件 */ status = unlink(path); if (0 != status) { abort(); } /* 将文件大小精确指定为count字节 */ status = ftruncate(fd, cb->count); if (0 != status) { abort(); } /* 将[ cb->ptr, cb->ptr + cb->count)地址空间映射到临时文件*/ address = mmap(cb->ptr, cb->count, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, fd, 0); if (address != cb->ptr) { abort(); } /* 将[ cb->ptr + cb->count, cb->ptr + 2 * cb->count)地址空间映射到临时文件*/ address = mmap(cb->ptr + cb->count, cb->count, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, fd, 0); if (address != cb->ptr + cb->count) { abort(); } status = close(fd); if (0 != status) { abort(); } return cb; }该实现采用了一种精妙的处理方式,用2倍的缓存空间简化数据的读写操作。
如下代码为读写环形缓冲区及计算缓冲区已使用空间大小的例程。cbUsedSpaceSize函数可用于cbIsEmpty及cbFreeSpaceSize函数的实现。cbReadBuffer函数则可用于实现cbReadUINT8、cbReadUINT16、cbReadSINT16、cbReadUINT24、cbReadSINT24、cbReadUINT32及cbReadSINT32。cbReadBuffer函数的buffer参数若传人为空,则忽略len指定长度字节的数据。
unsigned long cbPushBuffer(CircularBuffer *cb, void *buffer, unsigned long len) { unsigned long write_offset = cb->write_offset; cb->write_offset += len; memmove(cb->ptr + write_offset, buffer, len); return len; } void *cbReadBuffer(CircularBuffer *cb, void *buffer, unsigned long len) { void *address = NULL; /* 忽略len字节数据 */ if (NULL != buffer) { address = memmove(buffer, cb->ptr + cb->read_offset, len); } cb->read_offset += len; if (cb->read_offset > cb->count) { cb->read_offset -= cb->count; cb->write_offset -= cb->count; } return address; } unsigned long cbUsedSpaceSize(CircularBuffer *cb) { return cb->write_offset - cb->read_offset; }
1. 环形缓冲区特别适合于FIFO类型数据的处理,利用它可以不拷贝内存完成缓冲上数据的解析,提高数据解析效率。
2. 若数据读取函数采用单字节读、取模数计算偏移的方式,则可能带来性能上的损耗,该问题可以通过增加判断或以做位运算等机制来解决,但同时也增加了实现逻辑的复杂度。
3. 其不足之处在于需要预先估计数据缓冲的大小,并分配比预估大小大一个数量级的缓存空间。一种可能的解决办法是增加检测机制,若发现缓冲太小,则动态调大缓冲的大小,但这同时又可能导致频繁的调整内存大小,带来性能的下降。