上接:thrift 入門(1/2)
PS:我也不想拆,但是放一起文章太長無法發(fā)布仗嗦。惭笑。侣姆。
四、thrift 入門
4.1 小試牛刀
首先沉噩,我們還是先用 thrift 實(shí)現(xiàn)一下前文中的需求。
按如下步驟操作:
- 安裝 thrift(安裝方式請自行百度柱蟀,最新版本或稍老的版本都可以川蒙,我用的 0.9.3 )。
- 編寫 demo.thrift 文件长已,內(nèi)容如下:
// demo.thrift
namespace java com.ann.javas.frameworks.thrifts.demo.beans
enum Gender {
BOY = 1,
GIRL = 2,
}
struct UserInfo {
1: required string name,
3: required Gender gender,
6: required i32 weight,
}
service DemoService {
string sayHi(1:required UserInfo userInfo);
string locate();
string adsRecommendation();
}
- 打開終端畜眨,cd 到 demo.thrift 所在文件目錄執(zhí)行
thrift -r --gen java demo.thrift
昼牛,thrift 會(huì)編譯 demo.thrift 生成三個(gè) java 源文件(分別是:DemoService.java、UserInfo.java康聂、Gender.java贰健,源碼太多不在這兒貼了)到 ./gen-java 目錄下。 - maven 引入
libthrift
包恬汁,版本須與前面安裝的 thrift 版本保持一致伶椿。 - 把該文件拷貝到工程目錄。
- 然后創(chuàng)建 3 個(gè) java 源文件:
- DemoServiceImpl.java氓侧,實(shí)現(xiàn) thrift 定義的接口脊另。
- Demo1Server.java,服務(wù)端约巷。
- Demo1Client.java偎痛,客戶端。
// DemoServiceImpl.java
package com.ann.javas.frameworks.thrifts.demo.impl;
import com.ann.javas.frameworks.thrifts.demo.beans.DemoService;
import com.ann.javas.frameworks.thrifts.demo.beans.Gender;
import com.ann.javas.frameworks.thrifts.demo.beans.UserInfo;
import org.apache.thrift.TException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
/**
* Created by liyanling on 2018/9/9.
*/
public class DemoServiceImpl implements DemoService.Iface{
private static Logger logger = LoggerFactory.getLogger(DemoServiceImpl.class);
private final static String SAY_HI_TEMPLATE = "Hi,%s%s!%s";
private final static List<String> CITIES = Arrays.asList("上海", "北京", "廣州", "成都", "內(nèi)蒙古", "香港", "河北", "云南");
private final static List<String> ADS = Arrays.asList("坐紅旗車独郎,走中國路", "要想皮膚好踩麦,早晚用大寶", "喝匯源果汁,走健康之路", "送禮就送腦白金",
"飄柔氓癌,就是這么自信");
@Override
public String sayHi(UserInfo userInfo) throws TException {
String name = userInfo.getName();
String nameSuffix = Gender.BOY == userInfo.getGender() ? "先生" : "女士";
String weightNotice = userInfo.getWeight() >= 200 ? "你該減肥啦~" : "";
return String.format(SAY_HI_TEMPLATE, name, nameSuffix, weightNotice);
}
@Override
public String locate() throws TException {
int citySize = CITIES.size();
int randomIndex = Math.abs((new Random(System.currentTimeMillis())).nextInt()) % citySize;
return CITIES.get(randomIndex);
}
@Override
public String adsRecommendation() throws TException {
// logger.info("sleep..........");
// try {
// Thread.sleep(10000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// logger.info("awake..........");
int adsSize = ADS.size();
int randomIndex = Math.abs((new Random(System.currentTimeMillis())).nextInt()) % adsSize;
return ADS.get(randomIndex);
}
}
// Demo1Server.java
package com.ann.javas.frameworks.thrifts.demo.impl;
import com.ann.javas.frameworks.thrifts.demo.beans.DemoService;
import org.apache.thrift.TProcessor;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TSimpleServer;
import org.apache.thrift.transport.TServerSocket;
import org.apache.thrift.transport.TServerTransport;
import org.apache.thrift.transport.TTransportException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Created by liyanling on 2018/9/9.
*/
public class Demo1Server {
private static Logger logger = LoggerFactory.getLogger(Demo1Server.class);
public static final int PORT = 9081;
public static final int CLIENT_TIMEOUT = 100000;
public static void main(String[] args) {
Demo1Server server = new Demo1Server();
server.startServer();
}
/**
* 單線程服務(wù)模型:TSimpleServer + TServerSocket
*/
private void startServer() {
try {
int count = 1;
logger.info("{}:new 一個(gè) TServerSocket 實(shí)例谓谦,指定端口號(hào)為:{} 超時(shí)時(shí)間:{}", count++, PORT, CLIENT_TIMEOUT);
TServerTransport tServerTransport = new TServerSocket(PORT, CLIENT_TIMEOUT);
TSimpleServer.Args args = new TSimpleServer.Args(tServerTransport);
logger.info("{}:服務(wù)端初始化 {} 參數(shù)...", count++, "TSimpleServer.Args");
TBinaryProtocol.Factory proFactory = new TBinaryProtocol.Factory(true, true);
args.protocolFactory(proFactory);
logger.info("{}:設(shè)置協(xié)議工廠為 TBinaryProtocol.Factory,即:使用 TBinaryProtocol 協(xié)議", count++);
TProcessor processor = new DemoService.Processor(new DemoServiceImpl());
args.processor(processor);
logger.info("{}:設(shè)置 processor 為 {} 實(shí)例", count++, "UserServiceImpl");
TServer server = new TSimpleServer(args);
logger.info("{}:{} 實(shí)例顽铸,服務(wù)啟動(dòng)", count++, "TSimpleServer");
server.serve();
} catch (TTransportException e) {
e.printStackTrace();
}
}
}
// Demo1Client.java
package com.ann.javas.frameworks.thrifts.demo.impl;
import com.ann.javas.frameworks.thrifts.demo.beans.DemoService;
import com.ann.javas.frameworks.thrifts.demo.beans.Gender;
import com.ann.javas.frameworks.thrifts.demo.beans.UserInfo;
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;
import java.util.Scanner;
/**
* Created by liyanling on 2018/9/9.
*/
public class Demo1Client {
private static Logger logger = LoggerFactory.getLogger(Demo1Client.class);
public static String LOCAL_HOST = "127.0.0.1";
public static Scanner scanner;
public static void main(String[] args){
scanner = new Scanner(System.in);
callServer(LOCAL_HOST, Demo1Server.PORT);
scanner.close();
}
/**
* TSocket
*/
public static void callServer(String IP, int PORT) {
try {
int count = 1;
logger.info("{}:客戶端茁计,創(chuàng)建 TSocket 實(shí)例 (IP:{},PORT:{})...", count++, IP, PORT);
TTransport transport = new TSocket(LOCAL_HOST, PORT);
transport.open();
logger.info("{}:打開 socket 連接", count++);
TProtocol protocol = new TBinaryProtocol(transport);
logger.info("{}:創(chuàng)建 TBinaryProtocol 實(shí)例", count++);
DemoService.Iface client = new DemoService.Client(protocol);
logger.info("{}:創(chuàng)建 Demo1Service.Client 實(shí)例", count++);
// 測試 locate
String locateResult = client.locate();
logger.info("{}:locate 返回值 {}",count++,locateResult);
// 測試 adsRecommendation
String adsRecommendationResult = client.adsRecommendation();
logger.info("{}:adsRecommendation 返回值 {}",count++,adsRecommendationResult);
// 測試 sayHi
logger.info("{}:請輸入姓名:", count++);
String name = scanner.nextLine();
logger.info("{}:請輸入性別(GIRL / BOY):", count++);
String gender = scanner.nextLine();
logger.info("{}:請輸入體重(單位:斤):", count++);
Integer weight = Integer.valueOf(scanner.nextLine());
UserInfo userInfo = new UserInfo();
userInfo.setName(name);
userInfo.setGender(Gender.valueOf(gender));
userInfo.setWeight(weight);
String sayHiResult = client.sayHi(userInfo);
logger.info("{}:sayHi 返回值 {}",count++,sayHiResult);
transport.close();
logger.info("{}:關(guān)閉 socket 連接", count++);
} catch (Throwable t) {
t.printStackTrace();
}
}
}
分別測試 sayHi谓松、locate星压、adsRecommendation 功能,輸出日志如下:
// Demo1Server 日志
00:35:50.229 [main] INFO com.ann.javas.frameworks.thrifts.demo.impl.Demo1Server - 1:new 一個(gè) TServerSocket 實(shí)例鬼譬,指定端口號(hào)為:9081 超時(shí)時(shí)間:100000
00:35:50.249 [main] INFO com.ann.javas.frameworks.thrifts.demo.impl.Demo1Server - 2:服務(wù)端初始化 TSimpleServer.Args 參數(shù)...
00:35:50.249 [main] INFO com.ann.javas.frameworks.thrifts.demo.impl.Demo1Server - 3:設(shè)置協(xié)議工廠為 TBinaryProtocol.Factory娜膘,即:使用 TBinaryProtocol 協(xié)議
00:35:50.259 [main] INFO com.ann.javas.frameworks.thrifts.demo.impl.Demo1Server - 4:設(shè)置 processor 為 UserServiceImpl 實(shí)例
00:35:50.260 [main] INFO com.ann.javas.frameworks.thrifts.demo.impl.Demo1Server - 5:TSimpleServer 實(shí)例,服務(wù)啟動(dòng)
// Demo1Client 日志
00:35:53.312 [main] INFO com.ann.javas.frameworks.thrifts.demo.impl.Demo1Client - 1:客戶端优质,創(chuàng)建 TSocket 實(shí)例 (IP:127.0.0.1竣贪,PORT:9081)...
00:35:53.325 [main] INFO com.ann.javas.frameworks.thrifts.demo.impl.Demo1Client - 2:打開 socket 連接
00:35:53.326 [main] INFO com.ann.javas.frameworks.thrifts.demo.impl.Demo1Client - 3:創(chuàng)建 TBinaryProtocol 實(shí)例
00:35:53.328 [main] INFO com.ann.javas.frameworks.thrifts.demo.impl.Demo1Client - 4:創(chuàng)建 Demo1Service.Client 實(shí)例
00:35:53.351 [main] INFO com.ann.javas.frameworks.thrifts.demo.impl.Demo1Client - 5:locate 返回值 香港
00:35:53.363 [main] INFO com.ann.javas.frameworks.thrifts.demo.impl.Demo1Client - 6:adsRecommendation 返回值 喝匯源果汁,走健康之路
00:35:53.363 [main] INFO com.ann.javas.frameworks.thrifts.demo.impl.Demo1Client - 7:請輸入姓名:
超人
00:36:21.899 [main] INFO com.ann.javas.frameworks.thrifts.demo.impl.Demo1Client - 8:請輸入性別(GIRL / BOY):
BOY
00:36:24.783 [main] INFO com.ann.javas.frameworks.thrifts.demo.impl.Demo1Client - 9:請輸入體重(單位:斤):
230
00:36:27.376 [main] INFO com.ann.javas.frameworks.thrifts.demo.impl.Demo1Client - 10:sayHi 返回值 Hi,超人先生!你該減肥啦~
00:36:27.376 [main] INFO com.ann.javas.frameworks.thrifts.demo.impl.Demo1Client - 11:關(guān)閉 socket 連接
這個(gè)例子主要看 client 端日志巩螃,server 端的不重要演怎,一般都是 deamon 啟動(dòng)的。
不需要了解任何原理避乏,光看這個(gè)用法你就會(huì)發(fā)現(xiàn)爷耀,thrift 通過某種方式屏蔽了用戶對“序列化與反序列化”這個(gè)動(dòng)作的感知,用戶只需要:
- 按照某種規(guī)則拍皮、使用 xxx.thrift 文件定義 RPC 接口和數(shù)據(jù)結(jié)構(gòu)歹叮。
- 執(zhí)行命令生成對應(yīng)的 java 代碼跑杭。
- 然后在此基礎(chǔ)上,實(shí)現(xiàn)你的業(yè)務(wù)邏輯就可以了咆耿。
4.2 序列化分析
關(guān)于 thrift 序列化的具體實(shí)現(xiàn)德谅,可以在前面 demo 的基礎(chǔ)上,通過串調(diào)用鏈路的方式進(jìn)行了解萨螺。
需要進(jìn)行序列化和反序列化操作的無非就這么 4 個(gè)場景:
- client->server窄做,client 序列化參數(shù)。
- client->server屑迂,server 反序列化參數(shù)浸策,處理請求。
- server->client惹盼,server 序列化返回值庸汗。
- server->client,client 反序列化返回值手报。
以 “client 序列化參數(shù)” 為例進(jìn)行簡單分析蚯舱,過程如下:
- 以 sayHi() 為例,入口在 Demo1Client.java 客戶端代碼:
// Demo1Client.java
...
public class Demo1Client {
...
public static void callServer(String IP, int PORT) {
...
TProtocol protocol = new TBinaryProtocol(transport);
...
DemoService.Iface client = new DemoService.Client(protocol);
...
String sayHiResult = client.sayHi(userInfo);
...
}
}
- Command+B 點(diǎn) sayHi 進(jìn)到 DemoService.java 中的 Iface 接口定義掩蛤,這是編譯器根據(jù) IDL 自動(dòng)生成的代碼:
// DemoService.java
// Autogenerated by Thrift Compiler
public class DemoService {
public interface Iface {
public String sayHi(UserInfo userInfo) throws org.apache.thrift.TException;
...
}
...
}
- 如果你用的 IDE 是 idea 枉昏,在左側(cè)能看到一個(gè)綠色的小圓圈,鼠標(biāo)懸浮在上面揍鸟,會(huì)看到 Iface 接口定義的 sayHi() 方法有兩個(gè)實(shí)現(xiàn)類:
- ...beans.DemoService.Client兄裂,客戶端實(shí)現(xiàn),其實(shí)是 DemoService 的內(nèi)部類阳藻,編譯器自動(dòng)生成的晰奖。
-
...impl.DemoServiceImpl,服務(wù)端實(shí)現(xiàn)腥泥,繼承自 DemoService.Iface匾南,用于實(shí)現(xiàn)服務(wù)端業(yè)務(wù)邏輯。
- 點(diǎn)進(jìn)客戶端實(shí)現(xiàn)蛔外,也就是 DemoService 的內(nèi)部類 DemoService.Client蛆楞,然后你會(huì)看到下面這段代碼:
// DemoService.java
// Autogenerated by Thrift Compiler
...
public class DemoService {
...
public static class Client extends org.apache.thrift.TServiceClient implements Iface {
...
public String sayHi(UserInfo userInfo) throws org.apache.thrift.TException
{
send_sayHi(userInfo);
return recv_sayHi();
}
...
}
...
}
很明顯,send_sayHi(userInfo) 是發(fā)送請求到服務(wù)端夹厌,return recv_sayHi() 是從服務(wù)端獲取返回值豹爹。
- 本例需關(guān)注 send_sayHi(userInfo) 實(shí)現(xiàn):
- sayHi_args 又是個(gè)內(nèi)部類,定義了 sayHi() 方法參數(shù)的數(shù)據(jù)結(jié)構(gòu)矛纹,其唯一成員變量就是 UserInfo 類實(shí)例帅戒,對應(yīng)的是 demo.thrift 中定義的
string sayHi(1:required UserInfo userInfo);
。 - 先初始化一個(gè) sayHi_args 類實(shí)例崖技,準(zhǔn)備好參數(shù)逻住。
- sendBase(...) 是 TServiceClient 提供的方法,調(diào)用時(shí)指明方法名和方法參數(shù)實(shí)例迎献。
- 在本例中瞎访,方法名即:"sayHi",參數(shù)即:sayHi_args 實(shí)例吁恍。
// DemoService.java
// Autogenerated by Thrift Compiler
...
public class DemoService {
...
public static class Client extends org.apache.thrift.TServiceClient implements Iface {
...
public String sayHi(UserInfo userInfo) throws org.apache.thrift.TException
{
send_sayHi(userInfo);
return recv_sayHi();
}
public void send_sayHi(UserInfo userInfo) throws org.apache.thrift.TException
{
sayHi_args args = new sayHi_args();
args.setUserInfo(userInfo);
sendBase("sayHi", args);
}
...
}
...
}
- 進(jìn)到 TServiceClient 看 sendBase(...) 具體實(shí)現(xiàn):
...
public abstract class TServiceClient {
...
protected void sendBase(String methodName, TBase<?, ?> args) throws TException {
this.sendBase(methodName, args, (byte)1);
}
...
private void sendBase(String methodName, TBase<?, ?> args, byte type) throws TException {
this.oprot_.writeMessageBegin(new TMessage(methodName, type, ++this.seqid_));
args.write(this.oprot_);
this.oprot_.writeMessageEnd();
this.oprot_.getTransport().flush();
}
...
}
可以看到 sendBase(...) 是按照:MessageBegin扒秸、MessageBody、MessageEnd 的順序?qū)⑾懭?outputStream 冀瓦。
- writeMessageBegin 和 writeMessageEnd 操作伴奥,不同協(xié)議(TProtocol 子類)的實(shí)現(xiàn)各有不同。
- writeMessageBody 則由方法的參數(shù)結(jié)構(gòu)體自己實(shí)現(xiàn)翼闽,也就是
args.write(this.oprot_);
這一行拾徙。
- 跟進(jìn) args.write(this.oprot_); 里面,其實(shí)還是編譯器根據(jù) IDL 定義自動(dòng)生成的代碼:
// DemoService.java
// Autogenerated by Thrift Compiler
...
public class DemoService {
...
private static class sayHi_argsStandardScheme extends StandardScheme<sayHi_args> {
...
public void write(org.apache.thrift.protocol.TProtocol oprot, sayHi_args struct) throws org.apache.thrift.TException {
struct.validate();
oprot.writeStructBegin(STRUCT_DESC);
if (struct.userInfo != null) {
oprot.writeFieldBegin(USER_INFO_FIELD_DESC);
struct.userInfo.write(oprot);
oprot.writeFieldEnd();
}
oprot.writeFieldStop();
oprot.writeStructEnd();
}
}
...
}
- 在 write 操作前感局,有一個(gè) validate 校驗(yàn)尼啡,主要是校驗(yàn)參數(shù)格式(required 字段非null)。
- 剩下的應(yīng)該不用多介紹了询微,差不多的規(guī)則和實(shí)現(xiàn)崖瞭,到這兒往下跟就比較簡單了,不再貼代碼過來(有點(diǎn)多)撑毛。
- 也就是說书聚,一個(gè)結(jié)構(gòu)體大致會(huì)按照這個(gè)模板進(jìn)行序列化:
- ${Message Begin}
- Struct.validate()
- ${struct Begin}
- ${Field1 Begin}
- ${Feild1 Body}
- ${Field1 End}
- ${Field2 Begin}
- ${Feild2 Body}
- ${Field2 End}
- ......
- ${Field Stop}
- ${struct End}
- ${Message End}
PS:Struct 和 Feild 是可以嵌套的。
例如:sayHi_args 的 Feild1 是 userInfo藻雌,則 sayHi_args 的 Feild1Body 實(shí)際上是包含了一個(gè) UserInfo Struct 內(nèi)容雌续。
emm......有點(diǎn)繞,追幾遍調(diào)用鏈就能明白了蹦疑。
反序列化的流程不在這兒貼分析流程了西雀,感興趣的同學(xué)可以自己跟一下。
4.3 序列化方案初探
現(xiàn)在對我們前面的分析做個(gè)總結(jié)歉摧,thrift 序列化方案有3個(gè)關(guān)鍵點(diǎn):
- libthrift 包
- thrift IDL 語法規(guī)則
- thrift 編譯器
序列化和反序列化在 thrift 中是通過 “協(xié)議” 來體現(xiàn)的艇肴。
libthrift
- 首先,libthrift 中有一個(gè)抽象類
TProtocol
叁温,定義了一系列 readXXX 和 writeXXX 的方法再悼,包括前面提到過的 Message、Struct膝但、Feild 和 String冲九、Double、Map、List莺奸、I32丑孩、I64、Binary 等數(shù)據(jù)結(jié)構(gòu)的讀寫方法灭贷。 - TProtocol 有多個(gè)實(shí)現(xiàn)温学,如:
TBinaryProtocol
二進(jìn)制格式、TCompactProtocol
壓縮格式甚疟、TJSONProtocol
JSON 格式 等仗岖。 - 通常為節(jié)約帶寬,提高傳輸效率览妖,一般使用二進(jìn)制類型的傳輸協(xié)議(TBinaryProtocol)轧拄;但有時(shí)會(huì)還是會(huì)使用基于文本類型的協(xié)議,需根據(jù)項(xiàng)目/產(chǎn)品中的實(shí)際需求來確定讽膏。
- 對 TProtocol 協(xié)議的具體實(shí)現(xiàn)感興趣的同學(xué)檩电,可以通過跟蹤調(diào)用鏈的方式進(jìn)行分析,然后使用 Wireshark 等工具抓包數(shù)據(jù)進(jìn)行驗(yàn)證桅打。
若 thrift 提供的協(xié)議不能滿足要求是嗜,用戶還可以基于 TProtocol 來實(shí)現(xiàn)定制化協(xié)議。
IDL
- thrift 定義了一套 IDL 語法規(guī)則挺尾,可參見官方文檔:Thrift interface description language鹅搪。
- 用戶需嚴(yán)格按照 thrift IDL 語法規(guī)則來定義 RPC 的接口和數(shù)據(jù)結(jié)構(gòu)。
編譯器
- 指定需要編譯的文件及生成代碼的語言遭铺,執(zhí)行 thrift 編譯命令丽柿,thrift 編譯器會(huì)對 IDL 文件進(jìn)行編譯并輸出指定語言的源碼文件。
- 例如:
- IDL 定義的 service 會(huì)生成一個(gè)單獨(dú)的 Service 類魂挂,Service 類內(nèi)部的 Iface 接口包含了 IDL service 定義的所有方法甫题。
- IDL service 定義的每一個(gè)方法,其入?yún)⒑头祷刂刀紩?huì)自動(dòng)生成單獨(dú)的 Service 內(nèi)部類(function_args 和 function_result)涂召,并提供序列化/反序列化方法(read坠非、write)。
- IDL 中的 struct果正、enum炎码、exception 等定義,也會(huì)生成相應(yīng)的類秋泳,并提供 read潦闲、write 方法。
這三個(gè)要點(diǎn)結(jié)合起來迫皱,就構(gòu)成了 thrift 序列化和反序列化的整套機(jī)制歉闰。
五、thrift 服務(wù)框架
至此,thrift 的兩大關(guān)鍵點(diǎn)已經(jīng)搞定一個(gè):
1和敬、協(xié)議(序列化和反序列化)
2凹炸、服務(wù)框架(網(wǎng)絡(luò)通信)
協(xié)議(即:序列化和反序列化)反映的是“數(shù)據(jù)傳輸格式”,而對于一個(gè) RPC 框架來說概龄,再上層就是數(shù)據(jù)傳輸方式和服務(wù)模型了还惠。
5.1 demo 分析
在前面的例子中,哪一段代碼和“服務(wù)模型”或“數(shù)據(jù)傳輸方式”有關(guān)呢私杜?
...
public class Demo1Server {
...
private void startServer() {
...
TServerTransport tServerTransport = new TServerSocket(PORT, CLIENT_TIMEOUT);
TSimpleServer.Args args = new TSimpleServer.Args(tServerTransport);
...
TServer server = new TSimpleServer(args);
server.serve();
...
}
...
}
- 先看哪一句是用來啟動(dòng)服務(wù)的,顯然是
server.serve();
救欧。 - 再往上
TServer server = new TSimpleServer(args);
定義了 server 衰粹,是一個(gè) TSimpleServer 實(shí)例狰腌,這里選定的服務(wù)模型就是TSimpleServer
锐峭。 - 而初始化 server 實(shí)例時(shí)用到的
TServerTransport
就是其傳輸方式朱巨。
這次我們從上往下看适滓,先講服務(wù)模型暂刘。
5.2 TServer 服務(wù)模型
thrift 提供的服務(wù)模型可分為:單線程遥金、多線程逼裆、事件驅(qū)動(dòng) 3類蓝仲。從另一個(gè)角度也可以劃分為:阻塞服務(wù)模型办成、非阻塞服務(wù)模型 2類泡态。
thrift 的服務(wù)模型可以拿來對標(biāo)我們學(xué)習(xí)網(wǎng)絡(luò)編程時(shí)學(xué)到的網(wǎng)絡(luò)通信模型(IO模型)。
常見的有:
- TSimpleServer - 簡單的單線程服務(wù)模型迂卢,常用于測試某弦。
- TThreadPoolServer - 多線程、阻塞 IO 服務(wù)模型而克。
- TNonblockingServer - 多線程靶壮、非阻塞 IO 服務(wù)模型。
- 以及 TThreadedSelectorServer 和 THsHaServer员萍。
TSimpleServer
前例中腾降,Server 端使用的服務(wù)模型是 TSimpleServer 簡單的單線程服務(wù)模型,其特點(diǎn)是:
- 只有一個(gè)工作線程碎绎,循環(huán)監(jiān)聽新請求的到來螃壤,并完成請求的處理。
- 這種服務(wù)模型的優(yōu)點(diǎn)是使用方法簡單混卵、便于理解映穗。
- 但是一次只能接收和處理一個(gè) socket 連接,效率比較低幕随。
- 主要用于測試或演示 thrift 的工作過程蚁滋,在實(shí)際開發(fā)中很少會(huì)用到。
可以按照下面的步驟簡單做個(gè)小測試:
- 在 DemoServiceImpl 的某個(gè)方法里,加一段 Thread.sleep(xxxx) 辕录。
- 啟動(dòng)服務(wù)并建立一個(gè) clientA 連接并發(fā)起請求睦霎,當(dāng)前請求會(huì)夯在 sleep 的地方,不給客戶端返回結(jié)果走诞。
- 然后再新建一個(gè) clientB 連接并發(fā)起請求副女。
- 你會(huì)發(fā)現(xiàn),若 clientA 的請求不處理完蚣旱,server 端不會(huì)與 clientB 建立連接碑幅,clientB 需要一直等著,直到 clientA 的請求被處理完并返回結(jié)果塞绿。
TThreadPoolServer
TThreadPoolServer 靠引入 “工作線程池” 解決了 TSimpleServer 不支持并發(fā)和多連接的問題沟涨,其特點(diǎn)是:
- 使用一個(gè)專有線程負(fù)責(zé)監(jiān)聽端口、接受連接异吻,具體的業(yè)務(wù)處理則由一個(gè)工作線程池中的子線程來完成裹赴。
- 優(yōu)點(diǎn)是將監(jiān)聽請求和業(yè)務(wù)處理兩項(xiàng)工作做了拆分,在并發(fā)量較大時(shí)新連接也能夠被及時(shí)接受诀浪。
- 缺點(diǎn)是服務(wù)處理能力受限于線程池的工作能力棋返,當(dāng)并發(fā)請求大于線程池中的線程數(shù)時(shí),新請求還是要排隊(duì)等待雷猪。
TThreadServer 實(shí)現(xiàn)上還有一些細(xì)節(jié)沒畫在圖上睛竣,比如:
線程池滿如何處理?
有無異常重試機(jī)制春宣?
有無優(yōu)雅停服機(jī)制酵颁?
......
感興趣的同學(xué)可以讀源碼了解一下。
TNonblockingServer
TNonblockingServer 使用非阻塞 I/O 解決了 TSimpleServer 一個(gè)客戶端阻塞其他所有客戶端的問題月帝,其特點(diǎn)是:
- 服務(wù)啟動(dòng)時(shí)開一個(gè) SelectAcceptThread 線程躏惋,該線程內(nèi)部通過引入 java.nio.channels.Selector 選擇器(Java NIO 核心組件之一)實(shí)現(xiàn)單線程管理多個(gè)網(wǎng)絡(luò)連接的功能。
- 優(yōu)點(diǎn)是服務(wù)端可以同時(shí)處理多個(gè)客戶端連接請求嚷辅,而不需要像 TSimpleServer 那樣排隊(duì)等待簿姨。
- 缺點(diǎn)是業(yè)務(wù)處理還是單線程順序執(zhí)行,在業(yè)務(wù)處理比較復(fù)雜簸搞、耗時(shí)較長的場景下扁位,執(zhí)行效率也很難提升。
我先去補(bǔ)補(bǔ) Selector 的課再回來補(bǔ)這里的圖......
THsHaServer
THsHaServer 是 TNonblockingServer 的子類趁俊,可以簡單粗暴的理解成 TNonblockingServer 和 TThreadPoolServer 的結(jié)合版本:
- THsHaServer 在 TNonblockingServer 的基礎(chǔ)上 域仇,引入了 TThreadPoolServer 的工作線程池,用于進(jìn)行業(yè)務(wù)處理寺擂。
- 優(yōu)點(diǎn)是將業(yè)務(wù)處理過程扔到工作線程池處理暇务,主線程可以直接返回進(jìn)行下一次循環(huán)操作泼掠,效率大大提升。
- 缺點(diǎn)是垦细,由于主線程需要完成所有 socekt 的監(jiān)聽及數(shù)據(jù)讀寫工作择镇,當(dāng)并發(fā)請求數(shù)較大,且發(fā)送數(shù)據(jù)量較多時(shí)括改,監(jiān)聽 socket 上的新連接請求不能被及時(shí)接受腻豌。(TNonblockingServer 也有這個(gè)問題)
TThreadedSelectorServer
TThreadedSelectorServer 可以簡單看做是 THsHaServer 的擴(kuò)展(實(shí)際上并不是),其特點(diǎn)是:
- 在 THsHaServer 的基礎(chǔ)上嘱能,再引入一個(gè) SelectorThread 線程池吝梅,以分散網(wǎng)絡(luò)IO。
- 并提供一個(gè) SelectorThreadLoadBalancer 用作 SelectorThread 分發(fā)焰檩。
最后附上一張 TServer 精“剪”類圖:
5.3 TTransport 傳輸方式
傳輸方式這部分憔涉,首先請同學(xué)們區(qū)分 TServerTransport 和 TTransport:
-
TServerTransport
定義的是“以何種方式監(jiān)聽和處理請求”;初始化 Server 服務(wù)端實(shí)例的時(shí)候需要明確指定析苫;提供 listen()、accept()穿扳、interrupt() 等功能衩侥。 -
TTransport
定義的是“以何種形式在網(wǎng)絡(luò)上傳輸數(shù)據(jù)”,底層實(shí)現(xiàn)是對 ServerSocket 做了封裝矛物;初始化 Client 客戶端實(shí)例的時(shí)候需要明確指定茫死;提供 open()/close()、read()/write()履羞、peek()峦萎、flush() 等功能。
TTransport 和 TServerTransport 需要與 TServer 搭配使用:
- 服務(wù)端為阻塞式服務(wù)時(shí)忆首,使用 TServerSocket 爱榔;客戶端使用 TSocket 配合。
- 服務(wù)端為非阻塞式服務(wù)時(shí)糙及,使用 TNonblockingServerSocket 详幽;客戶端使用 TFramedTransport 封裝 TSocket 配合。
這里 TTransport 就是我們說的“傳輸方式”浸锨,即:how is transmitted唇聘?
- TSocket,阻塞 IO 傳輸柱搜。
- TFramedTransport迟郎,非阻塞 I/O,按幀/塊傳輸(支持定長數(shù)據(jù)發(fā)送和接收)聪蘸。
- TMemoryTransport宪肖,內(nèi)存IO表制。
- TFileTransport,文件格式傳輸匈庭,不提供java的實(shí)現(xiàn)夫凸。
- TZlibTransport,zlib壓縮傳輸阱持,不提供java的實(shí)現(xiàn)夭拌。
- TNonblockingTransport,非阻塞式I/O衷咽,用于構(gòu)建異步客戶端鸽扁。
......
不同語言對上述 Transport 的支持是不一樣的。
我個(gè)人只用過 TSocket 和 TFramedTransport镶骗,有點(diǎn)心虛桶现,慫...
5.4 小結(jié)
最后總結(jié)一下老生常談的 thrift 分層:
- Transport 傳輸層:定義了網(wǎng)絡(luò)數(shù)據(jù)的傳輸方式,負(fù)責(zé)完成數(shù)據(jù)的網(wǎng)絡(luò)傳輸鼎姊。
- TProtocol 協(xié)議層:定義了網(wǎng)絡(luò)傳輸數(shù)據(jù)的格式骡和,負(fù)責(zé)完成“應(yīng)用數(shù)據(jù)-網(wǎng)絡(luò)可傳輸?shù)臄?shù)據(jù)”的組裝和拆解(序列化和反序列化)。
- TProcessor:不重要相寇,自動(dòng)生成的處理器慰于。
- Server 服務(wù)層:把 Transport、Protocol唤衫、TProcessor 組合在一起婆赠,將服務(wù)運(yùn)行起來,在指定端口監(jiān)聽并處理客戶端請求佳励。
六休里、思考
曾經(jīng)在開發(fā)過程中遇到的問題,列在這里赃承,供感興趣的同學(xué)思考:
- 若返回值里有 map妙黍,而 map 里寫了個(gè) map.put("key",null),就會(huì)拋異常楣导,為什么废境?
- 若 Server 端和 Client 端使用的 thrift 版本不一致,可能會(huì)出現(xiàn)什么問題筒繁?
- IDL 中的 struct 如何擴(kuò)展升級(jí)噩凹?
thrift 入門的內(nèi)容差不多就到這里了,寫的心好累毡咏。
如果文中內(nèi)容有錯(cuò)誤的地方驮宴,歡迎各位大佬指出,感恩呕缭。