本节主要介绍缓冲相关的传输类,缓存的作用就是为了提高读写的效率。Thrift在实现缓存传输的时候首先建立一个缓存的基类,然后需要实现缓存功能的类都可以直接从这个基类继承。下面就详细分析这个基类以及一个具体的实现类。
缓存基类TBufferBase
缓存基类就是让传输类所有的读写函数都提供缓存来提高性能。它在通常情况下采用memcpy来设计和实现快路径的读写访问操作,这些操作函数通常都是小、非虚拟和内联函数。TBufferBase是一个抽象的基类,子类必须实现慢路径的读写函数等操作,慢路径的读写等操作主要是为了在缓存已经满或空的情况下执行。首先看看缓存基类的定义,代码如下:
class TBufferBase : public TVirtualTransport<TBufferBase> { public: uint32_t read(uint8_t* buf, uint32_t len) {//读函数 uint8_t* new_rBase = rBase_ + len;//得到需要读到的缓存边界 if (TDB_LIKELY(new_rBase <= rBound_)) {//判断缓存是否有足够的数据可读,采用了分支预测技术 std::memcpy(buf, rBase_, len);//直接内存拷贝 rBase_ = new_rBase;//更新新的缓存读基地址 return len;//返回读取的长度 } return readSlow(buf, len);//如果缓存已经不能够满足读取长度需要就执行慢读 } uint32_t readAll(uint8_t* buf, uint32_t len) { uint8_t* new_rBase = rBase_ + len;//同read函数 if (TDB_LIKELY(new_rBase <= rBound_)) { std::memcpy(buf, rBase_, len); rBase_ = new_rBase; return len; } return apache::thrift::transport::readAll(*this, buf, len);//调用父类的 } void write(const uint8_t* buf, uint32_t len) {//快速写函数 uint8_t* new_wBase = wBase_ + len;//写入后的新缓存基地址 if (TDB_LIKELY(new_wBase <= wBound_)) {//判断缓存是否有足够的空间可以写入 std::memcpy(wBase_, buf, len);//内存拷贝 wBase_ = new_wBase;//更新基地址 return; } writeSlow(buf, len);//缓存空间不足就调用慢写函数 } const uint8_t* borrow(uint8_t* buf, uint32_t* len) {//快速路径借 if (TDB_LIKELY(static_cast<ptrdiff_t>(*len) <= rBound_ - rBase_)) {//判断是否足够借的长度 *len = static_cast<uint32_t>(rBound_ - rBase_); return rBase_;//返回借的基地址 } return borrowSlow(buf, len);//不足就采用慢路径借 } void consume(uint32_t len) {//消费函数 if (TDB_LIKELY(static_cast<ptrdiff_t>(len) <= rBound_ - rBase_)) {//判断缓存是否够消费 rBase_ += len;//更新已经消耗的长度 } else { throw TTransportException(TTransportException::BAD_ARGS, "consume did not follow a borrow.");//不足抛异常 } } protected: virtual uint32_t readSlow(uint8_t* buf, uint32_t len) = 0;//慢函数 virtual void writeSlow(const uint8_t* buf, uint32_t len) = 0; virtual const uint8_t* borrowSlow(uint8_t* buf, uint32_t* len) = 0; TBufferBase() : rBase_(NULL) , rBound_(NULL) , wBase_(NULL) , wBound_(NULL) {}//构造函数,把所有的缓存空间设置为NULL void setReadBuffer(uint8_t* buf, uint32_t len) {//设置读缓存空间地址 rBase_ = buf;//读缓存开始地址 rBound_ = buf+len;//读缓存地址界限 } void setWriteBuffer(uint8_t* buf, uint32_t len) {//设置写缓存地址空间 wBase_ = buf;//起 wBound_ = buf+len;//边界 } virtual ~TBufferBase() {} uint8_t* rBase_;//读从这儿开始 uint8_t* rBound_;//读界限 uint8_t* wBase_;//写开始地址 uint8_t* wBound_;//写界限 };
从TBufferBase定义可以看出,它也是从虚拟类继承,主要采用了memcpy函数来实现缓存的快速读取,在判断是否有足够的缓存空间可以操作时采用了分支预测技术来提供代码的执行效率,且所有快路径函数都是非虚拟的、内联的小代码量函数。下面继续看看一个具体实现缓存基类的一个子类的情况!
TBufferedTransport
缓存传输类是从缓存基类继承而来,它对于读:实际读数据的大小比实际请求的大很多,多余的数据将为将来超过本地缓存的数据服务;对于写:数据在它被发送出去以前将被先写入内存缓存。
缓存的大小默认是512字节(代码:static const int DEFAULT_BUFFER_SIZE = 512;),提供多个构造函数,可以只指定一个传输类(另一层次的)、也可以指定读写缓存公用的大小或者分别指定。因为它是一个可以实际使用的缓存类,所以需要实现慢读和慢写功能的函数。它还实现了打开函数open、关闭函数close、刷新函数flush等,判断是否有数据处于未决状态函数peek定义和实现如下:
bool peek() { if (rBase_ == rBound_) {//判断读的基地址与读边界是否重合了,也就是已经读取完毕 setReadBuffer(rBuf_.get(), transport_->read(rBuf_.get(), rBufSize_));//是:重新读取底层来的数据 } return (rBound_ > rBase_);//边界大于基地址就是有未决状态数据 } 下面继续看看慢读函数和慢写函数的实现细节(快读和快写继承基类的:也就是默认的读写都是直接从缓存中读取,所谓的快读和快写)。慢读函数实现如下(详细注释): uint32_t TBufferedTransport::readSlow(uint8_t* buf, uint32_t len) { uint32_t have = rBound_ - rBase_;//计算还有多少数据在缓存中 // 如果读取缓存中已经存在的数据不能满足我们, // 我们(也仅仅在这种情况下)应该才从慢路径读数据。 assert(have < len); // 如果我们有一些数据在缓存,拷贝出来并返回它 // 我们不得不返回它而去尝试读更多的数据,因为我们不能保证 // 下层传输实际有更多的数据, 因此尝试阻塞式读取它。 if (have > 0) { memcpy(buf, rBase_, have);//拷贝数据 setReadBuffer(rBuf_.get(), 0);//设置读缓存,基类实现该函数 return have;//返回缓存中已经存在的不完整数据 } // 在我们的缓存中没有更多的数据可用。从下层传输得到更多以达到buffer的大小。 // 注意如果len小于rBufSize_可能会产生多种场景否则几乎是没有意义的。 setReadBuffer(rBuf_.get(), transport_->read(rBuf_.get(), rBufSize_));//读取数据并设置读缓存 // 处理我们已有的数据 uint32_t give = std::min(len, static_cast<uint32_t>(rBound_ - rBase_)); memcpy(buf, rBase_, give); rBase_ += give; return give; }
慢读函数主要考虑的问题就是缓存中还有一部分数据,但是不够我们需要读取的长度;还有比较麻烦的情况是虽然现在缓存中没有数据,但是我们从下层传输去读,读取的长度可能大于、小于或等于我们需要读取的长度,所以需要考虑各种情况。下面继续分析慢写函数实现细节:
void TBufferedTransport::writeSlow(const uint8_t* buf, uint32_t len) { uint32_t have_bytes = wBase_ - wBuf_.get();//计算写缓存区中已有的字节数 uint32_t space = wBound_ - wBase_;//计算剩余写缓存空间 // 如果在缓存区的空闲空间不能容纳我们的数据,我们采用慢路径写(仅仅) assert(wBound_ - wBase_ < static_cast<ptrdiff_t>(len)); //已有数据加上需要写入的数据是否大于2倍写缓存区或者缓存区为空 if ((have_bytes + len >= 2*wBufSize_) || (have_bytes == 0)) { if (have_bytes > 0) {//缓存大于0且加上需要再写入数据的长度大于2倍缓存区 transport_->write(wBuf_.get(), have_bytes);//先将已有数据写入下层传输 } transport_->write(buf, len);//写入这次的len长度的数据 wBase_ = wBuf_.get();//重新得到写缓存的基地址 return; } memcpy(wBase_, buf, space);//填充我们的内部缓存区为了写 buf += space; len -= space; transport_->write(wBuf_.get(), wBufSize_);//写入下层传输 assert(len < wBufSize_); memcpy(wBuf_.get(), buf, len);//拷贝剩余的数据到我们的缓存 wBase_ = wBuf_.get() + len;//重新得到写缓存基地址 return; }
慢写函数也有棘手的问题,就是我们应该拷贝我们的数据到我们的内部缓存并且从那儿发送出去,或者我们应该仅仅用一次系统调用把当前内部写缓存区的内容写出去,然后再用一次系统调用把我们当前需要写入长度为len的数据再次写入出去。如果当前缓存区的数据加上我们这次需要写入数据的长度至少是我们缓存区长度的两倍,我们将不得不至少调用两次系统调用(缓存区为空时有可能例外),那么我们就不拷贝了。否则我们就是按顺序递加的。具体实现分情况处理,最后我们在看看慢借函数的实现,借相关函数主要是为了实现可变长度编码。慢借函数实现细节如下:
const uint8_t* TBufferedTransport::borrowSlow(uint8_t* buf, uint32_t* len) { (void) buf; (void) len; return NULL;//默认返回空 }
在这个类我们可以看出,它什么也没有做,只是简单的返回NULL,所以需要阻塞去借。按照官方的说法,下面两种行为应该当前的版本中实现,在将来的版本可能会发生改变:
如果需要借的长度最多为缓存区的长度,那么永远不会返回NULL。依赖底层传输,它应该抛出一个异常或者永远不会挂掉;
一些借用请求可能内部字节拷贝,如果借用的长度最多是缓存区的一半,那么不去内部拷贝。为了优化性能保存这个限制。