文章目錄
- mprpc項目
- **項目概述**:
- 深入學習到什么
- **前置學習建議**:
- 核心內容
- 其他技術與工具
- **項目特點與要求**:
- **環境準備**:
- 技術棧
- 集群和分布式理論
- 單機聊天服務器案例分析
- 集群聊天服務器分析
- 分布式系統介紹
- 多個模塊的局限
- 引入分布式 RPC 通信框架的意義
- 三大形態對比--**重點**
- RPC 通信原理講解整理
- 為什么要使用 RPC?
- RPC 通信的完整過程(調用鏈)
- 關鍵模塊說明(圖中重要角色)
- 返回結果的組成與處理
- 框架的作用與實現目標
- 舉例鞏固(Login 方法調用)
- protobuf優點
- 總結
- 環境配置
- 類似于集群項目
- protobuf
- protobuf使用(一)
- 安裝vscode插件
- 簡單使用
- 代碼
- protoc編譯
- 序列化和反序列化使用
- 編譯注意
- protobuf使用(二)
- string和**bytes**
- `bytes` 類型的作用:
- 與 `string` 的區別:
- 注意事項
- protobuf的枚舉
- repeated(重復)
- repeated常用方法(C++)
- message嵌套message
- mutable_字段--**重點**
- add_成員和()
- main使用
- 重要的是-學會看**.h**里面的函數
mprpc項目
分布式 網絡通信 框架
基于muduo+protobuf
業界優秀的RPC框架:baidu的brpc,google的grpc
項目概述:
本課程將使用 C/C++ 編寫分布式網絡通信框架項目,重點講解從單機服務器到集群服務器,再到項目模塊化分解與分布式部署的過程。
深入學習到什么
希望 可以 對 單機----集群-----分布式 有更好的理解
前置學習建議:
學習本項目前,建議先完成 C/C++ 項目集群的網絡聊天通信項目,以便更好地理解服務器集群概念及其優勢,為學習分布式知識打下基礎。
技術選型
- 網絡庫:采用 muduo 高性能網絡庫(底層基于 I/O 線程池模型) 。
- 序列化 / 反序列化:使用 protobuf 處理數據序列化和反序列化,以及遠程調用方法的識別、參數處理。
- 命名:基于 muduo 庫和 protobuf 首字母,將項目命名為 mprpc。
核心內容
- 講解集群與分布式的概念及原理。
- 剖析 rpc 遠程過程調用的原理與實現。
- 闡述服務注冊中心(如 ZooKeeper)在分布式環境中的作用。
其他技術與工具
- 涉及 C++11 和 C++14 的新語法(如線程級別的本地變量、綁定器與函數對象等)。
- 使用 VS Code 進行跨平臺開發,在 Linux 環境下遠程開發項目。
- 介紹 muduo 庫網絡編程、conf 配置文件讀取、cmake 構建集成編譯環境及 GitHub 項目托管。
項目特點與要求:
項目代碼量雖比集群聊天項目少,但對技術棧的理解深度和廣度要求更高,更注重對集群和分布式的理解 。
環境準備:
開發前需掌握 Linux 環境下 muduo 網絡庫(依賴 boost 庫)的安裝,相關安裝步驟可參考博主博客,且 muduo 庫網絡編程示例、cmake 構建編譯環境在集群聊天服務器項目中已詳細講解。
技術棧
- 集群和分布式概念以及原理
- RPC遠程過程調用原理以及實現
- Protobuf數據序列化和反序列化協議
- ZooKeeper分布式一致性協調服務應用以及編程
- muduo網絡庫編程
- conf配置文件讀取
- 異步日志
- CMake構建項目集成編譯環境
- github管理項目
代碼沒有集群多, 但是 知識更深入
集群和分布式理論
單機聊天服務器案例分析
服務器模塊與業務:以單機聊天服務器為例,其包含用戶管理、好友管理、群組管理、消息管理和后臺管理五個模塊。每個模塊對應多項特定業務,如用戶管理包括登錄、注冊、注銷;好友管理涉及添加、刪除好友等,這些業務由一個或多個相關函數實現。
性能與設計瓶頸:
- 硬件資源限制:單機服務器受硬件資源制約,例如 32 位 Linux 系統的聊天服務器,進程資源耗盡時,最多僅能支持約兩萬用戶同時在線,難以承載更多客戶端連接與服務。
- 運維與代碼編譯成本高:由于模塊都在同一項目運行單元,任意模塊的修改(哪怕只是一行代碼),都需重新編譯整個項目代碼(耗時約 2 小時),并重新部署(耗時約 3 小時),成本巨大。
- 硬件資源分配不合理:不同模塊對硬件資源需求不同,存在 CPU 密集型和 IO 密集型模塊。但單機服務器只能采用平衡方案部署,無法針對各模塊需求進行硬件資源的精準匹配。
集群聊天服務器分析
性能提升:通過水平擴展硬件資源,增加服務器數量(如三臺或更多),每臺獨立運行聊天服務器程序,解決了單機服務器受硬件資源限制導致的用戶并發量低的問題。
存在問題:
- 編譯成本高:各服務器上的模塊仍在同一項目中部署,運行于一個服務進程,因此任意模塊修改仍需整體重新編譯代碼,且需多次部署到不同服務器,運維成本更高。
- 硬件資源分配不合理:集群只是簡單擴展機器,無法針對不同模塊(CPU 密集型或 IO 密集型)的硬件資源需求進行精準部署,存在資源浪費。例如后臺管理模塊并發需求低,卻隨整體系統在多臺服務器部署 。
其他特點應用:集群部署方式簡單,在高并發突發場景(如雙 11)能快速通過增加服務器和負載均衡器提升服務能力 。
在集群中機器數量與性能并非成正比,原因如下:
- 通信成本:機器增多使節點間通信開銷增大,占用帶寬與處理時間,易引發網絡擁塞。
- 分配難題:數據和任務難以在更多機器上均勻分配,易造成資源浪費。
- 復雜故障:系統復雜度隨機器數上升,故障和配置問題更易影響性能。
- 并行局限:部分任務不適合大規模并行或并行度有限,加機器也無法提升性能。
以下任務不適合大規模集群并行處理:
- 順序依賴型:步驟間嚴格先后關聯,無法拆分并行,如按序的數據清洗與分析。
- 高通信成本型:執行中需頻繁大量數據交互,易受網絡帶寬制約,如高頻金融交易數據處理。
- 任務粒度過小:任務簡單微小,集群調度、協調開銷超執行時間,如大量小文件簡單格式轉換。
分布式系統介紹
定義與特點:將一個工程拆分為多個模塊,每個模塊獨立部署為可運行的服務進程,多臺服務器(分布式節點)協同工作構成完整系統。與集群區別在于,集群中每臺服務器運行完整系統,而分布式是多臺服務器共同組成一個系統 。
解決的問題
- 并發與資源優化:可根據分布式節點的并發需求靈活擴容,如對用戶管理模塊所在節點增加服務器以支持更高并發,同時合理利用其他節點空閑資源,提升資源利用率。
- 編譯與部署優化:模塊獨立部署,單個模塊修改僅需重新編譯和更新該模塊,無需影響其他模塊,大大降低編譯和部署成本。
- 硬件匹配優化:模塊拆分后,可依據各模塊特性(CPU 密集型或 IO 密集型)精準匹配硬件資源,實現資源的合理配置。
潛在問題與應對:分布式系統中部分節點故障可能影響整體服務,但實際生產中可通過配置主備服務器等容災方案保障高可用性 。
多個模塊的局限
模塊劃分困難
- 模塊之間的邊界不清晰,容易出現功能重疊或代碼重復。
- 模塊耦合度高,修改難、維護難。
- 若劃分不當,容易造成大量重復代碼、邏輯冗余、維護成本高。
模塊之間的通信復雜–重點
- 分布式部署后,模塊間通信需跨進程、跨機器。
- 函數調用從本地調用變為遠程調用,需涉及:
- 函數名、參數傳輸
- 網絡通信、序列化/反序列化
- 異常處理、響應返回等機制
引入分布式 RPC 通信框架的意義
核心作用
讓“跨主機遠程調用函數”像“調用本地函數”一樣簡單透明
解決的核心問題
- 統一通信流程,屏蔽底層復雜性
- 請求封裝 + 網絡傳輸 + 響應處理全部自動完成。
- 提高模塊間調用效率與開發體驗
- 用戶感知不到遠程調用的差異,只需像本地函數一樣使用接口。
- 支持參數序列化與傳輸
- 使用 Protobuf 進行高效的數據結構序列化。
- 自動服務發現與定位
- 通過 ZooKeeper 注冊中心查找服務位置,實現動態服務綁定。
三大形態對比–重點
系統形態 | 特點 | 優勢 | 局限性 |
---|---|---|---|
單機服務器 | 所有模塊在一個進程 | 開發簡單 | 擴展性差、耦合高 |
集群服務器 | 多臺相同服務器 | 水平擴展、簡單粗暴 | 不是線性擴展、資源浪費 |
分布式模塊化系統 | 各模塊獨立部署 | 高可維護性、易擴展、低耦合 | 設計復雜、通信困難(需RPC) |
RPC 通信原理講解整理
為什么要使用 RPC?
- 為了解耦與擴展:
- 大型系統按需模塊化(不同模塊對硬件/并發等要求不同),分布式部署成為必然。
- 模塊分布在不同進程、甚至不同機器上,相互之間仍需調用方法 —— 就必須跨進程/跨機器通信。
- 屏蔽底層通信細節:
- 本質是“遠程函數調用”,但不同于本地函數調用的直接跳轉和傳參。
- 不希望每個開發者都去手動處理 socket、序列化、反序列化、錯誤碼等細節。
- 引入“框架”來自動完成這些通信細節,開發者只專注于業務邏輯即可。
RPC 通信的完整過程(調用鏈)
舉例:用戶模塊調用好友模塊的 getUserFriendList(userId)
方法(模塊部署在不同服務器)
Caller(調用方) → Stub(客戶端代理) → 網絡層↓ ↓ ↓發起調用(方法名+參數) → 序列化(打包) → 網絡發送↑ ↑ ↑接收返回(結果/錯誤) ← 反序列化(解包) ← 網絡接收
步驟 | 說明 |
---|---|
1. 調用方發起函數調用 | 比如:getUserFriendList(userId) ,但這個方法實際存在于另一臺機器。 |
2. Stub 代理類攔截調用 | 替你處理所有 RPC 通信細節。 |
3. 參數序列化(打包) | 方法名、參數 → 序列化成字節流(如 JSON、Protobuf) |
4. 網絡傳輸 | 使用網絡庫(如 muduo)將字節流發送到目標服務器 |
5. 服務端接收 | 網絡層接收到請求后交給服務端的 Stub 處理 |
6. 參數反序列化 | 字節流 → 方法名 + 參數 |
7. 執行遠程函數 | 找到目標函數(如 getUserFriendList ),執行邏輯處理 |
8. 返回結果處理 | 執行結果、錯誤碼、錯誤信息 → 序列化返回 |
9. 調用方接收響應 | 解包結果 → 返回給應用層,像本地函數一樣使用返回值 |
關鍵模塊說明(圖中重要角色)
角色 | 功能簡述 |
---|---|
Caller | 發起方(如用戶模塊),調用遠端方法 |
Stub(客戶端樁) | 代理模塊,封裝參數、處理序列化、發送請求等 |
網絡層 | 通信基礎設施(如 muduo 庫),負責字節流的收發 |
Stub(服務端樁) | 接收數據并反序列化,請求轉發到本地業務模塊 |
Callee | 被調方(如好友模塊),真正執行業務方法 |
結果返回路徑 | 與調用路徑對稱,同樣涉及打包、網絡傳輸和反序列化 |
返回結果的組成與處理
- 返回值通常包括:
- 錯誤碼(errorCode)
- 錯誤信息(errorMessage)
- 業務數據(result)
- 如果錯誤碼為非零,說明遠程執行出錯,不應使用返回值,僅使用錯誤信息。
框架的作用與實現目標
- 由框架來完成:
- 參數/返回值的序列化與反序列化
- 方法名的標識與分發
- 網絡通信(請求發送/接收)
- 錯誤處理與返回機制
- 開發者只需寫業務邏輯函數,像調用本地函數一樣調用遠程服務
舉例鞏固(Login 方法調用)
- 示例函數:
login(string name, string password)
- 發起調用:
login("zhangsan", "123456")
- 步驟:
- Stub 序列化請求(函數名 + 參數)
- 網絡發送請求
- 遠端反序列化 + 調用 login()
- 執行返回
true/false
+ 錯誤碼 + 信息 - 遠端再次序列化發送
- 調用方反序列化,判斷錯誤碼再處理返回值
protobuf優點
高效的序列化性能
- 體積小:二進制格式,比 JSON、XML 更精簡,節省網絡帶寬。
- 速度快:序列化和反序列化速度遠快于 JSON/XML,適合高頻數據傳輸場景。
- 示例:1000 條用戶消息用 JSON 可能幾百 KB,而 Protobuf 僅幾十 KB。
跨語言支持
- 支持多種語言自動生成代碼(C++、Java、Python、Go 等)。
- 不同平臺、語言之間通信無需手寫解析邏輯,提高開發效率。
- 示例:后端使用 C++,前端用 JavaScript,通過 proto 文件即可對接。
總結
- RPC 的目標:讓遠程調用就像本地函數調用一樣簡單
- 框架解決的是“通信”本質問題,而不是業務邏輯問題
- 圖中的每一步都需要代碼支持,RPC 框架的核心就是實現這些自動化處理
zookeeper服務配置中心(專門做服務發現)
環境配置
類似于集群項目
protobuf
github 進行下載安裝 https://github.com/protocolbuffers/protobuf
一定要 下載 包里面 有 autogen.sh 的版本
沒有的 就是 高版本, bazel 和 cmake 都不好用, bug特別多!!
折騰了 半天, 高版本 一個是 安裝步驟變了, 一個是 bug一堆!!
不要安裝 21 版本之上, 一堆bug, 就裝 21版本及以下
sudo apt-get update
sudo apt-get install autoconf automake libtool curl make g++ unzip
git clone 下來
./autogen.sh
./configure
make && sudo make install
sudo ldconfig
protobuf使用(一)
內容并不多, 后續 從實踐中 學習
安裝vscode插件
vscode-proto3
簡單使用
在 Protobuf 中,package
后面跟的就是 包名,表示該 .proto
文件中定義的所有消息、服務、枚舉等都屬于這個“命名空間”,稱為 包名。
包名是你自己定義的一個標識符,用來給這組 protobuf 定義加上“命名空間”。
代碼
test/protobuf/test.proto------proto 配置文件
syntax = "proto3"; //聲明protobuf的版本package hzhpro; // 聲明代碼所在的包(例如c++就是namespace)// 定義登錄請求消息類型 name pwd
message LoginRequest
{string name = 1;string pwd = 2;
}// 定義登錄響應消息類型
message LoginResponse
{int32 errcode = 1;string errmsg = 2;bool success = 3;
}
protoc編譯
–cpp_out=OUT_DIR Generate C++ header and source.
protoc test.proto --cpp_out=./
生成
test.pb.cc test.pb.h
messgae 相當于 class類, 里面的 相當于 成員變量
序列化和反序列化使用
test/protobuf/main.cc
#include "test.pb.h"
#include <iostream>
#include <string>
using namespace hzhpro; // 實際開發 要少用命名空間int main()
{// 封裝了login請求對象的數據LoginRequest req;req.set_name("zhang san");req.set_pwd("123123");// 對象數據序列化=>char*std::string send_buf; if(req.SerializeToString(&send_buf)){std::cout<< send_buf.c_str()<<std::endl;}// 從send_buf反序列化LoginRequest reqB;if(reqB.ParseFromString(send_buf)){std::cout<<reqB.name()<<std::endl;std::cout<<reqB.pwd()<<std::endl;}return 0;
}
編譯注意
必須加 pthread-----因為 Protobuf 內部使用了線程相關的功能(如 std::thread
, pthread_create
)
g++ main.cc test.pb.cc -lprotobuf -pthread
protobuf使用(二)
string和bytes
在 Protobuf 中,bytes
是一種字段類型,表示原始二進制數據,用途非常廣泛。
bytes
類型的作用:
它可以用來存儲:
- 二進制數據(圖片、文件內容、壓縮數據)
- 自定義序列化的結構體
- 加密密鑰、哈希值
- 或者就是一個 UTF-8 編碼的字符串(但不推薦當字符串來用)
與 string
的區別:
類型 | 內容編碼 | 是否可包含 \0 | 推薦用途 |
---|---|---|---|
string | UTF-8 | ? 不可包含 | 正常文本(人讀的) |
bytes | 原始數據 | ? 可以包含 | 任意二進制數據 |
注意事項
- 使用
bytes
時不能用set_content("str")
來設置包含\0
的數據,否則會截斷。 - 應使用
set_content(const void* data, size_t size)
。
protobuf的枚舉
.proto
中每個枚舉成員 必須指定數值(不自動遞增)。
必須用分號 ;
結束每一行,這和 C++ 是不同的。
枚舉成員名建議用 全大寫字母,符合 protobuf 的命名習慣。
repeated(重復)
repeated 類型 字段名 = 編號;
message FriendList {repeated string friends = 1;
}
下面這個是 基本類型
FriendList list;
list.add_friends("Tom");
list.add_friends("Jerry");for (int i = 0; i < list.friends_size(); ++i) {std::cout << list.friends(i) << std::endl;
}
repeated常用方法(C++)
add_字段()
?????→ 添加一個元素字段_size()
???? → 獲取數量字段(index)
????→ 獲取第 index 個元素(從 0 開始)mutable_字段()
??→ 獲取可修改的容器(高級操作)
message嵌套message
test/protobuf/test.proto
syntax = "proto3"; //聲明protobuf的版本package hzhpro; // 聲明代碼所在的包(例如c++就是namespace)message ResultCode
{int32 errcode = 1;bytes errmsg = 2;
}// 定義登錄請求消息類型 name pwd
message LoginRequest
{bytes name = 1;bytes pwd = 2;
}// 定義登錄響應消息類型
message LoginResponse
{ResultCode result = 1;// int32 errcode = 1;// bytes errmsg = 2;bool success = 3;
}message GetFriendListsRequest
{uint32 userid = 1; // 獲取誰的請求
}message User
{bytes name =1;uint32 age = 2;enum Sex // 枚舉寫法注意{MAN=0;WOMAN=1;}Sex sex=3;
}message GetFriendResponse
{// int32 errcode = 1; // 代碼重復// bytes errmsg = 2;ResultCode result = 1;repeated User friend_list=2; // 定義了一個列表類型, 這個_list沒啥特殊意義
}
重新編譯, 注意 vscode 緩存, 容易沒反應, 重新拉一下 頭文件
mutable_字段–重點
通過查找 pb.h 的 result 函數 ===== 這個result 就是 那個ResultCode 類對象
result 返回 const 引用, 不能修改值
mutable_result 返回指針, 可以修改值
const ::hzhpro::ResultCode& result() const;
::hzhpro::ResultCode* mutable_result();
void set_allocated_result(::hzhpro::ResultCode* result);
函數名 | 返回類型 | 用途 |
---|---|---|
result() | const ResultCode& | 只讀訪問 |
mutable_result() | ResultCode* | 可寫訪問 |
set_allocated_result(ResultCode*) | void | 設置已存在對象的所有權(高級用法) |
add_成員和()
::hzhpro::User* add_friend_list();
const ::hzhpro::User& friend_list(int index) const; //查看第幾個, index 根據上面
main使用
LoginResponse rsp;
ResultCode *rc = rsp.mutable_result();
rc->set_errcode(1);
rc->set_errmsg("登錄處理失敗");
自定義類型的 add_
int main()
{// LoginResponse rsp;// ResultCode *rc = rsp.mutable_result();// rc->set_errcode(1);// rc->set_errmsg("登錄處理失敗");GetFriendListsResponse rsp;ResultCode *rc = rsp.mutable_result();rc->set_errcode(0);User *user1 = rsp.add_friend_list();user1->set_name("zhang san");user1->set_age(26);user1->set_sex(User::MAN);User *user2 = rsp.add_friend_list();user2->set_name("zhang san-2");user2->set_age(26);user2->set_sex(User::MAN);User *user3 = rsp.add_friend_list();user3->set_name("zhang san-3");user3->set_age(26);user3->set_sex(User::WOMAN);std::cout<<rsp.friend_list_size()<<std::endl;User user = rsp.friend_list(2);std::string userstr;if(user.SerializeToString(&userstr)){// std::cout<<userstr.c_str()<<std::endl;// 這個有問題, 序列化后是二進制數據流, 本身是 字符串能打印出來, 要是有別的 類型, 就不好說了}User userB;if(userB.ParseFromString(userstr)){std::cout<<userB.name()<<std::endl;std::cout<<userB.age()<<std::endl;std::cout<<userB.sex()<<std::endl;}return 0;
}
3
zhang san-3
26
1