brpc访问MySQL_brpc一些笔记 -- 从http看协议实现

[我的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要做的工作还有很多。

你可能感兴趣的:(brpc访问MySQL)