笔记目录导航
C++游戏服务器框架笔记(一)_封装数据包类
C++游戏服务器框架笔记(二)_封装Socket类
......
上一章有简单讲过系统在每个socket创建的时候底层会配套给socket分配一个发送和接收缓冲区,发送数据时send返回后数据在发送缓冲区中由系统在合适时间真正的发送出去,接收数据时,数据先从网卡到接收缓冲区,再由用户调用recv从缓冲区中读取数据到用户态。
系统为每个socket分配的发送缓冲区,用户通常只能设置socket的缓冲区大小无法随意修改扩充,所以服务器程序应该为每个连接的客户端都对应创建一个接收缓冲区和发送缓冲区,程序中所有的数据都先到缓冲区,接收缓冲区中的数据在合适的时候,通过专门的解析数据的接口解析出数据包派发到对应的功能接口、发送缓冲区中的数据会在有可写事件的时候通过send发送出去。
为了方便 *** 作缓冲区,封装一个ByteBuffer缓冲区类
一个缓冲区需要如下基础成员数据:
m_MaxSize:缓冲区的大小,根据数据会自动扩充
m_Offset:可读偏移位置,标记缓冲区中可以读取数据的起始位置
m_CurrOffset:可写偏移位置,标记缓冲区中写数据的起始位置
m_Buffer:缓冲区起始地址
如下图结构
数据写入缓冲区中时,首先会检测当前缓冲区中可写空间是否足够写入新数据,如果足够则写入数据,偏移可写位置,如果空间不足,则先扩充内存空间,再写入数据。
但是我们看到上图中有一部分是 已读取过的数据,这部分已读过的数据已经是不需要的数据了,但是还占用着缓冲区的一部分空间,所以我们需要将已读取过的数据清空掉,这样子已读取过数据占用的空间在清掉数据后就可以再用,那么应该什么时候去清除已读取数据呢?
可以在写入数据的时候清除,当检测到当前缓冲区剩余写入空间不足时,这个时候先不着急扩充内存大小,先对缓冲区进行整理:
1.检测缓冲区中是否有未读数据,如果没有卫队数据则全部清除 可读偏移位置m_Offset 和可写偏移位置m_CurrOffset重置到起始位置
2.如果有未读数据,则使用memcp将缓冲区中可读数据移动到缓冲区起始地址,覆盖掉已读取过数据,这样已读取数据就相当于是被清除了,m_Offset可读偏移位置重置到起始位置
3.检测整理过后的缓冲区可写空间十分足够写入新数据,如果足够则返回,如果空间仍然不足,则调用realloc对m_Buffer进行内存扩充,通常扩充一个块的大小,也可以是一倍的大小,这里会定义一个内存块大小,然后每次扩充一个块大小
经过缓冲区整理后,就有了足够大小的可写空间,这时再写入数据到缓冲区中并偏移可写位置
以上是经过整理后写入新数据的流程,如果这时候再来了新数据,显然剩余空间又不足了,这个时候就需要调用realloc函数对缓冲区进行扩充了
关于realloc函数:
原型是extern void *realloc(void *mem_address, unsigned int newsize);
百度的函数说明如下:
大概原理讲过后,直接上代码
ByteBuffer头文件:
#ifndef _BYTEBUFFER_H_
#define _BYTEBUFFER_H_
//定义一个内存块大小 在扩充缓冲区的时候 一次扩充该大小空间
#define BLOCKSIZE 32 * 1024
class ByteBuffer
{
public:
ByteBuffer();
~ByteBuffer();
//写入数据到缓冲区
bool Put(const unsigned char * buf, const unsigned int size);
//缓冲区可读取数据起始地址
const unsigned char * RBuf();
//缓冲区可写入起始地址
char * WBuf();
//检查缓冲区中是否有有效数据
bool CanRead() const;
//获取缓冲区中可读数据的大小
unsigned int CanReadSize() const;
//偏移读取地址
void MoveROffset(const unsigned int size);
//获取缓冲区可写入数据大小
unsigned int CanWriteSize() const;
//偏移写入地址
void MoveWOffset(const unsigned int size);
//重置缓冲区数据
void Reset();
//返回缓冲区最大大小
unsigned int MaxSize() const;
//整理缓冲区 清除废弃数据 解放缓冲区 不足则 扩充缓冲区大小
void TidyBuf(const unsigned int size);
private:
//缓冲区大小
unsigned int m_MaxSize;
//可读偏移地址
unsigned int m_Offset;
//可写偏移地址
unsigned int m_CurrOffset;
//缓冲区
unsigned char * m_Buffer;
};
#endif
ByteBuffer cpp实现:
#include "ByteBuffer.h"
#include
#include
ByteBuffer::ByteBuffer():
m_MaxSize(0),
m_Offset(0),
m_CurrOffset(0),
m_Buffer(nullptr)
{
}
ByteBuffer::~ByteBuffer()
{
if (m_Buffer != nullptr)
free(m_Buffer);
}
//写入数据到缓冲区
bool ByteBuffer::Put(const unsigned char * buf, const unsigned int size){
//整理缓冲区 保证数据有足够空间可以写入
TidyBuf(size);
memcpy(&m_Buffer[m_CurrOffset], buf, size);
MoveWOffset(size);
return true;
}
//缓冲区可读取数据起始地址
const unsigned char * ByteBuffer::RBuf() {
return &m_Buffer[m_Offset];
}
//缓冲区可写入起始地址
char * ByteBuffer::WBuf() {
return (char *)&m_Buffer[m_CurrOffset];
}
//检查缓冲区中是否有有效数据
bool ByteBuffer::CanRead() const {
return m_Offset < m_CurrOffset;
}
//获取缓冲区中可读数据的大小
unsigned int ByteBuffer::CanReadSize() const {
return m_CurrOffset - m_Offset;
}
//偏移读取地址
void ByteBuffer::MoveROffset(const unsigned int size) {
m_Offset += size;
}
//获取缓冲区可写入数据大小
unsigned int ByteBuffer::CanWriteSize() const {
return m_MaxSize - m_CurrOffset;
}
//偏移写入地址
void ByteBuffer::MoveWOffset(const unsigned int size) {
m_CurrOffset += size;
}
//重置缓冲区数据
void ByteBuffer::Reset() {
m_Offset = 0;
m_CurrOffset = 0;
}
//返回缓冲区最大大小
unsigned int ByteBuffer::MaxSize() const {
return m_MaxSize;
}
//整理缓冲区 清除废弃数据 解放缓冲区 不足则 扩充缓冲区大小
void ByteBuffer::TidyBuf(const unsigned int size) {
//如果空间足够 则不扩充
if (CanWriteSize() > size)
return;
//如果有已读取数据可重用的空间,则清除数据解放空间
if (m_Offset > 0) {
//如果缓冲区中数据未全部读取, 则剩余数据需要保留 否则全部清除
unsigned int data_len = 0;
if (m_CurrOffset > m_Offset) {
//可读取数据
data_len = m_CurrOffset - m_Offset;
//从缓存可读数据起始地址起 将所有可读数据移动到缓冲区起始地址,会前面覆盖已读取的垃圾数据
memcpy(m_Buffer, &m_Buffer[m_CurrOffset], data_len);
}
m_Offset = 0;
m_CurrOffset = data_len;
}
//如果已读取清理数据后 空间足够则不扩充
if (CanWriteSize() > size)
return;
//扩充一个块大小
m_MaxSize += BLOCKSIZE;
m_Buffer = reinterpret_cast(realloc(m_Buffer, m_MaxSize));
}
我们这里初始是没有分配内存大小的,直到有数据写入的时候才去分配一块内存,大家也可以初始化时就分配一块内存
我们这里在整理缓冲区的时候需要进行内存 *** 作将可读数据移动到起始位置,多多少少有点影响性能,不过一般情况下基本可以忽略不计,如果要求更高的话,可以使用环形缓冲区,即写入空间不足时,不进行可读数据移动而是直接将新数据写入剩余可写空间,写不下的数据直接从缓冲区头部起始地址写入,形成了尾部衔接头部 一个环形,当然了前提是确保:
剩余可写空间+已读取数据空间>=新数据需要空间
环形缓冲区在扩充和读取的时候需要注意下方式方法,会比上面ByteBuffer稍稍复杂一些,感兴趣的话,可以自己实现一波,到时候@我欣赏一下
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)