RPC框架初體驗之Thrift
版本說明:thrfit 0.12.0
模塊說明:
- thrift-demo-java-api: 使用thrift生成Java api
- thrift-demo-java-server: Java 實現Thrift服務端
- thrift-demo-java-client:Java實現Thrift客戶端
- thrift-demo-py-api:使用thrift生成Python api
- thrift-demo-py-server:Python實現Thrift服務端
- thrift-demo-py-client:Python實現Thrift客戶端
1 前言
上一篇文章《RPC框架初體驗之Dubbo》,體驗了阿里開源的RPC框架,該框架體驗還算不錯,業界使用也較多。但是僅支持Java語言,不能進行跨語言。這裏就體驗一款性能不錯,評價不錯,且支持跨語言的RPC框架thrfit。本篇將分別使用Java和Python實現thrift的服務端和客戶端,並進行交叉調用。
2 項目準備
2.1 thrfit 安裝
thrift的安裝方式有好多種,像我在mac環境可以使用brew install thrfit的方式進行安裝,也可以通過源碼編譯的方式進行安裝。同樣在linux環境下,centos可以使用yum,ubantu可以使用apt,當然unix的環境都可以使用源碼編譯的方式安裝。thrfit的也支持window環境的安裝,在官網下載exe二進制的安裝文件進行安裝即可。http://www.apache.org/dyn/closer.cgi?path=/thrift/0.12.0/thrift-0.12.0.exe 。當然,官方也建議我們使用docker去安裝thrift環境,關於thrfit的安裝步驟這裏就不詳細介紹。
2.2 創建項目
先看一下整體的目錄結構
learn-demo-thrift/
├── README.md
├── pom.xml
├── thrfit-demo-java-server
├── thrift
├── thrift-demo-java-api
├── thrift-demo-java-client
├── thrift-demo-py-api
├── thrift-demo-py-client
└── thrift-demo-py-server
關於幾個模塊的功能在文章開頭已經描述過了,另外包塊一個thrift的文件夾,裏面用來存放我們的.thrirft生成文件和生成腳本。
2.2.1 創建一個Maven項目:learn-demo-thrift
這裏直接使用IDEA創建一個Maven項目即可,指定groupId爲learn.demo,指定artifactId爲thrift,指定version爲1.0,項目名稱爲:learn-demo-thrift
[外鏈圖片轉存失敗(img-MsgvI9c9-1566360312305)(https://raw.githubusercontent.com/shirukai/images/master/532984c696fa4701331335e2d56c243f.jpg)]
2.2.2 創建文件夾thrift
在剛纔創建的項目裏,創建一個名稱爲thrift的文件夾,用來保存我們的.thrift生成文件和生成腳本。
2.2.3 創建Maven子模塊:thrift-demo-java-api
在項目裏創建Maven子模塊,指定groupId爲learn.demo,指定artifactId爲thrift-demo-java-api,指定version爲1.0,模塊名稱爲:thrift-demo-java-api
2.2.4 創建Maven子模塊:thrift-demo-java-server
在項目裏創建Maven子模塊,指定groupId爲learn.demo,指定artifactId爲thrift-demo-java-server,指定version爲1.0,模塊名稱爲:thrift-demo-java-server
2.2.5 創建Maven子模塊:thrift-demo-java-client
在項目裏創建Maven子模塊,指定groupId爲learn.demo,指定artifactId爲thrift-demo-java-client,指定version爲1.0,模塊名稱爲:thrift-demo-java-client
2.2.6 創建Python子模塊:thrift-demo-py-api
在項目裏創建Python子模塊,Python環境選擇2.7,模塊名稱爲:thrift-demo-py-api
2.2.7 創建Python子模塊:thrift-demo-py-server
在項目裏創建Python子模塊,Python環境選擇2.7,模塊名稱爲:thrift-demo-py-server
2.2.8 創建Python子模塊:thrift-demo-py-client
在項目裏創建Python子模塊,Python環境選擇2.7,模塊名稱爲:thrift-demo-py-client
[外鏈圖片轉存失敗(img-8rIwP8tV-1566360312307)(https://raw.githubusercontent.com/shirukai/images/master/9cd73f82cf66b7228a942c54dfd4128e.jpg)]
最終項目框架創建完成。
3 Thrift API生成
上文提到,thrirft支持跨語言,它之所以支持跨語言,是因爲它的服務是我們根據.thrift文件生成的,我們只需按照固定的格式,定義好一個thrift服務,然後指定服務語言,就可以把代碼自動生成。這裏就簡單定義一個服務和一個數據類型,並分別生成java和python兩個語言的API。
3.1 定義.thrift文件
在項目下的thrift目錄下創建一個名爲demo.thrift的文件,該文件包括三部分內容:
- namespace: 用來描述生成的語言,以及包路徑,如namespace java learn.demo.thrift.api
- struct: 用來定義數據結構,對應Java裏的實體類
- service: 用來定義服務,裏面包括定義的抽象方法
如下demo.thrift文件,我們定義了兩個namespace,分別爲java和python的,並且定義了一個複雜的數據結構,包括一個int類型的id和一個string類型的name以及一個list<\striung>類型的列表。並且定義了一個service裏面包含了兩個方法。
namespace java learn.demo.thrift.api
namespace py thrift_demo.api
struct DemoInfo{
1:i32 id,
2:string name,
3:list<string> tags
}
service DemoService{
DemoInfo getDemoById(1:i32 id);
void createDemo(1:DemoInfo demo)
}
3.2 創建生成腳本
thrift文件定義好之後,我們就以使用thrift命令進行代碼生成,例如
thrift --gen java -out ../thrift-demo-java-api/src/main/java demo.thrift
–gen 指定生成的語言
–out 指定生成路徑
爲了方便起見,我們直接創建一個名爲gen-code.sh的shell腳本,一次性生成java和python的代碼,如下所示:
#!/usr/bin/env bash
thrift --gen java -out ../thrift-demo-java-api/src/main/java demo.thrift
thrift --gen py -out ../thrift-demo-py-api demo.thrift
3.3 生成代碼
執行gen-code.sh腳本,會在thrift-demo-java-api和thrirft-demo-py-api下生成代碼。如下圖所示,生成的Python代碼
[外鏈圖片轉存失敗(img-Q1EVr1qO-1566360312307)(https://raw.githubusercontent.com/shirukai/images/master/ebd59b411c15bca946abddfb9c2a468a.jpg)]
如下圖所示,生成的Java代碼
3.3.1 thrift-demo-java-api中添加依賴
生成Java代碼之後,我們打開代碼查看
[外鏈圖片轉存失敗(img-8hzOceGR-1566360312308)(https://raw.githubusercontent.com/shirukai/images/master/26dfbb026763856c5a70f1d9738b8073.jpg)]
發現代碼飄紅,原因是我們沒有引入thrift依賴,所以要在pom文件中引入相關依賴。
<dependency>
<groupId>org.apache.thrift</groupId>
<artifactId>libthrift</artifactId>
<version>0.12.0</version>
</dependency>
3.3.2 發佈thrift-demo-py-api
上面我們缺少Maven依賴,同理這裏缺少Python的包依賴,我們可以使用pip安裝thrift包
pip install thrift
這裏要注意,Maven模塊我們可以在其它模塊裏直接引入依賴即可,但是Python在不同模塊裏沒法直接使用。所以這裏把生成的Python代碼進行打包。在thrift-demo-py-api模塊下,創建一個setup.py文件用來進行打包安裝。內容如下
# encoding: utf-8
from setuptools import setup, find_packages
setup(name="thrift_demo_py_api",
version="1.0",
description="The api of thrift demo.",
author="shirukai",
author_email="[email protected]",
url="https://shirukai.github.io",
packages=find_packages(),
scripts=[]
)
將此模塊以包的形式發佈到環境中
python setup.py install
這樣我們在其它的模塊裏就可以直接引用了。
>>> from thrift_demo.api import DemoService
>>>
4 Java實現服務端和客戶端
上面我們已經在thrift-demo-java-api中生成了thrift服務相關的Java代碼,這裏我們就要使用Java去實現服務端和客戶端,服務端和客戶端都與SpringBoot整合實現。
5.1 服務端:thrift-demo-java-server
服務端主要實現兩個方面:
- 實現抽象接口
- 實現服務暴露
在這之前,我們需要對模塊進行稍加改造,因爲是springboot項目,所以這裏指定項目parent爲springboot。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
然後引入api依賴和springboot的依賴。
<!-- demo api -->
<dependency>
<groupId>learn.demo</groupId>
<artifactId>thrift-demo-java-api</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<!-- spring boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
5.1.1 實現抽象接口
在第三節定義demo.thrift的時候,我們在service裏定義了兩個抽象方法,thrift會給我生成一個接口類
public class DemoService {
public interface Iface {
public DemoInfo getDemoById(int id) throws org.apache.thrift.TException;
public void createDemo(DemoInfo demo) throws org.apache.thrift.TException;
}
//……
}
所以在服務端,我們首先要實現這個接口。在learn.demo.thrift.server.service包下創建一個名爲DemoServiceImpl的類。該類繼承DemoService.Iface接口,實現裏面的兩個方法。內容如下
package learn.demo.thrift.server.service;
import learn.demo.thrift.api.DemoInfo;
import learn.demo.thrift.api.DemoService;
import org.apache.thrift.TException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* Created by shirukai on 2019-06-27 09:28
* Demo Service
*/
@Service
public class DemoServiceImpl implements DemoService.Iface {
private static final Logger log = LoggerFactory.getLogger(DemoServiceImpl.class);
private static final Map<Integer, DemoInfo> demoCache = new HashMap<>(16);
@Override
public DemoInfo getDemoById(int id) throws TException {
log.info("The client invoke method: getDemoById");
if (demoCache.containsKey(id)) {
return demoCache.get(id);
}
return null;
}
@Override
public void createDemo(DemoInfo demo) throws TException {
log.info("The client invoke method: createDemo");
demoCache.put(demo.id, demo);
}
}
5.1.2 實現服務暴露
這裏就需要我們去實現一個thrift的服務暴露,暴露一個端口,使客戶端可以進行通訊。在這之前我們使用springboot統一的配置文件指定一下需要暴露的端口。
在resources下創建一個application.properties配置文件,指定thrift服務暴露的端口爲7911
thrift.server.name=thrift-demo-server
thrift.server.port=7911
然後像普通的SpringBoot應用一樣,創建一個啓動類,在learn.demo.thrift.server下創建Application類,用以啓動SpringBoot應用。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
//……
}
Thrift服務實現,大體分一下幾步:
- 創建處理器
- 設置監聽端口
- 構建服務參數:指定處理器、指定傳輸方式、指定傳輸協議
- 創建服務
- 啓動服務
這裏我們使用的是@Configuration的形式,將我們的Thrift服務注入到SpringBoot應用裏。在Application裏創建靜態內部類ThriftServerConfiguration
@Configuration
static class ThriftServerConfiguration {
private static final Logger log = LoggerFactory.getLogger(ThriftServerConfiguration.class);
@Value(("${thrift.server.port}"))
private int serverPort;
@Autowired
private DemoService.Iface demoService;
@PostConstruct
public void startThriftServer() throws TTransportException {
// 創建處理器
TProcessor processor = new DemoService.Processor<>(demoService);
// 監聽端口
TNonblockingServerSocket socket = new TNonblockingServerSocket(serverPort);
// 構建服務參數
TNonblockingServer.Args args = new TNonblockingServer.Args(socket);
// 設置處理器
args.processor(processor);
// 設置傳輸方式
args.transportFactory(new TFastFramedTransport.Factory());
// 設置傳輸協議
args.protocolFactory(new TBinaryProtocol.Factory());
// 創建服務
TServer server = new TNonblockingServer(args);
log.info("The application is starting thrift server on address 0.0.0.0/0.0.0.0:{}",serverPort);
// 啓動服務
server.serve();
}
}
5.2 客戶端:thrift-demo-java-client
客戶端同樣是與SpringBoot整合
- 實現客戶端並以Bean的形式注入Spring
- 實現REST接口,用來演示遠程調用
在這之前,我們依然需要對模塊進行稍加改造,因爲是springboot項目,所以這裏指定項目parent爲springboot。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
然後引入api依賴和springboot的依賴。
<!-- demo api -->
<dependency>
<groupId>learn.demo</groupId>
<artifactId>thrift-demo-java-api</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<!-- spring boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
5.2.1 實現客戶端並以Bean的形式注入Spring
我們需要從配置文件裏獲取遠程thrift服務的IP地址和端口號,所以需要先創建一個application.properties文件
thrift.server.name=thrift-demo-client
thrift.server.ip=127.0.0.1
thrift.server.port=8911
同樣在learn.demo.thrift.client下創建SpringBoot應用啓動類
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
// ……
}
接下來是客戶端實現的重頭戲,大體分以下幾步:
- 創建Socket連接
- 設置傳輸方式
- 設置傳輸協議
- 連接服務端
- 獲取客戶端實例
同樣是以@Configuration的形式,將客戶端實例以Bean的形式注入到Spring裏。在Application類下創建ThriftClientConfiguration靜態內部類
@Configuration
static class ThriftClientConfiguration {
private static final Logger log = LoggerFactory.getLogger(ThriftClientConfiguration.class);
@Value("${thrift.server.ip}")
private String serverIp;
@Value("${thrift.server.port}")
private int serverPort;
@Bean("demoService")
public DemoService.Iface createThriftClient() throws TTransportException {
// 創建socket
TTransport socket = new TSocket(serverIp, serverPort);
// 傳輸方式
TFramedTransport transport = new TFramedTransport(socket);
// 傳輸協議
TProtocol protocol = new TBinaryProtocol(transport);
// 創建連接
transport.open();
log.info("The application is creating thrift client from address {}:{} ……",serverIp,serverPort);
return new DemoService.Client(protocol);
}
}
5.2.2 實現REST接口,用來演示遠程調用
這個就比較基礎了,是SpringBoot Web開發裏的內容,上面我們已經將Thrift客戶端實例以Bean的形式注入到了Spring裏。這裏我們可以通過@Autowired直接拿到實例,然後調用其方法。在learn.demo.thrift.client.controller下創建DemoController類,實現兩個REST接口。
package learn.demo.thrift.client.controller;
import learn.demo.thrift.client.dto.DemoInfoDTO;
import learn.demo.thrift.api.DemoInfo;
import learn.demo.thrift.api.DemoService;
import org.apache.thrift.TException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* Created by shirukai on 2019-06-27 10:03
* controller
*/
@RestController
@RequestMapping(value = "/demo")
public class DemoController {
@Autowired
private DemoService.Iface demoService;
@PostMapping
public String creatDemo(
@RequestBody() DemoInfoDTO demoInfoDTO
) throws TException {
DemoInfo demoInfo = new DemoInfo();
BeanUtils.copyProperties(demoInfoDTO, demoInfo);
demoService.createDemo(demoInfo);
return "success";
}
@GetMapping(value = "/{id}")
public DemoInfoDTO getDemo(
@PathVariable("id") Integer id
) throws TException {
DemoInfo demoInfo = demoService.getDemoById(id);
if (demoInfo != null) {
DemoInfoDTO demoInfoDTO = new DemoInfoDTO();
BeanUtils.copyProperties(demoInfo, demoInfoDTO);
return demoInfoDTO;
}
return null;
}
}
5 Python實現服務端和客戶端
上面我們已經在thrift-demo-py-api中生成了thrift服務相關的Python代碼,並將該模塊打包發佈,這裏我們就要使用Python去實現服務端和客戶端。
5.1 服務端:thrift-demo-py-server
無論是什麼語言,對於thrift的服務端和客戶端的創建流程都是一樣的,這裏就不多撰述,服務端依然是兩方面實現:
- 實現抽象接口
- 實現服務暴露
Python沒有接口的概念,但是Thrift爲了統一,依然給我實現了一個抽象“接口”,實際上是一個沒有具體實現的類。如下所示:
class Iface(object):
def getDemoById(self, id):
"""
Parameters:
- id
"""
pass
def createDemo(self, demo):
"""
Parameters:
- demo
"""
pass
所以首先要繼承該類,並重寫其方法
class DemoServiceHandler(DemoService.Iface):
"""
繼承DemoService.Iface,重寫其方法
"""
def getDemoById(self, id):
print "The client invoke method: " + "getDemoById."
if id in demoCache:
return demoCache[id]
else:
return None
def createDemo(self, demo):
print "The client invoke method: " + "createDemo."
demoCache[demo.id] = demo
然後是服務端的暴露,老套路
- 創建處理器
- 設置監聽端口
- 初始化傳輸方式
- 初始化傳輸協議
- 創建服務
- 啓動服務
直接上代碼
if __name__ == '__main__':
handler = DemoServiceHandler()
# 創建處理器
processor = DemoService.Processor(handler)
# 監聽端口
transport = TSocket.TServerSocket("127.0.0.1", "8911")
# 傳輸方式工廠:TBufferedTransportFactory/TFramedTransportFactory
# 服務端使用什麼傳輸方式,客戶端就需要使用什麼傳輸方式
tfactory = TTransport.TFramedTransportFactory()
# 傳輸協議工廠:TCompactProtocol/TJSONProtocol/TBinaryProtocol
# 服務端使用什麼傳輸協議,客戶端就需要使用什麼傳輸協議
pfactory = TBinaryProtocol.TBinaryProtocolFactory()
# 創建服務:TSimpleServer/TForkingServer/TThreadedServer/TThreadPoolServer
server = TServer.TSimpleServer(processor, transport, tfactory, pfactory)
print "python thrift server start"
server.serve()
5.2 客戶端:thrift-demo-py-client
Python客戶端沒有整合複雜的服務,這裏直接創建客戶端,然後進行遠程調用
# encoding: utf-8
"""
@author : shirukai
@date : 2019-06-26 21:02
thrift consumer
官網:http://thrift.apache.org/tutorial/py#client
"""
from thrift.protocol import TBinaryProtocol
from thrift.transport import TSocket, TTransport
from thrift_demo.api import DemoService
from thrift_demo.api.ttypes import DemoInfo
if __name__ == '__main__':
# 建立socket
transport = TSocket.TSocket('127.0.0.1', 7911)
# 傳輸方式,與服務端一致
transport = TTransport.TFramedTransport(transport)
# 傳輸協議,與服務端一致
protocol = TBinaryProtocol.TBinaryProtocol(transport)
# 創建客戶端
client = DemoService.Client(protocol)
# 連接服務端
transport.open()
# 遠程調用
demo = DemoInfo()
demo.id = 1
demo.name = "demo1"
demo.tags = ['1', '2']
client.createDemo(demo)
print client.getDemoById(1)
6 交叉驗證
至此我們就完成了Java版的Thrift服務端和客戶端的開發以及Python版的服務端和客戶端開發。下面將進行交叉驗證,通過以下幾種方案來驗證我們實現的RPC是否可用。
- Java客戶端-Java服務端
- Java客戶端-Python服務端
- Python客戶端-Python服務端
- Python客戶端-Java服務端
6.1 Java客戶端-Java服務端
1首選我們啓動Java服務端,執行thrift-demo-java-server中Application的main方法。暴露Thrift服務端口爲7911
[外鏈圖片轉存失敗(img-aA3aomew-1566360312309)(https://raw.githubusercontent.com/shirukai/images/master/a85ebfeee813eee937f2551caeb9b409.jpg)]
在啓動Java客戶端之前,需要修改配置文件,將需要調用的服務端口改爲7911
thrift.server.port=7911
然後啓動應用
[外鏈圖片轉存失敗(img-taXqchlK-1566360312309)(https://raw.githubusercontent.com/shirukai/images/master/96f111535a46793360a51c01401dfc6c.jpg)]
啓動完成後,我們可以通過REST來進行遠程調用。
創建Demo
[外鏈圖片轉存失敗(img-aTZS80Xl-1566360312310)(https://raw.githubusercontent.com/shirukai/images/master/76b60b4e0c5111f092cb65ecb4fa80ac.jpg)]
服務端打印日誌,說明createDemo方法已被調用
2019-06-27 15:11:53.395 INFO 43138 --- [ Thread-2] l.d.t.server.service.DemoServiceImpl : The client invoke method: createDemo
獲取Demo
[外鏈圖片轉存失敗(img-fkvy2aAo-1566360312310)(https://raw.githubusercontent.com/shirukai/images/master/89ae3280b5fce569d8be847596445a8e.jpg)]
服務端打印日誌,並且得到相應,說明RPC正常。
2019-06-27 15:13:27.957 INFO 43138 --- [ Thread-2] l.d.t.server.service.DemoServiceImpl : The client invoke method: getDemoById
6.2 Java客戶端-Python服務端
現在我們將上面兩個服務停掉,將之前的Java服務端,改爲Python服務端,啓動thrift-demo-py-server模塊下server裏的main方法。暴露服務端口爲8911
[外鏈圖片轉存失敗(img-RjKe353F-1566360312310)(https://raw.githubusercontent.com/shirukai/images/master/2c43e404f037d4fee7b4afaa18ba7549.jpg)]
將Java客戶端裏需要調用的服務端的端口改爲8911
thrift.server.port=8911
然後啓動服務。
依然使用PostMan進行REST請求。
創建Demo
[外鏈圖片轉存失敗(img-trsdO0Ge-1566360312313)(https://raw.githubusercontent.com/shirukai/images/master/89ae3280b5fce569d8be847596445a8e.jpg)]
服務端打印日誌
The client invoke method: createDemo.
獲取Demo
[外鏈圖片轉存失敗(img-L07dSG85-1566360312314)(https://raw.githubusercontent.com/shirukai/images/master/89ae3280b5fce569d8be847596445a8e.jpg)]
驗證完畢,說明我們的RPC正常。
6.3 Python客戶端-Python服務端
同理進行Python客戶端-Python服務端的驗證。
[外鏈圖片轉存失敗(img-vBTnuIZb-1566360312314)(https://raw.githubusercontent.com/shirukai/images/master/795beb70286227fe21a336b0bb0b4498.gif)]
6.4 Python客戶端-Java服務端
同理機型Python客戶端-Java服務端的驗證
[外鏈圖片轉存失敗(img-0EroMOXM-1566360312314)(https://raw.githubusercontent.com/shirukai/images/master/86e7ce8a4b1d614f5168325a860226fe.gif)]
7 總結
Thrift的初體驗至此,相比較Dubbo感覺沒有走太多的坑。因爲thrift沒有註冊中心,是通過直連的方式進行通訊,所以配置起來並不複雜。