Thrift学习总结
发表于2016-09-03
Apahce Thrift是FaceBook实现的一种高效的、支持多种语言的远程服务调用的框架。本文结合网络上的资源对从C#开发人员的角度简单介绍Apache Thrift的架构、开发,并且针对不同的传输协议和服务类型给出相应的C#实例,同时简单介绍Thrift异步客户端的实现。
一、前言
Thrift是一款由Fackbook开发的可伸缩、跨语言的服务开发框架,该框架已经开源并且加入的Apache项目。Thrift主要功能是:通过自定义的Interface Definition Language(IDL),可以创建基于RPC的客户端和服务端的服务代码。数据和服务代码的生成是通过Thrift内置的代码生成器来实现的。Thrift 的跨语言性体现在,它可以生成C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, OCaml , Delphi等语言的代码,且它们之间可以进行透明的通信。
Thrift代码生成器windows版下载地址
http://www.apache.org/dyn/closer.cgi?path=/thrift/0.9.2/thrift-0.9.2.exe
Thrift源码下载地址
http://www.apache.org/dyn/closer.cgi?path=/thrift/0.9.2/thrift-0.9.2.tar.gz
二、准备工作
本文先举个栗子来简单说明Thrift的使用以及如何构建Thrift服务。首先需要下载Thrift的代码生成器和源码,如上连接。
有了代码生成工具,还需要Thrift类库文件,以便我们调用。打开下载好的源码,找到thrift-0.9.2libcsharpsrc下的解决方案,
然后用VS2013打开,可以看见如下结构
三、一个简单的小程序
1、准备工作
做完了前期准备工作就可以正式开始编写一个小程序,感受thrift。创建一个简单的Hello服务,根据thrift的语法规范编写脚本文件Hello.thrift,代码如下:
清单1.Hello.Thrift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | namespace csharp HelloThrift.Interface service HelloService{ string HelloString(1: string para) i32 HelloInt(1:i32 para) bool HelloBoolean(1: bool para) void HelloVoid() string HelloNull() } |
然后打开cmd切换到thrift代码生成工具的存放目录,在命令行中输入如下命令thrift -gen csharp Hello.thrift
代码生成工具会自动在当前目录下把定义好的接口脚本生成C#代码,生成后的代码目录如下
打开目录会看到生成的服务定义文件
其中定义了服务HelloService的五个方法,每个方法包含一个方法名,参数列表和返回类型。每个参数包括参数序号,参数类型以及参数名。Thrift 是对 IDL(Interface Definition Language)描述性语言的一种具体实现。因此,以上的服务描述文件使用 IDL语法编写.使用Thrift 工具编译 Hello.thrift,就会生成相应的 HelloService.cs文件。该文件包含了在 Hello.thrift 文件中描述的服务 HelloService 的接口定义,即HelloService.Iface 接口,以及服务调用的底层通信细节,包括客户端的调用逻辑HelloService.Client 以及服务器端的处理逻辑HelloService.Processor,用于构建客户端和服务器端的功能。
2、小试牛刀
做好准备工作以后就可以开始感受thrift的魅力。打开VS新建一个空白解决方案命名为HelloThrift。在解决方案根目录下创建一个lib文件夹,将准备工作中生成的Thrift.dll文件放入lib文件夹中。在解决方案分中建立两个控制台程序和一个类库,控制台程序分别命名为HelloThrift.Client和HelloThrift.Server,类库命名为Thrift.Interface。Client、Server和Interface分别引用lib文件夹中的Thrift.dll文件,将准备工作中生成的HelloService文件导入到Interface类库中。Client和Server分别引用Interface。具体结果如下图所示。
完成引用导入工作以后,在服务端创建一个类命名为MyHelloService,实现HelloService.Iface接口,代码如下:
清单2.MyHelloService
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | using System; using HelloThrift.Interface; namespace HelloThrift.Server { public class MyHelloService : HelloService.Iface { /// /// 只有一个参数返回值为字符串类型的方法 /// /// string类型参数 /// public string HelloString( string para) { Console.WriteLine( "客户端调用了HelloString方法" ); return para; } /// /// 只有一个参数,返回值为int类型的方法 /// /// /// public int HelloInt( int para) { Console.WriteLine( "客户端调用了HelloInt方法" ); return para; } /// /// 只有一个bool类型参数,返回值为bool类型的方法 /// /// /// public bool HelloBoolean( bool para) { Console.WriteLine( "客户端调用了HelloBoolean方法" ); return para; } /// /// 返回执行为空的方法 /// public void HelloVoid() { Console.WriteLine( "客户端调用了HelloVoid方法" ); Console.WriteLine( "HelloWorld" ); } /// /// 无参数,返回值为null的方法 /// /// public string HelloNull() { Console.WriteLine( "客户端调用了HelloNull方法" ); return null ; } } } |
创建服务器端实现代码,将MyHelloService 作为具体的处理器传递给 Thrift 服务器,代码如下:
清单3.HelloThrift.Server
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | using System; using HelloThrift.Interface; using Thrift; using Thrift.Protocol; using Thrift.Server; using Thrift.Transport; namespace HelloThrift.Server { class Program { /// ///启动服务端 /// /// static void Main( string [] args) { try { //设置服务端口为8080 TServerSocket serverTransport = new TServerSocket(8080); //设置传输协议工厂 TBinaryProtocol.Factory factory = new TBinaryProtocol.Factory(); //关联处理器与服务的实现 TProcessor processor = new HelloService.Processor( new MyHelloService()); //创建服务端对象 TServer server = new TThreadPoolServer(processor, serverTransport, new TTransportFactory(), factory); Console.WriteLine( "服务端正在监听8080端口" ); } catch (TTransportException ex) //捕获异常信息 { //打印异常信息 Console.WriteLine(ex.Message); } } } } |
创建客户端实现代码,调用HelloThrift.client访问服务端的逻辑实现,代码如下:
清单3.HelloThrift.Client
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 54 55 56 57 58 59 60 61 62 63 64 65 | using System; using HelloThrift.Interface; using Thrift.Protocol; using Thrift.Transport; namespace HelloThrift.Client { class Program { static void Main( string [] args) { try { //设置服务端端口号和地址 TTransport transport= new TSocket( "localhost" ,8080); transport.Open(); //设置传输协议为二进制传输协议 TProtocol protocol= new TBinaryProtocol(transport); //创建客户端对象 HelloService.Client client= new HelloService.Client(protocol); //调用服务端的方法 Console.WriteLine(client.HelloString( "HelloThrift" )); Console.ReadKey(); } catch (TTransportException e) { Console.WriteLine(e.Message); } } } } |
上面的代码调用了服务端的HelloString方法,服务端也会返回传入的传输值,客户端将服务端返回的数据打印出来。好了完成了代码以后,修改一下程序的启动项顺序,右键选择解决防范,启动项目,选择多启动项目,将Client和Server设置为启动。F5启动程序,见证奇迹的时刻到了,那么到底客户端会不会打印出来结果呢?
客户端调用结果
服务端显示请求结果
关于其他的方法本文就不再演示了,调用起来都是一样,只有那个返回值为null的方法客户端会抛异常,因为C#不知道这个是什么玩意。其他的方法返回值都是正常。下面简单讨论一下框架的通信原理。(就是复制粘贴)
四、深入挖掘
那么问题来了,挖掘技术哪家强,中国北京找知网。
Thrift 包含一个完整的堆栈结构用于构建客户端和服务器端。下图描绘了 Thrift 的整体架构。
1、架构图
如图所示,图中黄色部分是用户实现的业务逻辑,褐色部分是根据 Thrift 定义的服务接口描述文件生成的客户端和服务器端代码框架,红色部分是根据 Thrift 文件生成代码实现数据的读写操作。红色部分以下是 Thrift 的传输体系、协议以及底层 I/O 通信,使用 Thrift 可以很方便的定义一个服务并且选择不同的传输协议和传输层而不用重新生成代码。
Thrift 服务器包含用于绑定协议和传输层的基础架构,它提供阻塞、非阻塞、单线程和多线程的模式运行在服务器上,但是由于C#语言的限制,C#实现的服务端没有非阻塞模式,可以配合winform/webform调用。
服务端和客户端具体的调用流程如下:
该图所示是HelloServiceServer启动的过程以及服务被客户端调用时,服务器的响应过程。从图中我们可以看到,程序调用了TThreadPoolServer的server方法后,server进入阻塞监听状态,其阻塞在TServerSocket的accept方法上。当接收到来自客户端的消息后,服务器发起一个新线程处理这个消息请求,原线程再次进入阻塞状态。在新线程中,服务器通过TBinaryProtocol协议读取消息内容,调用HelloServiceImpl的helloVoid方法,并将结果写入helloVoid_result中传回客户端。
该图所示是HelloServiceClient调用服务的过程以及接收到服务器端的返回值后处理结果的过程。从图中我们可以看到,程序调用了Hello.Client的helloVoid方法,在helloVoid方法中,通过send_helloVoid方法发送对服务的调用请求,通过recv_helloVoid方法接收服务处理请求后返回的结果。
2、数据类型
Thrift 脚本可定义的数据类型包括以下几种类型:
基本类型:
bool:布尔值,true 或 false,对应 C#的 bool
byte:8 位有符号整数,对应 C#的 byte
i16:16 位有符号整数,对应 C#的 short
i32:32 位有符号整数,对应 C#的 int
i64:64 位有符号整数,对应 C#的 long
double:64 位浮点数,对应 C#的 double
string:未知编码文本或二进制字符串,对应 C#的 string
结构体类型:
struct:定义公共的对象,类似于 C 语言中的结构体定义,在 C#中是一个实体类
容器类型:
list:对应 C#的 List 有序集合
set:对应 C#的 HashSet无序但是不能重复的集合
map:对应 C#的 Dictionary键值对集合,键不能重复
异常类型:
exception:对应 C#的 Exception
服务类型:
service:对应服务的类
3、协议
Thrift可以让用户选择客户端与服务端之间传输通信协议的类别,在传输协议上总体划分为文本(text)和二进制(binary)传输协议,为节约带宽,提高传输效率,一般情况下使用二进制类型的传输协议为多数,有时还会使用基于文本类型的协议,这需要根据项目/产品中的实际需求。常用协议有以下几种:BinaryProtocol——二进制编码格式进行数据传输使,TCompactProtocol——高效率的、密集的二进制编码格式进行数据传输,TJSONProtocol——使用JSON 的数据编码协议进行数据传输。
4、传输层
TSocket —— 使用阻塞式 I/O 进行传输,是最常见的模式,由于C#语言的限制无法使用非阻塞同步传输和非阻塞异步传输的方式
5、服务端类型
TSimpleServer —— 单线程服务器端使用标准的阻塞式 I/O,TThreadPoolServer —— 多线程服务器端使用标准的阻塞式 I/O。由于C#语言的限制无法使用非阻塞的多线程服务端。一般开发使用阻塞式多线程服务端即可。再来一发
通过小试牛刀初步感受了thrift框架的魅力,通过简单的么分析了解了thrift框架的基本原理,那么如何结合三层架构进行复杂的应用呢?那么我们就再来一发,使用三层架构+thrift+web form进行CRUD,这次客户端不再是控制台程序而是web应用程序进行调用。
首先还是做好准备工作,定义好此次案例需要的脚本,定义好的脚本如下所示:
清单4.User.Thrift
上面的脚本定义了一个名为User的实体,这个实体有分别有五个属性,服务定义了7个操作方法。由于thrift框架没有对datetime类型数据的定义,只能用string类型进行代替。那么根据要求创建好实体类和服务接口,在解决方案中导入需要的各种文件并创建好三层架构。具体创建后程序结构如下图所示,由于篇幅原因在此就不在赘诉如何导入文件以及创建三层框架,详细的过程请参考源代码,本例着重介绍如何在web客户端调用服务端的方法对远程的数据进行操作。
搭建好框架以后,需要在客户端配置文件中指定服务端的地址和端口号,具体配置如下
1 2 3 4 5 6 7 8 9 10 11 |
|
制定好配置文件信息以后,在ThriftClinetFactory类中定义好客户端实现在代码,如下所示
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | using System; using System.Configuration; using Thrift.Protocol; using Thrift.Transport; using ThriftApp.Common; namespace ThriftApp.WebClient { public static class ThriftClinetFactory { public static UserService.Client CreateThriftClient() { //获取配置文件中的主机地址 string hostName = ConfigurationManager.AppSettings[ "host" ]; //获取服务端端口号 int port = Convert.ToInt32(ConfigurationManager.AppSettings[ "port" ]); //设置传输端口和服务器地址 TTransport transport = new TSocket(hostName, port); //设置传输协议为二进制协议 TProtocol protocol = new TBinaryProtocol(transport); //创建客户端对象并绑定传输协议 UserService.Client client = new UserService.Client(protocol); //打开传输端口 transport.Open(); //返回客户端对象 return client; } } } |
那么定义好客户端创建的工厂类以后,我们就可以直接通过工厂来创建客户端对象,对服务端的方法进行调用。在web服务端创建一个webform页面,命名为Test,在前台页面中拖入一个Repeater控件,并定义好模板,具体代码如下
设置一下解决方案的启动项,将web客户端和服务端同时设置为启动项,F5启动以后就会看到如下结果
更具具体的增删改查操作,请参考源码。另附无刷新CRUD结果图
本文主要参考引用的资料
Apache Thrift - 可伸缩的跨语言服务开发框架http://www.ibm.com/developerworks/cn/java/j-lo-apachethrift/