剛學會 C++ 的小白用這個開源框架,做個 RPC 服務要多久?


本文適合有 C++ 基礎的朋友

本文作者:HelloGitHub-Anthony

HelloGitHub 推出的《講解開源項目》系列,本期介紹基於 C++ 的 RPC 開源框架——rest_rpc,一個讓小白也可以快速(10 分鍾)開發 RPC 服務的框架。

項目地址:https://github.com/qicosmos/rest_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 有很多的優點:

  • 使用簡單
  • 支持訂閱模式
  • 允許 futurecallback 兩種異步調用接口,滿足不同人群愛好

二、快速開始

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/clientexamples/server 中,直接使用 visual studio 打開 basic_client.vcxprojbasic_server.vcxproj 后直接編譯即可,官方例程運行效果如圖:

注意:項目需要 Boost/asio 支持,如未安裝 Boost 需要先正確安裝 Boost后將 Boost 添加到工程。

工程中添加 Boost 方法如下:

  1. 打開工程后點擊菜單欄中的 項目屬性(快捷鍵 Alt+F7)
  2. 選擇左邊的 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 編寫服務端

生成一個能提供服務的客戶端要經歷一下幾個過程:

  1. rpc_server 對象的實例化,設置監聽端口等屬性
  2. 服務函數的注冊,定義服務端提供哪些服務
  3. 服務的啟動

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 編寫客戶端

生成一個能進行遠程服務調用的客戶端要經歷以下過程:

  1. rpc_client 對象實例化,設定服務端地址與端口
  2. 連接服務端
  3. 調用服務

1)rpc_client

rpc_client rest_rpc 客戶端對象,有連接服務端、調用服務端服務、序列化消息、反序列化消息等功能,位於 rest_rpc 命名空間。

使用時需要先實例化一個 rpc_client 對象,然后使用其提供的 connectasync_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_reconnectenable_auto_heartbeat 功能,用於不同情況下保持連接。

2)調用遠程服務

rpc_client 提供了 async_callcall 兩種方式來 異步/同步 的調用遠程服務,其中 async_call 又支持 callbackfuture 兩種處理返回消息的方法,這部分介紹 同步 調用方法 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_serverpublish 或者 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_clientsubscribe 方法即可:

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 公眾號 收到第一時間的更新。

還有更多開源項目的介紹和寶藏項目等待你的發掘。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM