[我的blog原地址:brpc一些笔记 -- 从http看协议实现,后续会持续努力同步更新到知乎~ ]
这篇是一篇单纯的读文档+源码的笔记,从brpc中的http看协议如何使用、各协议的关系是什么。本来只是工作上扩展思考的一些调研,不需要单独写个blog的,但是代码比较多,就还是整理过来。本文不足点在于不涉及基本知识,但是好处是结合了我同为rpc框架开发者角度的一些架构思考。所以如果看到这个标题觉得你也想了解,那么这篇文章你也可以看一看,如果看完标题觉得不知道为什么我要梳理这些,那请忽略~
1. http协议为什么特殊
brpc里同一个server支持多种协议,为了让协议们共存,如何收包是要做区分的,比如协议头如果可以同类识别,那么同类共享连接就很方便了。brpc给了三种类型的协议:baidu_std之类的(头部固定4个字节表示这种类型)
http(有复杂的语法,但没有特殊标示,只能解析一下才能知道是不是http)
协议中间才能包含特殊字符去解的,更麻烦。
对于第一类协议,protobuf的文件中自行定义Request和Response;第三种协议,brpc为你写好了协议(以redis和memcahce,为了兼容pb的rpc调用,这些协议都是从google::protobuf::Message派生的,这个很好理解);而http比较特殊,他是用不到pb消息的,为了走pb的rpc调用,会定义空的HttpRequest和HttpResponse。
message HttpRequest {};
message HttpResponse {};
那么框架如何知道特殊处理http协议呢?
目前个人理解是因为Controller层和Channel层都对PROTOCOL_HTTP进行了特判:
比如作为任何请求都要用到的Controller里留了HttpHeader& http_request()和HttpHeader& http_response()的mutable的接口,这是http协议独有的,而controller这一层需要看到的具体协议也只有http。
Channel层特判目前是为了ssl。
那么框架为什么需要单独把http拿出来特判呢?我认为原因之一是brpc实现了收完header先回复再继续收包的操作,这层目前只支持了http。后面第四点会展开说。
下面先梳理下框架从设置http到内部处理的流程。
2. 全局对协议的注册管理
1.使用的时候会这样写到options里;如果是baidu_std,不需要写,channel里默认就是baidu_std。
brpc::ChannelOptions options;
options.protocol = brpc::PROTOCOL_HTTP;
2.全局静态管理在global.cpp, GlobalInitializeOrDieImpl,里边包括ssl的init以及各protocol parser的各种回调的注册,比如:
Protocol baidu_protocol = { ParseRpcMessage,
// 默认协议都走这批回调 SerializeRequestDefault, PackRpcRequest,
ProcessRpcRequest, ProcessRpcResponse,
VerifyRpcRequest, NULL, NULL,
CONNECTION_TYPE_ALL, "baidu_std" };
if (RegisterProtocol(PROTOCOL_BAIDU_STD, baidu_protocol) != 0) {
exit(1);
}
Protocol http_protocol = { ParseHttpMessage,
// http协议自己的回调 SerializeHttpRequest, PackHttpRequest,
ProcessHttpRequest, ProcessHttpResponse,
VerifyHttpRequest, ParseHttpServerAddress,
GetHttpMethodName,
CONNECTION_TYPE_POOLED_AND_SHORT,
"http" };
if (RegisterProtocol(PROTOCOL_HTTP, http_protocol) != 0) {
exit(1);
}
3.这些函数的定义在protocol.h,框架会根据你注册的函数为你typedef你的Protocol::your_function,比如序列化函数,真身是这样的:
typedef void (*SerializeRequest)(
butil::IOBuf* request_buf,
Controller* cntl,
const google::protobuf::Message* request);
SerializeRequest serialize_request;
4.在channel中保存了它要用的函数,CallMethod的时候调用。这就是一般rpc做法了。除了序列化,其他函数也类似,这里不细说。
// channel中存了对应的函数 Protocol::SerializeRequest _serialize_request;
// 在Channel的CallMethod()中进行了调用 _serialize_request(&cntl->_request_buf, cntl, request);
3. 基本协议需要实现的回调函数
这里看具体http维度要做的事情,但是不会细说协议解析(主要我也不懂http协议解析)。
1.先看看policy/http_rpc_protocol.cpp中实现的几个:
// 把消息从source上拿下来// 返回的Result可能是错误也可能是HttpContext,由parser_result.h里协助管理// 这里的返回值会给process_request()/process_response()用ParseResult ParseHttpMessage(butil::IOBuf *source, Socket *socket,
bool read_eof, const void *arg)
{
...
rc = http_imsg->ParseFromArray(NULL, 0);
...
if (http_imsg->Completed())
...
}
2.brpc的压缩与我们是类似的,但是他无论是什么序列化器都需要一个pb作为承接载体。这些都可以写到ChannelOptions.protocol里,比如http:json h2:json http:proto h2:proto h2:grpc h2:grpc+json,前半段才是server会收到的,后半段只影响自己怎么发出,比如如果pb request不为空,那么就会把这个pb request填到json里发出。这些事情都在以下的函数里做
// 一般来说,这个函数是把第一个参数的iobuf序列化进pbreq。void SerializeHttpRequest(butil::IOBuf* /*not used*/,
Controller* cntl,
const google::protobuf::Message* pbreq)
{
HttpHeader& hreq = cntl->http_request();
...
// 但是http不会用到第一个参数,仔细看是因为它要收的东西是在controller上的 butil::IOBufAsZeroCopyOutputStream wrapper(&cntl->response_attachment());
}
3.序列化完就要打包了。以上序列化只序列化一次,但是pack可能会调用很多次(比如retry了之类的)
// 一般来说,是要把底下的const butil::IOBuf&那个参数打包进buf里。void PackHttpRequest(butil::IOBuf* buf,
SocketMessage**,
uint64_t correlation_id,
const google::protobuf::MethodDescriptor*,
Controller* cntl,
const butil::IOBuf& /*unused*/,
const Authenticator* auth)
{
HttpHeader* header = &cntl->http_request();
...
// 这里也因为header和body都在cntl里而没有用到此回调接口的第6个参数 MakeRawHttpRequest(buf, header, cntl->remote_side(),
&cntl->request_attachment());
}
4.特别说一下,ProcessHttpRequest需要在http_header.h中声明,还没太明白是为什么。先看看这对函数:
server端的函数大概300行,仅摘取些我感兴趣的。
void ProcessHttpRequest(InputMessageBase *msg)
{
DestroyingPtr imsg_guard(static_cast(msg));
// cntl表示的会话是被动生成的,这个生成时机跟我们一样 // 然后通过以下代码就可以恢复出request了 Controller* cntl = new (std::nothrow) Controller;
ControllerPrivateAccessor accessor(cntl);
butil::IOBuf& req_body = imsg_guard->body();
// 以下是一些rpc做法参考 google::protobuf::Service* svc = sp->service;
const google::protobuf::MethodDescriptor* method = sp->method;
accessor.set_method(method);
google::protobuf::Message* req = svc->GetRequestPrototype(method).New();
resp_sender.own_request(req);
google::protobuf::Message* res = svc->GetResponsePrototype(method).New();
resp_sender.own_response(res);
...
google::protobuf::Closure* done = new HttpResponseSenderAsDone(&resp_sender);
...
svc->CallMethod(method, cntl, req, res, done);
}
5.client端处理response的方式,可以跟以上对比下区别:
void ProcessHttpResponse(InputMessageBase* msg)
{
DestroyingPtr imsg_guard(static_cast(msg));
Socket* socket = imsg_guard->socket();
...
// 如果是http2可以这样的到stream ctx H2StreamContext* h2_sctx = static_cast(msg);
cid_value = socket->correlation_id(); // 或者h2_csctx-> ...
// 通过bthreadid拿到cntl,这个cntl是用户发送请求是new出来的 const bthread_id_t cid = { cid_value };
const int rc = bthread_id_lock(cid, (void**)&cntl);
ControllerPrivateAccessor accessor(cntl);
...
HttpHeader* res_header = &cntl->http_response();
res_header->Swap(imsg_guard->header());
butil::IOBuf& res_body = imsg_guard->body();
...
// 经过一堆错误处理后,解析body。是pb ParsePbFromIOBuf(cntl->response(), res_body);
// 或者是json json2pb::JsonToProtoMessage(&wrapper, cntl->response(), options, &err)
// 释放资源,应该还包括unlock上述的cid(correlation_id) imsg_guard.reset();
accessor.OnResponse(cid, saved_error);
}
以上用到的一些http协议相关的函数,在details/http_message.h,这个文件真正定义了HttpMessage,这里不细作分析。
4. client持续下载和server持续发送
brpc对http有特殊的一层值得说一下,就是支持client读完http header之后就结束rpc,而之后再持续接收body的数据。具体实现可以看brpc/progressive_reader.h,需要用户去实现OnReadOnePart(void *data, len)和OnEndOfMessage(&status),顾名思义分别是每次读到数据后要做的事情和数据结束/连接断开时要做的事情。
这是刚才第一part里讲的,controller中有一层progressive层,当前仅支持http。
使用方式是发起调用前要设置cntl.response_will_be_read_progressively();,表示发完header之后本次调用就结束了。调用结束后需要调用cntl.ReadProgressiveAttachmentBy(new MyProgressiveReader);,由这个Reader实例去做后续的事情,用户实现的OnEndOfMessage里可以删掉这个Reader实例。
client不支持持续上传。
server端相反,支持持续发送,但是不支持持续接收。
参考brpc/progressive_attachment.h里的实现,也是通过调用controller来实现:butil::intrusive_ptr<:progressiveattachment> pa(cntl->CreateProgressiveAttachment());,内部也是仅支持http。
然后会new ProgressiveAttachment(httpsock, http_request().before_http_1_1());,在调用到这个attechment的Write()时,如果还没有到server的done,那么数据会缓存;如果已经调用过了server的done,则数据被持续发送。
5. 总结和问题
在今天内容中brpc与Sogou RPC目前的实现上的一些对比和我的一些个人思考:我们对于消息发送,也是分批做,但是这是底层连接层做的事情,刚才说的progressive则是具体协议层所支持的功能,这一层次的提取值得参考。
为了通用性,brpc的server对于request和client对于response都是不会自动解压的,让用户自己来做。而我们目前压缩和解压对用户都是透明的。
uri也是http特有的。
这些协议的回调函数中,SerializeRequest只调用一次,而PackRequest是在每次网络发送时都会调用,这一点我们也是一样的。
brpc的协议共用连接的实现是:长连接总是记下来上次用了什么协议,下次还用这个来解基本可以支持大部分场景。而我们目前底层调度框架对于不同协议共享连接做得更好,是因为连接层面不需要知道协议,实现协议的人实现message层面的append()来告诉框架收完没有,框架收完了调用CommRequest层面的handle()(即调用异步任务完成所要做的事情),这里对于网络任务来说,就是调用协议设置的callback,也是由实现协议的人提供的。这样想想我们框架在底层设计上层次确实更好一些。
brpc是client与server框架无关的,即client制定协议之后可以发给任何起这个协议的server,可是目前Sogou RPC对于自定义协议还是必须走srpc,在协议层拆分我还有很多要思考和改进的地方。不过由于使用Sogou RPC的人也可以同时使用Workflow,所以不走rpc而直接使用workflow的create_http_task()、create_redis_task()、create_mysql_task()也可以直接访问没有部署Sogou RPC或Workflow的对应协议的server~
brpc对于协议版本升级的事情是自动做的,比如http协议任何一方收到为http2就自动升级。感觉在协议层我们的rpc要做的工作还有很多。