RPC, 遠程過程調用(Remote Procedure Call,RPC)是一個計算機通訊協議,該協議容許運行於一臺計算機的程序程調用另外一臺計算機的上的程序。通俗講,RPC經過把網絡通信抽象爲遠程的過程調用,調用遠程的過程就像調用本地的子程序同樣方便,從而屏蔽了通信複雜性,使開發人員能夠無需關注網絡編程的細節,將更多的時間和精力放在業務邏輯自己的實現上,提升工做效率。html
RPC本質上是一種 Inter-process communication(IPC)——進程間通訊的形式。常見的進程間通訊方式如管道、共享內存是同一臺物理機上的兩個進程間的通訊,而RPC就是兩個在不一樣物理機上的進程之間的通訊。歸納的說,RPC就是在一臺機器上調用另外一臺機器上的方法,這種調用在遠程機器上對代碼的執行就像在本機上對代碼的執行同樣,只是遷移了一個執行環境而已。前端
RPC是一種C/S架構的服務模型,server端提供接口供client調用,client端向server端發送數據,server端接收client端的數據進行相關計算並將結果返回給client端。linux
執行一次RPC一般須要經歷如下步驟(摘自 Wikipedia):git
1.The client calls the client stub. The call is a local procedure call, with parameters pushed on to the stack in the normal way. 2.The client stub packs the parameters into a message and makes a system call to send the message. Packing the parameters is called marshalling. 3.The client's local operating system sends the message from the client machine to the server machine. 4.The local operating system on the server machine passes the incoming packets to the server stub. 5.The server stub unpacks the parameters from the message. Unpacking the parameters is called unmarshalling. 6.Finally, the server stub calls the server procedure. The reply traces the same steps in the reverse direction
爲了實現上述RPC步驟,許多RPC工具被研發出來。這些RPC工具大多使用「接口描述語言」 —— interface description language (IDL) 來提供跨平臺跨語言的服務調用。如今生產中用的最多的IDL是Google開源的protobuf。github
在平常開發中一般有兩種形式來使用RPC,一種是團隊內部徹底實現上述RPC的6個步驟,本身序列化數據,而後本身利用socket或者http傳輸數據,最多見的就是遊戲開發了。另外一種就是利用現成的RPC工具,這些RPC工具實現了底層的數據通訊,開發人員只須要利用IDL定義實現本身的服務便可而不用關心數據是如何通訊的,最多見的RPC工具是Facebook開源的Thrift RPC框架。本文將重點講解Thrift RPC。web
Thrift是一個跨語言的服務部署框架,最初由Facebook於2007年開發,2008年進入Apache開源項目。Thrift經過一箇中間語言(IDL, 接口定義語言)來定義RPC的接口和數據類型,而後經過一個編譯器生成不一樣語言的代碼(目前支持C++,Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk和OCaml),並由生成的代碼負責RPC協議層和傳輸層的實現。apache
Thrift其實是實現了C/S模式,經過代碼生成工具將接口定義文件生成服務器端和客戶端代碼(能夠爲不一樣語言),從而實現服務端和客戶端跨語言的支持。用戶在Thirft描述文件中聲明本身的服務,這些服務通過編譯後會生成相應語言的代碼文件,而後用戶實現服務(客戶端調用服務,服務器端提服務)即可以了。其中protocol(協議層, 定義數據傳輸格式,能夠爲二進制或者XML等)和transport(傳輸層,定義數據傳輸方式,能夠爲TCP/IP傳輸,內存共享或者文件共享等)被用做運行時庫。編程
Thrift的協議棧以下圖所示:後端
在Client和Server的最頂層都是用戶自定義的處理邏輯,也就是說用戶只須要編寫用戶邏輯,就能夠完成整套的RPC調用流程。用戶邏輯的下一層是Thrift自動生成的代碼,這些代碼主要用於結構化數據的解析,發送和接收,同時服務器端的自動生成代碼中還包含了RPC請求的轉發(Client的A調用轉發到Server A函數進行處理)。安全
協議棧的其餘模塊都是Thrift的運行時模塊:
底層IO模塊,負責實際的數據傳輸,包括Socket,文件,或者壓縮數據流等。
TTransport負責以字節流方式發送和接收Message,是底層IO模塊在Thrift框架中的實現,每個底層IO模塊都會有一個對應TTransport來負責Thrift的字節流(Byte Stream)數據在該IO模塊上的傳輸。例如TSocket對應Socket傳輸,TFileTransport對應文件傳輸。
TProtocol主要負責結構化數據組裝成Message,或者從Message結構中讀出結構化數據。TProtocol將一個有類型的數據轉化爲字節流以交給TTransport進行傳輸,或者從TTransport中讀取必定長度的字節數據轉化爲特定類型的數據。如int32會被TBinaryProtocol Encode爲一個四字節的字節數據,或者TBinaryProtocol從TTransport中取出四個字節的數據Decode爲int32。
TServer負責接收Client的請求,並將請求轉發到Processor進行處理。TServer主要任務就是高效的接受Client的請求,特別是在高併發請求的狀況下快速完成請求。
Processor(或者TProcessor)負責對Client的請求作出相應,包括RPC請求轉發,調用參數解析和用戶邏輯調用,返回值寫回等處理步驟。Processor是服務器端從Thrift框架轉入用戶邏輯的關鍵流程。Processor同時也負責向Message結構中寫入數據或者讀出數據。
Thrift的模塊設計很是好,在每個層次均可以根據本身的須要選擇合適的實現方式。同時也應該注意到Thrift目前的特性並非在全部的程序語言中都支持。例如C++實現中有TDenseProtocol沒有TTupleProtocol,而Java實現中有TTupleProtocol沒有TDenseProtocol。
利用Thrift用戶只須要作三件事:
(1). 利用IDL定義數據結構及服務 (2). 利用代碼生成工具將(1)中的IDL編譯成對應語言(如C++、JAVA),編譯後獲得基本的框架代碼 (3). 在(2)中框架代碼基礎上完成完整代碼(純C++代碼、JAVA代碼等)
爲了實現上述RPC協議棧,Thrift定義了一套IDL,封裝了server相關類, processor相關類,transport相關類,protocol相關類以及併發和時鐘管理方面的庫。下文將一一介紹。
Thrift類型系統的目標是使編程者能使用徹底在Thrift中定義的類型,而不論他們使用的是哪一種編程語言。Thrift類型系統沒有引入任何特殊的動態類型或包裝器對象,也不要求開發者編寫任何對象序列化或傳輸的代碼。Thrift IDL文件在邏輯上,是開發者對他們的數據結構進行註解的一種方法,該方法告訴代碼生成器怎樣在語言之間安全傳輸對象,所需的額外信息量最小。
bool 布爾值,真或假 byte 有符號字節 i16 16位有符號整數 i32 32位有符號整數 i64 64位有符號整數 double 64位浮點數 string 與編碼無關的文本或二進制字符串
許多語言中都沒有無符號整數類型,且沒法防止某些語言(如Python)的開發者把一個負值賦給一個整型變量,這會致使程序沒法預料的行爲。從設計角度講,無符號整型鮮少用於數學目的,實際中更長用做關鍵詞或標識符。這種狀況下,符號是可有可無的,可用有符號整型代替。
Thrift結構體定義了一個用在多種語言之間的通用對象。定義一個Thrift結構體的基本語法與C結構體定義很是類似。域可由一個整型域標識符(在該結構體的做用域內是惟一的),以及可選的默認值來標註。
struct Phone { 1: i32 id, 2: string number, 3: PhoneType type }
enum Operation { ADD = 1, SUBTRACT = 2, MULTIPLY = 3, DIVIDE = 4 }
Thrift容器是強類型的,映射爲通用編程語言中最常使用的容器。使用C++模板類來標註。有三種可用類型:
list<type>:映射爲STL vector,Java ArrayList,或腳本語言中的native array。。 set<type>: 映射爲爲STL set,Java HashSet,Python中的set,或PHP/Ruby中的native dictionary。 Map<type1,type2>:映射爲STL map,Java HashMap,PHP associative array,或Python/Ruby dictionary。
在目標語言中,定義將產生有read和write兩種方法的類型,使用Thrift TProtocol對象對對象進行序列化和傳輸。
異常在語法和功能上都與結構體相同,惟一的區別是它們使用exception關鍵詞,而非struct關鍵詞進行聲明。 生成的對象繼承自各目標編程語言中適當的異常基類,以便與任何給定語言中的本地異常處理無縫地整合。
exception InvalidOperation { 1: i32 whatOp, 2: string why }
使用Thrift類型定義服務。對一個服務的定義在語法上等同於在面向對象編程中定義一個接口(或一個純虛抽象類)。Thrift編譯器生成實現該接口的客戶與服務器存根。服務的定義以下:
service <name> { <returntype> <name>(<arguments>) [throws (<exceptions>)] ... }
一個例子:
service StringCache { void set(1:i32 key, 2:string value), string get(1:i32 key) throws (1:KeyNotFound knf), void delete(1:i32 key) }
注意: 除其餘全部定義的Thrift類型外,void也是一個有效的函數返回類型。void函數可添加一個async修飾符,產生的代碼不等待服務器的應答。 一個純void函數會向客戶端返回一個應答,保證服務器一側操做已完成。應用開發者應當心,僅當方法調用失敗是能夠接受的,或傳輸層已知可靠的狀況下,才使用async優化。
Thrift核心庫提供一個TServer抽象類。
TServer在Thrift框架中的主要任務是接收Client的請求,並轉到某個TProcessor上進行請求處理。針對不一樣的訪問規模,Thrift提供了不一樣的TServer模型。Thrift目前支持的Server模型包括:
1. TSimpleServer:使用阻塞IO的單線程服務器,主要用於調試 2. TThreadedServer:使用阻塞IO的多線程服務器。每個請求都在一個線程裏處理,併發訪問狀況下會有不少線程同時在運行。 3. TThreadPoolServer:使用阻塞IO的多線程服務器,使用線程池管理處理線程。 4. TNonBlockingServer:使用非阻塞IO的多線程服務器,使用少許線程既能夠完成大併發量的請求響應,必須使用TFramedTransport。
Thrift 使用 libevent 做爲服務的事件驅動器, libevent 其實就是 epoll更高級的封裝而已(在linux下是epoll)。處理大量更新的話,主要是在TThreadedServer和TNonblockingServer中進行選擇。TNonblockingServer可以使用少許線程處理大量併發鏈接,可是延遲較高;TThreadedServer的延遲較低。實際中,TThreadedServer的吞吐量可能會比TNonblockingServer高,可是TThreadedServer的CPU佔用要比TNonblockingServer高不少。
TServer的Benchmark能夠參考: https://github.com/m1ch1/mapkeeper/wiki/TThreadedServer-vs.-TNonblockingServer
TServer對象一般以下工做:
1) 使用TServerTransport得到一個TTransport 2) 使用TTransportFactory,可選地將原始傳輸轉換爲一個適合的應用傳輸(典型的是使用TBufferedTransportFactory) 3) 使用TProtocolFactory,爲TTransport建立一個輸入和輸出 4) 調用TProcessor對象的process()方法
恰當地分離各個層次,這樣服務器代碼無需瞭解任何正在使用的傳輸、編碼或者應用。服務器在鏈接處理、線程等方面封裝邏輯,而processor處理RPC。惟一由應用開發者編寫的代碼存在於Thrift定義文件和接口實現裏。 Facebook已部署了多種TServer實現,包括單線程的TSimpleServer,每一個鏈接一個線程的TThreadedServer,以及線程池的TThreadPoolServer。 TProcessor接口在設計上具備很是高的廣泛性。不要求一個TServer使用一個生成的TProcessor對象。應用開發者能夠很容易地編寫在TProtocol對象上操做的任何類型的服務器(例如,一個服務器能夠簡單地將一個特定的對象類型流化,而沒有任何實際的RPC方法調用)。
Thrift中定義一個server的方法以下:
TSimpleServer server( boost::make_shared<CalculatorProcessor>(boost::make_shared<CalculatorHandler>()), boost::make_shared<TServerSocket>(9090), boost::make_shared<TBufferedTransportFactory>(), boost::make_shared<TBinaryProtocolFactory>()); TThreadedServer server( boost::make_shared<CalculatorProcessorFactory>(boost::make_shared<CalculatorCloneFactory>()), boost::make_shared<TServerSocket>(9090), //port boost::make_shared<TBufferedTransportFactory>(), boost::make_shared<TBinaryProtocolFactory>()); const int workerCount = 4;//線程池容量 boost::shared_ptr<ThreadManager> threadManager = ThreadManager::newSimpleThreadManager(workerCount); threadManager->threadFactory(boost::make_shared<PlatformThreadFactory>()); threadManager->start(); TThreadPoolServer server( boost::make_shared<CalculatorProcessorFactory>(boost::make_shared<CalculatorCloneFactory>()), boost::make_shared<TServerSocket>(9090), boost::make_shared<TBufferedTransportFactory>(), boost::make_shared<TBinaryProtocolFactory>(), threadManager); TNonBlockingServer server( boost::make_shared<CalculatorProcessorFactory>(boost::make_shared<CalculatorCloneFactory>()), boost::make_shared<TServerSocket>(9090), boost::make_shared<TFramedTransportFactory>(), boost::make_shared<TBinaryProtocolFactory>(), threadManager); server.serve();//啓動server
Thrift最底層的傳輸可使用Socket,File和Zip來實現,Memory傳輸在Thrift以前的版本里有支持,Thrift 0.8裏面就再也不支持了。TTransport是與底層數據傳輸緊密相關的傳輸層。每一種支持的底層傳輸方式都存在一個與之對應的TTransport。在TTransport這一層,數據是按字節流(Byte Stream)方式處理的,即傳輸層看到的是一個又一個的字節,並把這些字節按照順序發送和接收。TTransport並不瞭解它所傳輸的數據是什麼類型,實際上傳輸層也不關心數據是什麼類型,只須要按照字節方式對數據進行發送和接收便可。數據類型的解析在TProtocol這一層完成。
TTransport具體的有如下幾個類:
TSocket:使用阻塞的TCP Socket進行數據傳輸,也是最多見的模式 THttpTransport:採用Http傳輸協議進行數據傳輸 TFileTransport:文件(日誌)傳輸類,容許client將文件傳給server,容許server將收到的數據寫到文件中 TZlibTransport:與其餘的TTransport配合使用,壓縮後對數據進行傳輸,或者將收到的數據解壓 下面幾個類主要是對上面幾個類地裝飾(採用了裝飾模式),以提升傳輸效率。 TBufferedTransport:對某個Transport對象操做的數據進行buffer,即從buffer中讀取數據進行傳輸,或者將數據直接寫入buffer TFramedTransport:同TBufferedTransport相似,也會對相關數據進行buffer,同時,它支持定長數據發送和接收(按塊的大小,進行傳輸)。 TMemoryBuffer:從一個緩衝區中讀寫數據
Thrift實現中,一個關鍵的設計選擇就是將傳輸層從代碼生成層解耦。從根本上,生成的Thrift代碼只須要知道如何讀和寫數據。數據的源和目的地可有可無,可使一個socket,一段共享內存,或本地磁盤上的一個文件。TTransport(Thrift transport)接口支持如下方法:
open Opens the tranpsort close Closes the tranport isOpen Indicates whether the transport is open read Reads from the transport write Writes to the transport flush Forces any pending writes
除以上的TTransport接口外,還有一個TServerTransport接口,用來接受或建立原始傳輸對象。它的接口以下:
open Opens the transport listen Begins listening for connections accept Returns a new client transport close Closes the transport
TProtocol的主要任務是把TTransport中的字節流轉化爲數據流(Data Stream),在TProtocol這一層就會出現具備數據類型的數據,如整型,浮點數,字符串,結構體等。TProtocol中數據雖然有了數據類型,可是TProtocol只會按照指定類型將數據讀出和寫入,而對於數據的真正用途,須要在Thrift自動生成的Server和Client中裏處理。
Thrift 可讓用戶選擇客戶端與服務端之間傳輸通訊協議的類別,在傳輸協議上整體劃分爲文本 (text) 和二進制 (binary) 傳輸協議,爲節約帶寬,提升傳輸效率,通常狀況下使用二進制類型的傳輸協議爲多數。經常使用協議有如下幾種:
TBinaryProtocol: 二進制格式 TCompactProtocol: 高效率的、密集的二進制編碼格式 TJSONProtocol: 使用 JSON 的數據編碼協議進行數據傳輸 TSimpleJSONProtocol: 提供JSON只寫協議, 生成的文件很容易經過腳本語言解析。 TDebugProtocol: 使用易懂的可讀的文本格式,以便於debug
TCompactProtocol 高效的編碼方式,使用了相似於ProtocolBuffer的Variable-Length Quantity (VLQ) 編碼方式,主要思路是對整數採用可變長度,同時儘可能利用沒有使用Bit。對於一個int32並不保證必定是4個字節編碼,實際中多是1個字節,也多是5個字節,但最可能是五個字節。TCompactProtocol並不保證必定是最優的,但多數狀況下都會比TBinaryProtocol性能要更好。
TProtocol接口很是直接,它根本上支持兩件事: 1) 雙向有序的消息傳遞; 2) 基本類型、容器及結構體的編碼。
writeMessageBegin(name, type, seq) writeMessageEnd() writeStructBegin(name) writeStructEnd() writeFieldBegin(name, type, id) writeFieldEnd() writeFieldStop() writeMapBegin(ktype, vtype, size) writeMapEnd() writeListBegin(etype, size) writeListEnd() writeSetBegin(etype, size) writeSetEnd() writeBool(bool) writeByte(byte) writeI16(i16) writeI32(i32) writeI64(i64) writeDouble(double) writeString(string) name, type, seq = readMessageBegin() readMessageEnd() name = readStructBegin() readStructEnd() name, type, id = readFieldBegin() readFieldEnd() k, v, size = readMapBegin() readMapEnd() etype, size = readListBegin() readListEnd() etype, size = readSetBegin() readSetEnd() bool = readBool() byte = readByte() i16 = readI16() i32 = readI32() i64 = readI64() double = readDouble() string = readString()
注意到每一個write函數有且僅有一個相應的read方法。WriteFieldStop()異常是一個特殊的方法,標誌一個結構的結束。讀一個結構的過程是readFieldBegin()直到遇到stop域,而後readStructEnd()。生成的代碼依靠這個調用順序,來確保一個協議編碼器所寫的每一件事,均可被一個相應的協議解碼qi讀取。 這組功能在設計上更加註重健壯性,而非必要性。例如,writeStructEnd()不是嚴格必需的,由於一個結構體的結束可用stop域表示。
Processor是由Thrift生成的TProcessor的子類,主要對TServer中一次請求的 InputProtocol和OutputTProtocol進行操做,也就是從InputProtocol中讀出Client的請求數據,向OutputProtcol中寫入用戶邏輯的返回值。Processor是TServer從Thrift框架轉到用戶邏輯的關鍵流程。同時TProcessor.process是一個很是關鍵的處理函數,由於Client全部的RPC調用都會通過該函數處理並轉發。
Thrift在生成Processor的時候,會遵照一些命名規則,能夠參考 Thrift Generator部分的介紹。
TProcessor對於一次RPC調用的處理過程能夠歸納爲:
TServer接收到RPC請求以後,調用TProcessor.process進行處理
TProcessor.process首先調用TTransport.readMessageBegin接口,讀出RPC調用的名稱和RPC調用類型。若是RPC調用類型是RPC Call,則調用TProcessor.process_fn繼續處理,對於未知的RPC調用類型,則拋出異常。
TProcessor.process_fn根據RPC調用名稱到本身的processMap中查找對應的RPC處理函數。若是存在對應的RPC處理函數,則調用該處理函數繼續進行請求響應。不存在則拋出異常。
a) 在這一步調用的處理函數,並非最終的用戶邏輯。而是對用戶邏輯的一個包裝。
b) processMap是一個標準的std::map。Key爲RPC名稱。Value是對應的RPC處理函數的函數指針。 processMap的初始化是在Processor初始化的時候進行的。Thrift雖然沒有提供對processMap作修改的API,可是仍能夠經過繼承TProcessor來實現運行時對processMap進行修改,以達到打開或關閉某些RPC調用的目的。
a) 調用RPC請求參數的解析類,從TProtocol中讀入數據完成參數解析。無論RPC調用的參數有多少個,Thrift都會將參數放到一個Struct中去。Thrift會檢查讀出參數的字段ID和字段類型是否與要求的參數匹配。對於不符合要求的參數都會跳過。這樣,RPC接口發生變化以後,舊的處理函數在不作修改的狀況,能夠經過跳過不認識的參數,來繼續提供服務。進而在RPC框架中提供了接口的多Version支持。
b) 參數解析完成以後,調用用戶邏輯,完成真正的請求響應。
c) 用戶邏輯的返回值使用返回值打包類進行打包,寫入TProtocol。
瞭解了上述提到的TProtocol,TTransport,參數解析類和返回值打包類的概念,Thrift的Client就會變得很是容易理解。
ThriftClient跟TProcessor同樣都主要操做InputProtocol和OutputProtocol,不一樣的是ThritClient將RPC調用分爲Send和receive兩個步驟。
Send步驟,將用戶的調用參數做爲一個總體的Struct寫入TProcotol,併發送到TServer。
Send結束以後,ThriftClient便馬上進入Receive狀態等待TServer的相應。對於TServer返回的響應,使用返回值解析類進行返回值解析,完成RPC調用。
Thrift的RPC接口支持不一樣Version之間的兼容性。須要注意的是:
1. 不要修改已經存在數據的字段編號 2. 新加的字段必須是optional的。以保證新生成的代碼能夠序列舊的Message。同時儘可能爲新加的字段添加默認值。 3. Required字段不能被刪除。能夠字段前加上"OBSOLETE_"來提醒後續用戶該字段已經再也不使用,同時字段編號不能複用。 4. 修改默認值對Version控制沒有影響。由於默認值不會被傳輸,而是由數據的接受者來決定。
Thrift自動生成代碼的代碼框架被直接HardCode到了代碼生成器裏,所以對生成代碼的結構進行修改須要從新編譯Thrift,並非十分方便。若是Thrift將代碼結構保存到一個模板文件裏,修改生成代碼就會相對容易一些。
自動生成的代碼就會遵照必定的命名規則。Thrift中幾種主要的命名規則爲:
1. IDLName + 」_types.h」 :用戶自定義數據類型頭文件 2. IDLName + 」_constants.h」 :用戶自定義的枚舉和常量數據類型頭文件 3. ServiceName + 「.h」 :Server端Processor定義和Client定義頭文件 4. ServericeName + 」_」 + RPC名稱 + 「_args」 :服務器端RPC參數解析類 5. ServericeName + 」_」 + RPC名稱 + 「_result」 :服務器端RPC返回值打包類 6. ServericeName + 」_」 + RPC名稱 + 「_pargs」 :客戶端RPC參數打包類 7. ServericeName + 」_」 + RPC名稱 + 「_presult」 :客戶端RPC返回值解析類 8. 「process_」 + RPC名稱:服務器端RPC調用處理函數 9. 「send_」 + RPC名稱:客戶端發送RPC請求的方法 10. 「recv_」 + RPC名稱:客戶端接收RPC返回的方法
客戶端和服務器的參數解析和返回值解析雖然針對的是一樣的數據結構,可是Thrift並無使用同一個類來完成任務,而是將客戶端和服務器的解析類分開。
當RPC調用參數含有相同信息,並須要進行相同操做的時候,對參數解析類的集中管理就會變得很是有必要了。好比在一些用Thrift實現訪問控制的系統中,每個RPC調用都會加一個參數token做爲訪問憑證,並在每個用戶函數裏進行權限檢查。使用統一的參數解析類接口的話,就能夠將分散的權限檢查集中到一塊進行處理。Thrift中有衆多的解析類,這些解析類的接口相似,可是卻沒有一個共有的基類,對參數的集中管理形成了必定的困難。若是Thrift爲解析類創建一個基類,並把解析類指針放到一個Map中,這樣參數就能夠進行集中管理,不只能夠進一步減少自動生成代碼的體積,也知足了對參數進行統一管理的需求。
Thrift面對版本化和數據定義的改變是健壯的。將階段性的改變推出到已部署的服務中的能力相當重要。系統必須可以支持從日誌文件中讀取舊數據,以及過期的客戶(服務器)向新的服務器(客戶)發送的請求。
Thrift的版本化經過域標識符實現。Thrift中,一個結構體的每個成員的域頭都用一個惟一的域標識符編碼。域標識符和類型說明符結合起來,惟一地標誌該域。Thrift定義語言支持域標識符的自動分配,但最好始終顯式地指定域標識符。標識符的指定以下所示:
struct Example { 1:i32 number=10, 2:i64 bigNumber, 3:double decimals, 4:string name="thrifty" }
爲避免手動和自動分配的標識符之間的衝突,省略了標識符的域所賦的標識符從-1開始遞減,本語言對正的標識符僅支持手動賦值。
函數參數列表裏可以、而且應當指定域標識符。事實上,參數列表不只在後端表現爲結構,實際上在編譯器前端也表現爲與結構體一樣的代碼。這容許咱們對方法參數進行版本安全的修改。
service StringCache { void set(1:i32 key, 2:string value), string get(1:i32 key) throws (1:KeyNotFound knf), void delete(1:i32 key) }
可認爲結構體是一個字典,標識符是關鍵字,而值是強類型的有名字的域。 域標識符在內部使用i16的Thrift類型。然而要注意,TProtocol抽象能以任何格式編碼標識符。
若是遇到了一個預料以外的域,它可被安全地忽視並丟棄。當一個預期的域找不到時,必須有某些方法告訴開發者該域不在。這是經過定義的對象內部的一個isset結構實現的。(Isset功能在PHP裏默認爲null,Python裏爲None,Ruby裏爲nil)。 各個Thrift結構內部的isset對象爲各個域包含一個布爾值,表示該域在結構中是否存在。接收一個結構時,應當在直接對其進行操做以前,先檢查一個域是否已設置(being set)。
class Example { public: Example() : number(10), bigNumber(0), decimals(0), name("thrifty") {} int32_t number; int64_t bigNumber; double decimals; std::string name; struct __isset { __isset() : number(false), bigNumber(false), decimals(false), name(false) {}; bool number; bool bigNumber; bool decimals; bool name; } __isset; ... }
有四種可能發生版本不匹配的狀況:
新加的域,舊客戶端,新服務器。這種狀況下,舊客戶端不發送新的域,新服務器認出該域未設置,並對過期的請求執行默認行爲。
移除的域,舊客戶端,新服務器。這種狀況下,舊客戶端發送已被移除的域,而新服務器簡單地無視它。
新加的域,新客戶端,舊服務器。新客戶端發送一箇舊服務器不識別的域。舊服務器簡單地無視該域,像平時同樣進行處理。
移除的域,新客戶端,舊服務器。這是最危險的狀況,由於舊服務器不太可能對丟失的域執行適當的默認行爲。這種情形下,建議在新客戶端以前,先推出新服務器。
Thrift當前支持五種目標語言:C++,Java,Python,Ruby和PHP。在Facebook,用C++部署的服務器占主導地位。用PHP實現的Thrift服務也已被嵌入Apache web服務器,從後端透明地接入到許多使用THttpClient實現TTransport接口的前端結構。
爲處理來自多個客戶機的同時的請求,Thrift服務要求基本的多線程。對Thrift服務器邏輯的Python和Java實現來講,隨語言發佈的標準線程庫提供了足夠的支持。對C++實現來講,不存在標準的多線程運行時庫。具體說來,不存在健壯的、輕量的和可移植的線程管理器及定時器類。爲此,Thrift實現了本身的庫,以下所述。
ThreadManager建立一池工做者線程,一旦有空閒的工做者線程,應用就能夠調度任務來執行。ThreadManager並未實現動態線程池大小的調整,但提供了原語,以便應用能基於負載添加和移除線程。Thrift把複雜的API抽象留給特定應用,提供原語以制定所指望的政策,並對當前狀態進行採樣。
TimerManager容許應用在將來某個時間點調度Runnable對象以執行。它具體的工做是容許應用按期對ThreadManager的負載進行抽樣,並根據應用的方針使線程池大小發生改變。TimerManager也能用於生成任意數量的定時器或告警事件。 TimerManager的默認實現,使用了單個線程來處理過時的Runnable對象。所以,若是一個定時器操做須要作大量工做,尤爲是若是它須要阻塞I/O,則應當在一個單獨的線程中完成。
儘管Thrift傳輸接口更直接地映射到一個阻塞I/O模型,然而Thrift基於libevent和TFramedTransport,用C++實現了一個高性能的TNonBlockingServer。這是經過使用狀態機,把全部I/O移動到一個嚴密的事件循環中來實現的。實質上,事件循環將成幀的請求讀入TMemoryBuffer對象。一旦所有請求ready,它們會被分發給TProcessor對象,該對象能直接讀取內存中的數據。
Thrift編譯器是使用C++實現的。儘管若用另外一種語言來實現,代碼行數可能會少,但使用C++可以強制語言結構的顯示定義,使代碼對新的開發者來講更容易接近。 代碼生成使用兩遍pass完成。第一遍只看include文件和類型定義。這一階段,並不檢查類型定義,由於它們可能依賴於include文件。第一次pass,全部包含的文件按順序被掃描一遍。一旦解析了include樹,第二遍pass過全部文件,將類型定義插入語法樹,若是有任何未定義的類型,則引起一個error。而後,根據語法樹生成程序。 因爲固有的複雜性以及潛在的循環依賴性,Thrift顯式地禁止前向聲明。兩個Thrift結構不能各自包含對方的一個實例。
TFileTransport經過未來的數據及數據長度成幀,並將它寫到磁盤上,來對Thrift的請求/結構做日誌。使用一個成幀的磁盤上格式,容許了更好的錯誤檢查,並有助於處理有限數目的離散事件。TFileWriterTransport使用一個交換內存中緩衝區的系統,來確保做大量數據的日誌時的高性能。一個Thrift日誌文件被分裂成某一特定大小的塊,被記入日誌的信息不容許跨越塊的邊界。若是有一個可能跨越塊邊界的消息,則添加填塞直到塊的結束,而且消息的第一個字節與下一個塊的開始對齊。將文件劃分紅塊,使從文件的一個特定點讀取及解釋數據成爲可能。
Facebook中已經大量使用了Thrift,包括搜索、日誌、手機、廣告和開發者平臺。下面討論兩種具體的使用。
Facebook搜索服務使用Thrift做爲底層協議和傳輸層。多語言的代碼生成很適合搜索,由於能夠用高效的服務器端語言(C++)進行應用開發,而且Facebook基於PHP的web應用可使用Thrift PHP庫調用搜索服務。Thrift使搜索團隊可以利用各個語言的長處,快速地開發代碼。
使用Thrift TFileTransport功能進行結構化的日誌。可認爲各服務函數定義以及它的參數是一個結構化的日誌入口,由函數名識別。
與ProtocolBuffer不一樣,Thrift不只提供了跨語言的數據序列化和反序列化機制,更提供了跨語言的RPC實現。在Thrift的框架裏,用戶只須要實現用戶邏輯便可完成從客戶端到服務器的RPC調用。因爲Thrift良好的模塊設計,用戶也能夠很是方便的根據本身的須要選擇合適的模塊。例如RPC的Server既可使用單線程的SimpleServer,也可使用更高性能的多線程NonblockingServer。
Thrift和ProtocolBuffer解決的一個主要問題是結構化數據的序列化和反序列化。與ProtocolBuffer不一樣,Thrift在結構化數據以前加入了一個MessageHeader,並使用MessageHeader來完成RPC調用。在MessageBoy上,Thrift和ProtocolBuffer的結構大體相同,每個數據字段都由Meta信息和數據信息兩部分組成。Meta的內容會隨着數據信息的不一樣而發生變化,例如在表示String類型的數據時,Meta信息中會包含一個字長信息,而表示int32類型的數據,並不須要這種Meta信息。但通常都會包含字段類型(Byte,i32,String…)和字段編號兩個Meta信息。
Thrift不只支持的程序語言比ProtocolBuffer多,並且支持的數據結構也比ProtocolBuffer要多。Thrift不只支持Byte,i32,String等基本數據類型,更是支持List,Map,Set和Struct等複雜數據類型,可是Thrift在數據序列化和反序列化上的性能要比ProtocolBuffer稍微差一些。