brpc HTTP/h2 客户端编程指南:从 Channel 创建到持续下载的完整实战
2026/9/13 17:03:21 网站建设 项目流程

brpc HTTP/h2 客户端编程指南:从 Channel 创建到持续下载的完整实战

【免费下载链接】brpcbrpc is an Industrial-grade RPC framework using C++ Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Recommendation etc. "brpc" means "better RPC".项目地址: https://gitcode.com/GitHub_Trending/brpc/brpc

导读

本文基于 brpc 官方文档 docs/cn/http_client.md 展开,系统讲解如何用brpc::Channel以 HTTP/1.1 与 HTTP/2(brpc 统称 h2)协议访问远程服务,覆盖 Channel 初始化、GET/POST 请求构造、URL 与 Host 语义、header/query 操作、错误处理、gzip 压缩解压、持续下载(ProgressiveReader)以及 HTTPS 认证等全部实战要点。阅读完本文,你将能够独立编写一个可用的 brpc HTTP 客户端,并理解其底层协议实现(src/brpc/policy/http_rpc_protocol.cpp)与官方示例(example/http_c++/http_client.cpp)的对应关系。


一、关于 h2:brpc 对 HTTP/2 的统一封装

brpc 把 HTTP/2 协议统称为h2,不论是否加密。唯一可见的差异体现在/connections内建服务中:未开启 SSL 的 HTTP/2 连接会按官方名称以h2c显示,开启 SSL 的则以h2显示。

对使用者而言,brpc 中 http 和 h2 的编程接口基本没有区别。除非文档特别说明,所有提到的 http 特性(header 操作、query 操作、错误处理、压缩等)都同时对 h2 有效。这也意味着你可以用同一套代码访问 http/1.1 与 h2 服务,仅需在创建 Channel 时指定不同的协议即可。

从源码看,两种协议分别由 http_rpc_protocol.cpp 和 http2_rpc_protocol.cpp 两个协议处理器实现,而它们对外暴露的Controller::http_request()/http_response()接口是一致的,这正是"编程接口无差别"的底层原因。


二、创建 Channel:初始化 http/h2 客户端

brpc::Channel可访问 http/h2 服务,唯一的要求是在ChannelOptions.protocol中指定PROTOCOL_HTTPPROTOCOL_H2

brpc::ChannelOptions options; options.protocol = brpc::PROTOCOL_HTTP; // or brpc::PROTOCOL_H2 if (channel.Init("www.baidu.com" /*any url*/, &options) != 0) { LOG(ERROR) << "Fail to initialize channel"; return -1; }

这里有一个值得注意的设计:协议设定好后,Channel::Init的第一个参数可以是任意合法的 URL。允许任意 URL 是为了省去用户手动取出 host 和 port 的麻烦——Channel::Init只使用其中的 host 及 port,其他部分(path、query、fragment)都会被丢弃

http/h2 channel 同样支持 BNS 地址或其他 NamingService(命名服务),也就是说你可以用channel.Init("bns://your.service.name", &options)这类形式连接服务集群,负载均衡策略与 docs/cn/load_balancing.md 中描述的一致。


三、发起 GET 请求

设置好 Channel 后,一次 GET 请求只需要两步:给cntl.http_request().uri()赋值待访问的 URL,然后调用CallMethod

brpc::Controller cntl; cntl.http_request().uri() = "www.baidu.com/index.html"; // 设置为待访问的URL channel.CallMethod(nullptr, &cntl, nullptr, nullptr, nullptr/*done*/);

HTTP/h2 和 protobuf 关系不大,因此除Controllerdone外,CallMethod的其他参数均为nullptr;若要异步操作,把最后一个参数传入done回调即可。

响应体通过cntl.response_attachment()获取,类型为butil::IOBufIOBuf可通过to_string()转化为std::string,但需要分配内存并拷贝所有内容——如果关注性能,处理过程应直接支持IOBuf的分片读取,而不要求连续内存。IOBuf的详细设计见 docs/cn/iobuf.md。


四、发起 POST 请求

brpc 默认的 HTTP Method 为 GET,可设置为 POST 或其他 method。完整的 method 枚举定义在 src/brpc/http_method.h,包括HTTP_METHOD_DELETEHTTP_METHOD_HEADHTTP_METHOD_PUTHTTP_METHOD_PATCHHTTP_METHOD_OPTIONS等共 27 种标准及扩展方法(如 WebDAV 的PROPFINDLOCKMOVE)。

待 POST 的数据应置入request_attachment()butil::IOBuf可以直接appendstd::stringchar*

brpc::Controller cntl; cntl.http_request().uri() = "..."; // 设置为待访问的URL cntl.http_request().set_method(brpc::HTTP_METHOD_POST); cntl.request_attachment().append("{\"message\":\"hello world!\"}"); channel.CallMethod(nullptr, &cntl, nullptr, nullptr, nullptr/*done*/);

需要大量打印过程性 body 时,建议使用butil::IOBufBuilder,它的用法和std::ostringstream完全一致:

brpc::Controller cntl; cntl.http_request().uri() = "..."; // 设置为待访问的URL cntl.http_request().set_method(brpc::HTTP_METHOD_POST); butil::IOBufBuilder os; os << "A lot of printing" << printable_objects << ...; os.move_to(cntl.request_attachment()); channel.CallMethod(nullptr, &cntl, nullptr, nullptr, nullptr/*done*/);

对于有大量对象要打印的场景,IOBufBuilder既简化了代码,效率也可能比 c-styleprintf更高——它避免了中间字符串的多次拷贝。

实例参考:官方示例 example/http_c++/http_client.cpp 正是这样实现的:命令行-d '{"message":"hello"}'传入数据后,设置HTTP_METHOD_POST并 append 到request_attachment(),随后调用CallMethod访问http://www.foo.com:8765/EchoService/Echo


五、控制 HTTP 版本

brpc 的 http 行为默认是 http/1.1

http/1.0 相比 http/1.1 缺少长连接(keep-alive)功能,当 brpc client 与一些古老的 http server 通信时,可能需要显式将版本设置为 1.0:

cntl.http_request().set_version(1, 0);

需要注意两点:

  • 设置 http 版本对 h2 无效,但 client 收到的 h2 response 和 server 收到的 h2 request 中,version会被框架自动设置为(2, 0)
  • brpc server 会自动识别HTTP 版本并相应回复,无需用户设置。

六、URL 的结构与语义

理解 URL 结构是正确使用 brpc http client 的前提,其一般形式如下:

// URI scheme : http://en.wikipedia.org/wiki/URI_scheme // // foo://username:password@example.com:8042/over/there/index.dtb?type=animal&name=narwhal#nose // \_/ \_______________/ \_________/ \__/ \___/ \_/ \______________________/ \__/ // | | | | | | | | // | userinfo host port | | query fragment // | \________________________________/\_____________|____|/ \__/ \__/ // scheme | | | | | | // authority | | | | | // path | | interpretable as keys // | | // \_______________________________________________|____|/ \____/ \_____/ // | | | | | // hierarchical part | | interpretable as values // | | // interpretable as filename | // | // | // interpretable as extension

6.1 为什么Init的 URL 和uri()需要各设置一次?

细心的读者会发现,上面例子中Channel.Init()cntl.http_request().uri()被设置了相同的 URL。为什么 Channel 不直接利用 Init 时传入的 URL,而需要给uri()再设置一次?

确实,在简单使用场景下这两者有所重复;但在复杂场景中,两者差别很大,例如:

  • 访问命名服务(如 BNS)下的多个 http/h2 server:此时Channel.Init传入的是对该命名服务有意义的名称(如 BNS 中的节点名称),而对uri()的赋值则是包含 Host 的完整 URL(比如"www.foo.com/index.html?name=value");
  • 通过 http/h2 proxy 访问目标 server:此时Channel.Init传入的是 proxy server 的地址,但uri()填入的是目标 server 的 URL。

换言之,Channel.Init决定"连到哪台机器",uri()决定"请求哪个资源",两者解耦后上述高级场景才成为可能。

6.2 Host 字段的推导规则

Host 字段(h2 中对应:authority)的填充遵循以下优先级规则:

  1. 用户显式设置了 "host" 字段(大小写不敏感):框架不会修改,原样使用;
  2. 用户未设置,且 URL 中包含 host,如http://www.foo.com/path:http request 中会包含Host: www.foo.com
  3. 用户未设置,URL 不包含 host(如/index.html?name=value),但 Channel 初始化的地址 scheme 为 http(s) 且包含域名:框架以该域名作为 Host。例如地址为http://www.foo.com,server 将看到Host: www.foo.com;地址为http://www.foo.com:8989,则看到Host: www.foo.com:8989
  4. 用户未设置,URL 不包含 host,且 Channel 初始化地址也不包含域名:框架以目标 server 的 ip 和 port 为 Host。例如地址为10.46.188.39:8989的 http server 将看到Host: 10.46.188.39:8989

这一规则确保了无论直接连 IP 还是连域名,brpc 都能生成一个合法的 Host 头,同时保留了用户显式覆盖的能力。


七、常见设置:header、query、method、body 的完整操作

以下操作以 http request 为例(对 response 的操作自行替换http_request()http_response()即可),覆盖了日常使用中最常见的全部操作方式:

访问名为 Foo 的 header

const std::string* value = cntl->http_request().GetHeader("Foo"); //不存在为nullptr

设置名为 Foo 的 header

cntl->http_request().SetHeader("Foo", "value");

访问名为 Foo 的 query

const std::string* value = cntl->http_request().uri().GetQuery("Foo"); // 不存在为nullptr

设置名为 Foo 的 query

cntl->http_request().uri().SetQuery("Foo", "value");

设置 HTTP Method

cntl->http_request().set_method(brpc::HTTP_METHOD_POST);

设置 url

cntl->http_request().uri() = "http://www.baidu.com";

设置 content-type

cntl->http_request().set_content_type("text/plain");

访问 body

butil::IOBuf& buf = cntl->request_attachment(); std::string str = cntl->request_attachment().to_string(); // 有拷贝

设置 body

cntl->request_attachment().append("...."); butil::IOBufBuilder os; os << "...."; os.move_to(cntl->request_attachment());

关于 header 与 query 的三点注意事项

  • 根据 RFC 2616,http header 的 field_name不区分大小写。brpc 支持大小写不敏感访问,同时会在打印时保持用户传入的大小写
  • 若 http header 中出现了相同的 field_name,根据 RFC 2616,多个 value 应合并到一起、用逗号(,)分隔,此合并行为需要用户自行处理;
  • query 之间用&分隔,key 和 value 之间用=分隔,value 可以省略。比如key1=value1&key2&key3=value3key2是合理的 query,其值为空字符串。

八、查看 HTTP 消息:-http_verbose 调试利器

打开 GFlag-http_verbose(对应源码 src/brpc/details/http_message.cpp 中的定义)即可在 stderr 看到所有的 http/h2 request 和 response

./http_client -http_verbose http://www.foo.com:8765/vars/rpc_server*

从源码看,-http_verbose还有配套的-http_verbose_max_body_length(默认 512 字节),用于控制 body 打印的最大长度,超长部分会被截断并以字节数提示(见 src/brpc/details/http_message.cpp)。开启后 brpc 会自动打印响应内容,示例 example/http_c++/http_client.cpp 中即以此判断是否还需要手动输出。

重要提醒-http_verbose只用于线下调试,绝不能用于线上程序——它会将全部明文流量打印到日志中,造成性能与安全双重问题。


九、HTTP 错误处理:EHTTP 与 -use_http_error_code

当 Server 返回的 http status code不是 2xx时,该次 http/h2 访问被视为失败:

  • client 端会把cntl->ErrorCode()设置为EHTTP
  • 用户可通过cntl->http_response().status_code()获得具体的 http 错误码(如 404、500);
  • server 端同时可以把代表错误的 html 或 json 置入cntl->response_attachment()作为 http body 传递回来,客户端可以读取该 body 获取更详细的错误信息。

特殊场景:如果 Server 也是 brpc 框架实现的服务,client 端希望在 http/h2 失败时获取 brpc Server 返回的真实ErrorCode,而不是统一设置的EHTTP,则需要设置 GFlag-use_http_error_code=true

从源码看,该 flag 定义于 src/brpc/policy/http_rpc_protocol.cpp,其作用是把 brpc 的 error code 写入 http response 的x-bd-error-codeheader 中(见 src/brpc/policy/http_rpc_protocol.cpp),client 侧据此还原真实错误码。


十、压缩 request body

调用Controller::set_request_compress_type(brpc::COMPRESS_TYPE_GZIP)尝试用 gzip 压缩 http body。

这里"尝试"的含义是:压缩有可能不发生。触发条件在 src/brpc/policy/http_rpc_protocol.cpp 中有明确定义:

DEFINE_int32(http_body_compress_threshold, 512, "Not compress http body when it's less than so many bytes.")

即 body 尺寸小于-http_body_compress_threshold指定的字节数(默认 512)时不压缩。原因在于 gzip 并不是一个很快的压缩算法,当 body 较小时,压缩增加的延时可能比网络传输省下的还多——阈值本质上是在"压缩耗时"与"传输耗时"之间做权衡。源码 src/brpc/policy/http_rpc_protocol.cpp 中正是用request_size >= FLAGS_http_body_compress_threshold来判断是否执行压缩。


十一、解压 response body

出于通用性考虑,brpc 不会自动解压 response body。不过解压代码并不复杂,用户可以自己做,标准做法如下:

#include <brpc/policy/gzip_compress.h> ... const std::string* encoding = cntl->http_response().GetHeader("Content-Encoding"); if (encoding != nullptr && *encoding == "gzip") { butil::IOBuf uncompressed; if (!brpc::policy::GzipDecompress(cntl->response_attachment(), &uncompressed)) { LOG(ERROR) << "Fail to un-gzip response body"; return; } cntl->response_attachment().swap(uncompressed); } // cntl->response_attachment()中已经是解压后的数据了

所用到的GzipDecompress声明在 src/brpc/policy/gzip_compress.h,其签名接受butil::IOBuf输入与输出,与上面用法一一对应。该头文件同时提供GzipCompressZlibCompressZlibDecompress等配套函数,可满足多种压缩算法的需求。


十二、持续下载:处理超长/无限长 body

12.1 问题背景

普通 http client 往往需要等待到 body 下载完整才结束 RPC,这个过程中 body 都会存在内存中。如果 body 超长或无限长(比如直播用的 flv 文件),内存会持续增长,直到超时——这样的 http client 不适合下载大文件。

brpc client 支持在读取完 body 前就结束 RPC,让用户在 RPC 结束后再读取持续增长的 body。

注意:这个功能不等同于"支持 http chunked mode"。brpc 的 http 实现一直支持解析 chunked mode,这里要解决的是"用户如何处理超长或无限长的 body",与 body 是否以 chunked mode 传输无关。

12.2 使用步骤

第 1 步:实现ProgressiveReader接口

接口定义在 src/brpc/progressive_reader.h:

#include <brpc/progressive_reader.h> ... class ProgressiveReader { public: // Called when one part was read. // Error returned is treated as *permanent* and the socket where the // data was read will be closed. // A temporary error may be handled by blocking this function, which // may block the HTTP parsing on the socket. virtual butil::Status OnReadOnePart(const void* data, size_t length) = 0; // Called when there's nothing to read anymore. The `status' is a hint for // why this method is called. // - status.ok(): the message is complete and successfully consumed. // - otherwise: socket was broken or OnReadOnePart() failed. // This method will be called once and only once. No other methods will // be called after. User can release the memory of this object inside. virtual void OnEndOfMessage(const butil::Status& status) = 0; };
  • OnReadOnePart在每读到一段数据时被调用;
  • OnEndOfMessage在数据结束或连接断开时调用,且只会被调用一次,之后不会再有其他方法被调用,用户可以在其内部释放这个对象的内存。

实现前务必仔细阅读注释:OnReadOnePart返回的错误被视为永久性错误,数据所在的 socket 会被关闭;临时性错误则可以通过阻塞该函数来处理(但这会阻塞该 socket 上的 HTTP 解析)。

第 2 步:发起 RPC 前设置cntl.response_will_be_read_progressively();

这告诉 brpc:读取 http response 时只要读完 header 部分,RPC 就可以结束了。对应的控制器实现见 src/brpc/controller.h。

第 3 步:RPC 结束后调用cntl.ReadProgressiveAttachmentBy(new MyProgressiveReader);

MyProgressiveReader就是你实现的ProgressiveReader实例。用户可以在这个实例的OnEndOfMessage接口中删除这个实例(delete this)。

12.3 官方示例验证

example/http_c++/http_client.cpp 中的PartDataReader是这一机制的完整示范:

class PartDataReader : public brpc::ProgressiveReader { public: explicit PartDataReader(bthread::CountdownEvent* done) : _done(done) {} butil::Status OnReadOnePart(const void* data, size_t length) override { const std::string part(static_cast<const char*>(data), length); LOG(INFO) << "data: " << part << " size: " << length; return butil::Status::OK(); } void OnEndOfMessage(const butil::Status& status) override { LOG(INFO) << "progressive read data final status : " << status; _done->signal(); delete this; } private: bthread::CountdownEvent* _done; };

使用时通过-progressive开关启用,并配合set_progressive_read_timeout_ms()设置读取空闲超时(示例中默认 5000ms,见 example/http_c++/http_client.cpp):

if (FLAGS_progressive) { cntl.set_progressive_read_timeout_ms(FLAGS_progressive_read_timeout_ms); cntl.response_will_be_read_progressively(); } ... if (FLAGS_progressive) { bthread::CountdownEvent done(1); cntl.ReadProgressiveAttachmentBy(new PartDataReader(&done)); done.wait(); }

底层的读取链路(协议处理器 →HttpMessage::SetBodyReaderProgressiveReader::OnReadOnePart)在 src/brpc/progressive_reader.h 的注释中有完整描述:已经读到的 body 会立即喂给 reader 并被记住,新到达的数据会持续回调OnReadOnePart,直至所有 body 读完或 socket 被销毁。


十三、持续上传的现状限制

目前POST 的数据必须是完整生成好的,brpc 不适合 POST 超长的 body。也就是说,持续上传(类似分块流式上传)目前并不被支持,如果需要上传超大 body,需要考虑分片多次请求或使用其他传输机制。


十四、访问带认证的 Server

根据 Server 的认证方式生成对应的auth_data,并设置为 http header"Authorization"的值:

cntl.http_request().SetHeader("Authorization", auth_data);

比如用 curl 时,对应的做法是加上选项-H "Authorization : <auth_data>"


十五、发送 HTTPS 请求

https 是 http over SSL 的简称。需要强调的是:SSL 并不是 http 特有的,而是对所有协议都有效。开启客户端 SSL 的一般性方法见 docs/cn/client.md,核心是通过ChannelOptions.mutable_ssl_options()配置:

// 开启客户端SSL并使用默认值。 options.mutable_ssl_options(); // 开启客户端SSL并定制选项。 options.mutable_ssl_options()->ciphers_name = "..."; options.mutable_ssl_options()->sni_name = "..."; // 设置 ALPN 的协议优先级(默认不启用 ALPN)。 options.mutable_ssl_options()->alpn_protocols = {"h2", "http/1.1"};

brpc 针对 HTTPS 做了易用性优化:https://开头的 uri 会自动开启 SSL,无需额外设置。开启后,该 Channel 上任何协议的请求都会被 SSL 加密发送;如果希望某些请求不加密,需要额外再创建一个 Channel。此外开启-http_verbose后也会输出证书信息,便于排查 SSL 问题。


十六、小结

通过本文你可以看到,brpc 的 http/h2 客户端具备一套完整、统一且贴近底层的设计:

  • 协议统一:http/1.1 与 h2 共享同一编程接口,PROTOCOL_HTTP/PROTOCOL_H2一键切换;
  • IOBuf 贯穿始终:请求体与响应体均为butil::IOBuf,零拷贝友好,IOBufBuilder简化大量对象打印;
  • 灵活解耦Channel::Init决定连接目标,uri()决定请求资源,天然支持命名服务与 proxy 场景;
  • 流式能力ProgressiveReader支持在 header 读完即结束 RPC,可持续消费超长/无限长 body,适合直播流等场景;
  • 可观测与容错-http_verbose辅助调试,EHTTP+-use_http_error_code提供两级错误定位能力。

如需在真实环境中验证,可参考 example/http_c++/http_client.cpp 配合 example/http_c++/http_server.cpp 运行完整示例;服务端编程对应文档见 docs/cn/http_service.md,并行发起多个 http 请求的场景可参考 docs/cn/parallel_http.md。

【免费下载链接】brpcbrpc is an Industrial-grade RPC framework using C++ Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Recommendation etc. "brpc" means "better RPC".项目地址: https://gitcode.com/GitHub_Trending/brpc/brpc

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询