BRpc 二次封装:Channel 管理、服务管理与回调工厂

📚 系列:C++ 工程实践 第 10 篇 ⏱ 阅读约 13 分钟
📌 一句话总结

上一篇讲 brpc 怎么调通,这一篇讲怎么在业务里用得下去:用四个小类把「节点管理、选路、异步回调、起服务」收口,业务代码只写服务名、节点和业务 lambda。

文章目录

    • [1. 先看结果:封装后,一次异步调用长什么样](#1. 先看结果:封装后,一次异步调用长什么样)
    • [2. 为什么封装:四个类各解决什么](#2. 为什么封装:四个类各解决什么)
    • [3. Channels:一个服务的多节点选路](#3. Channels:一个服务的多节点选路)
    • [4. SvcChannels:服务名 → Channels](#4. SvcChannels:服务名 → Channels)
    • [5. ClosureFactory:把 lambda 变成 brpc Closure](#5. ClosureFactory:把 lambda 变成 brpc Closure)
    • [6. RpcServerFactory 与使用样例](#6. RpcServerFactory 与使用样例)
    • [7. 踩坑与总结](#7. 踩坑与总结)

1. 先看结果:封装后,一次异步调用长什么样

封装的目标不是「更高级」,而是让业务代码里不再出现 Channel、Controller 的样板。 上一篇的异步调用要自己 new 出 request/response/cntl、写 NewCallback;封装后是这样:

cpp 复制代码
#include "rpc.h"
#include "cal.pb.h"

#include <iostream>
#include <memory>

int main() {
    rpcwrap::SvcChannels scs;
    scs.setWatch("cal");                        // 关注哪个服务
    scs.addNode("cal", "127.0.0.1:9000");       // 节点从配置/服务发现来
    auto channel = scs.getNode("cal");          // 轮流拿一个可用节点
    if (!channel) { std::cerr << "no node\n"; return -1; }

    auto* cntl = new brpc::Controller;
    auto* req = new cal::AddReq;
    req->set_num1(22);
    req->set_num2(333);
    auto* rsp = new cal::AddRsp;

    auto closure = rpcwrap::ClosureFactory::create([=]() {
        std::unique_ptr<brpc::Controller> c(cntl);
        std::unique_ptr<cal::AddReq> q(req);
        std::unique_ptr<cal::AddRsp> s(rsp);
        if (c->Failed()) { std::cerr << c->ErrorText() << "\n"; return; }
        std::cout << "22 + 333 = " << s->result() << std::endl;
    });

    cal::CalService_Stub stub(channel.get());
    stub.Add(cntl, req, rsp, closure);
    std::cout << "异步请求已发出\n";
    std::cin.get();                              // 演示:等回调
    return 0;
}

和上一篇直接调 brpc 比,差别在「样板去哪了」:

事情 原生写法 封装后
管节点 手工 Channel.Init SvcChannels.addNode
选节点 自己记地址 getNode 轮询
异步回调 NewCallback(函数, 参数...) ClosureFactory::create(lambda)
起服务 AddService/Start 三件套 RpcServerFactory::create
客户端代码行数 约 35 行 约 18 行

真正省掉的不是行数,而是「出错的地方」:节点地址不再散落在业务里,改配置只改一处;回调原型固定,不会一半写 NewCallback 一半写 lambda;起服务不再复制粘贴三件套,端口和 Options 只在工厂里出现一次。

2. 为什么封装:四个类各解决什么

一个服务可能多节点,多服务要分别管;回调写法反人类;起服务样板重复------四个类正好对应四件事。
#mermaid-svg-zO4lELpa4mdcoNST{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zO4lELpa4mdcoNST .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zO4lELpa4mdcoNST .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zO4lELpa4mdcoNST .error-icon{fill:#552222;}#mermaid-svg-zO4lELpa4mdcoNST .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zO4lELpa4mdcoNST .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zO4lELpa4mdcoNST .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zO4lELpa4mdcoNST .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zO4lELpa4mdcoNST .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zO4lELpa4mdcoNST .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zO4lELpa4mdcoNST .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zO4lELpa4mdcoNST .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zO4lELpa4mdcoNST .marker.cross{stroke:#333333;}#mermaid-svg-zO4lELpa4mdcoNST svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zO4lELpa4mdcoNST p{margin:0;}#mermaid-svg-zO4lELpa4mdcoNST .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zO4lELpa4mdcoNST .cluster-label text{fill:#333;}#mermaid-svg-zO4lELpa4mdcoNST .cluster-label span{color:#333;}#mermaid-svg-zO4lELpa4mdcoNST .cluster-label span p{background-color:transparent;}#mermaid-svg-zO4lELpa4mdcoNST .label text,#mermaid-svg-zO4lELpa4mdcoNST span{fill:#333;color:#333;}#mermaid-svg-zO4lELpa4mdcoNST .node rect,#mermaid-svg-zO4lELpa4mdcoNST .node circle,#mermaid-svg-zO4lELpa4mdcoNST .node ellipse,#mermaid-svg-zO4lELpa4mdcoNST .node polygon,#mermaid-svg-zO4lELpa4mdcoNST .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zO4lELpa4mdcoNST .rough-node .label text,#mermaid-svg-zO4lELpa4mdcoNST .node .label text,#mermaid-svg-zO4lELpa4mdcoNST .image-shape .label,#mermaid-svg-zO4lELpa4mdcoNST .icon-shape .label{text-anchor:middle;}#mermaid-svg-zO4lELpa4mdcoNST .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zO4lELpa4mdcoNST .rough-node .label,#mermaid-svg-zO4lELpa4mdcoNST .node .label,#mermaid-svg-zO4lELpa4mdcoNST .image-shape .label,#mermaid-svg-zO4lELpa4mdcoNST .icon-shape .label{text-align:center;}#mermaid-svg-zO4lELpa4mdcoNST .node.clickable{cursor:pointer;}#mermaid-svg-zO4lELpa4mdcoNST .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zO4lELpa4mdcoNST .arrowheadPath{fill:#333333;}#mermaid-svg-zO4lELpa4mdcoNST .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zO4lELpa4mdcoNST .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zO4lELpa4mdcoNST .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zO4lELpa4mdcoNST .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zO4lELpa4mdcoNST .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zO4lELpa4mdcoNST .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zO4lELpa4mdcoNST .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zO4lELpa4mdcoNST .cluster text{fill:#333;}#mermaid-svg-zO4lELpa4mdcoNST .cluster span{color:#333;}#mermaid-svg-zO4lELpa4mdcoNST div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zO4lELpa4mdcoNST .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zO4lELpa4mdcoNST rect.text{fill:none;stroke-width:0;}#mermaid-svg-zO4lELpa4mdcoNST .icon-shape,#mermaid-svg-zO4lELpa4mdcoNST .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zO4lELpa4mdcoNST .icon-shape p,#mermaid-svg-zO4lELpa4mdcoNST .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zO4lELpa4mdcoNST .icon-shape .label rect,#mermaid-svg-zO4lELpa4mdcoNST .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zO4lELpa4mdcoNST .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zO4lELpa4mdcoNST .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zO4lELpa4mdcoNST :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 业务: setWatch + addNode
SvcChannels.getNode
Channels.select 轮询
Stub 发起调用
ClosureFactory 回调

类 职责 一句话
Channels 一个服务的全部节点信道 + 轮询选路 「这个服务能连谁」
SvcChannels 服务名 → Channels 的映射 「这个服务归谁管」
ClosureFactory lambda → google::protobuf::Closure 「回调别写 NewCallback」
RpcServerFactory 一行创建并启动 Server 「起服务别抄三件套」

类之间的关系:
#mermaid-svg-4DLyoSG4ujdolSAM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4DLyoSG4ujdolSAM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4DLyoSG4ujdolSAM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4DLyoSG4ujdolSAM .error-icon{fill:#552222;}#mermaid-svg-4DLyoSG4ujdolSAM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4DLyoSG4ujdolSAM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4DLyoSG4ujdolSAM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4DLyoSG4ujdolSAM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4DLyoSG4ujdolSAM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4DLyoSG4ujdolSAM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4DLyoSG4ujdolSAM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4DLyoSG4ujdolSAM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4DLyoSG4ujdolSAM .marker.cross{stroke:#333333;}#mermaid-svg-4DLyoSG4ujdolSAM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4DLyoSG4ujdolSAM p{margin:0;}#mermaid-svg-4DLyoSG4ujdolSAM g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-4DLyoSG4ujdolSAM g.classGroup text .title{font-weight:bolder;}#mermaid-svg-4DLyoSG4ujdolSAM .cluster-label text{fill:#333;}#mermaid-svg-4DLyoSG4ujdolSAM .cluster-label span{color:#333;}#mermaid-svg-4DLyoSG4ujdolSAM .cluster-label span p{background-color:transparent;}#mermaid-svg-4DLyoSG4ujdolSAM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4DLyoSG4ujdolSAM .cluster text{fill:#333;}#mermaid-svg-4DLyoSG4ujdolSAM .cluster span{color:#333;}#mermaid-svg-4DLyoSG4ujdolSAM .nodeLabel,#mermaid-svg-4DLyoSG4ujdolSAM .edgeLabel{color:#131300;}#mermaid-svg-4DLyoSG4ujdolSAM .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-4DLyoSG4ujdolSAM .label text{fill:#131300;}#mermaid-svg-4DLyoSG4ujdolSAM .labelBkg{background:#ECECFF;}#mermaid-svg-4DLyoSG4ujdolSAM .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-4DLyoSG4ujdolSAM .classTitle{font-weight:bolder;}#mermaid-svg-4DLyoSG4ujdolSAM .node rect,#mermaid-svg-4DLyoSG4ujdolSAM .node circle,#mermaid-svg-4DLyoSG4ujdolSAM .node ellipse,#mermaid-svg-4DLyoSG4ujdolSAM .node polygon,#mermaid-svg-4DLyoSG4ujdolSAM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4DLyoSG4ujdolSAM .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-4DLyoSG4ujdolSAM g.clickable{cursor:pointer;}#mermaid-svg-4DLyoSG4ujdolSAM g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-4DLyoSG4ujdolSAM g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-4DLyoSG4ujdolSAM .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-4DLyoSG4ujdolSAM .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-4DLyoSG4ujdolSAM .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-4DLyoSG4ujdolSAM .dashed-line{stroke-dasharray:3;}#mermaid-svg-4DLyoSG4ujdolSAM .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-4DLyoSG4ujdolSAM #compositionStart,#mermaid-svg-4DLyoSG4ujdolSAM .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-4DLyoSG4ujdolSAM #compositionEnd,#mermaid-svg-4DLyoSG4ujdolSAM .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-4DLyoSG4ujdolSAM #dependencyStart,#mermaid-svg-4DLyoSG4ujdolSAM .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-4DLyoSG4ujdolSAM #dependencyStart,#mermaid-svg-4DLyoSG4ujdolSAM .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-4DLyoSG4ujdolSAM #extensionStart,#mermaid-svg-4DLyoSG4ujdolSAM .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-4DLyoSG4ujdolSAM #extensionEnd,#mermaid-svg-4DLyoSG4ujdolSAM .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-4DLyoSG4ujdolSAM #aggregationStart,#mermaid-svg-4DLyoSG4ujdolSAM .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-4DLyoSG4ujdolSAM #aggregationEnd,#mermaid-svg-4DLyoSG4ujdolSAM .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-4DLyoSG4ujdolSAM #lollipopStart,#mermaid-svg-4DLyoSG4ujdolSAM .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-4DLyoSG4ujdolSAM #lollipopEnd,#mermaid-svg-4DLyoSG4ujdolSAM .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-4DLyoSG4ujdolSAM .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-4DLyoSG4ujdolSAM .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4DLyoSG4ujdolSAM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4DLyoSG4ujdolSAM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4DLyoSG4ujdolSAM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} SvcChannels
Channels
Channel
RpcServerFactory
Server
ClosureFactory
Closure

3. Channels:一个服务的多节点选路

每个服务一个 Channels,内部用 vector<地址, Channel> 存节点,select 按下标轮询返回一个可用信道。 完整类:

cpp 复制代码
#pragma once

#include <brpc/channel.h>

#include <mutex>
#include <optional>
#include <string>
#include <utility>
#include <vector>

namespace rpcwrap {

using ChannelPtr = std::shared_ptr<brpc::Channel>;

class Channels {
public:
    using ptr = std::shared_ptr<Channels>;

    void insert(const std::string& addr);
    void remove(const std::string& addr);
    ChannelPtr select();
    std::optional<std::string> selectAddr();

private:
    std::mutex mtx_;
    uint32_t idx_ = 0;                                  // 轮询下标
    std::vector<std::pair<std::string, ChannelPtr>> channels_;
};

}  // namespace rpcwrap

关键实现只有两个函数:

cpp 复制代码
void Channels::insert(const std::string& addr) {
    std::unique_lock<std::mutex> lock(mtx_);
    auto channel = std::make_shared<brpc::Channel>();
    brpc::ChannelOptions options;
    options.protocol = "baidu_std";
    options.timeout_ms = 30000;                         // 按业务调,别照抄
    if (channel->Init(addr.c_str(), &options) != 0) {
        return;                                          // 节点不可用,不加入
    }
    channels_.emplace_back(addr, channel);
}

ChannelPtr Channels::select() {
    std::unique_lock<std::mutex> lock(mtx_);
    if (channels_.empty()) return ChannelPtr();
    const size_t idx = (idx_++) % channels_.size();      // 简单轮询
    return channels_[idx].second;
}
设计点 为什么这么写
vector 而不是 map 节点数量少,顺序遍历/轮询更直接
idx_ 自增取模 最简单的负载均衡;要加权/一致性哈希再换策略
锁保护整个 vector 服务发现会并发增删节点,选路也在并发读
Init 失败直接不加 坏节点不进池,调用方自然跳过

这里有个取舍:Channel 是「到某个节点的连接」,不是一次请求。所以同一个节点的 Channel 被多次调用复用,底层连接池也由 brpc 管理;这正是不能每个请求都新建 Channel 的原因------那样等于把连接池扔掉,延迟和 fd 都会爆。
ℹ️ 轮询只是起点

这个封装把「选路」抽出来以后,换成随机、加权、一致性哈希都只改 select() 一个函数;接口不变,业务无感。

4. SvcChannels:服务名 → Channels

它维护「服务名 → 该服务的 Channels」映射,并规定只有 setWatch 声明过的服务才会被管理。 这是「先声明再使用」的模式,避免拼错服务名时静默建出空对象。

cpp 复制代码
class SvcChannels {
public:
    void setWatch(const std::string& svc);                       // 预声明
    void addNode(const std::string& svc, const std::string& addr);
    void delNode(const std::string& svc, const std::string& addr);
    ChannelPtr getNode(const std::string& svc);
    std::optional<std::string> getNodeAddr(const std::string& svc);

private:
    Channels::ptr channelsOf(const std::string& svc);

    std::mutex mtx_;
    std::unordered_map<std::string, Channels::ptr> maps_;
};
cpp 复制代码
void SvcChannels::setWatch(const std::string& svc) {
    std::unique_lock<std::mutex> lock(mtx_);
    maps_.emplace(svc, std::make_shared<Channels>());
}

void SvcChannels::addNode(const std::string& svc, const std::string& addr) {
    auto channels = channelsOf(svc);          // 内部只锁很短一段
    if (!channels) return;                    // 没声明过的服务,直接忽略
    channels->insert(addr);
}

ChannelPtr SvcChannels::getNode(const std::string& svc) {
    auto channels = channelsOf(svc);
    return channels ? channels->select() : ChannelPtr();
}

这里有个锁的细节 :channelsOf 只保护「查 map」这一小段,拿到 shared_ptr 后立刻释放锁,后面的 insert/select 由 Channels 自己的锁保护。两层锁各管各的,避免大锁套小锁。

为什么用「预声明」而不是「第一次 addNode 时自动建」?预声明能把配置错误提前暴露:拼错服务名时 addNode 直接忽略并打日志,而不是安静地建出一个空的 Channels、等到调用时才报「没有可用节点」。如果服务列表来自配置中心,启动时逐个 setWatch 一遍即可;懒加载(用到再建)也成立,但要把「建对象」和「加节点」都放进同一个双检锁,复杂度更高,收益不大。

5. ClosureFactory:把 lambda 变成 brpc Closure

brpc 只认 google::protobuf::Closure*,而它的 NewCallback 不接受仿函数(lambda),所以包一层:把 lambda 塞进 shared_ptr 对象,再用静态函数做回调。

cpp 复制代码
class ClosureFactory {
public:
    using callback_t = std::function<void()>;

    static google::protobuf::Closure* create(callback_t cb) {
        auto obj = std::make_shared<Object>();
        obj->callback = std::move(cb);
        return brpc::NewCallback(&ClosureFactory::run, obj);
    }

private:
    struct Object {
        callback_t callback;
    };

    static void run(std::shared_ptr<Object> obj) {
        if (obj && obj->callback) obj->callback();
    }
};

两个必须知道的约束:

约束 说明
NewCallback 参数不能传引用 框架按值保存参数,传引用会悬垂;这里传的是 shared_ptr 副本,安全
闭包 Run() 后自删 brpc 的 Closure 在执行后自行 delete,不要手动 delete 回调对象
捕获的堆对象谁释放 由 lambda 内部 unique_ptr 接管(见第 1 节),外层不要提前 delete

为什么把 lambda 装进 shared_ptr 而不是裸指针?NewCallback 会把参数按值保存一份,若传裸指针,谁负责 delete 就说不清了;shared_ptr 的引用计数保证闭包执行完、最后一个持有者析构时才释放,配合 brpc「Run 后自删」刚好闭环。

6. RpcServerFactory 与使用样例

起服务原本要 AddService、ServerOptions、Start 三步,工厂一行搞定;服务对象由服务器接管(SERVER_OWNS_SERVICE)。

cpp 复制代码
class RpcServerFactory {
public:
    static std::shared_ptr<brpc::Server> create(int port,
                                                google::protobuf::Service* svc) {
        auto server = std::make_shared<brpc::Server>();
        if (server->AddService(svc, brpc::SERVER_OWNS_SERVICE) != 0) {
            std::cerr << "AddService failed" << std::endl;
            abort();
        }
        brpc::ServerOptions options;
        options.idle_timeout_sec = -1;
        if (server->Start(port, &options) != 0) {
            std::cerr << "Start failed" << std::endl;
            abort();
        }
        return server;
    }
};

服务端业务代码只剩两行:

cpp 复制代码
int main() {
    auto* svc = new CalServiceImpl;                       // 堆上创建,交给服务器管
    auto server = rpcwrap::RpcServerFactory::create(9000, svc);
    server->RunUntilAskedToQuit();
    return 0;
}

对应的 cal.proto(一个做加法,一个留作 HTTP 回显):

proto 复制代码
syntax = "proto3";

package cal;

option cc_generic_services = true;

message AddReq {
  int32 num1 = 1;
  int32 num2 = 2;
}

message AddRsp {
  int32 result = 1;
}

message HelloReq {}
message HelloRsp {}

service CalService {
  rpc Add(AddReq) returns (AddRsp);
  rpc Hello(HelloReq) returns (HelloRsp);
}

服务起来后,客户端跑一次:

text 复制代码
添加 cal 服务节点 127.0.0.1:9000 管理
异步请求已发出
22 + 333 = 355

✅ 这套封装适合谁

服务数量多、节点会动态上下线的项目最划算:节点变更只动 SvcChannels,业务代码不用改一行;只有一两个固定节点的工具,直接用原生 brpc 更省事。

7. 踩坑与总结

坑集中在三处:回调对象的生命周期、两层锁、节点初始化失败。

坑 现象 修法
手动 delete 回调对象 崩溃/双重释放 brpc 闭包 Run 后自删,只释放 lambda 捕获的 req/rsp/cntl
NewCallback 传引用参数 回调里拿到悬垂引用 传值或 shared_ptr,别传 &obj
节点 Init 失败仍入池 调用随机失败 insert 里判断 Init 返回值,失败不加入
两层锁顺序不一致 偶发死锁 SvcChannels 锁只管查 map,Channels 锁管节点池,永不在持锁时调用对方
SERVER_OWNS_SERVICE 配栈对象 二次释放 服务对象必须 heap 分配,生命周期交给 Server
服务名拼错 节点加了但调不通 setWatch 预声明,addNode 对未声明的服务直接忽略并打日志
超时写死 30s 故障节点拖慢整体 按接口定 timeout_ms,配合重试与熔断

这套封装不追求「什么都能干」:它把易错的样板收走,把协议和 Stub 留给 brpc 原生。这样出问题时,日志、/status、抓包依然按 brpc 的方式看,排查路径不会被封装层挡住。
📝 小结

① 四层各管一件事:节点池、服务映射、回调包装、服务器创建。

② 封装只包样板,不包协议:proto 和 Stub 还是原生的,出问题照样能查。

③ 最值钱的是选路可替换:改一个 `select()` 就能换负载均衡策略。
📦 项目源码

本文示例所在的 C++ 微服务脚手架(点播系统):https://github.com/syagain-zsx/cpp-microservice-scaffold-vod-system
🔜 下一篇

《CMake 工程化》------把前面这些轮子收进一个可维护的构建体系。关注专栏不迷路。
🔗 参考

1 brpc 官方文档:https://brpc.apache.org/

2 brpc GitHub:https://github.com/apache/brpc

3 上一篇《BRpc 搭建简单 rpc 服务器》

相关推荐
其实防守也摸鱼1 小时前
Cylab在线靶场——CTF中的通用技能
linux·数据库·学习·算法·靶场·ctf·学习网络安全
我命由我123451 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
mister_guo1 小时前
HTTP 和 RPC:从一次干饭说清它们的区别
网络协议·http·rpc
fpcc2 小时前
计算机原理—如何高效的操作文件
c++·计算机原理
kkkkkkkkkk_Z2 小时前
新手自学嵌入式 | 学习日记:Linux驱动复习之系统移植(U-Boot篇)
linux·运维·学习
Rabitebla2 小时前
【系统性学习笔记】Linux 权限篇(二):文件属性、权限修改与目录权限的底层逻辑
linux·c++·笔记·学习·算法
system_error_x2 小时前
C++ 编程基础入门教程
开发语言·c++
fanged4 小时前
嵌入式架构学习6--ZSWatch(TODO)
学习
叼烟扛炮4 小时前
Linux 学习笔记(十三):高效排错利器——GDB 调试器入门
linux·笔记·学习