RPC:grpc的使用与安装、server和client的使用示例、lsb_relese、自定制协议(LVC)+protobuf的RPC调用框架的实现

rpc

远程过程调用

  • Remote Procrdure Call ---->底层自动处理网络传输数据序列化细节

使用客户端服务器 的模式,将本地数据 传输到服务器 上进行处理并获取结果 ,但整个过程,是封装 起来的,对用户不可见 的,使用过程就类似本地的一个函数调用

grpc

安装

复制代码
sudo apt install libgrpc-dev libgrpc++-dev
//grpc框架中用于对protobuf的特殊生成插件
sudo apt install protobuf-compiler-grpc

RPC 的核心工作流程

  1. 客户端调用 :客户端代码像调用本地方法一样发起请求(如 userService.getUser(id))。

  2. Client Stub(客户端存根/代理):将调用的方法名、参数打包序列化(Marshalling)为二进制或文本字节流。

  3. 网络传输(Transport):通过 TCP、UDP 或 HTTP/2 将数据发送给服务端。

  4. Server Stub(服务端存根/解包):服务端接收数据包,反序列化(Unmarshalling)还原出方法名与参数。

  5. 服务端执行:根据解析出的方法,调用本地服务逻辑(如查询数据库),获得执行结果。

  6. 响应返回:结果由 Server Stub 序列化后发回客户端,Client Stub 解包并返回给客户端调用方。

RPC vs RESTful

比较维度 RPC (如 gRPC) RESTful API
设计理念 面向动作/过程(getUser(), createOrder() 面向资源(GET /users/1, POST /orders
数据格式 主要是二进制(如 Protobuf),也可用 JSON 主要为 JSON、XML
传输协议 通常基于 TCP 或 HTTP/2 通常基于 HTTP/1.1
性能与延迟 极高(Payload 极小,序列化与反序列化速度极快) 中等(HTTP Header 较大,文本解析开销相对高)
适用场景 微服务内部 高吞吐、低延迟的 Server-to-Server 通信 对外开放接口、Web 前端/移动端与后端通信

定义与使用

1**.请求与响应** 的数据结构定义

2.定义rpc相关的调用接口格式

rpc.proto

cpp 复制代码
syntax="proto3";
package calculate;
 
//开启rpc接口生成 选项
//option cc_generic_services=true;
message AddRequest
{
    int32 num1=1;
    int32 num2=2;
}
​
message AddResponse
{
    int32 result=1;
}
service Calculate
{
    rpc Add(AddRequest) returns (AddResponse);
}
​
//protoc -I. --cpp_out=. --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` rpc.proto

编译

bash 复制代码
protoc -I. --cpp_out=. --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` rpc.proto

这条命令通过 Protocol Buffers 编译器(protoc)将 rpc.proto 文件编译生成 C++ 语言的代码,同时包含Protobuf 数据序列化代码 与 gRPC 服务网络接口代码

各参数的具体含义如下:

  • protoc:Protocol Buffers 编译器主程序,负责解析 .proto 语法。

  • -I. (即 --proto_path=.):指定查找 .proto 依赖文件的搜索路径。. 表示在当前工作目录下搜索。

  • --cpp_out=.:指示生成标准 Protobuf 数据结构的 C++ 代码,并将文件输出到当前目录(.)。

  • --grpc_out=.:指示生成 gRPC 服务框架相关的 C++ 代码,并将文件输出到当前目录(.)。

  • --plugin=protoc-gen-grpc=\which grpc_cpp_plugin**:为 `protoc` 指定生成 gRPC 代码所需的 C++ 插件路径。which grpc_cpp_plugin```会在系统环境变量中自动获取``grpc_cpp_plugin` 可执行文件的绝对路径。

    • 不加这个参数,protoc 就只能生成数据结构代码无法生成 gRPC 的客户端与服务端网络调用代码.grpc.pb.h / .grpc.pb.cc
  • rpc.proto:作为输入的源定义文件。

编译生成的产物说明

选项 生成的文件 主要作用
--cpp_out=. rpc.pb.h rpc.pb.cc 定义消息体类(如 AddRequestAddResponse),提供数据设置、获取以及二进制序列化/反序列化方法。
--grpc_out=. rpc.grpc.pb.h rpc.grpc.pb.cc 定义 RPC 服务基类(calculate::Calculate::Service)以及客户端调用的存根类(Stub),提供网络传输与方法调度能力。

server.cc

cpp 复制代码
#include <iostream>
#include <memory>
#include <string>
​
#include <grpcpp/grpcpp.h>
#include "rpc.pb.h"
#include "rpc.grpc.pb.h" // 包含 gRPC 生成的服务头文件
​
// 继承 gRPC 生成的 Service 基类
class CalculateImpl final : public calculate::Calculate::Service 
{
public:
    // 使用 gRPC 标准的重写签名
    grpc::Status Add(grpc::ServerContext* context,
                     const ::calculate::AddRequest* request,
                     ::calculate::AddResponse* response) override
    {
        // 实现真正的数据处理逻辑
        int result = request->num1() + request->num2();
        response->set_result(result);
        
        // 返回 OK 状态告诉 gRPC 框架处理成功
        return grpc::Status::OK;
    }
};
​
int main()
{
    std::string server_address("0.0.0.0:9000");
    
    // 实例化计算服务对象(保持生命周期贯穿服务器运行期)
    CalculateImpl service;
​
    // 实例化 gRPC 服务器构建器
    grpc::ServerBuilder builder;
​
    // 设置监听地址与非加密通信凭据
    builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
​
    // 注册计算服务
    builder.RegisterService(&service);
​
    // 构建并启动 gRPC 服务器
    std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
    std::cout << "Server listening on " << server_address << std::endl;
​
    // 阻塞主线程,保持服务器处于监听并响应状态
    server->Wait();
​
    return 0;
}

client.cc

cpp 复制代码
#include <iostream>
#include <memory>
#include <string>
​
#include <grpcpp/grpcpp.h>
#include "rpc.pb.h"
#include "rpc.grpc.pb.h"
​
class CalculateClient {
public:
    // 1. 构造函数:基于 Channel 创建服务的 Stub 实例
    explicit CalculateClient(std::shared_ptr<grpc::Channel> channel)
        : stub_(calculate::Calculate::NewStub(channel)) {}
​
    // 2. 业务封装方法
    int Add(int num1, int num2) {
        // 组装请求消息
        calculate::AddRequest request;
        request.set_num1(num1);
        request.set_num2(num2);
​
        // 准备响应结构体与 Context
        calculate::AddResponse response;
        grpc::ClientContext context; // 可在此设置超时时间等
​
        // 3. 发起同步 RPC 调用(阻塞等待服务端响应)
        grpc::Status status = stub_->Add(&context, request, &response);
​
        // 4. 检查调用结果
        if (status.ok()) {
            return response.result();
        } else {
            std::cerr << "RPC 调用失败 (" << status.error_code() << "): " 
                      << status.error_message() << std::endl;
            return -1;
        }
    }
​
private:
    // 持有 gRPC 生成的 Stub 指针
    std::unique_ptr<calculate::Calculate::Stub> stub_;
};
​
int main() {
    // 设置服务端监听地址
    std::string target_str("127.0.0.1:9000");
​
    // 创建非加密通信的 Channel 管道
    auto channel = grpc::CreateChannel(target_str, grpc::InsecureChannelCredentials());
​
    // 实例化客户端对象
    CalculateClient client(channel);
​
    // 发起 RPC 计算请求
    int a = 15;
    int b = 27;
    int result = client.Add(a, b);
​
    std::cout << "客户端调用成功: " << a << " + " << b << " = " << result << std::endl;
​
    return 0;
}

makefile

cpp 复制代码
.PHONY: all clean proto
​
# 1. 显式指定生成的 protobuf/grpc 源文件
PB_SRC = rpc.pb.cc rpc.grpc.pb.cc
​
all: proto client server
​
# 2. 自动由 rpc.proto 生成 c++ 源文件
proto: rpc.proto
    protoc -I. --cpp_out=. --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` rpc.proto
​
# 3. 编译客户端(注意:使用 -lgrpc++)
client: $(PB_SRC) client.cc
    g++ -std=c++17 $^ -o $@ -lgrpc++ -lprotobuf
​
# 4. 编译服务端
server: $(PB_SRC) server.cc
    g++ -std=c++17 $^ -o $@ -lgrpc++ -lprotobuf
​
clean:
    rm -f client server *.pb.cc *.pb.h

lsb_release -a

Linux 命令,查看系统发行版本信息

拆解
  • lsb_release:工具命令,遵循 LSB (Linux Standard Base) 标准,用来输出系统版本

  • -a:all,输出全部信息

执行示例
bash 复制代码
lsb_release -a

输出类似:

bash 复制代码
LSB Version:    ::core-4.1-amd64:core-4.1-noarch
Distributor ID: CentOS
Description:    CentOS Linux release 7.9.2009 (Core)
Release:    7.9.2009
Codename:   Core

字段含义:

  1. Distributor ID:发行商(Ubuntu/CentOS/Debian)

  2. Description:系统完整描述

  3. Release:系统版本号

  4. Codename:版本代号

⚠️注意
  1. 最小化安装可能没有这个命令

    (提示 command not found)

    • CentOS/RHEL:yum install redhat-lsb-core

    • Ubuntu/Debian:apt install lsb-release

  2. 替代查看版本命令(不用装包)

复制代码
cat /etc/os-release
cat /etc/*release
常用简写
  • lsb_release -d 只看系统描述

  • lsb_release -r 只看版本号

用途:编译、部署软件时确认是什么 Linux 发行版、版本。

自定义RPC框架

RPC请求

bash 复制代码
rpc_message.proto

syntax="proto3"
package rpc_message;
​
//枚举消息类型
enum MessageType
{
    ERROR=0;
    REQUEST=1;
    RESPONSE=2;
}
​
//错误码
enum ErorrCode
{
    NO_ERROR=0;
    WRONG_PROTO=1;//协议类型错误
    NO_SERVICE=2;
    NO_METHOD=3;
    INVALID_REQUEST=4;//无效请求
    INVALID_RESPONSE=5;//无效响应
    TIMEOUT=6;
}
//通信消息结构
message RpcMessage
{
    fixed32 id=1;
    MessageType type=2;//消息类型
    string service=3;//服务名称
    string method=4;//方法名称
    
    //bytes request=5;
    //bytes response=6;
    // oneof联合体:request 和 response 互斥,最多只有一个生效
    oneof payload {
        bytes request =5;
        bytes response =6;
    }
    ErorrCode error=7;
}

RPC服务端

RPC客户端

如果我们要自己实现一个RPC客户端

1.实现一个类,继承google::protobuf::RpcChannel类

  • 重写CallMethod函数:实现通过我们自己的connection发送请求数据****(按照自定制协议(LVC)组装我们的RPC请求工具)

  • 请求发送后,服务器处理完毕会进行响应,我们需要能够知道收到的那个响应对应 的是哪个请求 ,应该是使用哪个回调函数 进行响应处理

2.在客户端的OnMessage 中,接收响应并解析响应 ,并且根据响应找到对应的响应处理回调函数

  • 客户端需要建立一个映射关系:请求ID<-->响应处理回调函数

序列化和协议之间有什么关系?

序列化:目的是将多个独立的数据对象按照指定格式组织到一起,用于传输

网络通信协议通信的客户端与服务器 之间的数据格式约定

  • 数据的组织格式

  • 解决通信中可能存在的问题(TCP粘包问题

    • TCP粘包:TCP传输层不关注数据边界,因此可能将多条数据当作一条处理

    • 解决:LV(Length+value)格式------>数据增加边界

cpp 复制代码
// 设置给网络通信链接的消息处理回调函数
void RpcChannel::onMessage(net::TcpConnectionPtr conn, 
    net::Buffer* buf, 
    net::Timestamp rtime){
    // 使用RpcCodec解析收到的数据
    _codec.onMessage(conn, buf, rtime);
}
​
// 提供给RpcCodec的protobuf消息处理回调函数
void RpcChannel::onRpcMessage(net::TcpConnectionPtr conn, 
    MessagePtr message, 
    net::Timestamp rtime){
    //根据消息类型,决定处理方式:请求还是响应,处理方式是不一样的
    if (message->type() == rpc_message::MessageType::REQUEST) {
        assert(message->has_service() && message->has_method() && 
            message->has_request());
        rpc_message::ErrorCode ecode = rpc_message::ErrorCode::NO_ERROR;
        //1. 取出消息中的核心要素:服务名称 + 方法名称
        std::string service_name = message->service();
        std::string method_name = message->method();
        if (_services == NULL) {
            ecode = rpc_message::ErrorCode::NO_SERVICE;
        }else {
            //2. 从路由表中根据服务名称,查找服务对象,进而就能得到方法描述对象
            auto it = _services->find(service_name);
            if (it == _services->end()) {
                ecode = rpc_message::ErrorCode::NO_SERVICE;
            }else {
                google::protobuf::Service* service = it->second;
                auto *method = service->GetDescriptor()->FindMethodByName(method_name);
                if (method == NULL) {
                    ecode = rpc_message::ErrorCode::NO_METHOD;
                }else {
                    //3. 构造请求与响应数据对象, 并使用请求对象对收到的消息中的request进行反序列化
                    auto *request = service->GetRequestPrototype(method).New();
                    auto *response = service->GetResponsePrototype(method).New();
                    bool ret = request->ParseFromString(message->request());
                    if (ret == false) {
                        ecode = rpc_message::ErrorCode::INVALID_REQUEST;
                    }else {
                        //4. 构造处理回调函数:设置给protobuf框架,业务处理完毕后,调用的函数
                        int64_t rid = message->id();
                        gp::Closure *done = gp::NewCallback(this, &RpcChannel::doneCallback, response, rid);
                        //5. 通过服务对象,进行业务处理
                        service->CallMethod(method, nullptr, request, response, done);
                    }
                }
            }
        }
        if (ecode != rpc_message::ErrorCode::NO_ERROR) {
            //发送错误响应
            rpc_message::RpcMessage resp;
            resp.set_id(message->id());
            resp.set_type(rpc_message::MessageType::RESPONSE);
            resp.set_error(ecode);
            _codec.send(_connection, resp);
        }
    }else if (message->type() == rpc_message::MessageType::RESPONSE) {
        assert(message->has_response() && message->has_error());
        // 1. 取出响应中的核心要素:请求ID
        int64_t rid = message->id();
        // 2. 从outstandings的map中根据请求ID查找未完成处理对象
        OutStanding out = {nullptr, nullptr};
        {
            std::unique_lock<std::mutex> lock(_mtx);
            auto it = _outstandins.find(rid);
            if (it != _outstandins.end()) {
                out = it->second;
                _outstandins.erase(it);
            }
        }
        // 3. 未完成对象中,若存在response,则使用response对响应数据进行反序列化
        if (out.response) {
            out.response->ParseFromString(message->response());
        }
        // 4. 未完成对象中,若存在done,则调用done->Run()进行响应处理
        if (out.done) {
            out.done->Run();
        }
    }else {
        LOG_ERROR("消息类型错误");
    }
}

校验和算法

算法 特点 常见场景
简单加法 Checksum 非常简单 简单协议
CRC 检错能力强 网络、存储、文件
Internet Checksum 基于 16 位反码求和 IP/TCP/UDP
Adler-32 速度快 一些压缩/数据处理场景
Fletcher Checksum 比简单加法更强 数据完整性检查
相关推荐
luj_17681 小时前
虚实交融中的真实人物塑造
c语言·开发语言·网络·经验分享·算法
青瓦梦滋2 小时前
NAT技术
linux·服务器·网络·网络协议·tcp/ip
RobinDevNotes2 小时前
RoCEv2如何扛起大模型训练网络
linux·网络·ai·网络linux
G佳伟3 小时前
宝塔面板打不开且 Bt-Panel 未运行:从 HTTP 502 到 gevent 缺失的完整排查与修复
网络协议·http·php
keyipatience3 小时前
IP网络层
服务器·网络·网络协议·ubuntu·http
白狐_7983 小时前
408 计算机网络|HTTP 的 RTT 怎么数:用多道题讲清楚
网络协议·计算机网络·http
Discipline~Hai3 小时前
Linux网络编程03-HTTP协议
linux·运维·服务器·网络·网络协议·http·linux网络编程
程序猿乐锅4 小时前
【计算机组成原理 | 第八章】I/O系统
运维·服务器·网络