Thrift之TProtocol类体系原理及源码详细解析之紧凑协议类TCompactProtocolT(TCompactProtocol)

本文主要是介绍Thrift之TProtocol类体系原理及源码详细解析之紧凑协议类TCompactProtocolT(TCompactProtocol),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

http://blog.csdn.net/wanweiaiaqiang/article/details/7654475



这个协议类采用了zigzag 编码,这种编码是基于Variable-length quantity编码提出来的,因为Variable-length quantity编码对于负数的编码都需要很长的字节数,而zigzag 编码对于绝对值小的数字,无论正负都可以采用较少的字节来表示,充分利用了 Varint技术。所以这个协议类采用zigzag 编码可以节省传输空间,使数据的传输效率更高。至于zigzag具体的编码实现方式可以网上查查,其实就是把从低位到最后一个还存在1(二进制)的最高位表示出来就可以了。这个协议类对外提供的方法和上面介绍的二进制协议相同,这样可以很方便使用者从一种协议改变到另一种协议。

下面我同样结合scribe提供的Log方法来分析这个协议类的功能,不过不会像上面二进制协议在把整个过程分析了,我只会分析与协议相关的部分了,分析一些比较难懂的一些函数功能,分析的思路还是按照函数调用过程来分析。

首先还是分析writeMessageBegin函数,下面是这个函数的实现代码:

template <class Transport_> uint32_t TCompactProtocolT<Transport_>::writeMessageBegin(

    const std::string& name, const TMessageType messageType, const int32_t seqid) {

  uint32_t wsize = 0;

  wsize += writeByte(PROTOCOL_ID);//写入这个协议的产品ID号:为0x82

  wsize += writeByte((VERSION_N & VERSION_MASK) | (((int32_t)messageType << TYPE_SHIFT_AMOUNT) & TYPE_MASK));//写入此协议的版本号和消息类型:前3位是消息类型,后面5位是协议版本号

  wsize += writeVarint32(seqid);//写入请求序列号

  wsize += writeString(name);//写入消息名称(也就是函数调用名称)

  return wsize;//返回写入的大小,多少字节

}

因为这些协议类都是模板类,所以每一个函数也就是模板函数了。函数具体的功能代码里有详细注释,其中的writeByte函数就是写入一个字节到服务器。这里与二进制协议不同的是这里写入请求序列号(也就是对于所有的整型数)都调用的writeVarint32函数,这个函数就是采用zigzag编码写入整型数到服务器,代码如下:

template <class Transport_> uint32_t TCompactProtocolT<Transport_>::writeVarint32(uint32_t n) {

  uint8_t buf[5];//对于一个整数,zigzag编码最大采用5个字节保存

  uint32_t wsize = 0;

  while (true) {

    if ((n & ~0x7F) == 0) {//判断除了最低7位是否还有其他高位为1(二进制)

      buf[wsize++] = (int8_t)n;//没有了代表着就是最后一个字节

      break;//退出循环

    } else {

      buf[wsize++] = (int8_t)((n & 0x7F) | 0x80);//取最低7位加上第8位(为1代表后续还有字节属于这个整数,为0代表这是这个整数的最后一个字节了。

      n >>= 7;//移走已经编码的位数

    }

  }

  trans_->write(buf, wsize);//写入编码的字节数

  return wsize;//返回写入的字节数

}

这个函数的功能就是对整数进行Variable-length quantity编码后写入,如果为负数需要处理。如果不处理那么每一个负数都需要5个字节来编码,因为最高位表示符号位,而负数的符号位用1表示(也就是说负数的最高位永远为1)。处理的方式也很简单(就是zigzag编码),就是把最高位(符号位)移动到最低位,最低位到次高位一次向高位移动一位,代码如下(就一句就实现了):

template <class Transport_>

uint32_t TCompactProtocolT<Transport_>::i32ToZigzag(const int32_t n) {

  return (n << 1) ^ (n >> 31);

}

上面写入整数和处理负数都是针对的32位的,当然也有64位的相应函数,实现方式相同。我们在回到writeMessageBegin函数,里面还有一个writeString函数用来写入一个字符串的,与二进制不同的是写入字符串长度也是采用了可变长度编码的方式写入,然后写入字符串的具体数据,它是调用另一个函数writeBinary写入,writeBinary实现代码如下:

template <class Transport_>

uint32_t TCompactProtocolT<Transport_>::writeBinary(const std::string& str) {

  uint32_t ssize = str.size();

  uint32_t wsize = writeVarint32(ssize) + ssize;//写入字符串的长度并计算写入的长度(包括字符串的长度)

  trans_->write((uint8_t*)str.data(), ssize);//写入字符串的数据

  return wsize;

}

写消息函数分析完毕以后我们在来看看对应的读消息函数readMessageBegin,看这个函数必须和写入消息的函数对应起来看,不然就不能理解它读取和处理的流程代码,具体实现如下代码:

template <class Transport_> uint32_t TCompactProtocolT<Transport_>::readMessageBegin(

    std::string& name, TMessageType& messageType, int32_t& seqid) {

  uint32_t rsize = 0;

  int8_t protocolId;

  int8_t versionAndType;

  int8_t version;

  rsize += readByte(protocolId);//读取协议产品ID

  if (protocolId != PROTOCOL_ID) {//判断是不是这个协议的产品ID号,不是就抛出异常

    throw TProtocolException(TProtocolException::BAD_VERSION, "Bad protocol identifier");

  }

  rsize += readByte(versionAndType);//读取此协议的版本号和消息类型

  version = (int8_t)(versionAndType & VERSION_MASK);//取出协议版本号

  if (version != VERSION_N) {//判断是不是对应的协议版本号,不是抛出异常

    throw TProtocolException(TProtocolException::BAD_VERSION, "Bad protocol version");

  }

  messageType = (TMessageType)((versionAndType >> TYPE_SHIFT_AMOUNT) & 0x03);//取出消息类型

  rsize += readVarint32(seqid);//读取请求序列号

  rsize += readString(name);//读取消息名称(函数名称)

  return rsize;//返回读取的长度(字节)

}

通过对照写入消息的函数就很容易理解,因为你写入什么我就读什么并且判断是不是相同协议写入的,具体分析可以看上面的代码和详细的注释。而且还有一点就是具体的写入数据类型的函数也是采用对应类型的读函数,例如读可变长整型写入就是采用可变长读函数readVarint32,写字符串对应读字符串函数readString,对照相应的写入函数来看这些读数据函数就非常好理解了,就不具体分析这些读函数了。

下面在分析几个复合数据类型的写入函数,因为这些写入函数存在一定技巧不容易(或者说不那么直观吧)理解清楚。首先看看struct类型的数据写入的过程,它分为写入开始、中间处理和写入结束。下面是开始写入struct的代码:

template <class Transport_>

uint32_t TCompactProtocolT<Transport_>::writeStructBegin(const char* name) {

  (void) name;

  lastField_.push(lastFieldId_);//把最后写入的字段ID压入堆栈

  lastFieldId_ = 0;//重新设置为0

  return 0;

}

这开始写入的函数没有做什么具体的工作,只是把最后写入的字段ID压入堆栈,这样做的目的是处理那种struct嵌套的数据结构类型。

Struct里面的是一个一个的字段,所以根据struct的字段个数分别调用字段写入函数依次写入,字段写入函数定义如下:

template <class Transport_> int32_t TCompactProtocolT<Transport_>::writeFieldBeginInternal(

    const char* name, const TType fieldType, const int16_t fieldId, int8_t typeOverride) {

  (void) name;//为了防止编译器产生警告信息

  uint32_t wsize = 0;

  // 如果存在对于对应的类型就转换为对应的

  int8_t typeToWrite = (typeOverride == -1 ? getCompactType(fieldType) : typeOverride);

  // 检查字段ID是否使用了增量编码

  if (fieldId > lastFieldId_ && fieldId - lastFieldId_ <= 15) {//如果使用了增量编码并增量且小于等于15

    wsize += writeByte((fieldId - lastFieldId_) << 4 | typeToWrite);//字段ID和数据类型一起写入

  } else {//否则单独写入

    wsize += writeByte(typeToWrite);//写入数据类型

    wsize += writeI16(fieldId);//写入字段ID

  }

  lastFieldId_ = fieldId;//保存写入字段ID为最后一个写入的ID

  return wsize;//返回写入的长度

}

当结构体里面的每一个字段都写入以后还需要调用writeStructEnd函数来处理结束一个struct的写入,主要处理是字段ID的相关内容,实现代码如下:

template <class Transport_> uint32_t TCompactProtocolT<Transport_>::writeStructEnd() {

  lastFieldId_ = lastField_.top();//取得最后一次压入堆栈的字段ID

  lastField_.pop();//弹出以取得的字段ID

  return 0;

}

同样的结构体也有对应的读取函数,具体实现就不在具体分析了!下面继续分析一些特殊的处理代码,首先看看负数在进行zigzag编码前怎样处理,对于32位和64位都是一句代码就搞定,如下代码:

return (n >> 1) ^ -(n & 1);

这句代码的作用就是把最高位的符号位移动到最低位,然后最低位到次高位依次向高位移动一位,这样就避免了所有负数都需要最长的字节来编码。在看看读可变长编码写入整型数的函数,32位和64位都是相同的实现,因为32位也是调用64位的函数实现的,实现代码如下:

template <class Transport_> uint32_t TCompactProtocolT<Transport_>::readVarint64(int64_t& i64) {

  uint32_t rsize = 0;

  uint64_t val = 0;

  int shift = 0;

  uint8_t buf[10];  // 64 位采用zigzag编码最长可能是10字节

  uint32_t buf_size = sizeof(buf);

  const uint8_t* borrowed = trans_->borrow(buf, &buf_size);//并不是所有transport都支持 

  if (borrowed != NULL) {// 快路径,要读的数据已经在缓存中

    while (true) {

      uint8_t byte = borrowed[rsize];

      rsize++;

      val |= (uint64_t)(byte & 0x7f) << shift;//取得对应编码数据的7

      shift += 7;//7

      if (!(byte & 0x80)) {//是否还有属于这个数的编码字节,字节的最高位表示:0表示没有了

        i64 = val;//读取解码后的真正有效值

        trans_->consume(rsize);//消耗了多少字节,即表示这个编码用了多少字节

        return rsize;

      }

      // 检查编码数据是否超过了最长限制,是就抛出一个无效的异常

      if (UNLIKELY(rsize == sizeof(buf))) {

        throw TProtocolException(TProtocolException::INVALID_DATA, "Variable-length int over 10 bytes.");

      }

    }

  } 

  else {// 慢路径,要读的数据还没有存在缓存中

    while (true) {

      uint8_t byte;

      rsize += trans_->readAll(&byte, 1);//读取一个字节

      val |= (uint64_t)(byte & 0x7f) << shift;//取得7位的编码数据

      shift += 7;

      if (!(byte & 0x80)) {

        i64 = val;

        return rsize;

      } 

      if (UNLIKELY(rsize >= sizeof(buf))) {//同样检查数据的有效性:最大字节长度不超过10个字节

        throw TProtocolException(TProtocolException::INVALID_DATA, "Variable-length int over 10 bytes.");

      }

    }

  }

}

由于采用了可变长度编码的原因,所以不知道一次性应该读取多少个字节是一个完整的数据。为了读取效率所以一次性直接读取最长可能的字节数量,也就是10字节,因为64位最长的可变长编码就是10字节长,然后根据实际消耗的字节数从读取元跳过已经消耗的字节数。不过底层的传输层,有些协议可能不支持这种预读取方式,所以就只有一个字节一个字节的读取。

这个协议最大的特点就是采用了可变长度编码,并且采用zigzag编码处理负数总是需要采用最长的编码字节的问题,所以相对于比较二进制而言效率提高了不少。



这篇关于Thrift之TProtocol类体系原理及源码详细解析之紧凑协议类TCompactProtocolT(TCompactProtocol)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/810969

相关文章

Java集合中的List超详细讲解

《Java集合中的List超详细讲解》本文详细介绍了Java集合框架中的List接口,包括其在集合中的位置、继承体系、常用操作和代码示例,以及不同实现类(如ArrayList、LinkedList和V... 目录一,List的继承体系二,List的常用操作及代码示例1,创建List实例2,增加元素3,访问元

SpringBoot整合easy-es的详细过程

《SpringBoot整合easy-es的详细过程》本文介绍了EasyES,一个基于Elasticsearch的ORM框架,旨在简化开发流程并提高效率,EasyES支持SpringBoot框架,并提供... 目录一、easy-es简介二、实现基于Spring Boot框架的应用程序代码1.添加相关依赖2.添

Qt 中集成mqtt协议的使用方法

《Qt中集成mqtt协议的使用方法》文章介绍了如何在工程中引入qmqtt库,并通过声明一个单例类来暴露订阅到的主题数据,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧... 目录一,引入qmqtt 库二,使用一,引入qmqtt 库我是将整个头文件/源文件都添加到了工程中进行编译,这样 跨平台

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2

Goland debug失效详细解决步骤(合集)

《Golanddebug失效详细解决步骤(合集)》今天用Goland开发时,打断点,以debug方式运行,发现程序并没有断住,程序跳过了断点,直接运行结束,网上搜寻了大量文章,最后得以解决,特此在这... 目录Bug:Goland debug失效详细解决步骤【合集】情况一:Go或Goland架构不对情况二:

Python itertools中accumulate函数用法及使用运用详细讲解

《Pythonitertools中accumulate函数用法及使用运用详细讲解》:本文主要介绍Python的itertools库中的accumulate函数,该函数可以计算累积和或通过指定函数... 目录1.1前言:1.2定义:1.3衍生用法:1.3Leetcode的实际运用:总结 1.1前言:本文将详

Deepseek R1模型本地化部署+API接口调用详细教程(释放AI生产力)

《DeepseekR1模型本地化部署+API接口调用详细教程(释放AI生产力)》本文介绍了本地部署DeepSeekR1模型和通过API调用将其集成到VSCode中的过程,作者详细步骤展示了如何下载和... 目录前言一、deepseek R1模型与chatGPT o1系列模型对比二、本地部署步骤1.安装oll

Spring Boot整合log4j2日志配置的详细教程

《SpringBoot整合log4j2日志配置的详细教程》:本文主要介绍SpringBoot项目中整合Log4j2日志框架的步骤和配置,包括常用日志框架的比较、配置参数介绍、Log4j2配置详解... 目录前言一、常用日志框架二、配置参数介绍1. 日志级别2. 输出形式3. 日志格式3.1 PatternL

Springboot 中使用Sentinel的详细步骤

《Springboot中使用Sentinel的详细步骤》文章介绍了如何在SpringBoot中使用Sentinel进行限流和熔断降级,首先添加依赖,配置Sentinel控制台地址,定义受保护的资源,... 目录步骤 1: 添加 Sentinel 依赖步骤 2: 配置 Sentinel步骤 3: 定义受保护的