文章目录
- 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