WhyApacheThrift
因为最近在项目中需要集成进来一个Python编写的机器学习算法,但是我的后端主要使用的是SpringCloud技术栈. 于是面临着异构语言之间的通信实现方式的抉择. 因为业务逻辑是这样的
主要就是实现2-3这部分请求响应, 实现的方式挺多的, 只要有能力甚至将py封装成一个WebServer对外提供服务, 或者是选择使用消息中间件, 但是大部分消息中间的通信模型都是单向的,即发布订阅, 不过也能实现上面的业务需求
项目中一开始的实现其实是像下面这样的, 选择简单粗暴直接使用socket编程实现, py用socket写一个服务端, java用socket实现客户端, 双方之间实现异构通信, 就像下面代码的两段,在本地运行的话双方通信的速度还可以,但是当我将他制作成docket镜像打包发布到线上时, 双方的通信竟然需要9s
一个请求需要九秒钟, 这肯定是不能接受的
InetAddress localhost = InetAddress.getByName("192.168.88.1");
Socket socket = new Socket(localhost.getHostName(), 9999);
OutputStream outputStream = socket.getOutputStream();
InputStream inputStream = socket.getInputStream();
// 向py发送消息
PrintStream out = new PrintStream(outputStream);
// 发送内容
out.print("hi python");
// 获取消息
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String targetName = bufferedReader.readLine();
System.err.println("获取返回的消息 " + targetName);
import socket
import time
# 建立一个服务端
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(('localhost',9999)) #绑定要监听的端口
server.listen(5) #开始监听 表示可以使用五个链接排队
while True:# conn就是客户端链接过来而在服务端为期生成的一个链接实例
conn,addr = server.accept() #等待链接,多个链接的时候就会出现问题,其实返回了两个值
print(conn,addr)
while True:
try:
data = conn.recv(1024) #接收数据
print('recive:',data.decode()) #打印接收到的数据
conn.send(data.upper()) #然后再发送数据
time.sleep(3)
except ConnectionResetError as e:
print('关闭了正在占线的链接!')
break
conn.close()
其实我们现在遇到的大部分问题,前辈们都遇到过,就比如针对这个需求,其实业内的实现可能有很多种, Apache的顶级 项目Thrift就能完美解决这个问题,而且通信的速度,效果, 稳定性都超级理想
Thrift简介
Apache Thrift 是facebook捐献给Apache, 现在它也是 Apace的顶级项目 , Thrift的意为节俭的, 本质上thrift是一种接口描述语言和二进制通信协议 , 设计的初衷就是为了实现跨越语言的服务调用服务, 是一个绝对优秀的RPC框架
目前,Thrift 支持的语言有很多, 诸如: C#、C++(基于POSIX兼容系统)、Cappuccino、Cocoa、Delphi、Erlang、Go、Haskell、Java、Node.js、OCaml、Perl、PHP、Python、Ruby和Smalltalk。
安装
官网下载地址: http://thrift.apache.org/download
下载编译器
为啥使用这个编译器呢? 因为我们真正在开始时, 使用的代码都是使用Thrift自动为我们生成出来的, 说白了, 就是我们只需要根据业务逻辑定义好 .thrift 文件, 通过这个它的编译器编译这个配置文件,并且使用告诉编译器为我们生成什么语言的代码就ok了, Thrift 框架会自动的将双方通信的编解码的逻辑生成在给我们的代码中, 还包含对socket的封装, 整体是一条龙的服务
因为是使用的windows本, 所以下载 .exe 结尾的编译器
添加环境变量
过一会生成代码代码时是在idea中完成的, 故这一步免不了, 不然它会说找不到这个命令
第一个坑: 重命名一下这个编译器, 去掉前面的小版本号再去添加环境变量
第二个坑: 如果在控制台输入thrift显示版本号, 但是idea中不识别, 就重启电脑
Thrift 的架构体系
为啥要看Thrift的架构体系呢? 虽然Thrift会帮我们自动的生成一些模板代码, 但是还是需要自己手动编码客户端和服务端的代码的, 了解了它的架构体系, 再看如果编写服务端和客户端的代码也能看的懂, 在代码中构造的对象在下面的架构体系中都是有迹可循的
如上图,Thrift是典型的CS架构 我们可以将thrift称为 IDL(Interface DescriptionLanguage)语言 , 并且上图中的服务端和客户端可以使用不同的语言进行开发, 那两种不同语言开发的服务端和客户端之间使用什么进行通信由Thrift来实现
- YourCode: 就是我们的自己的业务代码
- FooServiceClient: 用来和服务端通信的客户端对象
- Foo write()/read() : 这是Thrift为我们自动生成的代码, 底层封装了通过socket对数据的传输逻辑
- TProtocol: 协议层, 在这一层中规定了数据传输使用的哪种协议
- TTransport: 传输层: 在这一层中规定了数传输的格式,比如需不需要进行压缩
- Underlying IO : 数据在网络中的IO交互
Thrift的传输协议
这种传输协议实际上就是规定了数据在网络上采用什么样的格式进行传输
- TBinaryProtocol : 以二进制格式进行传输
- TCompactProtocol: 对二进制数据进一步压缩的格式
- TJsonProtocol: json格式
- TSimpleJsonProtocol: 针对Json的只写协议
- TDebugProtocol: 简单易懂的文本格式, 常用于去 调试代码使用
Thrift的数据传输方式
- TSocket : 阻塞式的Socket 效率最低
- TFrameTransport: 在非阻塞应用服务中常用. 以Frame为单位进行传输
- TFileTransport: 以文本格式进行传输
- TMemoryTransport: 使用内存进行IO, 在java中的实现是 ByteArrayOutPutStream
Thrift支持的服务模型
这种服务类型说的就是服务端示例的类型, 有如下几种
- TSimpleServer: 简单的单线程服务模型
- TThreadPoolServer: 虽然是表中的阻塞式IO, 但是采用多线程模型处理
- TNonblockingServer: 多线程服务模型, 使用的是非阻塞IO常和TFramedTransport数据传输方式搭配使用
- THsHaServer: 引用线程池去处理, 采用的是半同步,半异步的模式, 针对不同类型的消息, 进行不同的处理, 比如对IO类型的消息异步处理, 对Handler的RPC远程过程调用进行同步处理
Thrift的数据类型
thrift不支持无符号数据类型
简单的数据类型:
名称 | 简介 |
---|---|
byte | 有符号字节 |
i16 | 16位有符号整数 |
i32 | 32位有符号整数 |
i64 | 64位有符号整数 |
double | 64位浮点数 |
string | 字符串类型 |
thrift的容器类型: (支持泛型)
- list: 表示一系列T类型的数据组成的有序列表, 元素可以重复
- set: 一系列T类型的数据组合成的无序集合, 元素不重复
- map: 一个字典结构, key为K类型, value为V类型,相当于java中的hashmap
结构体: 这个struct类似C语言中的结构体, 初衷也是将不同的数据聚合在一起,方便传输,经过编译器编译完成后其实就是java中的类
struct Student{
1:string name;
2:i32 age;
}
枚举类型
enum Gender{
MALE,
FEMALE
}
异常类型: thrift 支持异常类型表示服务端和客户端之间的通信所可能抛出来的异常, 并且我们可以在service中的方法上throws 异常, 用描述异常出现的时间,异常的类型
exception RequestException{
1:i32 coed
2: string reason
}
服务类型: 服务端和客户端通信使用到的接口 , 就好比java中的接口, 它是一系列方法的集合, thrift 会将service转换成客户端和服务端的框架的代码 , 定义形式如下
service MyService{
string ask(1:string name,2:i32 age)
}
类型定义: 可以像下面这样,使用类似C语言的语法为变量取别名, 转换成我们习惯的命名格式
typedef i32 int
typedef i64 long
常量const类型: thrift 同样支持常量的定义, 使用const关键字:
const string NAME="XXX"
命名空间类型: 关键字是 namespace , thrift的命名空间相当于java中的package, 实际使用上thrift也会将生成的代码放在这下面指定的包中
格式: namespace 语言 路径
实例: namespace java com.changwu.thrift.Demo
文件包含: 同样向C/C++那样,支持文件之间相互包含的操作. 在java中这个动作就是Import
include "global.thrift"
注释: thrift 中的注释有一下几种
// XXXX
# XXX
/*XXX*/
可选和必填的选项, 关键字分别是 required 和 optional, 分别表示对应的字段是可选的还是必填的
struct Student{
1:required string name;
2:optional string age;
}
实战
编写Thrift文件
我们使用上面定义好的thrift文件,去生成我们预期的目标语言的代码
namespace java com.changwu.thrift
namespace py py.thrift
// 去别名字
typedef string String
typedef bool boolean
// 我们通过 .thrift文件 描述对象(struct), 方法(service), 类型, 异常等信息
struct Message {
1: optional String msg,
}
exception MyExcetion{
1:optional String message,
2:optional String callStack,
3:optional String date
}
service PersonService{
Message getResultFromPy(1:required String message) throws(1:MyExcetion e),
}
代码生成
代码生成命令 执行后会在根路径下多出 gen-java的目录,生成的代码也在这里面
命令 thrift --gen 语言 .thrift文件路径
java实例: thrift --gen java src/thrift/data.thrift
py 实例: thrift --gen py src/thrift/data.thrift
执行完这两条命令后我们会得到这样的结果
导入运行时依赖
导入运行时依赖jar包
<!-- https://mvnrepository.com/artifact/org.apache.thrift/libthrift -->
<dependency>
<groupId>org.apache.thrift</groupId>
<artifactId>libthrift</artifactId>
<version>0.13.0</version>
</dependency>
编码
使用java实现客户端, Thrift将客户端的代码生成在 服务类型类中, 也就是我们上面说的那个MessageService中,
通过上面的对Thrift架构的了解我们也能知道, 构建客户端时需要选择 TProtocol 和 TTransport ,并且得和服务端保持一致, 故下面的示例代码也就清晰明了了
// 帮点端口号和超时时间
TTransport tTransport = new TFramedTransport(new TSocket("localhost",9999),600);
TProtocol protocol = new TCompactProtocol(tTransport);
MessageService.Client client = new MessageService.Client(protocol);
try{
// 打开socket
tTransport.open();
// 发送
Message p = client.getResultFromPy("张三123");
System.out.println("结果: "+p.getMsg());
}catch (Exception e){
e.printStackTrace();
}finally {
tTransport.close();
}
py端Server实现
首先在py端, 实现前面定义的接口的信息, 当收到消息时回调的的具体的业务逻辑就在这里定义
class MessageServiceImpl:
def getResultFromPy(self, msg):
print("获取到msg = " + msg)
message = ttypes.Message()
message.msg = 'hello java'
return messag
服务端的编写
服务端和客户端代码的编写其实都是遵循前面所说的Thrift的架构规范的, 其次, 需要使用Idea安装一下thrift与py相关的类库
下载 http://www.apache.org/dyn/closer.cgi?path=/thrift/0.13.0/thrift-0.13.0.tar.gz
在本地解压开, 然后找到里面的py目录, 进入lib 找到py 安装对py的支持
执行命令:
python setup.py install
from thrift.transport import TTransport
from thrift.protocol import TCompactProtocol
from thrift.server import TServer
from py.thrift import MessageService
from MessageServiceImpl import MessageServiceImpl
try:
# handler就是我们对 ThriftService的实现
personServiceHandler = MessageServiceImpl()
# Foo read()/write() 被Thrift生成在 MesageServive中
processor = MessageService.Processor(personServiceHandler)
# TSocket TProtocol 和 Transport 从上面下载的类库导入进来
# 这三者的作用在上面的架构图上也能提现出来
serverSocket = TSocket.TServerSocket(host="127.0.0.1",port=9999)
transportFactory = TTransport.TFramedTransportFactory()
protocolFactory = TCompactProtocol.TCompactProtocolFactory()
# py提供了四种Server , TThreadPoolServer TServer THeaderProtocolFactory
server = TServer.TThreadPoolServer(processor,serverSocket,transportFactory,protocolFactory)
print(">>>>>>>>>>>>>>>>>>服务端启动>>>>>>>>>>>>>>>>>>>>")
server.serve()
except Thrift.TException as ex:
print("%s" % ex.message)
ok, 至此代码编写完了, 可是试着运行一下, 体验异构通信魅力