RPC框架初體驗之Thrift

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服務實現,大體分一下幾步:

  1. 創建處理器
  2. 設置監聽端口
  3. 構建服務參數:指定處理器、指定傳輸方式、指定傳輸協議
  4. 創建服務
  5. 啓動服務

這裏我們使用的是@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);
    }
    // ……
 }

接下來是客戶端實現的重頭戲,大體分以下幾步:

  1. 創建Socket連接
  2. 設置傳輸方式
  3. 設置傳輸協議
  4. 連接服務端
  5. 獲取客戶端實例

同樣是以@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

然後是服務端的暴露,老套路

  1. 創建處理器
  2. 設置監聽端口
  3. 初始化傳輸方式
  4. 初始化傳輸協議
  5. 創建服務
  6. 啓動服務

直接上代碼

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沒有註冊中心,是通過直連的方式進行通訊,所以配置起來並不複雜。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章