一、簡介
之前我們研究了一下thrift的一些知識,我們知道他是一個rpc框架,他作為rpc自然是提供了客戶端到服務端的訪問以及兩端數據傳輸的消息序列化,消息的協議解析和傳輸,所以我們今天就來了解一下他是如何實現這些功能,并且如何在實際代碼中使用。
我們需要搭建環境。
1. 安裝Thrift 作用:把IDL語言描述的接口內容,生成對應編程語言的代碼,簡化開發。我們已經介紹了在mac如何使用brew安裝了。
2. 引入依賴 作用:引入thrift針對于某一種編程語言的封裝 (網絡通信 協議【序列化】)
二、結構和模塊
1、Thrift核心對象
1. TTransport作用:底層封裝了rpc的網絡通信能力,具體實現有下面三種TSocket 阻塞IO通信TNonblockdingTransport 非阻塞網絡通信TFramedTransport 加入了封幀的操作 (壓縮后 數據邊界問題)
2. TProtocol作用:底層封裝了rpc的關于協議的處理 (序列化方式)TBinayProtocol 二進制進行序列化TCompactProtocol 壓縮方式 處理二進制 TJSONProtocol JSON進行序列化這里需要說明一下,當我們的序列化方式走了TCompactProtocol壓縮處理的時候,你最好使用TFramedTransport來處理網絡通信,因為一旦數據開啟了壓縮,那就可能傳輸很大量的數據,數據的模樣和原始的不同,此時可能因為緩沖區大小等等出現半包粘包問題。需要解決封幀。其他的方式一般沒啥,因為rpc數據量不會很大,一般沒有半包粘包,具體問題具體分析吧。而且你能看到他提供多種協議,并不是用的http協議這個最常見的,這個沒啥說的,高性能的網絡協議,一般都不用http,http的優勢是簡單通用。
3. TProcessor作用:進行業務處理,把通信數據 和 業務功能整合在一起。換句話說我們的業務代碼就在這里,其實就是把數據拿過來了,開始業務處理了。
4. TServer 服務端服務端就是用上面的三個組件,組合起來實現具體的服務端邏輯,根據不同的實現有異步的有同步的等等,并且發布服務出去。
2、項目結構
我們在編碼之前需要明確一點,就是作為rpc(Remote Procedure Call),遠程調用必然存在一個服務的提供者和一個服務的調用者。所以我們必須有的兩個模塊是rpc-server和rpc-client。
在實際開發的時候,我們比如說服務調用者和提供者可能有相同的數據比如User這個類,那你不能在兩邊各自都寫一個吧,肯定是寫一份維護就好了。所以我們還需要一個rpc-common的模塊,用來聲明那些公用的類,包括一些公用的實體類,還有發起rpc調用的service代理類等等。
于是我們就抽象出了三個模塊。
1. thrift-client 代表的是服務的調用者,客戶端要想本地方法那樣調用遠端方法,底層必然是代理。
2. thrift-server 代表的是服務的提供者2.1. 實現服務接口 :idl語言生成的2.2. 創建服務端代碼
3. thrift-common RPC編程共有的內容 1,實體類型 2,服務接口3.1. 通過IDL語言 定義 client與服務端 共用的數據類型 和 服務接口 3.2. client server端需要引入 common模塊
我們這里是以同一個工程下的模塊化的形式搭建項目,實際上你也可以單獨創建不同的工程。
3、引入依賴
我們這里是一個項目多個模塊,所以我直接在父級項目引入了。
<modules><module>thrift-client</module><module>thrift-server</module><module>thrift-common</module>
</modules><properties><thrift.version>0.22.0</thrift.version><javax.annotation.version>1.3.2</javax.annotation.version><slf4j-api.version>1.7.32</slf4j-api.version><logback-classic.version>1.2.9</logback-classic.version><junit.version>3.8.1</junit.version><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties><dependencies><!-- 引入thrift針對于某一種編程語言的封裝,java就是maven管理jar,你要是用py開發,那就是pip (網絡通信 協議【序列化】)--><dependency><groupId>org.apache.thrift</groupId><artifactId>libthrift</artifactId><version>${thrift.version}</version></dependency><dependency><groupId>javax.annotation</groupId><artifactId>javax.annotation-api</artifactId><version>${javax.annotation.version}</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>${slf4j-api.version}</version></dependency><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>${logback-classic.version}</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>${junit.version}</version><scope>test</scope></dependency></dependencies>
4、構建thrift文件
此時我們需要構建一些用來編程的代碼。首先我們的實體類和service要放在common里面,然后client和server引用common即可。我們實現一個保存用戶User和根據用戶名查看用戶的實現。
因為我們是java開發,所以我們其實目標就是一個User類,和一個service類,service里面提供void save(User user)和User getUserByName(String name)這樣的東西。所以我們就基于這個構建IDL,然后用thrift翻譯出來java代碼即可。根據我們昨天對于IDL的語法梳理,可以這么編寫.thrift文件。
名稱為rpc-thrift.thrift
# 用來構建rpc的thrift文件namespace java com.levi
struct User {1: required string name,2: required string password
}
service UserService {/*** 注冊用戶* @param user 用戶信息* @return 注冊結果*/void registerUser(1: User user)/*** 獲取用戶信息* @param name 用戶名稱* @return 用戶信息*/User getUser(1: string name)
}
然后我們進入到這個文件的目錄,使用如下命令:
thrift --gen java rpc-thrift.thrift
生成如下結果:
我們把這些轉移到類目錄下即可,然后刪除這些即可。
然后我們maven install,再把common這個依賴給client和server模塊引入即可。
<dependencies><dependency><groupId>com.levi</groupId><artifactId>thrift-common</artifactId><version>1.0-SNAPSHOT</version></dependency>
</dependencies>
此時我們就完成了對應的項目結構搭建,下面我們來實現業務。
三、業務實現
1、服務端實現
package com.levi.service;import com.levi.User;
import com.levi.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;/*** 注意這里實現的接口不是UserService,而是UserService.Iface,這是thrift生成的接口* 按照thrift的規范,每個接口都有一個對應的Iface接口,用于封裝接口的方法,方便調用*/
public class UserServiceImpl implements UserService.Iface{Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);@Overridepublic void registerUser(User user) {logger.info("register user:{}",user);}@Overridepublic User getUser(String name) {logger.info("get user:{}",name);return new User(name,"123456");}
}
有了業務類之后,就啟動服務端。
package com.levi;import com.levi.service.UserServiceImpl;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TSimpleServer;
import org.apache.thrift.transport.TServerSocket;public class TestServer {public static void main(String[] args) {// 創建服務端,監聽端口8888,其實就是對應的TTransport組件,通信功能在此封裝try (TServerSocket tServerSocket = new TServerSocket(8888);){// 協議工廠,用于封裝協議,比如二進制協議、JSON協議等,此處使用二進制協議,其實對應的就是TProtocol組件,協議功能在此封裝TBinaryProtocol.Factory protocolFactory = new TBinaryProtocol.Factory();// 處理器工廠,用于封裝處理器,比如UserService.Processor,其實對應的就是TProcessor組件,處理器功能在此封裝。綁定我們的業務邏輯。UserService.Processor processor = new UserService.Processor(new UserServiceImpl());// 封裝參數TSimpleServer.Args arg = new TSimpleServer.Args(tServerSocket).protocolFactory(protocolFactory).processor(processor);// 構建服務端起動器,其實對應的就是TServer組件,服務端功能在此封裝,發布服務出去,等待客戶端連接TSimpleServer tSimpleServer = new TSimpleServer(arg);tSimpleServer.serve();}catch (Exception e){e.printStackTrace();}}
}
2、客戶端實現
package com.levi;import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class TestClient {private static Logger logger = LoggerFactory.getLogger(TestClient.class);public static void main(String[] args) {// 創建服務端,監聽端口8888,其實就是對應的TTransport組件,通信功能在此封裝try (TTransport tTransport = new TSocket("localhost", 8888);){// 協議工廠,用于封裝協議,比如二進制協議、JSON協議等,此處使用二進制協議,其實對應的就是TProtocol組件,協議功能在此封裝,和服務端保持一致TProtocol tProtocol = new TBinaryProtocol(tTransport);// rpc的本質,就是像調用本地服務一樣調用遠程服務,此處就是一個體現,但是本質都是代理模式,只不過這個代理thrift封裝了UserService.Client userService = new UserService.Client(tProtocol);tTransport.open();User user = userService.getUser("levi");logger.info("client get user is {}",user);}catch (Exception e){e.printStackTrace();}}
}
啟動服務端之后開始監聽,然后啟動客戶端。輸出如下:
client get user is User(name:levi, password:123456)
沒毛病。
四、重構
實際上開發的時候,我們一般是有些業務公用的,可能RPC調用在用其他的業務,所以最好是封裝隔離開,我們一般是在RPC這里直接調用,實在沒得用再寫。
于是我們把thrift的這個實現改為UserServiceImplRPC
package com.levi.service;import com.levi.User;
import com.levi.UserService;/*** 注意這里實現的接口不是UserService,而是UserService.Iface,這是thrift生成的接口* 按照thrift的規范,每個接口都有一個對應的Iface接口,用于封裝接口的方法,方便調用*/
public class UserServiceImplRPC implements UserService.Iface{@Overridepublic void registerUser(User user) {UserServiceImpl userService = new UserServiceImpl();userService.registerUser(user);}@Overridepublic User getUser(String name) {UserServiceImpl userService = new UserServiceImpl();return userService.getUser(name);}
}
常規的業務放在UserServiceImpl
package com.levi.service;import com.levi.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;/*** 注意這里實現的接口不是UserService,而是UserService.Iface,這是thrift生成的接口* 按照thrift的規范,每個接口都有一個對應的Iface接口,用于封裝接口的方法,方便調用*/
public class UserServiceImpl {Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);public void registerUser(User user) {logger.info("register user:{}",user);}public User getUser(String name) {logger.info("get user:{}",name);return new User(name,"123456");}
}
這樣方便維護也比較清晰。