转载自http://shift-alt-ctrl.iteye.com/blog/1987416

Apache Thrift是一个跨语言的服务框架,本质上为RPC,同时具有序列化、发序列化机制;当我们开发的service需要开放出去的时候,就会遇到跨语言调用的问题,JAVA语言开发了一个UserService用来提供获取用户信息的服务,如果服务消费端有PHP/Python/C++等,我们不可能为所有的语言都适配出相应的调用方式,有时候我们会很无奈的使用Http来作为访问协议;但是如果服务消费端不能使用HTTP,而且更加倾向于以操作本地API的方式来使用服务,那么我们就需要Thrift来提供支持.

不过,如果你的JAVA服务,并没有跨语言调用的需求,那么使用thrift作为RPC框架,似乎不是最好的选择,不管thrift是否性能优越,但是它使用起来确实没有类似于Hessian/CXF等那样便捷和易于使用.

本文以UserService为例,描述一下使用thrift的方式,以及其原理..

一. service.thrift

  1. struct User{
  2. 1:i64 id,
  3. 2:string name,
  4. 3:i64 timestamp,
  5. 4:bool vip
  6. }
  7. service UserService{
  8. User getById(1:i64 id)
  9. }

你可以将自己的JAVA服务通过".thrift"文件描述出来,并提供给服务消费端,那么消费端即可以生成自己的API文件..Thrift框架目前已经支持大部分主流的语言,.需要注意,因为Thrift考虑到struct/service定义需要兼容多种语言的"风格",所以它只支持一些基本的数据类型(比如i32,i64,string等),以及service定义的方法不能重名,即使参数列表不同.(并不是所有的语言都能像JAVA一样支持重载)

二. 生成API文件

首先下载和安装thrift客户端,比如在windows平台下,下载thrift.exe,不过此处需要提醒,不同的thrift客户端版本生成的API可能不兼容.本例使用thrift-0.9.0.exe;通过"--gen"指定生成API所适配的语言.本实例为生成java客户端API.

  1. //windows平台下,将API文件输出在service目录下(此目录需要存在)
  2. > thrift.exe --gen java -o service service.thrift

需要明确的是:Thrift和其他RPC框架不同,thrift在生成的API文件中,已经描述了"调用过程"(即硬编码),而不是像其他RPC那样在运行时(runtime)动态解析方法调用或者参数.

三. UserService实现类

  1. public class UserServiceImpl implements UserService.Iface {
  2. @Override
  3. public User getById(long id){
  4. System.out.println("invoke...id:" + id);
  5. return new User();//for test
  6. }
  7. }

实现类,需要放在Thrift server端.

四.原理简析

1. User.java: thrift生成API的能力还是非常的有限,比如在struct中只能使用简单的数据类型(不支持Date,Collection<?>等),不过我们能从User中看出,它生成的类实现了"Serializable"接口和"TBase"接口.

其中Serializable接口表明这个类的实例是需要序列化之后在网络中传输的,为了不干扰JAVA本身的序列化和反序列化机制,它还重写了readObject和writeObject方法.不过这对thrift本身并没有帮助.

TBase接口是thrift序列化和反序列化时使用的,它的两个核心方法:read和write.在上述的thrift文件中,struct定义的每个属性都有一个序号,比如:1:id,那么thrift在序列化时,将会根据序号的顺序依次将属性的"名称 + 值"写入inputStream中,反序列化也是如此.(具体参见read和write的实现).

  1. //read方法逐个读取字段,按照"索引",最终将"struct"对象封装完毕.
  2. //write方法也非常类似,按照"索引"顺序逐个输出到流中.
  3. while (true){
  4. schemeField = iprot.readFieldBegin();
  5. if (schemeField.type == org.apache.thrift.protocol.TType.STOP) {
  6. break;
  7. }
  8. switch (schemeField.id) {
  9. case 1: // ID
  10. if (schemeField.type == org.apache.thrift.protocol.TType.I32) {
  11. struct.id = iprot.readI32();
  12. struct.setIdIsSet(true);
  13. } else {
  14. org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);
  15. }
  16. break;
  17. case 2: // NAME
  18. ..
  19. }
  20. }

因为thrift的序列化和反序列化实例数据时,是根据"属性序号"进行,这可以保证数据在inputstream和outputstream中顺序是严格的,此外每个struct中"序号"不能重复,但是可以不需要从"1"开始.如果"序号"有重复,将导致无法生成API文件.这一点也要求API开发者,如果更改了thrift文件中的struct定义,需要重新生成客户端API,否则服务将无法继续使用(可能报错,也可能数据错误).thrift序列化/反序列化的过程和JAVA自带的序列化机制不同,它将不会携带额外的class结构,此外thrift这种序列化机制更加适合网络传输,而且性能更加高效.

2. UserService.Client:  在生成的UserService中,有个Client静态类,这个类就是一个典型的代理类,此类已经实现了UserService的所有方法.开发者需要使用Client类中的API方法与Thrift server端交互,它将负责与Thrift server的Socket链接中,发送请求和接收响应.

需要注意的时,每次Client方法调用,都会在一个Socket链接中进行,这就意味着,在使用Client消费服务之前,需要和Thrift server建立有效的TCP链接.(稍后代码示例)

1) 发送请求:

  1. //参见:TServiceClient
  2. //API方法调用时,发送请求数据流
  3. protected void sendBase(String methodName, TBase args) throws TException {
  4. oprot_.writeMessageBegin(new TMessage(methodName, TMessageType.CALL, ++seqid_));//首先写入"方法名称"和"seqid_"
  5. args.write(oprot_);//序列化参数
  6. oprot_.writeMessageEnd();
  7. oprot_.getTransport().flush();
  8. }
  9. protected void receiveBase(TBase result, String methodName) throws TException {
  10. TMessage msg = iprot_.readMessageBegin();//如果执行有异常
  11. if (msg.type == TMessageType.EXCEPTION) {
  12. TApplicationException x = TApplicationException.read(iprot_);
  13. iprot_.readMessageEnd();
  14. throw x;
  15. }//检测seqid是否一致
  16. if (msg.seqid != seqid_) {
  17. throw new TApplicationException(TApplicationException.BAD_SEQUENCE_ID, methodName + " failed: out of sequence response");
  18. }
  19. result.read(iprot_);//反序列化
  20. iprot_.readMessageEnd();
  21. }

Thrift提供了简单的容错方式:每次方法调用,都会在Client端标记一个seqid,这是一个自增的本地ID,在TCP请求时将此seqid追加到流中,同时Server端响应时,也将此seqid原样返回过来;这样客户端就可以根据此值用来判断"请求--响应"是对应的,如果出现乱序,将会导致此请求以异常的方式结束.

2) 响应

  1. //参考: TBaseProcessor.java
  2. @Override
  3. public boolean process(TProtocol in, TProtocol out) throws TException {
  4. TMessage msg = in.readMessageBegin();
  5. ProcessFunction fn = processMap.get(msg.name);//根据方法名,查找"内部类"
  6. if (fn == null) {
  7. TProtocolUtil.skip(in, TType.STRUCT);
  8. in.readMessageEnd();
  9. TApplicationException x = new TApplicationException(TApplicationException.UNKNOWN_METHOD, "Invalid method name: '"+msg.name+"'");
  10. out.writeMessageBegin(new TMessage(msg.name, TMessageType.EXCEPTION, msg.seqid));
  11. x.write(out);//序列化响应结果,直接输出
  12. out.writeMessageEnd();
  13. out.getTransport().flush();
  14. return true;
  15. }
  16. fn.process(msg.seqid, in, out, iface);
  17. return true;
  18. }

thrift生成的UserService.Processor类,就是server端用来处理请求过程的"代理类";server端从socket中读取请求需要调用的"方法名" +参数列表,并交付给Processor类处理;和其他的RPC调用不同的时,thrift并没有使用类似于"反射机制"的方式来调用方法,而是将UserService的每个方法生成一个"内部类":

  1. public static class getById<I extends Iface> extends org.apache.thrift.ProcessFunction<I, getById_args> {
  2. public getById() {
  3. super("getById");//其中getById为标识符
  4. }
  5. public getById_args getEmptyArgsInstance() {
  6. return new getById_args();
  7. }
  8. protected boolean isOneway() {
  9. return false;
  10. }
  11. //实际处理方法
  12. public getById_result getResult(I iface, getById_args args) throws org.apache.thrift.TException {
  13. getById_result result = new getById_result();
  14. result.success = iface.getById(args.id);
  15. return result;
  16. }
  17. }

这个"内部类",将会在Processor初始化的时候,放入到一个map中,此后即可以通过"方法名"查找,然后调用其"getResult"方法了.

  1. public static class Processor<I extends Iface> extends org.apache.thrift.TBaseProcessor<I> implements org.apache.thrift.TProcessor {
  2. public Processor(I iface) {
  3. super(iface, getProcessMap(new HashMap<String, org.apache.thrift.ProcessFunction<I, ? extends org.apache.thrift.TBase>>()));
  4. }
  5. protected Processor(I iface, Map<String,  org.apache.thrift.ProcessFunction<I, ? extends  org.apache.thrift.TBase>> processMap) {
  6. super(iface, getProcessMap(processMap));
  7. }
  8. private static <I extends Iface> Map<String,  org.apache.thrift.ProcessFunction<I, ? extends  org.apache.thrift.TBase>> getProcessMap(Map<String,  org.apache.thrift.ProcessFunction<I, ? extends  org.apache.thrift.TBase>> processMap) {
  9. //放入map
  10. processMap.put("getById", new getById());
  11. return processMap;
  12. }
  13. ....
  14. }

    3) Server端Socket管理和执行策略

  1. TThreadPoolServer
  2. public void serve() {
  3. try {
  4. //启动服务
  5. serverTransport_.listen();
  6. } catch (TTransportException ttx) {
  7. LOGGER.error("Error occurred during listening.", ttx);
  8. return;
  9. }
  10. // Run the preServe event
  11. if (eventHandler_ != null) {
  12. eventHandler_.preServe();
  13. }
  14. stopped_ = false;
  15. setServing(true);
  16. //循环,直到被关闭
  17. while (!stopped_) {
  18. int failureCount = 0;
  19. try {
  20. //accept客户端Socket链接,
  21. //对于每个新链接,将会封装成runnable,并提交给线程或者线程池中运行.
  22. TTransport client = serverTransport_.accept();
  23. WorkerProcess wp = new WorkerProcess(client);
  24. executorService_.execute(wp);
  25. } catch (TTransportException ttx) {
  26. if (!stopped_) {
  27. ++failureCount;
  28. LOGGER.warn("Transport error occurred during acceptance of message.", ttx);
  29. }
  30. }
  31. }
  32. //....
  33. }

Thrift Server端,设计思路也非常的直接...当前Service server启动之后,将会以阻塞的方式侦听Socket链接(代码参考TThreadPoolServer),每建立一个Socket链接,都会将此Socket经过封装之后,放入线程池中,本质上也是一个Socket链接对应一个Worker Thread.这个Thread只会处理此Socket中的所有数据请求,直到Socket关闭.

  1. //参考:WorkerProcess
  2. while (true) {
  3. if (eventHandler != null) {
  4. eventHandler.processContext(connectionContext, inputTransport, outputTransport);
  5. }
  6. if(stopped_ || !processor.process(inputProtocol, outputProtocol)) {
  7. break;
  8. }
  9. }

当有Socket链接不是很多的时候,TThreadPoolServer并不会有太大的性能问题,可以通过指定ThreadPool中线程的个数进行简单的调优..如果Socket链接很多,我们只能使用TThreadedSelectorServer来做支撑,TThreadedSelectorServer内部基于NIO模式,具有异步的特性,可以极大的提升server端的并发能力;不过在绝大多数情况下,在thrift中使用"异步"似乎不太容易让人接受,毕竟这意味着Client端需要阻塞,并且在高并发环境中这个阻塞时间是不可控的.但SelecorServer确实可以有效的提升Server的并发能力,而且在一定程度上可以提升吞吐能力,这或许是我们优化Thrift Server比较可靠的方式之一.

3. Client端代码示例

  1. public class UserServiceClient {
  2. public void startClient() {
  3. TTransport transport;
  4. try {
  5. transport = new TSocket("localhost", 1234);
  6. TProtocol protocol = new TBinaryProtocol(transport);
  7. UserService.Client client = new UserService.Client(protocol);
  8. transport.open();
  9. User user = client.getById(1000);
  10. ////
  11. transport.close();
  12. } catch (TTransportException e) {
  13. e.printStackTrace();
  14. } catch (TException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. }

4. Server端代码示例 

  1. public class Server {
  2. public void startServer() {
  3. try {
  4. TServerSocket serverTransport = new TServerSocket(1234);
  5. UserService.Processor process = new Processor(new UserServiceImpl());
  6. Factory portFactory = new TBinaryProtocol.Factory(true, true);
  7. Args args = new Args(serverTransport);
  8. args.processor(process);
  9. args.protocolFactory(portFactory);
  10. TServer server = new TThreadPoolServer(args);
  11. server.serve();
  12. } catch (TTransportException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. }

到这里,你就会发现,一个service,需要server端启动一个ServerSocket,如果你有很多service,那么你需要让这些service尽可能的分布在不同的物理server上,否则一个物理server上运行太多的ServerSocket进程并不是一件让人愉快的事情. 或者你让几个service整合成一个.

问题总没有想象的那么简单,其实service被拆分的粒度越细,越容易被部署和扩展,对于负载均衡就更加有利.如何让一个service分布式部署,稍后再继续分享.

总结:

1) thrift文件定义struct和serivice API,此文件可以被其他语言生成API文件或者类文件.

2) 使用thrift客户端生成API文件

3) JAVA服务端(即服务提供端),实现service功能.

4) 服务端将server发布成一个Thrift server: 即将service嵌入到一个serverSocket中.

5) 客户端启动Socket,并和Thrift server建立TCP连接.并使用Client代理类操作远程接口.

[转载] Thrift原理简析(JAVA)的更多相关文章

  1. Java Android 注解(Annotation) 及几个常用开源项目注解原理简析

    不少开源库(ButterKnife.Retrofit.ActiveAndroid等等)都用到了注解的方式来简化代码提高开发效率. 本文简单介绍下 Annotation 示例.概念及作用.分类.自定义. ...

  2. Java Annotation 及几个常用开源项目注解原理简析

    PDF 版: Java Annotation.pdf, PPT 版:Java Annotation.pptx, Keynote 版:Java Annotation.key 一.Annotation 示 ...

  3. JDK框架简析--java.lang包中的基础类库、基础数据类型

    题记 JDK.Java Development Kit. 我们必须先认识到,JDK不过,不过一套Java基础类库而已,是Sun公司开发的基础类库,仅此而已,JDK本身和我们自行书写总结的类库,从技术含 ...

  4. PHP的错误报错级别设置原理简析

    原理简析 摘录php.ini文件的默认配置(php5.4): ; Common Values: ; E_ALL (Show all errors, warnings and notices inclu ...

  5. Spring系列.@EnableRedisHttpSession原理简析

    在集群系统中,经常会需要将Session进行共享.不然会出现这样一个问题:用户在系统A上登陆以后,假如后续的一些操作被负载均衡到系统B上面,系统B发现本机上没有这个用户的Session,会强制让用户重 ...

  6. SIFT特征原理简析(HELU版)

    SIFT(Scale-Invariant Feature Transform)是一种具有尺度不变性和光照不变性的特征描述子,也同时是一套特征提取的理论,首次由D. G. Lowe于2004年以< ...

  7. 基于IdentityServer4的OIDC实现单点登录(SSO)原理简析

    写着前面 IdentityServer4的学习断断续续,兜兜转转,走了不少弯路,也花了不少时间.可能是因为没有阅读源码,也没有特别系统的学习资料,相关文章很多园子里的大佬都有涉及,有系列文章,比如: ...

  8. 动态代理 原理简析(java. 动态编译,动态代理)

    动态代理: 1.动态编译 JavaCompiler.CompilationTask 动态编译想理解自己查API文档 2.反射被代理类 主要使用Method.invoke(Object o,Object ...

  9. Android热补丁技术—dexposed原理简析(手机淘宝采用方案)

    上篇文章<Android无线开发的几种常用技术>我们介绍了几种android移动应用开发中的常用技术,其中的热补丁正在被越来越多的开发团队所使用,它涉及到dalvik虚拟机和android ...

随机推荐

  1. cors解决ajax请求跨域问题

    Access-Control-Allow-Origin: * 适用tomcat部署的项目 在web.xml里添加以下内容 <filter> <filter-name>CorsF ...

  2. ZOJ2105 终于找到错误

    ZOJ2105:点击打开链接 错误代码 #include<stdio.h> #include<stdlib.h> int q[110]; int main() { int a, ...

  3. Linux上mysql的安装与配置

    前言 在我们使用Linux的过程中,可能会使用到数据库.那么,数据库的安装与配置就是我们需要掌握的了~所以呢,这篇博客小编就来给大家唠唠数据库的安装与配置. 说到编译安装,小编脑海里浮现的第一个方法就 ...

  4. Elasticsearch分片、副本与路由(shard replica routing)

    本文讲述,如何理解Elasticsearch的分片.副本和路由策略. 1.预备知识 1)分片(shard) Elasticsearch集群允许系统存储的数据量超过单机容量,实现这一目标引入分片策略sh ...

  5. Python和SQL 2017的强大功能

    Python和SQL Server 2017的强大功能   原文来自:https://www.red-gate.com/simple-talk/sql/sql-development/power-py ...

  6. js 查询 添加 删除 练习

    <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title&g ...

  7. 【ASP.NET MVC 学习笔记】- 08 URL Routing

    本文参考:http://www.cnblogs.com/willick/p/3343105.html 1.URL Routing告诉MVC如何正确的定位Controller和Action. 2.URL ...

  8. ELK系列~nxlog实现多位置文件的收集

    前几天我写了几篇关于ELK日志收集,存储和分析的文章: ELK系列~NLog.Targets.Fluentd到达如何通过tcp发到fluentd ELK系列~Nxlog日志收集加转发(解决log4日志 ...

  9. Vue 国际化 vue-i18n 用法详解

    vue-i18n 仓库地址:https://github.com/kazupon/vue-i18n 兼容性: 支持 Vue.js 2.x 以上版本 安装方法:(此处只演示 npm) npm insta ...

  10. C++运算符重载(10)

    编译器在默认情况下为每个类生成一个默认的赋值操作,用于同类的两个对象之间相互赋值.默认的含义是逐个为成员赋值,即将一个对象的成员的值赋给另一个对象相应的成员,这种赋值方式对于有些类可能是不正确的. 运 ...