簡介
Apache Thrift是Facebook開源的跨語言的RPC通信框架,目前已經捐獻給Apache基金會管理,由於其跨語言特性和出色的性能,在很多互聯網公司得到應用,有能力的公司甚至會基於thrift研發一套分布式服務框架,增加諸如服務注冊、服務發現等功能。
RPC即Remote Procedure Call,翻譯為遠程過程調用。任何RPC協議的實現終極目標都是讓使用者在調用遠程方法的時候就像是調用本地方法一樣簡單,從而提高使用遠程服務的效率。
現代互聯網架構多數基於SOA思想而搭建,即面向服務化的架構。服務提供方稱為Provider,服務的使用方稱為Consumer,有時也把服務提供方稱為Server端,使用方稱為Client端,即典型的CS模型。這里的遠程調用,主要指跨進程的調用,Provider和Consumer可能是同一機器的不同進程,也可能在不同的機器,通過網絡相互通信,大部分情況下兩者會部署在不同的物理機器上,這種情況下由於網絡通信的開銷就會對RPC框架的性能要求極高。
下面分別從服務端和客戶端的視角來介紹Thrift在RPC中的應用。
服務端(Server)
服務端需要發布一個服務給別人使用,首先要約定好服務的接口,包括以下幾個部分:
- 服務的名稱
- 服務使用時的參數
- 返回結果
Thrift自己規定了一套接口定義語言(IDL)來描述服務,用后綴為.thrift的文件來描述,比如我們要提供一個打招呼的服務,傳入姓名,然后返回一段友好的語句,Thrift文件HelloService.thrift的內容如下:
namespace java com.yuanwhy.service service HelloService{ string sayHello(1:string name) }
Thrift文件定義好之后,就約定好了接口的使用方式,但是仍然還不能使用,需要我們用thrift命令來生成對應的編程語言的文件,比如用一下命令來生成HelloService.class的Java文件。
thrift -r --gen java HelloService.thrift
命令行參數 -r 代表遞歸生成里面引用的其他文件, --gen 后面跟生成的目標語言,最后跟上thrift文件。
生成的Java文件里會有一個接口HelloService.Iface,這就是一個普通的Java Interface,服務端要提供該接口的實現,實現之前需要引用libthrift的jar包,比如我們這么實現:
package com.yuanwhy.service; import org.apache.thrift.TException; public class HelloServiceImpl implements HelloService.Iface { @Override public String sayHello(String name) throws TException { return "Hello, " + name + "!"; } }
這樣,服務端就實現了服務的邏輯部分,但是要讓別人在網絡上真正可用,我們還得把這個服務發布出去,發布的方式就是借助Socket編程,監聽一個對外服務的端口,這也是網絡通訊的基本套路。利用Thrift提供的API,在HelloServiceProvider類中啟動Thrift服務:
package com.yuanwhy.demo; import com.yuanwhy.service.HelloService; import com.yuanwhy.service.HelloServiceImpl; import org.apache.thrift.TProcessor; import org.apache.thrift.server.TServer; import org.apache.thrift.server.TSimpleServer; import org.apache.thrift.transport.TServerSocket; import org.apache.thrift.transport.TTransportException; public class HelloServiceProvider { /** * 啟動 Thrift 服務器 * * @param args */ public static void main(String[] args) { try { // 設置服務端口為 7911 TServerSocket serverTransport = new TServerSocket(7911); TProcessor processor = new HelloService.Processor(new HelloServiceImpl()); TServer server = new TSimpleServer(new TServer.Args(serverTransport).processor(processor)); System.out.println("Start server on port 7911..."); server.serve(); } catch (TTransportException e) { e.printStackTrace(); } } }
運行main函數之后,服務端就會監聽7911端口,開始對外提供sayHello的服務了。
客戶端(Client)
客戶端想要使用服務端通過thrift發布的服務,只需要遵循面向接口編程的基本思想,引用Java接口,用thrift的API連接服務端即可,比如HelloServiceConsumer類中這么使用sayHello服務:
package com.yuanwhy.demo; import com.yuanwhy.service.HelloService; import org.apache.thrift.TApplicationException; import org.apache.thrift.TEnum; import org.apache.thrift.TException; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.protocol.TProtocol; import org.apache.thrift.transport.TSocket; import org.apache.thrift.transport.TTransport; /** * Created by zeze on 2018/03/23. */ public class HelloServiceConsumer { public static void main(String[] args) { TTransport transport = new TSocket("0.0.0.0", 7911); try { transport.open(); TProtocol protocol = new TBinaryProtocol(transport); HelloService.Client client = new HelloService.Client(protocol); System.out.println(client.sayHello("yuanwhy")); transport.close(); } catch (TApplicationException e) { if (e.getType() == TApplicationException.MISSING_RESULT) { System.out.println("null"); } } catch (TException e){ } } }
客戶端運行結果會打印Hello, yuanwhy!
,表明服務調用成功。仔細觀察一下客戶端的代碼會發現,基本和普通的Socket編程沒有太大的區別,只是又被thrift做了一層的封裝,讓我們可以按照約定的接口直接像client.sayHello("yuanwhy")
這樣調用遠程服務。
注意:客戶端如果是在不同的Java項目中調用服務,只需要服務端把thrift文件或者生成的Java接口文件以API的方式提供出來即可,客戶端絕對不需要引用HelloServiceImpl實現類,因為目的就是讓邏輯在服務端實現,對客戶端透明。
另外,當服務端返回null時,客戶端會拋一個Type為TApplicationException.MISSING_RESULT
的異常出來,如果不處理就會影響客戶端正常的流程。這一點可以在Thrift的生成代碼中看出來:
public String recv_sayHello() throws org.apache.thrift.TException { sayHello_result result = new sayHello_result(); receiveBase(result, "sayHello"); if (result.isSetSuccess()) { return result.success; } throw new org.apache.thrift.TApplicationException(org.apache.thrift.TApplicationException.MISSING_RESULT, "sayHello failed: unknown result"); }
總結
以上對Thrift的使用只做了個簡單的介紹,真正在項目中使用Thrift還會涉及很多,比如各種Thrift數據結構的使用,在對Thrift接口進行升級過程中struct的字段最好保留原有字段順序以達到兼容目的,還比如客戶端應該建立連接池機制,而不是每次調用服務時都去新建一次TCP連接等等。