cpp教程4-编程实战-一个通用c++字符串缓冲类的设计

一个通用字符串缓存类的设计

最近在写一个 cpp server, 其中有用到设计一个缓存类, 然后我也是借鉴了其他大佬的思路, 从中学到了不少知识, 就在这里总结和分享一下自己的感受.

一个缓存类的设计应该有哪些特性呢?

在我们分析其有哪些特性前, 让我们先思考一个问题. 这个缓存类是用来干什么的?
在我做的这个项目中, 这个缓存类是用来存放 http request 报文用的,我们是从 socket 中读取的数据, 然后放到这个容器中.

  1. 我们不知道报文的长度, 所以缓存类需要能够自适应变长.
  2. 一个缓存类核心要素是, 读指针,写指针, 总长度, 可读长度, 可写长度.
  3. 一个缓存类的意义在于, 我们可以反复使用这段空间, 而不必开辟过多内存, 循环利用空间是重中之重.

什么情况下会用到缓存?

我举一个例子, 比如我们要构建一个字符串, 以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;
    }
}

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值