访问 http:h2
访问 http:h2
学习如何访问 Http2。
示例
关于 h2
brpc 将 HTTP/2 协议统一命名为“h2”,无论是否加密。不过,未使用 SSL 的 HTTP/2 连接在 /connections 页面上会以官方名称“h2c”显示,而使用 SSL 的连接则显示为“h2”。
brpc 中 http 与 h2 的 API 基本相同。除非特别说明,提到的 http 特性对 h2 同样适用。
创建 Channel
为了使用 brpc::Channel 访问 http/h2 服务,必须将 ChannelOptions.protocol 设置为 PROTOCOL_HTTP 或 PROTOCOL_H2。
协议设置好之后,Channel::Init 的第一个参数可以是任意合法的 URL。注意:Init() 只会使用 URL 中的 host 和端口,其余部分会被丢弃。允许传入完整 URL 只是为了让用户省去额外的解析代码。
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;
}http/h2 通道同样支持 BNS 地址或其他命名服务。
GET
brpc::Controller cntl;
cntl.http_request().uri() = "www.baidu.com/index.html"; // Request URL
channel.CallMethod(NULL, &cntl, NULL, NULL, NULL/*done*/);http/h2 与 protobuf 关系不大,因此 CallMethod 的所有参数除 Controller 和 done 外均为 NULL。使用非空的 done 发起异步 RPC。
cntl.response_attachment() 是 http/h2 响应的 body,类型为 butil::IOBuf。IOBuf 可以通过 to_string() 转换为 std::string,这需要分配内存并复制全部数据。如果性能很重要,代码应该考虑直接支持 IOBuf,而不是要求连续的内存。
POST
默认的 HTTP 方法是 GET,可以改为 POST 或 其他 http 方法。需要 POST 的数据应放入 request_attachment(),其类型为 butil::IOBuf,可以直接追加 std :: string 或 char *。
brpc::Controller cntl;
cntl.http_request().uri() = "..."; // Request URL
cntl.http_request().set_method(brpc::HTTP_METHOD_POST);
cntl.request_attachment().append("{\"message\":\"hello world!\"}");
channel.CallMethod(NULL, &cntl, NULL, NULL, NULL/*done*/);如果构建 body 需要大量打印,考虑使用 butil::IOBufBuilder,它与 std::ostringstream 具有相同的接口,当需要打印大量对象时,可能比 C 风格的 printf 更简单、更高效。
brpc::Controller cntl;
cntl.http_request().uri() = "..."; // Request 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(NULL, &cntl, NULL, NULL, NULL/*done*/);修改 HTTP 版本
brpc 默认按 http/1.1 工作。
与 http/1.1 相比,http/1.0 缺少长连接(KeepAlive)支持。若要让 brpc 客户端与某些遗留的 http 服务器通信,客户端可以配置如下:
cntl.http_request().set_version(1, 0);为 h2 设置 http 版本并不生效,但客户端接收到的 h2 响应以及服务端接收到的 h2 请求中的版本都会被设置为 (2, 0)。
brpc 服务端会自动识别 http 版本并作出相应响应,无需用户干预。
URL
URL 的一般形式:
// 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如上文示例所示,Channel.Init() 和 cntl.http_request().uri() 都需要 URL。为什么还需要额外设置 uri(),而不是直接用 URL 来 Init() 呢?
在简单场景下,这些设置确实是重复的。但在更复杂的场景中,它们是不同的:
- 通过 NamingService 访问多个服务器(例如 BNS),此时
Channel::Init接收对 NamingService 有意义的名称(例如 BNS 中的节点名),而uri()被赋值为 URL。 - 通过 http/h2 代理访问服务器,此时
Channel::Init接收代理服务器的地址,而uri()仍然被赋值为 URL。
如果用户已经设置了 Host 头(不区分大小写),框架不做任何修改。
如果用户未设置 Host 头且 URL 中包含主机名,例如 http://www.foo.com/path,则 http 请求中会包含 “Host: www.foo.com”。
如果用户未设置 host 头且 URL 中也不包含主机名,例如 “/index.html?name=value”,但由 channel 初始化的地址中包含域名,则框架会用目标服务器的域名设置 Host 头。如果该地址是 “http://www.foo.com”,则此 http 服务器应看到 Host: www.foo.com;如果该地址是 “http://www.foo.com:8989”,则此 http 服务器应看到 Host: www.foo.com:8989。
如果用户未设置 host 头且 URL 中也不包含主机名,例如 “/index.html?name=value”,并且由 channel 初始化的地址中不包含域名,则框架会用目标服务器的 IP 和端口设置 Host 头。位于 10.46.188.39:8989 的 http 服务器应看到 Host: 10.46.188.39:8989。
在 h2 中,该头的名称为 “:authority”。
常见用法
以 http 请求为例(http 响应类似),常见操作列举如下:
访问名为 Foo 的 HTTP 头
const std::string* value = cntl->http_request().GetHeader("Foo"); // NULL when not exist设置名为 Foo 的 HTTP 头部
cntl->http_request().SetHeader("Foo", "value");访问名为 Foo 的查询
const std::string* value = cntl->http_request().uri().GetQuery("Foo"); // NULL when not exist设置一个名为 Foo 的查询
cntl->http_request().uri().SetQuery("Foo", "value");设置 HTTP 方法
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");获取 HTTP body
butil::IOBuf& buf = cntl->request_attachment();
std::string str = cntl->request_attachment().to_string(); // trigger copy underlying设置 HTTP body
cntl->request_attachment().append("....");
butil::IOBufBuilder os; os << "....";
os.move_to(cntl->request_attachment());HTTP 头部说明:
- 根据 rfc2616,头部的 field_name 不区分大小写。brpc 支持不区分大小写的字段名,并且在打印时保留用户设置的原始大小写。
- 如果多个头部具有相同的字段名,根据 rfc2616,其值应当用逗号(,)合并并分隔。这类值的具体用法由用户自行判断。
- 查询串(Query)以 “&” 分隔,查询串中的键与值以 “=” 分隔。值可以省略。例如,
key1=value1&key2&key3=value3是一个合法的查询串,其中key2对应的值为空字符串。
调试 HTTP 消息
打开 -http_verbose 后,框架会打印每个 HTTP 请求与响应。请注意,该选项仅应于测试或调试环境,不应在在线服务中使用。
HTTP 错误
当服务端返回非 2xx 的 HTTP 状态码时,该 HTTP RPC 被视为失败,客户端侧的 cntl->ErrorCode() 被置为 EHTTP,用户可查看 cntl-> http_response().status_code() 以获取更具体的 HTTP 错误信息。此外,服务端可以将描述错误的 HTML 或 JSON 放入 cntl->response_attachment() 中,作为 HTTP body 返回给客户端。
压缩请求体
Controller::set_request_compress_type(brpc::COMPRESS_TYPE_GZIP) 会使框架尝试对 HTTP body 进行 gzip 压缩。“尝试”意味着压缩不一定真正发生,因为:
- body 的大小小于 -http_body_compress_threshold 指定的字节数,默认为 512 字节。原因是 gzip 并不是一种非常快的压缩算法,当 body 较小时,压缩引入的延迟甚至可能超过更快传输所节省的时间。
解压响应体
出于通用性考虑,brpc 不会自动解压响应的 body。解压代码并不复杂,用户可以自行完成。代码如下:
#include <brpc/policy/gzip_compress.h>
...
const std::string* encoding = cntl->http_response().GetHeader("Content-Encoding");
if (encoding != NULL && *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);
}
// Now cntl->response_attachment() contains the decompressed data渐进式下载
http 客户端通常要等到 http body 完全下载完成才结束 RPC。在此过程中,http body 会一直存放在内存中。如果 body 非常大或者无限大(例如用于直播的 FLV 文件),内存就会持续增长,直到 RPC 超时。这样的 http 客户端不适合下载超大文件。
brpc 客户端支持在读取完整 body 之前就完成 RPC,让用户可以在 RPC 之后渐进式地读取 http body。请注意,这个特性并不意味着"支持 http chunked 模式",实际上 brpc 中的 http 实现从一开始就支持 chunked 模式。真正的问题在于如何让用户处理非常大或无限大的 http body,而这与 chunked 模式无关。
使用方法:
实现下面的 ProgressiveReader:
#include <brpc/progressive_reader.h> ... class ProgressiveReader { public: // Called when one part was read. // Error returned is treated as *permenant* 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。实现前请仔细阅读注释。
2. 在 RPC 之前设置 cntl.response_will_be_read_progressively();,使 brpc 在读完所有 header 之后立即结束 RPC。
3. 在 RPC 之后调用 cntl.ReadProgressiveAttachmentBy(new MyProgressiveReader);。MyProgressiveReader 是用户实现的 ProgressiveReader 的实例。用户可以在 OnEndOfMessage 内部删除该对象。
渐进式上传
目前 POST 数据必须在发起 http 调用之前就完整准备好,因此 brpc 的 http 客户端仍然不适合上传非常大的请求体。
访问带认证的服务端
根据服务端的认证方式生成 auth_data,并将其设置到 Authorization header 中。如果你使用 curl,可添加选项 -H "Authorization : <auth_data>"。
发送 https 请求
https 是 "http over SSL" 的缩写,SSL 并非 http 专有,而是适用于所有协议。开启客户端 SSL 的通用方法见此处。brpc 会自动为以 https:// 开头的 URI 启用 SSL,以使使用更加方便。
最后修改于 2022 年 5 月 17 日:更新 brpc 用户页面(devlive-community/knowforge#71)(a31ce10d3)](https://github.com/apache/brpc-website/commit/a31ce10d3d732f29e56dd2c8c8a0d07c3e633209)
评论
登录后参与评论
KnowForge