一:什么是rpc
rpc通俗來理解就是遠程調用函數,相對於本地調用來說,只需要在主調函數中調用被掉函數即可,代碼如下:
1 void fun(int i) 2 { 3 cout << "function call" << endl; 4 cout << "args: " << i << endl; 5 return; 6 } 7 8 int main() 9 { 10 fun(5); 11 return 0; 12 }
在上面的代碼中,main( )函數在第10行調用了本地函數fun( ),本地調用就是這么簡單。如果要遠程調用一個函數,那么就需要進行網絡通信,網絡通信就涉及到了網絡編程,網絡編程中有一本著名的經典書籍:《UNIX網絡編程》,簡稱UNP,這本書基本上是系統層網絡編程人員必讀書籍,但是讀過這本書的人都知道,網絡的細節很多,也較復雜,如果每個項目都需要親自寫這些底層實現,那無疑大大延緩了項目開發進度,而且很多上層開發人員不懂得這些細節。解決辦法就是造輪子,以庫的形式封裝這些底層細節,屏蔽掉底層,讓其他開發人員可以簡單直接使用。
二:thrift的安裝
thrift 就是前面提到的一種rpc庫。它是跨語言的,並且是C/S模式。thrift 的安裝與使用過程在其官網是有說明的:https://thrift.apache.org/
本文安裝過程基於官網教程,安裝環境為:Ubuntu 16.04 x64,下面是具體過程:
1. 首先需要下載 thrift 工具,下載地址為:https://thrift.apache.org/download
2. 然后需要編譯安裝該 thrift 工具,安裝教程在此處有詳細說明,本文是依據“Debian/Ubuntu install”的安裝說明進行的,步驟如下:
首先安裝 thrift 編譯需要的基本依賴組件。
sudo apt-get install automake bison flex git libboost-all-dev libevent-dev libssl-dev libtool make pkg-config build-essential g++
由於我們是利用 thrift 進行 C++ 與 Go 之間的通信,因此還要安裝 Go 編譯環境。如果沒有安裝 Go 編譯環境,編譯thrift工具時會看到對 Go 的支持為 NO。具體 Go 編譯環境請自行參考相關安裝教程。
接下來開始配置 thrift 工具:
這里下載的是0.10.0版本,測試發現0.11.0版本有一些問題,缺少某些動態庫,另外Python庫的編譯在Ubuntu上也存在頭文件路徑錯誤,而CentOS則沒有這個問題。所以建議使用0.10.0版本。
wget "https://mirrors.tuna.tsinghua.edu.cn/apache/thrift/0.10.0/thrift-0.10.0.tar.gz" tar zxvf thrift-0.10.0.tar.gz cd thrift-0.10.0
./configure
如果依賴安裝妥當且配置無誤,則會看到下面輸出:
這里會列出 thrift 工具構建了哪種語言支持的庫,其后還有該語言庫相關的詳細說明,如下:
接下來開始編譯和安裝:
make && make install //如果內存夠大、CPU夠多,可使用 make -j && make install
由於 configure 配置時沒有指定安裝的路徑,因此這里的 make install 安裝到系統默認路徑"/usr/local/bin/"下,需要root權限。
需要注意的是,編譯 thrift 時,thrift 可能會通過網絡利用"go get"工具來安裝一些第三方庫,如"golang.org/x/net/context",該庫由於一些"網絡問題"會安裝失敗,進而導致編譯失敗,本文利用了命令行的"http_proxy"環境變量設置了相關代理,個人用戶請自行准備好代理工具以解決該問題。
安裝完成后,thrift工具就可以使用了,如下:
3. 編寫 thrift 文件並轉換為代碼
在安裝完成 thrift 工具之后,我們需要用 thrift 定義的語法來編寫 thrift 文件,再使用 thrift 工具來轉換 thrift 文件以生成代碼,最后將生成的代碼集成到項目中使用。架構圖如下:
下面是詳細步驟:
cd mkdir thrift cd thrift/ vim timeRPC.thrift thrift --gen cpp timeRPC.thrift
以上命令是在我的機器上個人home目錄下創建了一個叫做thrift的文件夾並在該文件夾下編寫了一個叫做 "timeRPC.thrift" 的 thrift 文件。然后使用 thrift 工具轉換該文件生成了 C++ 的服務端代碼。
這里的例子很簡單,模仿UNIX的daytime服務,提供“時間問答”,允許客戶端向服務端詢問現在是幾點,服務端會把現在的Unix時間回送給客戶端。timeRPC.thrift文件的內容如下:
service timeServe {
i32 getCurrtentTime()
}
這里的service相當於C++的類、Go的包,而getCurrentTime( )是對應的成員/包函數,函數體內是具體的功能實現。
如果上面的轉換操作成功,會在當前目錄下生成一個叫做 "gen-cpp" 的文件夾,里面包含了需要的代碼,如下圖:
總共生成了7個文件,其中最后一個文件是我們的"main.cpp"文件,我們需要對它進行適當修改以使用。
先重命名一下,如下圖:
三:利用thrift進行C++與Go通信
在經過前面步驟之后,我們已經得到了C++版本的代碼,這些代碼需要分別引入服務端和客戶端以進行通信使用。
在前面我們已經得到了C++服務端的代碼,服務端代碼的源文件被重名成了"main.cpp",代碼內容如下:
1 // This autogenerated skeleton file illustrates how to build a server. 2 // You should copy it to another filename to avoid overwriting it. 3 4 #include "timeServe.h" 5 #include <thrift/protocol/TBinaryProtocol.h> 6 #include <thrift/server/TSimpleServer.h> 7 #include <thrift/transport/TServerSocket.h> 8 #include <thrift/transport/TBufferTransports.h> 9 10 using namespace ::apache::thrift; 11 using namespace ::apache::thrift::protocol; 12 using namespace ::apache::thrift::transport; 13 using namespace ::apache::thrift::server; 14 15 using boost::shared_ptr; 16 17 18 class timeServeHandler : virtual public timeServeIf 19 { 20 public: 21 timeServeHandler() 22 { 23 // Your initialization goes here 24 } 25 26 int32_t getCurrtentTime() 27 { 28 // Your implementation goes here 29 auto t = time(nullptr); 30 printf("getCurrtentTime: %ld\n", t); 31 return t; 32 } 33 34 }; 35 36 int main(int argc, char **argv) 37 { 38 int port = 9090; 39 shared_ptr<timeServeHandler> handler(new timeServeHandler()); 40 shared_ptr<TProcessor> processor(new timeServeProcessor(handler)); 41 shared_ptr<TServerTransport> serverTransport(new TServerSocket(port)); 42 shared_ptr<TTransportFactory> transportFactory(new TBufferedTransportFactory()); 43 shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory()); 44 45 TSimpleServer server(processor, serverTransport, transportFactory, protocolFactory); 46 server.serve(); 47 return 0; 48 }
這里需要修改的是第28-31行,這個函數具體實現就是我們需要的功能,結果從函數返回值獲得。這里是調用C標准庫的time( )函數來獲得一個Unix時間戳,在服務端打印該時間,並且將這個時間戳返回給客戶端。
對它進行編譯:
g++ -std=c++11 -o cpp-server timeRPC_constants.cpp timeRPC_types.cpp timeServe.cpp main.cpp -lthrift
這里代碼使用了C++11的auto推斷功能,因此使用-std=c++11選項,另外還需要鏈接thrift庫。結果如下:
C++客戶端代碼如下:
// system #include <iostream> // lib #include <thrift/protocol/TBinaryProtocol.h> #include <thrift/transport/TSocket.h> #include <thrift/transport/TTransportUtils.h> #include <boost/shared_ptr.hpp> using namespace apache::thrift; using namespace apache::thrift::protocol; using namespace apache::thrift::transport; using boost::shared_ptr; // project #include "gen-cpp/timeServe.h" int main() { // get socket auto p = new TSocket("127.0.0.1", 9090); shared_ptr<TTransport> socket(p); // choose transport:Socket/Http/File auto q = new TBufferedTransport(socket); shared_ptr<TTransport> transport(q); // serialize:Binary/Compact/JSON auto r = new TBinaryProtocol(transport); shared_ptr<TProtocol> protocol(r); timeServeClient client(protocol); // open connect transport->open(); auto timeNow = client.getCurrtentTime(); std::cout << timeNow << std::endl; transport->close(); return 0; }
對它進行編譯:
g++ -std=c++11 -o cpp-client client.cpp gen-cpp/timeServe.cpp -lthrift
這里在gen-cpp的上一層目錄中創建了client.cpp文件,代碼中使用了C++11的auto推斷功能和智能指針,因此使用-std=c++11選項,另外同樣需要鏈接thrift庫。結果如下:
到這里為止,已經完成了通過thrift進行C++客戶端和服務端之間的通信,運行程序測試一下結果,如下:
從上面的截圖可以看到,運行服務端程序之后,通過客戶端程序去請求,可以得到服務端返回的時間戳,並且服務端同時也進行了打印。
接下來使用golang語言來實現客戶端。類似C++需要鏈接thrift庫一樣,golang也需要對應的thrift的package來提供支持。這個包叫做"git.apache.org/thrift.git/lib/go/thrift",可以通過下面的命令來安裝
go get git.apache.org/thrift.git/lib/go/thrift
需要注意的是,這個地址被牆了,因此需要代理訪問。當然,最終安裝的thrift包可能會編譯出錯。報類似這樣的錯誤:"not enough arguments in call to oprot.Flush
",之所以報這個錯是apache更新了thrift的接口,不兼容導致。因此,我們也可以不安裝上面的包,而是使用最前面編譯得到的golang包。如下:
注意拷貝thrift包的時候,要正確創建package的目錄,因為后面生成的服務代碼中導入的包路徑對此有要求。
除了golang的包需要拷貝到開發目錄下,我們也需要生成特定的服務代碼,如下:
然后再將生成的服務代碼拷貝到go開發環境目錄下,然后創建一個go類型的客戶端,如下:
client.go客戶端的代碼如下:
package main import ( "fmt" "os" "timerpc" "git.apache.org/thrift.git/lib/go/thrift" ) func main() { // get socket socket, err := thrift.NewTSocket("127.0.0.1:9090") // choose transport transport := thrift.NewTBufferedTransport(socket, 8192) // serialize protocolFactory := thrift.NewTBinaryProtocolFactoryDefault() client := timerpc.NewTimeServeClientFactory(transport, protocolFactory) // open connect transport.Open() defer socket.Close() timeResult, err := client.GetCurrtentTime() if err != nil { fmt.Println(err.Error()) os.Exit(2) } fmt.Println(timeResult) }
對它進行編譯:
go build client.go
運行程序測試一下結果,如下: