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 - 可伸缩的跨语言服务开发框架
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章