转载

Apache Thrift 跨语言服务开发框架

Apache Thrift 是一种支持多种编程语言的远程服务调用框架,由 Facebook 于 2007 年开发,并于 2008 年进入 Apache 开源项目管理。Apache Thrift 通过 IDL 来定义 RPC 的接口和数据类型,然后通过代码生成工具来生成针对不同编程语言的代码,目前支持 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, OCaml, Delphi 等。

本文将从 C# 开发人员的角度介绍基于 Apache Thrift 的服务开发过程。

在文章《 开源跨平台数据格式化框架概览 》中主要介绍了各开源框架的数据格式化处理部分,但并没有描述消息的传输和 RPC 服务的定义。而实际上,Apache Thrift 与 Google Protocol Buffers 的一大不同点就是,Google Protocol Buffers 仅支持定义 RPC 服务接口,而 Apache Thrift 不仅支持定义 RPC 服务接口,还提供了支持 RPC 服务实现的完整的堆栈结构,并为 RPC 服务的 Server 端和 Client 端直接生成了可用代码。如下图描绘了 Thrift 的堆栈架构 。

Apache Thrift 跨语言服务开发框架

传输层(Transport)

传输层提供对网络 I/O 的抽象,通过 Transport 对客户端进行抽象,ServerTransport 对服务端进行抽象。

  • TTransport
    • TBufferedTransport
    • TFramedTransport
    • TStreamTransport
      • TSocket
      • TTLSSocket
    • THttpClient
    • TMemoryBuffer
    • TNamedPipeClientTransport
  • TServerTransport
    • TServerSocket
    • TTLSServerSocket
    • TNamedPipeServerTransport

其实,看一眼 TSocket 的源代码就可以了解事情的真相了。

 1     public TSocket(string host, int port, int timeout)  2     {  3       this.host = host;  4       this.port = port;  5       this.timeout = timeout;  6   7       InitSocket();  8     }  9  10     private void InitSocket() 11     { 12       client = new TcpClient(); 13       client.ReceiveTimeout = client.SendTimeout = timeout; 14       client.Client.NoDelay = true; 15     }

协议层(Protocol)

协议层抽象了数据结构的定义,描述了如何组织数据以进行传输,包括 Encode 和 Decode 数据处理。所以,协议层负责实现数据的序列化和反序列化机制,例如序列化 Json, XML, Plain Text, Binary, Compact Binary 等。

协议层抽象了大量的读写操作接口,以供扩展。

 1   public abstract void WriteMessageBegin(TMessage message);  2   public abstract void WriteMessageEnd();  3   public abstract void WriteStructBegin(TStruct struc);  4   public abstract void WriteStructEnd();  5   public abstract void WriteFieldBegin(TField field);  6   public abstract void WriteFieldEnd();  7   public abstract void WriteFieldStop();  8   public abstract void WriteMapBegin(TMap map);  9   public abstract void WriteMapEnd(); 10   public abstract void WriteListBegin(TList list); 11   public abstract void WriteListEnd(); 12   public abstract void WriteSetBegin(TSet set); 13   public abstract void WriteSetEnd(); 14   public abstract void WriteBool(bool b); 15   public abstract void WriteByte(sbyte b); 16   public abstract void WriteI16(short i16); 17   public abstract void WriteI32(int i32); 18   public abstract void WriteI64(long i64); 19   public abstract void WriteDouble(double d); 20   public virtual void WriteString(string s); 21   public abstract void WriteBinary(byte[] b); 22    23   public abstract TMessage ReadMessageBegin(); 24   public abstract void ReadMessageEnd(); 25   public abstract TStruct ReadStructBegin(); 26   public abstract void ReadStructEnd(); 27   public abstract TField ReadFieldBegin(); 28   public abstract void ReadFieldEnd(); 29   public abstract TMap ReadMapBegin(); 30   public abstract void ReadMapEnd(); 31   public abstract TList ReadListBegin(); 32   public abstract void ReadListEnd(); 33   public abstract TSet ReadSetBegin(); 34   public abstract void ReadSetEnd(); 35   public abstract bool ReadBool(); 36   public abstract sbyte ReadByte(); 37   public abstract short ReadI16(); 38   public abstract int ReadI32(); 39   public abstract long ReadI64(); 40   public abstract double ReadDouble(); 41   public virtual string ReadString(); 42   public abstract byte[] ReadBinary();

处理层(Processor)

Processor 封装了对输入输出流的读写操作,输入输出流也就代表着协议层处理的对象。Processor 接口定义的极其简单。

public interface TProcessor   {     bool Process(TProtocol iprot, TProtocol oprot);   }

服务层(Server)

Server 将所有功能整合到一起:

  • 创建一个 Transport;
  • 创建 Transport 使用的 I/O Protocol;
  • 为 I/O Protocol 创建 Processor;
  • 启动服务,等待客户端的连接;

通过抽象 TServer 类来提供上述整合。

  • TServer
    • TSimpleServer -- Simple single-threaded server for testing.
    • TThreadedServer -- Server that uses C# threads (as opposed to the ThreadPool) when handling requests.
    • TThreadPoolServer -- Server that uses C# built-in ThreadPool to spawn threads when handling requests.
 1     public TServer(TProcessor processor,  2               TServerTransport serverTransport,  3               TTransportFactory inputTransportFactory,  4               TTransportFactory outputTransportFactory,  5               TProtocolFactory inputProtocolFactory,  6               TProtocolFactory outputProtocolFactory,  7               LogDelegate logDelegate)  8     {  9     } 10  11     public abstract void Serve(); 12     public abstract void Stop();

Thrift 实例

使用 Thrift 的过程:

  • 编写类似于结构体的消息格式定义,使用类似于 IDL 的语言定义。
  • 使用代码生成工具,生成目标语言代码。
  • 在程序中直接使用这些代码。

Apache Thrift 跨语言服务开发框架

这里我们从一个简单的 Thrift 实例开始,对 Thrift 服务的构建进行直观的展示。创建一个简单的 CalculatorService,通过 Calculate 接口来支持 "+ - x /" 简单的计算。Thrift 文件名为 calculator.thrift。

namespace cpp com.contracts.calculator namespace java com.contracts.calculator namespace csharp Contracts  enum Operation {   Add = 1,   Subtract = 2,   Multiply = 3,   Divide = 4 }  exception DivideByZeroException {   1: string Message; }  service CalculatorService {    i32 Calculate(1:i32 x, 2:i32 y, 3:Operation op) throws (1:DivideByZeroException divisionByZero); }

上面的 calculator.thrift 实例中,

  • namespace 定义针对不同编程语言的名空间或者包;
  • enum 定义了 Calculate 需要支持的枚举类型;
  • exception 定义了 Calculate 中可能发生的异常类型;
  • service 定义了 CalculatorService 服务接口;

Apache Thrift 与 Google Protocol Buffers 的另一个不同点就是,Apache Thrift 支持对 Exception 的定义,使得在定义服务和实现服务接口时可以方便的处理服务端异常。

在命令行使用 Thrift 代码生成工具为 C# 编程语言生成代码:

thrift --gen csharp calculator.thrift

代码生成工具根据 calculator.thrift 中的定义会生成 3 个 C# 代码文件:

  • CalculatorService.cs
  • DivideByZeroException.cs
  • Operation.cs

有了这些生成的代码文件,就可以设计服务端和客户端代码了。这里,创建 3 个 solution 文件:

  • Contracts:存放生成的代码文件,共享给 Server 和 Client;
  • Server:实现服务端代码,为客户端提供服务;
  • Client:实现客户端代码,调用服务端;

Apache Thrift 跨语言服务开发框架

Contracts 代码

由于在 calculator.thrift 文件中定义了 C# 的名空间:

namespace csharp Contracts

所以生成的代码的 namespace 即为 Contracts。

namespace Contracts {   public enum Operation   {     Add = 1,     Subtract = 2,     Multiply = 3,     Divide = 4,   } }

相应的,在 CalculatorService 文件中也生成了名为 Iface 的接口定义,也就是 Server 端需要为 Client 端实现的接口。

public partial class CalculatorService {     public interface Iface {       int Calculate(int x, int y, Operation op);     }   }

Server 端实现

为了实现 CalculatorService,需要实现一个 CalculatorServiceHandler 类来实现生成的 Contracts 中的 CalculatorService.Iface 接口。

 1   public class CalculatorServiceHandler : CalculatorService.Iface  2   {  3     public int Calculate(int x, int y, Operation op)  4     {  5       switch (op)  6       {  7         case Operation.Add:  8           return x + y;  9         case Operation.Subtract: 10           return x - y; 11         case Operation.Multiply: 12           return x * y; 13         case Operation.Divide: 14           if (y == 0) 15             throw new Contracts.DivideByZeroException() 16             { 17               Message = "Cannot divide by zero." 18             }; 19           return x / y; 20       } 21  22       throw new NotImplementedException(); 23     } 24   }

上面代码中的 Operation.Divide 段,判断了当除数为 0 时将抛出 Contracts.DivideByZeroException 异常。

然后,需要启动 Server 来提供 CalculatorService 服务。将 CalculatorServiceHandler 类的实例传递给 CalculatorService.Processor 的构造函数,指定 Socket 绑定端口 8888,然后启动服务。

 1   class Program  2   {  3     static void Main(string[] args)  4     {  5       var handler = new CalculatorServiceHandler();  6       var processor = new CalculatorService.Processor(handler);  7   8       TServerTransport transport = new TServerSocket(8888);  9       TServer server = new TThreadPoolServer(processor, transport); 10  11       server.Serve(); 12  13       Console.ReadKey(); 14     } 15   }

Client 端实现

Client 端消费 Server 端的代码更加简单,基本上 Thrift 都已提供了默认的实现,需要做的就是指定地址、端口和协议。

 1   class Program  2   {  3     static void Main(string[] args)  4     {  5       var transport = new TSocket("localhost", 8888);  6       var protocol = new TBinaryProtocol(transport);  7       var client = new CalculatorService.Client(protocol);  8   9       transport.Open(); 10  11       var test1 = client.Calculate(100, 2, Operation.Add); 12       Console.WriteLine(test1); 13  14       var test2 = client.Calculate(100, 2, Operation.Subtract); 15       Console.WriteLine(test2); 16  17       var test3 = client.Calculate(100, 2, Operation.Multiply); 18       Console.WriteLine(test3); 19  20       var test4 = client.Calculate(100, 2, Operation.Divide); 21       Console.WriteLine(test4); 22  23       try 24       { 25         var test5 = client.Calculate(100, 0, Operation.Divide); 26         Console.WriteLine(test5); 27       } 28       catch (Contracts.DivideByZeroException ex) 29       { 30         Console.WriteLine(ex.Message); 31       } 32  33       Console.ReadKey(); 34     } 35   }

然后,就可以启动 Server 端和 Client 端程序,实现简单的服务调用了。

正文到此结束
Loading...