施磊老师rpc(四)
文章目录
- rpc网络服务
- 简介
- RpcProvider 的设计目标
- Eventloop不使用智能指针-弃用
- RpcProvider类似于集群的服务器
- provider网络实现
- **src/include/rpcprovider.h**
- **src/include/mprpcapplication.h**
- **src/rpcprovider.cc**
- 错误1
- 错误2-重点
- **本项目的 mprpc 是动态库, muduo..是静态库**
- 重点错误!
- 详见知识补充
- 错误3
- 测试
- RpcProvider发布服务方法(一)
- RPC Provider的核心功能
- RPC Provider 网络模块实现
- RPC 服务方法的发布功能
- 框架提供的接口:
- 角色说明
- run函数的流程
- `NotifyService`的作用
- 框架帮我们做了什么?
- RpcProvider发布服务方法(二)
- 实现 `RpcProvider::NotifyService`
- 为什么这样做(目的):
- 框架中处理服务的通用做法
- 具体实现步骤(NotifyService 方法逻辑)
- 错误-1
- **解决方法**
- NotifyService实现
- 单个服务:**Service**
- 所有服务的集合:**服务注册表(Service Registry)** 或 **服务映射表(Service Map)**
- **src/include/rpcprovider.h**
- **src/rpcprovider.cc**
- 注意
- 示例
- 总结:为什么我们选择 Protocol Buffer 而不是 JSON
- 常见的选择:
- JSON 和 Protobuf 的对比:
- 为什么 Protobuf 更适合 RPC 场景:
- RpcProvider分发rpc服务(一)
- 本节课任务
- 回顾
- onMessage 中要做什么?
- 理解流程-不要混
- 理解错误
- **序列化后的字符串 和 传输的字节流**
- **以json为例**
- 英语单词
- **Customer(顾客)**
- **Consumer(消费者)**
- json vs proto反序列化
- 1. **Proto需要预定义数据结构**
- 2. **Proto生成代码**
- 3. **数据反序列化过程**
- 4. **数据格式**
- 数据头
- json也可以 数据头
- protobuf使用不太一样---具体看实现
- 粘包处理机制
- **例子**
- 为什么不用“加竖杠分隔字符串”的做法?
- 上不了 台面的 玩意儿!! 太勾八垃圾了
- proto反序列化
- **自定义协议设计(数据格式)**
- 示例
- Proto 实现
- 数据处理流程总结(onMessage 中)
- **这是整个流程, 本节课仅完成 前三个步骤**
- 对protobuf字节流的理解-不一定对
- **重点理解**
- onmessage实现-部分
- 总结
- 小技巧:读取定长二进制整数--暂时还没遇到问题
rpc网络服务
简介
从 配置文件 读取 ip和port 后, 就需要 进行连接 传输 了
也就是 rpc框架准备好了, 现在需要 网络, 服务方 才能发布 rpc, 然后 消费端 才能 连接 去调用
RpcProvider 的设计目标
- 让使用者只需要注册服务即可,不暴露
muduo
网络细节。 - 从配置文件中自动加载 IP 和端口,避免用户手动输入。
- 封装
muduo
的启动流程,让RpcProvider::Run()
启动网络服务。 - 使用基于 Reactor 模型的高性能网络服务。
Eventloop不使用智能指针-弃用
在 muduo 网络库中,
EventLoop
通常不使用智能指针管理,而是直接使用原始指针,主要原因如下:
- 生命周期明确
EventLoop
通常是长期存在的对象(如伴随整个线程或程序生命周期),其销毁时机由代码逻辑直接控制,无需智能指针自动管理。- 栈对象主导
muduo 的设计中,EventLoop
多作为栈对象(如EventLoop loop;
),依赖作用域自动析构,无需堆内存分配和智能指针介入。- 从属关系清晰
TcpServer
仅持有EventLoop
的指针(表示依赖关系),而非所有权。EventLoop
的实际生命周期由更高层(如main()
或线程函数)管理,避免所有权混乱。- 性能与简洁性
原始指针更轻量,符合 muduo 高性能网络库的设计目标,同时代码更直观(智能指针在此场景无显著优势)。总结:
EventLoop
的生命周期由应用逻辑显式控制,且多为栈对象,使用原始指针更符合 muduo 的设计哲学和实际使用模式。
RpcProvider类似于集群的服务器
类似, 并不完全相同
provider网络实现
[!TIP]
这个
run 函数
就相当于 集群聊天里 的main
调用 chatserver 差不多
src/include/rpcprovider.h
private: // // 组合TcpServer ----- 不写成成员了, 只有 run 会访问 // std::unique_ptr<muduo::net::TcpServer> m_pTcpServer; // 智能指针 // 组合 EventLoop muduo::net::EventLoop m_eventLoop; // 事件循环 void OnConnection(const muduo::net::TcpConnectionPtr &conn); // 连接回调函数void OnMessage(const muduo::net::TcpConnectionPtr &conn, // 消息回调函数muduo::net::Buffer *buffer,muduo::Timestamp time);
src/include/mprpcapplication.h
// 获取配置文件对象static MprpcConfig &GetConfig();
.cc
// 获取配置文件对象 MprpcConfig &MprpcApplication::GetConfig() {return m_config; // 返回配置文件对象 }
[!tip]
如果 有学过 集群项目
run 里面的
muduo部分
将是 融合的 集群服务器的main, chatserver
src/rpcprovider.cc
// 启动rpc服务发布 节点, 开始提供rpc远程网络调用服务 void RpcProvider::Run() {std::string ip = MprpcApplication::GetInstance().GetConfig().Load("rpcserverip");uint16_t port = atoi(MprpcApplication::GetInstance().GetConfig().Load("rpcserverport").c_str());muduo::net::InetAddress addr(ip, port); // 绑定ip和端口号// 创建TcpServer对象muduo::net::TcpServer server(&m_eventLoop, addr, "RpcProvider");// 设置线程数量server.setThreadNum(4);// 设置连接回调函数server.setConnectionCallback(std::bind(&RpcProvider::OnConnection, this, std::placeholders::_1));// 设置消息回调函数server.setMessageCallback(std::bind(&RpcProvider::OnMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));std::cout << "RpcProvider start service " << std::endl;// 启动服务server.start();// 事件循环m_eventLoop.loop(); }// 实现连接回调函数 void RpcProvider::OnConnection(const muduo::net::TcpConnectionPtr &conn) { }// 实现消息回调函数 void RpcProvider::OnMessage(const muduo::net::TcpConnectionPtr &conn, // 消息回调函数muduo::net::Buffer *buffer,muduo::Timestamp time) {// 1. 解析rpc请求数据// 2. 生成响应数据// 3. 发送响应数据 }
由于使用了 muduo, 因此 cmke 要添加 编译选项
错误1
如果 cmake 加的编译选项是 muduo, 一般会 找不到库
find /usr -name "libmuduo*"
/usr/local/lib/libmuduo_base.a
/usr/local/lib/libmuduo_net.a
/usr/local/lib/libmuduo_http.a
/usr/local/lib/libmuduo_inspect.a
[!important]
muduo 变为 muduo_net muduo_base
顺序必须一样, net依赖base
一般是这些库, 没有 muduo.a
前两个最重要
库文件 | 功能说明 | 典型依赖关系 |
---|---|---|
libmuduo_base.a | 基础核心库,包含 EventLoop 、Timestamp 、Logging 等基础工具类 | 无依赖,是其他模块的基础 |
libmuduo_net.a | 网络通信库,提供 TcpServer 、TcpClient 、Buffer 等网络相关类 | 依赖 libmuduo_base.a |
libmuduo_http.a | HTTP 协议支持库,提供简单的 HTTP 服务器和客户端功能 | 依赖 libmuduo_net.a |
libmuduo_inspect.a | 调试监控库,支持通过 HTTP 接口查看服务器内部状态(如连接数、线程状态) | 依赖 libmuduo_http.a |
错误2-重点
[build] /usr/bin/ld: /usr/local/lib/libmuduo_net.a(EventLoop.cc.o): relocation R_X86_64_TPOFF32 against `_ZN12_GLOBAL__N_118t_loopInThisThreadE' can not be used when making a shared object; recompile with -fPIC
[build] /usr/bin/ld: failed to set dynamic section sizes: bad value
[build] collect2: error: ld returned 1 exit status
[build] gmake[2]: *** [src/CMakeFiles/mprpc.dir/build.make:129: ../lib/libmprpc.so] Error 1
[build] gmake[1]: *** [CMakeFiles/Makefile2:131: src/CMakeFiles/mprpc.dir/all] Error 2
本项目的 mprpc 是动态库, muduo…是静态库
错误的原因:你在编译动态库时,链接了一个静态库(
libmuduo_net.a
),但是这个静态库中的对象文件(EventLoop.cc.o
)没有使用-fPIC
编译。这导致链接器出现了 “relocation can not be used when making a shared object” 错误,因为静态库中的代码是位置相关的,不能直接与动态库链接。
[!important]
重点错误!
当你在构建动态库(
.so
)时,链接了一个静态库(.a
),但是静态库中的代码没有使用 位置无关代码(Position Independent Code,PIC) 编译选项(即-fPIC
),就会出现链接错误,通常表现为类似以下的错误信息:relocation R_X86_64_TPOFF32 against `symbol` can not be used when making a shared object; recompile with -fPIC
详见知识补充
静态库 一般不需要 -fPIC, 一般只有动态库需要
但是, 当 静态库 需要连接到 动态库时, 静态库必须加 -fPIC 选项
错误3
muduo_net muduo_base
顺序必须一样, net依赖base
测试
自行
RpcProvider发布服务方法(一)
[!tip]
建议看 原视频, 过程讲的很清晰
RPC Provider的核心功能
- 已实现功能:(run函数)
- 网络模块:基于Muduo库实现TCP服务器,处理网络数据收发(绿色部分)。
- 事件循环与回调:通过
onConnection
和onMessage
回调处理连接和消息。
- 待实现功能(黄色部分):
- 服务方法注册与映射:通过
NotifyService
记录服务对象及其方法,供远程调用时定位。
- 服务方法注册与映射:通过
RPC Provider 网络模块实现
- 利用
muduo
网络库,实现了 RPC Provider 的网络通信能力; - 涉及
TcpServer
和EventLoop
以及两个核心回调函数:onConnection
:新连接建立onMessage
:接收远程调用请求(字节流)
RPC 服务方法的发布功能
目的:让使用者能够将本地服务对象的方法注册为远程可调用的 RPC 方法。
框架提供的接口:
void notifyService(::google::protobuf::Service* service);
- 参数:是一个继承自
protobuf::Service
的指针; - 使用者将自定义的服务类(如
UserService
)传入,即可注册它的所有 RPC 方法。
角色说明
名称 | 说明 |
---|---|
caller | RPC 的调用者(客户端) |
callee | RPC 的提供者(服务端) |
mprpc 框架 | 提供 mprpcApplication 、RpcProvider 等功能模块 |
[!tip]
任何 分布式节点 都可能成为一个 rpc服务器—collee, 也可能 请求调用 其他rpc方法 — coller
run函数的流程
流程示例(以UserService::Login
为例):
- 接收请求:RPC Provider通过网络模块接收字节流。
- 反序列化:Protobuf将数据解析为
LoginRequest
对象。 - 方法调用:框架查表找到
UserService
的Login
方法并调用。 - 处理响应:用户代码填充
LoginResponse
,框架序列化后通过网络返回。
NotifyService
的作用
-
核心目标:建立服务对象与方法调用的映射表,使框架能根据请求调用正确的本地方法。
-
记录服务对象(如
UserServiceRpc
);记录该服务对象中有哪些方法(login、register 等);
使用 protobuf 提供的反射接口,获取:
- service 名称;
- 每个方法的名称;
- 方法的编号 / 反射调用方式。
框架帮我们做了什么?
步骤 | 谁来做? | 作用 |
---|---|---|
接收字节流 | 网络模块(muduo) | 收到 RPC 请求 |
反序列化 | protobuf | 还原请求对象 |
定位方法 | 框架内部映射表 | 找到服务和函数 |
反射调用 | protobuf + 注册表 | 调用我们自己重写的逻辑 |
回传结果 | 框架负责序列化 + 网络发送 | 客户端接收结果 |
RpcProvider发布服务方法(二)
实现 RpcProvider::NotifyService
为什么这样做(目的):
- 将用户定义的 service 对象(如
UserService
)注册到框架中。 - 框架后续通过 service 名和 method 名即可定位到具体的服务方法,实现远程调用。
- 抽象性设计原则:框架不依赖具体业务类,仅依赖于 protobuf 的
Service
基类。
框架中处理服务的通用做法
-
使用
google::protobuf::Service
作为服务对象的基类指针。 -
利用
protobuf
的 反射机制(通过 descriptor 描述服务和方法),实现对服务对象的动态管理。 -
// pb.h 里面 server的子类里 static const ::PROTOBUF_NAMESPACE_ID::ServiceDescriptor* descriptor();
-
至于 这个 类型里 有什么, 点进去看即可 , 挺多的, 下面这个不全
-
具体实现步骤(NotifyService 方法逻辑)
- 获取服务描述信息
使用service->GetDescriptor()
得到服务的元数据,包括服务名称、方法个数等。- 提取服务名
用descriptor->name()
获取服务的唯一标识名(如UserServiceRpc
)。- 提取每个方法的描述信息
通过循环descriptor->method(i)
获取每个方法的MethodDescriptor
,记录方法名等。- 定义数据结构用于存储注册信息
自定义一个ServiceInfo
结构体:
- 包含一个服务对象指针
Service*
.- 包含一个
unordered_map<string, const MethodDescriptor*>
存储方法名与其描述对象的映射。- 维护全局服务映射表
用unordered_map<string, ServiceInfo>
,键是服务名,值是该服务的所有信息(对象+方法表)。- 将服务及其方法插入映射表中
完成注册,后续框架收到请求时即可根据服务名和方法名快速查找、调用对应的业务逻辑。
错误-1
不允许使用指向不完整类型 “google::protobuf::ServiceDescriptor” 的指针或引用
这个错误表明编译器在处理
google::protobuf::ServiceDescriptor
时认为它是一个不完整类型(incomplete type),即编译器看到了它的声明(比如前向声明),但没有看到完整的定义。这通常是因为缺少对应的头文件包含。
解决方法
#include <google/protobuf/descriptor.h> // 这个头文件里有 ServiceDescriptor
NotifyService实现
[!important]
每个
ServiceInfo
记录一个服务及其所有方法.整个服务注册表维护多个这样的服务信息,支持多服务统一管理。
服务注册表(map<string, ServiceInfo>)
└── "UserServiceRpc" → ServiceInfo├── service指针(UserServiceRpc*)└── method_map(map<string, const MethodDescriptor*>)├── "Login" → MethodDescriptor*└── "Register" → MethodDescriptor*
└── "FriendServiceRpc" → ServiceInfo├── service_ptr: FriendServiceRpc*└── method_map├── "AddFriend" → AddFriend 方法的描述符└── "GetList" → GetList 方法的描述符
当你调用
NotifyService(UserServiceRpc*)
:
- 通过服务对象获取其描述信息(ServiceDescriptor)。
- 从描述信息中提取出:
- 服务名称(如 “UserServiceRpc”)
- 每一个方法的:
- 方法名(如 “Login”, “Register”)
- 输入参数类型(如
LoginRequest
)- 输出参数类型(如
LoginResponse
)
在 RPC 框架中,命名一般如下:
单个服务:Service
指的是一个具体的业务服务,比如
UserService
、OrderService
,通常是继承自google::protobuf::Service
的类实例。它包含多个可以远程调用的方法(RPC 方法)。所有服务的集合:服务注册表(Service Registry) 或 服务映射表(Service Map)
src/include/rpcprovider.h
// private 添加 // service 服务类型信息----服务名及方法名struct ServiceInfo{google::protobuf::Service *m_service; // 服务对象std::unordered_map<std::string, const google::protobuf::MethodDescriptor *> m_methodMap; // 方法名和方法描述对象的映射关系};// 存储服务对象的容器std::unordered_map<std::string, ServiceInfo> m_serviceMap; // 服务名和服务对象的映射关系
src/rpcprovider.cc
// 这里是框架提供给外部使用的, 可以发布rpc方法的函数接口 void RpcProvider::NotifyService(google::protobuf::Service *server) {ServiceInfo service_info; // 服务对象信息// 获取服务对象描述信息---- 去看看 pb.h 和 pb.cc 这个接口, 是const!!const google::protobuf::ServiceDescriptor *pserviceDesc = server->GetDescriptor(); // 获取服务名称std::string service_name = pserviceDesc->name(); // 获取服务名称std::cout << "service_name: " << service_name << std::endl;// 获取服务方法数量int method_count = pserviceDesc->method_count(); // 获取服务方法数量// 获取服务对象指定下标的方法描述信息for (int i = 0; i < method_count; ++i){const google::protobuf::MethodDescriptor *pMethodDesc = pserviceDesc->method(i); // 获取服务对象指定下标的方法描述信息// 获取方法名称std::string method_name = pMethodDesc->name(); // 获取方法名称service_info.m_methodMap.insert({method_name, pMethodDesc}); // 将方法名称和方法描述对象的映射关系存入容器std::cout << "method_name: " << method_name << std::endl;}service_info.m_service = server; // 将服务对象存入容器// 将服务名称和服务对象存入容器m_serviceMap.insert({service_name, service_info}); }
注意
[!warning]
NotifyService
函数 每次 只注册一个 服务想要注册多个 服务, 就要 多次进行调用
示例
// 用户代码:注册多个服务 UserServiceRpc user_service; OrderServiceRpc order_service; provider.NotifyService(&user_service); // 注册 UserService provider.NotifyService(&order_service); // 注册 OrderService
总结:为什么我们选择 Protocol Buffer 而不是 JSON
在网络通信中,必须选用一种数据传输协议来进行结构化数据的交换。不能直接传输原始的字节流或字符串,因为我们需要明确区分不同的字段、类型和结构,这就需要一种标准化的“数据格式”。
常见的选择:
- XML:过于冗长、效率低,已经很少使用。
- JSON:结构清晰,易于阅读,学习成本低,但效率偏低。
- Protocol Buffer(Protobuf):谷歌开发的高效二进制序列化协议。
JSON 和 Protobuf 的对比:
项目 JSON Protobuf 存储格式 文本(可读性好) 二进制(效率高) 序列化性能 较慢 非常快 传输体积 大(包含键名) 小(紧凑、无额外字段名) 类型系统 弱类型(依赖解析库) 强类型(.proto 明确定义) 支持 RPC 方法 不支持 支持 Service 定义和方法描述(gRPC) 为什么 Protobuf 更适合 RPC 场景:
- 它不仅提供数据结构的序列化与反序列化功能(像 JSON 一样),
- 还支持对服务(service)和方法(rpc method)的描述,可以用于自动生成代码、进行远程调用处理(这是 JSON 无法做到的)。
因此,在构建聊天服务器等高性能通信系统时,我们更倾向于使用 Protobuf 而不是 JSON,尤其是在服务之间通过 RPC 调用的场景下,它能极大提升效率与可维护性。
RpcProvider分发rpc服务(一)
本节课任务
完成proto的反序列化
完成 RPC 框架中 Provider 端的 onMessage
方法,实现:
- 从网络接收 RPC 请求数据;
- 解析出请求的目标服务、方法名、参数;
- 找到对应方法并调用;
- 返回结果给 Client。
回顾
notifyService()
中已将用户发布的服务和方法注册进 Map 中(类比于“服务表”)。run()
方法完成了网络监听和服务启动。onConnection()
:短连接模型下,当断开连接时关闭 socket(shutdown(fd, SHUT_RDWR)
)。
onMessage 中要做什么?
收到的数据本质是一个字符流(字节流),必须:
- 拆包:防止 TCP 粘包;
- 反序列化:根据协议提取出
serviceName
、methodName
和args
; - 查找目标方法并调用;
- 序列化响应并返回给 Client。
理解流程-不要混
【客户端】 【服务端】RPCStub::login() onMessage()↓ ↓
序列化:方法名+参数 <----- 收到字节流↓ ↓
通过 TCP 发出请求 -----> 提取数据(Buffer)↓RPC框架反序列化↓映射到本地的 login()↓执行并获取返回值↓序列化返回值,send()
[!important]
RPC 是目的,Muduo 是网络通信框架,onMessage 是 Muduo 中处理收到消息的“钩子函数”,它帮我们解包网络数据,并触发 RPC 调用。
理解错误
序列化后的字符串 和 传输的字节流
你发送的是字符串(字节数组),网络传输的是流(字节流),收到后要自己从流中“分段”,提取成完整字符串,再反序列化还原数据。
muduo 中的 conn->send() 使得 序列化后的 数据 以字节流 传输
因此,
onmessage
基本第一步 都是把 收到的字节流 转为 字符串, 然后 再反序列化----->以json为例
{"name": "Tom","age": 18 }
// 序列化 std::string jsonStr = "{\"name\":\"Tom\",\"age\":18}";
// 字节流 0x7b 0x22 0x6e 0x61 0x6d 0x65 0x22 0x3a 0x22 0x54 0x6f 0x6d 0x22 0x2c ...
// 再转字符串, 并 反序列化
英语单词
Customer(顾客)
Consumer(消费者)
json vs proto反序列化
1. Proto需要预定义数据结构
- JSON: 通常是动态的,不需要预定义数据结构,你可以直接使用字符串和数字等常见数据类型。
- Proto: 使用 proto 文件 定义数据结构,所有的消息格式需要事先声明和编译生成相应的代码(例如
.proto
文件生成 C++、Java 或 Python 代码)。这意味着你必须严格遵循定义好的消息结构才能正确地进行序列化和反序列化。2. Proto生成代码
- JSON: 不需要生成任何代码,直接通过标准库(如
nlohmann/json
、rapidjson
)进行处理。- Proto: 需要用 protoc 编译器将
.proto
文件转换为特定语言的类。这些类中会包含生成的 getter 和 setter 方法来访问字段。3. 数据反序列化过程
- JSON: 在反序列化时,直接通过 JSON 库将字符串转为 JSON 对象,通常代码简洁。----
json::parse(recvBuf);
- Proto: 反序列化时,需要将字节流(例如,网络传输的消息)解析为特定的 protobuf 对象。这个过程稍微复杂一些,通常涉及到对序列化数据流的解析,调用
ParseFromString
或类似的方法。4. 数据格式
- JSON: 是文本格式,便于阅读和调试,但占用空间相对较大。
- Proto: 是二进制格式,具有更高的性能和更小的消息体积,但不容易直接读取和调试。
数据头
json也可以 数据头
#include <iostream> #include <nlohmann/json.hpp>using json = nlohmann::json;struct Header {std::string message_type;size_t message_length;int checksum;std::string protocol_version; };struct Message {Header header;json body; };int main() {// 构建 JSON 数据体json body = {{"userId", 1},{"name", "Alice"},{"age", 30}};// 构建数据头Header header = {"Request", // 消息类型body.dump().size(), // 消息体的长度12345, // 校验和"1.0" // 协议版本};// 构建完整消息Message message = { header, body };// 输出完整消息(仅示意)std::cout << "Header:\n";std::cout << "Message Type: " << message.header.message_type << "\n";std::cout << "Message Length: " << message.header.message_length << "\n";std::cout << "Checksum: " << message.header.checksum << "\n";std::cout << "Protocol Version: " << message.header.protocol_version << "\n";std::cout << "\nBody:\n";std::cout << message.body.dump() << std::endl;return 0; }
protobuf使用不太一样—具体看实现
粘包处理机制
TCP 是流式协议,不能保证一次接收的数据就是一整包。为此我们必须:
- 用固定长度(4字节)作为 header 长度标志;
- 然后再根据这个长度去读取 header,再读取参数。
例子
假设客户端连续发送了三个消息,每个消息的长度为 5 字节:
Message 1: "Hello" Message 2: "World" Message 3: "Data"
然而,TCP 协议会将这些数据合并成一个连续的字节流进行传输,可能会变成如下形式(这仅是一个例子,实际数据可能更复杂):
HelloWorldData
接收方收到的就是一个连续的数据流:“HelloWorldData”,无法直接知道哪个部分属于哪个消息。
为什么不用“加竖杠分隔字符串”的做法?
上不了 台面的 玩意儿!! 太勾八垃圾了
因为:
- 不规范、性能差;
- 不支持嵌套结构;
- 不可扩展;
- 容易出错;
正确做法是采用 protobuf 作为结构化序列化协议,明确字段类型和结构。
proto反序列化
自定义协议设计(数据格式)
RPC Client 与 RPC Server 之间使用自定义协议通信。数据格式如下:
[4字节 header size][header 字符串][args 字符串]
- header size:4 字节整数(使用二进制形式存储,不能转字符串!)
- header 字符串:使用 protobuf 定义的
RpcHeader
,包含:
service_name
method_name
args_size
(参数部分的字符串长度)- args 字符串:参数 message 的序列化结果
示例
内容类型 十六进制内容 含义 头部长度 0x00 0x00 0x00 0x12
18 字节,表示后面数据头部分长度为 18 字节 数据头 {"method":"Chat"}
一个 JSON 字符串,18 字节,表示调用的 RPC 方法名是 Chat 消息体 "hello world"
实际发送的消息内容
Proto 实现
syntax = "proto3";
package mprpc;message RpcHeader {string service_name = 1;string method_name = 2;uint32 args_size = 3;
}
生成对应 .pb.h
、.pb.cc
后在 onMessage
中使用。
数据处理流程总结(onMessage 中)
一个 char 占用 1个字节(8bit)
string 每个字符 就是一个字节
std::string[index]
操作的是字节
这是整个流程, 本节课仅完成 前三个步骤
- 读取前 4 字节 ➜ 得到 header 的长度; ----- 这个长度 不能用字符串, 因为长度 就不固定了!!! “10” “10000”
- 读取 header 字符串 ➜
RpcHeader::ParseFromString()
得到 service/method/args_size;- 读取 args 部分 ➜ 根据 args_size 读取参数字符串;
- 从 map 中查找 service 对象;
- 通过 method_name 查找 MethodDescriptor;
- 调用
CallMethod()
执行目标服务逻辑;- 将响应结果 serialize 成字符串,通过 TCP 发回 client。
对protobuf字节流的理解-不一定对
重点理解
在写好 proto 文件后, 就有了 类似于键值对的 东西, 但是 protobuf 自动完成了封装, 也就是 message类, 使用标签 就可以知道 是哪个字段
使用时, 就相当于 是是用类, 定义一个 message 对象, 进行 set_字段 赋值
序列化后
SerializeToString
, 可能变为了标签:值
这样的, 1:name 2:pwd传输可能就是 这么传输
然后 反序列化
ParseFromString
后, 使用.字段()
就能直接拿到值, 这在内部 protobuf 本身进行了封装, 使得 调用 直接就拿到了 值
onmessage实现-部分
src/rpcprovider.cc
#include "rpcheader.pb.h" // 这个头文件里有 RpcHeader/*
在框架内部, rpcprovider 和 rpcconsumer 协商好 之间的协议, 约定好数据的格式
service_name method_name args
UserServiceLoginzhang san123456 传这样的 肯定不行,需要拆分
定义 proto 的 message 类型, 进行 数据头的 序列化和 反序列化\加上头部长度
16UserServiceLoginzhang san123456
16 4个字节的头部长度
从 4个子节后 取出 16
UserServiceLoginzhang san123456
*/// 实现消息回调函数
// 已建立连接用户的 读写事件回调, 如果远程有 rpc服务的 调用请求, 那么onMessage 就会被调用
void RpcProvider::OnMessage(const muduo::net::TcpConnectionPtr &conn, // 消息回调函数muduo::net::Buffer *buffer,muduo::Timestamp time)
{// 网络上接收的远程rpc调用请求的字符流 std::string recv_buf = buffer->retrieveAllAsString(); // 获取接收的字符流uint32_t header_len = 0; // 定义头部长度recv_buf.copy((char *)&header_len, 4, 0); // 本项目 自定义 是 4个字节的头部长度// 根据头部长度, 获取到数据头的内容 // recv_buf 是一个字符串, 头部长度是4个字节, 所以从第4个字节开始, 取header_len长度的内容std::string rpc_header_str = recv_buf.substr(4, header_len);// 反序列化数据, 得到rpc请求的 详细信息mprpc::RpcHeader rpc_header; // 定义rpc请求头对象std::string service_name; // 定义服务名称std::string method_name; // 定义方法名称uint32_t args_size = 0; // 调用方法所要的参数大小if(rpc_header.ParseFromString(rpc_header_str)) // 反序列化数据, 得到rpc请求的 详细信息{rpc_header.service_name(); // 获取服务名称service_name = rpc_header.service_name(); // 获取服务名称method_name = rpc_header.method_name(); // 获取方法名称}else{std::cout << "rpc_header parse error" << std::endl;return;}// 获取 rpc的方法参数 的字符流数据std::string args_str = recv_buf.substr(4 + header_len, recv_buf.size() - 4 - header_len); // 获取方法参数的字符流数据// 打印调试信息std::cout<<"============================="<<std::endl;std::cout<<"header_len: "<<header_len<<std::endl;std::cout<<"rpc_header_str: "<<rpc_header_str<<std::endl;std::cout<<"service_name: "<<service_name<<std::endl;std::cout<<"method_name: "<<method_name<<std::endl;std::cout<<"args_size: "<<args_str.size()<<std::endl;std::cout<<"args_str: "<<args_str<<std::endl;std::cout<<"============================="<<std::endl;}
总结
本节完成 proto 和 onmessage 实现
小技巧:读取定长二进制整数–暂时还没遇到问题
C++ 里读取头部 4 字节为整数的方法(假设 buffer 是 std::string):
uint32_t header_size = 0;
memcpy(&header_size, recv_str.data(), sizeof(uint32_t));
注意字节序问题(本地是小端序)——视项目需求是否使用 ntohl()
。
相关文章:
施磊老师rpc(四)
文章目录 rpc网络服务简介RpcProvider 的设计目标Eventloop不使用智能指针-弃用RpcProvider类似于集群的服务器provider网络实现**src/include/rpcprovider.h****src/include/mprpcapplication.h****src/rpcprovider.cc** 错误1错误2-重点**本项目的 mprpc 是动态库, muduo..是…...
Java学习手册:MyBatis 框架作用详解
一、MyBatis 简介 MyBatis 是一款优秀的持久层框架,用于简化 JDBC 开发。它通过将 Java 对象与数据库表之间的映射关系进行配置,使得开发者可以使用简单的 SQL 语句和 Java 代码来完成复杂的数据操作。MyBatis 支持自定义 SQL 语句,提供了灵…...
【PostgreSQL数据分析实战:从数据清洗到可视化全流程】3.1 数据质量评估指标(完整性/一致性/准确性)
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 数据质量评估核心指标:完整性、一致性、准确性实战解析3.1 数据质量评估指标体系3.1.1 完整性:数据是否存在缺失1.1.1 核心定义与业务影响1.1.2 检测…...
分布式系统中的 ActiveMQ:异步解耦与流量削峰(一)
一、引言 在当今数字化时代,分布式系统已成为构建大规模应用的关键架构。随着业务的快速发展和用户量的急剧增长,分布式系统面临着诸多挑战,其中异步通信、系统解耦和流量削峰是亟待解决的重要问题。 以电商系统为例,在秒杀活动中…...
【PostgreSQL数据分析实战:从数据清洗到可视化全流程】2.5 事务与锁机制(ACID特性/事务控制语句)
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 PostgreSQL 事务与锁机制深度解析:ACID 特性与事务控制全流程2.5 事务与锁机制2.5.1 ACID 特性与实现原理2.5.1.1 ACID 核心概念2.5.1.2 MVCC(多版本并发控制)与WAL(预写式日志)协同效应2.5.2 事务…...
STM32教程:ADC原理及程序(基于STM32F103C8T6最小系统板标准库开发)*详细教程*
前言: 本文章介绍了STM32微控制器的ADC外设,介绍了ADC的底层原理以及基本结构,介绍了ADC有关的标准库函数,以及如何编写代码实现ADC对电位器电压的读取。 可以根据基本结构图来编写代码 大体流程: 1、开启RCC时钟&am…...
RabbitMQ 深度解析:从核心组件到复杂应用场景
一.RabbitMQ简单介绍 消息队列作为分布式系统中不可或缺的组件,承担着解耦系统组件、保障数据可靠传输、提高系统吞吐量等重要职责。在众多消息队列产品中,RabbitMQ 凭借其可靠性和丰富的特性,在企业级应用中获得了广泛应用。本研究报告将全…...
linux 使用nginx部署ssl证书,将http升级为https
前言 本文基于:操作系统 CentOS Stream 8 使用工具:Xshell8、Xftp8 服务器基础环境: nginx - 请查看 linux 使用nginx部署vue、react项目 所需服务器基础环境,请根据提示进行下载、安装。 1.下载证书 以腾讯云为例ÿ…...
iview 分页改变每页条数时请求两次问题
问题 在iview page分页的时候,修改每页条数时,会发出两次请求。 iview 版本是4.0.0 原因 iview 的分页在调用on-page-size-change之前会调用on-Change。默认会先调用on-Change回到第一页,再调用on-page-size-change改变分页显示数量 此时就会…...
【Hive入门】Hive与Spark SQL深度集成:Metastore与Catalog兼容性全景解析
目录 引言 1 元数据管理体系架构对比 1.1 Hive Metastore架构解析 1.2 Spark Catalog系统设计 2 元数据兼容性深度剖析 2.1 元数据模型映射关系 2.2 元数据同步机制 3 生产环境配置指南 3.1 基础兼容性配置 3.1.1 Spark连接Hive Metastore 3.1.2 多引擎共享配置 3.…...
C#与西门子PLC通信:S7NetPlus和HslCommunication使用指南
西门子S7协议是用来和PLC进行通讯的一个协议,默认端口是102,数据会保存在一个个DB块中,比较经典的用法是一个DB块专门用来读取,一个用来写入。 DB(数据块) {块号}.DBX/DBD/DBW{字节地址}.{位偏移} 1、数据…...
湖北理元理律师事务所:法律科技融合下的债务管理实践
随着债务纠纷数量攀升,如何通过合法途径化解债务风险成为社会焦点。湖北理元理律师事务所作为国家司法局注册的债事服务机构,尝试以“法律技术”重构传统服务模式,为债务人提供系统性解决方案。 专业化服务架构 该律所设立客服、运营、法务…...
Spring Cloud Gateway MVC 基于 Spring Boot 3.4 以 WAR 包形式部署于外部 Tomcat 实战
一、引言 随着微服务架构的广泛应用,Spring Cloud Gateway 作为网关层的核心组件,为服务间的通信与流量管理提供了强大支持。spring-cloud-starter-gateway-mvc 则进一步助力开发者以熟悉的 MVC 模式进行网关开发。同时,将项目以 WAR 包形式…...
LLM论文笔记 27: Looped Transformers for Length Generalization
Arxiv日期:2024.9.25 关键词 长度泛化 transformer结构优化 核心结论 1. RASP-L限制transformer无法处理包含循环的任务的长度泛化 2. Loop Transformer显著提升了长度泛化能力 Input Injection 显著提升了模型的长度泛化性能,尤其在二进制加法等复杂…...
PCIe TLP | 报头 / 包格式 / 地址转换 / 寄存器 / 配置空间类型
注:本文为 “PCIe TLP” 相关文章合辑。 英文引文,机翻未校。 中文引文,未整理去重。 图片清晰度受引文原图所限。 略作重排,如有内容异常,请看原文。 PCIe - TLP Header, Packet Formats, Address Translation, Conf…...
《AI大模型应知应会100篇》第46篇:大模型推理优化技术:量化、剪枝与蒸馏
第46篇:大模型推理优化技术:量化、剪枝与蒸馏 📌 目标读者:人工智能初中级入门者 🧠 核心内容:量化、剪枝、蒸馏三大核心技术详解 实战代码演示 案例部署全流程 💻 实战平台:PyTor…...
C++/SDL 进阶游戏开发 —— 双人塔防(代号:村庄保卫战 20)
🎁个人主页:工藤新一 🔍系列专栏:C面向对象(类和对象篇) 🌟心中的天空之城,终会照亮我前方的路 🎉欢迎大家点赞👍评论📝收藏⭐文章 文章目录 三…...
【Python生成器与迭代器】核心原理与实战应用
目录 前言技术背景与价值当前技术痛点解决方案概述目标读者说明一、技术原理剖析核心概念图解核心作用讲解关键技术模块说明技术选型对比二、实战演示环境配置要求核心代码实现案例1:自定义迭代器类案例2:生成器函数案例3:生成器表达式运行结果验证三、性能对比测试方法论量…...
2025年最新嵌入式开发STM32单片机详细教程(更新中)
ARM 处理器架构 ARM 处理器从 1984 ARM-1 发展到 2004 ARM-11 之后,放弃数字命名,用 cortex 来命令处理器产品。 Cortex-A系列 主打高性能 手机,平板,智能电视等 Cortex-R系列 主打实时 汽车,工业控…...
neatchat轻量级丝滑的ai模型web客户端
NeatChat 人工智能模型对话web客户端 前言 此项目是nextchat分支,相比原者更加简洁流畅。 部署 docker部署 name: next-chat services:chatgpt-next-web:ports:- 8080:3000environment:- OPENAI_API_KEYsk-xx543Ef3d- BASE_URLhttps://api.ai.com- GOOGLE_API_K…...
学习黑客分析案例
▶️ Day 2 任务 – 「怪物图鉴」实战 选一条最新安全事件(国内外均可,建议 1 年内) 例:CVE-2024-21887 Ivanti VPN RCE 用下列表格框架,3 句话归纳它的“派系”“CIA 受击点”“一句话原理”: 攻击流派…...
sonar-scanner在扫描JAVA项目时为什么需要感知.class文件
1 概述 SonarQube是一个静态代码分析工具,主要用于检查源代码的质量,包括代码重复、潜在漏洞、代码风格问题等。而SonarScanner是SonarQube的客户端工具,负责将代码进行形态分析,并将结果发送到SonarQube服务器。所以,…...
AtCoder Beginner Contest 404(ABCDE)
A - Not Found 翻译: 给您一个字符串S,长度在1 到25 之间,由小写英文字母组成。 输出S 中没有出现的一个小写英文字母。 如果有多个这样的字母,可以输出其中任何一个。 思路: 数组记录存在于 s 中的字母。(…...
【言语理解】中心理解题目之结构分析
front:中心理解题目之抓住关键信息 3.1 五种常见对策表达方式 3.1.1 祈使或建议给对策 应该(应) 需要(要) eg:……。对此,媒体要做好自我规约。……。 eg:……。然而,两地仅简单承接…...
DeepSeek-Prover-V2-671B:AI在数学定理证明领域的重大突破
文章目录 什么是DeepSeek-Prover-V2-671B?核心技术亮点1. **超大规模参数与高效推理**2. **超长上下文窗口**3. **强化学习与合成数据** 主要应用场景1. **教育领域**2. **科学研究**3. **工程设计**4. **金融分析** 开源与商业化性能表现总结 2025年4月30日&#x…...
React18组件通信与插槽
1、为DOM组件设置Props 在react中jsx中的标签属性被称为Props DOM组件的类属性,为了防止与js中的class属性冲突改成了className DOM组件的style属性 import image from "./logo.svg"; function App() {const imgStyleObj {width: 200,height: 200,};re…...
第15章 对API的身份验证和授权
第15章 对API的身份验证和授权 在构建RESTful API时,确保只有经过身份验证和授权的用户才能访问特定资源是至关重要的。身份验证是确认用户身份的过程,而授权则是决定用户是否有权访问特定资源的过程。在本章中,我们将详细探讨如何在ASP.NET Core Web API中实现身份验证和授…...
【项目归档】数据抓取+GenAI+数据分析
年后这两个月频繁组织架构变动,所以博客很久没更新。现在暂时算是尘埃落定,趁这段时间整理一下。 入职九个月,自己参与的项目有4个,负责前后端开发,测试,devops(全栈/doge)ÿ…...
如何优化MySQL主从复制的性能?
优化MySQL主从复制的性能需要从硬件、配置、架构设计和运维策略等多方面入手。以下是详细的优化方案: 一、减少主库写入压力 1. 主库优化 二进制日志(binlog)优化: 使用 binlog_formatROW 以获得更高效的复制和更少的数…...
asp.net客户管理系统批量客户信息上传系统客户跟单系统crm
# crm-150708 客户管理系统批量客户信息上传系统客户跟单系统 # 开发背景 本软件是给郑州某企业管理咨询公司开发的客户管理系统软件 # 功能 1、导入客户数据到系统 2、批量将不同的客户分配给不同的业务员跟进 3、可以对客户数据根据紧急程度标记不同的颜色,…...
PCIe | TLP | 报头 / 包格式 / 地址转换 / 配置空间 / 寄存器 / 配置类型
注:本文为 “PCIe - TLP” 相关文章合辑。 英文引文,机翻未校。 中文引文,未整理去重。 图片清晰度受引文原图所限。 略作重排,如有内容异常,请看原文。 PCIe - TLP Header, Packet Formats, Address Translation, Co…...
ip和域名
好的,我来依次回答你的问题: 域名和 IP 地址是什么关系? IP 地址 (Internet Protocol Address):可以想象成互联网上每台设备(比如服务器、电脑、手机)的门牌号码。它是一串数字(例如 IPv4 地址 …...
《解锁GCC版本升级:开启编程新世界大门》
《解锁GCC版本升级:开启编程新世界大门》 一、引言:GCC 版本升级的魔法钥匙 在编程的广阔天地里,GCC(GNU Compiler Collection)宛如一座灯塔,为无数开发者照亮前行的道路。它是一款开源且功能强大的编译器集合,支持 C、C++、Objective - C、Fortran、Ada 等多种编程语言…...
前端跨域问题怎么在后端解决
目录 简单的解决方法: 添加配置类: 为什么会跨域 1. 什么是源 2. URL结构 3. 同源不同源举🌰 同源例子 不同源例子 4. 浏览器为什么需要同源策略 5. 常规前端请求跨域 简单的解决方法: 添加配置类: packag…...
生成式 AI 的工作原理
在科技浪潮汹涌澎湃的当下,生成式 AI 宛如一颗璀璨的新星,照亮了我们探索未知的征程。它不再仅仅是科幻电影中的幻想,而是已经悄然融入我们生活的方方面面,从智能客服的贴心应答,到艺术创作的天马行空,生成式 AI 正以一种前所未有的姿态重塑着世界。然而,你是否曾好奇,…...
DeepSeek辅助学术写作之修订与校稿以及发表与推广相关提示词分享祝你顺利毕业~
目录 1.修订与校对 2.发表与推广 大家好这里是AIWritePaper官方账号,官网👉AIWritePaper~ 宝子们可以使用小编精选的“ChatGPT研究论文提示词”集合来创建研究论文。利用ChatGPT的智能回应生成详尽有效的内容,这样可以加快研究论文的策划、…...
叠层阻抗线框
1.阻抗介绍 特性阻抗:又称“特征阻抗”,它不是直流电阻,属于长线传输中的概念。在高频范围内,信号传输过程中,信号沿到达的地方,信号线和参考平面(电源或地平面)间由于电场的建立&am…...
大数据:驱动技术创新与产业转型的引擎
📝个人主页🌹:慌ZHANG-CSDN博客 🌹🌹期待您的关注 🌹🌹 在全球数字化转型的浪潮中,大数据已经成为推动各行各业革新的核心力量。随着信息技术的迅猛发展,数据的收集、存储、处理和分析能力不断提升,大数据不仅改变了企业的运营模式,更引领了技术创新和产业结构…...
C++继承基础总结
引言 在编写多个类时,类之间可能会存在多个相同的成员变量,导致代码冗余度过高,C继承的出现,使得我们可以在已有类的基础上构建新类,从而实现代码复用与结构扩展。 一、继承的基本概念 继承是指子类(派生…...
2025年斯诺克世锦赛——“75三杰”在1/4决赛作为对手的概率
“在最近的斯诺克世锦赛中,中国两名球员成功挺进前8强。此外,前8强也出现令人感慨的一幕:75三杰全部进入到了八强,这也是历史第5次,50岁正是打拼的年纪!传奇之旅继续!”——摘自50岁正是打拼的年…...
Python绘制地球的重力地图
文章目录 Boule重力地图从ensaio下载重力数据Boule boule中定义了多种参考椭球,可用于表示地球、火星等星体的重力分布。可通过pip安装 pip install bouleboule中已经定义的椭球如下 椭球GRS80WGS84MARSMERCURYMOONVENUSVESTA星体地球地球火星水星月球金星灶神星这些椭球可直…...
多端定制系统开发:打造高效全平台覆盖的APP解决方案
在移动互联网时代,用户需求日益多样化,企业若想触达更广泛的受众,仅靠单一平台的应用已远远不够。无论是iOS、Android、Web端,还是智能手表、车载设备等新兴终端,多端适配已成为企业数字化转型的刚需。多端定制系统开发…...
WGDI-分析WGD及祖先核型演化的集成工具-文献精读126
WGDI: A user-friendly toolkit for evolutionary analyses of whole-genome duplications and ancestral karyotypes WGDI:一款面向全基因组重复事件与祖先核型演化分析的易用工具集 摘要 在地球上大多数主要生物类群中,人们已检测到全基因组复制&…...
旋转矩阵公式理解
这里给出其中一种理解方法: 设原始直角坐标系下的坐标为(x,y),我们可以将它分解为两个向量(x,0)和(0,y)。接下来将两个向量分别顺时针旋转θ度,二者就会分别变成:(xcosθ,xsinθ)和(-ysinθ,ycosθ)。 也就是说,二者…...
网络Tips20-002
1..某主机接口的IP地址为192.16.7.131/26.则该IP地址所在网络的广播地址是:192 16.7.191 广播地址是指在特定网络上发送广播消息的地址。它用于向网络上的所有设备发送信息。 方法1:广播地址掩码取反和网络地址的或运算 方法2:广播地址将网…...
firewall docker 冲突问题解决(亲测有效)
# 关闭iptables,使用firewall systemctl disable iptables # 禁用服务 systemctl stop iptables # 关闭服务 systemctl status iptables # 查看服务状态 systemctl enable firewalld # 设置防火墙开机自启动 systemctl start firewalld # 开启服务 systemctl s…...
SwiftUI-MLX本地大模型开发(二)
介绍 在 SwiftUI-MLX本地大模型开发一文中,我们已经详细讲了如何利用 MLX 进行本地大模型的开发。但是通过案例可以发现 2 个问题: MLX 内置的大模型数量有限。每次大模型都需要从 HuggingFace 下载。 如何解决这 2 个问题,方案是:…...
基于「骑手外卖系统」串联7大设计原则
你说得对!这些设计原则听起来都很抽象、很“玄”,如果不是实际开发过系统,很难理解“到底为什么要这样设计”。 那我现在就用一个你能想象得很清楚的真实例子,帮你把这7个设计原则一一落地到具体情境里,你会一眼明白。…...
泰迪杯特等奖案例学习资料:基于时空图卷积网络的城市排水系统水位精准重建与异常检测
(第十四届泰迪杯数据挖掘挑战赛A题特等奖案例解析) 一、案例背景与核心挑战 1.1 应用场景与行业痛点 城市排水系统(Urban Drainage Network, UDN)是城市基础设施的重要组成部分,其运行效率直接影响防洪排涝能力和水环境质量。然而,实际运维中面临以下难题: 监测数据稀…...
嵌入式Linux应用项目----智能网关
一、网关概述: Linux网关的作用在于,通过蓝牙、LoRa、串口、CAN等接口,与哪些无法连接网络的设备建立联系,将它们的数据转发至服务器。这过程中,网关充当了数据的桥梁,将下级设备所产生的数据发送至服务器。…...