RPC框架Thrift學習

1. 前言

1.1 RPC協議

RPC(Remote Procedure Call Protocol)遠程過程調用協議,它是一種通過網絡從遠程計算機程序上請求服務,而不需要了解底層網絡技術的協議。RPC協議假定某些傳輸協議的存在,如TCP或UDP,爲通信程序之間攜帶信息數據。在OSI網絡通信模型中,RPC跨越了傳輸層和應用層。RPC使得開發包括網絡分佈式多程序在內的應用程序更加容易。一個通俗的描述是:客戶端在不知道調用細節的情況下,調用存在於遠程計算機上的某個對象,就像調用本地應用程序中的對象一樣。

1.2 常見開源RPC框架

thrift RESTful dubbo gRPC
代碼規範 基於Thrift的IDL生成代碼 基於JAX-RS規範 無代碼入侵 基於.Proto生成代碼
通信協議 TCP HTTP TCP HTTP/2
序列化協議 thrift JSON 多協議支持,默認hessian protobuf
IO框架 Thrift自帶 Servlet容器 Netty Netty
負載均衡 TCP HTTP TCP HTTP/2
跨語言 多種語言 多種語言 Java 多種語言
可擴展性 一般

1.3 thrift

thrift是一個軟件框架,能夠進行可擴展和跨語言的服務開發。它結合了功能強大的軟件堆棧和代碼生成引擎,以構建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 等等編程語言間無縫結合的、高效的服務。thrift允許你定義一個簡單的定義文件中的數據類型和服務接口以作爲輸入文件,編譯器生成代碼用來方便地生成RPC客戶端和服務器通信的無縫跨編程語言。

2. Thrift 架構

Thrift 包含一個完整的堆棧結構用於構建客戶端和服務器端。下圖描繪了 Thrift 的整體架構。
thrift協議堆棧
如圖所示,圖中黃色部分是用戶實現的業務邏輯,褐色部分是根據 Thrift 定義的服務接口描述文件生成的客戶端和服務器端代碼框架,紅色部分是根據 Thrift 文件生成代碼實現數據的讀寫操作。紅色部分以下是 Thrift 的傳輸體系、協議以及底層 I/O 通信,使用 Thrift 可以很方便的定義一個服務並且選擇不同的傳輸協議和傳輸層而不用重新生成代碼。

Thrift 服務器包含用於綁定協議和傳輸層的基礎架構,它提供阻塞、非阻塞、單線程和多線程的模式運行在服務器上,可以配合服務器 / 容器一起運行,可以和現有的 J2EE 服務器 /Web 容器無縫的結合。

3. 一個簡單的 Thrift 實例

首先來一個簡單的 Thrift 實現栗子,通過簡單實例能夠快速直觀地瞭解什麼是 Thrift 以及如何使用 Thrift 構建服務。

首先根據 Thrift 的語法規範編寫腳本文件 User.thrift來創建一個實體,代碼如下:

User.thrift

namespace java com.mnmlist.domain

typedef i32 int

struct User {
    1: int id;
    3: string name;
}

User.java關鍵代碼邏輯

public class User implements org.apache.thrift.TBase<User, User._Fields>, java.io.Serializable, Cloneable {
  private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("User");

  private static final org.apache.thrift.protocol.TField ID_FIELD_DESC = new org.apache.thrift.protocol.TField("id", org.apache.thrift.protocol.TType.I32, (short)1);
  private static final org.apache.thrift.protocol.TField NAME_FIELD_DESC = new org.apache.thrift.protocol.TField("name", org.apache.thrift.protocol.TType.STRING, (short)2);

  //定義字段
  public int id; // required
  public String name; // required

  /** The set of fields this struct contains, along with convenience methods for finding and manipulating them. */
  public enum _Fields implements org.apache.thrift.TFieldIdEnum {
    ID((short)1, "id"),
    NAME((short)2, "name");

    private static final Map<String, _Fields> byName = new HashMap<String, _Fields>();

    static {
      for (_Fields field : EnumSet.allOf(_Fields.class)) {
        byName.put(field.getFieldName(), field);
      }
    }
  }
  // 序列化和反序列化邏輯
    private static class UserStandardScheme extends StandardScheme<User> {
    public void read(org.apache.thrift.protocol.TProtocol iprot, User struct) throws org.apache.thrift.TException {
      org.apache.thrift.protocol.TField schemeField;
      iprot.readStructBegin();
      while (true)
      {
        schemeField = iprot.readFieldBegin();
        if (schemeField.type == org.apache.thrift.protocol.TType.STOP) { 
          break;
        }
        switch (schemeField.id) {
          case 1: // ID
            if (schemeField.type == org.apache.thrift.protocol.TType.I32) {
              struct.id = iprot.readI32();
              struct.setIdIsSet(true);
            } else { 
              org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);
            }
            break;
          case 2: // NAME
            if (schemeField.type == org.apache.thrift.protocol.TType.STRING) {
              struct.name = iprot.readString();
              struct.setNameIsSet(true);
            } else { 
              org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);
            }
            break;
          default:
            org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);
        }
        iprot.readFieldEnd();
      }
      iprot.readStructEnd();

      // check for required fields of primitive type, which can't be checked in the validate method
      struct.validate();
    }

    public void write(org.apache.thrift.protocol.TProtocol oprot, User struct) throws org.apache.thrift.TException {
      struct.validate();

      oprot.writeStructBegin(STRUCT_DESC);
      oprot.writeFieldBegin(ID_FIELD_DESC);
      oprot.writeI32(struct.id);
      oprot.writeFieldEnd();
      if (struct.name != null) {
        oprot.writeFieldBegin(NAME_FIELD_DESC);
        oprot.writeString(struct.name);
        oprot.writeFieldEnd();
      }
      oprot.writeFieldStop();
      oprot.writeStructEnd();
    }
  }
}

由上述關鍵邏輯代碼可以看出,thrift是按“字段序號+字段類型”順序進行序列化和反序列化對象的,因此客服端和服務器端要保證上述順序是一致的,否則會問題,栗子一枚。

創建一個服務UserService,腳本如下:

UserService.thrift

namespace java com.mnmlist.service

include '../domain/User.thrift'

typedef i32 int

service UserService {
  User.User getUser(1:int id);
}

根據腳本問題生成UserService.java,生成的該類的作用就是用來處理客服端調用遠程服務的邏輯。

UserService.java關鍵代碼

public static class Client extends org.apache.thrift.TServiceClient implements Iface {
    public static class Factory implements org.apache.thrift.TServiceClientFactory<Client> {
      public Factory() {}
      public Client getClient(org.apache.thrift.protocol.TProtocol prot) {
        return new Client(prot);
      }
      public Client getClient(org.apache.thrift.protocol.TProtocol iprot, org.apache.thrift.protocol.TProtocol oprot) {
        return new Client(iprot, oprot);
      }
    }
    public com.mnmlist.domain.User getUser(int id) throws org.apache.thrift.TException
    {
      send_getUser(id);
      return recv_getUser();
    }

    public void send_getUser(int id) throws org.apache.thrift.TException
    {
      getUser_args args = new getUser_args();
      args.setId(id);
      sendBase("getUser", args);
    }

    public com.mnmlist.domain.User recv_getUser() throws org.apache.thrift.TException
    {
      getUser_result result = new getUser_result();
      receiveBase(result, "getUser");
      if (result.isSetSuccess()) {
        return result.success;
      }
      throw new org.apache.thrift.TApplicationException(org.apache.thrift.TApplicationException.MISSING_RESULT, "getUser failed: unknown result");
    }

  }

TServiceClient.java關鍵代碼

public abstract class TServiceClient {
	...
  protected TProtocol iprot_;
  protected TProtocol oprot_;
  protected int seqid_;

  protected void sendBase(String methodName, TBase args) throws TException {
    oprot_.writeMessageBegin(new TMessage(methodName, TMessageType.CALL, ++seqid_));
    args.write(oprot_);//本例中按照User.java中定義的序列化格式進行序列化
    oprot_.writeMessageEnd();
    oprot_.getTransport().flush();
  }

  protected void receiveBase(TBase result, String methodName) throws TException {
    TMessage msg = iprot_.readMessageBegin();
    if (msg.type == TMessageType.EXCEPTION) {
      TApplicationException x = TApplicationException.read(iprot_);
      iprot_.readMessageEnd();
      throw x;
    }
    result.read(iprot_);//本例中按照User.java中定義的反序列化格式進行解析
    iprot_.readMessageEnd();
  }

TMessageType.java

public final class TMessageType {
  public static final byte CALL  = 1;
  public static final byte REPLY = 2;
  public static final byte EXCEPTION = 3;
  public static final byte ONEWAY = 4;
}

TType.java

public final class TType {
  public static final byte STOP   = 0;
  public static final byte VOID   = 1;
  public static final byte BOOL   = 2;
  public static final byte BYTE   = 3;
  public static final byte DOUBLE = 4;
  public static final byte I16    = 6;
  public static final byte I32    = 8;
  public static final byte I64    = 10;
  public static final byte STRING = 11;
  public static final byte STRUCT = 12;
  public static final byte MAP    = 13;
  public static final byte SET    = 14;
  public static final byte LIST   = 15;
  public static final byte ENUM   = 16;
}

Server端請求處理過程

Server端啓動時序圖(以HelloServiceServer爲例)
server端啓動時序圖
該圖所示是 Server 啓動的過程以及服務被客戶端調用時,服務器的響應過程。從圖中我們可以看到,程序調用了 TThreadPoolServer 的 serve 方法後,server 進入阻塞監聽狀態,其阻塞在 TServerSocket 的 accept 方法上。當接收到來自客戶端的消息後,服務器發起一個新線程處理這個消息請求,原線程再次進入阻塞狀態。在新線程中,服務器通過 TBinaryProtocol 協議讀取消息內容,調用 UserServiceImpl 的 getUser 方法,並將結果寫入 getUser_result 中傳回客戶端。

TSimpleServer.java server接受請求

public class TSimpleServer extends TServer {
  private boolean stopped_ = false;
  public TSimpleServer(AbstractServerArgs args) {
    super(args);
  }
  public void serve() {
    stopped_ = false;
    serverTransport_.listen();
    setServing(true);
    while (!stopped_) {
      TTransport client = null;
      TProcessor processor = null;
      TTransport inputTransport = null;
      TTransport outputTransport = null;
      TProtocol inputProtocol = null;
      TProtocol outputProtocol = null;
      try {
        client = serverTransport_.accept();
        if (client != null) {
          processor = processorFactory_.getProcessor(client);
          inputTransport = inputTransportFactory_.getTransport(client);
          outputTransport = outputTransportFactory_.getTransport(client);
          inputProtocol = inputProtocolFactory_.getProtocol(inputTransport);
          outputProtocol = outputProtocolFactory_.getProtocol(outputTransport);
          while (processor.process(inputProtocol, outputProtocol)) {}
        }
      } //非核心邏輯略
    }
    setServing(false);
  }

TBaseProcessor.java關鍵邏輯

public abstract class TBaseProcessor<I> implements TProcessor {
  private final I iface;
  private final Map<String,ProcessFunction<I, ? extends TBase>> processMap;

  protected TBaseProcessor(I iface, Map<String, ProcessFunction<I, ? extends TBase>> processFunctionMap) {
    this.iface = iface;
    this.processMap = processFunctionMap;
  }

  @Override
  public boolean process(TProtocol in, TProtocol out) throws TException {
    TMessage msg = in.readMessageBegin();
    ProcessFunction fn = processMap.get(msg.name);
    .......
    fn.process(msg.seqid, in, out, iface);
    return true;
  }
}

ProcessFunction.java 處理具體的請求

public abstract class ProcessFunction<I, T extends TBase> {
  private final String methodName;

  public ProcessFunction(String methodName) {
    this.methodName = methodName;
  }

  public final void process(int seqid, TProtocol iprot, TProtocol oprot, I iface) throws TException {
    T args = getEmptyArgsInstance();
    try {
      args.read(iprot);
    } catch (TProtocolException e) {
      iprot.readMessageEnd();
      TApplicationException x = new TApplicationException(TApplicationException.PROTOCOL_ERROR, e.getMessage());
      oprot.writeMessageBegin(new TMessage(getMethodName(), TMessageType.EXCEPTION, seqid));
      x.write(oprot);
      oprot.writeMessageEnd();
      oprot.getTransport().flush();
      return;
    }
    iprot.readMessageEnd();
    TBase result = getResult(iface, args);
    oprot.writeMessageBegin(new TMessage(getMethodName(), TMessageType.REPLY, seqid));
    result.write(oprot);
    oprot.writeMessageEnd();
    oprot.getTransport().flush();
  }
}

Client 端調用服務時序圖
client時序圖
該圖所示是 Client 調用服務的過程以及接收到服務器端的返回值後處理結果的過程。從圖中我們可以看到,程序調用了 UserService.Client 的 getUser 方法,在 getUser 方法中,通過 send_getUser 方法發送對服務的調用請求,通過 recv_getUser 方法接收服務處理請求後返回的結果。

4. 數據類型

Thrift 腳本可定義的數據類型包括以下幾種類型:

基本類型:

bool:布爾值,true 或 false,對應 Java 的 boolean

byte:8 位有符號整數,對應 Java 的 byte

i16:16 位有符號整數,對應 Java 的 short

i32:32 位有符號整數,對應 Java 的 int

i64:64 位有符號整數,對應 Java 的 long

double:64 位浮點數,對應 Java 的 double

string:未知編碼文本或二進制字符串,對應 Java 的 String

結構體類型:

struct:定義公共的對象,類似於 C 語言中的結構體定義,在 Java 中是一個 JavaBean

容器類型:

list:對應 Java 的 ArrayList

set:對應 Java 的 HashSet

map:對應 Java 的 HashMap

異常類型:

exception:對應 Java 的 Exception

服務類型:

service:對應服務的類

5. 協議

Thrift 可以讓用戶選擇客戶端與服務端之間傳輸通信協議的類別,在傳輸協議上總體劃分爲文本 (text) 和二進制 (binary) 傳輸協議,爲節約帶寬,提高傳輸效率,一般情況下使用二進制類型的傳輸協議爲多數,有時還會使用基於文本類型的協議,這需要根據項目 / 產品中的實際需求。常用協議有以下幾種:

5.1 TBinaryProtocol —— 二進制編碼格式進行數據傳輸

binary protocol
TBinaryProtocol詳細分析見https://www.cnblogs.com/voipman/p/5125278.html

5.2 TCompactProtocol —— 高效率的、密集的二進制編碼格式進行數據傳輸

compact protocol
TCompactProtocol.Factory proFactory = new TCompactProtocol.Factory();
TCompactProtocol protocol = new TCompactProtocol(transport);

TBinaryProtocol詳細分析見https://www.cnblogs.com/voipman/p/5163267.html

5.3 TJSONProtocol —— 使用 JSON 的數據編碼協議進行數據傳輸

json protocol
TJSONProtocol.Factory proFactory = new TJSONProtocol.Factory();
TJSONProtocol protocol = new TJSONProtocol(transport);

TJSONProtocol分析詳見https://www.cnblogs.com/voipman/p/5175169.html

5.4 TSimpleJSONProtocol —— 只提供 JSON 只寫的協議,適用於通過腳本語言解析

各協議優缺點對比

協議 優點 缺點
TBinaryProtocol 易理解,mtthrift採用的即該協議 效率不夠高
TCompactProtocol 高效率的、密集 序列化和反序列化較複雜,難理解
TJSONProtocol 相對比較簡單,在網絡中以文本方式傳輸,易於抓包分析和理解 效率低

常用的傳輸層有以下幾種:
TSocket —— 使用阻塞式 I/O 進行傳輸,是最常見的模式
TFramedTransport —— 使用非阻塞方式,按塊的大小進行傳輸,類似於 Java 中的 NIO
若使用 TFramedTransport 傳輸層,其服務器必須修改爲非阻塞的服務類型,客戶端只需替換清單 4 中 TTransport 部分,代碼如下, TNonblockingServerTransport 類是構建非阻塞 socket 的抽象類,TNonblockingServerSocket 類繼承 TNonblockingServerTransport
使用 TFramedTransport 傳輸層構建的Server

TNonblockingServerTransport serverTransport; 
serverTransport = new TNonblockingServerSocket(10005); 
Hello.Processor processor = new Hello.Processor(new HelloServiceImpl()); 
TServer server = new TNonblockingServer(processor, serverTransport); 
System.out.println("Start server on port 10005 ..."); 
server.serve();

使用 TFramedTransport 傳輸層的Client

TTransport transport = new TFramedTransport(new TSocket("localhost", 10005));

TNonblockingTransport —— 使用非阻塞方式,用於構建異步客戶端

6. 服務端類型

常見的服務端類型有以下幾種:
TSimpleServer —— 單線程服務器端使用標準的阻塞式 I/O
TThreadPoolServer —— 多線程服務器端使用標準的阻塞式 I/O
TNonblockingServer —— 多線程服務器端使用非阻塞式 I/O

參考文獻:

  1. Apache Thrift - 可伸縮的跨語言服務開發框架
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章