剛學會 C++ 的小白用這個開源框架,做個 RPC 服務(wù)要多久?
本文適合有 C++ 基礎(chǔ)的朋友

本文作者:HelloGitHub-Anthony
HelloGitHub 推出的《講解開源項目》系列,本期介紹基于 C++ 的 RPC 開源框架——rest_rpc,一個讓小白也可以快速(10 分鐘)開發(fā) RPC 服務(wù)的框架。
項目地址:https://github.com/qicosmos/rest_rpc
rest_rpc 是一個高性能、易用、跨平臺、header only 的 C++11 RPC 庫,它的目標是讓 TCP 通信變得非常簡單易用,即使不懂網(wǎng)絡(luò)通信的人也可以直接使用它、快速上手。同時使用者只需要關(guān)注自己的業(yè)務(wù)邏輯即可。

簡而言之 rest_rpc 能讓您能在沒有任何網(wǎng)絡(luò)編程相關(guān)知識的情況下通過幾行代碼快速編寫屬于自己的網(wǎng)絡(luò)程序,而且使用非常方便,是入門網(wǎng)絡(luò)編程及 RPC 框架的不二之選!
一、預(yù)備知識
1.1 什么是 RPC
RPC 是 Remote Procedure Call 即 遠程過程調(diào)用 的縮寫。
1.2 RPC 有什么用
舉個例子來講,有兩臺服務(wù)器 A、B 現(xiàn)在 A 上的程序想要遠程調(diào)用 B 上應(yīng)用提供的函數(shù)/方法,就需要通過網(wǎng)絡(luò)來傳輸調(diào)用所需的消息。
但是消息的網(wǎng)絡(luò)傳輸涉及很多東西,例如:
客戶端和服務(wù)端間 TCP 連接的建立、維持和斷開
消息的序列化、編組
消息的網(wǎng)絡(luò)傳輸
消息的反序列化
等等
RPC 的作用就是屏蔽網(wǎng)絡(luò)相關(guān)操作,讓不在一個內(nèi)存空間,甚至不在一個機器內(nèi)的程序可以像調(diào)用普通函數(shù)一樣被調(diào)用。
1.3 rest_rpc 優(yōu)點
rest_rpc 有很多的優(yōu)點:
使用簡單 支持訂閱模式 允許 future和callback兩種異步調(diào)用接口,滿足不同人群愛好
二、快速開始
rest_rpc 依賴 Boost 在使用之前應(yīng)正確安裝 Boost.
2.1 安裝
通過 git clone 命令將項目下載到本地:
git?clone?https://github.com/qicosmos/rest_rpc
2.2 目錄結(jié)構(gòu)
rest_rpc 項目根目錄中文件及其意義如表所示:
| 文件名 | 作用 |
|---|---|
| doc | rest_rpc 性能測試報告 |
| examples | rest_rpc 例子,包含 client 和 server 兩部分 |
| include | rest_rpc 框架頭文件 |
| third | msgpack 支持庫,用于用序列化和反序列化消息 |
2.3 運行例程
rest_rpc 例程為 visual studio 工程,客戶端和服務(wù)端例程分別存儲在 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 寫在前面
無論 服務(wù)端 還是 客戶端 都只用包含 include/rest_rpc.hpp 這一個文件即可。
所有示例代碼都是用了如下內(nèi)容作為框架:
#include?
#include?
#include?
using?namespace?rest_rpc;
using?namespace?rest_rpc::rpc_service;
int?main(){
????//?do?something
}
3.2 編寫服務(wù)端
生成一個能提供服務(wù)的客戶端要經(jīng)歷一下幾個過程:
rpc_server對象的實例化,設(shè)置監(jiān)聽端口等屬性服務(wù)函數(shù)的注冊,定義服務(wù)端提供哪些服務(wù) 服務(wù)的啟動
1)rpc_server
rpc_server 為 rest_rpc 服務(wù)端對象,負責注冊服務(wù)、發(fā)布訂閱、線程池管理等服務(wù)端基本功能,位于 rest_rpc::rpc_service 命名空間。
使用時需要先實例化一個 rpc_server 對象并提供 監(jiān)聽端口、線程池大小,例如:
rpc_server?server(9000,?6);?//?監(jiān)聽?9000?端口,線程池大小為?6
2)服務(wù)端注冊與啟動
rpc_server 提供了 register_handler 方法注冊服務(wù)以及 run 方法啟動服務(wù)端,具體例子如下:
/*服務(wù)函數(shù)第一個參數(shù)必須為?rpc_conn,然后才是實現(xiàn)功能需要的參數(shù)(為可變參數(shù),數(shù)量可變,也可以沒有*/
std::string?hello(rpc_conn?conn,?std::string?name){?
?/*可以為?void?返回類型,代表調(diào)用后不給遠程客戶端返回消息*/
????return?("Hello?"?+?name);?/*返回給遠程客戶端的內(nèi)容*/
}
int?main(){
????rpc_server?server(9000,?6);
????
????/*func_greet?為服務(wù)名,遠程調(diào)用通過服務(wù)名確定調(diào)用函數(shù)*/
????/*hello?為函數(shù),綁定當前服務(wù)調(diào)用哪個函數(shù)*/
????server.register_handler("func_greet",?hello);
?
????server.run();//啟動服務(wù)端
????
????return?EXIT_SUCCESS;
}
其中 function 可以為 仿函數(shù) 或 lambda,例子分別如下:
使用仿函數(shù):
/*仿函數(shù)方法*/
struct?test_func{
????std::string?hello(rpc_conn?conn){
????????return?"Hello?Github!";
????}
};
int?main(){
????test_func?greeting;
????rpc_server?server(9000,?6);
????
????/*greet?為服務(wù)名,遠程調(diào)用通過服務(wù)名確定調(diào)用函數(shù)*/
????/*test_func::hello?為函數(shù),綁定當前服務(wù)調(diào)用哪個函數(shù)*/
????/*greeting?為實例化仿函數(shù)對象*/
????server.register_handler("greet",?&test_func::hello,?&greeting);
????
????server.run();//啟動服務(wù)端
????
????return?EXIT_SUCCESS;
}
使用 lambda 方法的例子:
/*使用?lambda?方法*/
int?main(){
????rpc_server?server(9000,?6);
????
????/*call_lambda?為服務(wù)名,遠程調(diào)用通過服務(wù)名確定調(diào)用函數(shù)*/
????/*[&server](rpc_conn?conn){...}?為?lambda?對象*/
????server.register_handler("call_lambda",?
????????????????????????????/*除?conn?外其他參數(shù)為可變參數(shù)*/
????????????????????????????[&server](rpc_conn?conn?/*其他參數(shù)可有可無*/)?{
????????????????????????????????std::cout?<"Hello?Github!"?<std::endl;
????????????????????????????????//?返回值可有可無
????????????????????????????});
????
????server.run();//啟動服務(wù)端
????
????return?EXIT_SUCCESS;
}
3)注冊異步服務(wù)
有時因為各種原因我們無法或者不希望一個遠程調(diào)用能同步返回(比如需要等待一個線程返回),這時候只需給 register_handler 方法一個 Async 模板參數(shù)(位于 rest_rpc 命名空間):
/*異步服務(wù)返回類型為?void*/
void?async_greet(rpc_conn?conn,?const?std::string&?name)?{
????auto?req_id?=?conn.lock()->request_id();//?異步服務(wù)需要先保存請求?id
????//?這里新建了一個線程,代表異步處理了一些任務(wù)
????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_greet",?async_greet);//?使用?Async?作為模板參數(shù)
????
????server.run();//啟動服務(wù)端
????
????return?EXIT_SUCCESS;
}
rest_rpc 支持在同一個端口上注冊多個服務(wù),例如:
server.register_handler("func_greet",?hello);
server.register_handler("greet",?&test_func::hello,?&greeting);
server.register_handler("call_lambda",?
????????????????????????/*除?conn?外其他參數(shù)為可變參數(shù)*/
????????????????????????[&server](rpc_conn?conn?/*其他參數(shù)可有可無*/)?{
????????????????????????????std::cout?<"Hello?Github!"?<std::endl;
????????????????????????????//?返回值可有可無
????????????????????????});
//?其他服務(wù)等等
server.run();
3.3 編寫客戶端
生成一個能進行遠程服務(wù)調(diào)用的客戶端要經(jīng)歷以下過程:
rpc_client對象實例化,設(shè)定服務(wù)端地址與端口連接服務(wù)端 調(diào)用服務(wù)
1)rpc_client
rpc_client 為 rest_rpc 客戶端對象,有連接服務(wù)端、調(diào)用服務(wù)端服務(wù)、序列化消息、反序列化消息等功能,位于 rest_rpc 命名空間。
使用時需要先實例化一個 rpc_client 對象,然后使用其提供的 connect 或 async_connect 方法來 同步/異步 的連接到服務(wù)器,如:
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)調(diào)用遠程服務(wù)
rpc_client 提供了 async_call 和 call 兩種方式來 異步/同步 的調(diào)用遠程服務(wù),其中 async_call 又支持 callback 和 future 兩種處理返回消息的方法,這部分介紹 同步 調(diào)用方法 call。
在調(diào)用 call 方法時如果我們的服務(wù)有返回值則需要設(shè)定模板參數(shù),比如遠程服務(wù)返回一個整數(shù)需要這樣指定返回值類型 call,如果不指定則代表無返回值。
在 編寫服務(wù)端 部分我們說過每個服務(wù)在注冊的時候都有一個名字,通過名字可以進行遠程服務(wù)的調(diào)用,現(xiàn)在我們調(diào)用 服務(wù)端 部分寫的第一個例子:
int?main(){
????/*?rest_rpc?在遇到錯誤(調(diào)用服務(wù)傳入?yún)?shù)和遠程服務(wù)需要參數(shù)不一致、連接失敗等)時會拋出異常*/
????try{
????????/*建立連接*/
????????rpc_client?client("127.0.0.1",?9000);//?IP?地址,端口號
????????/*設(shè)定超時?5s(不填默認為?3s),connect?超時返回?false,成功返回?true*/
????????bool?has_connected?=?client.connect(5);
????????/*沒有建立連接則退出程序*/
????????if?(!has_connected)?{
????????????std::cout?<"connect?timeout"?<std::endl;
????????????exit(-1);
????????}
????????/*調(diào)用遠程服務(wù),返回歡迎信息*/
????????std::string?result?=?client.call<std::string>("func_greet",?"HG");//?func_greet?為事先注冊好的服務(wù)名,需要一個?name?參數(shù),這里為?Hello?Github?的縮寫?HG
????????std::cout?<std::endl;
????}
????/*遇到連接錯誤、調(diào)用服務(wù)時參數(shù)不對等情況會拋出異常*/
????catch?(const?std::exception?&?e)?{
????????std::cout?<std::endl;
????}
????
????return?EXIT_SUCCESS;
}
當然,有些調(diào)用也許沒有任何消息返回,這是時候直接使用 client.call("xxx", ...) 即可,此時 call 方法返回類型為 void。
3)異步調(diào)用遠程服務(wù)
有些時候我們調(diào)用的遠程服務(wù)由于各種原因需要一些時間才能返回,這時候可以使用 rpc_client 提供的異步調(diào)用方法 async_call ,它默認為 callback 模式,模板參數(shù)為 timeout 時間,如想要使用 future 模式則需要特別指定。
callback 模式,回調(diào)函數(shù)形參要與例程中一樣,在調(diào)用之后需要加上 client.run():
/*默認為?call?back?模式,模板參數(shù)代表?timeout?2000ms,async_call?參數(shù)順序為?服務(wù)名,?回調(diào)函數(shù),?調(diào)用服務(wù)需要的參數(shù)(數(shù)目類型不定)*/
/*timeout?不指定則默認為?5s,設(shè)定為?0?代表不檢查?timeout?*/
client.async_call<2000>("async_greet",?
??????????????????/*在遠程服務(wù)返回時自動調(diào)用該回調(diào)函數(shù),注意形參只能這樣寫*/
??????????????????[&client](const?boost::system::error_code?&?ec,?string_view?data)?{
????????????????????????
????????????????????????auto?str?=?as<std::string>(data);
????????????????????????std::cout?<std::endl;
???????????????????},?
??????????????????"HG");//?echo?服務(wù)將傳入的參數(shù)直接返回
client.run();?//?啟動服務(wù)線程,等待返回
//?其余部分和?call?的使用方法一樣
Future 模式:
auto?f?=?client.async_call("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>();//?轉(zhuǎn)換為?string?對象,無返回值可以寫?f.get().as()
????std::cout?<std::endl;
}
3.4 序列化
使用 rest_rpc 時如果參數(shù)是標準庫相關(guān)對象則不需要單獨指定序列化方式,如果使用自定義對象,則需要使用 msgpack 定義序列化方式,例如要傳輸這樣一個結(jié)構(gòu)體:
struct?person?{
?int?id;
?std::string?name;
?int?age;
};
則需要加上 MSGPACK_DEFINE():
/*
注意:無論是服務(wù)端還是客戶端都要進行這樣的操作
客戶端和服務(wù)端?MSGPACK_DEFINE()?中的填入的參數(shù)順序必須一致,這一點和?msgpack?的序列化方式有
如客戶端和服務(wù)端中?MSGPACK_DEFINE()?中參數(shù)順序不一致可能會導(dǎo)致解包時發(fā)生錯誤
*/
struct?person?{
?int?id;
?std::string?name;
?int?age;
?MSGPACK_DEFINE(id,?name,?age);//定義需要序列化的內(nèi)容
};
在對象中也是同理:
class?person{
????private:
?????int?id;
????????std::string?name;
????????int?age;
????public:
?????MSGPACK_DEFINE(id,?name,?age);//需要在?public?中
}
然后即可將 person 作為參數(shù)類型進行使用。
四、特點:發(fā)布/訂閱模式
rest_rpc 的一大特色就是提供了 發(fā)布-訂閱 模式,這個模式在客戶端和服務(wù)端之間需要不停傳輸消息時非常有用。
服務(wù)端 只需要使用 rpc_server 的 publish 或者 publish_by_token 方法即可發(fā)布一條訂閱消息,其中如果使用 token 則訂閱者需要使用相同的 token 才能訪問,例如:
int?main()?{
????rpc_server?server(9000,?6);
????std::thread?broadcast([&server]()?{
????????while?(true)?{
????????????/*發(fā)布訂閱消息,所有訂閱了?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();//啟動服務(wù)端
????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?<std::endl;
????????});
????//?需要?token?才能正常獲得訂閱消息
????client.subscribe("secret_greet",?"www.hellogithub.com",?[](string_view?data)?{
????????std::cout?<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);
};
服務(wù)端 直接將其作為一個參數(shù)即可,例如:
person?p{?1,?"tom",?20?};
server.publish("key",?p);
客戶端 需要進行 反序列化:
client.subscribe("key",?
?????????????????[](string_view?data)?{
?????????????????????msgpack_codec?codec;
?????????????????????
?????????????????????person?p?=?codec.unpack(data.data(),?data.size());
?????????????????????std::cout?<std::endl;
?????????????????});
五、最后
RPC 有很多成熟的工業(yè)框架如:
谷歌的 grpc 百度的 brpc 等
但是相較 rest_rpc 來講配置和使用較為復(fù)雜。新手將 rest_rpc 作為 RPC 的入門項目是一個非常好的選擇。
至此,相信你已經(jīng)掌握了 rest_rpc 的絕大部分功能,那么是時候動手搞一個 RPC 服務(wù)啦!

點擊關(guān)注第一時間收到推送
▼ 點擊?閱讀原文?投稿
