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, 至此代碼編寫完了, 可是試着運行一下, 體驗異構通信魅力