thrift 入門(2/2)

上接:thrift 入門(1/2)
PS:我也不想拆,但是放一起文章太長(zhǎng)無(wú)法發(fā)布。。。


四、thrift 入門

4.1 小試牛刀

首先,我們還是先用 thrift 實(shí)現(xiàn)一下前文中的需求。

按如下步驟操作:

  1. 安裝 thrift(安裝方式請(qǐng)自行百度,最新版本或稍老的版本都可以,我用的 0.9.3 )。
  2. 編寫(xiě) 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();
}
  1. 打開(kāi)終端,cd 到 demo.thrift 所在文件目錄執(zhí)行 thrift -r --gen java demo.thrift,thrift 會(huì)編譯 demo.thrift 生成三個(gè) java 源文件(分別是:DemoService.java、UserInfo.java、Gender.java,源碼太多不在這兒貼了)到 ./gen-java 目錄下。
  2. maven 引入 libthrift 包,版本須與前面安裝的 thrift 版本保持一致。
  3. 把該文件拷貝到工程目錄。
  4. 然后創(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("坐紅旗車,走中國(guó)路", "要想皮膚好,早晚用大寶", "喝匯源果汁,走健康之路", "送禮就送腦白金",
                                                        "飄柔,就是這么自信");


  @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("{}:打開(kāi) 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++);

      // 測(cè)試 locate
      String locateResult = client.locate();
      logger.info("{}:locate 返回值 {}",count++,locateResult);

      // 測(cè)試 adsRecommendation
      String adsRecommendationResult = client.adsRecommendation();
      logger.info("{}:adsRecommendation 返回值 {}",count++,adsRecommendationResult);

      // 測(cè)試 sayHi
      logger.info("{}:請(qǐng)輸入姓名:", count++);
      String name = scanner.nextLine();
      logger.info("{}:請(qǐng)輸入性別(GIRL / BOY):", count++);
      String gender = scanner.nextLine();
      logger.info("{}:請(qǐng)輸入體重(單位:斤):", 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();
    }

  }
}

分別測(cè)試 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:打開(kāi) 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:請(qǐng)輸入姓名:
超人
00:36:21.899 [main] INFO  com.ann.javas.frameworks.thrifts.demo.impl.Demo1Client - 8:請(qǐng)輸入性別(GIRL / BOY):
BOY
00:36:24.783 [main] INFO  com.ann.javas.frameworks.thrifts.demo.impl.Demo1Client - 9:請(qǐng)輸入體重(單位:斤):
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 通過(guò)某種方式屏蔽了用戶對(duì)“序列化與反序列化”這個(gè)動(dòng)作的感知,用戶只需要:

  1. 按照某種規(guī)則、使用 xxx.thrift 文件定義 RPC 接口和數(shù)據(jù)結(jié)構(gòu)。
  2. 執(zhí)行命令生成對(duì)應(yīng)的 java 代碼。
  3. 然后在此基礎(chǔ)上,實(shí)現(xiàn)你的業(yè)務(wù)邏輯就可以了。

4.2 序列化分析

關(guān)于 thrift 序列化的具體實(shí)現(xiàn),可以在前面 demo 的基礎(chǔ)上,通過(guò)串調(diào)用鏈路的方式進(jìn)行了解。

需要進(jìn)行序列化和反序列化操作的無(wú)非就這么 4 個(gè)場(chǎng)景:

  1. client->server,client 序列化參數(shù)。
  2. client->server,server 反序列化參數(shù),處理請(qǐng)求。
  3. server->client,server 序列化返回值。
  4. server->client,client 反序列化返回值。

以 “client 序列化參數(shù)” 為例進(jìn)行簡(jiǎn)單分析,過(guò)程如下:

  1. 以 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);
      ...
  }
}
  1. 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;
    ...
  }
  ...
}
  1. 如果你用的 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ù)邏輯。


    Iface.sayHi 的兩個(gè)實(shí)現(xiàn)
  1. 點(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ā)送請(qǐng)求到服務(wù)端,return recv_sayHi() 是從服務(wù)端獲取返回值。

  1. 本例需關(guān)注 send_sayHi(userInfo) 實(shí)現(xiàn):
  • sayHi_args 又是個(gè)內(nèi)部類,定義了 sayHi() 方法參數(shù)的數(shù)據(jù)結(jié)構(gòu),其唯一成員變量就是 UserInfo 類實(shí)例,對(duì)應(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);
    }
    ...
  }
  ...
}
  1. 進(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ū)⑾?xiě)入 outputStream 。

  • writeMessageBegin 和 writeMessageEnd 操作,不同協(xié)議(TProtocol 子類)的實(shí)現(xiàn)各有不同。
  • writeMessageBody 則由方法的參數(shù)結(jié)構(gòu)體自己實(shí)現(xiàn),也就是 args.write(this.oprot_); 這一行。
  1. 跟進(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),到這兒往下跟就比較簡(jiǎn)單了,不再貼代碼過(guò)來(lái)(有點(diǎn)多)。
  1. 也就是說(shuō),一個(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)在對(duì)我們前面的分析做個(gè)總結(jié),thrift 序列化方案有3個(gè)關(guān)鍵點(diǎn):

  1. libthrift 包
  2. thrift IDL 語(yǔ)法規(guī)則
  3. thrift 編譯器
序列化方案關(guān)鍵點(diǎn)

序列化和反序列化在 thrift 中是通過(guò) “協(xié)議” 來(lái)體現(xiàn)的。

libthrift

  • 首先,libthrift 中有一個(gè)抽象類 TProtocol ,定義了一系列 readXXX 和 writeXXX 的方法,包括前面提到過(guò)的 Message、Struct、Feild 和 String、Double、Map、List、I32、I64、Binary 等數(shù)據(jù)結(jié)構(gòu)的讀寫(xiě)方法。
  • 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í)際需求來(lái)確定。
  • 對(duì) TProtocol 協(xié)議的具體實(shí)現(xiàn)感興趣的同學(xué),可以通過(guò)跟蹤調(diào)用鏈的方式進(jìn)行分析,然后使用 Wireshark 等工具抓包數(shù)據(jù)進(jìn)行驗(yàn)證。

若 thrift 提供的協(xié)議不能滿足要求,用戶還可以基于 TProtocol 來(lái)實(shí)現(xiàn)定制化協(xié)議。

IDL

  • thrift 定義了一套 IDL 語(yǔ)法規(guī)則,可參見(jiàn)官方文檔:Thrift interface description language。
  • 用戶需嚴(yán)格按照 thrift IDL 語(yǔ)法規(guī)則來(lái)定義 RPC 的接口和數(shù)據(jù)結(jié)構(gòu)。

編譯器

  • 指定需要編譯的文件及生成代碼的語(yǔ)言,執(zhí)行 thrift 編譯命令,thrift 編譯器會(huì)對(duì) IDL 文件進(jìn)行編譯并輸出指定語(yǔ)言的源碼文件。
  • 例如:
    1. IDL 定義的 service 會(huì)生成一個(gè)單獨(dú)的 Service 類,Service 類內(nèi)部的 Iface 接口包含了 IDL service 定義的所有方法。
    2. IDL service 定義的每一個(gè)方法,其入?yún)⒑头祷刂刀紩?huì)自動(dòng)生成單獨(dú)的 Service 內(nèi)部類(function_args 和 function_result),并提供序列化/反序列化方法(read、write)。
    3. IDL 中的 struct、enum、exception 等定義,也會(huì)生成相應(yīng)的類,并提供 read、write 方法。

這三個(gè)要點(diǎn)結(jié)合起來(lái),就構(gòu)成了 thrift 序列化和反序列化的整套機(jī)制。


五、thrift 服務(wù)框架

至此,thrift 的兩大關(guān)鍵點(diǎn)已經(jīng)搞定一個(gè):
1、協(xié)議(序列化和反序列化)
2、服務(wù)框架(網(wǎng)絡(luò)通信)

協(xié)議(即:序列化和反序列化)反映的是“數(shù)據(jù)傳輸格式”,而對(duì)于一個(gè) RPC 框架來(lái)說(shuō),再上層就是數(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();
    ...
  }
  ...
}
  • 先看哪一句是用來(lái)啟動(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ù)模型可分為:?jiǎn)尉€程、多線程、事件驅(qū)動(dòng) 3類。從另一個(gè)角度也可以劃分為:阻塞服務(wù)模型、非阻塞服務(wù)模型 2類。

thrift 的服務(wù)模型可以拿來(lái)對(duì)標(biāo)我們學(xué)習(xí)網(wǎng)絡(luò)編程時(shí)學(xué)到的網(wǎng)絡(luò)通信模型(IO模型)。

常見(jiàn)的有:

  • TSimpleServer - 簡(jiǎn)單的單線程服務(wù)模型,常用于測(cè)試。
  • TThreadPoolServer - 多線程、阻塞 IO 服務(wù)模型。
  • TNonblockingServer - 多線程、非阻塞 IO 服務(wù)模型。
  • 以及 TThreadedSelectorServer 和 THsHaServer。

TSimpleServer

前例中,Server 端使用的服務(wù)模型是 TSimpleServer 簡(jiǎn)單的單線程服務(wù)模型,其特點(diǎn)是:

  • 只有一個(gè)工作線程,循環(huán)監(jiān)聽(tīng)新請(qǐng)求的到來(lái),并完成請(qǐng)求的處理。
  • 這種服務(wù)模型的優(yōu)點(diǎn)是使用方法簡(jiǎn)單、便于理解。
  • 但是一次只能接收和處理一個(gè) socket 連接,效率比較低。
  • 主要用于測(cè)試或演示 thrift 的工作過(guò)程,在實(shí)際開(kāi)發(fā)中很少會(huì)用到。

可以按照下面的步驟簡(jiǎn)單做個(gè)小測(cè)試:

  1. 在 DemoServiceImpl 的某個(gè)方法里,加一段 Thread.sleep(xxxx) 。
  2. 啟動(dòng)服務(wù)并建立一個(gè) clientA 連接并發(fā)起請(qǐng)求,當(dāng)前請(qǐng)求會(huì)夯在 sleep 的地方,不給客戶端返回結(jié)果。
  3. 然后再新建一個(gè) clientB 連接并發(fā)起請(qǐng)求。
  4. 你會(huì)發(fā)現(xiàn),若 clientA 的請(qǐng)求不處理完,server 端不會(huì)與 clientB 建立連接,clientB 需要一直等著,直到 clientA 的請(qǐng)求被處理完并返回結(jié)果。
TSimpleServer 示意圖

TThreadPoolServer

TThreadPoolServer 靠引入 “工作線程池” 解決了 TSimpleServer 不支持并發(fā)和多連接的問(wèn)題,其特點(diǎn)是:

  • 使用一個(gè)專有線程負(fù)責(zé)監(jiān)聽(tīng)端口、接受連接,具體的業(yè)務(wù)處理則由一個(gè)工作線程池中的子線程來(lái)完成。
  • 優(yōu)點(diǎn)是將監(jiān)聽(tīng)請(qǐng)求和業(yè)務(wù)處理兩項(xiàng)工作做了拆分,在并發(fā)量較大時(shí)新連接也能夠被及時(shí)接受。
  • 缺點(diǎn)是服務(wù)處理能力受限于線程池的工作能力,當(dāng)并發(fā)請(qǐng)求大于線程池中的線程數(shù)時(shí),新請(qǐng)求還是要排隊(duì)等待。
TThreadPoolServer 示意圖

TThreadServer 實(shí)現(xiàn)上還有一些細(xì)節(jié)沒(méi)畫(huà)在圖上,比如:
線程池滿如何處理?
有無(wú)異常重試機(jī)制?
有無(wú)優(yōu)雅停服機(jī)制?
......
感興趣的同學(xué)可以讀源碼了解一下。

TNonblockingServer
TNonblockingServer 使用非阻塞 I/O 解決了 TSimpleServer 一個(gè)客戶端阻塞其他所有客戶端的問(wèn)題,其特點(diǎn)是:

  • 服務(wù)啟動(dòng)時(shí)開(kāi)一個(gè) SelectAcceptThread 線程,該線程內(nèi)部通過(guò)引入 java.nio.channels.Selector 選擇器(Java NIO 核心組件之一)實(shí)現(xiàn)單線程管理多個(gè)網(wǎng)絡(luò)連接的功能。
  • 優(yōu)點(diǎn)是服務(wù)端可以同時(shí)處理多個(gè)客戶端連接請(qǐng)求,而不需要像 TSimpleServer 那樣排隊(duì)等待。
  • 缺點(diǎn)是業(yè)務(wù)處理還是單線程順序執(zhí)行,在業(yè)務(wù)處理比較復(fù)雜、耗時(shí)較長(zhǎng)的場(chǎng)景下,執(zhí)行效率也很難提升。

我先去補(bǔ)補(bǔ) Selector 的課再回來(lái)補(bǔ)這里的圖......

THsHaServer
THsHaServer 是 TNonblockingServer 的子類,可以簡(jiǎn)單粗暴的理解成 TNonblockingServer 和 TThreadPoolServer 的結(jié)合版本:

  • THsHaServer 在 TNonblockingServer 的基礎(chǔ)上 ,引入了 TThreadPoolServer 的工作線程池,用于進(jìn)行業(yè)務(wù)處理。
  • 優(yōu)點(diǎn)是將業(yè)務(wù)處理過(guò)程扔到工作線程池處理,主線程可以直接返回進(jìn)行下一次循環(huán)操作,效率大大提升。
  • 缺點(diǎn)是,由于主線程需要完成所有 socekt 的監(jiān)聽(tīng)及數(shù)據(jù)讀寫(xiě)工作,當(dāng)并發(fā)請(qǐng)求數(shù)較大,且發(fā)送數(shù)據(jù)量較多時(shí),監(jiān)聽(tīng) socket 上的新連接請(qǐng)求不能被及時(shí)接受。(TNonblockingServer 也有這個(gè)問(wèn)題)

TThreadedSelectorServer
TThreadedSelectorServer 可以簡(jiǎn)單看做是 THsHaServer 的擴(kuò)展(實(shí)際上并不是),其特點(diǎn)是:

  • 在 THsHaServer 的基礎(chǔ)上,再引入一個(gè) SelectorThread 線程池,以分散網(wǎng)絡(luò)IO。
  • 并提供一個(gè) SelectorThreadLoadBalancer 用作 SelectorThread 分發(fā)。

最后附上一張 TServer 精“剪”類圖:


TServer 精“剪”類圖

5.3 TTransport 傳輸方式

傳輸方式這部分,首先請(qǐng)同學(xué)們區(qū)分 TServerTransport 和 TTransport:

  • TServerTransport 定義的是“以何種方式監(jiān)聽(tīng)和處理請(qǐng)求”;初始化 Server 服務(wù)端實(shí)例的時(shí)候需要明確指定;提供 listen()、accept()、interrupt() 等功能。
  • TTransport 定義的是“以何種形式在網(wǎng)絡(luò)上傳輸數(shù)據(jù)”,底層實(shí)現(xiàn)是對(duì) 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 就是我們說(shuō)的“傳輸方式”,即:how is transmitted?

  • TSocket,阻塞 IO 傳輸。
  • TFramedTransport,非阻塞 I/O,按幀/塊傳輸(支持定長(zhǎng)數(shù)據(jù)發(fā)送和接收)。
  • TMemoryTransport,內(nèi)存IO。
  • TFileTransport,文件格式傳輸,不提供java的實(shí)現(xiàn)。
  • TZlibTransport,zlib壓縮傳輸,不提供java的實(shí)現(xiàn)。
  • TNonblockingTransport,非阻塞式I/O,用于構(gòu)建異步客戶端。
    ......

不同語(yǔ)言對(duì)上述 Transport 的支持是不一樣的。
我個(gè)人只用過(guò) TSocket 和 TFramedTransport,有點(diǎn)心虛,慫...

TServerTransport 類圖
TTransport 類圖

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)行起來(lái),在指定端口監(jiān)聽(tīng)并處理客戶端請(qǐng)求。
thrift 層次圖

六、思考

曾經(jīng)在開(kāi)發(fā)過(guò)程中遇到的問(wèn)題,列在這里,供感興趣的同學(xué)思考:

  • 若返回值里有 map,而 map 里寫(xiě)了個(gè) map.put("key",null),就會(huì)拋異常,為什么?
  • 若 Server 端和 Client 端使用的 thrift 版本不一致,可能會(huì)出現(xiàn)什么問(wèn)題?
  • IDL 中的 struct 如何擴(kuò)展升級(jí)?

thrift 入門的內(nèi)容差不多就到這里了,寫(xiě)的心好累。

如果文中內(nèi)容有錯(cuò)誤的地方,歡迎各位大佬指出,感恩。


七、參考

最后編輯于
?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)自:http://blog.csdn.net/kesonyk/article/details/50924489 ...
    晴天哥_王志閱讀 25,329評(píng)論 2 38
  • 一. 與 Thrift 的初識(shí) 也許大多數(shù)人接觸 Thrift 是從序列化開(kāi)始的。每次搜索 “java序列化” +...
    java菜閱讀 1,515評(píng)論 0 6
  • Thrift是什么? Thrift是Facebook于2007年開(kāi)發(fā)的跨語(yǔ)言的rpc服框架,提供多語(yǔ)言的編譯功能,...
    jiangmo閱讀 9,654評(píng)論 0 6
  • 步步驚心麗主題曲 EXO 為了你 伯賢 CHEN XIUMIN
    卟尼與鹿閱讀 509評(píng)論 0 1
  • 結(jié)果,都要努力追求,怎不精疲力盡?經(jīng)過(guò),需要用心體會(huì),才會(huì)回味無(wú)窮!人生,的經(jīng)過(guò),我們都要用心去經(jīng)過(guò)!結(jié)果,該來(lái)就...
    再湊熱鬧閱讀 112評(píng)論 0 0

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