一个通用字符串缓存类的设计
最近在写一个 cpp server, 其中有用到设计一个缓存类, 然后我也是借鉴了其他大佬的思路, 从中学到了不少知识, 就在这里总结和分享一下自己的感受.
一个缓存类的设计应该有哪些特性呢?
在我们分析其有哪些特性前, 让我们先思考一个问题. 这个缓存类是用来干什么的?
在我做的这个项目中, 这个缓存类是用来存放 http request 报文用的,我们是从 socket 中读取的数据, 然后放到这个容器中.
- 我们不知道报文的长度, 所以缓存类需要能够自适应变长.
- 一个缓存类核心要素是, 读指针,写指针, 总长度, 可读长度, 可写长度.
- 一个缓存类的意义在于, 我们可以反复使用这段空间, 而不必开辟过多内存, 循环利用空间是重中之重.
什么情况下会用到缓存?
我举一个例子, 比如我们要构建一个字符串, 以Java距离, 那么我们使用一个 StringBuilder, 我们一直往StringBuilder对象里面插入数据, 最后完成了, 一把导出,这就有缓存的思想在里面了.
再比如, 我们从一个流里面读数据, 我们会读很多数据读入到一个容器里, 然后给正式处理程序批量读出去操作, 这就是缓存思想.
再举个通俗的例子, 一个车站就像一个缓存, 我们很多人都在那里等车, 等人够了, 大巴过来把我们带走, 而不用来一个人就发一次车,这样不实惠.
所谓的缓存, 其实应该叫缓冲区. 英文叫 Buffer, 如果翻译成缓存, 其实又可以叫 cache, 等等, 给人误解的词语.
源代码
一个缓存有两个相对指针, readerIndex_ 和 writerIndex_, 还有开始位置 start 和结束位置 end, 总共被分为了3段
从[readerIndex_, writerIndex_)这一段是可读的, 因为readerIndex_前面的已经读过了, 所以不需要再读
我们需要知道, buffer 是一次性的
[writerIndex_, end) 这一段是可以继续写的剩余空间
而 [start,readerIndex_) 这一段, 因为已经读过了, 所以也是可以写的剩余空间
指针会被操作,从而保证缓存类被反复使用.
下面, 我贴出我在写 cpp11 特性的 webserver 时候的缓存类的源码.
#ifndef __BUFFER_H__
#define __BUFFER_H__
#include <vector>
#include <string>
#include <algorithm> // copy
#include <iostream>
#include <cassert>
namespace zxzx {
class Buffer {
public:
Buffer()
: buffer_(1024),
readerIndex_(0),
writerIndex_(0)
{
assert(readableBytes() == 0);
assert(writableBytes() == INIT_SIZE);
}
~Buffer() {}
// 默认拷贝构造函数和赋值函数可用
size_t readableBytes() const // 可读字节数
{ return writerIndex_ - readerIndex_; }
size_t writableBytes() const // 可写字节数
{ return buffer_.size() - writerIndex_; }
size_t prependableBytes() const // readerIndex_前面的空闲缓冲区大小
{ return readerIndex_; }
const char* peek() const // 第一个可读位置,绝对位置
{ return __begin() + readerIndex_; }
void retrieve(size_t len) // 取出len个字节,前进N个字节
{
assert(len <= readableBytes());
readerIndex_ += len;
}
void retrieveUntil(const char* end) // 取出数据直到end
{
assert(peek() <= end);
assert(end <= beginWrite());
retrieve(end - peek());
}
void retrieveAll() // 取出buffer内全部数据
{
readerIndex_ = 0;
writerIndex_ = 0;
}
std::string retrieveAsString() // 以string形式取出全部数据
{
std::string str(peek(), readableBytes());
retrieveAll();
return str;
}
void append(const std::string& str) // 插入数据
{ append(str.data(), str.length()); }
void append(const char* data, size_t len) // 插入数据
{
ensureWritableBytes(len);
std::copy(data, data + len, beginWrite());
hasWritten(len);
}
void append(const void* data, size_t len) // 插入数据
{ append(static_cast<const char*>(data), len); }
void append(const Buffer& otherBuff) // 把其它缓冲区的数据添加到本缓冲区
{ append(otherBuff.peek(), otherBuff.readableBytes()); }
void ensureWritableBytes(size_t len) // 确保缓冲区有足够空间
{
if(writableBytes() < len) {
__makeSpace(len);
}
assert(writableBytes() >= len);
}
char* beginWrite() // 可写char指针
{ return __begin() + writerIndex_; }
const char* beginWrite() const
{ return __begin() + writerIndex_; }
void hasWritten(size_t len) // 写入数据后移动writerIndex_
{ writerIndex_ += len; }
ssize_t readFd(int fd, int* savedErrno); // 从套接字读到缓冲区
ssize_t writeFd(int fd, int* savedErrno); // 缓冲区写到套接字
private:
char* __begin() // 返回缓冲区头指针
{ return &*buffer_.begin(); }
const char* __begin() const // 返回缓冲区头指针
{ return &*buffer_.begin(); }
// 这个函数有必要解释一下,
// 从字面意思来看, 是保证足够的空间
// 一个缓存有两个相对指针, readerIndex_ 和 writerIndex_, 还有开始位置 start 和结束位置 end, 总共被分为了3段
// 从[readerIndex_, writerIndex_)这一段是可读的, 因为readerIndex_前面的已经读过了, 所以不需要再读
// 我们需要知道, buffer 是一次性的
// [writerIndex_, end) 这一段是可以继续写的剩余空间
// 而 [start,readerIndex_) 这一段, 因为已经读过了, 所以也是可以写的剩余空间
// 指针会被操作,从而保证缓存类被反复使用
void __makeSpace(size_t len) // 确保缓冲区有足够空间
{
if(writableBytes() + prependableBytes() < len) {
buffer_.resize(writerIndex_ + len);
}
else {
size_t readable = readableBytes();
std::copy(__begin() + readerIndex_,
__begin() + writerIndex_,
__begin());
readerIndex_ = 0;
writerIndex_ = readerIndex_ + readable;
assert(readable == readableBytes());
}
}
private:
std::vector<char> buffer_;
// 双指针,
// writerIndex_ 表示已经写入了多少个字节,并且从第N个字节开始可读
// 它其实是一个相对位置而非绝对位置
size_t readerIndex_;
size_t writerIndex_;
}; // class Buffer
}
#endif
Buffer.cpp 文件
#include "Buffer.h"
#include <cstring> // perror
#include <iostream>
#include <unistd.h> // write
#include <sys/uio.h> // readv
using namespace zxzx;
// 从套接字里面读出数据
// fd: 套接字的id, 在linux 里面, 文件句柄就是一个数字
// savedErrno: 一个指针,其指向空间被用来设置错误号,如果正常, *savedErrno 会被设置为0
// 返回值: 实际读到了几个字节
ssize_t Buffer::readFd(int fd, int* savedErrno){
// 保证一次读到足够多的数据
char extrabuf[65536]; // 2**16, 也就是2的16次方,64KB
struct iovec vec[2];
const size_t writable = writableBytes();
vec[0].iov_base = __begin() + writerIndex_;
vec[0].iov_len = writable;
vec[1].iov_base = extrabuf;
vec[1].iov_len = sizeof(extrabuf);
// readv 这个函数意思是, 如果一个空间不够, 那就往其他后续的缓存空间读, 也就是说,缓存空间可能是不连续的,分好几块
const ssize_t n = ::readv(fd, vec, 2);
if(n < 0) {
printf("[Buffer:readFd]fd = %d readv : %s\n", fd, strerror(errno));
*savedErrno = errno;
}
else if(static_cast<size_t>(n) <= writable)
writerIndex_ += n;
else {
writerIndex_ = buffer_.size();
append(extrabuf, n - writable);
}
return n;
}
// 向套接字里面写
ssize_t Buffer::writeFd(int fd, int* savedErrno){
size_t nLeft = readableBytes();
char* bufPtr = __begin() + readerIndex_;
ssize_t n;
if((n = ::write(fd, bufPtr, nLeft)) <= 0) {
if(n < 0 && n == EINTR)
return 0;
else {
printf("[Buffer:writeFd]fd = %d write : %s\n", fd, strerror(errno));
*savedErrno = errno;
return -1;
}
} else {
readerIndex_ += n;
return n;
}
}