
📚 系列:C++ 工程实践 第 09 篇 ⏱ 阅读约 12 分钟
📌 一句话总结
本文从一份 .proto 写起,跑通 brpc 的完整闭环:① 定义 EchoService 并起服务端;② 同步客户端一个 stub.Echo 拿结果;③ 异步客户端用 done 回调收结果;④ 同一个端口再用 HTTP 访问一次。
文章目录
-
- [1. BRpc 是什么:一个端口、三种调用](#1. BRpc 是什么:一个端口、三种调用)
- [2. 先定 proto:两个 message 一个 service,生成 Stub 和基类](#2. 先定 proto:两个 message 一个 service,生成 Stub 和基类)
- [3. 实现 Service:done 是唯一需要你负责的参数](#3. 实现 Service:done 是唯一需要你负责的参数)
- [4. 起服务端:AddService、Start、RunUntilAskedToQuit](#4. 起服务端:AddService、Start、RunUntilAskedToQuit)
- [5. 同步客户端:第 4 个参数传 NULL](#5. 同步客户端:第 4 个参数传 NULL)
- [6. 异步客户端:done 一响,回调里收尾](#6. 异步客户端:done 一响,回调里收尾)
- [7. 一个端口两种协议:同一份 Echo,HTTP 也能调](#7. 一个端口两种协议:同一份 Echo,HTTP 也能调)
- [8. 构建与踩坑:Makefile 五条链接参数 + 八个高频坑](#8. 构建与踩坑:Makefile 五条链接参数 + 八个高频坑)
1. BRpc 是什么:一个端口、三种调用
BRpc 是 C++ 的工业级 RPC 框架,本篇只用它四个角色:Server、Service、Channel、Controller。 RPC 的目标很朴素------让调用远程服务像调用本地函数,序列化、网络、超时重试都由框架吞掉。
先认识这六个名词,后面代码里出现什么都不会慌:
| 角色 | 是什么 | 谁创建 |
|---|---|---|
.proto 里的 message |
请求/响应的数据结构 | 你写,protoc 生成 C++ 类 |
service |
一组 RPC 方法的声明 | 你写,protoc 生成基类和 Stub |
brpc::Server |
服务端容器 | 你在 main 里创建 |
Service 子类 |
你实现的业务逻辑 | 你写类,AddService 注册 |
brpc::Channel |
客户端到服务端的一条连接 | 客户端 Init |
brpc::Controller |
一次调用的上下文(超时、错误) | 每次调用创建 |
它最实用的三个特性:一个端口跑 RPC + HTTP 、同步/异步/半同步都支持 、自带 /status 调试页。本文只讲原生用法,二次封装放到下一篇。
先把环境装好(源码编译,首次大约 5~10 分钟):
bash
sudo apt-get install -y git g++ make libssl-dev libprotobuf-dev \
libprotoc-dev protobuf-compiler libleveldb-dev
git clone https://github.com/apache/brpc.git && cd brpc && mkdir build && cd build
cmake -DCMAKE_INSTALL_PREFIX=/usr .. && make -j6 && sudo make install
装完 protoc、libbrpc、libprotobuf 都可用,下面四个文件放在同一目录即可编译。
上手成本要有心理准备:brpc 不是 header-only,它依赖 protobuf、gflags、leveldb、openssl 这一串库,第一次源码编译要等几分钟;但装好之后,同一份代码既能当 RPC 服务,又能当 HTTP 服务,后面加服务只需要新增 .proto 和实现类,不用再动网络层。
2. 先定 proto:两个 message 一个 service,生成 Stub 和基类
option cc_generic_services = true; 是 brpc 能生成 Service/Stub 的开关,漏了它后面全是编译错误。 新建 main.proto:
proto
syntax = "proto3";
package example;
// 生成 C++ 服务基类,必须打开
option cc_generic_services = true;
message EchoRequest {
string message = 1;
}
message EchoResponse {
string message = 1;
}
// 定义一个 RPC 方法:Echo
service EchoService {
rpc Echo(EchoRequest) returns (EchoResponse);
}
目录里一共四个文件,make 会先生成 main.pb.* 再编译:
text
brpc-demo/
├── main.proto
├── brpc_server.cc
├── brpc_client.cc
├── brpc_client_async.cc
└── makefile
执行 protoc --cpp_out=. main.proto 后得到两类东西:
| 生成物 | 内容 | 谁用 |
|---|---|---|
main.pb.h / main.pb.cc |
消息类 + EchoService 基类 + EchoService_Stub |
服务端、客户端都要 |
EchoService(虚基类) |
纯虚方法 Echo(...),服务端继承它 |
服务端 |
EchoService_Stub |
带 Channel 的调用代理,客户端直接 .Echo(...) |
客户端 |
⚠️ 生成物不要提交、不要手改
main.pb.cc / main.pb.h 由 .proto 每次生成,入库存一份只会制造冲突;改接口永远改 .proto,让 Makefile 重新生成。
3. 实现 Service:done 是唯一需要你负责的参数
四个参数里 request 只读、response 你填、done 必须 Run 一次------ClosureGuard 让它自动 Run。
cpp
#include <brpc/server.h>
#include "main.pb.h"
namespace example {
class EchoServiceImpl : public EchoService {
public:
void Echo(google::protobuf::RpcController* cntl_base,
const EchoRequest* request,
EchoResponse* response,
google::protobuf::Closure* done) override {
// 离开作用域自动 done->Run(),成功失败都不会漏
brpc::ClosureGuard done_guard(done);
// response 由框架创建,这里只负责填充
response->set_message("echo: " + request->message());
}
};
} // namespace example
| 参数 | 谁负责 | 说明 |
|---|---|---|
cntl_base |
框架 | 需要读超时/错误时 static_cast<brpc::Controller*> |
request |
框架(只读) | 不要改它,也不要保存指针到回调外 |
response |
你 | 框架已创建,填字段即可;required 没填会失败 |
done |
你(Run 一次) | ClosureGuard 兜底,异常路径也不会漏 |
还有一个容易被忽略的协议细节:proto3 的标量字段默认没有「是否设置」的语义,int32=0 和「没赋值」在序列化后无法区分。如果你的接口需要区分这两种情况,用 optional(protobuf 3.15+)或 wrapper 类型,别用 -1 这类魔法值兜底。
4. 起服务端:AddService、Start、RunUntilAskedToQuit
三行起服务:AddService 注册、Start 监听、RunUntilAskedToQuit 阻塞;SERVER_DOESNT_OWN_SERVICE 表示 service 对象生命周期你自己管。
cpp
#include <gflags/gflags.h>
#include <brpc/server.h>
#include <iostream>
#include "main.pb.h"
DEFINE_int32(listen_port, 8000, "监听端口");
DEFINE_int32(thread_count, 3, "工作线程数");
int main(int argc, char* argv[]) {
google::ParseCommandLineFlags(&argc, &argv, true);
brpc::Server server;
example::EchoServiceImpl svc;
if (server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE) != 0) {
std::cerr << "AddService failed" << std::endl;
return -1;
}
brpc::ServerOptions options;
options.num_threads = FLAGS_thread_count;
if (server.Start(FLAGS_listen_port, &options) != 0) {
std::cerr << "Start failed" << std::endl;
return -1;
}
server.RunUntilAskedToQuit(); // 阻塞到 Ctrl+C
return 0;
}
| ServerOptions / 调用 | 说明 |
|---|---|
num_threads |
默认等于 CPU 核数,压测时按业务调 |
idle_timeout_sec |
空闲连接多久断开,-1 表示不断 |
SERVER_DOESNT_OWN_SERVICE |
服务对象在栈上,服务器不要帮忙 delete;反之会二次释放 |
启动成功的日志大致长这样(前两行是 brpc 自己的日志):
text
I... brpc/server.cpp] Server[EchoServiceImpl] is serving on port=8000.
I... brpc/server.cpp] Check out http://127.0.0.1:8000 in web browser.
5. 同步客户端:第 4 个参数传 NULL
stub.Echo(&cntl, &req, &rsp, nullptr) 的 done 为 nullptr 就是同步:阻塞到响应或错误。
cpp
#include <brpc/channel.h>
#include <iostream>
#include "main.pb.h"
int main() {
brpc::Channel channel;
brpc::ChannelOptions options;
options.protocol = "baidu_std"; // 默认协议
options.timeout_ms = 500;
options.max_retry = 3;
if (channel.Init("127.0.0.1:8000", &options) != 0) {
std::cerr << "channel init failed" << std::endl;
return -1;
}
example::EchoService_Stub stub(&channel);
example::EchoRequest req;
example::EchoResponse rsp;
brpc::Controller cntl;
req.set_message("hello brpc");
// done = nullptr:同步阻塞
stub.Echo(&cntl, &req, &rsp, nullptr);
if (cntl.Failed()) {
std::cerr << "rpc failed: " << cntl.ErrorText() << std::endl;
return -1;
}
std::cout << rsp.message() << std::endl;
return 0;
}
运行后输出:
text
echo: hello brpc
⚠️ 超时别用默认值兜底
timeout_ms 默认 500ms,压测或跨机房很容易误判失败;按接口的 P99 再加余量,别一个值走天下。
6. 异步客户端:done 一响,回调里收尾
把 done 换成 NewCallback,stub.Echo 立刻返回;response/controller 必须 new 在堆上,在回调里释放。
cpp
#include <brpc/channel.h>
#include <atomic>
#include <chrono>
#include <iostream>
#include <memory>
#include <thread>
#include "main.pb.h"
static std::atomic<bool> g_done{false};
static void OnEchoDone(example::EchoResponse* rsp, brpc::Controller* cntl) {
// 回调结束时自动释放,避免泄漏
std::unique_ptr<example::EchoResponse> rsp_guard(rsp);
std::unique_ptr<brpc::Controller> cntl_guard(cntl);
if (cntl->Failed()) {
std::cerr << "rpc failed: " << cntl->ErrorText() << std::endl;
return;
}
std::cout << rsp->message() << std::endl;
g_done = true; // 通知主线程:回调结束
}
int main() {
brpc::Channel channel;
brpc::ChannelOptions options;
options.timeout_ms = 500;
if (channel.Init("127.0.0.1:8000", &options) != 0) {
std::cerr << "channel init failed" << std::endl;
return -1;
}
example::EchoService_Stub stub(&channel);
example::EchoRequest req; // 请求可以栈上
req.set_message("hello async");
auto* rsp = new example::EchoResponse;
auto* cntl = new brpc::Controller;
// 回调由框架在响应到达时执行一次
stub.Echo(cntl, &req, rsp, brpc::NewCallback(OnEchoDone, rsp, cntl));
// 等回调真正结束再退出,避免固定 sleep 在慢网络下提前退出
while (!g_done) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
return 0;
}
服务端 客户端 服务端 客户端 #mermaid-svg-vEFPl6QehjKh0ogW{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-vEFPl6QehjKh0ogW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vEFPl6QehjKh0ogW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vEFPl6QehjKh0ogW .error-icon{fill:#552222;}#mermaid-svg-vEFPl6QehjKh0ogW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vEFPl6QehjKh0ogW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vEFPl6QehjKh0ogW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vEFPl6QehjKh0ogW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vEFPl6QehjKh0ogW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vEFPl6QehjKh0ogW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vEFPl6QehjKh0ogW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vEFPl6QehjKh0ogW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vEFPl6QehjKh0ogW .marker.cross{stroke:#333333;}#mermaid-svg-vEFPl6QehjKh0ogW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vEFPl6QehjKh0ogW p{margin:0;}#mermaid-svg-vEFPl6QehjKh0ogW .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vEFPl6QehjKh0ogW text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-vEFPl6QehjKh0ogW .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vEFPl6QehjKh0ogW .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-vEFPl6QehjKh0ogW .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-vEFPl6QehjKh0ogW .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-vEFPl6QehjKh0ogW #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-vEFPl6QehjKh0ogW .sequenceNumber{fill:white;}#mermaid-svg-vEFPl6QehjKh0ogW #sequencenumber{fill:#333;}#mermaid-svg-vEFPl6QehjKh0ogW #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-vEFPl6QehjKh0ogW .messageText{fill:#333;stroke:none;}#mermaid-svg-vEFPl6QehjKh0ogW .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vEFPl6QehjKh0ogW .labelText,#mermaid-svg-vEFPl6QehjKh0ogW .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-vEFPl6QehjKh0ogW .loopText,#mermaid-svg-vEFPl6QehjKh0ogW .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-vEFPl6QehjKh0ogW .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vEFPl6QehjKh0ogW .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-vEFPl6QehjKh0ogW .noteText,#mermaid-svg-vEFPl6QehjKh0ogW .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-vEFPl6QehjKh0ogW .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vEFPl6QehjKh0ogW .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vEFPl6QehjKh0ogW .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vEFPl6QehjKh0ogW .actorPopupMenu{position:absolute;}#mermaid-svg-vEFPl6QehjKh0ogW .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-vEFPl6QehjKh0ogW .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vEFPl6QehjKh0ogW .actor-man circle,#mermaid-svg-vEFPl6QehjKh0ogW line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-vEFPl6QehjKh0ogW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 阻塞等待 直到拿到结果 stub.Echo 同步请求 响应
服务端 客户端 服务端 客户端 #mermaid-svg-jSHn9oUomscA2FAM{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-jSHn9oUomscA2FAM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jSHn9oUomscA2FAM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jSHn9oUomscA2FAM .error-icon{fill:#552222;}#mermaid-svg-jSHn9oUomscA2FAM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jSHn9oUomscA2FAM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jSHn9oUomscA2FAM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jSHn9oUomscA2FAM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jSHn9oUomscA2FAM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jSHn9oUomscA2FAM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jSHn9oUomscA2FAM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jSHn9oUomscA2FAM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jSHn9oUomscA2FAM .marker.cross{stroke:#333333;}#mermaid-svg-jSHn9oUomscA2FAM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jSHn9oUomscA2FAM p{margin:0;}#mermaid-svg-jSHn9oUomscA2FAM .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jSHn9oUomscA2FAM text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-jSHn9oUomscA2FAM .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jSHn9oUomscA2FAM .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-jSHn9oUomscA2FAM .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-jSHn9oUomscA2FAM .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-jSHn9oUomscA2FAM #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-jSHn9oUomscA2FAM .sequenceNumber{fill:white;}#mermaid-svg-jSHn9oUomscA2FAM #sequencenumber{fill:#333;}#mermaid-svg-jSHn9oUomscA2FAM #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-jSHn9oUomscA2FAM .messageText{fill:#333;stroke:none;}#mermaid-svg-jSHn9oUomscA2FAM .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jSHn9oUomscA2FAM .labelText,#mermaid-svg-jSHn9oUomscA2FAM .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-jSHn9oUomscA2FAM .loopText,#mermaid-svg-jSHn9oUomscA2FAM .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-jSHn9oUomscA2FAM .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jSHn9oUomscA2FAM .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-jSHn9oUomscA2FAM .noteText,#mermaid-svg-jSHn9oUomscA2FAM .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-jSHn9oUomscA2FAM .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jSHn9oUomscA2FAM .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jSHn9oUomscA2FAM .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jSHn9oUomscA2FAM .actorPopupMenu{position:absolute;}#mermaid-svg-jSHn9oUomscA2FAM .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-jSHn9oUomscA2FAM .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jSHn9oUomscA2FAM .actor-man circle,#mermaid-svg-jSHn9oUomscA2FAM line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-jSHn9oUomscA2FAM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 立即返回 主线程继续 触发 done 回调 收尾 stub.Echo 带 done 回调 响应到达
| 对象 | 放哪 | 谁释放 |
|---|---|---|
EchoRequest |
栈或堆都行 | 调用返回后框架不再用它 |
EchoResponse |
堆 | 回调里 unique_ptr 释放 |
brpc::Controller |
堆 | 回调里 unique_ptr 释放 |
done(Closure) |
框架创建 | Run() 后自己 delete,不用管 |
为什么请求可以放栈上、响应必须放堆上?因为请求在 stub.Echo 返回时就已经被框架序列化发走,之后不再访问;而响应要等网络回来才被写入,回调执行完才安全释放。把这条记成一句话:调用返回后仍会被框架访问的对象,必须活到回调结束。
7. 一个端口两种协议:同一份 Echo,HTTP 也能调
brpc 的 Server 天然双协议:RPC 走默认协议,HTTP 走 POST /服务全名/方法名,调的是同一个方法。 服务起来后,另开一个终端:
bash
curl -X POST http://127.0.0.1:8000/example.EchoService/Echo \
-H 'Content-Type: application/json' \
-d '{"message":"from http"}'
text
{"message":"echo: from http"}
| 调用方式 | 走什么 | 适合谁 |
|---|---|---|
stub.Echo(...) |
brpc 默认协议 | 内部 C++ 服务之间 |
curl / 浏览器 |
HTTP + JSON | 脚本、调试、其他语言 |
/status 页面 |
HTTP 内置页 | 排查连接数、QPS、延迟分位 |
#mermaid-svg-cJAtwRdEQFXgIwHM{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-cJAtwRdEQFXgIwHM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cJAtwRdEQFXgIwHM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cJAtwRdEQFXgIwHM .error-icon{fill:#552222;}#mermaid-svg-cJAtwRdEQFXgIwHM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cJAtwRdEQFXgIwHM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cJAtwRdEQFXgIwHM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cJAtwRdEQFXgIwHM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cJAtwRdEQFXgIwHM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cJAtwRdEQFXgIwHM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cJAtwRdEQFXgIwHM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cJAtwRdEQFXgIwHM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cJAtwRdEQFXgIwHM .marker.cross{stroke:#333333;}#mermaid-svg-cJAtwRdEQFXgIwHM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cJAtwRdEQFXgIwHM p{margin:0;}#mermaid-svg-cJAtwRdEQFXgIwHM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cJAtwRdEQFXgIwHM .cluster-label text{fill:#333;}#mermaid-svg-cJAtwRdEQFXgIwHM .cluster-label span{color:#333;}#mermaid-svg-cJAtwRdEQFXgIwHM .cluster-label span p{background-color:transparent;}#mermaid-svg-cJAtwRdEQFXgIwHM .label text,#mermaid-svg-cJAtwRdEQFXgIwHM span{fill:#333;color:#333;}#mermaid-svg-cJAtwRdEQFXgIwHM .node rect,#mermaid-svg-cJAtwRdEQFXgIwHM .node circle,#mermaid-svg-cJAtwRdEQFXgIwHM .node ellipse,#mermaid-svg-cJAtwRdEQFXgIwHM .node polygon,#mermaid-svg-cJAtwRdEQFXgIwHM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cJAtwRdEQFXgIwHM .rough-node .label text,#mermaid-svg-cJAtwRdEQFXgIwHM .node .label text,#mermaid-svg-cJAtwRdEQFXgIwHM .image-shape .label,#mermaid-svg-cJAtwRdEQFXgIwHM .icon-shape .label{text-anchor:middle;}#mermaid-svg-cJAtwRdEQFXgIwHM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cJAtwRdEQFXgIwHM .rough-node .label,#mermaid-svg-cJAtwRdEQFXgIwHM .node .label,#mermaid-svg-cJAtwRdEQFXgIwHM .image-shape .label,#mermaid-svg-cJAtwRdEQFXgIwHM .icon-shape .label{text-align:center;}#mermaid-svg-cJAtwRdEQFXgIwHM .node.clickable{cursor:pointer;}#mermaid-svg-cJAtwRdEQFXgIwHM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cJAtwRdEQFXgIwHM .arrowheadPath{fill:#333333;}#mermaid-svg-cJAtwRdEQFXgIwHM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cJAtwRdEQFXgIwHM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cJAtwRdEQFXgIwHM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cJAtwRdEQFXgIwHM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cJAtwRdEQFXgIwHM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cJAtwRdEQFXgIwHM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cJAtwRdEQFXgIwHM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cJAtwRdEQFXgIwHM .cluster text{fill:#333;}#mermaid-svg-cJAtwRdEQFXgIwHM .cluster span{color:#333;}#mermaid-svg-cJAtwRdEQFXgIwHM 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-cJAtwRdEQFXgIwHM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cJAtwRdEQFXgIwHM rect.text{fill:none;stroke-width:0;}#mermaid-svg-cJAtwRdEQFXgIwHM .icon-shape,#mermaid-svg-cJAtwRdEQFXgIwHM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cJAtwRdEQFXgIwHM .icon-shape p,#mermaid-svg-cJAtwRdEQFXgIwHM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cJAtwRdEQFXgIwHM .icon-shape .label rect,#mermaid-svg-cJAtwRdEQFXgIwHM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cJAtwRdEQFXgIwHM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cJAtwRdEQFXgIwHM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cJAtwRdEQFXgIwHM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} RPC 客户端 stub
8000 端口
curl 或浏览器
EchoService.Echo
路径写成 /example.EchoService/Echo(全名)或 /EchoService/Echo(短名)都能命中------brpc 按首段是否带 . 来判断用哪种查找方式。
这就是「一个端口多协议」最直观的收益:同一份业务逻辑,不用为 HTTP 再写一遍 Controller。
对调试也友好:联调时不用先写一个客户端,curl 一条命令就能验证参数和返回;线上排查时打开 /status,能看到每个方法的 QPS、延迟分位和错误码分布,比在业务代码里到处埋点更省事。
8. 构建与踩坑:Makefile 五条链接参数 + 八个高频坑
链接参数固定 -lbrpc -lleveldb -lprotobuf -lgflags -lssl -lcrypto;坑集中在 proto 开关、端口、done 生命周期、超时。
makefile
CXX := g++
CXXFLAGS := -std=c++17 -O2
LDLIBS := -lbrpc -lleveldb -lprotobuf -lgflags -lssl -lcrypto -lpthread
all: brpc_server brpc_client brpc_client_async
main.pb.cc main.pb.h: main.proto
protoc --cpp_out=. $<
brpc_server: brpc_server.cc main.pb.cc
$(CXX) $(CXXFLAGS) -o $@ $^ $(LDLIBS)
brpc_client: brpc_client.cc main.pb.cc
$(CXX) $(CXXFLAGS) -o $@ $^ $(LDLIBS)
brpc_client_async: brpc_client_async.cc main.pb.cc
$(CXX) $(CXXFLAGS) -o $@ $^ $(LDLIBS)
clean:
rm -f brpc_server brpc_client brpc_client_async main.pb.cc main.pb.h
.PHONY: all clean
| 坑 | 现象 | 修法 |
|---|---|---|
忘了 cc_generic_services |
没有 EchoService 类 |
打开该选项后重新 protoc |
| 少链接 brpc 依赖 | undefined reference to brpc::... |
补 -lbrpc -lleveldb -lprotobuf -lgflags -lssl -lcrypto |
| 端口被占 | Start failed |
换端口,或 ss -lntp 查占用 |
| 异步用栈上 response | 偶发崩溃 | response/controller 必须堆上且活到回调 |
| 回调忘了释放 | 内存缓慢上涨 | unique_ptr 守卫 / ClosureGuard |
| 同步调用卡住 | 服务端没起、地址/端口不对 | 先跑 ./brpc_server,核对 channel.Init 的地址与端口 |
| brpc 与 protobuf 版本不匹配 | 编译/链接诡异报错 | 用配套版本,或按 brpc README 安装 |
| 生成物提交进仓库 | 每次编译冲突 | .gitignore 掉 *.pb.cc / *.pb.h |
如果只记一句话:proto 是契约,框架是管道,业务只负责填 response。契约稳定了,后面换协议、加节点、做灰度,都是管道层的事。
📝 小结
① 接口先行:`.proto` 定契约,`cc_generic_services` 生成 Stub。
② 同步/异步只差最后一个参数:`nullptr` 阻塞,`NewCallback` 回调。
③ 一个端口两种协议:RPC 给服务调,HTTP 给脚本和浏览器调。
📦 项目源码
本文示例所在的 C++ 微服务脚手架(点播系统):https://github.com/syagain-zsx/cpp-microservice-scaffold-vod-system
🔜 下一篇
《BRpc 二次封装:Channel 管理、服务管理与回调工厂》------业务代码里不该到处 new Channel。关注专栏不迷路。
🔗 参考
1 brpc 官方文档:https://brpc.apache.org/
2 brpc GitHub:https://github.com/apache/brpc
3 Protocol Buffers:https://protobuf.dev/