SpringBoot集成Thrift實(shí)現(xiàn)RPC服務(wù)基礎(chǔ)示例-Java服務(wù)端和客戶(hù)端

項(xiàng)目需要使用跨語(yǔ)言RPC服務(wù),RPC這里就不多講了,跨語(yǔ)言的框架目前比較有名的有Thrift、gRPC、Hessian這幾個(gè)。

  • Thrift原來(lái)是Facebook為大規(guī)??缯Z(yǔ)言RPC服務(wù)開(kāi)發(fā)的項(xiàng)目,后臺(tái)捐給Apache了,官網(wǎng)鏈接https://thrift.apache.org/;

  • gRPC是Google開(kāi)發(fā)的高性能、通用的開(kāi)源RPC框架,官網(wǎng)鏈接https://www.grpc.io/

  • Hessian是一個(gè)輕量級(jí)的使用二進(jìn)制序列化的的RPC框架,基于HTTP,所以不存在什么語(yǔ)言問(wèn)題,官網(wǎng)鏈接http://hessian.caucho.com

對(duì)于這三個(gè)框架的性能比較網(wǎng)上很多文章分析,總的來(lái)說(shuō)還是Thrift性能占優(yōu)勢(shì)一點(diǎn),但是Thrift官方維護(hù)并不太好,最新版本還是2018年12月發(fā)布的,而且github上連issue都沒(méi)開(kāi),Hessian也是更新慢,而gRPC更新活躍,用的人也比較多。至于使用哪個(gè)看個(gè)人吧。

說(shuō)明一點(diǎn):Thrift有BIO(阻塞IO)方式和NIO(非阻塞IO)方式,下面先講BIO方式,然后再說(shuō)NIO的。

下面進(jìn)入主題:

一、下載Thrift接口生成工具

Windows版本下載地址:https://mirrors.tuna.tsinghua.edu.cn/apache/thrift/0.11.0/ (清華大學(xué)開(kāi)源軟件鏡像站)

其他版本源碼下載地址:https://thrift.apache.org/download#maven-artifact 或者 https://github.com/apache/thrift
安裝文檔:https://thrift.apache.org/docs/install/ 或者 https://github.com/apache/thrift/tree/master/doc/install

我這里用的的是Windows版本的exe,直接拿來(lái)用,下載后可以把路徑加入到系統(tǒng)Path里面這樣在cmd里面就可以直接使用thrift命令來(lái)操作了。其他系統(tǒng)的安裝方法請(qǐng)參考文檔。

\color{red}{注意:這里的版本必須是0.11版本的}
因?yàn)樽钚?.12版本有一個(gè)錯(cuò)誤,在使用BIO多線(xiàn)程的情況下會(huì)一直打印一個(gè)異常,雖然并不影響服務(wù),但是每次都會(huì)打印一個(gè)Error日志很不好,錯(cuò)誤情況這篇文章
后面我也會(huì)說(shuō)NIO方式,是可以直接用0.12版本沒(méi)問(wèn)題

二、創(chuàng)建TestService.thrift文件

thrift文件規(guī)范說(shuō)明請(qǐng)看這篇文章。

創(chuàng)建一個(gè)很簡(jiǎn)單的示例文件

namespace java com.example.thrift.service

service TestService {
    string sayHello(1:string message);
}

三、生成Java代碼

在thrift文件目錄下使用命令

thrift --gen java TestService.thrift

執(zhí)行完之后,會(huì)在TestService.thrift目錄下生成一個(gè)gen-java目錄,里面有個(gè)生成好的TestService.java文件,這個(gè)就是用來(lái)實(shí)現(xiàn)遠(yuǎn)程調(diào)用的接口,接下來(lái)把生成的Java文件復(fù)制到項(xiàng)目源碼里面。這里執(zhí)行命令可以直接在IDEA下面工具欄里的Terminal選項(xiàng)卡里面執(zhí)行,首先要進(jìn)入到TestService.thrift的目錄下面,然后直接執(zhí)行上面的命令就可以了,不用去切換CMD了。


TestService.thrift和生成的Java文件

四、SpringBoot中引入Thrift

maven artifact:

<dependency>
    <groupId>org.apache.thrift</groupId>
    <artifactId>libthrift</artifactId>
    <version>0.11.0</version>
</dependency>

這里也要注意引入的版本跟上面生成代碼的工具版本要一致,這里也使用0.11的版本。然后看一下整個(gè)項(xiàng)目的結(jié)構(gòu):


示例代碼結(jié)構(gòu)

加下來(lái)看幾個(gè)文件的代碼。

五、實(shí)現(xiàn)服務(wù)接口

上面生成的TestService源碼復(fù)制到service包下,然后在這里新建接口實(shí)現(xiàn)類(lèi)TestServiceImpl:

package com.example.thrift.service;

import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.TException;

@Slf4j
public class TestServiceImpl implements TestService.Iface {
    @Override
    public String sayHello(String message) throws TException {
        log.info("<<<收到消息:{}", message);
        return "Hi, I'm Server!";
    }
}

六、編寫(xiě)服務(wù)端

在server包下面新建ThriftServer服務(wù)端代碼:

package com.example.thrift.server;

import com.example.thrift.service.TestService;
import com.example.thrift.service.TestServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.TProcessor;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TThreadPoolServer;
import org.apache.thrift.transport.TServerSocket;

@Slf4j
public class ThriftServer {

    public void start() {
        try {
            log.info(">>>Thrift服務(wù)端開(kāi)啟");
            TServerSocket serverSocket = new TServerSocket(1998); //服務(wù)端口1998
            TProcessor tProcessor = new TestService.Processor<TestService.Iface>(new TestServiceImpl());

            //BIO單線(xiàn)程版
            //TSimpleServer.Args sArgs = new  TSimpleServer.Args(serverSocket);
            //sArgs.processor(tProcessor);  //注冊(cè)服務(wù)
            //TServer server = new TSimpleServer(sArgs);

            //BIO多線(xiàn)程版
            TThreadPoolServer.Args tArgs = new TThreadPoolServer.Args(serverSocket);
            tArgs.processor(tProcessor); //注冊(cè)服務(wù)
            tArgs.minWorkerThreads(2); //設(shè)置線(xiàn)程池核心線(xiàn)程數(shù)量
            tArgs.maxWorkerThreads(10); //設(shè)置線(xiàn)程池最大線(xiàn)程數(shù)量
            TServer server = new TThreadPoolServer(tArgs);

            server.serve();
        } catch (Exception e) {
            log.error("Thrift服務(wù)發(fā)生錯(cuò)誤:", e);
        }
    }
}

注意Thrift提供了兩種線(xiàn)程版本,一個(gè)是單線(xiàn)程的一個(gè)是多線(xiàn)程的。另外一點(diǎn)要注意的是服務(wù)端server是會(huì)阻塞主線(xiàn)程的,所以在server開(kāi)啟之后不要有任務(wù)其他操作,否則是運(yùn)行不到的。在SpringBoot初始化的過(guò)程中也不要啟動(dòng)服務(wù),因?yàn)镾pringBoot默認(rèn)是以類(lèi)名稱(chēng)排序去掃描包下面的配置或者組件等等,如果Thrift服務(wù)類(lèi)名稱(chēng)在其他配置前面,剛好你在初始化Thrift的時(shí)候順便也啟動(dòng)了服務(wù)的話(huà),那么其他的Spring配置就會(huì)不能初始化了,所以可以在SpringBoot完全初始化之后再啟動(dòng)Thrift,或者干脆起一個(gè)線(xiàn)程跑Thrift服務(wù)。
這里的單線(xiàn)程是共用主線(xiàn)程就是main線(xiàn)程,多線(xiàn)程則是另外開(kāi)啟一個(gè)ThreadPoolExecutor線(xiàn)程池來(lái)處理socket的。這個(gè)看TThreadPoolServer源代碼就知道了:


TThreadPoolServer初始化源碼片段

七、編寫(xiě)客戶(hù)端

在client包下面創(chuàng)建ThriftClient,為了簡(jiǎn)單,這里只用一個(gè)簡(jiǎn)單Java類(lèi)來(lái)當(dāng)做客戶(hù)端,代碼很少:

package com.example.thrift.client;

import com.example.thrift.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;

@Slf4j
public class ThriftClient {
    public static void main(String[] args) {
        TTransport transport = null;
        try {
            //TSocket 參數(shù)分別是:服務(wù)器地址,端口,連接超時(shí)時(shí)間(毫秒)
            transport = new TSocket("localhost", 1998, 30000);
            TProtocol protocol = new TBinaryProtocol(transport);
            TestService.Client client = new TestService.Client(protocol);
            transport.open();

           //這里得到返回?cái)?shù)據(jù)是同步的
            String reply = client.sayHello("Hello, I'm Client");
            log.info(">>>收到回復(fù)消息:{}", reply);

            //如果需要異步,不要返回?cái)?shù)據(jù)可以使用send_***方法
            //client.send_sayHello("Hello, I'm Client ~ ");
            //注意recv_***方法是同步的,會(huì)阻塞到收完數(shù)據(jù)
            //String reply = client.recv_sayHello();
            //log.info(">>>收到回復(fù)消息:{}", reply);

        } catch (Exception e) {
            log.error("Thrift客戶(hù)端發(fā)生錯(cuò)誤", e);
        } finally {
            if (transport != null) {
                transport.close();
            }
        }
    }
}

好了,BIO部分就到這里。下面來(lái)說(shuō)說(shuō)NIO部分,其實(shí)也差不多,只是換了個(gè)類(lèi)。現(xiàn)在可以把Thrift的版本改到最新版本0.12.0,接口生產(chǎn)工具Windows下載地址只要把0.11.0的地址改成0.12.0就可以了https://mirrors.tuna.tsinghua.edu.cn/apache/thrift/0.12.0/,對(duì)應(yīng)的maven資源版本也需要改成0.12.0,因?yàn)?.12版工具生成的代碼跟0.11版的有點(diǎn)不一樣,不過(guò)0.11版改一下也是可以用,但是最好還是跟著版本匹配來(lái)。下面的NIO的都是基于0.12版的

八、創(chuàng)建NIO服務(wù)端

在server包下創(chuàng)建ThriftNIOServer:

package com.example.thrift.server;

import com.example.thrift.service.TestService;
import com.example.thrift.service.TestServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.TProcessor;
import org.apache.thrift.server.THsHaServer;
import org.apache.thrift.server.TNonblockingServer;
import org.apache.thrift.transport.TNonblockingServerSocket;

@Slf4j
public class ThriftNIOServer {
    public void start() {
        try {
            log.info(">>>Thrift NIO 服務(wù)端開(kāi)啟");
            TProcessor tprocessor = new TestService.Processor<TestService.Iface>(new TestServiceImpl());
            TNonblockingServerSocket serverSocket = new TNonblockingServerSocket(1998);
            //NIO單線(xiàn)程版
            //TNonblockingServer.Args args = new TNonblockingServer.Args(serverSocket);
            //args.processor(tprocessor);
            //TNonblockingServer server = new TNonblockingServer(args);

            //NIO多線(xiàn)程版
            THsHaServer.Args args = new THsHaServer.Args(serverSocket);
            args.processor(tprocessor);
            args.minWorkerThreads(2);
            args.maxWorkerThreads(10);
            THsHaServer server = new THsHaServer(args);
            server.serve();
        } catch (Exception e) {
            log.error("Thrift服務(wù)端發(fā)生錯(cuò)誤:", e);
        }
    }
}

這里也有單線(xiàn)程和多線(xiàn)程版本。要注意的一點(diǎn)是NIO版本的transport默認(rèn)必須用TFramedTransport,所以Client里面也要用同樣的transport,否則服務(wù)端直接打印一個(gè)錯(cuò)誤信息,不會(huì)拋異常,只是提醒客戶(hù)端使用的transport不正確:

ERROR 14012 --- [       Thread-5] .s.AbstractNonblockingServer$FrameBuffer : Read an invalid frame size of -2147418111. Are you using TFramedTransport on the client side?

客戶(hù)端則會(huì)直接拋異常,因?yàn)榉?wù)端根本就沒(méi)處理請(qǐng)求。

九、創(chuàng)建NIO客戶(hù)端

在client包下創(chuàng)建ThriftNIOClient:

package com.example.thrift.client;

import com.example.thrift.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;

@Slf4j
public class ThriftNIOClient {
    public static void main(String[] args) {
        TTransport transport = null;
        try {
            //TSocket 參數(shù)分別是:服務(wù)器地址,端口,連接超時(shí)時(shí)間(毫秒),與BIO比較,只有這里不同,
            //就是在創(chuàng)建transport 的時(shí)候,包了一層TFramedTransport,和服務(wù)端匹配起來(lái)
            transport = new TFramedTransport(new TSocket("localhost", 1998, 30000));
            TProtocol protocol = new TBinaryProtocol(transport);
            TestService.Client client = new TestService.Client(protocol);
            transport.open();

            //這里得到返回?cái)?shù)據(jù)是同步的
            String reply = client.sayHello("Hello, I'm NIO Client");
            log.info(">>>收到回復(fù)消息:{}", reply);

            //如果需要異步,不要返回?cái)?shù)據(jù)可以使用send_***方法
            //client.send_sayHello("Hello, I'm Client ~ ");
            //注意recv_***方法是同步的,會(huì)阻塞到收完數(shù)據(jù)
            //String replyAsync = client.recv_sayHello();

        } catch (Exception e) {
            log.error("Thrift客戶(hù)端發(fā)生錯(cuò)誤", e);
        } finally {
            //NIO版本這一定要注意關(guān)閉transport
            if (transport != null) {
                transport.close();
            }
        }
    }
}

在這里使用0.12的版本一切都很正常,如果換成是BIO多線(xiàn)程方式,這個(gè)版本會(huì)一直打印一個(gè)錯(cuò)誤。

十、總結(jié)

總的來(lái)說(shuō)Thrift的使用還是簡(jiǎn)單的,接口代碼可以生成不同語(yǔ)言平臺(tái)的代碼,這樣一個(gè)服務(wù)端可以對(duì)應(yīng)各種不同語(yǔ)言的客戶(hù)端,對(duì)于小型項(xiàng)目RPC來(lái)說(shuō),確實(shí)是很方便的事情,至于服務(wù)治理、分布式這里就不討論了。
在生產(chǎn)環(huán)境下服務(wù)端最好還是采用NIO方式。

====> 本文源碼

好了,到此本文結(jié)束,感謝你的閱讀~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容