当前位置: 代码迷 >> 综合 >> 架构设计:系统间通信(11)——RPC实例Apache Thrift 上篇
  详细解决方案

架构设计:系统间通信(11)——RPC实例Apache Thrift 上篇

热度:29   发布时间:2024-01-15 14:07:26.0

1、概述

通过上一篇文章《架构设计:系统间通信(10)——RPC的基本概念》的介绍,相信读者已经理解了基本的RPC概念。为了加深这个理解,后面几篇文章我将详细讲解一款典型的RPC规范的实现Apache Thrift。Apache Thrift的介绍一共分为三篇文章,上篇讲解Apache Thrift的基本使用;中篇讲解Apache Thrift的工作原理(主要围绕Apache Thrift使用的消息格式封装、支持的网络IO模型和它的客户端请求处理方式);下篇对Apache Thrift的不足进行分析,并基于Apache Thrift实现一个自己设计的RPC服务治理的管理方案。这样对我们后续理解Dubbo的服务治理方式会有很好的帮助作用。

2、基本知识

Thrift最初由facebook开发用做系统内各语言之间的RPC框架 。2007年由facebook贡献到apache基金 ,08年5月进入apache孵化器 ,称为Apache Thrift。和其他RPC实现相比,Apache Thrift主要的有点是:支持的语言多(C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk等多种语言)、并发性能高(还记得上篇文章中,我们提到的影响RPC性能的几个关键点吗?)。

为了支持多种语言,Apache Thrift有一套自己的接口定义语言,并且通过Apache Thrift的代码生成程序,能够生成各种编程语言的代码。这样是保证各种语言进行通讯的前提条件。为了能够实现简单的Apache Thrift实例,首先我们就需要讲解一下Apache Thrift的IDL。

2-1、Thrift代码生成程序安装

如果您是在windows环境下运行进行Apache Thrift的试验,那么您无需安装任何工具,直接下载Apache Thrift在windows下的代码生成程序http://www.apache.org/dyn/closer.cgi?path=/thrift/0.9.3/thrift-0.9.3.exe(在这篇文章写作时,使用的是Apache Thrift的0.9.3版本);如果您运行在Linux系统下,那么下载http://www.apache.org/dyn/closer.cgi?path=/thrift/0.9.3/thrift-0.9.3.tar.gz,并进行编译、安装(过程很简单,这里就不再赘述了)。安装后记得添加运行位置到环境变量中。

2-2、IDL格式概要

以下是一个简单的IDL文件定义:

# 命名空间的定义 注意‘java’的关键字
namespace java testThrift.iface# 结构体定义
struct Request {1:required string paramJSON;2:required string serviceName;
}# 另一个结构体定义
struct Reponse {1:required  RESCODE responeCode;2:required  string responseJSON;
}# 异常描述定义
exception ServiceException {1:required EXCCODE exceptionCode;2:required string exceptionMess;
}# 枚举定义
enum RESCODE {_200=200;_500=500;_400=400;
}# 另一个枚举
enum EXCCODE {PARAMNOTFOUND = 2001;SERVICENOTFOUND = 2002;
}# 服务定义
service HelloWorldService {Reponse send(1:Request request) throws (1:ServiceException e);
}

以上IDL文件是可以直接用来生成各种语言的代码的。下面给出常用的各种不同语言的代码生成命令:

# 生成java
thrift-0.9.3 -gen java ./demoHello.thrift# 生成c++
thrift-0.9.3 -gen cpp ./demoHello.thrift# 生成php
thrift-0.9.3 -gen php ./demoHello.thrift# 生成node.js
thrift-0.9.3 -gen js:node ./demoHello.thrift# 生成c#
thrift-0.9.3 -gen csharp ./demoHello.thrift# 您可以通过以下命令查看生成命令的格式
thrift-0.9.3 -help

2-2-1、基本类型

基本类型就是:不管哪一种语言,都支持的数据形式表现。Apache Thrift中支持以下几种基本类型:

  • bool: 布尔值 (true or false), one byte
  • byte: 有符号字节
  • i16: 16位有符号整型
  • i32: 32位有符号整型
  • i64: 64位有符号整型
  • double: 64位浮点型
  • string: 字符串/字符数组
  • binary: 二进制数据(在java中表现为java.nio.ByteBuffer)

2-2-2、struct结构

在面向对象语言中,表现为“类定义”;在弱类型语言、动态语言中,表现为“结构/结构体”。定义格式如下:

struct <结构体名称> {<序号>:[字段性质] <字段类型> <字段名称> [= <默认值>] [;|,]
}

实例:

struct Request {1:required binary paramJSON;2:required string serviceName3:optional i32 field1 = 0;4:optional i64 field2,5: list<map<string , string>> fields3
}
  • 结构体名称:可以按照您的业务需求,给定不同的名称(区分大小写)。但是要注意,一组IDL定义文件中结构体名称不能重复,且不能使用IDL已经占用的关键字(例如required 、struct 等单词)。

  • 序号:序号非常重要。正整数,按照顺序排列使用。这个属性在Apache Thrift进行序列化的时候被使用。

  • 字段性质:包括两种关键字:required 和 optional,如果您不指定,那么系统会默认为required。required表示这个字段必须有值,并且Apache Thrift在进行序列化时,这个字段都会被序列化;optional表示这个字段不一定有值,且Apache Thrift在进行序列化时,这个字段只有有值的情况下才会被序列化。

  • 字段类型:在struct中,字段类型可以是某一个基础类型,也可以是某一个之前定义好的struct,还可以是某种Apache Thrift支持的容器(set、map、list),还可以是定义好的枚举。字段的类型是必须指定的。

  • 字段名称:字段名称区分大小写,不能重复,且不能使用IDL已经占用的关键字(例如required 、struct 等单词)。

  • 默认值:您可以为某一个字段指定默认值(也可以不指定)。

  • 结束符:在struct中,支持两种结束符,您可以使用“;”或者“,”。当然您也可以不使用结束符(Apache Thrift代码生成程序,会自己识别到)

2-2-3、containers集合/容器

Apache Thrift支持三种类型的容器,容器在各种编程语言中普遍存在:

  • list< T >:有序列表(JAVA中表现为ArrayList),T可以是某种基础类型,也可以是某一个之前定义好的struct,还可以是某种Apache Thrift支持的容器(set、map、list),还可以是定义好的枚举。有序列表中的元素允许重复。

  • set< T >:无序元素集合(JAVA中表现为HashSet),T可以是某种基础类型,也可以是某一个之前定义好的struct,还可以是某种Apache Thrift支持的容器(set、map、list),还可以是定义好的枚举。无序元素集合中的元素不允许重复,一旦重复后一个元素将覆盖前一个元素。

  • map

2-2-4、enmu枚举

enum <枚举名称> {<枚举字段名> = <枚举值>[;|,]
}

示例如下:

enum RESCODE {_200=200;_500=500;_400=400;
}

2-2-5、常量定义

Apache Thrift允许定义常量。常量的关键字为“const”,常量的类型可以是Apache Thrift的基础类型,也可以是某一个之前定义好的struct,还可以是某种Apache Thrift支持的容器(set、map、list),还可以是定义好的枚举。示例如下:

const i32 MY_INT_CONST = 111111; const i64 MY_LONG_CONST = 11111122222222333333334444444;const RESCODE MY_RESCODE = RESCODE._200;

2-2-6、exception 异常

Apache Thrift的exception,主要在定义服务接口时使用。其定义方式类似于struct(您可以理解成,把struct关键字换成exception关键字即可),示例如下:

exception ServiceException {1:required EXCCODE exceptionCode;2:required string exceptionMess;
}

2-2-7、service 服务接口

Apache Thrift中最重要的IDL定义之一。在后续的代码生成阶段,通过IDL定义的这些服务将构成Apache Thrift客户端调用Apache Thrift服务端的基本远端过程。service服务接口的定义形式如下所示:

service <服务名称> {<void | 返回指类型> <服务方法名>([<入参序号>:[required | optional] <参数类型> <参数名> ...]) [throws ([<异常序号>:[required | optional] <异常类型> <异常参数名>...])]
}
  • 服务名称:服务名可以按照您的业务需求自行制定,注意服务名是区分大小写的。IDL中服务名称只有两个限制,就是不能重复使用相同的名称,不能使用IDL已经占用的关键字(例如required 、struct 等单词)。

  • 返回值类型:如果这个调用方法没有返回类型,那么可以关键字“void”; 可以是Apache Thrift的基础类型,也可以是某一个之前定义好的struct,还可以是某种Apache Thrift支持的容器(set、map、list),还可以是定义好的枚举。

  • 服务方法名:服务方法名可以根据您的业务需求自定制定,注意区分大小写。在同一个服务中,不能重复使用一个服务方法名命名多个方法(一定要注意),不能使用IDL已经占用的关键字。

  • 服务方法参数:<入参序号>:[required | optional] <参数类型> <参数名>。注意和struct中的字段定义相似,可以指定required或者optional;如果不指定则系统默认为required 。如果一个服务方法中有多个参数名,那么这些参数名称不能重复。

  • 服务方法异常:throws ([<异常序号>:[required | optional] <异常类型> <异常参数名>。throws关键字是服务方法异常定义的开始点。在throws关键字后面,可以定义1个或者多个不同的异常类型。

Apache Thrift服务定义的示例如下:

service HelloWorldService {Reponse send(1:Request request) throws (1:ServiceException e);
}

2-2-8、namespace命名空间

Apache Thrift支持为不同语言制定不同的命名空间:

namespace java testThrift.ifacenamespace php testThrift.ifacenamespace cpp testThrift.iface

2-2-9、注释

Apache Thrift 支持多种风格的注释。这是为了适应不同语言背景的开发者:

/* * 注释方式1: **/// 注释方式2# 注释方式3

2-2-10、include关键字

如果您的整个工程中有多个IDL定义文件(IDL定义文件的文件名可以随便取)。那么您可以使用include关键字,在IDL定义文件A中,引入一个其他的IDL文件:

include "other.thrift"

请注意,一定使用双引号(不要用成中文的双引号咯),并且不使用“;”或者“,”结束符。

以上就是IDL基本的语法了,由于篇幅原因不可能把每种语法、每一个细节都讲到,但是以上的语法要点已经足够您编辑一个适应业务的,灵活的IDL定义了。如果您需要了解更详细的Thrift IDL语法,可以参考官方文档的讲述:http://thrift.apache.org/docs/idl

2-3、最简单的Thrift代码

  • 定义Thrift中业务接口HelloWorldService.Iface的实现:
package testThrift.impl;import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.thrift.TException;import testThrift.iface.HelloWorldService.Iface;
import testThrift.iface.RESCODE;
import testThrift.iface.Reponse;
import testThrift.iface.Request;/*** 我们定义了一个HelloWorldService.Iface接口的具体实现。<br>* 注意,这个父级接口:HelloWorldService.Iface,是由thrift的代码生成工具生成的<br>* 要运行这段代码,请导入maven-log4j的支持。否则修改LOGGER.info方法* @author yinwenjie*/
public class HelloWorldServiceImpl implements Iface {
     /*** 日志*/private static final Log LOGGER = LogFactory.getLog(HelloWorldServiceImpl.class);/*** 在接口定义中,只有一个方法需要实现。<br>* HelloWorldServiceImpl.send(Request request) throws TException <br>* 您可以理解成这个接口的方法接受客户端的一个Request对象,并且在处理完成后向客户端返回一个Reponse对象<br>* Request对象和Reponse对象都是由IDL定义的结构,并通过“代码生成工具”生成相应的JAVA代码。*/@Overridepublic Reponse send(Request request) throws TException {/** 这里就是进行具体的业务处理了。* */String json = request.getParamJSON();String serviceName = request.getServiceName();HelloWorldServiceImpl.LOGGER.info("得到的json:" + json + " ;得到的serviceName: " + serviceName);// 构造返回信息Reponse response = new Reponse();response.setResponeCode(RESCODE._200);response.setResponseJSON("{\"user\":\"yinwenjie\"}");return response;} 
}

各位可以看到,上面一段代码中具体业务和过程和普通的业务代码没有任何区别。甚至这段代码的实现都不知道自己将被Apache Thrift框架中的客户端调用

  • 然后我们开始书写Apache Thrift的服务器端代码:
package testThrift.man;import java.util.concurrent.Executors;import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.BasicConfigurator;
import org.apache.thrift.TProcessor;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TThreadPoolServer;
import org.apache.thrift.server.TThreadPoolServer.Args;
import org.apache.thrift.transport.TServerSocket;import testThrift.iface.HelloWorldService;
import testThrift.iface.HelloWorldService.Iface;
import testThrift.impl.HelloWorldServiceImpl;public class HelloBoServerDemo {
     static {BasicConfigurator.configure();}/*** 日志*/private static final Log LOGGER =LogFactory.getLog(HelloBoServerDemo.class);public static final int SERVER_PORT = 9111;public void startServer() {try {HelloBoServerDemo.LOGGER.info("看到这句就说明thrift服务端准备工作 ....");// 服务执行控制器(只要是调度服务的具体实现该如何运行)TProcessor tprocessor = new HelloWorldService.Processor<Iface>(new HelloWorldServiceImpl());// 基于阻塞式同步IO模型的Thrift服务,正式生产环境不建议用这个TServerSocket serverTransport = new TServerSocket(HelloBoServerDemo.SERVER_PORT);// 为这个服务器设置对应的IO网络模型、设置使用的消息格式封装、设置线程池参数Args tArgs = new Args(serverTransport);tArgs.processor(tprocessor);tArgs.protocolFactory(new TBinaryProtocol.Factory());tArgs.executorService(Executors.newFixedThreadPool(100));// 启动这个thrift服务TThreadPoolServer server = new TThreadPoolServer(tArgs);server.serve();} catch (Exception e) {HelloBoServerDemo.LOGGER.error(e);}}/*** @param args*/public static void main(String[] args) {HelloBoServerDemo server = new HelloBoServerDemo();server.startServer();}
}

以上的代码有几点需要说明:

  1. TBinaryProtocol:这个类代码Apache Thrift特有的一种二进制描述格式。它的特点是传输单位数据量所使用的传输量更少。Apache Thrift还支持多种数据格式,例如我们熟悉的JSON格式。后文我们将详细介绍Apache Thrift中的数据格式。

  2. tArgs.executorService():是不是觉得这个executorService很熟悉,是的这个就是JAVA JDK 1.5+ 后java.util.concurrent包提供的异步任务调度服务接口,Java标准线程池ThreadPoolExecutor就是它的一个实现。

  3. server.serve(),由于是使用的同步阻塞式网络IO模型,所以这个应用程序的主线程执行到这句话以后就会保持阻塞状态了。不过下层网络状态不出现错误,这个线程就会一直停在这里。

另外,同HelloWorldServiceImpl 类中的代码,请使用Log4j。如果您的测试工程里面没有Log4j,请改用System.out。

  • 接下来我们进行最简单的Apache Thrift Client的代码编写:
package testThrift.client;import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.BasicConfigurator;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TSocket;import testThrift.iface.HelloWorldService;
import testThrift.iface.Reponse;
import testThrift.iface.Request;/*** 同样是基于同步阻塞模型的thrift client。* @author yinwenjie*/
public class HelloClient {
     static {BasicConfigurator.configure();}/*** 日志*/private static final Log LOGGER = LogFactory.getLog(HelloClient.class);public static final void main(String[] args) throws Exception {// 服务器所在的IP和端口TSocket transport = new TSocket("127.0.0.1", 9111);TProtocol protocol = new TBinaryProtocol(transport);// 准备调用参数Request request = new Request("{\"param\":\"field1\"}", "\\mySerivce\\queryService");HelloWorldService.Client client = new HelloWorldService.Client(protocol);// 准备传输transport.open();// 正式调用接口Reponse reponse = client.send(request);// 一定要记住关闭transport.close();HelloClient.LOGGER.info("response = " + reponse);}
}
  • Thrift客户端所使用的网络IO模型,必须要与Thrift服务器端所使用的网络IO模型一致。也就是说服务器端如果使用的是阻塞式同步IO模型,那么客户端就必须使用阻塞式同步IO模型。

  • Thrift客户端所使用的消息封装格式,必须要与Thrift服务器端所使用的消息封装格式一直。也就是说服务器端如果使用的是二进制流的消息格式TBinaryProtocol,那么客户端就必须同样使用二进制刘的消息格式TBinaryProtocol。

  • 其它的代码要么就是由IDL定义并由Thrift的代码生成工具生成;要么就不是重要的代码,所以为了节约篇幅就没有必要再贴出来了。以下是运行效果。

  • 服务器端运行效果

这里写图片描述

  • 服务器端收到客户端请求后,取出线程池中的线程进行运行

这里写图片描述

请注意服务器端在收到客户端请求后的运行方式:取出一条线程池中的线程,并且运行这个服务接口的具体实现。接下来我们马上介绍Apache Thrift的工作细节。

(接下文)

  相关解决方案