文章目录
前言
thrift文章整理:
1.thrift简介
2.thrift源码解析之compiler
3.thrift源码解析之processor
4.thrift源码解析之protocol
5.thrift源码解析之transport
6.thrift源码解析之server
概述
传输的方式多种多样,可以采用压缩、分帧等,而这些功能的实现都是相互独立。
分成两部分:1、TTransport级他的子孙以及亲戚。2、各种传输类的对象生成工厂类,负责某一种具体传输类对象的生产。
TTransport
辅助传输层的全局模板函数readAll
template <class Transport_>
uint32_t readAll(Transport_ &trans, uint8_t* buf, uint32_t len) {
uint32_t have = 0;
uint32_t get = 0;
while (have < len) {
get = trans.read(buf+have, len-have);//通过具体的传输类读取剩余的需要读取的数据
if (get <= 0) {//处理数据以读完异常
throw TTransportException(TTransportException::END_OF_FILE, "No more data to read.");
}
have += get;//已经读取的字节数
}
return have;//返回读到的字节数
}
接口:
| 函数名称 | 函数参数 | 函数作用 |
|---|---|---|
| isOpen | 传输层是否打开 | |
| peek | 测试是否有数据可读或者远程那边是否任然打开。当传输是打开的默认为true,不过具体的实现逻辑需要根据可能的条件。 | |
| open | 为了通信打开传输层 | |
| close | 关闭传输层 | |
| read、read_virt | uint8_t* buf:读入数据的本地缓存。uint32_t len:需要读取数据的长度 | 尝试读取指定的字节数到字符串 |
| readAll、readAll_virt | buf、 len | 无论如何都要读取被给长度的数据 |
| readEnd | 当读取完成时调用 | |
| write、write_virt | buf、len | 写入字符串到缓存,必须调用flush函数后才真正的写入,下次读取的时候才是可利用的。 |
| writeEnd | 写入完成时调用 | |
| flush | 刷新任何被阻塞或缓存的数据真正被写入 | |
| borrow、borrow_virt | buf、len | 尝试返回一个指向len字节的字符串缓存并不是真正的读取消耗它。这个函数主要用于可变长度编码中,因为刚开始不知道具体长度。 |
| consume、consume_virt | len:消耗数据的长度 | 从传输层消耗len字节的数据,这个需要根据borrow函数具体使用的长度来决定。 |
1、通常一个传输层的对象要么作为输出要么作为输入。
2、特别注意最后两组函数,主要用于支持可变长度编码,所以如果传输层对象需要支持可变长度编码必须实现这两组函数。
TTransport的子孙类
TSocket类
基于TCP socket实现
其构造函数如下:
TSocket();//默认构造函数
TSocket(std::string host, int port);//根据主机名和端口构造一个socket
TSocket(std::string path);//构造unix域的一个socket
TSocket(int socket);//构造一个原始的unix句柄socket
用于不同的情况下来产生不同的TSocket对象,对其成员变量做一个基本的初始化。不做详细介绍,我们看一下unix domain socket。
socket API原本是为网络通讯设计的,但后来在socket的框架上发展出一种IPC机制,就是UNIX Domain Socket。虽然网络socket也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是UNIX Domain Socket用于IPC更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。这是因为,IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。UNIX Domain Socket也提供面向流和面向数据包两种API接口,类似于TCP和UDP,但是面向消息的UNIX Domain Socket也是可靠的,消息既不会丢失也不会顺序错乱。
UNIX Domain Socket是全双工的,API接口语义丰富,相比其它IPC机制有明显的优越性,目前已成为使用最广泛的IPC机制,比如X Window服务器和GUI程序之间就是通过UNIX Domain Socket通讯的。
使用UNIX Domain Socket的过程和网络socket十分相似,也要先调用socket()创建一个socket文件描述符,address family指定为AF_UNIX,type可以选择SOCK_DGRAM或SOCK_STREAM,protocol参数仍然指定为0即可。
UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同,用结构体sockaddr_un表示,网络编程的socket地址是IP地址加端口号,而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind()调用创建,如果调用bind()时该文件已存在,则bind()错误返回。
接下来我们看socket中的方法:
1、open:
void TSocket::open() {
if (isOpen()) {//如果已经打开就直接返回
return;
}
if (! path_.empty()) {//如果unix路径不为空就打开unix domian socket
unix_open();
} else {
local_open();//打开通用socket
}
}
Open函数又根据路径为不为空(不为空就是unix domain socket)调用相应的函数来继续打开连接:
void TSocket::unix_open(){
if (! path_.empty()) {//保证path_不为空
// Unix Domain SOcket does not need addrinfo struct, so we pass NULL
openConnection(NULL);//调用真正的打开连接函数
}
}
追到openConnection函数(错误部分代码省略)
void TSocket::openConnection(struct addrinfo *res) {
if (isOpen()) {
return;//如果已经打开了直接返回
}
if (! path_.empty()) {//根据路径是否为空创建不同的socket
socket_ = socket(PF_UNIX, SOCK_STREAM, IPPROTO_IP);//创建unix domain socket
} else {
socket_ = socket(res->ai_family, res->ai_socktype, res->ai_protocol);//创建通用的网络通信socket
}
if (sendTimeout_ > 0) {//如果发生超时设置大于0就调用设置发送超时函数设置发送超时
setSendTimeout(sendTimeout_);
}
if (recvTimeout_ > 0) {//如果接收超时设置大于0就调用设置接收超时函数设置接收超时
setRecvTimeout(recvTimeout_);
}
setLinger(lingerOn_, lingerVal_);//设置优雅断开连接或关闭连接参数
setNoDelay(noDelay_);//设置无延时
#ifdef TCP_LOW_MIN_RTO
if (getUseLowMinRto()) {//设置是否使用较低的最低TCP重传超时
int one = 1;
setsockopt(socket_, IPPROTO_TCP, TCP_LOW_MIN_RTO, &one, sizeof(one));
}
#endif
//如果超时已经存在设置连接为非阻塞
int flags = fcntl(socket_, F_GETFL, 0);//得到socket_的标识
if (connTimeout_ > 0) {//超时已经存在
if (-1 == fcntl(socket_, F_SETFL, flags | O_NONBLOCK)) {//设置为非阻塞
}
} else {
if (-1 == fcntl(socket_, F_SETFL, flags & ~O_NONBLOCK)) {//设置为阻塞
}
}
// 连接socket
int ret;
if (! path_.empty()) {//unix domain socket
#ifndef _WIN32 //window不支持
struct sockaddr_un address;
socklen_t len;
if (path_.length() > sizeof(address.sun_path)) {//path_长度不能超过最长限制
}
address.sun_family = AF_UNIX;
snprintf(address.sun_path, sizeof(address.sun_path), "%s", path_.c_str());
len = sizeof(address);
ret = connect(socket_, (struct sockaddr *) &address, len);//连接unix domain socket
#else
//window不支持unix domain socket
#endif
} else {
ret = connect(socket_, res->ai_addr, res->ai_addrlen);//连接通用的非unix domain socket
}
if (ret == 0) {//失败了就会执行后面的代码,用poll来监听写事件
goto done;//成功了就直接跳转到完成处
}
struct pollfd fds[1];//定于用于poll的描述符
std::memset(fds, 0 , sizeof(fds));//初始化为0
fds[0].fd = socket_;//描述符为socket
fds[0].events = POLLOUT;//接收写事件
ret = poll(fds, 1, connTimeout_);//调用poll,有一个超时值
if (ret > 0) {
// 确保socket已经被连接并且没有错误被设置
int val;
socklen_t lon;
lon = sizeof(int);
int ret2 = getsockopt(socket_, SOL_SOCKET, SO_ERROR, cast_sockopt(&val), &lon);//得到错误选项参数
if (val == 0) {// socket没有错误也直接到完成处了
goto done;
}
} else if (ret == 0) {// socket 超时
//相应处理代码省略
} else {
// poll()出错了,相应处理代码省略
}
done:
fcntl(socket_, F_SETFL, flags);//设置socket到原来的模式了(阻塞)
if (path_.empty()) {//如果是unix domain socket就设置缓存地址
setCachedAddress(res->ai_addr, res->ai_addrlen);
}
}
local_open函数
void TSocket::local_open(){
#ifdef _WIN32
TWinsockSingleton::create();//兼容window平台
#endif // _WIN32
if (isOpen()) {//打开了就直接返回
return;
}
if (port_ < 0 || port_ > 0xFFFF) {//验证端口是否为有效值
throw TTransportException(TTransportException::NOT_OPEN, "Specified port is invalid");
}
struct addrinfo hints, *res, *res0;
res = NULL;
res0 = NULL;
int error;
char port[sizeof("65535")];
std::memset(&hints, 0, sizeof(hints));//内存设置为0
hints.ai_family = PF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG;
sprintf(port, "%d", port_);
error = getaddrinfo(host_.c_str(), port, &hints, &res0);//根据主机名得到所有网卡地址信息
// 循环遍历所有的网卡地址信息,直到有一个成功打开
for (res = res0; res; res = res->ai_next) {
try {
openConnection(res);//调用打开函数
break;//成功就退出循环
} catch (TTransportException& ttx) {
if (res->ai_next) {//异常处理,是否还有下一个地址,有就继续
close();
} else {
close();
freeaddrinfo(res0); // 清除地址信息内存和资源
throw;//抛出异常
}
}
}
freeaddrinfo(res0);//释放地址结构内存
}
整个local_open函数就是根据主机名得到所有的网卡信息,然后依次尝试打开,直到打开一个为止就退出循环,如果所有都不成功就抛出一个异常信息。
2、read:
在实现读函数的时候需要注意区分返回错误为EAGAIN的情况,因为当超时和系统资源耗尽都会产生这个错误(没有明显的特征可以区分它们),所以Thrift在实现的时候设置一个最大的尝试次数,如果超过这个了这个次数就认为是系统资源耗尽了。下面具体看看read函数的实现,(省略一些参数检查和错误处理的代码):
uint32_t TSocket::read(uint8_t* buf, uint32_t len) {
int32_t retries = 0;//重试的次数
uint32_t eagainThresholdMicros = 0;
if (recvTimeout_) {//如果设置了接收超时时间,那么计算最大时间间隔来判断是否系统资源耗尽
eagainThresholdMicros = (recvTimeout_*1000)/ ((maxRecvRetries_>0) ? maxRecvRetries_ : 2);
}
try_again:
struct timeval begin;
if (recvTimeout_ > 0) {
gettimeofday(&begin, NULL);//得到开始时间
} else {
begin.tv_sec = begin.tv_usec = 0;//默认为0,不需要时间来判断是超时了
}
int got = recv(socket_, cast_sockopt(buf), len, 0);//从socket接收数据
int errno_copy = errno; //保存错误代码
++g_socket_syscalls;//系统调用次数统计加1
if (got < 0) {//如果读取错误
if (errno_copy == EAGAIN) {//是否为EAGAIN
if (recvTimeout_ == 0) {//如果没有设置超时时间,那么就是资源耗尽错误了!抛出异常
throw TTransportException(TTransportException::TIMED_OUT, "EAGAIN (unavailable resources)");
}
struct timeval end;
gettimeofday(&end, NULL);//得到结束时间,会改变errno,所以前面需要保存就是这个原因
uint32_t readElapsedMicros = (((end.tv_sec - begin.tv_sec) * 1000 * 1000)//计算消耗的时间
+ (((uint64_t)(end.tv_usec - begin.tv_usec))));
if (!eagainThresholdMicros || (readElapsedMicros < eagainThresholdMicros)) {
if (retries++ < maxRecvRetries_) {//重试次数还小于最大重试次数
usleep(50);//睡眠50毫秒
goto try_again;//再次尝试从socket读取数据
} else {//否则就认为是资源不足了
throw TTransportException(TTransportException::TIMED_OUT, "EAGAIN (unavailable resources)");
}
} else {//推测为超时了
throw TTransportException(TTransportException::TIMED_OUT, "EAGAIN (timed out)");
}
}
if (errno_copy == EINTR && retries++ < maxRecvRetries_) {//如果是中断并且重试次数没有超过
goto try_again;//那么重试
}
#if defined __FreeBSD__ || defined __MACH__
if (errno_copy == ECONNRESET) {//FreeBSD和MACH特殊处理错误代码
return 0;
}
#endif
#ifdef _WIN32
if(errno_copy == WSAECONNRESET) {//win32平台处理错误代码
return 0; // EOF
}
#endif
return got;
}
3、write:
写函数写的内容可能一次没有发送完毕,所以是在一个while循环中一直发送直到指定的内容全部发送完毕。
void TSocket::write(const uint8_t* buf, uint32_t len) {
uint32_t sent = 0;//记录已经发送了的字节数
while (sent < len) {//是否已经发送了指定的字节长度
uint32_t b = write_partial(buf + sent, len - sent);//调部分写入函数
if (b == 0) {//发送超时过期了
throw TTransportException(TTransportException::TIMED_OUT, "send timeout expired");
}
sent += b;//已经发送的字节数
}
}
write_partial:
uint32_t TSocket::write_partial(const uint8_t* buf, uint32_t len) {
uint32_t sent = 0;
int flags = 0;
#ifdef MSG_NOSIGNAL
//使用这个代替SIGPIPE 错误,代替我们检查返回EPIPE错误条件和关闭socket的情况
flags |= MSG_NOSIGNAL;//设置这个标志位
#endif
int b = send(socket_, const_cast_sockopt(buf + sent), len - sent, flags);//发送数据
++g_socket_syscalls;//系统调用计数加1
if (b < 0) { //错误处理
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return 0;//应该阻塞错误直接返回
}
int errno_copy = errno;//保存错误代码
if (errno_copy == EPIPE || errno_copy == ECONNRESET || errno_copy == ENOTCONN) {
close();//连接错误关闭掉socket
}
}
return b;//返回写入的字节数
}
socket的大致情况就是这样,其他函数分析略。
缓存传输类TBufferedTransport和缓冲基类TBufferBase
为了提高读写效率,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
class TBufferedTransport : public TVirtualTransport<TBufferedTransport, TBufferBase> {
public:
static const int DEFAULT_BUFFER_SIZE = 512;
/// Use default buffer sizes.
TBufferedTransport(std::shared_ptr<TTransport> transport)
: transport_(transport),
rBufSize_(DEFAULT_BUFFER_SIZE),
wBufSize_(DEFAULT_BUFFER_SIZE),
rBuf_(new uint8_t[rBufSize_]),
wBuf_(new uint8_t[wBufSize_]) {
initPointers();
}
/// Use specified buffer sizes.
TBufferedTransport(std::shared_ptr<TTransport> transport, uint32_t sz)
: transport_(transport),
rBufSize_(sz),
wBufSize_(sz),
rBuf_(new uint8_t[rBufSize_]),
wBuf_(new uint8_t[wBufSize_]) {
initPointers();
}
/// Use specified read and write buffer sizes.
TBufferedTransport(std::shared_ptr<TTransport> transport, uint32_t rsz, uint32_t wsz)
: transport_(transport),
rBufSize_(rsz),
wBufSize_(wsz),
rBuf_(new uint8_t[rBufSize_]),
wBuf_(new uint8_t[wBufSize_]) {
initPointers();
}
void open() override { transport_->open(); }
bool isOpen() const override { return transport_->isOpen(); }
bool peek() override {
if (rBase_ == rBound_) {
setReadBuffer(rBuf_.get(), transport_->read(rBuf_.get(), rBufSize_));
}
return (rBound_ > rBase_);
}
void close() override {
flush();
transport_->close();
}
uint32_t readSlow(uint8_t* buf, uint32_t len) override;
void writeSlow(const uint8_t* buf, uint32_t len) override;
void flush() override;
/**
* Returns the origin of the underlying transport
*/
const std::string getOrigin() const override { return transport_->getOrigin(); }
/**
* The following behavior is currently implemented by TBufferedTransport,
* but that may change in a future version:
* 1/ If len is at most rBufSize_, borrow will never return NULL.
* Depending on the underlying transport, it could throw an exception
* or hang forever.
* 2/ Some borrow requests may copy bytes internally. However,
* if len is at most rBufSize_/2, none of the copied bytes
* will ever have to be copied again. For optimial performance,
* stay under this limit.
*/
const uint8_t* borrowSlow(uint8_t* buf, uint32_t* len) override;
std::shared_ptr<TTransport> getUnderlyingTransport() { return transport_; }
/*
* TVirtualTransport provides a default implementation of readAll().
* We want to use the TBufferBase version instead.
*/
uint32_t readAll(uint8_t* buf, uint32_t len) { return TBufferBase::readAll(buf, len); }
protected:
void initPointers() {
setReadBuffer(rBuf_.get(), 0);
setWriteBuffer(wBuf_.get(), wBufSize_);
// Write size never changes.
}
std::shared_ptr<TTransport> transport_;
uint32_t rBufSize_;
uint32_t wBufSize_;
boost::scoped_array<uint8_t> rBuf_;
boost::scoped_array<uint8_t> wBuf_;
};
缓存传输类是从缓存基类继承而来,它对于读:实际读数据的大小比实际请求的大很多,多余的数据将为将来超过本地缓存的数据服务;对于写:数据在它被发送出去以前将被先写入内存缓存。
缓存的大小默认是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。依赖底层传输,它应该抛出一个异常或者永远不会挂掉;
一些借用请求可能内部字节拷贝,如果借用的长度最多是缓存区的一半,那么不去内部拷贝。为了优化性能保存这个限制。
分帧传输类TFramedTransport
帧传输类就是按照一帧的固定大小来传输数据,所有的写操作首先都是在内存中完成的直到调用了flush操作,然后传输节点在flush操作之后将所有数据根据数据的有效载荷写入数据的长度的二进制块发送出去,允许在接收的另一端按照固定的长度来读取。
帧传输类同样还是从缓存基类TBufferBase继承而来,实现的接口当然也基本相同,只是实现的方式不同而已,下面就来看看具体的实现过程和原理。
这个类所采用的默认缓存长度是512,下面还是重点分析慢读、读帧等操作的实现过程:
(1)慢读实现如下:
uint32_t TFramedTransport::readSlow(uint8_t* buf, uint32_t len) {
uint32_t want = len;//想要读取的长度
uint32_t have = rBound_ - rBase_;//内存缓存中已经有的数据的长度
assert(have < want);//如果以后数据长度满足需要读的长度就不需要采用慢读
// 如果我们有一些数据在缓存,拷贝出来并且返回它。
// 我们没有试图读取更多的数据而是不得不返回它,因为我们不能保证在下面的
// 传输层实际上有更多的数据,因此应该尝试阻塞式读它。
if (have > 0) {
memcpy(buf, rBase_, have);//拷贝出缓存中已有的数据
setReadBuffer(rBuf_.get(), 0);//重新设置缓存基地址
return have;//返回
}
// 读取另一帧。
if (!readFrame()) {
// EOF. No frame available.
return 0;
}
// 处理我们已有的数据
uint32_t give = std::min(want, static_cast<uint32_t>(rBound_ - rBase_));//已有数据想要读取长度取短的
memcpy(buf, rBase_, give);//拷贝
rBase_ += give;//调整缓存基地址
want -= give;//计算还有多少想要的数据没有得到
return (len - want);//返回实际读取长度
}
缓存中没有数据的时候就会调用读取帧的函数readFrame,这个函数实现如下:
bool TFramedTransport::readFrame() {
//首先读下一帧数据的长度
int32_t sz;//存放长度的变量
uint32_t size_bytes_read = 0;//读取长度数据的字节数
while (size_bytes_read < sizeof(sz)) {//表示长度的数据小于存放长度数据的字节数
uint8_t* szp = reinterpret_cast<uint8_t*>(&sz) + size_bytes_read;//长度变量转换为指针
uint32_t bytes_read = transport_->read(szp, sizeof(sz) - size_bytes_read);//读取
if (bytes_read == 0) {//如果返回为0表示没有数据了
if (size_bytes_read == 0) {//没有任何数据读到,返回false
return false;
} else {
// 部分的帧头部,抛出异常。
throw TTransportException(TTransportException::END_OF_FILE,
"No more data to read after "
"partial frame header.");
}
}
size_bytes_read += bytes_read;//以读取的长度
}
sz = ntohl(sz);//长整数的网络字节序转换为主机字节序
if (sz < 0) {//帧的长度不能是负数涩,抛出异常
throw TTransportException("Frame size has negative value");
}
// 读取有效数据负载,重新设置缓存标记。
if (sz > static_cast<int32_t>(rBufSize_)) {
rBuf_.reset(new uint8_t[sz]);//接收基地址
rBufSize_ = sz;//缓存大小
}
transport_->readAll(rBuf_.get(), sz);//调用readAll读取sz长度的数据
setReadBuffer(rBuf_.get(), sz);//设置读缓存基地址
return true;
}
从上面实现代码看出,在按帧读取的过程中,首先需要读取这一帧的头部信息,而这个头部信息就是这一帧的长度,后面就根据头部信息中给定的长度来读取数据部分,读出来的数据放入缓存中。读取头部信息时注意处理异常的情况,还有就是读出来的数据需要经过网络字节序到主机字节序的转换。下面继续看慢写函数和flush刷新函数的实现过程,慢写函数实现如下(快读和快写基类TBufferBase的实现已经满足要求了,所以不需要再去单独实现了):
void TFramedTransport::writeSlow(const uint8_t* buf, uint32_t len) {
// 直到有足够的双缓冲大小
uint32_t have = wBase_ - wBuf_.get();//缓存空间已经有多少数据
uint32_t new_size = wBufSize_;
if (len + have < have /* overflow */ || len + have > 0x7fffffff) {//如果长度溢出或大于2GB了
throw TTransportException(TTransportException::BAD_ARGS,
"Attempted to write over 2 GB to TFramedTransport.");//抛出异常
}
while (new_size < len + have) {//缓存空间的长度小于已有数据的长度和需要写入数据长度的和
new_size = new_size > 0 ? new_size * 2 : 1;如果缓存空间长度是大于0的话就扩容一倍的空间
}
uint8_t* new_buf = new uint8_t[new_size];// 分配新空间
memcpy(new_buf, wBuf_.get(), have);// 拷贝已有的数据到新空间.
wBuf_.reset(new_buf);// 缓存地址重新设置
wBufSize_ = new_size;// 缓存新长度
wBase_ = wBuf_.get() + have;//新的开始写入地址
wBound_ = wBuf_.get() + wBufSize_;//写入界限
memcpy(wBase_, buf, len);//拷贝数据到新缓存地址
wBase_ += len;//更新缓存基地址
}
上面代码就是实现把从上层传输的数据写入缓存中以供下层发送使用,这段代码需要注意的是while循环,这个while循环保证有足够的缓存来存放写入的数据到缓存中,每次增长的长度是上次的一倍;还需要注意的是,分配了新的空间需要把原来还没有真正写入的数据拷贝到新缓存中来,不然就会造成内容丢失;最后就是更新缓存的基地址和长度等描述缓存的信息。继续看flush函数的实现代码:
void TFramedTransport::flush() {
int32_t sz_hbo, sz_nbo;
assert(wBufSize_ > sizeof(sz_nbo));//断言缓存长度应该大于个字节sizeof(int32_t)
sz_hbo = wBase_ - (wBuf_.get() + sizeof(sz_nbo));// 滑动到第一帧数据的开始位置。
sz_nbo = (int32_t)htonl((uint32_t)(sz_hbo));//主机字节序转换为网络字节序
memcpy(wBuf_.get(), (uint8_t*)&sz_nbo, sizeof(sz_nbo));//头部长度拷贝写缓存
if (sz_hbo > 0) {//保证缓存有需要写入的数据
//如果底层传输写抛出了异常注意确保我们处于安全的状态
//(例如内部缓冲区清理),重置我们写入前的状态(因为底层没有传输成功)
wBase_ = wBuf_.get() + sizeof(sz_nbo);//得到
// 写入长度和帧
transport_->write(wBuf_.get(), sizeof(sz_nbo)+sz_hbo);
}
// 刷新底层传输.
transport_->flush();
}
刷新函数就是把缓存中的数据真正的发送出去,但是在写入到底层时,底层传输可能不会真正成功(如网络突然断了),这个时候底层会抛出异常,那么我们需要捕获异常,以便重新处理这些数据,只有数据真正写入成功的时候我们才计算我们写如数据的长度。所以还有写结束和读结束函数writeEnd、readEnd,它们都只有简单的一句代码就是计算真正完成读写数据的长度。
整个按帧传输的类的功能介绍完毕了,主要需要注意的就是缓存的操作,保证数据不丢失。将来实现考虑分配内存使用c语言的malloc类函数,而不是使用new操作,这样也能提高不少的效率。
3732

被折叠的 条评论
为什么被折叠?



