在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函数,下面的方式比较直观,在应用层没有指针的移动,也就不会暴露内部的细节。