1. 程式人生 > 其它 >muduo原始碼分析之Buffer

muduo原始碼分析之Buffer

這一次我們來分析下muduoBuffer的作用,我們知道,當我們客戶端向伺服器傳送資料時候,伺服器就會讀取我們傳送的資料,然後進行一系列處理,然後再發送到其他地方,在這裡我們想象一下最簡單的EchoServer伺服器,客戶端建立一個連線,以後伺服器和客戶端之間的通訊都是通過這個connfd傳送和接受資料,於是每一個connfd都應該有一個自己buffer,當我們傳送資料太快,伺服器傳送的太慢,則伺服器會將待發送的資料這個buffer中,所以這就是這個類的作用。我們先看下buffer的結構是什麼:

我們這裡主要針對connfd這個對應的channel進行分析,首先上圖是buffer的初始狀態,前面8

個位元組中表示該buffer的大小,初始大小為1024。當客戶端傳送資料給伺服器,同時若伺服器接受緩慢,則會向buffer中開始寫資料,則writerIndex_會向右移動,假如此時移動到如下形式:

則此時緩衝區可以讀的資料為writerIndex_ - readerIndex_,可以寫的資料為buffer_.size() - writerIndex_。這時候當伺服器有多餘資源進行讀操作,就可以去緩衝區讀資料了,假如這時候的狀態為如下:

這就是常見的幾個狀態,下面我們去看幾個重點的函式:

    // 把onMessage函式上報的buffer內容轉為string
    std::string retrieveAllAsString()
    {
        return retrieveAsString(readableBytes()); // 應用可讀取資料的長度
    }
    
    // 可讀的資料 就是存放的是即傳送的資料
    size_t readableBytes() const 
    {
        return writerIndex_ - readerIndex_;
    }
    
    std::string retrieveAsString(size_t len)
    {
		// 從可讀資料開始位置,長度為len的char構造為一個string
        std::string result(peek(), len);
        retrieve(len); // 上面一句把緩衝區中可讀的資料,已經讀取出來,這裡肯定要對緩衝區進行復位操作
        return result;
    }
    
    // 將緩衝區len的長度進行復位
    void retrieve(size_t len)
    {
		// 表示還沒有讀完資料
        if (len < readableBytes())
        {
            readerIndex_ += len; // 應用只讀取了刻度緩衝區資料的一部分,就是len,還剩下readerIndex_ += len -> writerIndex_
        }
        else   // len == readableBytes()
        {
            retrieveAll();
        }
    }

以上是基本的操作,下面的2個函式很重要,一個是向connfd寫資料,一個是讀資料,對於一個TcpConnection而言,當有資料來的時候,回去呼叫handleRead回撥函式。我們知道muduo設定的每次讀取的大小為65536位元組,當緩衝區可寫的資料大小大於65536,就會直接將讀到的資料寫入到緩衝區中,但當緩衝區的可寫資料大小小於65536的時候,就會將剩餘資料先寫到一個額外的空間

ssize_t Buffer::readFd(int fd, int* saveErrno)
{
    char extrabuf[65536] = {0}; // 棧上的記憶體空間  64K
    
    struct iovec vec[2];
    
	// 這是buffer可寫的資料
    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;
    
    const int iovcnt = (writable < sizeof extrabuf) ? 2 : 1;
	// 去百度下readv
    const ssize_t n = ::readv(fd, vec, iovcnt);
    if (n < 0)
    {
        *saveErrno = errno;
    }
    else if (n <= writable) // Buffer的可寫緩衝區已經夠儲存讀出來的資料了
    {
        writerIndex_ += n;
    }
	// extrabuf 也寫了資料
    else 
    {
        writerIndex_ = buffer_.size();
        append(extrabuf, n - writable);  // writerIndex_開始寫 n - writable大小的資料
    }

    return n;
}

這裡巧妙的使用了一個readv函式,可以通過按照大小自動寫到不同的地方。其中當extrabuf也寫了資料,就會呼叫append函式。

    // 要寫len長度的資料
    void ensureWriteableBytes(size_t len)
    {
        if (writableBytes() < len)
        {
            makeSpace(len); // 擴容函式
        }
    }

    // 向緩衝區新增資料
    void append(const char *data, size_t len)
    {
        ensureWriteableBytes(len);
        std::copy(data, data+len, beginWrite());
        writerIndex_ += len;
    }

注意到有一個makeSpace函式,其中有一個注意點,比如當如下這種狀態的時候:

此時readerIndex_前面有一部分其實已經被讀完了,是空的資料,所以makeSpace函式考慮了這一點,採用記憶體重組的方式,將readerIndex_向前移動到kCheapPrepend處,然後就可以讓空餘的記憶體挨在一起

    void makeSpace(size_t len)
    {
        if (writableBytes() + prependableBytes() < len + kCheapPrepend)
        {
            buffer_.resize(writerIndex_ + len);
        }
        else
        {
            size_t readalbe = readableBytes();
            std::copy(begin() + readerIndex_, 
                    begin() + writerIndex_,
                    begin() + kCheapPrepend);
            readerIndex_ = kCheapPrepend;
            writerIndex_ = readerIndex_ + readalbe;
        }
    }

當向connfd傳送資料的時候就比較簡單了,直接將可讀的資料傳送給出去就行

// 通過fd傳送資料
ssize_t Buffer::writeFd(int fd, int* saveErrno)
{
    ssize_t n = ::write(fd, peek(), readableBytes());
    if (n < 0)
    {
        *saveErrno = errno;
    }
    return n;
}

自己的網址:www.shicoder.top
歡迎加群聊天 452380935
本文由部落格一文多發平臺 OpenWrite 釋出!