muduo筆記 網絡庫(九)輸入輸出緩沖區Buffer


應用層緩沖區Buffer設計

Buffer設計思想

應用層緩沖區Buffer設計

  • 為什么要需要有應用層緩沖區
  • Buffer結構

epoll使用LT模式原因

  • 與poll兼容
  • LT模式不會發生漏掉事件的BUG,但POLLOUT事件不能一開始就關注,否則會出現busy loop,而應該在write無法完全寫入內核緩沖區的時候才關注,將未寫入內核緩沖區的數據添加到應用層output buffer,直到應用層output buffer寫完,停止關注POLLOUT事件。-- 相當於寫完成回調,處理寫完成事件。
  • 讀寫的時候不必等候EAGAIN,可以節省系統調用次數,降低延遲。(注:如果用ET模式,讀的時候讀到EAGAIN,寫的時候直到output buffer寫完成或者EAGAIN)

為什么需要有應用層緩沖區?

non-blocking網絡編程中,non-blocking IO核心思想是避免阻塞在read()/write()或其他IO系統調用上,可以最大限度復用thread-of-control,讓一個線程能服務於多個socket連接。而IO線程只能阻塞在IO-multiplexing函數上,如select()/poll()/epoll_wait(),這樣應用層的緩沖區就是必須的,每個TCP socket都要有stateful的input buffer和output buffer。

具體來說,

  • TcpConnection必須要有output buffer
    一個常見場景:程序想通過TCP連接發送100K byte數據,但在write()調用中,OS只接收80K(受TCP通告窗口advertised window的控制),而程序又不能原地阻塞等待,事實上也不知道要等多久。程序應該盡快交出控制器,返回到event loop。此時,剩余20K數據怎么辦?

對應用程序,它只管生成數據,不應該關系到底數據是一次發送,還是分幾次發送,這些應該由網絡庫操心,程序只需要調用TcpConnection::send()就行。網絡庫應該接管剩余的20K數據,把它保存到TcpConnection的output buffer,然后注冊POLLOUT事件,一旦socket變得可寫就立刻發送數據。當然,第二次不一定能完全寫入20K,如果有剩余,網絡庫應該繼續關注POLLOUT事件;如果寫完20K,網絡庫應該停止關注POLLOUT,以免造成busy loop。

如果程序又寫入50K,而此時output buffer里還有待發20K數據,那么網絡庫不應該直接調用write(),而應該把這50K數據append到那20K數據之后,等socket變得可寫時再一並寫入。

如果output buffer里還有待發送數據,而程序又想關閉連接,但對程序而言,調用TcpConnection::send()后就認為數據遲早會發出去,此時網絡庫不應該直接關閉連接,而要等數據發送完畢。因為此時數據可能還在內核緩沖區中,並沒有通過網卡成功發送給接收方。
將數據append到buffer,甚至write進內核,都不代表數據成功發送給對端。

綜上,要讓程序在write操作上不阻塞,網絡庫必須給每個tcp connection配置output buffer。

  • TcpConnection必須要有input buffer
    TCP是一個無邊界的字節流協議,接收方必須要處理“收到的數據尚不構成一條完整的消息”“一次收到兩條消息的數據”等情況。
    一個常見場景:發送方send 2條10K byte消息(共計20K),接收方收到數據的可能情況:
  • 一次性收到20K數據
  • 分2次收到,第一次5K,第二次15K
  • 分2次收到,第一次15K,第二次5K
  • 分2次收到,第一次10K,第二次10K
  • 分3次收到,第一次6K,第二次8K,第三次6K
  • 其他任何可能

這些情況統稱為粘包問題。

LT模式下,如何解決的粘包問題?
可以一次把內核緩沖區中的數據讀完,存至input buffer,通知應用程序,進行onMessage(Buffer* buffer)回調。在onMessage回調中,應用層協議判定是否是一個完整的包,如果不是一條完整的消息,不會取走數據,也不會進行相應的處理;如果是一條完整的消息,將取走這條消息,並進行相應的處理。

如何判斷是一條完整的消息?
相應應用層協議制定協議,不由網絡庫負責。

網絡庫如何處理 “socket可讀”事件?
一次性把socket中數據讀完,從內核緩沖區讀取到應用層buffer,否則會反復觸發POLLIN事件,造成busy-loop。

綜上,tcp網絡編程中,網絡庫必須給每個tcp connection配置input buffer。

應用層與input buffer、output buffer

muduo中的IO都是帶緩沖的IO(buffered IO),應用層不會自行去read()或write()某個socket,只會操作TcpConnection的input buffer和output buffer。更准確來說,是在onMessage()回調中讀取input buffer;調用TcpConnection::send() 來間接操作output buffer,而不直接操作output buffer。


Buffer要求

muduo buffer 的設計考慮常見網絡編程需求,在易用性和性能之間找一個平衡點。目前更偏向易用性。

muduo Buffer設計要點:

  • 對外表現為一塊連續的內存(char*, len),以便客戶代碼的編寫。
  • size()可以自動增長,以適應不同大小的消息。不是一個fixed size array(不是固定大小數組,char buf[8192])。
  • 內部以vector of char來保存數據,並提供相應的訪問函數。

Buffer更像一個queue,從末尾寫入數據,從頭部讀出數據。

誰使用Buffer,讀、寫Buffer?
TcpConnection有2個Buffer:input buffer,output buffer。

  • input buffer,TcpConnection會從socket讀取數據,然后寫入input buffer(由Buffer::readFd()完成);客戶代碼在onMessage回調中,從input buffer讀取數據。
  • output buffer,客戶代碼把數據寫入output buffer(用Connection::send()完成);TcpConnection從output buffer讀取數據並寫入socket。

input、output是針對客戶代碼而言的,對TcpConnection來說讀寫方向相反。

線程安全

Buffer故意設計成非線程安全的,原因如下:

  • 對於input buffer,onMessage()回調發生在該TcpConnection所屬IO線程,應用程序應該在onMessage()完成對input buffer的操作,並且不要把input buffer暴露給其他線程。這樣,對input buffer的操作都在同一個IO線程,因此Buffer class不必是線程安全的。
  • 對於output buffer,應用程序不會直接操作它,而是調用TcpConenction::send()來發送數據,后者是線程安全的。准確來說,是會讓output buffer只會在所屬IO線程操作。

Buffer數據結構

2個indices(標記)readIndex、writeIndex把vector內容分為3塊:prependable、readable、writable,各塊大小關系:
prependable = readIndex
readable = writeIndex - readIndex
writable = size() - writeIndex

灰色部分是Buffer的有效載荷(payload),prependable能讓程序以極低代價在數據前添加幾個字節,從而簡化客戶代碼。

readIndex,writeIndex滿足以下不變式(invariant):
0 ≤ readIndex ≤ writeIndex ≤ data.size()

Buffer class的核心數據成員:

class Buffer : public muduo::copyable
{
public:
    ...
private:
    std::vector<char> buffer_; // 存儲數據的線性緩沖區, 大小可變
    size_t readerIndex_; // 可讀數據首地址, i.e. readable空間首地址
    size_t writerIndex_; // 可寫數據首地址, i.e. writable空間首地址
    ...
};

Buffer中有2個常數:kCheapPrepend,kInitialSize,分別定義了prependable初始大小,writable的初始大小。readable初始大小0。

    static const size_t kCheapPrepend = 8;   // 初始預留的prependable空間大小
    static const size_t kInitialSize = 1024; // Buffer初始大小

初始化完成后,Buffer數據結構如下:


Buffer的操作

基本IO操作

Buffer初始化完成后,向Buffer寫入200byte,其布局是:

可以看到,writeIndex向后移動了200,readIndex保持不變。readable、writable都有變化。

如果從Buffer read(), retrieve()(“讀入”)50byte,其布局是:

可以看到,readIndex向后移動50,writeIndex保持不變,readable和writable的值也有變化。

自動增長

Buffer長度不是固定的,可以自動增長,因為底層存儲利用的是vector

假設當前可寫空間writable為624 byte,現客戶代碼一次寫入1000,那么buffer會自動增長,如下圖。

增長前:

增長后:

readable由350增長為1350(剛好增加了1000),writable由624減為0。另外,readIndex由58回到了初始位置8,保證prependable等於kCheapPrependable。

buffer沒有縮小功能,下次寫入1350byte就不會重新分配內存,一方面避免浪費內存,另一方面避免反復分配內存。如果有需要,客戶代碼也可以手動shrink() buffer size()。

size()和capacity()

Buffer使用vector 存儲數據,可以得到它的capacity()機制的好處,減少內存分配次數。在寫入的數據不超過capacity() - size()時,都不會重新分配內存。

在muduo中,不調用reserve()預先分配空間,而是在Buffer構造時把初始size()設為1K,當size()超過1K時,vector會把capacity()加倍,等於說resize()做了reserve()的事。

內部騰挪

經過若干次讀寫,readIndex移到了比較靠后的位置,留下了很大的prependable空間,如下圖已由初始8 byte,變成532。

此時,如果想寫入300byte,而writable只有200,怎么辦?
Buffer不會重新分配內存,而是先把已有的數據移到前面去,減小多余prependable空間,為writable騰出空間。

這樣,writable變成724 byte,就可以寫入300 byte 數據了。

prepend

prepend 提供prependable空間,讓程序能以極低的代價在數據前添加幾個字節。通過預留kCheapPrepend空間,能簡化客戶代碼。


Buffer類的實現

構造函數與析構函數

構造函數見下,析構函數才用trivial dctor(編譯器合成的默認版本,即平凡的析構函數)

    /* 構造函數 內部緩沖區buffer_初始大小默認kCheapPrepend + initialSize (1032byte) */
    explicit Buffer(size_t initialSize = kInitialSize)
    : buffer_(kCheapPrepend + initialSize),
      readerIndex_(kCheapPrepend), // 8
      writerIndex_(kCheapPrepend)  // 8
    {
        assert(readableBytes() == 0);
        assert(writableBytes() == initialSize);
        assert(prependableBytes() == kCheapPrepend);
    }

    /*
     * implicit copy-ctor, move-ctor, dtor and assignment are fine
     * NOTE: implicit move-ctor is added in g++ 4.6
     */

讀取prependable, readable, writable空間地址、大小等屬性的方法

    /* 返回 readable 空間大小 */
    size_t readableBytes() const
    { return writerIndex_ - readerIndex_; }

    /* 返回 writeable 空間大小 */
    size_t writableBytes() const
    { return buffer_.size() - writerIndex_; }

    /* 返回 prependable 空間大小 */
    size_t prependableBytes() const
    { return readerIndex_; }

    /* readIndex 對應元素地址 */
    const char* peek() const
    { return begin() + readerIndex_; }

    /* 返回待寫入數據的地址, 即writable空間首地址 */
    char* beginWrite()
    { return begin() + writerIndex_; }
    const char* beginWrite() const
    { return begin() + writerIndex_; }

    /* 將writerIndex_往后移動len byte, 需要確保writable空間足夠大 */
    void hasWritten(size_t len)
    {
        assert(len <= writableBytes());
        writerIndex_ += len;
    }
    /* 將writerIndex_往前移動len byte, 需要確保readable空間足夠大 */
    /*
     * Cancel written bytes.
     */
    void unwrite(size_t len)
    {
        assert(len <= readableBytes());
        writerIndex_ -= len;
    }

private:
    /* 返回緩沖區的起始位置, 也是prependable空間起始位置 */
    char* begin()
    {
//       <=> return buffer_.data();
        return &*buffer_.begin();
    }
    const char* begin() const
    { return &*buffer_.begin(); }

retrieve*取走數據

retrieve系列函數從readable空間取走數據,只關心移動readableIndex_,改變readable空間大小,通常不關心讀取數據具體內容,除非有指定具體的返回值,如retrieveAllAsString。
retrieve系列函數會改變readable空間大小,但通常不會改變writable空間大小,除非retrieveAll取完所有readable空間的數據,readable空間將會合並到writable空間。

    /* 從readable頭部取走最多長度為len byte的數據. 會導致readable空間變化, 可能導致writable空間變化.
     * 這里取走只是移動readerIndex_, writerIndex_, 並不會直接讀取或清除readable, writable空間數據 */
    /*
     * retrieve() returns void, to prevent
     * string str(retrieve(readableBytes()), readableBytes());
     * the evaluation of two functions are unspecified
     */
    void retrieve(size_t len)
    {
        assert(len <= readableBytes());
        if (len < readableBytes()) // readable 中數據充足時, 只取走len byte數據
        {
            readerIndex_ += len;
        }
        else
        { // readable中數據不足時, 取走所有數據
            retrieveAll();
        }
    }
    /* 從readable空間取走 [peek(), end)這段區間數據, peek()是readable空間首地址 */
    void retrieveUntil(const char* end)
    {
        assert(peek() <= end);
        assert(end <= beginWrite());
        retrieve(end - peek());
    }
    /* 從readable空間取走一個int64_t數據, 長度8byte */
    void retrieveInt64()
    {
        retrieve(sizeof(int64_t));
    }
    /* 從readable空間取走一個int32_t數據, 長度4byte */
    void retrieveInt32()
    {
        retrieve(sizeof(int32_t));
    }
    /* 從readable空間取走一個int16_t數據, 長度2byte */
    void retrieveInt16()
    {
        retrieve(sizeof(int16_t));
    }
    /* 從readable空間取走一個int8_t數據, 長度1byte */
    void retrieveInt8()
    {
        retrieve(sizeof(int8_t));
    }
    /* 從readable空間取走所有數據, 直接移動readerIndex_, writerIndex_指示器即可 */
    void retrieveAll()
    {
        readerIndex_ = kCheapPrepend;
        writerIndex_ = kCheapPrepend;
    }
    /* 從readable空間取走所有數據, 轉換為字符串返回 */
    std::string retrieveAllAsString()
    {
        return retrieveAsString(readableBytes());
    }
    /* 從readable空間頭部取走長度len byte的數據, 轉換為字符串返回 */
    std::string retrieveAsString(size_t len)
    {
        assert(len <= readableBytes());
        string result(peek(), len);
        retrieve(len);
        return result;
    }

readInt*取數據

readInt系列函數從readable空間讀取指定長度(類型)的數據,不僅從readable空間讀取數據,還會利用相應的retrieve函數把數據從中取走,導致readable空間變小。

    /* 從readable空間頭部讀取一個int64_類型數, 由網絡字節序轉換為本地字節序 */
    /**
     * Read int64_t from network endian
     *
     * Require: buf->readableBytes() >= sizeof(int64_t)
     */
    int64_t readInt64()
    {
        int64_t result = peekInt64();
        retrieveInt64();
        return result;
    }
    /* 從readable空間頭部讀取一個int32_類型數, 由網絡字節序轉換為本地字節序 */
    int32_t readInt32()
    {
        int32_t result = peekInt32();
        retrieveInt32();
        return result;
    }
    /* 從readable空間頭部讀取一個int16_類型數, 由網絡字節序轉換為本地字節序 */
    int16_t readInt16()
    {
        int16_t result = peekInt16();
        retrieveInt16();
        return result;
    }
    /* 從readable空間頭部讀取一個int8_類型數, 由網絡字節序轉換為本地字節序 */
    int8_t readInt8()
    {
        int8_t result = peekInt8();
        retrieveInt8();
        return result;
    }

peek*讀取而不取走緩沖區數據

peek系列函數只從readable空間頭部(peek())讀取數據,而不取走數據,不會導致readable空間變化。

    /* 從readable的頭部peek()讀取一個int64_t數據, 但不移動readerIndex_, 不會改變readable空間 */
    /**
     * Peek int64_t from network endian
     *
     * Require: buf->readableBytes() >= sizeof(int64_t)
     */
    int64_t peekInt64() const
    {
        assert(readableBytes() >= sizeof(int64_t));
        int64_t be64 = 0;
        ::memcpy(&be64, peek(), sizeof(be64));
        return sockets::networkToHost64(be64); // 網絡字節序轉換為本地字節序
    }
    /* 從readable的頭部peek()讀取一個int32_t數據, 但不移動readerIndex_, 不會改變readable空間 */
    int32_t peekInt32() const
    {
        assert(readableBytes() >= sizeof(int32_t));
        int32_t be32 = 0;
        ::memcpy(&be32, peek(), sizeof(be32));
        return sockets::networkToHost32(be32); // 網絡字節序轉換為本地字節序
    }
    /* 從readable的頭部peek()讀取一個int16_t數據, 但不移動readerIndex_, 不會改變readable空間 */
    int16_t peekInt16() const
    {
        assert(readableBytes() >= sizeof(int16_t));
        int16_t be16 = 0;
        ::memcpy(&be16, peek(), sizeof(be16));
        return sockets::networkToHost16(be16); // 網絡字節序轉換為本地字節序
    }
    /* 從readable的頭部peek()讀取一個int8_t數據, 但不移動readerIndex_, 不會改變readable空間.
     * 1byte數據不存在字節序問題 */
    int8_t peekInt8() const
    {
        assert(readableBytes() >= sizeof(int8_t));
        int8_t x = *peek();
        return x;
    }

prepend*預置數據到緩沖區

prepend系列函數將預置指定長度數據到prependable空間,但不會改變prependable空間大小。

    /* 在prependable空間末尾預置int64_t類型網絡字節序的數x, 預置數會被轉化為本地字節序 */
    /**
     * Prepend int64_t using network endian
     */
     void prependInt64(int64_t x)
    {
         int64_t be64 = sockets::hostToNetwork64(x);
         prepend(&be64, sizeof(be64));
    }
    /* 在prependable空間末尾預置int32_t類型網絡字節序的數x, 預置數會被轉化為本地字節序 */
    /**
     * Prepend int32_t using network endian
     */
    void prependInt32(int32_t x)
    {
        int64_t be32 = sockets::hostToNetwork32(x);
        prepend(&be32, sizeof(be32));
    }
    /* 在prependable空間末尾預置int16_t類型網絡字節序的數x, 預置數會被轉化為本地字節序 */
    void prependInt16(int16_t x)
    {
        int16_t be16 = sockets::hostToNetwork16(x);
        prepend(&be16, sizeof(be16));
    }
    /* 在prependable空間末尾預置int8_t類型網絡字節序的數x, 預置數會被轉化為本地字節序 */
    void prependInt8(int8_t x)
    {
        prepend(&x, sizeof(x));
    }
    /* 在prependable空間末尾預置一組二進制數據data[len].
     * 表面上看是加入prependable空間末尾, 實際上是加入readable開頭, 會導致readerIndex_變化 */
    void prepend(const void* data, size_t len)
    {
        assert(len <= prependableBytes());
        readerIndex_ -= len;
        const char* d = static_cast<const char*>(data);
        std::copy(d, d+len, begin()+readerIndex_);
    }

內部緩沖區操作

包含3個操作:
1)shrink自動收縮內部緩沖區(buffer_)大小;
2)返回buffer_的capaticy;
3)makeSpace生產足夠大小的writable空間,以寫入新的len byte數據;

    /* 收縮緩沖區空間, 將緩沖區中數據拷貝到新緩沖區, 確保writable空間最終大小為reserve */
    void shrink(size_t reserve)
    {
        // FIXME: use vector::shrink_to_fit() in C++ 11 if possible.
        Buffer other;
        other.ensureWritableBytes(readableBytes() + reserve);
        other.append(toStringPiece());
        swap(other);
    }

    /* 返回buffer_的容量capacity() */
    size_t internalCapacity() const
    {
        return buffer_.capacity();
    }

    /* writable空間不足以寫入len byte數據時,
     * 1)如果writable空間 + prependable空間不足以存放數據, 就resize 申請新的更大的內部緩沖區buffer_
     * 2)如果足以存放數據, 就將prependable多余空間騰挪出來, 合並到writable空間 */
    void makeSpace(size_t len)
    {
        if (writableBytes() + prependableBytes() < len + kCheapPrepend)
        { // writable 空間大小 + prependable空間大小 不足以存放len byte數據, resize內部緩沖區大小
            // FIXME: move readable data
            buffer_.resize(writerIndex_ + len);
        }
        else
        { // writable 空間大小 + prependable空間大小 足以存放len byte數據, 移動readable空間數據, 合並多余prependable空間到writable空間
            // TODO: ???
            // move readable data to the front, make space inside buffer
            assert(kCheapPrepend < readerIndex_);
            size_t readable = readableBytes();
            std::copy(begin() + readerIndex_,
                      begin() + writerIndex_,
                      begin() + kCheapPrepend);
            readerIndex_ = kCheapPrepend;
            writerIndex_ = readerIndex_ + readable;
            assert(readable == readableBytes());
        }
    }

readFd從指定fd讀取數據

readFd從指定連接對應fd讀取數據,當讀取的數據超過內部緩沖區writable空間大小時,采用的策略是先用一個64K棧緩存extrabuf臨時存儲,然后根據需要合並prependable空間到writable空間,或者resize buffer_大小。

/**
* 從fd讀取數據到內部緩沖區, 將系統調用錯誤保存至savedErrno
* @param 要讀取的fd, 通常是代表連接的conn fd
* @param savedErrno[out] 保存的錯誤號
* @return 讀取數據結果. < 0, 發生錯誤; >= 成功, 讀取到的字節數
*/
ssize_t Buffer::readFd(int fd, int* savedErrno)
{
    // saved an ioctl()/FIONREAD call to tell how much to read.
    char extrabuf[65536]; // 65536 = 64K bytes
    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);

    const int iovcnt = (writable < sizeof(extrabuf)) ? 2 : 1;
    const ssize_t n = sockets::readv(fd, vec, iovcnt);
    if (n < 0)
    { // ::readv系統調用錯誤
        *savedErrno = errno;
    }
    else if (implicit_cast<size_t>(n) <= writable)
    {
        writerIndex_ += n;
    }
    else
    {// 讀取的數據超過現有內部buffer_的writable空間大小時, 啟用備用的extrabuf 64KB空間, 並將這些數據添加到內部buffer_的末尾
        // 過程可能會合並多余prependable空間或resize buffer_大小, 以騰出足夠writable空間存放數據
        // n >= 0 and n > writable
        // => buffer_ is full, then append extrabuf to buffer_
        writerIndex_ = buffer_.size();
        append(extrabuf, n - writable);
    }
    return n;
}

readable空間特定字符串查找

有時,用戶需要先對readable空間中的字符進行專門的查找,如找CRLF,EOL,而不取走數據,根據查找結果再決定取走多少數據。muduo庫提供了幾個輔助函數,便於用戶進行這樣的讀取操作。

    /* 在readable空間找CRLF位置, 返回第一個出現CRLF的位置 */
    // CR = '\r', LF = '\n'
    const char* findCRLF() const
    {
        // FIXME: replace with memmem()?
        const char* crlf = std::search(peek(), beginWrite(), kCRLF, kCRLF+2);
        return crlf == beginWrite() ? NULL : crlf;
    }

    /* 在start~writable首地址 之間找CRLF, 要求start在readable地址空間中 */
    // CR = '\r', LF = '\n'
    const char* findCRLF(const char* start) const
    {
        assert(peek() <= start);
        assert(start <= beginWrite());
        // FIXME: replace with memmem()?
        const char* crlf = std::search(start, beginWrite(), kCRLF, kCRLF+2);
        return crlf == beginWrite() ? NULL : crlf;
    }

    /* 在readable空間中找EOL, 即LF('\n') */
    // EOL = '\n'
    // find end of line ('\n') from range [peek(), end)
    const char* findEOL() const
    {
        const void* eol = memchr(peek(), '\n', readableBytes());
        return static_cast<const char*>(eol);
    }

    /* 在start~writable首地址 之間找EOL, 要求start在readable地址空間中 */
    // EOL = '\n
    // find end of line ('\n') from range [start(), end)
    // @require peek() < start
    const char* findEOL(const char* start) const
    {
        assert(peek() <= start);
        assert(start <= beginWrite());
        const void* eol = memchr(start, '\n', beginWrite() - start);
        return static_cast<const char*>(eol);
    }

測試Buffer類

主要測試幾個public 接口:append, retrieve, peekInt, readInt*, findEOL/findCRLF, std::move。
測試的時候,主要通過觀察prependable, readable, writable 這3個空間的大小變化,看是否等於預期值。這樣可以實現自動化比較、測試。

// test append data to writable space
// test retrieve data from readable space
BOOST_AUTO_TEST_CASE(testBufferAppendRetrieve)
{
    // check constructor
    Buffer buf;
    BOOST_CHECK_EQUAL(buf.readableBytes(), 0);
    BOOST_CHECK_EQUAL(buf.writableBytes(), Buffer::kInitialSize);
    BOOST_CHECK_EQUAL(buf.prependableBytes(), Buffer::kCheapPrepend);

    // check append string
    const string str(200, 'x');
    buf.append(str);
    BOOST_CHECK_EQUAL(buf.readableBytes(), str.size());
    BOOST_CHECK_EQUAL(buf.writableBytes(), Buffer::kInitialSize - str.size());
    BOOST_CHECK_EQUAL(buf.prependableBytes(), Buffer::kCheapPrepend);

    // check retrieveAsString(num)
    const string str2 = buf.retrieveAsString(50);
    BOOST_CHECK_EQUAL(str2.size(), 50);
    BOOST_CHECK_EQUAL(buf.readableBytes(), str.size() - str2.size());
    BOOST_CHECK_EQUAL(buf.writableBytes(), Buffer::kInitialSize - str.size());
    BOOST_CHECK_EQUAL(buf.prependableBytes(), Buffer::kCheapPrepend + str2.size());
    BOOST_CHECK_EQUAL(str2, string(50, 'x'));

    // check append string
    buf.append(str);
    BOOST_CHECK_EQUAL(buf.readableBytes(), 2*str.size() - str2.size());
    BOOST_CHECK_EQUAL(buf.writableBytes(), Buffer::kInitialSize - 2*str.size());
    BOOST_CHECK_EQUAL(buf.prependableBytes(), Buffer::kCheapPrepend + str2.size());

    // check retrieveAllAsString()
    const string str3 =  buf.retrieveAllAsString();
    BOOST_CHECK_EQUAL(str3.size(), 350);
    BOOST_CHECK_EQUAL(buf.readableBytes(), 0);
    BOOST_CHECK_EQUAL(buf.writableBytes(), Buffer::kInitialSize);
    BOOST_CHECK_EQUAL(buf.prependableBytes(), Buffer::kCheapPrepend);
    BOOST_CHECK_EQUAL(str3, string(350, 'x'));
}

// test resize buffer size
BOOST_AUTO_TEST_CASE(testBufferGrow)
{
    // check buffer fileSize after ctor
    Buffer buf;
    buf.append(string(400, 'y'));
    BOOST_CHECK_EQUAL(buf.readableBytes(), 400);
    BOOST_CHECK_EQUAL(buf.writableBytes(), Buffer::kInitialSize-400);

    // check buffer fileSize after retrieve(num)
    buf.retrieve(50);
    BOOST_CHECK_EQUAL(buf.readableBytes(), 350);
    BOOST_CHECK_EQUAL(buf.writableBytes(), Buffer::kInitialSize-400);
    BOOST_CHECK_EQUAL(buf.prependableBytes(), Buffer::kCheapPrepend+50);

    // check buffer fileSize after append
    buf.append(string(1000, 'z'));
    BOOST_CHECK_EQUAL(buf.readableBytes(), 1350);
    BOOST_CHECK_EQUAL(buf.writableBytes(), 0);
    BOOST_CHECK_EQUAL(buf.prependableBytes(), Buffer::kCheapPrepend+50); // FIXME

    // check buffer fileSize after retrieveAll()
    buf.retrieveAll();
    BOOST_CHECK_EQUAL(buf.readableBytes(), 0);
    BOOST_CHECK_EQUAL(buf.writableBytes(), 1400); // FIXME
    BOOST_CHECK_EQUAL(buf.prependableBytes(), Buffer::kCheapPrepend);
}

// test prependable, readable, writable size
// after makeSpace() growing inside writable size.
// inside grow type : merge prependable space with writable space
BOOST_AUTO_TEST_CASE(testBufferInsideGrow)
{
    // check buffer fileSize after ctor
    Buffer buf;
    buf.append(string(800, 'y'));
    BOOST_CHECK_EQUAL(buf.readableBytes(), 800);
    BOOST_CHECK_EQUAL(buf.writableBytes(), Buffer::kInitialSize-800);

    // check buffer fileSize after retrieve(num)
    buf.retrieve(500);
    BOOST_CHECK_EQUAL(buf.readableBytes(), 300);
    BOOST_CHECK_EQUAL(buf.writableBytes(), Buffer::kInitialSize-800);
    BOOST_CHECK_EQUAL(buf.prependableBytes(), Buffer::kCheapPrepend+500);

    // check buffer fileSize after append string
    buf.append(string(300, 'z'));
    BOOST_CHECK_EQUAL(buf.readableBytes(), 600);
    BOOST_CHECK_EQUAL(buf.writableBytes(), Buffer::kInitialSize-600);
    BOOST_CHECK_EQUAL(buf.prependableBytes(), Buffer::kCheapPrepend);
}

/* test shrink */
BOOST_AUTO_TEST_CASE(testBufferShrink)
{
    // check buffer fileSize after ctor
    Buffer buf;
    buf.append(string(2000, 'y'));
    BOOST_CHECK_EQUAL(buf.readableBytes(), 2000);
    BOOST_CHECK_EQUAL(buf.writableBytes(), 0);
    BOOST_CHECK_EQUAL(buf.prependableBytes(), Buffer::kCheapPrepend);

    // check buffer fileSize after retrieve(num)
    buf.retrieve(1500);
    BOOST_CHECK_EQUAL(buf.readableBytes(), 500);
    BOOST_CHECK_EQUAL(buf.writableBytes(), 0);
    BOOST_CHECK_EQUAL(buf.prependableBytes(), Buffer::kCheapPrepend+1500);

    // check buffer fileSize after shrink string
    buf.shrink(0);
    BOOST_CHECK_EQUAL(buf.readableBytes(), 500);
    BOOST_CHECK_EQUAL(buf.writableBytes(), Buffer::kInitialSize-500);
    BOOST_CHECK_EQUAL(buf.retrieveAllAsString(), string(500, 'y'));
    BOOST_CHECK_EQUAL(buf.prependableBytes(), Buffer::kCheapPrepend);
}

// check buffer prepend
BOOST_AUTO_TEST_CASE(testBufferPrepend)
{
    Buffer buf;
    buf.append(string(200, 'y'));
    BOOST_CHECK_EQUAL(buf.readableBytes(), 200);
    BOOST_CHECK_EQUAL(buf.writableBytes(), Buffer::kInitialSize-200);
    BOOST_CHECK_EQUAL(buf.prependableBytes(), Buffer::kCheapPrepend);

    int x = 0;
    buf.prepend(&x, sizeof x);
    BOOST_CHECK_EQUAL(buf.readableBytes(), 204);
    BOOST_CHECK_EQUAL(buf.writableBytes(), Buffer::kInitialSize-200);
    BOOST_CHECK_EQUAL(buf.prependableBytes(), Buffer::kCheapPrepend - 4);
}

// test read int from readable segment
BOOST_AUTO_TEST_CASE(testBufferReadInt)
{
    Buffer buf;
    buf.append("HTTP");

    BOOST_CHECK_EQUAL(buf.readableBytes(), 4);
    BOOST_CHECK_EQUAL(buf.peekInt8(), 'H');
    int top16 = buf.peekInt16();
    BOOST_CHECK_EQUAL(top16, 'H'*256 + 'T');
    BOOST_CHECK_EQUAL(buf.peekInt32(), top16*65536 + 'T'*256 + 'P');

    BOOST_CHECK_EQUAL(buf.readInt8(), 'H');
    BOOST_CHECK_EQUAL(buf.readInt16(), 'T'*256 + 'T');
    BOOST_CHECK_EQUAL(buf.readInt8(), 'P');
    BOOST_CHECK_EQUAL(buf.readableBytes(), 0);
    BOOST_CHECK_EQUAL(buf.writableBytes(), Buffer::kInitialSize);

    buf.appendInt8(-1);
    buf.appendInt16(-2);
    buf.appendInt32(-3);
    BOOST_CHECK_EQUAL(buf.readableBytes(), 7);
    BOOST_CHECK_EQUAL(buf.readInt8(), -1);
    BOOST_CHECK_EQUAL(buf.readInt16(), -2);
    BOOST_CHECK_EQUAL(buf.readInt32(), -3);
}

// test find EOL
BOOST_AUTO_TEST_CASE(testBufferFindEOL)
{
    Buffer buf;
    buf.append(string(100000, 'x'));
    const char* null = NULL;
    BOOST_CHECK_EQUAL(buf.findEOL(), null);
    BOOST_CHECK_EQUAL(buf.findEOL(buf.peek()+90000), null);
}

void output(Buffer&& buf, const void* inner)
{
    Buffer newbuf(std::move(buf)); // move ctor
    // printf("New Buffer at %p, inner %p\n", &newbuf, newbuf.peek());
    BOOST_CHECK_EQUAL(inner, newbuf.peek());
}

// test std::move for Buffer
// NOTE: This test fails in g++ 4.4, passes in g++ 4.6.
BOOST_AUTO_TEST_CASE(testMove)
{
    Buffer buf;
    buf.append("muduo", 5);
    const void* inner = buf.peek();
    // printf("Buffer at %p, inner %p\n", &buf, inner);
    output(std::move(buf), inner);
}

注意:Buffer沒有自定義move ctor,采用的是編譯器合成的默認版本。


知識點

大小端轉換

網絡字節序默認大端,本地字節序可能大端,也可能小端。網絡字節序和本地字節序,可以用htobe* / betoh* 系列函數轉換,也可以用htonl/htons/ntohl/ntohs轉換,前者是glibc 2.9的部分,屬於BSDs標准,后者屬於POSIX.1-2001標准。也就是說,如果要開發可移植的程序,使用htonl/htons/ntohl/ntohs系列函數轉換;否則,如果只考慮特定平台如Linux,可以使用htobe* / betoh* 系列函數轉換。

htobe* / betoh* 系列函數:
支持16bit、32bit、64bit 字節序的數據轉換

       #define _BSD_SOURCE             /* See feature_test_macros(7) */
       #include <endian.h>

       uint16_t htobe16(uint16_t host_16bits);
       uint16_t htole16(uint16_t host_16bits);
       uint16_t be16toh(uint16_t big_endian_16bits);
       uint16_t le16toh(uint16_t little_endian_16bits);

       uint32_t htobe32(uint32_t host_32bits);
       uint32_t htole32(uint32_t host_32bits);
       uint32_t be32toh(uint32_t big_endian_32bits);
       uint32_t le32toh(uint32_t little_endian_32bits);

       uint64_t htobe64(uint64_t host_64bits);
       uint64_t htole64(uint64_t host_64bits);
       uint64_t be64toh(uint64_t big_endian_64bits);
       uint64_t le64toh(uint64_t little_endian_64bits);

htonl/htons/ntohl/ntohs系列函數:
只支持16bit、32bit 字節序的數據轉換

       #include <arpa/inet.h>

       uint32_t htonl(uint32_t hostlong);
       uint16_t htons(uint16_t hostshort);
       uint32_t ntohl(uint32_t netlong);
       uint16_t ntohs(uint16_t netshort);

參考

Muduo 網絡庫使用手冊 陳碩


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM