應用層緩沖區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
在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 網絡庫使用手冊 陳碩