本文適合有 C++ 基礎的朋友
本文作者:HelloGitHub-Anthony
HelloGitHub 推出的《講解開源專案》系列,本期介紹基於 C++ 的 RPC 開源框架——rest_rpc,一個讓小白也可以快速(10 分鐘)開發 RPC 服務的框架。
rest_rpc 是一個高效能、易用、跨平臺、header only 的 C++11 RPC 庫,它的目標是讓 TCP 通訊變得非常簡單易用,即使不懂網路通訊的人也可以直接使用它、快速上手。同時使用者只需要關注自己的業務邏輯即可。
簡而言之 rest_rpc 能讓您能在沒有任何網路程式設計相關知識的情況下通過幾行程式碼快速編寫屬於自己的網路程式,而且使用非常方便,是入門網路程式設計及 RPC 框架的不二之選!
一、預備知識
1.1 什麼是 RPC
RPC 是 Remote Procedure Call 即 遠端過程呼叫 的縮寫。
1.2 RPC 有什麼用
舉個例子來講,有兩臺伺服器 A、B 現在 A 上的程式想要遠端呼叫 B 上應用提供的函式/方法,就需要通過網路來傳輸呼叫所需的訊息。
但是訊息的網路傳輸涉及很多東西,例如:
-
客戶端和服務端間 TCP 連線的建立、維持和斷開
-
訊息的序列化、編組
-
訊息的網路傳輸
-
訊息的反序列化
-
等等
RPC 的作用就是遮蔽網路相關操作,讓不在一個記憶體空間,甚至不在一個機器內的程式可以像呼叫普通函式一樣被呼叫。
1.3 rest_rpc 優點
rest_rpc 有很多的優點:
- 使用簡單
- 支援訂閱模式
- 允許
future
和callback
兩種非同步呼叫介面,滿足不同人群愛好
二、快速開始
rest_rpc
依賴 Boost
在使用之前應正確安裝 Boost
.
2.1 安裝
通過 git clone
命令將專案下載到本地:
git clone https://github.com/qicosmos/rest_rpc
2.2 目錄結構
rest_rpc 專案根目錄中檔案及其意義如表所示:
檔名 | 作用 |
---|---|
doc | rest_rpc 效能測試報告 |
examples | rest_rpc 例子,包含 client 和 server 兩部分 |
include | rest_rpc 框架標頭檔案 |
third | msgpack 支援庫,用於用序列化和反序列化訊息 |
2.3 執行例程
rest_rpc 例程為 visual studio 工程,客戶端和服務端例程分別儲存在 examples/client
和 examples/server
中,直接使用 visual studio 開啟 basic_client.vcxproj
或 basic_server.vcxproj
後直接編譯即可,官方例程執行效果如圖:
注意:專案需要
Boost/asio
支援,如未安裝Boost
需要先正確安裝Boost
後將Boost
新增到工程。
工程中新增 Boost
方法如下:
- 開啟工程後點選選單欄中的
專案
→屬性
(快捷鍵Alt
+F7
) - 選擇左邊的
VC++ 目錄
選項,在右邊的包含目錄
和庫目錄
中新增Boost
的根目錄
和依賴庫
後儲存
我使用的為 Boost 1.75
安裝目錄為 D:\devPack\boost_1_75_0
,配置過程如圖所示:
三、詳細教程
3.1 寫在前面
無論 服務端
還是 客戶端
都只用包含 include/rest_rpc.hpp
這一個檔案即可。
所有示例程式碼都是用瞭如下內容作為框架:
#include <iostream>
#include <rest_rpc.hpp>
#include <chrono>
using namespace rest_rpc;
using namespace rest_rpc::rpc_service;
int main(){
// do something
}
3.2 編寫服務端
生成一個能提供服務的客戶端要經歷一下幾個過程:
rpc_server
物件的例項化,設定監聽埠等屬性- 服務函式的註冊,定義服務端提供哪些服務
- 服務的啟動
1)rpc_server
rpc_server
為 rest_rpc
服務端物件,負責註冊服務、釋出訂閱、執行緒池管理等服務端基本功能,位於 rest_rpc::rpc_service
名稱空間。
使用時需要先例項化一個 rpc_server
物件並提供 監聽埠、執行緒池大小,例如:
rpc_server server(9000, 6); // 監聽 9000 埠,執行緒池大小為 6
2)服務端註冊與啟動
rpc_server
提供了 register_handler
方法註冊服務以及 run
方法啟動服務端,具體例子如下:
/*服務函式第一個引數必須為 rpc_conn,然後才是實現功能需要的引數(為可變引數,數量可變,也可以沒有*/
std::string hello(rpc_conn conn, std::string name){
/*可以為 void 返回型別,代表呼叫後不給遠端客戶端返回訊息*/
return ("Hello " + name); /*返回給遠端客戶端的內容*/
}
int main(){
rpc_server server(9000, 6);
/*func_greet 為服務名,遠端呼叫通過服務名確定呼叫函式*/
/*hello 為函式,繫結當前服務呼叫哪個函式*/
server.register_handler("func_greet", hello);
server.run();//啟動服務端
return EXIT_SUCCESS;
}
其中 function
可以為 仿函式
或 lambda
,例子分別如下:
使用仿函式:
/*仿函式方法*/
struct test_func{
std::string hello(rpc_conn conn){
return "Hello Github!";
}
};
int main(){
test_func greeting;
rpc_server server(9000, 6);
/*greet 為服務名,遠端呼叫通過服務名確定呼叫函式*/
/*test_func::hello 為函式,繫結當前服務呼叫哪個函式*/
/*greeting 為例項化仿函式物件*/
server.register_handler("greet", &test_func::hello, &greeting);
server.run();//啟動服務端
return EXIT_SUCCESS;
}
使用 lambda 方法的例子:
/*使用 lambda 方法*/
int main(){
rpc_server server(9000, 6);
/*call_lambda 為服務名,遠端呼叫通過服務名確定呼叫函式*/
/*[&server](rpc_conn conn){...} 為 lambda 物件*/
server.register_handler("call_lambda",
/*除 conn 外其他引數為可變引數*/
[&server](rpc_conn conn /*其他引數可有可無*/) {
std::cout << "Hello Github!" << std::endl;
// 返回值可有可無
});
server.run();//啟動服務端
return EXIT_SUCCESS;
}
3)註冊非同步服務
有時因為各種原因我們無法或者不希望一個遠端呼叫能同步返回(比如需要等待一個執行緒返回),這時候只需給 register_handler
方法一個 Async
模板引數(位於 rest_rpc
名稱空間):
/*非同步服務返回型別為 void*/
void async_greet(rpc_conn conn, const std::string& name) {
auto req_id = conn.lock()->request_id();// 非同步服務需要先儲存請求 id
// 這裡新建了一個執行緒,代表非同步處理了一些任務
std::thread thd([conn, req_id, name] {
std::string ret = "Hello " + name + ", Welcome to Hello Github!";
/*這裡的 conn 是一個 weak_ptr*/
auto conn_sp = conn.lock();// 使用 weak_ptr 的 lock 方法獲取一個 shared_ptr
if (conn_sp) {
/*操作完成,返回;std::move(ret) 為返回值*/
conn_sp->pack_and_response(req_id, std::move(ret));
}
});
thd.detach();
}
int main(){
rpc_server server(9000, 6);
server.register_handler<Async>("async_greet", async_greet);// 使用 Async 作為模板引數
server.run();//啟動服務端
return EXIT_SUCCESS;
}
rest_rpc 支援在同一個埠上註冊多個服務,例如:
server.register_handler("func_greet", hello);
server.register_handler("greet", &test_func::hello, &greeting);
server.register_handler("call_lambda",
/*除 conn 外其他引數為可變引數*/
[&server](rpc_conn conn /*其他引數可有可無*/) {
std::cout << "Hello Github!" << std::endl;
// 返回值可有可無
});
// 其他服務等等
server.run();
3.3 編寫客戶端
生成一個能進行遠端服務呼叫的客戶端要經歷以下過程:
rpc_client
物件例項化,設定服務端地址與埠- 連線服務端
- 呼叫服務
1)rpc_client
rpc_client
為 rest_rpc
客戶端物件,有連線服務端、呼叫服務端服務、序列化訊息、反序列化訊息等功能,位於 rest_rpc
名稱空間。
使用時需要先例項化一個 rpc_client
物件,然後使用其提供的 connect
或 async_connect
方法來 同步/非同步 的連線到伺服器,如:
rpc_client client;
bool has_connected = client.connect("127.0.0.1", 9000);//同步連線,返回是否連線成功
client.async_connect("127.0.0.1", 9000);//非同步連線,無返回值
當然,rpc_client
還提供了 enable_auto_reconnect
和 enable_auto_heartbeat
功能,用於不同情況下保持連線。
2)呼叫遠端服務
rpc_client
提供了 async_call
和 call
兩種方式來 非同步/同步 的呼叫遠端服務,其中 async_call
又支援 callback
和 future
兩種處理返回訊息的方法,這部分介紹 同步 呼叫方法 call
。
在呼叫 call
方法時如果我們的服務有返回值則需要設定模板引數,比如遠端服務返回一個整數需要這樣指定返回值型別 call<int>
,如果不指定則代表無返回值。
在 編寫服務端 部分我們說過每個服務在註冊的時候都有一個名字,通過名字可以進行遠端服務的呼叫,現在我們呼叫 服務端 部分寫的第一個例子:
int main(){
/* rest_rpc 在遇到錯誤(呼叫服務傳入引數和遠端服務需要引數不一致、連線失敗等)時會丟擲異常*/
try{
/*建立連線*/
rpc_client client("127.0.0.1", 9000);// IP 地址,埠號
/*設定超時 5s(不填預設為 3s),connect 超時返回 false,成功返回 true*/
bool has_connected = client.connect(5);
/*沒有建立連線則退出程式*/
if (!has_connected) {
std::cout << "connect timeout" << std::endl;
exit(-1);
}
/*呼叫遠端服務,返回歡迎資訊*/
std::string result = client.call<std::string>("func_greet", "HG");// func_greet 為事先註冊好的服務名,需要一個 name 引數,這裡為 Hello Github 的縮寫 HG
std::cout << result << std::endl;
}
/*遇到連線錯誤、呼叫服務時引數不對等情況會丟擲異常*/
catch (const std::exception & e) {
std::cout << e.what() << std::endl;
}
return EXIT_SUCCESS;
}
當然,有些呼叫也許沒有任何訊息返回,這是時候直接使用 client.call("xxx", ...)
即可,此時 call 方法返回型別為 void
。
3)非同步呼叫遠端服務
有些時候我們呼叫的遠端服務由於各種原因需要一些時間才能返回,這時候可以使用 rpc_client
提供的非同步呼叫方法 async_call
,它預設為 callback 模式,模板引數為 timeout 時間,如想要使用 future 模式則需要特別指定。
callback 模式,回撥函式形參要與例程中一樣,在呼叫之後需要加上 client.run()
:
/*預設為 call back 模式,模板引數代表 timeout 2000ms,async_call 引數順序為 服務名, 回撥函式, 呼叫服務需要的引數(數目型別不定)*/
/*timeout 不指定則預設為 5s,設定為 0 代表不檢查 timeout */
client.async_call<2000>("async_greet",
/*在遠端服務返回時自動呼叫該回撥函式,注意形參只能這樣寫*/
[&client](const boost::system::error_code & ec, string_view data) {
auto str = as<std::string>(data);
std::cout << str << std::endl;
},
"HG");// echo 服務將傳入的引數直接返回
client.run(); // 啟動服務執行緒,等待返回
// 其餘部分和 call 的使用方法一樣
Future 模式:
auto f = client.async_call<FUTURE>("async_greet", "HG");
if (f.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout) {
std::cout << "timeout" << std::endl;
}
else {
auto ret = f.get().as<std::string>();// 轉換為 string 物件,無返回值可以寫 f.get().as()
std::cout << ret << std::endl;
}
3.4 序列化
使用 rest_rpc 時如果引數是標準庫相關物件則不需要單獨指定序列化方式,如果使用自定義物件,則需要使用 msgpack 定義序列化方式,例如要傳輸這樣一個結構體:
struct person {
int id;
std::string name;
int age;
};
則需要加上 MSGPACK_DEFINE()
:
/*
注意:無論是服務端還是客戶端都要進行這樣的操作
客戶端和服務端 MSGPACK_DEFINE() 中的填入的引數順序必須一致,這一點和 msgpack 的序列化方式有
如客戶端和服務端中 MSGPACK_DEFINE() 中引數順序不一致可能會導致解包時發生錯誤
*/
struct person {
int id;
std::string name;
int age;
MSGPACK_DEFINE(id, name, age);//定義需要序列化的內容
};
在物件中也是同理:
class person{
private:
int id;
std::string name;
int age;
public:
MSGPACK_DEFINE(id, name, age);//需要在 public 中
}
然後即可將 person 作為引數型別進行使用。
四、特點:釋出/訂閱模式
rest_rpc 的一大特色就是提供了 釋出-訂閱 模式,這個模式在客戶端和服務端之間需要不停傳輸訊息時非常有用。
服務端 只需要使用 rpc_server
的 publish
或者 publish_by_token
方法即可釋出一條訂閱訊息,其中如果使用 token 則訂閱者需要使用相同的 token 才能訪問,例如:
int main() {
rpc_server server(9000, 6);
std::thread broadcast([&server]() {
while (true) {
/*釋出訂閱訊息,所有訂閱了 greet 的客戶端都可以獲得訊息*/
server.publish("greet", "Hello GitHub!");
/*只有訂閱了 secret_greet 並且提供了 www.hellogithub.com 作為 token 才可以獲得訊息*/
server.publish_by_token("secret_greet", "www.hellogithub.com", "Hello Github! this is secret message");
std::this_thread::sleep_for(std::chrono::seconds(1));// 等待一秒
}
});
server.run();//啟動服務端
return EXIT_SUCCESS;
}
客戶端 只需使用 rpc_client
的 subscribe
方法即可:
void test_subscribe() {
rpc_client client;
client.enable_auto_reconnect();// 自動重連
client.enable_auto_heartbeat();// 自動心跳包
bool r = client.connect("127.0.0.1", 9000);
if (!r) {
return;
}
// 直接訂閱,無 token
client.subscribe("greet", [](string_view data) {
std::cout << data << std::endl;
});
// 需要 token 才能正常獲得訂閱訊息
client.subscribe("secret_greet", "www.hellogithub.com", [](string_view data) {
std::cout << data << std::endl;
});
client.run();// 不斷執行
}
int main() {
test_subscribe();
return EXIT_SUCCESS;
}
1)訂閱時傳輸自定義物件
如果有這樣一個物件需要傳輸:
struct person {
int id;
std::string name;
int age;
MSGPACK_DEFINE(id, name, age);
};
服務端 直接將其作為一個引數即可,例如:
person p{ 1, "tom", 20 };
server.publish("key", p);
客戶端 需要進行 反序列化:
client.subscribe("key",
[](string_view data) {
msgpack_codec codec;
person p = codec.unpack<person>(data.data(), data.size());
std::cout << p.name << std::endl;
});
五、最後
RPC 有很多成熟的工業框架如:
- 谷歌的 grpc
- 百度的 brpc 等
但是相較 rest_rpc 來講配置和使用較為複雜。新手將 rest_rpc 作為 RPC 的入門專案是一個非常好的選擇。
至此,相信你已經掌握了 rest_rpc 的絕大部分功能,那麼是時候動手搞一個 RPC 服務啦!
六、參考資料
關注 HelloGitHub 公眾號 收到第一時間的更新。
還有更多開源專案的介紹和寶藏專案等待你的發掘。