技术: Thrift

经典的跨语言编程框架Thrift, 有着protobuffer无法匹敌的, 庞大…

有同行老早就在实践这个库了.毫无疑问, 本文是继 protobuffer 之后, 又要花大力气讲解的一个(FB的)库(而且绝对是值得你花一个晚上自习看看的库). 不过可惜的是, 实际开发中它并不如pb应用广泛, 可能很大一个原因是因为太重了, 而且代码还依赖了外部库boost, libevent等, 相比之下, RESTful的做法就会简单很多.(我不得不说, 花了很多时间在踩开源的坑…最终结论, 于开源来说, Thrift可能不是最好的选择)

还是强调一些最基本的概念吧:

框架的协议是指传输时候的编码方式(而不是网络协议); 框架的传输方式包括文件, 缓存buffer, 压缩二进制或者文本, 以及其他需要服务器端同步阻塞或者非阻塞操作,多线程或者异步IO支持的方式(因为可能传输方式使用了一些同步或者异步的IO), 传输方式也会(包括)指定传输的网络协议. 服务器类型是指服务器的工作模式, 是同步阻塞处理请求还是非阻塞操作(轮询检查IO-fd), 是否支持多线程模型, 还是异步多路IO.

本篇也作为 跨语言编程实践 的最后一篇, 后期可能会有更新(主要是补充 源码分析 部分).

引子

From Wikipedia, the free encyclopedia:

Thrift is an interface definition language and binary communication protocol that is used to define and create services for numerous languages.

wiki上的一句话概括了: 用于使用接口定义语言和二进制通信协议定义并创建跨语言服务的框架.(通信格式是二进制, 和pb一样)

实际上, 谈thrift更多的也是从服务架构, 传输, RPC上说它的作用(可以简单把RPC理解成非同一进程调用, 非异步网络情况下的阻塞调用), 简单的了解可以说它如何构建服务, 深入的可以分析一下它的rpc调用机制. 总之, 你不仅可以了解它的序列化机制, c-s服务模型, 传输体系, 还能学习它内部代码生成引擎. (看样子是应该不支持协程的)

(有时间可以学习一下它的源码)

正文

安装

下面记录在Ubuntu64上进行编译安装的过程, 我不得不说, 编译安装是个很坑爹的过程, 所有我接触过的开源产品里面, thrift做的支持最差了.
安装绝对没有那么顺利, 上述任何过程出错, 请根据相关提示拍错, 下面是我遇到的:

首先确保你的环境里面安装了 libssl-dev, libtool, flex, bison(即yacc), pkg-config, boost和libevent(a minimal RPC framework), 之后再去安装thrift. 下面是一个详细记录.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 先安装我缺失的
# automake
$ sudo apt-get install automake
# yacc: command not found
$ sudo apt-get install -y byacc

# flex: command not found
$ sudo apt-get install -y flex

# 安装libevent-dev
$ sudo apt-get install libevent-dev

# 安装ssl
$ sudo apt-get install libssl-dev

# 下载
$ wget http://mirror.bit.edu.cn/apache/thrift/0.10.0/thrift-0.10.0.tar.gz -P Desktop/apps
$ tar xzvf thrift-0.10.0.tar.gz
$ cd thrift-0.10.0

# 配置
$ ./configure

注意一下日志, 如果你还要编译其他语言的, 请注意安装相应的库, 比如我要再去装Go语言支持, 那么我就要在日志中找Go语言相关的依赖库.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
thrift 0.10.0

Building Plugin Support ...... : yes
Building C++ Library ......... : yes
Building C (GLib) Library .... : yes
Building Java Library ........ : yes
Building C# Library .......... : no
Building Python Library ...... : yes
Building Ruby Library ........ : no
Building Haxe Library ........ : no
Building Haskell Library ..... : no
Building Perl Library ........ : no
Building PHP Library ......... : no
Building Dart Library ........ : no
Building Erlang Library ...... : no
Building Go Library .......... : yes
Building D Library ........... : no
Building NodeJS Library ...... : yes
Building Lua Library ......... : no

C++ Library:
Build TZlibTransport ...... : yes
Build TNonblockingServer .. : yes
Build TQTcpServer (Qt4) .... : yes
Build TQTcpServer (Qt5) .... : no

Java Library:
Using javac ............... : javac
Using java ................ : java
Using ant ................. : /usr/bin/ant

Python Library:
Using Python .............. : /usr/bin/python
Using Python3 ............. : /usr/bin/python3

Go Library:
Using Go................... : /usr/lib/go-1.7/bin/go
Using Go version........... : go version go1.7.4 linux/amd64

NodeJS Library:
Using NodeJS .............. : /home/merlin/Software/node-v6.11.2-linux-x64/bin/node
Using NodeJS version....... : v6.11.2

If something is missing that you think should be present,
please skim the output of configure to find the missing
component. Details are present in config.log.

但是注意:

你支持的库越多, 后期编译出问题的可能性越大.

最好禁用一些选项, 和自定义一些选项.

1
./configure --libdir=/usr/local/lib --without-java --without-python

编译安装

1
2
3
4
5
6
7
$ make -j8 && make check

# 跨语言测试
$ sh test/test.sh

# 编译
$ sudo make install

查看一下目录, 发现勉强还是安装成功了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ls /usr/local/lib | grep libthrift
libthrift-0.10.0.jar
libthrift-0.10.0-javadoc.jar
libthrift-0.10.0.so
libthrift.a
libthriftc.a
libthrift_c_glib.a
libthrift_c_glib.la
libthrift_c_glib.so
libthrift_c_glib.so.0
libthrift_c_glib.so.0.0.0
libthriftc.la
libthriftc.so
libthriftc.so.0
libthriftc.so.0.0.0
libthrift.la
libthriftnb-0.10.0.so
libthriftnb.a
libthriftnb.la
libthriftnb.so
libthriftqt-0.10.0.so
libthriftqt.a
libthriftqt.la
libthriftqt.so
libthrift.so
libthriftz-0.10.0.so
libthriftz.a
libthriftz.la
libthriftz.so

$ ls /usr/local/include | grep thrift
thrift

$ which thrift
/usr/local/bin/thrift

(先跑起来再说)

编译日志里有一大段比较重要:

1
2
3
4
5
6
7
8
9
10
11
12
13
If you ever happen to want to link against installed libraries
in a given directory, LIBDIR, you must either use libtool, and
specify the full pathname of the library, or use the `-LLIBDIR'
flag during linking and do at least one of the following:
- add LIBDIR to the `LD_LIBRARY_PATH' environment variable
during execution
- add LIBDIR to the `LD_RUN_PATH' environment variable
during linking
- use the `-Wl,-rpath -Wl,LIBDIR' linker flag
- have your system administrator add LIBDIR to `/etc/ld.so.conf'

See any operating system documentation about shared libraries for
more information, such as the ld(1) and ld.so(8) manual pages.

去检查一下/etc/ld.so.conf是否已经囊括了你安装库的目录.

开发流程

一般的开发流程是这样的(和pb类似):

  • 规矩需求编写thrift接口定义文件
  • 使用thrift binary为不同的语言生成代码
  • 根据需求, 修改生成的代码(主要是Server端骨架代码), 编写实际的业务逻辑
  • 编译, 集成

通过IDL, 即thrft描述文件, 对其中定义的数据结构, 如struct等, 以及传输业务逻辑, 根据不同的运行环境构建相应的代码. 至于内部序列化, 压缩, 文本交互, 并发, 这个在其框架内部已经解决, 描述起来就是下图:

整个过程非常清晰, 只是中间有一些小细节需要注意一下, 比如说thrift文件怎么写才规范, 怎么生成代码, 运行生成的代码加入实际业务逻辑等. 下面给出一个案例.(我习惯用cpp语言, 当然别的语言也有案例, 暂时小的案例用cpp)

案例

rpc

按照上面讲的流程, 先跑起来; 为了简单, 我先不管目录结构. 下面的例子, 演示一下rpc.

先创建必要的目录

1
2
3
$ mkdir thrift_test
$ cd thrift_test
$ touch ping.thrift

ping.thrift的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ping.thrift


/**
* Thrift files can namespace, package, or prefix their output in various
* target languages.
*/
namespace cpp pingtest

/**
* Defining a class named pinger
*/
service pinger
{

/**
* client calls ping method to make sure service process is active or dead
*/
void ping()

}

(注意我使用的关键词 service)
代码比较简单, 然后就在本目录下生成server端的骨架(框架模板代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ thrift --gen cpp -o . ping.thrift 

$ tree -L 2
.
├── gen-cpp
│   ├── ping_constants.cpp
│   ├── ping_constants.h
│   ├── pinger.cpp
│   ├── pinger.h
│   ├── pinger_server.skeleton.cpp
│   ├── ping_types.cpp
│   └── ping_types.h
└── ping.thrift

1 directory, 8 files

也就是说服务器端的模板代码全部产品了, 但是这些代码基本不涉及业务, 所以还要写自己的业务server端代码来调用上述框架代码, 但是这里仅仅是演示, 不需要写了, 直接利用pinger_server.skeleton.cpp 这个主文件就好(这个文件你可以改名为server.cpp, 它就是server端的主文件, 含有main函数).

1
2
$ cp gen-cpp/* .
$ cat pinger_server.skeleton.cpp

代码如下: (中间我就加了一句打印)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// This autogenerated skeleton file illustrates how to build a server.
// You should copy it to another filename to avoid overwriting it.

#include "pinger.h"
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/server/TSimpleServer.h>
#include <thrift/transport/TServerSocket.h>
#include <thrift/transport/TBufferTransports.h>

using namespace ::apache::thrift;
using namespace ::apache::thrift::protocol;
using namespace ::apache::thrift::transport;
using namespace ::apache::thrift::server;

using boost::shared_ptr;

using namespace ::pingtest;

class pingerHandler : virtual public pingerIf {
public:
pingerHandler() {
// Your initialization goes here
}

/**
* client calls ping method to make sure service process is active or dead
*/
void ping() {
// Your implementation goes here
printf("ping\n");
}

};

int main(int argc, char **argv) {
//默认9090端口
int port = 9090;

//下面是一些通讯必要的协议等内容
shared_ptr<pingerHandler> handler(new pingerHandler());
shared_ptr<TProcessor> processor(new pingerProcessor(handler));
shared_ptr<TServerTransport> serverTransport(new TServerSocket(port));
shared_ptr<TTransportFactory> transportFactory(new TBufferedTransportFactory());
shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());


//创建一个TSimpleServer对象, 虽然效率不高, 但是仅仅做远端检查足够了
TSimpleServer server(processor, serverTransport, transportFactory, protocolFactory);
server.serve();//开始监听了
return 0;
}

好了, 先把server端可执行文件编译出来:

1
$ g++ -g -Wall -std=c++11  -lthrift ./*.cpp -o server

如果你没有配置环境, 完整的应该是这样:

1
g++ -g -Wall -std=c++11 -I/usr/local/include -L/usr/local/lib -lthrift ./*.cpp -o server

如果不是需要客户端调用, 可以用curl或者nc进行调试, 但是这里是RPC, 不仅仅是通讯或者交换数据, 所以这里客户端要手写 client.cpp. 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>

#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/transport/TSocket.h>
#include <thrift/transport/TTransportUtils.h>

#include "pinger.h" //使用pingerClient对象

using namespace std;
using namespace apache::thrift;
using namespace apache::thrift::protocol;
using namespace apache::thrift::transport;

using namespace pingtest;

int main(void)
{
//连接远端服务器
boost::shared_ptr<TTransport> socket(new TSocket("localhost", 9090));
boost::shared_ptr<TTransport> transport(new TBufferedTransport(socket));
boost::shared_ptr<TProtocol> protocol(new TBinaryProtocol(transport));
pingerClient client(protocol);

try {
transport->open();

client.ping();//远端调用

transport->close();

} catch (TException& tx) {
cout << "ERROR: " << tx.what() << endl;
}

return 0;
}

编译

1
g++ -g -Wall -std=c++11  -lthrift client.cpp ping_constants.cpp pinger.cpp ping_types.cpp  -o client

运行一下(开两个端口, 先开server, 在开client)

1
2
3
$ ./server 
ping
ping

server一直运行(死循环), 然后client运行一次, server端打印ping一次.

远端调用就是这么简单? 其实是骨架代码做了很多, 先不探讨源码(这里只说简单的案例)

最后补充关于目录组织, makefile, 请按照你们研发经理的要求来.

数据传递

上面那个仅仅有远端调用, 没有实际的c-s传输数据, 下面这个案例带有数据传递.
数据传递
先写thrift接口文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# transfer.thrift
namespace cpp transfertest


enum ResponseState
{
StateOk = 0,
StateError = 1,
StateEmpty = 2
}

/*请求时的数据*/
struct Request
{
1: i32 studentID = 0
}


/*响应时的数据*/
struct Response
{
1: i32 studentID = 0,
2: string name,
3: list<string> infos,
4: ResponseState state
}

/*远端调用*/
service TransferService
{
Response getStudentInfo(1: Request request);
}

注意 Response getStudentInfo(1: Request request) 原型和生成的代码有区别, 当然你的IDL文件里面写很多crud的servcie都没有关系, 根据你的业务需要来.

生成框架代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ thrift --gen cpp transfer.thrift
$ cp gen-cpp/* .
$ ll
总用量 68
drwxr-xr-x 3 merlin merlin 4096 9月 2 17:23 .
drwxr-xr-x 4 merlin merlin 4096 9月 2 17:11 ..
drwxr-xr-x 2 merlin merlin 4096 9月 2 17:22 gen-cpp
-rw-r--r-- 1 merlin merlin 305 9月 2 17:23 transfer_constants.cpp
-rw-r--r-- 1 merlin merlin 393 9月 2 17:23 transfer_constants.h
-rw-r--r-- 1 merlin merlin 12831 9月 2 17:23 TransferService.cpp
-rw-r--r-- 1 merlin merlin 9888 9月 2 17:23 TransferService.h
-rw-r--r-- 1 merlin merlin 1423 9月 2 17:23 TransferService_server.skeleton.cpp
-rw-r--r-- 1 merlin merlin 433 9月 2 17:20 transfer.thrift
-rw-r--r-- 1 merlin merlin 7347 9月 2 17:23 transfer_types.cpp
-rw-r--r-- 1 merlin merlin 3205 9月 2 17:23 transfer_types.h

修改TransferService_server.skeleton.cpp代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// TransferService_server.skeleton.cpp

#include "TransferService.h"
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/server/TSimpleServer.h>
#include <thrift/transport/TServerSocket.h>
#include <thrift/transport/TBufferTransports.h>

using namespace ::apache::thrift;
using namespace ::apache::thrift::protocol;
using namespace ::apache::thrift::transport;
using namespace ::apache::thrift::server;

using boost::shared_ptr;

using namespace ::transfertest;

class TransferServiceHandler : virtual public TransferServiceIf {
public:
TransferServiceHandler() {
// Your initialization goes here
}

void getStudentInfo(Response& _return, const Request& request) {
// Your implementation goes here
printf("getStudentInfo called\n");

//处理请求数据, 发送响应给客户端
printf("client request studentID: %d\n", request.studentID);

//给Response设置数据
_return.studentID = request.studentID;
_return.name = "merlin";
_return.infos.push_back("信息1");
_return.infos.push_back("信息2");
_return.state = ResponseState::StateOk;
printf("data has benn sent to client\n");
}

};

int main(int argc, char **argv) {
int port = 9090;
shared_ptr<TransferServiceHandler> handler(new TransferServiceHandler());
shared_ptr<TProcessor> processor(new TransferServiceProcessor(handler));
shared_ptr<TServerTransport> serverTransport(new TServerSocket(port));
shared_ptr<TTransportFactory> transportFactory(new TBufferedTransportFactory());
shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());

TSimpleServer server(processor, serverTransport, transportFactory, protocolFactory);
server.serve();
return 0;
}

函数void getStudentInfo(Response& _return, const Request& request)原型和我们在thrift里面指定的有区别.

编译服务端:

1
$ g++ -g -Wall -std=c++11 *.cpp -o server -lthrift

然后在写客户端client.cpp, 发送请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/transport/TSocket.h>
#include <thrift/transport/TBufferTransports.h>
#include <thrift/protocol/TCompactProtocol.h>

#include "TransferService.h" //使用TransferServiceClient对象


using namespace ::transfertest; //Request, Response

using namespace apache::thrift;
using namespace apache::thrift::protocol;
using namespace apache::thrift::transport;



using boost::shared_ptr;

int main(int argc, char **argv)
{
//如果和服务端信息不匹配就报错
boost::shared_ptr<TSocket> socket(new TSocket("localhost", 9090));
boost::shared_ptr<TTransport> transport(new TBufferedTransport(socket));
boost::shared_ptr<TProtocol> protocol(new TBinaryProtocol(transport));

//设置响应的数据(发给服务端, 其实是rpc)
Request quest;
quest.studentID = 1;

//用于接收服务端信息
Response resp;

//打开连接
transport->open();
TransferServiceClient client(protocol);
client.getStudentInfo(resp, quest);
//关闭连接
transport->close();

//输出信息
printf("get response: ID=%d name=%s state=%d\n", resp.studentID,
resp.name.c_str(), resp.state);
return 0;
}

编译

1
2
$ g++ -g -Wall -std=c++11 -lthrift client.cpp \ 
transfer_constants.cpp transfer_types.cpp TransferService.cpp -o client

运行一下, 效果如下:

1
2
3
4
5
6
7
8
9
$ ./server 
getStudentInfo called
client request studentID: 1
data has benn sent to client
^C

##另外一个terminal
$ ./client
get response: ID=1 name=merlin state=0

这个例子就这样了, 当然官网还有一个类似的, 稍稍复杂的 例子 , 就不再多说了.

架构

本段内容是对 thrift-wiki 架构部分的剖析.

总体的架构图如下:
架构

可以看到, thrift实际跨了两层: 应用层和传输层(有些博文非要把它的那个颜色认为是不同的层, 我也就无话可说了), 也就是我们再网络编程中最长需要打交道的, 并且read/write以下的部分, 即TProtocol, TTransport属于thrift框架的传输体系, 如果不是研究源码, 可以暂时不关注(使用的时候具体选择以下使用哪种类型的传输协议). 实际上研究源码, 发现也就是我们通常需要手工完成的:

  • 服务器端这里还有一个TProcessor负责读写 消息体等操作(客户端对应TProcessor的是TriftClient)
  • 服务器端这里还有Tserver, 负责接收Client的请求, 并将请求转换给TProcessor
  • 编码封装 (TProtocol) 把用户的数据封装成一个 消息体, 通常是二进制的; (注意原来的数据是带有类型的, 消息体的数据只管encode编码字节数)
  • 字节流转换 (TTransport) 把消息体以字节流的方式发送给底层IO(或者从其接收字节流再转换成消息体), 到底是流式IO还是文件IO; 每一种底层IO对应这里的一种TTransport
  • 底层IO (socket/file/zip)

我们真正需要处理(修改)的也就是Input code部分和Service Client.

如下图:(详细架构)

准确说 TProtocol定义了传输协议规范(编码规范-大体上分为二进制和文本方式), TTransport定义了传输数据标准(根据不同的方式把说句做转换), 以及服务器类型, 当然服务器类型不仅仅包含IO类型.

Processor(或者TProcessor)负责对Client的请求做出响应, 包括 RPC 请求转发(TProcessor.process), 调用参数解析和用户逻辑调用, 返回值写回等处理步骤, 也包括thrift消息结构体的读取和写入. Processor是服务器端从Thrift框架转入用户业务逻辑的关键流程, 你简单理解成这个复杂系统中统一的输入和输出处理即可.(针对详细结构体和远端调用)

其模块设计, 层次界限非常到位.

强调一些最基本的概念.

框架的协议是指编码方式; 框架的传输方式包括文件,缓存buffer, 压缩或者需要服务器端同步异步工作支持的IO类型(因为传输方式使用了一些同步或者异步的IO), 传输方式也会指定传输协议. 服务器类型是指服务器的工作模式, 是同步阻塞处理请求还是非阻塞操作, 是否支持多线程模型, 还是异步多路IO.

协议规范

Thrift可以让用户选择客户端与服务端之间传输通信协议的类别,在传输协议上总体划分为文本(text)和二进制(binary)传输协议,为节约带宽,提高传输效率,一般情况下使用二进制类型的传输协议为多数,有时还会使用基于文本类型的协议,这需要根据项目/产品中的实际需求.
常用协议有以下几种:

  • BinaryProtocol: 二进制编码格式进行数据传输使
  • TCompactProtocol: 高效率的、密集的二进制编码格式进行数据传输
  • TJSONProtocol: 使用 JSON 数据编码协议进行数据传输

wiki中说到的其他的:

  • TDebugProtocol(调试时使用的文本格式)
  • TDenseProtocol(类似TCompactProtocol, 但不包含 meta info)
  • TSimpleJSONProtocol(drops metadata using JSON, 所以只用于只读解析).

传输标准

这里的传输标准通俗说就是读写方式, 可能有一些方式需要服务器以同步或者异步的工作形式支持, 也可能有些形式仅用于服务端或者客户端等, 比如下面名字中带有Server字样的, 仅用于服务器端. (不强调Server字样的, 一般既可以用于服务器端也可以用于客户端)

由于一些语言有限制,或者标准不同, thrift定义的标准在具体语言中可能处理情况不一样.

  • TSocket : 使用阻塞式 I/O 进行传输,是最常见的模式(就是简单的阻塞IO: read, write)
  • TServerSocket:非阻塞型 socket 传输, 轮询的时候, 没有IO读写就直接返回, 但一旦 accecpt 到, 还是阻塞读写, 即TSocket(即阻塞型 socket).

wiki里面也谈到了其他种类:(可能不太常用)

  • TFDTransport 是非常简单地写数据到文件和从文件读数据,它的 write 和 read 函数都是直接调用系统函数 write 和 read 进行写和读文件
  • TSimpleFileTransport 直接继承 TFDTransport,没有添加任何成员函数和成员变量, 算是对TFDTransport的轻量扩展
  • TFileTransport - (一些写线程, 一个读线程)以文件的方式传输, 主线程负责将事件入列,写线程将事件入列,并将事件里的数据写入磁盘.(继承自TTransport)
  • TBufferedTransport - 带缓存的传输形式(需要使用non-blocking IO)
  • TFramedTransport - 带缓存(以帧的)形式传输(要求使用非阻塞IO, 可能因为帧指定了长度; 头4个字节指定了长度; int32_t)
  • TMemoryTransport - 这个使用内存做IO(用于程序内部通信用,不涉及任何网络I/O)
    • OBSERVE模式,不可写数据到缓存
    • TAKE_OWNERSHIP模式,需负责释放缓存
    • COPY模式, 拷贝外面的内存块到TMemoryBuffer
  • TZlibTransport - 压缩传输需要配合其他传输协议一起.

(一般用于缓存读写的方式是TMemoryTransport, 没有调用flush方法之前, 不涉及网络)

还有2个比较特殊的:

  • TSSLSocket 继承 TSocket,阻塞型 socket, 用于客户端. (采用 openssl 的接口进行读写数据)
    checkHandshake()函数调用 SSL_set_fd 将 fd 和 ssl 绑定在一起,之后就可以通过 ssl 的 SSL_read和SSL_write 接口进行读写网络数据
  • TSSLServerSocket 继承 TServerSocket, 非阻塞型 socket, 用于服务器端. accecpt 到的 socket 类型都是 TSSLSocket 类型.

当然, 也提供了基于 HTTP协议 的传输类型: (继承 THttpTransport, 基于Http1.1)

  • THttpClient 用于客户端
  • THttpServer 用于服务器端
    两者都调用下一层 TTransport 类进行读写操作, 均用到 TMemoryBuffer 作为读写缓存, 只有调用 flush() 函数才会将真正调用网络 I/O 接口发送数据; 并且这里的TTransport为上层提供的类似多态的方法, 通过TTransport接口可以调用其子类的不同实现. (TTransport 是所有 Transport 类的父类; 而THttpTransport是Transport的子类)

服务端类型

根据传输标准的不同, 并发程度的要求, 也有多种不同类型的服务器(也就是服务端的工作模式), 并且生成不同语言代码时, 实现也不一样, 比如说非阻塞IO, Java使用NIO, 而C++借助Lievent库. 根据不同场合选择不同的server类型, 需要对网络模型非常熟悉.

下面是主要的服务端类型:(源码剖析的时候会再说)

  • TSimpleServer : 单线程服务器端使用标准的阻塞式 I/O
  • TNonblockingServer : 单线程异步IO(多路轮询检测, 处理读写的时候还是阻塞IO), 该类型Server必须使用TFramedTransport.
  • THsHaServer : select轮询 + 线程池工作线程处理读任务, 写任务还是阻塞IO. (半同步半异步)
  • TThreadPoolServer : 线程池+标准的阻塞式 I/O
  • TThreadedSelectorServer : AcceptThread + SelectorThread + SelectorThreadLoadBalancer(调度器) + ExecutorService(工作线程池)

(如果看源码, 我局的最后这一种高级方式, 值得一看)

TSimpleServer 接受一个连接,处理连接请求,直到客户端关闭了连接,它才回去接受一个新的连接。正因为它只在一个单独的线程中以阻塞 I/O 的方式完成这些工作,所以它只能服务一个客户端连接,其他所有客户端在被服务器端接受之前都只能等待. 可以看到TSimpleServer基本只能用于测试.

TThreadPoolServer 如果有连接请求来了, 那么从线程池中拿一个工作线程来应对网络IO事件, 主线程是非阻塞的, worker线程则是使用阻塞IO. (但是吞吐量是原来的N倍, N为work线程的个数)

TNonblockingServer 这种模式对应linux网络编程中的select模型; 所以socket都注册到select, 然后一个线程中通过seletor循环监控所有的socket,每次selector结束时,处理所有的处于就绪状态的socket,对于有数据到来的socket进行数据读取操作,对于有数据发送的socket则进行数据发送,对于监听socket则产生一个新业务socket并将其注册到selector中. (如果没有则进行下一次轮询, 如果有, 就必须处理完相关的socket IO才能进行下一次轮询) 该模式比TSimpleServer好的地方在于, 原来是单线程阻塞, 即一旦连接上了, 非要你IO完毕, 我服务端才处理下一个请求. (该模型是, 我都(监听)处理, 你都来吧, 但是真正有IO读写的, select才去调用阻塞IO去处理读写任务)监听的多, 但是真正处理起来还是一个一个顺序执行, 一旦有耗时任务, 效率就不高了.

THsHaServer类是TNonblockingServer类的子类, 是TThreadPoolServer单线程的部分解决方(引入了一个线程池做优化), 专门进行读业务处理. 也就是原来selector的主线程主要负责写任务, 之后进入下一次轮询(主线程也处理就绪需要accept的socket); 但是读任务全部交给线程池中的工作线程, 不阻塞主线程. 只能说部分优化了, 当并发请求数较大时,且发送数据量较多时,监听socket上新连接请求不能被及时接受(毕竟主线程还是会被写任务阻塞住)。

TThreadPoolServer模式, 这种模式没有使用select这类异步操作, 而是同步的去判断是否由socket就绪(accept), 对仅仅是处理accept, 没有的话阻塞整个进程在那儿等待, 然后一旦有连接, 不管有没有IO任务, 它都会启动一个专门的线程去处理这个连接上的所有任务(方式是把socket封装成一个新任务交给线程池, 之后工作线程才去从socket拿出请求, 完成具体调用返还客户客户端), 这样得以把主线程空出来继续阻塞等待别的连接请求. 这个缺点就很明显了, 受限于线程池中线程的数量(并发量大于线程池数量时, 能扩展线程数最好, 不能的话, 那么只能进入队列等待了), 并发量不大. (最好你知道有多少个, 最多有多少个客户端会连接的情况)

TThreadedSelectorServer : 这种模型, 把网络中的任务细致划分. 可谓足够精细, 多个部分协作工作. 如果说上面的都是个人&两三个人, 这个模型就是一个小团队, 请直接看图.

稍微解释一下, 普通的网络编程中成功用例是这么工作的: (服务端)

接受连接请求 –> 检测是否有IO就绪 —> 处理真正的IO操作

但是引入线程池之后, 这个模型变成了:(流程自上而下)

  • 线程AcceptThread阻塞(或者非阻塞)等待是否由心的连接 (这个是销售顾问, 拿到订单)
  • 有新的连接就把它交给SelectorThread以检测socket是否就绪(是否由IO读写请求) (这个是项目经理, 负责拿到的订单项目)
  • 被accept的连接,需要SelectorThreadLoadBalancer来调度,以免某个SelectorThread压力过大(这个是研发经理, 分派任务)
  • SelectorThread读取具体的请求, 但是实际完成耗时IO(或者具体调用)的却是ExecutorService. (这个就是高级工程师)

如果你的场景不需要某种支持高并发的姿态, 就不要浪费资源选择高级模式; 够用就好. 但是还有一句, 如果几种模型的优缺点你不知道, 那么选择最好的总不会错, 直接上TThreadedSelectorServer也没事儿.


详细说明

这一部分是阅读其官网文档的总结, 你可以把它看做使用手册.

传输框架

该部分请 参考架构部分. (上面)

主要是协议protocol, transport方式, 以及server类型即可. 协议部分我们在pb里面以及涉及到编码规则内容了,没有必要详细深挖; 主要值得研究的是在 传输方式服务器类型上, 不同的方式有不同的实现细节, 以及后端思想; 并且效率也可能相差很多.

后期有时间, 可以像Libevent一样, 把TSocket等传输方式, 以及TSimpleServer等服务器类型; 源码剖析一下.

(见下面 源码分析 部分)

名字空间

越来越多的语言使用package作为代码控制机制, 到具体的语言可能是模块(Python), 包(Java), 或者namespace(Cpp)

数据类型

thrift定义了几大数据类型, 在不同环境中翻译成不同语言时候mapping关系也不大一样; 幸运的是, 这一层抽象被thrift框架处理了, 我们只用关心thrift数据类型即可.

类型主要包括: 基本类型、结构体和异常类型(异常使用关键字exception)、容器类型、服务(service)类型;

基本类型:

  • bool:布尔值 (true or false), 1 字节
  • byte:有符号字节
  • i16:16位有符号整型
  • i32:32位有符号整型
  • i64:64位有符号整型
  • double:64位浮点型
  • string:未知编码或者二进制的字符串

结构体:
例如:

1
2
3
4
5
6
struct UserDemo {
  1: i32 id;
  2: string name;
  3: i32 age = 25;
  4: string phone;
}

必须注意:

  • struct 不能继承,但是可以嵌套别人,不能嵌套自己
  • 其成员都是有明确类型
  • 成员是被正整数编号过的,其中的编号使不能重复的,这个是为了在传输过程中编码是用(和pb一样)
  • 成员分割符可以是逗号或是分号, 而且可以混用(建议使用分号, C语言习惯)
  • 字段会有optional和required之分(和pb一样)
  • 每个字段可以设置默认值(和pb一样)
  • 同一文件可以定义多个struct; 可以用include包含别的文件的struct定义

补充说明:

  • 编码值, 不要随便编;
  • 字段类型required或者optional(没有赋值则不进行序列化), 不指定则默认是会被序列化的(基本和required相同, 但required会被框架提示)

容器类型:
主要使用编程语言无关的数据结构作为容器, 一般就三个:

  • list: 有序表, 允许元素重复
  • set: 无序表, 不容许元素重复
  • map: 键类型为t, 值类型为t的kv对, 键不容许重复

生成的代码中, 不同语言对应的数据结构可能有一些差异, 但是不影响其功能.

服务类型:
就是trift文件中的service, 代码中等价于类或者接口集合(代码中支持继承, 但如果继承这个类, 其方法必须实现), 但接口里面定义的方法不支持重载.
由上面的实践来看, 参数一般是const类型, 并且最终生成的代码和你定义的service函数原型可能不一样, 可能会根据具体的语言进行调整(具体规则, 需要参考它的gen代码生成器).

代码生成

Thrift自动生成代码的代码框架被直接硬编码(hardcode)到了代码生成器里(不知道Facebook有没有工具),因此对生成代码的结构进行修改需要重新编译Thrift,并不是十分方便。如果Thrift将代码结构保存到一个模板文件里,修改生成代码就会相对容易一些。

自动生成的代码就会遵守一定的命名规则。Thrift中几种主要的命名规则为:

1
2
3
4
5
6
7
8
9
10
11
12
13
1.	IDLName + ”_types.h” :用户自定义数据类型头文件
2. IDLName + ”_constants.h” :用户自定义的枚举和常量数据类型头文件
3. ServiceName + “.h” :Server端Processor定义和Client定义头文件

----------------------------------------------------------------------------------------

4. ServericeName + ”_” + RPC名称 + “_args” :服务器端RPC参数解析类---统一参数解析类
5. ServericeName + ”_” + RPC名称 + “_result” :服务器端RPC返回值打包类
6. ServericeName + ”_” + RPC名称 + “_pargs” :客户端RPC参数打包类
7. ServericeName + ”_” + RPC名称 + “_presult” :客户端RPC返回值解析类
8. “process_” + RPC名称:服务器端RPC调用处理函数
9. “send_” + RPC名称:客户端发送RPC请求的方法
10. “recv_” + RPC名称:客户端接收RPC返回的方法

统一参数解析类和处理方法的设想:
客户端和服务器的参数解析和返回值解析, 虽然针对的是同样的数据结构, 但是 thrift 并没有使用同一个类来完成任务, 而是将客户端和服务器的解析类分开. (TProcess和Client)

当 RPC 调用参数含有相同信息并需要进行相同操作的时候, 对参数解析类的集中管理就会变得非常有必要了. 比如在一些用 thrift 实现访问控制的系统中, 每一个 RPC 调用都会加一个参数token作为访问凭证(是否可以访问), 并在每一个用户函数里进行权限检查. 使用 统一的参数解析类接口 的话, 就可以将分散的权限检查集中到一块进行处理. thrift 中有众多的解析类, 这些解析类的接口类似, 但是却没有一个共有的基类. 对参数的集中管理造成了一定的困难. 如果Thrift为解析类建立一个基类, 并把解析类指针放到一个Map中, 这样参数就可以进行集中管理, 不仅可以进一步减小自动生成代码的体积, 也满足了对参数进行统一管理的需求.

下面开始重头戏.


源码分析

框架本身有很多精华, 我抽取其中我熟悉的进行分析.

调用流程

该部分其实是解释thrift框架如何工作的, 形式上是c-s一次通信的过程; 这个要结合源码起来看(下面图片请在新窗口中打开观看), 不同源码版本可能不一样.

服务端主要流程如下:

TThreadPoolServer 的 serve() 方法后,server 进入阻塞监听状态,其阻塞在 TServerSocket 的 accept()方法上。当接收到来自客户端的消息后,服务器发起一个新线程处理这个消息请求,原线程再次进入阻塞状态。在新线程中,服务器通过 TBinaryProtocol 协议读取消息内容,调用 HelloServiceImpl 的 helloVoid() 方法,并将结果写入 helloVoid_result 中传回客户端。

过程如下图:(下面的图请单独拉开网页看)
server

(整理处理流程比较简单, 就是调用过程负责)

客户端的处理流程如下: (下面的图请单独拉开网页看)

程序调用了 Hello.Client 的 helloVoid() 方法,在 helloVoid() 方法中,通过 send_helloVoid() 方法发送对服务的调用请求,通过 recv_helloVoid() 方法接收服务处理请求后返回的结果。远程rpc调用过程,以及网络传输过程, 全部被封装在框架里了.

client

Server工作模式

但凡后端人员接触这个库, 我觉得最感兴趣的, 一定是server的类型, 即服务器的工作模式. 当然网络编程玩烂的后端人员真正剖析的识货, 也就是见怪不怪了. (不知道我说啥的同学, 可以补一下 apue的9种网络模式或者参考unp 卷1)

下面主要剖析一下(权做复习):

  • TSimpleServer
  • TNonblockingServer
  • THsHaServer
  • TThreadPoolServer
  • TThreadedSelectorServer—优先剖析

连接池实现

再讲线程相关的内容时, 我连着对象池, 连接池, 线程池一起说了. 请参考本博客里线程池相关内容.

(谁有时间再来看它的连接池接口代码吧—不懂源码不影响使用流程的)

对比其他框架.

看到的RPC框架或者有RPC功能的框架已经算比较多的了, protobuffer, soap, grpc, 甚至libevent也说自己是rpc框架.

pb更多的是提供了跨语言的序列化和反序列化机制, 而thrift则是在数据结构信息中提供了消息头用来进行RPC, 详细信息可以参考下图:(thrift消息体逻辑结构)

优点:

  • 用于搭建大型数据交换及存储的通用工具, 对于大型系统中的内部数据传输相对于 JSON 和 XML 无论在性能、传输大小上有明显的优势.
  • 拥有比较完整的体系(内嵌完整的RPC层次), 省去了很多手工编码的机会.
  • 多语言, 多数据类型支持(支持的语言比pb多, 还支持容器数据结构)
  • 运行开销性能比pb略好(cpu占用率维持在30%以内)

缺点:

  • 框架庞大(IDL借鉴与CORBA, 完成了整个服务端和客户端的架构体系), 排查错误费时间
  • thrift是完全静态化的描述文件(生成器是硬编码而不是依据模板来的), 一旦数据结构发生改变, 须重新编辑IDL文件, 代码生成, 再编译载入的流程(和pb相比较弱)
  • 传输性能上比pb要略差(即使是压缩二进制 TCompactProtocol方式)
  • 参考资料少啊!

尾巴

综合一圈玩下来, 这个库太棒了, 把我从学习网络以来的绝大部分工作全部整合成流程了, 而且效率也不差(参考网友的评测).

但是就是坑太多! 如果你没有把它大部分源码吃透, 目前还不适合用于实际工程项目(我是指你负责这个项目工程的兜底工作), 并且由于其框架太重, 所以可能学习成本会稍高.

墨镜王(王家卫导演啦)东邪西毒里有一句话,

“男人看见一座山总想翻过去, 看看山的那一边是什么, 等真正翻过去了, 也没发现有什么好, 说不定山的这一边儿反而更好.”

以后等它成熟了再来, 目前libevent + 手写代码(或者pb), 挺好的. (但是这个 库的学习价值真的很高 , 比如说在线程管理上, 它用 boost::shared_ptr 弱引用保证被多个线程接纳的已死对象的清理工作; 并且在保证返回给调用者一定能拿到操作系统创建的线程时使用了弱引用指向自身, 这样在真正拿到线程之前就能保证不被操作系统过早清理掉–过早返回给ThreadMain的调用者是有可能被清理掉的…等等技巧)

这个库没有说完, 以后有时间再回来更新.

好累.

参考资料

  1. http://thrift.apache.org/
  2. https://en.wikipedia.org/wiki/Apache_Thrift
  3. https://www.ibm.com/developerworks/cn/java/j-lo-apachethrift
  4. http://thrift.apache.org/static/files/thrift-20070401.pdf
  5. https://wiki.apache.org/thrift/ThriftInstallation
  6. https://wiki.apache.org/thrift/FrontPage
  7. 和 Thrift 的一场美丽邂逅 文章有一些地方讲述错误
  8. http://blog.csdn.net/houjixin/article/details/42779915
文章目录
  1. 1. 引子
  2. 2. 正文
    1. 2.1. 安装
    2. 2.2. 开发流程
    3. 2.3. 案例
      1. 2.3.1. rpc
      2. 2.3.2. 数据传递
    4. 2.4. 架构
      1. 2.4.1. 协议规范
      2. 2.4.2. 传输标准
      3. 2.4.3. 服务端类型
    5. 2.5. 详细说明
      1. 2.5.1. 传输框架
      2. 2.5.2. 名字空间
      3. 2.5.3. 数据类型
      4. 2.5.4. 代码生成
    6. 2.6. 源码分析
      1. 2.6.1. 调用流程
      2. 2.6.2. Server工作模式
      3. 2.6.3. 连接池实现
    7. 2.7. 对比其他框架.
  3. 3. 尾巴
  4. 4. 参考资料
|