背景
先来想这么个问题:有个订单服务order部署在A服务器,还有个用户服务user部署在B服务器,现在前端点击下单,在调用A服务器的order服务时,同时order服务器要判断用户信息是否正确。
这时候你可以找到前端小伙伴,告诉他能不能在点下单时先调用B服务器的user服务,然后在返回信息之后调用A服务器的order,前端小伙伴肯定会给你个鄙视的眼神~
现在是在不同的服务器,就像隔河相望,而就算想发消息出去,对方又不能接收。必须得先让B服务器的user服务提供一个接口,才能够外部访问啊。。。
我们今天讲的thrift,就可以很好的解决这个问题~
在讲thrift之前我们不得不先说下RPC
RPC (Remote Procedure Call)
RPC中文翻译过来叫远程过程调用,是计算机的一种通信的协议,该协议允许在不同服务器的进程相互调用。也就是说通过RPC技术,虽然进程不在同一台服务器,但是我可以像调用自身服务一样调用其他服务器上的服务,它会帮我们封装网络编程的细节(网络通信是很复杂的),让我们将更多的精力投入到业务本身上边。
RPC是典型的C/S架构,还记得java的网络编程吗?服务端需要启动ServerSocket对象,调用bind()绑定监听端口,lisener()设置监听队列,然后调用accept方法接收请求。客户端开启一个socket,调用connect()去连接一个serverSocket,调用send()发送数据:
RPC调用过程
执行一次RPC操作则是需要做以下几个操作的(图摘网络):
- 客户端调用程序
- 客户端桩(stub)构建要发送的信息
- 该信息通过网络被发送到服务器端
- 服务器端的操作系统接收到消息,将其交给服务端桩(stub)
- 服务器端桩(stub)解开包,拿到消息内容
- 服务器桩(stub)调用本地的方法
要实现一个完整的RPC是很复杂的,现在网上也有开源可用的RPC工具,它们使用IDL(interface description language)接口定义语言来提供跨平台跨语言的服务调用,thrift就是其中一种,它适用于多种场景,支持不同的语言,在跨语言时保持性能和易用性。
Thrift
thrift是一个跨平台,跨语言的RPC工具,是由facebook开源的一个项目。thrift通过IDL(接口定义语言)来定义RPC的接口和数据类型,然后使用编译器编译成不同语言的接口文件,这个生成的接口文件已经负责了RPC通信的实现了。
我们先想下,面向接口编程中,客户端不清楚服务端是如何实现的,当客户端调用接口方法时,是有具体的实现类去完成功能。我们在此,先将thrift接口信息分别放入客户端和服务端,令服务端实现这个接口,客户端调用这个接口,而这个接口是由thrift生成的,已经封装好了通信协议,那么即使客户端和服务端部署在不同的服务器,也可以正常调用,发送请求。
Demo:
创建thrift接口
说多无用,来个代码(talk is cheap, show me the code)
在写代码之前需要在电脑中安装thrift编译器(这个自行网上搜索即可),安装完后创建一个thrift文件:HiThrift.thrft,内容如下:
namespace java com.lsd.service
service HiThrift{
string sayHello(1:string name)
}
java后边的是包名,创建完成后用thrift命令编译此信息:
这个是生成了java的文件,当然也可以生成其他语言的接口。
会发现在同路径下多了gen-java目录,在目录最后有个HiThrift.java文件,这个文件就是thrift生成的,打开后发现好多内容~
服务端
创建maven项目,引入thrift依赖:
<groupId>org.apache.thrift</groupId>
<artifactId>libthrift</artifactId>
版本自己选择即可。
接下来将生成的HiThrift.java放入工程中(注意包路径对应好),然后创建其实现类
public class ThriftServer {
public static void main(String[] args) {
try {
TProcessor processor = new HiThrift.Processor<HiThrift.Iface>(new HiThriftImpl());
TServerSocket tServerSocket = new TServerSocket(8888);
TBinaryProtocol.Factory factory = new TBinaryProtocol.Factory();
TServer.Args tArgs = new TServer.Args(tServerSocket);
tArgs.processor(processor);
tArgs.protocolFactory(factory);
TServer tServer = new TSimpleServer(tArgs);
tServer.serve();
System.out.println("服务端启动成功~");
} catch (TTransportException e) {
System.out.println(e);
}
}
}
在TServerSocket中我们写了8888,这个是服务启动监听的端口号,之后我们会讲解TProcessor,TBinaryProtocol,TSimpleServer等是啥东东~
右键运行main方法,此程序运行,它现在已经在监听本地的8888端口。
客户端
在本服务中建立一个服务调用方:
public class ThriftClient {
public static void main(String[] args) {
try (TTransport transport = new TSocket("localhost", 8888, 30000)) {
TProtocol protocol = new TBinaryProtocol(transport);
HiThrift.Client client = new HiThrift.Client(protocol);
transport.open();
String result = client.sayHello("张三");
System.out.println(result);
} catch (final Exception e) {
e.printStackTrace();
}
}
}
我们创建了一个TSocket,目的是向localhost的8888端口发送请求,超时时间设定30000ms。
现在就完成了客户端和服务端的编码工作,我们运行下客户端的main()启动程序:
发现在客户端已经输出了结果~
是不是很神奇,客户端没有依赖任何服务实现类,服务端却执行了代码。
Thrift协议
上边我们用一个简单的例子使用了thrift完成了一次RPC调用,但是公司里肯定不会这么用,你想想谁会把ip啥的写到代码里,一般会把ip等信息存放到一个公共的地方,让服务端启动的时候存进去,客户端想调用的时候拿出来,这个地方就叫做服务中心,现在有很多的工具都可以完成项任务,比如说eureka,zookeeper,nacos,redis等等,这服务中心会在另一篇文章中具体介绍,此处不再赘述~
首先看下thrift协议栈(图片来源网络):
- 底层IO,负责数据的网络传输,Socket,File,Zip
- TTansport是以字节流(Byte Stream)方式发送和接收信息,每一个底层IO模块都会有一个对应的TTransport来负责字节流在该IO模块的传输。比如TSocket对应Socket传输,TFileTransport对应文件传输。
- TProtocol是将数据组装成信息,或者将信息读取出来形成数据,即将发送的数据转换成字节流(Data Stream)和将字节流转换成数据(也就是将字节流传给TTransport或者从TTransport中读取成字节数据)
- TServer:接收Client的请求,并将请求转发到Processor进行处理
- TProcessor:对Client的请求做出响应,包括RPC请求转发,调用参数解析和代码逻辑调用,返回值写回等
TTransport
TTransport下边就是底层IO了,所以TTransport要支持底层IO。到了TTransport这一层,数据就是按照字节流(Byte Stream)处理,并不关心数据到底是什么类型,什么内容。
TTransport分为以下几类:
- TSocket:阻塞的TCP Socket进行数据传输
- TServerTransport:服务端接收传输对象
TProtocol
它是把TTransport传过来的字节流(Byte Stream)转换为数据流(Date Stream),它对数据来说就像是一个分水岭,从这个协议往上,数据就是数据,有自己的类型,名称等,从这个协议往底层走,就变成了字节流传输,没有任何的意义了。
在这里用户可以自选协议:
- TBinaryProtocol:二进制格式 (这个用的多)
- **TCompactProtocol **:压缩格式 (这个效率高)
- TJSONProtocol :使用JSON的数据编码协议进行数据传输
- TSimpleJSONProtocol:提供JSON只写协议,生成的文件很容易通过脚本语言解析
- TDebugProtocol:使用易懂的可读的文本格式,以便于debug
TServer
上边讲了TServer是用来处理客户端请求,将请求转发给Processor的。所以TServer的好坏影响着整个流程的性能。thrift对TServer有不同的实现,来解决访问量或多或少的情况:
- TSimpleServer:上述demo中用到的,使用BIO单线程服务器
- TThreadPoolServer:BIO多线程服务器
- TNonBlockingServer:NIO单线程服务器,用少量线程就可以完成高并发请求(必须使用TFramedTransport)
- THsHaServer:它是TNonBlockingServer的子类,它觉得父类是单线程处理,效率不高,所以自己搞了个线程池处理业务
-
TThreadedSelectorServer:这个实现较为复杂,也是TServer里最高级的,它维护了两个线程池,一个用来处理网络I/O,另一个用ExecutorService来进行业务的处理
推荐一篇文章thrift各种TServer实现
TProcessor
走到这一步,相当于数据从thrift框架中拿到了,要调用真正的接口实现类了。
它有个process函数,任何调用都会经过此方法,然后转向特定的地方。
它会解析请求数据(反序列化),解析参数信息,然后调用实现类方法,完成真正的请求,最后将返回值进行包装(序列化),传给TProtocol。
它的逻辑有点像springmvc(自我感觉哈),你看springmvc,不就是所有请求走dipatcherServlet吗,任何请求被它拦截之后,都会调用dispatch()->doDispatch()...
总结:
thrift是一个RPC工具,它封装了网络传输的细节,让我们集中心思到业务逻辑上。让我们可以像调用本地代码一样调用远程服务
thrift内部结构非常清晰,从底层IO一直到最上层的TProcessor,各个层级负责各自的事情,高效
thrift可以让用户根据不同的场景定制不同的处理策略并发量小可以选择TSimpleServer或者TThreadPoolServer,并发量太大则可以选择THsHaServer或者TThreadedSelectorServer,增加了框架的灵活性
还有重要的一点,thrift是跨语言的,不管是java,php,python...都可以实现自己的服务,或者调用不同语言的服务~
到此,我们的thrift解析就over啦,说下开篇的问题,我们可以生成一个thrift接口,在B服务器中添加该接口并在user服务中实现它(当然关于IP和端口这个不可能固定到代码中,它是由服务中心去处理),然后在A服务器的order服务中引入该接口,在调用下单逻辑之前,通过thrift请求到B服务器的user服务,这时就可以正常访问啦~