记一次内存工具Buffer的实现

在Java里面或者Netty框架里面,都有自己封装好的ByteBuffer结构,用于将应用层数据转成字节数组,最终通过网络发送出去。熟悉Java的同学可能很熟悉这些API,本文的目的是参考Netty里面的ByteBuffer封装一个类似的结构,用于C/C++项目中内存的申请及维护。


0、结构定义

     对于C/C++语言来说,都是指针,因此我们需要一个结构体保存这些信息,其中total_size代表目前Buffer的容量大小。

//数据缓冲区struct buffer {    char *data;          //实际缓冲指针    int readIndex;       //缓冲读取位置    int writeIndex;      //缓冲写入位置    int total_size;      //总大小};

1、内存分配/释放

     可以通过buffer_alloc函数进行申请一块内存,这里可以提高一个无参数构造的函数,默认申请8192字节。也提供了一个有参数构造的函数,用于申请指定大小的内存。由于C/C++语言没有垃圾回收机制,因此堆内存需要自己手动申请,手动释放,如下面的buffer_release函数释放申请的内存,最后释放buffer结构体。

#define INIT_BUFFER_SIZE 8192 struct buffer *buffer_alloc() { //无参构造      return buffer_alloc(INIT_BUFFER_SIZE);}
struct buffer *buffer_alloc(int Capacity)//有参构造{    if(Capacity<=0) return NULL;    struct buffer *buffer1 = malloc(sizeof(struct buffer));    if (!buffer1)            return NULL;    buffer1->data = malloc(Capacity);    buffer1->total_size = Capacity;    buffer1->readIndex = 0;    buffer1->writeIndex = 0;    return buffer1;}
void buffer_release(struct buffer *buffer1) {    free(buffer1->data);    free(buffer1);}//辅助函数int buffer_writeable_size(struct buffer *buffer) {    return buffer->total_size - buffer->writeIndex;}//可读字节数int buffer_readable_size(struct buffer *buffer) {    return buffer->writeIndex - buffer->readIndex;}


2、读写操作

    在网络框架中,通常会写入字节、拷贝字节字符串、写短整型和整形数据等,这里需要我们在写入的时候进行字节转换。我们可以统一封装起来,对外不提供细节。
    下面的buffer_append函数,用于向buffer中写入一段字节,我们首先会检查buffer中内存是否够用,如果不够则需要调用make_buffer_more函数进行扩容。

void make_buffer_more(struct buffer *buffer, int size) {    if (buffer_writeable_size(buffer) >= size) {        return;    }   //如果当前buffer可以容纳数据,则把可读数据往前面拷贝if(buffer_front_spare_size(buffer)+buffer_writeable_size(buffer)>=size){        int readable = buffer_readable_size(buffer);        int i;        for (i = 0; i < readable; i++) {          memcpy(buffer->data+i,buffer->data+buffer->readIndex+i,1);        }        buffer->readIndex = 0;        buffer->writeIndex = readable;    } else {        //内存不够了,扩大缓冲区        void *tmp = realloc(buffer->data, buffer->total_size+size);        if (tmp == NULL) {            return;        }        buffer->data = tmp;        buffer->total_size += size;    }}int buffer_append(struct buffer *buffer, void *data, int size) {    if (data != NULL) {        make_buffer_more(buffer, size);        //拷贝数据到可写空间中        memcpy(buffer->data + buffer->writeIndex, data, size);        buffer->writeIndex += size;        return 0;    }    return -1;}

     同样的,读写CHAR,USHORT和INT型的函数,也很容易实现了。

int buffer_write_char(struct buffer *buffer, char data) {    make_buffer_more(buffer, 1);    //拷贝数据到可写空间中    buffer->data[buffer->writeIndex++] = data;    return 0;}
int buffer_write_ushort(struct buffer *buffer, ushort data) {    make_buffer_more(buffer, 2);    //拷贝数据到可写空间中    ushort ndata = htons(data);//字节序转换    buffer->data[buffer->writeIndex] = data;    memcpy(&(buffer->data[buffer->writeIndex]),ndata,sizeof(data));    buffer->writeIndex+=2;//移动2个字节    return 0;} //省略其他函数


3、使用案例

        我们经常使用到的网络编解码方式是基于长度域的编解码方式,比如我们使用这种格式<msg_len><msg_id>{content},一般msg_len为2个字节,代表后面内容的长度。msg_id为2个字节,用于区分应用层消息的类型。最后是数据体content。下面看一下应用层发送心跳包的参考代码:

void write_polling(){  //<msg_len><msg_id>  BYTE buffer[16];  memset(buffer,0,sizeof(buffer));  BYTE* pSend = buffer;  *(USHORT*)pSend = htons(2);//msg_len  pSend+=2;  *(USHORT*)pSend=htons(0x1001);//msg_id  pSend+=2;  SendOut(buffer,pSend-buffer); //网络发送出去}
        上面的代码涉及到指针的移动和计算,如果消息体比较复杂,维护代码的人很容易出错,比如忘记移动指针等。那么使用我们封装好的buffer怎么实现呢?
void write_polling(){  //<msg_len><msg_id>  struct buffer* ByteBuffer = buffer_alloc(16);  buffer_write_ushort(ByteBuffer,2);//len  buffer_write_ushort(ByteBuffer,0x1001);//id  SendOut(ByteBuffer->data+ByteBuffer->readIndex,//其他封装          buffer_readable_size(ByteBuffer));  //释放...}

       对比起来看write_polling函数,下面的方式比较直观,在应用层没有指针的移动,也就不会暴露内部的细节。


猜你喜欢

转载自blog.51cto.com/15060546/2641108