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 的堆棧架構。
傳輸層(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 的語言定義。
- 使用代碼生成工具,生成目標語言代碼。
- 在程序中直接使用這些代碼。
這里我們從一個簡單的 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:實現客戶端代碼,調用服務端;
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 端程序,實現簡單的服務調用了。
本篇文章《Apache Thrift 跨語言服務開發框架》由 Dennis Gao 發表自博客園,未經作者本人同意禁止任何形式的轉載,任何自動或人為的爬蟲轉載行為均為耍流氓。
文章列表