前言

思来想去,不知起什么题目给这篇文章。我是准备自底向下来写这个专栏的,于是就想到了这句话(好吧其实后半句是我自己加的,不是很押韵,读着非常难受)。这句话前半句原处是出自侯捷老师的《深入浅出MFC》,引申义为做什么事都要脚踏实地,打好基础,同时做事要选择自己所擅长的和所感兴趣的方面,侯捷老师开篇第一句就告诉了我们。项目如同建筑一般,每下一层必须比上一层坚固,否则会崩塌。所以前期的选择很重要,地基很重要。

导航:从零开始实现C++ TinyWebServer 全过程记录


1. 纵观Buffer类

纵观全项目,最让我眼前一亮的地方就是Buffer类的设计。我下去搜了一下,这个类的设计源自陈硕大佬的muduo网络库。由于muduo库使用的是非阻塞I/O模型,即每次send()不一定会发送完,没发完的数据要用一个容器进行接收,所以必须要实现应用层缓冲区。
在这里插入图片描述
这里贴一张我对这个类的总结,其中ReadFdWriteFd应该是重点,外部调用接口也是会调用这两个。

另外就是几个下标之间的关系,根据这个图来好好看一下,一定不要弄混了,写是往writeIndex的指针写,读取则是从readIndex的指针读。当外部写入fd的时候,是将buffer中的readable写入fd;当需要读取fd的内容时,则是读到writable的位置,注意区分写和读,这里我最开始绕了好久都没理请。可以结合源码和我画的图来看一下,这里就不细讲了。

2. 开始写代码

之前都是看了源码理解后,copy一遍,但是我觉得这样对自己的提升还是太少了,你看源码5遍都没有自己亲手写一遍强,过程非常痛苦,但是总得跨出自己的舒适圈嘛,我这次就尽量自己实现,先把.h文件给定义好,自己实现,实在想不起来了再去看源码,然后再把源码关了,继续自己写,相信这样对自己提升会非常大。

buffer.h

#ifndef BUFFER_H
#define BUFFER_H
#include <cstring>   //perror
#include <iostream>
#include <unistd.h>  // write
#include <sys/uio.h> //readv
#include <vector> //readv
#include <atomic>
#include <assert.h>
class Buffer {
public:
    Buffer(int initBuffSize = 1024);
    ~Buffer() = default;

    size_t WritableBytes() const;       
    size_t ReadableBytes() const ;
    size_t PrependableBytes() const;

    const char* Peek() const;
    void EnsureWriteable(size_t len);
    void HasWritten(size_t len);

    void Retrieve(size_t len);
    void RetrieveUntil(const char* end);

    void RetrieveAll();
    std::string RetrieveAllToStr();

    const char* BeginWriteConst() const;
    char* BeginWrite();

    void Append(const std::string& str);
    void Append(const char* str, size_t len);
    void Append(const void* data, size_t len);
    void Append(const Buffer& buff);

    ssize_t ReadFd(int fd, int* Errno);
    ssize_t WriteFd(int fd, int* Errno);

private:
    char* BeginPtr_();  // buffer开头
    const char* BeginPtr_() const;
    void MakeSpace_(size_t len);

    std::vector<char> buffer_;  
    std::atomic<std::size_t> readPos_;  // 读的下标
    std::atomic<std::size_t> writePos_; // 写的下标
};

#endif //BUFFER_H

buffer.cpp

#include "buffer.h"

// 读写下标初始化,vector<char>初始化
Buffer::Buffer(int initBuffSize) : buffer_(initBuffSize), readPos_(0), writePos_(0)  {}  

// 可写的数量:buffer大小 - 写下标
size_t Buffer::WritableBytes() const {
    return buffer_.size() - writePos_;
}

// 可读的数量:写下标 - 读下标
size_t Buffer::ReadableBytes() const {
    return writePos_ - readPos_;
}

// 可预留空间:已经读过的就没用了,等于读下标
size_t Buffer::PrependableBytes() const {
    return readPos_;
}

const char* Buffer::Peek() const {
    
    return &buffer_[readPos_];
}

// 确保可写的长度
void Buffer::EnsureWriteable(size_t len) {
    if(len > WritableBytes()) {
        MakeSpace_(len);
    }
    assert(len <= WritableBytes());
}

// 移动写下标,在Append中使用
void Buffer::HasWritten(size_t len) {
    writePos_ += len;
}

// 读取len长度,移动读下标
void Buffer::Retrieve(size_t len) {
    readPos_ += len;
}

// 读取到end位置
void Buffer::RetrieveUntil(const char* end) {
    assert(Peek() <= end );
    Retrieve(end - Peek()); // end指针 - 读指针 长度
}

// 取出所有数据,buffer归零,读写下标归零,在别的函数中会用到
void Buffer::RetrieveAll() {
    bzero(&buffer_[0], buffer_.size()); // 覆盖原本数据
    readPos_ = writePos_ = 0;
}

// 取出剩余可读的str
std::string Buffer::RetrieveAllToStr() {
    std::string str(Peek(), ReadableBytes());
    RetrieveAll();
    return str;
}

// 写指针的位置
const char* Buffer::BeginWriteConst() const {
    return &buffer_[writePos_];
}

char* Buffer::BeginWrite() {
    return &buffer_[writePos_];
}

// 添加str到缓冲区
void Buffer::Append(const char* str, size_t len) {
    assert(str);
    EnsureWriteable(len);   // 确保可写的长度
    std::copy(str, str + len, BeginWrite());    // 将str放到写下标开始的地方
    HasWritten(len);    // 移动写下标
}

void Buffer::Append(const std::string& str) {
    Append(str.c_str(), str.size());
}

void Append(const void* data, size_t len) {
    Append(static_cast<const char*>(data), len);
}

// 将buffer中的读下标的地方放到该buffer中的写下标位置
void Append(const Buffer& buff) {
    Append(buff.Peek(), buff.ReadableBytes());
}

// 将fd的内容读到缓冲区,即writable的位置
ssize_t Buffer::ReadFd(int fd, int* Errno) {
    char buff[65535];   // 栈区
    struct iovec iov[2];
    size_t writeable = WritableBytes(); // 先记录能写多少
    // 分散读, 保证数据全部读完
    iov[0].iov_base = BeginWrite();
    iov[0].iov_len = writeable;
    iov[1].iov_base = buff;
    iov[1].iov_len = sizeof(buff);

    ssize_t len = readv(fd, iov, 2);
    if(len < 0) {
        *Errno = errno;
    } else if(static_cast<size_t>(len) <= writeable) {   // 若len小于writable,说明写区可以容纳len
        writePos_ += len;   // 直接移动写下标
    } else {    
        writePos_ = buffer_.size(); // 写区写满了,下标移到最后
        Append(buff, static_cast<size_t>(len - writeable)); // 剩余的长度
    }
    return len;
}

// 将buffer中可读的区域写入fd中
ssize_t Buffer::WriteFd(int fd, int* Errno) {
    ssize_t len = write(fd, Peek(), ReadableBytes());
    if(len < 0) {
        *Errno = errno;
        return len;
    } 
    Retrieve(len);
    return len;
}

char* Buffer::BeginPtr_() {
    return &buffer_[0];
}

const char* Buffer::BeginPtr_() const{
    return &buffer_[0];
}

// 扩展空间
void Buffer::MakeSpace_(size_t len) {
    if(WritableBytes() + PrependableBytes() < len) {
        buffer_.resize(writePos_ + len + 1);
    } else {
        size_t readable = ReadableBytes();
        std::copy(BeginPtr_() + readPos_, BeginPtr_() + writePos_, BeginPtr_());
        readPos_ = 0;
        writePos_ = readable;
        assert(readable == ReadableBytes());
    }
}

这里的BUFFER,仿照的是陈硕老师的muduo库,具体可看文档底部的博客连接,讲的非常通透。这里主要讲以下他的主要实现步骤和创新点。

3. 主要实现方法

在WebServer中,客户端连接发来的HTTP请求(放到conn的读缓冲区)以及回复给客户端所请求的响应报文(放到conn的写缓冲区),都需要通过缓冲区来进行。我们以vector容器作为底层实体,在它的上面封装自己所需要的方法来实现一个自己的buffer缓冲区,满足读写的需要。
在这里插入图片描述

  • buffer的存储实体
    缓冲区的最主要需要是读写数据的存储,也就是需要一个存储的实体。自己去写太繁琐了,直接用vector来完成。也就是buffer缓冲区里面需要一个:
std::vector<char>buffer_;
  • buffer所需要的变量
    由于buffer缓冲区既要作为读缓冲区,也要作为写缓冲区,所以我们既需要指示当前读到哪里了,也需要指示当前写到哪里了。所以在buffer缓冲区里面设置变量:
std::atomic<std::size_t>readPos_;
std::atomic<std::size_t>writePos_;

分别指示当前读写位置的下标。其中atomic是一种原子类型,可以保证在多线的情况下,安全高性能得执行程序,更新变量。

  • buffer所需要的方法

读写接口

缓冲区最重要的就是读写接口,主要可以分为与客户端直接IO交互所需要的读写接口,以及收到客户端HTTP请求后,我们在处理过程中需要对缓冲区的读写接口。

与客户端直接I/O得读写接口(httpconn中就是调用的该接口。):

ssize_t ReadFd();
ssize_t WriteFd();

这个功能直接用read()/write()、readv()/writev()函数来实现。从某个连接接受数据的时候,有可能会超过vector的容量,所以我们用readv()来分散接受来的数据。

4. 创新点

问题的提出:在非阻塞网络编程中,如何设计并使用缓冲区?一方面我们希望减少系统调用,一次读的数据越多越划算,那么似乎应该准备一个大的缓冲区。另一方面,我们系统减少内存占用。如果有 10k 个连接,每个连接一建立就分配 64k 的读缓冲的话,将占用 640M 内存,而大多数时候这些缓冲区的使用率很低。muduo 用 readv 结合栈上空间巧妙地解决了这个问题。

在栈上准备一个 65536 字节的 stackbuf,然后利用 readv() 来读取数据,iovec 有两块,第一块指向 muduo Buffer 中的 writable 字节,另一块指向栈上的 stackbuf。这样如果读入的数据不多,那么全部都读到 Buffer 中去了;如果长度超过 Buffer 的 writable 字节数,就会读到栈上的 stackbuf 里,然后程序再把 stackbuf 里的数据 append 到 Buffer 中。

这么做利用了临时栈上空间,避免开巨大 Buffer 造成的内存浪费,也避免反复调用 read() 的系统开销(通常一次 readv() 系统调用就能读完全部数据)。

参考博客:

https://blog.csdn.net/Solstice/article/details/6329080

https://blog.csdn.net/wanggao_1990/article/details/119426351

结束语

在写完buffer.cpp的时候,正准备上传到github,但是怎样都上传不了,好像出了点小问题,可能是我前一两天修改仓库tag和branch的时候出了点小状况,具体咋做我也不知道。当时学git的时候,只是想能够上传到github上,方便备份,至于每一步是干啥的我也不知道,看来还得花时间重新好好学一下这个工具。太痛苦了我。

anyway,算是开了个好头吧,下篇见(捣鼓我的git去了)

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐