分类目录归档:rpc

Protocol Buffers

RPC(Remote Procedure Call,远程过程调用)框架是分布式服务的基石,实现RPC框架需要考虑方方面面。其对业务隐藏了底层通信过程(TCP/UDP、打包/解包、序列化/反序列化),使上层专注于功能实现;框架层面,提供各类可选架构(多进程/多线程/协程);应对设备故障(高负载/死机)、网络故障(拥塞/网络分化),提供相应容灾措施。

 

RPC节点间为了协同工作、实现信息交换,需要协商一定的规则和约定,例如字节序、压缩或加密算法、各字段类型。通信协议的应用随处可见,例如我们对可选信息或字段经常使用TLV进行编码,HTTP、FTP等协议基于可读文本的 "Field: Value" 格式,各种系统也经常使用json、XML格式完成相互间通信。

 

不同的通信协议适用于不同的应用场景,比如内部系统的交互我们选择json,一来可读性较好,二来各种语言都提供了解析json的库、方便编码。Google Protocol Buffers是生成环境中常用的通信协议,除了可以设定Client/Server间通信格式,Protocol Buffers还对数据进行压缩,节省传输流量、加快传输速度。下面我们来了解Google Protocol Buffers。

 

Protocol Buffers

我们看如何使用Protocol Buffers(以下简称PB),首先在.proto文件中定义数据格式,下面以Person.proto为例:

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}
message类型内,可以定义int、string、bool、string等类型的字段,也可以嵌套定义messages类型。每个字段可以是required、optional或repeated类型,分别表示必须每次通信必须填充该字段、可选或可重复。每个message类型内的每个字段被赋值唯一的数字值,PB以二进制格式进行数据传输,数字值在二进制中作为该字段的标识。关于PB数据格式的更多内容可参考Protocol Buffers Language Guide

 

完成数据定义后,接下来可以使用protoc工具解析Person.proto文件,生成Person类:

protoc --cpp_out=/home/bangerlee/PB ./Person.proto

执行以上命令后,可以看到 /home/bangerlee/PB 目录下生成了两个文件:

person.pb.cc  person.pb.h

其中定义了操作(get/set)Person类各个字段的函数。

 

有了接口,我们就可以在代码中这样使用Person类,写入操作如下:

Person person;
person.set_name("bangerlee");
person.set_id(1234);
person.set_email("bangerlee@gmail.com");
fstream output("myfile", ios::out | ios::binary);
person.SerializeToOstream(&output);
读取操作如下:
fstream input("myfile", ios::in | ios::binary);
Person person;
person.ParseFromIstream(&input);
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;

以上我们初步了解了如何使用PB,PB运用了一些编码规则,使得需要传输的数据(二进制格式)更小,下面我们就来了解PB如何对不同数据类型的编码规则。

 

编码(Encoding)

对整形int、字符串类型string等,PB有不同的编码方式。对整型int,PB使用了Varints编码方式,Varints编码的优势是使用了更少的bytes来表示很小的int类型值。

 

Varints编码方式中,每个byte的最高位bit有特殊含义,如果为1,表示后续的byte也是这个数字的一部分;如果为0,则表示结束。剩余的7个bit用于表示数据。数字300用Varints编码方式表示为:

1010 1100 0000 0010

由Varints编码规则,去掉第一个byte的最高位1,去掉第二个byte的最高位0,则有:

1010 1100 0000 0010
→ 010 1100  000 0010

Varints字节序使用little-endian,以上数字用big-endian并转换成10进制有:

000 0010  010 1100
→  000 0010 ++ 010 1100
→  100101100
→  256 + 32 + 8 + 4 = 300

以上了解了Varints对int整型的编码方式,我们再来看PB如何编码更多数据类型:

PB编码中,数据以key-value的形式表示,第一个byte即为key。以上表格中不同数据类型对应指定type值,假设message中各字段的数字标识为tag,则key、type和tag有以下对应关系:

key = tag << 3 | type

即key的最后3个bit用于存储type,有了这层关系,我们试着演算PB中对int和string的编码。

 

假设我们截获到以下PB数据:

08 96 01

这段数据具体表示什么?我们用以上对应关系演算一下,首先该数据key是08,二进制表示即:

0000 1000

最后3个bit表示type,即type为0(Varint格式数据),左移3位得到tag值为1。有了这些信息,我们可以知道这个数据应该是这样定义的:

message Test1 {
  xxx int32 a = 1;
}

继续地,我们用Varint格式来解析 96 01,有以下演算过程:

96 01 = 1001 0110  0000 0001
       → 000 0001  ++  001 0110 (丢弃最高位的bit并转为big-endian)
       → 10010110
       → 2 + 4 + 16 + 128 = 150

因此我们可以知道这段数据表示150这个数。

 

又假设我们截获到以下一段PB编码:

12 07 74 65 73 74 69 6e 67

同样套用以上关系,key是12,二进制表示即:

0001 0010

最后3个bit表示type,即type为2(Length-delimited),左移3位得到tag值为2。有了这些信息,我们知道这个数据可能是这样定义的:

message Test2 {
  xxx string b = 2;
}

数据类型具体是string、bytes或其他,这并不影响我们解析这段数据,对于Length-delimited格式数据,第2个byte表示数据长度(Len),对应以上编码即Len为7,这实质是TLV编码格式。

后续的7个bytes表示有效的传输数据,为UTF-8编码下的"testing"字符串。

 

小结

以上介绍了通信协议 - Google Protocol Buffers,了解了其基本使用方法和编码方式。PB支持前向兼容,可以在不修改Client/Server程序的情况下修改其中一端的数据格式,在各种RPC框架中经常可以看到它的身影。

Reference: Protocol Buffers Developer Guide

Protocol Buffers Encoding

Apache Thrift

Thrift源于大名鼎鼎的facebook之手,在2007年facebook提交Apache基金会将Thrift作为一个开源项目,对于当时的facebook来说创造thrift是为了解决facebook系统中各系统间大数据量的传 输通信以及系统之间语言环境不同需要跨平台的特性。所以thrift可以支持多种程序语言,例如:  C++, C#, Cocoa, Erlang, Haskell, Java, Ocami, Perl, PHP, Python, Ruby, Smalltalk. 在多种不同的语言之间通信thrift可以作为二进制的高性能的通讯中间件,支持数据(对象)序列化和多种类型的RPC服务。Thrift适用于程序对程 序静态的数据交换,需要先确定好他的数据结构,他是完全静态化的,当数据结构发生变化时,必须重新编辑IDL文件,代码生成,再编译载入的流程,跟其他IDL工具相比较可以视为是Thrift的弱项,Thrift适用于搭建大型数据交换及存储的通用工具,对于大型系统中的内部数据传输相对于JSON和xml无论在性能、传输大小上有明显的优势。

Thrift是IDL(interface definition language)描述性语言的一个具体实现,关于IDL的话题我们可以追溯到CORBA盛行1999-2001年(Common Object Request Broker Architecture/公用对象请求代理体系结构),在 IDL 中我们似乎不会忘记到这几个关键字:module、interface、string、long 和 int,我还记得IDL利用module来创建名称空间,并且准确地映射为 Java 的 package,这些特性几乎和现在thrift的特性完全相同,所以thrift的设计思想和理念绝不是什么从火星来的new idea,看看在那个CORBA盛行的年代人们提出的概念,如图所示CORBA 请求的各个部分,回头我们再与thrift进行对比一下:

 

Thrift 基础架构
Thrift是一个服务端和客户端的架构体系,从我个人的感官上来看Thrift是一个类似XML-RPC+Java-to- IDL+Serialization Tools=Thrift 的东东,Thrift 具有自己内部定义的传输协议规范(TProtocol)和传输数据标准(TTransports),通过IDL脚本对传输数据的数据结构(struct) 和传输数据的业务逻辑(service)根据不同的运行环境快速的构建相应的代码,并且通过自己内部的序列化机制对传输的数据进行简化和压缩提高高并发、 大型系统中数据交互的成本,下图描绘了Thrift的整体架构,分为6个部分:1.你的业务逻辑实现(You Code) 2.客户端和服务端对应的Service 3.执行读写操作的计算结果4.TProtocol 5.TTransports  6.底层I/O通信

 

 

图 中前面3个部分是1.你通过Thrift脚本文件生成的代码,2.图中的褐色框部分是你根据生成代码构建的客户端和处理器的代码,3.图中红色的部分是2 端产生的计算结果。从TProtocol下面3个部分是Thrift的传输体系和传输协议以及底层I/O通信,Thrift并且提供 堵塞、非阻塞,单线程、多线程的模式运行在服务器上,还可以配合服务器/容器一起运行,可以和现有JEE服务器/Web容器无缝的结合。

数据类型
* Base Types:基本类型
* Struct:结构体类型
* Container:容器类型,即List、Set、Map
* Exception:异常类型
* Service: 定义对象的接口,和一系列方法

协议
Thrift可以让你选择客户端与服务端之间传输通信协议的类别,在传输协议上总体上划分为文本(text)和二进制(binary)传输协议, 为节约带宽,提供传输效率,一般情况下使用二进制类型的传输协议为多数,但有时会还是会使用基于文本类型的协议,这需要根据项目/产品中的实际需求:
* TBinaryProtocol – 二进制编码格式进行数据传输。
* TCompactProtocol – 这种协议非常有效的,使用Variable-Length Quantity (VLQ) 编码对数据进行压缩。
* TJSONProtocol – 使用JSON的数据编码协议进行数据传输。
* TSimpleJSONProtocol – 这种节约只提供JSON只写的协议,适用于通过脚本语言解析
* TDebugProtocol – 在开发的过程中帮助开发人员调试用的,以文本的形式展现方便阅读。

传输层
* TSocket- 使用堵塞式I/O进行传输,也是最常见的模式。
* TFramedTransport- 使用非阻塞方式,按块的大小,进行传输,类似于Java中的NIO。
* TFileTransport- 顾名思义按照文件的方式进程传输,虽然这种方式不提供Java的实现,但是实现起来非常简单。
* TMemoryTransport- 使用内存I/O,就好比Java中的ByteArrayOutputStream实现。
* TZlibTransport- 使用执行zlib压缩,不提供Java的实现。

服务端类型
* TSimpleServer -  单线程服务器端使用标准的堵塞式I/O。
* TThreadPoolServer -  多线程服务器端使用标准的堵塞式I/O。
* TNonblockingServer – 多线程服务器端使用非堵塞式I/O,并且实现了Java中的NIO通道。