网络字节流数据解析组件的设计与实现--Circular Buffer(Ring Buffer)

本文介绍了一种适用于网络编程场景的环形缓冲区组件设计,尤其针对边接收数据边解析的应用需求。通过2倍缓存空间的巧妙利用,简化了数据读写逻辑,并提供了高效的读写接口。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 应用场景

      网络编程中有这样一种场景:需要应用程序代码一边从TCP/IP协议栈接收数据(reading data from socket),一边解析接收的数据。具体场景例如:用户点击Youtube或优酷网站上的视频内容,这时用户PC上的播放软件就是一边接收数据一边对数据进行解码并播放的。这样的场景的存在如下约束:
1. 必须边接收数据,边对数据进行解析,不能等待到数据全部接收完整后才解析(用户等待的时间与体验成反比)。
2. 数据为流式数据(如TCP承载),需对接收到的数据进行定界分析,将数据转化为可被应用程序解析的结构化数据。
3. 数据的解析需要兼顾性能和内存空间的利用效率(如果减少内存拷贝,分配适当大小的缓存空间)。image

     本文将设计一个适合上述场景的环形缓冲组件,提供方便的数据缓存与读取接口,让编码专注于数据解析的逻辑,而不是将过多的精力消耗在缓冲区本身的处理上。本文讨论POSIX的一种优化的环形缓冲实现方式,并提出了进一步优化:
1. 高效的数据写入与读取接口,如应用程序可能对某段数据不感兴趣,则可将其直接忽略掉。
2. 封装了常见的整形数据读取接口,解析程序可以直接读数1~4字节的整形数据。

2. 基于Circular Buffer的字节流解析组件实现

       http://en.wikipedia.org/wiki/Circular_buffer 这里对Circular Buffer的原理及各种不同的实现进行了讨论。下面我们来看下我们基于POSIX方式的实现并进行分析。

#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);

#endif

 

    cbCreate接口创建并初始化一个环形缓冲区,实现如下:

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倍的缓存空间简化数据的读写操作。
    第1个mmap采用私有匿名的方式分配了一块为指定缓冲区大小2倍的内存空间;第2个mmap将mkstemp创建的临时文件映射到[ptr, ptr + count)地址,第3个mmap将mkstemp创建的临时文件映射到[ptr + count, ptr + 2 * count)地址,这样对ptr[i]的读写操作将等同于对ptr[i + count]的读写操作,从而达到简化了环形缓冲区对于数据回绕的逻辑。

image

     如下代码为读写环形缓冲区及计算缓冲区已使用空间大小的例程。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;
}
 

3. 分析与讨论

1. 环形缓冲区特别适合于FIFO类型数据的处理,利用它可以不拷贝内存完成缓冲上数据的解析,提高数据解析效率。
2. 若数据读取函数采用单字节读、取模数计算偏移的方式,则可能带来性能上的损耗,该问题可以通过增加判断或以做位运算等机制来解决,但同时也增加了实现逻辑的复杂度。
3. 其不足之处在于需要预先估计数据缓冲的大小,并分配比预估大小大一个数量级的缓存空间。一种可能的解决办法是增加检测机制,若发现缓冲太小,则动态调大缓冲的大小,但这同时又可能导致频繁的调整内存大小,带来性能的下降。

4. 参考文献

http://en.wikipedia.org/wiki/Circular_buffer

本文许可自由转载,转载请注明出处!

转载于:https://www.cnblogs.com/dskit/archive/2012/08/09/2631026.html

#include "malloc.h" #include <string.h> // 内部内存池(32字节对齐,便于DMA操作) __align(32) u8 mem1base[MEM1_MAX_SIZE]; // 内存管理表(记录每个内存块的状态) u16 mem1mapbase[MEM1_ALLOC_TABLE_SIZE]; void my_mem_init(u8 memx); u8 my_mem_perused(u8 memx); // 内存管理参数常量数组 const u32 memtblsize[SRAMBANK] = {MEM1_ALLOC_TABLE_SIZE}; // 管理表大小 const u32 memblksize[SRAMBANK] = {MEM1_BLOCK_SIZE}; // 内存块大小 const u32 memsize[SRAMBANK] = {MEM1_MAX_SIZE}; // 内存池总大小 // 全局内存管理设备实例 struct _m_mallco_dev mallco_dev = { my_mem_init, // 指向初始化函数 my_mem_perused, // 指向内存使用率函数 {mem1base}, // 内存池基地址(仅内部内存) {mem1mapbase}, // 内存管理表基地址 {0} // 初始化状态(0=未初始化) }; // 内存复制函数(内部使用) void mymemcpy(void *des, void *src, u32 n) { u8 *xdes = des; u8 *xsrc = src; while(n--)*xdes++=*xsrc++; // 逐字节复制 } // 内存设置函数(内部使用) void mymemset(void *s, u8 c, u32 count) { u8 *xs = s; while(count--)*xs++=c; // 逐字节设置 } // 内存池初始化(内部调用) void my_mem_init(u8 memx) { // 清零内存管理表(初始化所有块为空闲) mymemset(mallco_dev.memmap[memx], 0, memtblsize[memx]*2); // 标记内存池已初始化 mallco_dev.memrdy[memx]=1; } // 获取内存池使用率(内部调用) u8 my_mem_perused(u8 memx) { u32 used=0; // 遍历管理表,统计已用块数 for(u32 i=0;i<memtblsize[memx];i++) { if(mallco_dev.memmap[memx][i])used++; } // 计算并返回使用百分比 return (used*100)/(memtblsize[memx]); } // 内存分配核心算法(内部调用) u32 my_mem_malloc(u8 memx,u32 size) { signed long offset=0; u32 nmemb; // 需要的内存块数 u32 cmemb=0; // 当前找到的连续空闲块数 // 检查内存池是否初始化 if(!mallco_dev.memrdy[memx])mallco_dev.init(memx); // 计算需要的内存块数(向上取整) nmemb=size/memblksize[memx]; if(size%memblksize[memx])nmemb++; // 从内存池尾部向前搜索连续空闲块 for(offset=memtblsize[memx]-1;offset>=0;offset--) { if(!mallco_dev.memmap[memx][offset])cmemb++; // 空闲块计数 else cmemb=0; // 中断连续 // 找到足够的连续空闲块 if(cmemb==nmemb) { // 标记这些块为已分配(存储分配块数) for(u32 i=0;i<nmemb;i++) { mallco_dev.memmap[memx][offset+i]=nmemb; } return (offset*memblksize[memx]); // 返回偏移地址 } } return 0XFFFFFFFF; // 分配失败(内存不足) } // 内存释放(内部调用) u8 my_mem_free(u8 memx,u32 offset) { // 检查内存池是否初始化 if(!mallco_dev.memrdy[memx]) { mallco_dev.init(memx); return 1; // 返回未初始化错误 } // 检查偏移是否有效 if(offset<memsize[memx]) { // 计算块索引 int index=offset/memblksize[memx]; // 获取该块分配时记录的块数 int nmemb=mallco_dev.memmap[memx][index]; // 释放连续的内存块(标记为空闲) for(int i=0;i<nmemb;i++) { mallco_dev.memmap[memx][index+i]=0; } return 0; // 成功 } return 2; // 偏移超出范围 } // 释放内存(用户接口) void myfree(u8 memx,void *ptr) { if(ptr==NULL)return; // 空指针检查 // 计算偏移量(地址 - 基地址) u32 offset=(u32)ptr-(u32)mallco_dev.membase[memx]; my_mem_free(memx,offset); // 调用内部释放 } // 分配内存(用户接口) void *mymalloc(u8 memx,u32 size) { // 调用内部分配函数获取偏移 u32 offset=my_mem_malloc(memx,size); if(offset==0XFFFFFFFF)return NULL; // 分配失败 else return (void*)((u32)mallco_dev.membase[memx]+offset); // 返回绝对地址 }#ifndef __MALLOC_H #define __MALLOC_H #include "stm32f10x.h" // 内存池定义:STM32F103C8T6只有内部RAM(20KB),不包含外部RAM #define SRAMIN 0 // 内部内存池编号 #define SRAMBANK 1 // 内存池数量(仅使用1个) // 内存管理参数调整(适配C8T6的20KB RAM) #define MEM1_BLOCK_SIZE 32 // 内存块大小设为32字节(提高小内存分配效率) #define MEM1_MAX_SIZE 12 * 1024 // 分配给内存管理的最大内存(12KB,保留8KB给堆栈和系统) #define MEM1_ALLOC_TABLE_SIZE (MEM1_MAX_SIZE/MEM1_BLOCK_SIZE) // 管理表大小 // 内存管理设备结构体 struct _m_mallco_dev { void (*init)(u8); // 初始化函数指针 u8 (*perused)(u8); // 内存使用率计算函数指针 u8 *membase[SRAMBANK]; // 内存池基地址数组 u16 *memmap[SRAMBANK]; // 内存池管理表基地址数组 u8 memrdy[SRAMBANK]; // 内存池初始化状态标志 }; extern struct _m_mallco_dev mallco_dev; // 声明全局内存管理设备 // 用户接口函数声明 void myfree(u8 memx, void *ptr); // 释放内存(外部调用) void *mymalloc(u8 memx, u32 size); // 分配内存(外部调用) u8 my_mem_perused(u8 memx); // 获取内存使用率(外部调用) #endif #include "RingBuffer.h" #include <stdlib.h> #include <string.h> // RingBuffer.h #ifndef min #define min(a, b) (((a) < (b)) ? (a) : (b)) #endif // 判断一个数是否是2的幂(环形缓冲区要求) static bool is_power_of_2(uint32_t x) { return (x != 0) && ((x & (x - 1)) == 0); } // 向上取整到2的幂(确保缓冲区大小符合要求) static uint32_t roundup_pow_of_two(uint32_t x) { uint32_t b = 1; // 从1开始 // 找到不小于x的最小2的幂 while (b < x) { b <<= 1; // 左移一位(相当于×2) } return b; } // 创建并初始化环形缓冲区 RingBuffer *RingBuffer_Malloc(uint32_t size) { // 分配管理结构体内存 RingBuffer *fifo = RING_BUFFER_MALLOC(sizeof(RingBuffer)); if(!fifo) return NULL; // 分配失败 // 调整大小为2的幂 if(!is_power_of_2(size)) size = roundup_pow_of_two(size); // 分配缓冲区内存 fifo->buffer = RING_BUFFER_MALLOC(size); if(!fifo->buffer) { RING_BUFFER_FREE(fifo); return NULL; // 分配失败 } // 初始化结构体成员 fifo->size = size; fifo->in = fifo->out = 0; fifo->use_dma = false; // 默认不启用DMA return fifo; } // 释放环形缓冲区 void RingBuffer_Free(RingBuffer *fifo) { if(fifo) { RING_BUFFER_FREE(fifo->buffer); // 先释放缓冲区 RING_BUFFER_FREE(fifo); // 再释放结构体 } } // 向环形缓冲区写入数据 uint32_t RingBuffer_In(RingBuffer *fifo, void *in, uint32_t len) { // 参数检查 if(!fifo || !in) return 0; // 计算实际可写入长度(取请求长度和可用空间的最小值) len = min(len, RingBuffer_Avail(fifo)); if(len == 0) return 0; // 计算第一部分长度(从写索引到缓冲区末尾) uint32_t l = min(len, fifo->size - (fifo->in & (fifo->size - 1))); // 复制第一部分数据 memcpy(fifo->buffer + (fifo->in & (fifo->size - 1)), in, l); // 复制剩余部分数据(从缓冲区开头继续) if(len > l) { memcpy(fifo->buffer, (uint8_t *)in + l, len - l); } // 原子更新写索引(缓冲区大小是2的幂,自然溢出安全) fifo->in += len; return len; // 返回实际写入字节数 } // 从环形缓冲区读取数据 uint32_t RingBuffer_Out(RingBuffer *fifo, void *out, uint32_t len) { // 参数检查 if(!fifo || !out) return 0; // 计算实际可读取长度(取请求长度和已用空间的最小值) len = min(len, RingBuffer_Len(fifo)); if(len == 0) return 0; // 计算第一部分长度(从读索引到缓冲区末尾) uint32_t l = min(len, fifo->size - (fifo->out & (fifo->size - 1))); // 复制第一部分数据 memcpy(out, fifo->buffer + (fifo->out & (fifo->size - 1)), l); // 复制剩余部分数据(从缓冲区开头继续) if(len > l) { memcpy((uint8_t *)out + l, fifo->buffer, len - l); } // 原子更新读索引 fifo->out += len; return len; // 返回实际读取字节数 } // DMA初始化(预留功能) void RingBuffer_InitDMA(RingBuffer *fifo, DMA_Channel_TypeDef *dma_tx, DMA_Channel_TypeDef *dma_rx) { if(!fifo) return; fifo->use_dma = true; // 启用DMA标志 // 此处实现DMA配置代码 // 实际项目中需配置DMA通道、地址、长度等参数 // 启用USART的DMA请求等 } #ifndef __RINGBUFFER_H #define __RINGBUFFER_H #include <stdint.h> #include <stdbool.h> #include "malloc.h" // 使用自定义内存管理 // 定义内存分配宏(使用内部内存池) #define RING_BUFFER_MALLOC(size) mymalloc(SRAMIN, size) #define RING_BUFFER_FREE(block) myfree(SRAMIN, block) // 环形缓冲区结构体 typedef struct { uint8_t *buffer; // 缓冲区基地址 uint32_t size; // 缓冲区大小(必须是2的幂) volatile uint32_t in; // 写索引(生产者位置)volatile防止编译器优化 volatile uint32_t out; // 读索引(消费者位置)volatile防止编译器优化 bool use_dma; // DMA使用标志(预留扩展) } RingBuffer; // 函数声明 RingBuffer *RingBuffer_Malloc(uint32_t size); // 创建环形缓冲区 void RingBuffer_Free(RingBuffer *fifo); // 释放环形缓冲区 uint32_t RingBuffer_In(RingBuffer *fifo, void *in, uint32_t len); // 写入数据 uint32_t RingBuffer_Out(RingBuffer *fifo, void *out, uint32_t len); // 读取数据 void RingBuffer_InitDMA(RingBuffer *fifo, DMA_Channel_TypeDef *dma_tx, DMA_Channel_TypeDef *dma_rx); // DMA初始化(预留) // 内联辅助函数 static inline void RingBuffer_Reset(RingBuffer *fifo) { fifo->in = fifo->out = 0; // 重置缓冲区 } static inline uint32_t RingBuffer_Size(RingBuffer *fifo) { return fifo->size; // 返回缓冲区容量 } static inline uint32_t RingBuffer_Len(RingBuffer *fifo) { return fifo->in - fifo->out; // 已用空间(无锁,仅适用单生产者单消费者) } static inline uint32_t RingBuffer_Avail(RingBuffer *fifo) { return fifo->size - RingBuffer_Len(fifo); // 可用空间 } static inline bool RingBuffer_IsEmpty(RingBuffer *fifo) { return (RingBuffer_Len(fifo) == 0); // 缓冲区是否为空 } static inline bool RingBuffer_IsFull(RingBuffer *fifo) { return (RingBuffer_Avail(fifo) == 0); // 缓冲区是否已满 } #endif /* __RINGBUFFER_H */ 这些代码是否正确,有没有逻辑错误,能否实现环形缓冲区
07-16
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值