ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

brpc HTTP/h2 客户端编程指南:从 Channel 创建到持续下载的完整实战 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/2brpc 统称 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的对应关系。一、关于 h2brpc 对 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_HTTP或PROTOCOL_H2brpc::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然后调用CallMethodbrpc::Controller cntl; cntl.http_request().uri() www.baidu.com/index.html; // 设置为待访问的URL channel.CallMethod(nullptr, cntl, nullptr, nullptr, nullptr/*done*/);HTTP/h2 和 protobuf 关系不大因此除Controller和done外CallMethod的其他参数均为nullptr若要异步操作把最后一个参数传入done回调即可。响应体通过cntl.response_attachment()获取类型为butil::IOBuf。IOBuf可通过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_DELETE、HTTP_METHOD_HEAD、HTTP_METHOD_PUT、HTTP_METHOD_PATCH、HTTP_METHOD_OPTIONS等共 27 种标准及扩展方法如 WebDAV 的PROPFIND、LOCK、MOVE。待 POST 的数据应置入request_attachment()butil::IOBuf可以直接appendstd::string或char*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.0cntl.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:passwordexample.com:8042/over/there/index.dtb?typeanimalnamenarwhal#nose // \_/ \_______________/ \_________/ \__/ \___/ \_/ \______________________/ \__/ // | | | | | | | | // | userinfo host port | | query fragment // | \________________________________/\_____________|____|/ \__/ \__/ // scheme | | | | | | // authority | | | | | // path | | interpretable as keys // | | // \_______________________________________________|____|/ \____/ \_____/ // | | | | | // hierarchical part | | interpretable as values // | | // interpretable as filename | // | // | // interpretable as extension6.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?namevalue通过 http/h2 proxy 访问目标 server此时Channel.Init传入的是 proxy server 的地址但uri()填入的是目标 server 的 URL。换言之Channel.Init决定连到哪台机器uri()决定请求哪个资源两者解耦后上述高级场景才成为可能。6.2 Host 字段的推导规则Host 字段h2 中对应:authority的填充遵循以下优先级规则用户显式设置了 host 字段大小写不敏感框架不会修改原样使用用户未设置且 URL 中包含 host如http://www.foo.com/pathhttp request 中会包含Host: www.foo.com用户未设置URL 不包含 host如/index.html?namevalue但 Channel 初始化的地址 scheme 为 http(s) 且包含域名框架以该域名作为 Host。例如地址为http://www.foo.comserver 将看到Host: www.foo.com地址为http://www.foo.com:8989则看到Host: www.foo.com:8989用户未设置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 的 headerconst std::string* value cntl-http_request().GetHeader(Foo); //不存在为nullptr设置名为 Foo 的 headercntl-http_request().SetHeader(Foo, value);访问名为 Foo 的 queryconst std::string* value cntl-http_request().uri().GetQuery(Foo); // 不存在为nullptr设置名为 Foo 的 querycntl-http_request().uri().SetQuery(Foo, value);设置 HTTP Methodcntl-http_request().set_method(brpc::HTTP_METHOD_POST);设置 urlcntl-http_request().uri() http://www.baidu.com;设置 content-typecntl-http_request().set_content_type(text/plain);访问 bodybutil::IOBuf buf cntl-request_attachment(); std::string str cntl-request_attachment().to_string(); // 有拷贝设置 bodycntl-request_attachment().append(....); butil::IOBufBuilder os; os ....; os.move_to(cntl-request_attachment());关于 header 与 query 的三点注意事项根据 RFC 2616http header 的 field_name不区分大小写。brpc 支持大小写不敏感访问同时会在打印时保持用户传入的大小写若 http header 中出现了相同的 field_name根据 RFC 2616多个 value 应合并到一起、用逗号(,)分隔此合并行为需要用户自行处理query 之间用分隔key 和 value 之间用分隔value 可以省略。比如key1value1key2key3value3中key2是合理的 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、500server 端同时可以把代表错误的 html 或 json 置入cntl-response_attachment()作为 http body 传递回来客户端可以读取该 body 获取更详细的错误信息。特殊场景如果 Server 也是 brpc 框架实现的服务client 端希望在 http/h2 失败时获取 brpc Server 返回的真实ErrorCode而不是统一设置的EHTTP则需要设置 GFlag-use_http_error_codetrue。从源码看该 flag 定义于 src/brpc/policy/http_rpc_protocol.cpp其作用是把 brpc 的 error code 写入 http response 的x-bd-error-codeheader 中见 src/brpc/policy/http_rpc_protocol.cppclient 侧据此还原真实错误码。十、压缩 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 its 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输入与输出与上面用法一一对应。该头文件同时提供GzipCompress、ZlibCompress、ZlibDecompress等配套函数可满足多种压缩算法的需求。十二、持续下载处理超长/无限长 body12.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 theres 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_castconst 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.cppif (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::SetBodyReader→ProgressiveReader::OnReadOnePart在 src/brpc/progressive_reader.h 的注释中有完整描述已经读到的 body 会立即喂给 reader 并被记住新到达的数据会持续回调OnReadOnePart直至所有 body 读完或 socket 被销毁。十三、持续上传的现状限制目前POST 的数据必须是完整生成好的brpc 不适合 POST 超长的 body。也就是说持续上传类似分块流式上传目前并不被支持如果需要上传超大 body需要考虑分片多次请求或使用其他传输机制。十四、访问带认证的 Server根据 Server 的认证方式生成对应的auth_data并设置为 http headerAuthorization的值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),仅供参考
返回列表